A Work in Progress

developer's blog by Wei
lots of work in progress notes
TimelineCredits

Starting CLI Development

March 30, 2020

Step by step initialize a CLI app

The very basics

A very bare bone node CLI app may look like this, where bin/index.js contains an executable and the package.json contains the necessary manifest to make it work:

.
├── README.md
├── bin
│   └── index.js
└── package.json

The bin file should contain a "shebang", #!/usr/bin/env node, like this:

#!/usr/bin/env node

console.log('hello, world!);

To link the command associated with bin/index.js, include these fields in package.json:

"name": "@box/cli",
"main": "bin/index.js",
"bin": {
  "box": "./bin/index.js"
}

This will link command line calls of box to the associated bin/index.js

Then, for bash to be able to execute it, the bin/index.js needs write access:

chmod +x ./bin/index.js

Now, run yarn link:

yarn link v1.21.1
success Registered "@box/cli".
info You can now run `yarn link "@box/cli"` in the projects where you want to use this package and it will be used instead.
✨  Done in 0.05s.

Using commanderjs

Commanderjs is a godlike package to create cli apps. To a near minimum an app may look like this:

#!/usr/bin/env node
const program = require('commander');

program
  .version('0.0.1')
  .option('-h, --hash [hash]', 'download a box by hash')
  .parse(process.argv);

console.log(program.hash);

Structuring the commands

Say the box cli app will implement an install and a list sub-commands, it can look like this:

#!/usr/bin/env node

const program = require('commander');

program.version('0.0.1');

// command "install" 

program
  .command('install <hash>')
  .description('download a box by hash')
  .action(hash => {
    console.log('installing %s', hash);
  });

// command "list"

program
  .command('list')
  .description('list installed boxes')
  .action(() => {
    console.log('here are the installed boxes');
  });


program.parse(process.argv);

now running box install XXX will execute the install subcommand with a hash input, and running box list will execute the list subcommand.

Bundling with RollupJS

We may prefer to have some organization of our code, i.e., separate them into a few files in src/ and use a package bundler to automatically build them into a single bin/index.js node executable, like this:

.
├── README.md
├── bin
│   └── index.js
├── package.json
└── src
    ├── api.js
    └── index.js

We can use a bundler, such as rollup.js. Here is a very minimalistic config:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'bin/index.js',
    format: 'cjs',
  },
};

Automatically add shebang

The bin entry point should have a shebang, but including the shebang in src/index.js would not pass rollup compiling. there are a number of things we can do, although the one that makes most sence is to not include shebang in src/index, but add the line during rollup compilation. we can do this by adding a banner in rollup output:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'bin/index.js',
    format: 'cjs',
+   banner: '#!/usr/bin/env node\n', // add shebang    
  },
};

chmod for rollup output

saw rich harris did this

{
  "scripts": {
    "build": "rollup -c rollup.config.js && chmod a+x bin/index.js"
  }
}

this will append a chmod after every build

Watch file changes on src/

{
  "scripts": {
    "build": "rollup -c rollup.config.js && chmod a+x bin/index.js",
    "dev": "rollup -w -c rollup.config.js"
  }
}

this completes the rollup setup for now

Relevant links

Tutorials