Project: CLI Tool & npm Package

0/3 in this phase0/48 across the roadmap

šŸ“– 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

codeTap to expand ā›¶
1// CLI Tool with Commander.js
2
3// #!/usr/bin/env node
4const { Command } = require("commander");
5const chalk = require("chalk");
6const ora = require("ora");
7const inquirer = require("inquirer");
8const fs = require("fs");
9const path = require("path");
10
11const program = new Command();
12
13program
14 .name("devenv")
15 .description("Developer environment management CLI")
16 .version("1.0.0");
17
18// === init command ===
19program
20 .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"));
25
26 // Interactive prompts
27 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 ]);
62
63 const spinner = ora("Creating project structure...").start();
64
65 try {
66 // Create project files based on template
67 await createProjectStructure(answers);
68 spinner.succeed("Project structure created");
69
70 if (answers.installDeps) {
71 spinner.start("Installing dependencies...");
72 // await execAsync("npm install");
73 spinner.succeed("Dependencies installed");
74 }
75
76 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 });
87
88// === env commands ===
89const envCmd = program.command("env").description("Manage environments");
90
91envCmd
92 .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 ];
100
101 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)));
106
107 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 });
115
116envCmd
117 .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 creation
123 await new Promise((r) => setTimeout(r, 2000));
124 spinner.succeed(`Environment "${name}" created successfully`);
125 });
126
127// === secrets commands ===
128const secretsCmd = program.command("secrets").description("Manage secrets");
129
130secretsCmd
131 .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 });
139
140secretsCmd
141 .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 ];
150
151 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 });
157
158// Parse arguments
159program.parse();
160
161// Helper
162async function createProjectStructure(config) {
163 const dirs = ["src", "tests", "config"];
164 for (const dir of dirs) {
165 fs.mkdirSync(dir, { recursive: true });
166 }
167}
168
169// 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:

  1. Build a CLI with Commander.js or yargs — at least 5 commands with options and arguments
  2. Add interactive prompts using Inquirer.js for user configuration
  3. Implement colored output with chalk and progress spinners with ora
  4. Create a config file manager — read/write JSON/YAML configuration files
  5. Publish your CLI to npm — set up the bin field, add a shebang line, test global installation
  6. 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 command

  • Not setting the bin field in package.json — this is what creates the global command when users npm install -g your package

  • Not 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(), and process.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.