Back to DIY Tutorials
DIY TutorialBeginner

Build a CLI Tool from Scratch

Create a powerful command-line interface tool using Node.js with argument parsing, interactive prompts, and colorful output.

March 10, 202435 min read
CLINode.jsnpm
BUILD A CLI TOOL FROM SCRATCH Every developer reaches for the terminal dozens of times a day. Building your own CLI tool means you can automate the repetitive parts of your workflow and share those automations with the world via npm. The Node.js ecosystem has excellent libraries that handle all the hard parts — argument parsing, interactive prompts, color output, spinners — so you can focus on the actual logic. We'll build scaffold-it: a tool that generates project boilerplate from templates. By the end you'll know the full lifecycle from an empty folder to a published npm package. WHAT YOU'LL BUILD - A CLI that accepts both positional arguments and flags from the command line - Interactive prompts that ask for missing information when arguments are omitted - Colored, structured terminal output using Chalk - Animated spinners during async operations like file copying - A template system with variable substitution - npm publish setup so anyone can run your tool with npx PREREQUISITES - Comfortable with Node.js and async/await - Node 18 or higher installed - An npm account (free) if you want to publish at the end STEP 1: PROJECT SETUP Create the project directory and initialize it with npm init -y. Then open package.json and add the bin field — this is what wires up the command name to your entry file: "bin": { "scaffold-it": "./dist/index.js" } Also add a scripts section with build (tsc), dev (ts-node src/index.ts), and start (node dist/index.js). Set the engines field to require Node 18 or higher. Install the runtime dependencies: npm install commander inquirer chalk ora fs-extra And dev dependencies: npm install -D typescript @types/node @types/inquirer @types/fs-extra ts-node Run npx tsc --init to create a tsconfig.json, then update it to set target to ES2022, module to CommonJS, rootDir to src, and outDir to dist with strict mode enabled. One thing that trips people up: the bin file must start with a shebang line (#!/usr/bin/env node) so the OS knows to run it with Node. Without this line, executing the file directly will fail with a syntax error or permission issue. STEP 2: PROJECT STRUCTURE The project has three main concerns: the entry point that sets up the CLI program, the commands that implement each subcommand, and utilities that the commands share. Lay it out like this: src/ index.ts (entry point and shebang) commands/ create.ts (scaffold-it create <name>) list.ts (scaffold-it list) utils/ logger.ts (colored console helpers) template.ts (file generation logic) templates/ react-ts/ (template files for each project type) node-api/ The templates directory ships with the package and contains the actual files that get copied when a user runs scaffold-it create. These can be anything — a package.json, a tsconfig, starter source files — with placeholder variables that get swapped at generation time. STEP 3: ENTRY POINT AND COMMANDER SETUP The entry point file starts with the shebang on line 1. Then it imports Command from commander, imports the two command modules, creates a program instance, and registers the commands. The program.name(), program.description(), and program.version() calls set up the help text that appears when someone runs scaffold-it --help. Finally, program.parse(process.argv) kicks everything off. Commander handles the rest: it parses the arguments, routes to the right command handler, and prints usage errors if something doesn't match the registered command signatures. STEP 4: COLORED LOGGING HELPERS A logger utility wraps Chalk so you get consistent, structured output across all commands. Create five log levels: info (blue), success (green), warn (yellow), error (red to stderr), title (bold cyan for section headers), and dim (muted gray for secondary content like "Next steps" instructions). Using a centralized logger instead of raw console.log calls means you can easily add timestamps, disable color in CI environments, or redirect output — all in one place. STEP 5: THE LIST COMMAND The list command is the simplest possible Commander command. It defines a TEMPLATES array of objects with name and description, then in the action handler loops over the array and prints each entry with the name padded and colored green and the description in dim gray. Running scaffold-it list produces a clean, formatted list with no interaction required. This is a good command to build first because it confirms your Commander setup and logger are working before you tackle the more complex create flow. STEP 6: THE CREATE COMMAND WITH INTERACTIVE PROMPTS The create command is where Inquirer earns its keep. The command accepts an optional positional argument (the project name) and an optional --template flag. If either is missing, Inquirer prompts for it interactively. The Inquirer prompts array defines three questions. The first is a text input for project name, only shown if no name argument was provided, with a validation function that rejects names containing uppercase letters or spaces. The second is a list prompt to choose a template, only shown if --template was not passed. The third is a confirm prompt asking whether to initialize a git repository, defaulting to true. After collecting all the inputs, the command checks if the target directory already exists and exits with an error if so. Then it runs through three operations, each wrapped in an ora spinner: copying the template files, optionally initializing a git repository with an initial commit, and optionally running npm install. Each spinner shows success or failure with an appropriate message. The --no-install flag (which Commander parses as options.install being false) lets users skip the npm install step, useful when they want to review the generated files before installing. STEP 7: TEMPLATE UTILITIES The template utility has two functions. copyTemplate looks up a named template in the src/templates directory and copies it to the target destination using fs-extra's copy function. If the template doesn't exist, it throws a descriptive error. processTemplateFiles walks the destination directory recursively and replaces all occurrences of {{ variableName }} placeholders in text files with values from a variables object. So if a template file contains {{ projectName }}, it becomes the actual project name after generation. This is the same convention used by create-react-app and create-next-app. The replacement regex is /{{s*(w+)s*}}/g, which allows optional whitespace around the variable name. Any placeholder that doesn't match a key in the variables object is left unchanged. STEP 8: TESTING LOCALLY WITH npm link Before publishing, test the CLI from your local machine using npm link. First build the project with npm run build, then run npm link. This creates a global symlink so you can run scaffold-it directly as if it were installed globally. Test the full flow: scaffold-it list scaffold-it create my-test-app --template react-ts scaffold-it create (interactive mode) When you're done testing, run npm unlink scaffold-it to remove the global symlink. STEP 9: PUBLISHING TO NPM Before publishing, make sure your package.json has a few key fields: a description, a keywords array (cli, scaffold, boilerplate, generator), an author field, a license (MIT), and a files array that lists only what the runtime needs — typically ["dist", "src/templates"]. This last field controls what gets bundled in the published package and keeps the tarball small. Log in with npm login, then run npm run build followed by npm publish --access public. For scoped package names like @yourname/scaffold-it, the --access public flag is required for the first publish. Once published, anyone can use your tool immediately without installing it globally: npx scaffold-it create my-app Or install it permanently: npm install -g scaffold-it HANDLING ERRORS GRACEFULLY A CLI that crashes with a raw stack trace is a bad user experience. Register two global error handlers at the top of your entry point. One handles uncaughtException — it logs the error message using your logger and exits with code 1. The other handles SIGINT (Ctrl+C) — it prints a blank line, logs "Cancelled" in yellow, and exits with code 0. With these in place, pressing Ctrl+C during an interactive prompt exits cleanly instead of printing a stack trace, and unexpected exceptions show a clean one-line error instead of a wall of Node internals. WHAT TO BUILD NEXT - Config file support: read a .scaffolditrc.json from the user's home directory for default template paths and preferences - Remote templates: fetch templates from GitHub tarballs using the GitHub API so templates can be updated independently of the CLI version - Plugin system: let third parties register their own templates with scaffold-it register <npm-package> - Auto-update check: on startup, compare the installed version against the npm registry and prompt the user if a newer version is available

Related Content