Project: CLI Tool & npm Package
š Concept
Build a command-line tool and publish it as an npm package. CLI tools are one of Node.js's strongest use cases ā many popular developer tools are built with Node.js.
Project: Dev Environment Manager CLI
Features:
devenv init # Initialize a project with templates
devenv env create staging # Create an environment
devenv env list # List all environments
devenv secrets set KEY VALUE # Set an encrypted secret
devenv secrets list # List secrets (masked values)
devenv deploy staging # Deploy to an environment
devenv logs --follow # Tail production logs
Popular CLI frameworks for Node.js:
| Framework | Pros |
|---|---|
| Commander.js | Most popular, simple, well-documented |
| yargs | Powerful argument parsing, auto-generated help |
| oclif | Framework by Heroku, TypeScript-first, plugin system |
| Inquirer.js | Interactive prompts (lists, checkboxes, passwords) |
| chalk | Terminal string styling (colors, bold, etc.) |
| ora | Elegant terminal spinners |
Publishing to npm:
# 1. Set up package.json with "bin" field
# 2. Add shebang to entry file: #!/usr/bin/env node
# 3. npm login
# 4. npm publish
# 5. Users install globally: npm install -g your-package
This project demonstrates skills in:
- CLI argument parsing and validation
- Interactive terminal UIs (prompts, progress bars, tables)
- File system operations (config files, templates)
- Process management (spawning commands)
- npm package publishing and versioning
š» Code Example
1// CLI Tool with Commander.js23// #!/usr/bin/env node4const { Command } = require("commander");5const chalk = require("chalk");6const ora = require("ora");7const inquirer = require("inquirer");8const fs = require("fs");9const path = require("path");1011const program = new Command();1213program14 .name("devenv")15 .description("Developer environment management CLI")16 .version("1.0.0");1718// === init command ===19program20 .command("init")21 .description("Initialize a new project")22 .option("-t, --template <type>", "Template type", "express")23 .action(async (options) => {24 console.log(chalk.bold.blue("\nš DevEnv Initializer\n"));2526 // Interactive prompts27 const answers = await inquirer.prompt([28 {29 type: "input",30 name: "name",31 message: "Project name:",32 default: path.basename(process.cwd()),33 validate: (input) => (input.length > 0 ? true : "Name is required"),34 },35 {36 type: "list",37 name: "template",38 message: "Select a template:",39 choices: ["express-api", "express-fullstack", "fastify-api", "koa-api"],40 default: options.template,41 },42 {43 type: "checkbox",44 name: "features",45 message: "Select features:",46 choices: [47 { name: "TypeScript", value: "typescript", checked: true },48 { name: "Docker", value: "docker", checked: true },49 { name: "CI/CD (GitHub Actions)", value: "cicd" },50 { name: "Database (PostgreSQL)", value: "database" },51 { name: "Redis", value: "redis" },52 { name: "Testing (Jest)", value: "testing", checked: true },53 ],54 },55 {56 type: "confirm",57 name: "installDeps",58 message: "Install dependencies?",59 default: true,60 },61 ]);6263 const spinner = ora("Creating project structure...").start();6465 try {66 // Create project files based on template67 await createProjectStructure(answers);68 spinner.succeed("Project structure created");6970 if (answers.installDeps) {71 spinner.start("Installing dependencies...");72 // await execAsync("npm install");73 spinner.succeed("Dependencies installed");74 }7576 console.log(chalk.green("\nā Project initialized successfully!"));77 console.log(chalk.gray("\nNext steps:"));78 console.log(chalk.cyan(" npm run dev") + " Start development server");79 console.log(chalk.cyan(" npm test") + " Run tests");80 console.log(chalk.cyan(" npm run build") + " Build for production");81 } catch (err) {82 spinner.fail("Initialization failed");83 console.error(chalk.red(err.message));84 process.exit(1);85 }86 });8788// === env commands ===89const envCmd = program.command("env").description("Manage environments");9091envCmd92 .command("list")93 .description("List all environments")94 .action(() => {95 const envs = [96 { name: "development", status: "active", url: "localhost:3000" },97 { name: "staging", status: "active", url: "staging.example.com" },98 { name: "production", status: "active", url: "api.example.com" },99 ];100101 console.log(chalk.bold("\nEnvironments:\n"));102 console.log(103 chalk.gray(" NAME".padEnd(18) + "STATUS".padEnd(12) + "URL")104 );105 console.log(chalk.gray(" " + "-".repeat(50)));106107 for (const env of envs) {108 const statusColor = env.status === "active" ? chalk.green : chalk.red;109 console.log(110 ` ${chalk.white(env.name.padEnd(18))}${statusColor(env.status.padEnd(12))}${chalk.cyan(env.url)}`111 );112 }113 console.log();114 });115116envCmd117 .command("create <name>")118 .description("Create a new environment")119 .option("-c, --clone <source>", "Clone from existing environment")120 .action(async (name, options) => {121 const spinner = ora(`Creating environment: ${name}`).start();122 // Simulate creation123 await new Promise((r) => setTimeout(r, 2000));124 spinner.succeed(`Environment "${name}" created successfully`);125 });126127// === secrets commands ===128const secretsCmd = program.command("secrets").description("Manage secrets");129130secretsCmd131 .command("set <key> <value>")132 .description("Set a secret")133 .option("-e, --env <environment>", "Target environment", "development")134 .action((key, value, options) => {135 console.log(136 chalk.green(`ā Secret "${key}" set for ${options.env}`)137 );138 });139140secretsCmd141 .command("list")142 .description("List all secrets")143 .option("-e, --env <environment>", "Target environment", "development")144 .action((options) => {145 const secrets = [146 { key: "DATABASE_URL", preview: "postgres://...****" },147 { key: "JWT_SECRET", preview: "****" },148 { key: "REDIS_URL", preview: "redis://...****" },149 ];150151 console.log(chalk.bold(`\nSecrets (${options.env}):\n`));152 for (const s of secrets) {153 console.log(` ${chalk.cyan(s.key.padEnd(20))} ${chalk.gray(s.preview)}`);154 }155 console.log();156 });157158// Parse arguments159program.parse();160161// Helper162async function createProjectStructure(config) {163 const dirs = ["src", "tests", "config"];164 for (const dir of dirs) {165 fs.mkdirSync(dir, { recursive: true });166 }167}168169// package.json for publishing:170const packageJson = {171 name: "devenv-cli",172 version: "1.0.0",173 description: "Developer environment management CLI",174 bin: { devenv: "./src/cli.js" },175 files: ["src/", "templates/"],176 keywords: ["cli", "devtools", "environment"],177 engines: { node: ">=18" },178};
šļø Practice Exercise
Exercises:
- Build a CLI with Commander.js or yargs ā at least 5 commands with options and arguments
- Add interactive prompts using Inquirer.js for user configuration
- Implement colored output with chalk and progress spinners with ora
- Create a config file manager ā read/write JSON/YAML configuration files
- Publish your CLI to npm ā set up the
binfield, add a shebang line, test global installation - Add automated tests for your CLI commands using Jest (test argument parsing and output)
ā ļø Common Mistakes
Forgetting the shebang line (
#!/usr/bin/env node) ā without it, the OS doesn't know to run the file with Node.js when called as a CLI commandNot setting the
binfield in package.json ā this is what creates the global command when usersnpm install -gyour packageNot handling errors gracefully ā uncaught errors dump stack traces; CLI tools should show friendly error messages with chalk formatting
Not providing --help and --version flags ā users expect these; Commander.js adds them automatically
Hardcoding file paths ā use
path.resolve(),os.homedir(), andprocess.cwd()for cross-platform compatibility
š¼ Interview Questions
š¤ Mock Interview
Mock interview is powered by AI for Project: CLI Tool & npm Package. Login to unlock this feature.