"Josh Beckman", "url"=>"https://www.joshbeckman.org", "email"=>"josh@joshbeckman.org"}" />
# Build Your Own Node.js CLI Package
![GIF](https://media.giphy.com/media/yR4xZagT71AAM/giphy.gif)
## Coverage
* Node.js CLI first principles
* Frameworks
* Configuration
* Distribution
### Node.js Scripting
Running an event loop to completion or interruption
### Interactive / Session
Great for destructive or constructive actions
`command (begin session)`
### Single Command
Great for compositional, single-purpose scripts
`command [arguments]`
```bash
$ ls -l -a
total 24
drwxr-xr-x 6 Josh staff 204 Jul 26 2017 .
drwxr-xr-x 6 Josh staff 204 Aug 10 2017 ..
drwxr-xr-x 15 Josh staff 510 Apr 15 17:26 .git
-rw-r--r-- 1 Josh staff 1061 Jul 26 2017 LICENSE
-rw-r--r-- 1 Josh staff 1030 Jul 26 2017 README.md
-rwxr-xr-x 1 Josh staff 1190 Jul 26 2017 heroku_multi
```
### Multiple Commands
More like complete applications
`prefix command [arguments]`
```bash
$ git log --pretty=format:%s
new:usr: Add initial working version
Initial commit
$ git status
On branch master
Your branch is up to date with 'origin/master'.
```
### What makes Node.js a good candidate?
* Widely available
* Event Loop can be a benefit
* Forked processes can be a benefit
* Distribution is wickedly simple/fast
## Communicating to Node.js Scripts
### What's in a `process`?
Your script always operates in context
```js
process {
title: 'node',
version: 'v8.1.2',
moduleLoadList:
[ 'Binding contextify',
'Binding natives',
'Binding config',
'NativeModule events',
'Binding async_wrap',
'Binding icu',
'NativeModule util',
'Binding uv',
'NativeModule buffer',
'Binding buffer',
'Binding util',
'NativeModule internal/util',
'NativeModule internal/errors',
'Binding constants',
'NativeModule internal/buffer',
'NativeModule timers',
'Binding timer_wrap',
'NativeModule internal/linkedlist',
'NativeModule async_hooks',
'NativeModule assert',
'NativeModule internal/process',
'NativeModule internal/process/warning',
'NativeModule internal/process/next_tick',
'NativeModule internal/process/promises',
'NativeModule internal/process/stdio',
'NativeModule internal/url',
'NativeModule internal/querystring',
'NativeModule querystring',
'Binding url',
'NativeModule path',
'NativeModule module',
'NativeModule internal/module',
'NativeModule vm',
'NativeModule fs',
'Binding fs',
'NativeModule stream',
'NativeModule internal/streams/legacy',
'NativeModule _stream_readable',
'NativeModule internal/streams/BufferList',
'NativeModule internal/streams/destroy',
'NativeModule _stream_writable',
'NativeModule _stream_duplex',
'NativeModule _stream_transform',
'NativeModule _stream_passthrough',
'Binding fs_event_wrap',
'NativeModule internal/fs',
'NativeModule console',
'Binding tty_wrap',
'NativeModule tty',
'NativeModule net',
'NativeModule internal/net',
'Binding cares_wrap',
'Binding tcp_wrap',
'Binding pipe_wrap',
'Binding stream_wrap',
'Binding signal_wrap',
'Binding inspector' ],
versions:
{ http_parser: '2.7.0',
node: '8.1.2',
v8: '5.8.283.41',
uv: '1.12.0',
zlib: '1.2.11',
ares: '1.10.1-DEV',
modules: '57',
openssl: '1.0.2l',
icu: '59.1',
unicode: '9.0',
cldr: '31.0.1',
tz: '2017b' },
arch: 'x64',
platform: 'darwin',
release:
{ name: 'node',
sourceUrl: 'https://nodejs.org/download/release/v8.1.2/node-v8.1.2.tar.gz',
headersUrl: 'https://nodejs.org/download/release/v8.1.2/node-v8.1.2-headers.tar.gz' },
argv: [ '/usr/local/bin/node', '/Users/Joshua/src/foo.js' ],
execArgv: [],
env:
{ TERM_PROGRAM: 'Apple_Terminal',
ANDROID_HOME: '/Users/Joshua/Library/Android/sdk',
TERM: 'xterm-256color',
SHELL: '/bin/bash',
CLICOLOR: '1',
TMPDIR: '/var/folders/0c/1xh24_cd64z0qgdyhrrhqmzm0000gp/T/',
Apple_PubSub_Socket_Render: '/private/tmp/com.apple.launchd.xzSg211msb/Render',
TERM_PROGRAM_VERSION: '404',
USER: 'Joshua',
SSH_AUTH_SOCK: '/private/tmp/com.apple.launchd.I8BfXr2o6k/Listeners',
PATH: '/Users/Joshua/.cargo/bin:~/Library/Android/sdk/tools:~/Library/Android/sdk/platform-tools:/Library/Frameworks/Python.framework/Versions/2.7/bin:/usr/local/bin:/usr/local/share/npm/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/opt/X11/bin:/usr/local/git/bin:/Users/Joshua/.rvm/bin:/Users/Joshua/projects/golang/bin:/Users/Joshua/bin',
PWD: '/Users/Joshua/src',
EDITOR: 'vim',
LANG: 'en_US.UTF-8',
XPC_FLAGS: '0x0',
XPC_SERVICE_NAME: '0',
HOME: '/Users/Joshua',
SHLVL: '1',
PYTHONPATH: '/usr/local/lib/python2.7/site-packages:',
LOGNAME: 'Joshua',
GOPATH: '/Users/Joshua/projects/golang',
PKG_CONFIG_PATH: '/opt/local/lib/pkgconfig:',
DISPLAY: '/private/tmp/com.apple.launchd.Vdwo7PHHuC/org.macosforge.xquartz:0',
SECURITYSESSIONID: '186a6',
OLDPWD: '/Users/Joshua/src/github.com/andjosh/andjosh.github.io',
_: '/usr/local/bin/node',
__CF_USER_TEXT_ENCODING: '0x1F6:0x0:0x0' },
pid: 99573,
features:
{ debug: false,
uv: true,
ipv6: true,
tls_npn: true,
tls_alpn: true,
tls_sni: true,
tls_ocsp: true,
tls: true },
_needImmediateCallback: false,
execPath: '/usr/local/bin/node',
debugPort: 9229,
_startProfilerIdleNotifier: [Function: _startProfilerIdleNotifier],
_stopProfilerIdleNotifier: [Function: _stopProfilerIdleNotifier],
_getActiveRequests: [Function: _getActiveRequests],
_getActiveHandles: [Function: _getActiveHandles],
reallyExit: [Function: reallyExit],
abort: [Function: abort],
chdir: [Function: chdir],
cwd: [Function: cwd],
umask: [Function: umask],
getuid: [Function: getuid],
geteuid: [Function: geteuid],
setuid: [Function: setuid],
seteuid: [Function: seteuid],
setgid: [Function: setgid],
setegid: [Function: setegid],
getgid: [Function: getgid],
getegid: [Function: getegid],
getgroups: [Function: getgroups],
setgroups: [Function: setgroups],
initgroups: [Function: initgroups],
_kill: [Function: _kill],
_debugProcess: [Function: _debugProcess],
_debugPause: [Function: _debugPause],
_debugEnd: [Function: _debugEnd],
hrtime: [Function: hrtime],
cpuUsage: [Function: cpuUsage],
dlopen: [Function: dlopen],
uptime: [Function: uptime],
memoryUsage: [Function: memoryUsage],
binding: [Function: binding],
_linkedBinding: [Function: _linkedBinding],
_setupDomainUse: [Function: _setupDomainUse],
_events:
{ warning: [Function],
newListener: [Function],
removeListener: [Function],
SIGWINCH: [Function] },
_rawDebug: [Function],
_eventsCount: 4,
domain: null,
_maxListeners: undefined,
_fatalException: [Function],
_exiting: false,
assert: [Function],
config:
{ target_defaults:
{ cflags: [],
default_configuration: 'Release',
defines: [],
include_dirs: [],
libraries: [] },
variables:
{ asan: 0,
coverage: false,
debug_devtools: 'node',
force_dynamic_crt: 0,
host_arch: 'x64',
icu_data_file: 'icudt59l.dat',
icu_data_in: '../../deps/icu-small/source/data/in/icudt59l.dat',
icu_endianness: 'l',
icu_gyp_path: 'tools/icu/icu-generic.gyp',
icu_locales: 'en,root',
icu_path: 'deps/icu-small',
icu_small: true,
icu_ver_major: '59',
llvm_version: 0,
node_byteorder: 'little',
node_enable_d8: false,
node_enable_v8_vtunejit: false,
node_install_npm: true,
node_module_version: 57,
node_no_browser_globals: false,
node_prefix: '/',
node_release_urlbase: 'https://nodejs.org/download/release/',
node_shared: false,
node_shared_cares: false,
node_shared_http_parser: false,
node_shared_libuv: false,
node_shared_openssl: false,
node_shared_zlib: false,
node_tag: '',
node_use_bundled_v8: true,
node_use_dtrace: true,
node_use_etw: false,
node_use_lttng: false,
node_use_openssl: true,
node_use_perfctr: false,
node_use_v8_platform: true,
node_without_node_options: false,
openssl_fips: '',
openssl_no_asm: 0,
shlib_suffix: '57.dylib',
target_arch: 'x64',
uv_parent_path: '/deps/uv/',
uv_use_dtrace: true,
v8_enable_gdbjit: 0,
v8_enable_i18n_support: 1,
v8_enable_inspector: 1,
v8_no_strict_aliasing: 1,
v8_optimized_debug: 0,
v8_promise_internal_field_count: 1,
v8_random_seed: 0,
v8_use_snapshot: true,
want_separate_host_toolset: 0,
want_separate_host_toolset_mkpeephole: 0,
xcode_version: '7.0' } },
emitWarning: [Function],
nextTick: [Function: nextTick],
_tickCallback: [Function: _tickCallback],
_tickDomainCallback: [Function: _tickDomainCallback],
stdout: [Getter],
stderr: [Getter],
stdin: [Getter],
openStdin: [Function],
exit: [Function],
kill: [Function],
argv0: 'node',
mainModule:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/Joshua/src/foo.js',
loaded: false,
children: [],
paths:
[ '/Users/Joshua/src/node_modules',
'/Users/Joshua/node_modules',
'/Users/node_modules',
'/node_modules' ] } }
```
### Executing with environment variables
Pervasive, global, shared
Like cues, rather than directives
### Executing with flags/argv
Specific, pedantic
Like directives, rather than cues
_Will exclude node-specific flags placed before the script name._
### Standard Input
Powerful in conjunction with others
Data, rather than directive
### Placement Matters
```bash
#
$ [env] node [node-flags] <script> [argv]
# [stdin]
#
```
### Interactive / Session
[flatiron/prompt](https://github.com/flatiron/prompt)
A beautiful command-line prompt for node.js
* Simple, familiar API
```js
var prompt = require('prompt');
prompt.start();
prompt.get(['username', 'email'], function (err, result) {
console.log('Command-line input received:');
console.log(' username: ' + result.username);
console.log(' email: ' + result.email);
});
```
```sh
$ node examples/simple-prompt.js
prompt: username: some-user
prompt: email: some-user@some-place.org
Command-line input received:
username: some-user
email: some-user@some-place.org
```
### Session & Args
[esatterwhite/node-seeli](https://github.com/esatterwhite/node-seeli)
Object orientated, event driven CLI module
* Handles both interactive and directive
* Heavy up-front configuration
* Allows for heavily automated parsing
* Emits events
```js
const os = require('os');
const cli = require('seeli');
cli.set({
exitOnError: true
, color: 'green'
, name: 'example'
});
const Hello = new cli.Command({
description:"displays a simple hello world command"
, name: 'hello'
, ui: 'dots'
, usage:[
`${cli.bold("Usage:")} ${cli.get('name')} hello --interactive`
, `${cli.bold("Usage:")} ${cli.get('name')} hello --name=john`
, `${cli.bold("Usage:")} ${cli.get('name')} hello --name=john --name=marry --name=paul -v screaming`
]
, flags:{
name:{
type:[ String, Array ]
, shorthand:'n'
, description:"The name of the person to say hello to"
, required:true
}
, volume:{
type:String
, choices:['normal', 'screaming']
, description:"Will yell at each person"
, default:'normal'
, shorthand:'v'
}
}
, onContent: (content) => {
console.log(content.join(os.EOL))
}
, run: async function( cmd, data ){
const out = [];
this.ui.start('processing names');
var names = Array.isArray( data.name ) ? data.name : [ data.name ];
for( var x = 0; x< names.length; x++ ){
this.ui.text = (`processing ${names[x]}`)
let value = "Hello, " + names[x];
out.push( data.volume === 'screaming' ? value.toUpperCase() : value );
}
this.ui.succeed('names processed successfully');
return out
}
});
cli.use('hello', Hello);
cli.run();
```
![Example seeli GIF](https://raw.githubusercontent.com/esatterwhite/node-seeli/master/assets/seeli.gif)
### Argument-Based
[tj/commander.js](https://github.com/tj/commander.js#readme)
Node.js command-line interfaces made easy
* No dependencies
* As simple as possible
* Lends itself to more complex CLIs
```js
var program = require('commander');
program
.option('--no-sauce', 'Remove sauce')
.option('--topping', 'The good parts', 'cheese')
.parse(process.argv);
console.log(`you ordered a ${program.topping} pizza`);
if (program.sauce)
console.log(' with sauce');
else
console.log(' without sauce');
```
```bash
$ node index.js --topping green
you ordered a green pizza
with sauce
$ node index.js --no-saouce
you ordered a cheese pizza
without sauce
```
### Arguments & Meta-CLI
[heroku/oclif](https://github.com/oclif/oclif)
Node.js Open CLI Framework. Built with 💜 by Heroku.
* Argument parsing
* Command structure, generation
* Testing helpers
* Plugins
* Hooks
* Optional Typescript
* Brand new
```js
const {Command, flags} = require('@oclif/command');
class OclifExampleSingleJsCommand extends Command {
async run() {
const {flags} = this.parse(OclifExampleSingleJsCommand);
const name = flags.name || 'world';
this.log(`hello ${name} from ./src/index.js`);
}
}
OclifExampleSingleJsCommand.flags = {
version: flags.version({char: 'v'}),
help: flags.help({char: 'h'}),
name: flags.string({char: 'n', description: 'name to print'}),
};
```
```bash
$ node index.js
hello world from ./src/index.js
$ node index.js --name foobar
hello foobar from ./src/index.js
```
## Configuration & Settings
### Avoid If Possible
CI build scripts probably don't need much
* Standard input
* Environment variables
* Arguments
### Persistent State
Daily interactive scripts can benefit from pre-configuration
* Authentication
* Preferences
* Aliases
### Config / Dot Files
* Default empty
* Loading
* Saving
* Upgrading / Migrating
* Be a good neighbor
### Where to Load?
* Root vs. local config files
```js
const path = require('path');
const os = require('os');
const pkg = require('../../package.json');
const rootConfig = path.resolve(os.homedir(), '.' + pkg.name, 'config.json');
const localConfig = path.resolve(process.cwd(), '.' + pkg.name + '.json');
```
### Config File Tools
[indexzero/nconf](https://github.com/indexzero/nconf)
Hierarchical node.js configuration with files, environment variables, command-line arguments, and atomic object merging.
## Packaging & Distribution
Make a package!
Decide if your package is a module or a script.
(or both)
### Registering a bin command
```js
// package.json
{
"name": "foobar",
//...
"bin": {
"foobar": "./src/bin"
}
// ...
```
* Symlinked into **prefix/bin** for global installs
* **./node_modules/.bin/** for local installs.
Make sure that your file(s) referenced in **bin** starts with **#!/usr/bin/env node**, otherwise the scripts are started without the node executable!
```js
#!/usr/bin/env node
const pkg = require('../../package.json');
```
### Naming things is hard
It's possible to have [zero or more](https://docs.npmjs.com/files/package.json#bin) bin commands
```js
"bin": "./bin/cmd.js",
// or
"bin": {
"one": "./bin/one.js",
"two": "./bin/two/",
}
```
### Remote Diagnostics
Allow for introspection
[visionmedia/debug](https://github.com/visionmedia/debug#readme)
A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers
```js
var a = require('debug')('worker:a')
, b = require('debug')('worker:b');
(function work() {
a('doing lots of uninteresting work');
setTimeout(work, Math.random() * 1000);
})();
(function workb() {
b('doing some work');
setTimeout(workb, Math.random() * 2000);
})();
```
![](https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png)
### Remote Logging
* Think carefully about whether to send remote logs
* Homebrew's Google Analytics tussle
### Communicating Updates
Be careful, as this can get annoying
```bash
$ nom version [major|minor|patch]
# can later be fetched from
# https://api.github.com/repos/<user>/<repo>/tags
```
[oclif/plugin-warn-if-update-available](https://github.com/oclif/plugin-warn-if-update-available)
### Automated interactions
Propagating CI events
* Generating builds
* Notifying of events
### Replicating UI
Sometimes you don't want to leave the editor/terminal
* Remote authoring
* Clicking buttons
* Consolidated monitoring
### Configure Existing Scripts
* What options are you setting with environment variables?
* Make those explicit
* What options are you editing on the fly?
* Make those explicit
## Questions & Thanks
[www.andjosh.com/presents](https://www.andjosh.com/presents)