├── 01-node-js-develop-and-publish-a-node-js-cli-from-scratch.md ├── 02-create-a-single-command-node-js-cli-with-oclif-typescript-and-yarn-workspaces.md ├── 03-parse-flags-and-args-in-node-js-clis-with-oclif-and-typescript.md ├── 04-convert-a-single-command-cli-into-a-multi-command-cli-with-oclif-and-typescript.md ├── 05-create-a-hybrid-single-multi-command-node-js-cli-with-oclif-and-typescript.md ├── 06-debug-node-js-clis-with-the-vs-code-debugger-and-the-debug-module.md ├── 07-test-node-js-clis-with-jest-and-oclif.md ├── 08-design-beautiful-intuitive-and-secure-node-js-cli-user-input-experiences-with-enquirer.md ├── 09-create-highly-configurable-node-js-clis-with-cosmiconfig-and-oclif.md ├── 10-scaffold-out-files-and-projects-from-templates-in-a-node-js-cli.md ├── 11-spawn-and-manage-child-processes-and-node-js-streams-with-execa.md ├── 12-prompt-users-to-update-your-node-js-clis-with-update-notifier.md ├── 13-store-state-on-filesystem-in-node-js-clis-with-conf.md ├── 14-create-node-js-cli-s-that-intelligently-adapt-to-usage-with-frecency.md ├── 15-polish-node-js-cli-output-with-chalk-and-ora.md └── README.md /01-node-js-develop-and-publish-a-node-js-cli-from-scratch.md: -------------------------------------------------------------------------------- 1 | # 01. Develop and Publish a Node.js CLI from Scratch 2 | 3 | ## Notes 4 | 5 | A minimum viable CLI can be a simple JS file: 6 | 7 | ```js 8 | #!/usr/bin/env node 9 | console.log("hello world!"); 10 | ``` 11 | 12 | \*nix systems also require you to mark these files as executable before you run them: 13 | 14 | ```shell 15 | chmod +x foo.js # mark executable 16 | ./foo.js # run the file 17 | 18 | ``` 19 | 20 | You can can handle arguments in whatever way you see fit. For example, one of the typical ways is to do it positionally: 21 | 22 | - If I want to have the convention that the first argument is the specified directory: `args[0]` 23 | - and to take a name flag by parsing the name flag: `args[1].slice("--name=")[1]` 24 | 25 | To publish just use `npm publish` like any other npm project. This includes a run.cmd script that will automatically be used for Windows users. 26 | 27 | ```shell 28 | $ npm version (major|minor|patch) # bumps version, updates README, adds git tag 29 | $ npm publish 30 | $ npm install -g mynewcli 31 | $ mynewcli 32 | # OR 33 | $ npx mynewcli 34 | ``` 35 | 36 | ## Personal Take 37 | 38 | Node.js isn't the only language/environment to write CLI's with. It can be slower than Go or Rust. But it has two advantages: 39 | 40 | - JS devs will already have it installed, no extra step needed 41 | - Being able to leverage the vast npm ecosystem 42 | 43 | ## Additional Resources 44 | 45 | These are some CLI's with a great developer experience: 46 | 47 | - https://github.com/ChristopherBiscardi/gatsby-theme 48 | - https://github.com/netlify/cli/ 49 | - https://github.com/plopjs/plop#why-generators 50 | -------------------------------------------------------------------------------- /02-create-a-single-command-node-js-cli-with-oclif-typescript-and-yarn-workspaces.md: -------------------------------------------------------------------------------- 1 | # 02. Create a Single-Command Node.js CLI with Oclif, TypeScript and Yarn Workspaces 2 | 3 | ## Notes 4 | 5 | The fastest way to create a robust, cross-platform compatible Node.js CLI is by running `npx oclif single mycli`. 6 | 7 | Oclif is both a CLI framework and a CLI that scaffolds CLI's. 8 | 9 | There are two ways to simulate this locally. You can actually `yarn link --global`, or you can use a yarn workspace. 10 | 11 | ## Personal Take 12 | 13 | For local development you don't want to reinstall your CLI every time. Use symlinking. 14 | 15 | ```shell 16 | ## example 1: global dep 17 | ## in CLI folder 18 | yarn link --global 19 | ## in project folder 20 | myfirstcli init 21 | 22 | ## example 2: local dep 23 | ## in CLI folder 24 | yarn link 25 | ## in project folder 26 | yarn link myfirstcli 27 | yarn myfirstcli init 28 | ``` 29 | -------------------------------------------------------------------------------- /03-parse-flags-and-args-in-node-js-clis-with-oclif-and-typescript.md: -------------------------------------------------------------------------------- 1 | # 03. Parse Flags and Args in Node.js CLIs with Oclif and TypeScript 2 | 3 | ## Notes 4 | 5 | - Flag options are non-positional arguments passed to the command. 6 | - Flags can either be option flags which take an argument, or boolean flags which do not. An option flag must have an argument. 7 | - For larger CLIs, it can be useful to declare a custom flag that can be shared amongst multiple commands. 8 | 9 | **Arguments** are positional arguments passed to the command. 10 | 11 | For example, if this command was run with `mycli arg1 arg2` it would be declared like this: 12 | 13 | ```js 14 | import Command from '@oclif/command' 15 | 16 | export class MyCLI extends Command { 17 | static args = [ 18 | {name: 'firstArg'}, 19 | {name: 'secondArg'}, 20 | ] 21 | 22 | async run() { 23 | // can get args as an object 24 | const {args} = this.parse(MyCLI) 25 | console.log(`running my command with args: ${args.firstArg}, ${args.secondArg}`) 26 | // can also get the args as an array 27 | const {argv} = this.parse(MyCLI) 28 | console.log(`running my command with args: ${argv[0]}, ${argv[1]}`) 29 | 30 | ``` 31 | 32 | Oclif comes with some best practice flag parsing built in: 33 | 34 | ```js 35 | static flags = { 36 | name: flags.string({ 37 | char: 'n', // shorter flag version 38 | description: 'name to print', // help description for flag 39 | env: 'MY_NAME', // default to value of environment variable 40 | options: ['a', 'b'], // only allow the value to be from a discrete set 41 | parse: input => 'output', // instead of the user input, return a different value 42 | default: 'world', // default value if flag not passed (can be a function that returns a string or undefined) 43 | required: false, // make flag required (this is not common and you should probably use an argument instead) 44 | dependsOn: ['extra-flag'], // this flag requires another flag 45 | exclusive: ['extra-flag'], // this flag cannot be specified alongside this other flag 46 | }), 47 | 48 | // flag with no value (-f, --force) 49 | force: flags.boolean({ 50 | char: 'f', 51 | default: true, // default value if flag not passed (can be a function that returns a boolean) 52 | // boolean flags may be reversed with `--no-` (in this case: `--no-force`). 53 | // The flag will be set to false if reversed. This functionality 54 | // is disabled by default, to enable it: 55 | // allowNo: true 56 | }), 57 | } 58 | 59 | ``` 60 | 61 | ## Personal Take 62 | 63 | - Multiple arguments are fine but when they are different types, that's when you get into issues. 64 | - A good rule of thumb: is 1 type of argument is fine, 2 types are very suspect, and 3 are never good. 65 | 66 | ## Additional Resources 67 | 68 | - [Command Flags](https://oclif.io/docs/args) 69 | - [Command Arguments](https://oclif.io/docs/args]) 70 | - [CLI Flags in Practice + How to Make Your Own CLI Command with oclif](https://blog.heroku.com/cli-flags-get-started-with-oclif) 71 | -------------------------------------------------------------------------------- /04-convert-a-single-command-cli-into-a-multi-command-cli-with-oclif-and-typescript.md: -------------------------------------------------------------------------------- 1 | # 04. Convert a Single Command CLI into a Multi Command CLI with Oclif and TypeScript 2 | 3 | ## Notes 4 | 5 | You can run `npx oclif multi mycli` to start a new CLI that is multi by default. 6 | 7 | ```typescript 8 | import { Command, flags } from "@oclif/command"; 9 | 10 | class Mycli extends Command { 11 | static description = "describe the command here"; 12 | 13 | static flags = { 14 | // add --version flag to show CLI version 15 | version: flags.version({ char: "v" }), 16 | help: flags.help({ char: "h" }), 17 | // flag with a value (-n, --name=VALUE) 18 | name: flags.string({ char: "n", description: "name to print" }), 19 | // flag with no value (-f, --force) 20 | force: flags.boolean({ char: "f" }) 21 | }; 22 | 23 | static args = [{ name: "file" }]; 24 | 25 | async run() { 26 | const { args, flags } = this.parse(Mycli); 27 | // console.log(argv) 28 | const name = flags.name || "world"; 29 | this.log(`hello egghead ${name} from ./src/index.ts`); 30 | } 31 | } 32 | 33 | export = Mycli; 34 | ``` 35 | 36 | The result of a single command CLI refactored into a multi command CLI: 37 | 38 | ```typescript 39 | import { Command, flags } from "@oclif/command"; 40 | 41 | class Mycli extends Command { 42 | static description = "describe the command here"; 43 | 44 | static flags = { 45 | // add --version flag to show CLI version 46 | version: flags.version({ char: "v" }), 47 | help: flags.help({ char: "h" }), 48 | // flag with a value (-n, --name=VALUE) 49 | name: flags.string({ char: "n", description: "name to print" }), 50 | // flag with no value (-f, --force) 51 | force: flags.boolean({ char: "f" }) 52 | }; 53 | 54 | static args = [{ name: "file" }]; 55 | 56 | async run() { 57 | const { args, flags } = this.parse(Mycli); 58 | console.log("hello from build"); 59 | const name = flags.name || "world"; 60 | this.log(`hello egghead ${name} from ./src/index.ts`); 61 | } 62 | } 63 | 64 | export = Mycli; 65 | ``` 66 | 67 | Original command to creating a multi-command CLI: 68 | 69 | ```shell 70 | $ npx oclif multi mynewcli 71 | ? npm package name (mynewcli): mynewcli 72 | $ cd mynewcli 73 | $ ./bin/run --version 74 | mynewcli/0.0.0 darwin-x64 node-v9.5.0 75 | $ ./bin/run --help 76 | USAGE 77 | $ mynewcli [COMMAND] 78 | 79 | COMMANDS 80 | hello 81 | help display help for mynewcli 82 | 83 | $ ./bin/run hello 84 | hello world from ./src/hello.js! 85 | ``` 86 | 87 | ## Personal Take 88 | 89 | To share logic or share initialization code for more than one command add a base command at the top level where you import that command. 90 | -------------------------------------------------------------------------------- /05-create-a-hybrid-single-multi-command-node-js-cli-with-oclif-and-typescript.md: -------------------------------------------------------------------------------- 1 | # 05. Create a Hybrid Single-Multi Command Node.js CLI with Oclif and TypeScript 2 | 3 | ## Notes 4 | 5 | To support multi commands in your CLI: 6 | 7 | Go into the bin directory of the oclif command, `packages/mycli/bin/run`. 8 | 9 | And check out the `run` file. This is where we actually get some insight into how oclif works with TypeScript under the hood and initializes its commands. 10 | 11 | ```javascript 12 | #!/usr/bin/env node 13 | 14 | const fs = require("fs"); 15 | const path = require("path"); 16 | const project = path.join(__dirname, "../tsconfig.json"); 17 | const dev = fs.existsSync(project); 18 | 19 | if (dev) { 20 | require("ts-node").register({ project }); 21 | } 22 | 23 | require(`../${dev ? "src" : "lib"}`) 24 | .run() 25 | .catch(require("@oclif/errors/handle")); 26 | ``` 27 | 28 | ## Personal Take 29 | 30 | You can actually have a list of recognized commands, for example `init`, `serve`, and `build`. Then we can put an if block saying that if there's a `process.argv`, if the length is more than two, which means that there is an additional multi-command selected and it's a `recognizedCommand`, then we run the multi-command code. 31 | -------------------------------------------------------------------------------- /06-debug-node-js-clis-with-the-vs-code-debugger-and-the-debug-module.md: -------------------------------------------------------------------------------- 1 | # 06. Debug Node.js CLIs with the VS Code Debugger and the Debug module 2 | 3 | ## Notes 4 | 5 | - If you're looking for performance issues rather than code errors, you'll want a robust logging solution. The debug module is best in class and is built into Oclif. 6 | 7 | - The CLI uses this module for all of its debugging. If you set the environment variable `DEBUG=\*` it will print all the debug output to the screen. 8 | 9 | - Depending on your shell you may need to escape this with `DEBUG=\*.` On Windows you can't set environment variables in line, so you'll need to run set `DEBUG=\*` before running the command. 10 | 11 | For example, I can place a break point inside of this run file. 12 | 13 | ```typescript 14 | const recognizedCommands = ["init", "serve", "build"]; 15 | if (process.argv.length > 2 && recognizedCommands.includes(process.argv[2])) { 16 | require(`../${dev ? "src" : "lib"}`) // break point here 17 | .run() 18 | .catch(require("@oclif/errors/handle")); 19 | } else { 20 | require(`../${dev ? "src" : "lib"}/commands/init`) 21 | .run() 22 | .catch(require("@oclif/errors/handle")); 23 | } 24 | ``` 25 | 26 | Running code: 27 | 28 | ```bash 29 | node --inspect-brk ./bin/run init 30 | ``` 31 | 32 | ## Personal Take 33 | 34 | - Always test on Windows/Linux OClif provides debug logging by default. 35 | - Log levels help filter for the thing you’re looking for by using a regex. 36 | -------------------------------------------------------------------------------- /07-test-node-js-clis-with-jest-and-oclif.md: -------------------------------------------------------------------------------- 1 | # 07. Test Node.js CLIs with Jest and Oclif 2 | 3 | ## Notes 4 | 5 | Install dependencies 6 | 7 | ```bash 8 | yarn add jest 9 | yarn add -D @oclif/test # v1 at time of writing 10 | 11 | ## for typescript 12 | yarn add jest-diff # v20 at time of writing 13 | yarn add -D @types/jest ts-jest # v24 at time of writing 14 | ``` 15 | 16 | Set up Jest config: 17 | 18 | ```js 19 | // jest.config.js 20 | module.exports = { 21 | testEnvironment: "node", 22 | moduleFileExtensions: ["ts", "js", "json"], 23 | testMatch: ["/test/jest/**/*.ts"], 24 | transform: { "\\.ts$": "ts-jest/preprocessor" }, 25 | mapCoverage: true, 26 | coverageReporters: ["lcov", "text-summary"], 27 | // collectCoverage: !!`Boolean(process.env.CI)`, 28 | collectCoverageFrom: ["src/**/*.ts"], 29 | coveragePathIgnorePatterns: ["/templates/"], 30 | coverageThreshold: { 31 | global: { 32 | branches: 100, 33 | functions: 100, 34 | lines: 100, 35 | statements: 100 36 | } 37 | } 38 | }; 39 | ``` 40 | 41 | A basic jest test looks like this - it can be helpful to test core logic outside of Oclif commands: 42 | 43 | ```js 44 | // /test/jest/foo.ts 45 | import { add } from "../../src"; 46 | // or in plain node.js 47 | // const {add} = require('../../src') 48 | 49 | describe("add", () => { 50 | test("1+2=3", () => { 51 | expect(add(1, 2)).toBe(3); 52 | }); 53 | }); 54 | ``` 55 | 56 | When testing the command itself: 57 | 58 | ```js 59 | import { test } from "@oclif/test"; 60 | 61 | describe("hello", () => { 62 | test 63 | .stdout() 64 | .command(["dev"]) // the command 65 | .it("runs dev", ctx => { 66 | expect(ctx.stdout).toBe("hello world"); 67 | }); 68 | 69 | test 70 | .stdout() 71 | .command(["dev", "--name", "jeff"]) 72 | .it("runs dev --name jeff", ctx => { 73 | expect(ctx.stdout).toBe("hello jeff"); 74 | }); 75 | }); 76 | ``` 77 | 78 | ## Personal Take 79 | 80 | - Investing in testing early will reap rewards in future. 81 | -------------------------------------------------------------------------------- /08-design-beautiful-intuitive-and-secure-node-js-cli-user-input-experiences-with-enquirer.md: -------------------------------------------------------------------------------- 1 | # 08. Design Beautiful, Intuitive and Secure Node.js CLI User Input Experiences with Enquirer 2 | 3 | ## Notes 4 | 5 | [Enquirer](https://github.com/enquirer/enquirer) is a great foundation to build Interactive CLIs that don't leak tokens and passwords. 6 | 7 | These are the [built in prompts](https://www.npmjs.com/package/enquirer#built-in-prompts) you get: 8 | 9 | - [AutoComplete Prompt](https://github.com/enquirer/enquirer#autocomplete-prompt) 10 | - [BasicAuth Prompt](https://github.com/enquirer/enquirer#basicauth-prompt) 11 | - [Confirm Prompt](https://github.com/enquirer/enquirer#confirm-prompt) 12 | - [Form Prompt](https://github.com/enquirer/enquirer#form-prompt) 13 | - [Input Prompt](https://github.com/enquirer/enquirer#input-prompt) 14 | - [Invisible Prompt](https://github.com/enquirer/enquirer#invisible-prompt) 15 | - [List Prompt](https://github.com/enquirer/enquirer#list-prompt) 16 | - [MultiSelect Prompt](https://github.com/enquirer/enquirer#multiselect-prompt) 17 | - [Numeral Prompt](https://github.com/enquirer/enquirer#numeral-prompt) 18 | - [Password Prompt](https://github.com/enquirer/enquirer#password-prompt) 19 | - [Quiz Prompt](https://github.com/enquirer/enquirer#quiz-prompt) 20 | - [Survey Prompt](https://github.com/enquirer/enquirer#survey-prompt) 21 | - [Scale Prompt](https://github.com/enquirer/enquirer#scale-prompt) 22 | - [Select Prompt](https://github.com/enquirer/enquirer#select-prompt) 23 | - [Sort Prompt](https://github.com/enquirer/enquirer#sort-prompt) 24 | - [Snippet Prompt](https://github.com/enquirer/enquirer#snippet-prompt) 25 | - [Toggle Prompt](https://github.com/enquirer/enquirer#toggle-prompt) 26 | 27 | Installing: 28 | 29 | ```bash 30 | yarn add enquirer 31 | ``` 32 | 33 | A basic enquirer prompt looks like this: 34 | 35 | ```js 36 | const { prompt } = require("enquirer"); 37 | 38 | // assuming you are in an async block 39 | const response = await prompt({ 40 | type: "input", 41 | name: "username", 42 | message: "What is your username?" 43 | }); 44 | 45 | console.log(response); // { username: 'jonschlinkert' } 46 | ``` 47 | 48 | Once you're familiar with the [prompt options API](https://github.com/enquirer/enquirer#prompt-options) you can use them pretty much everywhere: 49 | 50 | ```js 51 | { 52 | // required 53 | type: string | function, 54 | name: string | function, 55 | message: string | function | async function, 56 | 57 | // optional 58 | initial: string | function | async function, // The default value to return if the user does not supply a value. 59 | format: function | async function, // Function to format user input in the terminal. 60 | result: function | async function, // Function to format the final submitted value before it's returned. 61 | validate: function | async function, // Function to validate the submitted value before it's returned. This function may return a boolean or a string. If a string is returned it will be used as the validation error message. 62 | 63 | // each command will also recognize additional options 64 | // e.g. `limit`, `choices`, `hint`, `fields` 65 | } 66 | ``` 67 | 68 | Adding AutoComplete prompt: 69 | 70 | ```js 71 | const { prompt } = require("enquirer"); 72 | 73 | // inside async block 74 | const response = await prompt({ 75 | type: "autocomplete", 76 | name: "flavor", 77 | message: "Pick your favorite flavor", 78 | limit: 10, 79 | choices: [ 80 | "Almond", 81 | "Apple", 82 | "Banana", 83 | "Blackberry", 84 | "Blueberry", 85 | "Cherry", 86 | "Chocolate", 87 | "Cinnamon", 88 | "Coconut", 89 | "Cranberry", 90 | "Grape", 91 | "Nougat", 92 | "Orange", 93 | "Pear", 94 | "Pineapple", 95 | "Raspberry", 96 | "Strawberry", 97 | "Vanilla", 98 | "Watermelon", 99 | "Wintergreen" 100 | ] 101 | }); 102 | ``` 103 | 104 | ## Personal Take 105 | 106 | Principles to help your CLI be CI friendly: 107 | 108 | - Every prompt should have a corresponding flag that skips the prompt 109 | - Every prompt should inform the user about what flag they can use to skip the prompt (nice to have) 110 | -------------------------------------------------------------------------------- /09-create-highly-configurable-node-js-clis-with-cosmiconfig-and-oclif.md: -------------------------------------------------------------------------------- 1 | # 09. Create Highly Configurable Node.js CLIs with Cosmiconfig and Oclif 2 | 3 | ## Notes 4 | 5 | Cosmiconfig will start where you tell it to start and search up the directory tree: 6 | 7 | - a `package.json` property 8 | - a JSON or YAML, extensionless "rc file" 9 | - an "rc file" with the extensions `.json, .yaml, .yml, or .js`. 10 | - a `.config.js` CommonJS module 11 | 12 | If your module's name is "myapp", cosmiconfig will search up the directory tree for configuration in the following places: 13 | 14 | - a `myapp` property in package.json 15 | - a `.myapprc` file in JSON or YAML format 16 | - a `.myapprc.json` file 17 | - a `.myapprc.yaml`, `.myapprc.yml`, or `.myapprc.js` file 18 | - a `myapp.config.js` file exporting a JS object 19 | 20 | Installing: 21 | 22 | ```bash 23 | yarn add cosmiconfig 24 | ``` 25 | 26 | Result: 27 | 28 | ```js 29 | const { cosmiconfigSync } = require("cosmiconfig"); 30 | const explorerSync = cosmiconfigSync("myfirstcli"); 31 | const searchedFor = explorerSync.search(); 32 | const loaded = explorerSync.load(pathToConfig); 33 | ``` 34 | 35 | ## Personal Take 36 | 37 | Comsiconfig is [very configurable](https://github.com/davidtheclark/cosmiconfig#cosmiconfigoptions) - you can modify where it searches and where it stops and how it loads. 38 | 39 | ## Additional Resources 40 | 41 | See the docs for [async search](https://github.com/davidtheclark/cosmiconfig#usage) as well. 42 | -------------------------------------------------------------------------------- /10-scaffold-out-files-and-projects-from-templates-in-a-node-js-cli.md: -------------------------------------------------------------------------------- 1 | # 10. Scaffold out Files and Projects from Templates in a Node.js CLI 2 | 3 | ## Notes 4 | 5 | The best library for this is [copy-template-dir](https://www.npmjs.com/package/copy-template-dir). 6 | 7 | Given a source folder and a destination folder, copy from one to the other. 8 | 9 | ```typescript 10 | const copy = require('copy-template-dir') 11 | const path = require('path') 12 | 13 | const vars = { foo: 'bar' } 14 | const inDir = path.join(process.cwd(), 'templates') 15 | const outDir = path.join(process.cwd(), 'dist') 16 | 17 | copy(inDir, outDir, vars, (err, createdFiles) => { 18 | if (err) throw err 19 | createdFiles.forEach(filePath => console.log(`Created ${filePath}`)) 20 | console.log('done!') 21 | }) 22 | 23 | // promise based alternative 24 | const {promisify} = require('utils') 25 | promisify(copy(inDir, outDir, vars)) 26 | .then(() => console.log('done')) 27 | .catch(err => throw err) 28 | ``` 29 | 30 | ## Personal Take 31 | 32 | - Scaffolding is nice to have for your CLI. 33 | 34 | ## Additional Resources 35 | 36 | Other Express templating libraries: 37 | 38 | - https://www.npmjs.com/package/consolidate 39 | - https://www.npmjs.com/package/ejs 40 | 41 | As well as specific template flavors: 42 | 43 | - http://mustache.github.com/mustache.5.html 44 | - https://github.com/wycats/handlebars.js 45 | - https://github.com/Shopify/liquid 46 | -------------------------------------------------------------------------------- /11-spawn-and-manage-child-processes-and-node-js-streams-with-execa.md: -------------------------------------------------------------------------------- 1 | # 11. Spawn and Manage Child Processes and Node.js Streams with execa 2 | 3 | ## Notes 4 | 5 | Add an extra level of power by runnigng multiple concurrent child processes. 6 | 7 | Two ways to implement this: with `execa` and with a dedicated `yarn-or-npm` utility. 8 | 9 | Node child process API: 10 | 11 | ```javascript 12 | const { spawn } = require("child_process"); 13 | const ls = spawn("ls", ["-lh", "/usr"]); // calling a *nix command and passing args 14 | 15 | ls.stdout.on("data", data => { 16 | console.log(`stdout: ${data}`); 17 | }); 18 | 19 | ls.stderr.on("data", data => { 20 | console.error(`stderr: ${data}`); 21 | }); 22 | 23 | ls.on("close", code => { 24 | console.log(`child process exited with code ${code}`); 25 | }); 26 | ``` 27 | 28 | And for `execa` run: 29 | 30 | ``` 31 | yarn add execa 32 | ``` 33 | 34 | ```javascript 35 | const execa = require("execa"); 36 | 37 | const subprocess = execa("echo", ["foo"]); 38 | 39 | subprocess.stdout.pipe(process.stdout); 40 | // or subprocess.all.pipe(process.stdout); // both stdout and stderr 41 | // or subprocess.stdout.pipe(fs.createWriteStream('stdout.txt')) // write to file 42 | 43 | // or 44 | // const subprocess = execa(execPath, args, { stdio: 'inherit' }) // pass on all std streams 45 | 46 | // close this process when subprocess closes 47 | subprocess.on("close", code => process.exit(code)); 48 | subprocess.on("SIGINT", process.exit); 49 | subprocess.on("SIGTERM", process.exit); 50 | ``` 51 | 52 | ## Additional Resources 53 | 54 | - [List of Node exit codes](https://stackoverflow.com/a/47163396) if you'd like to stick to convention. 55 | -------------------------------------------------------------------------------- /12-prompt-users-to-update-your-node-js-clis-with-update-notifier.md: -------------------------------------------------------------------------------- 1 | # 12. Prompt Users to Update Your Node.js CLIs with Update-Notifier 2 | 3 | ## Notes 4 | 5 | If your users globally install your CLI's, will they ever update them? How will they know about new features and patches? How do you check for updates without impacting performance? How do you respect user preferences to ignore your updates? 6 | 7 | These are all solved problems with the wonderful `update-notifier` library. 8 | 9 | ``` 10 | $ npm install update-notifier 11 | ``` 12 | 13 | Example: 14 | 15 | ```typescript 16 | const updateNotifier = require("update-notifier"); 17 | const pkg = require("./package.json"); 18 | 19 | // Checks for available update and returns an instance 20 | const notifier = updateNotifier({ pkg }); 21 | 22 | // Notify using the built-in convenience method 23 | notifier.notify(); 24 | 25 | // `notifier.update` contains some useful info about the update 26 | console.log(notifier.update); 27 | /* 28 | { 29 | latest: '1.0.1', 30 | current: '1.0.0', 31 | type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build 32 | name: 'pageres' 33 | } 34 | */ 35 | ``` 36 | 37 | The first time the user runs your app, it will check for an update, and even if an update is available, it will wait the specified `updateCheckInterval` before notifying the user. 38 | 39 | You probably want to check for updates in a slightly longer interval, for example weekly. 40 | 41 | ```javascript 42 | const notifier = updateNotifier({ 43 | pkg, 44 | updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week 45 | }); 46 | 47 | if (notifier.update) { 48 | console.log(`Update available: ${notifier.update.latest}`); 49 | } 50 | ``` 51 | 52 | ## Personal Take 53 | 54 | - It's a very good thing to include this by default. 55 | 56 | ## Additional Resources 57 | 58 | There are a bunch projects using it. Check how they implement it. 59 | 60 | - [npm](https://github.com/npm/npm) - Package manager for JavaScript 61 | - [Yeoman](http://yeoman.io) - Modern workflows for modern webapps 62 | - [AVA](https://ava.li) - Simple concurrent test runner 63 | - [XO](https://github.com/xojs/xo) - JavaScript happiness style linter 64 | - [Pageres](https://github.com/sindresorhus/pageres) - Capture website screenshots 65 | - [Node GH](http://nodegh.io) - GitHub command line tool 66 | -------------------------------------------------------------------------------- /13-store-state-on-filesystem-in-node-js-clis-with-conf.md: -------------------------------------------------------------------------------- 1 | # 13. Store State on Filesystem in Node.js CLIs with Conf 2 | 3 | ## Notes 4 | 5 | The Conf library provides an unbelievably easy way to persist values to memory in an XDG compliant fashion. 6 | 7 | The standard that everyone has agreed on is [the XDG Spec](https://wiki.archlinux.org/index.php/XDG_Base_Directory), which uses 4 environment variables: 8 | 9 | - `XDG_CONFIG_HOME` for user-specific configurations (default `$HOME/.config`) 10 | - `XDG_CACHE_HOME` for user-specific non-essential (cached) data (default `$HOME/.cache`) 11 | - `XDG_DATA_HOME` for user-specific data files (default `$HOME/.local/share`) 12 | - `XDG_RUNTIME_DIR` for temporary, non-essential, user-specific data files such as sockets, named pipes (no default) 13 | 14 | How it's used: 15 | 16 | ```javascript 17 | #!/usr/bin/env node 18 | const { prompt } = require("enquirer"); 19 | const fs = require("fs"); 20 | const Conf = require("conf"); 21 | const config = new Conf(); 22 | 23 | console.log({ configPath: config.path }); 24 | 25 | (async function() { 26 | await prompt({ 27 | type: "input", 28 | name: "name", 29 | message: "Where is Harvey Dent?", 30 | default: config.get("name") 31 | }) 32 | .then(result => { 33 | config.set("name", result.name); 34 | return result; 35 | }) 36 | .then(console.log) 37 | .catch(console.error); 38 | })(); 39 | ``` 40 | 41 | ```javascript 42 | #!/usr/bin/env node 43 | const { prompt } = require("enquirer"); 44 | const Conf = require("conf"); 45 | const config = new Conf(); 46 | const colors = require("ansi-colors"); 47 | const presets = [ 48 | "apple", 49 | "grape", 50 | "watermelon", 51 | "cherry", 52 | "strawberry", 53 | "lemon", 54 | "orange" 55 | ]; 56 | const priorChoices = config.get("choices") || []; 57 | const separator = priorChoices && 58 | priorChoices.length && { role: "separator", value: colors.dim("────") }; 59 | const choices = [ 60 | ...priorChoices, 61 | separator, 62 | ...presets.filter(x => !priorChoices.includes(x)) 63 | ].filter(Boolean); 64 | 65 | (async function() { 66 | await prompt({ 67 | type: "select", 68 | name: "color", 69 | message: "Pick your favorite color", 70 | choices 71 | }) 72 | .then(result => { 73 | config.set("choices", [result.color, ...priorChoices].slice(0, 3)); 74 | return result; 75 | }) 76 | .then(console.log) 77 | .catch(console.error); 78 | })(); 79 | ``` 80 | 81 | ## Personal Take 82 | 83 | - Offer to persist state when possible to create defaults 84 | -------------------------------------------------------------------------------- /14-create-node-js-cli-s-that-intelligently-adapt-to-usage-with-frecency.md: -------------------------------------------------------------------------------- 1 | # 14. Create Node.js CLI's that Intelligently Adapt to Usage with Frecency 2 | 3 | ## Notes 4 | 5 | - Recency biases towards newer entries being more likely to be what you are looking to do. 6 | - Frequency biases towards betting on you doing the same thing you've always done. 7 | - A good example of this is in Slack or other similar interfaces! 8 | 9 | ## Personal Take 10 | 11 | Frecency is made for the browser, so in Node it assumes the localStorage API: 12 | 13 | ```javascript 14 | const Conf = require("conf"); 15 | const path = require("path"); 16 | import { LocalStorage } from "node-localstorage"; 17 | 18 | const config = new Conf(); 19 | const storageProviderFrecencyFilePath = path.join( 20 | path.basedir(config.path), 21 | "frecency" 22 | ); 23 | const storageProvider = new LocalStorage(storageProviderFrecencyFilePath); 24 | const Fruitcency = new Frecency({ 25 | key: "fruits", 26 | // idAttribute: '_id', // unique identifier, defaults to '_id' 27 | storageProvider 28 | }); 29 | ``` 30 | 31 | Now when your user makes selections: 32 | 33 | ```js 34 | onSelect: (searchQuery, selectedResult) => { 35 | // ... 36 | Fruitcency.save({ 37 | searchQuery, // an object, with _id in it 38 | selectedId: selectedResult._id 39 | }); 40 | // ... 41 | }; 42 | ``` 43 | 44 | And before you display: 45 | 46 | ```js 47 | onSearch: (searchQuery) => { 48 | ... 49 | // Search results received from a search API. 50 | const searchResults = [ 51 | { _id: 'Apple'}, 52 | { _id: 'Banana' }, 53 | // ... 54 | ]; 55 | 56 | return Fruitcency.sort({ 57 | searchQuery, 58 | results: searchResults 59 | }); 60 | ``` 61 | 62 | ## Additional Resources 63 | 64 | - Plugin to add frecency to search results. Original blog post on Frecency by Slack: 65 | https://slack.engineering/a-faster-smarter-quick-switcher-77cbc193cb60 66 | -------------------------------------------------------------------------------- /15-polish-node-js-cli-output-with-chalk-and-ora.md: -------------------------------------------------------------------------------- 1 | # 15. Polish Node.js CLI Output with Chalk and Ora 2 | 3 | ## Notes 4 | 5 | CLI output can look extremely unfriendly and confusing. These libraries help create a better experience: 6 | 7 | - Chalk 8 | - CLI-UX 9 | - Ora 10 | 11 | ### Chalk 12 | 13 | Library for terminal string styling: 14 | 15 | ```bash 16 | npm install chalk 17 | ``` 18 | 19 | Example of API: 20 | 21 | ```js 22 | const chalk = require("chalk"); 23 | 24 | console.log(chalk.blue("Hello world!")); 25 | ``` 26 | 27 | ### CLI-UX 28 | 29 | ```bash 30 | npm install cli-ux 31 | ``` 32 | 33 | CLI-UX is oclif's first party utility library. It includes prompting, and other cool tools: 34 | 35 | - `cli.url(text, uri)`: Create a hyperlink (if supported in the terminal) 36 | - `cli.open`: Open a url in the browser 37 | - `cli.annotation`: Shows an iterm annotation 38 | - `cli.wait`: Waits for 1 second or given milliseconds 39 | - `cli.action`: Shows a spinner 40 | - `cli.table`: Displays tabular data 41 | 42 | ```js 43 | import { Command } from "@oclif/command"; 44 | import { cli } from "cli-ux"; 45 | import axios from "axios"; 46 | 47 | export default class Users extends Command { 48 | static flags = { 49 | ...cli.table.flags() 50 | }; 51 | 52 | async run() { 53 | const { flags } = this.parse(Users); 54 | const { data: users } = await axios.get( 55 | "https://jsonplaceholder.typicode.com/users" 56 | ); 57 | 58 | cli.table( 59 | users, 60 | { 61 | name: { 62 | minWidth: 7 63 | }, 64 | company: { 65 | get: row => row.company && row.company.name 66 | }, 67 | id: { 68 | header: "ID", 69 | extended: true 70 | } 71 | }, 72 | { 73 | printLine: this.log, 74 | ...flags // parsed flags 75 | } 76 | ); 77 | } 78 | } 79 | ``` 80 | 81 | - `cli.tree`: Generate a tree and display it 82 | 83 | ``` 84 | ├─ foo 85 | └─ bar 86 | └─ baz 87 | ``` 88 | 89 | ### Ora 90 | 91 | Ora is THE spinner library. Much of your CLI time will be spent doing asynchronous tasks. 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Build Custom CLI Tooling with oclif and TypeScript

2 | 3 |

4 | 5 | ## About 6 | 7 | These notes are intended to be used and studied in tandem with [Shawn Wang](https://twitter.com/swyx)'s [Build Custom CLI Tooling with oclif and TypeScript](https://egghead.io/courses/build-custom-cli-tooling-with-oclif-and-typescript) course. 8 | 9 | ## Summary 10 | 11 | This cheatsheet cover: 12 | 13 | - Create a Simple CLI 14 | - Pass Args and flags to a CLI 15 | - Set up testing for a CLI 16 | - Add filesystem state to a CLI 17 | - Scaffold boilerplates (e.g. templates) 18 | - Polish the CLI with colors, spinners, etc. 19 | - Spawn child processes so other CLIs can run 20 | - Control logging & output from other processes 21 | 22 | ## Contribute 23 | 24 | These are community notes that I hope everyone who studies benefits from. If you notice areas that could be improved please feel free to open a PR! 25 | --------------------------------------------------------------------------------