CLI tools are powerful for automation and developer workflows. Here is how to build one properly.
Project Setup
mkdir my-cli && cd my-cli
npm init -y
npm install commander chalk ora prompts conf
npm install -D typescript @types/node @types/prompts tsx
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
Main CLI Entry
// src/index.ts
#!/usr/bin/env node
import { Command } from "commander";
import { initCommand } from "./commands/init.js";
import { generateCommand } from "./commands/generate.js";
import { configCommand } from "./commands/config.js";
const program = new Command();
program
.name("my-cli")
.description("A helpful CLI tool")
.version("1.0.0");
program.addCommand(initCommand);
program.addCommand(generateCommand);
program.addCommand(configCommand);
program.parse();
Init Command With Interactive Prompts
// src/commands/init.ts
import { Command } from "commander";
import prompts from "prompts";
import chalk from "chalk";
import ora from "ora";
import { writeFile, mkdir } from "fs/promises";
import { join } from "path";
export const initCommand = new Command("init")
.description("Initialize a new project")
.option("-y, --yes", "Skip prompts and use defaults")
.action(async (options) => {
console.log(chalk.cyan.bold("\nProject Setup\n"));
let answers: Record<string, unknown>;
if (options.yes) {
answers = {
name: "my-project",
framework: "nextjs",
typescript: true,
styling: "tailwind",
};
} else {
answers = await prompts(
[
{
type: "text",
name: "name",
message: "Project name:",
initial: "my-project",
validate: (value: string) =>
/^[a-z0-9-]+$/.test(value) || "Lowercase letters, numbers, and hyphens only",
},
{
type: "select",
name: "framework",
message: "Framework:",
choices: [
{ title: "Next.js", value: "nextjs" },
{ title: "Remix", value: "remix" },
{ title: "Astro", value: "astro" },
],
},
{
type: "toggle",
name: "typescript",
message: "Use TypeScript?",
initial: true,
active: "Yes",
inactive: "No",
},
{
type: "select",
name: "styling",
message: "Styling:",
choices: [
{ title: "Tailwind CSS", value: "tailwind" },
{ title: "CSS Modules", value: "modules" },
{ title: "Styled Components", value: "styled" },
],
},
],
{ onCancel: () => process.exit(0) },
);
}
const spinner = ora("Creating project files...").start();
try {
const dir = join(process.cwd(), answers.name as string);
await mkdir(dir, { recursive: true });
// Generate config file
const config = {
name: answers.name,
framework: answers.framework,
typescript: answers.typescript,
styling: answers.styling,
createdAt: new Date().toISOString(),
};
await writeFile(
join(dir, "project.config.json"),
JSON.stringify(config, null, 2),
);
// Generate package.json
const packageJson = {
name: answers.name,
version: "0.1.0",
private: true,
scripts: {
dev: "next dev",
build: "next build",
start: "next start",
},
};
await writeFile(
join(dir, "package.json"),
JSON.stringify(packageJson, null, 2),
);
spinner.succeed(chalk.green("Project created successfully!"));
console.log(`\n ${chalk.dim("cd")} ${answers.name as string}`);
console.log(` ${chalk.dim("npm install")}`);
console.log(` ${chalk.dim("npm run dev")}\n`);
} catch (error) {
spinner.fail(chalk.red("Failed to create project"));
console.error(error);
process.exit(1);
}
});
Generate Command With Templates
// src/commands/generate.ts
import { Command } from "commander";
import chalk from "chalk";
import { writeFile, mkdir } from "fs/promises";
import { join, dirname } from "path";
const templates: Record<string, (name: string) => string> = {
component: (name) => `interface ${name}Props {
children: React.ReactNode;
}
export function ${name}({ children }: ${name}Props) {
return <div>{children}</div>;
}
`,
hook: (name) => `import { useState, useEffect } from "react";
export function ${name}() {
const [data, setData] = useState(null);
useEffect(() => {
// fetch data
}, []);
return { data };
}
`,
page: (name) => `export default function ${name}Page() {
return (
<main>
<h1>${name}</h1>
</main>
);
}
`,
};
export const generateCommand = new Command("generate")
.alias("g")
.description("Generate a file from a template")
.argument("<type>", `Type: ${Object.keys(templates).join(", ")}`)
.argument("<name>", "Name for the generated file")
.option("-d, --dir <directory>", "Output directory", "src")
.action(async (type: string, name: string, options) => {
const template = templates[type];
if (!template) {
console.error(
chalk.red(`Unknown type "${type}". Available: ${Object.keys(templates).join(", ")}`),
);
process.exit(1);
}
const pascalName = name
.split("-")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join("");
const fileName =
type === "hook" ? `use-${name}.ts` : `${name}.tsx`;
const filePath = join(process.cwd(), options.dir, fileName);
try {
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, template(pascalName));
console.log(chalk.green(`Created ${filePath}`));
} catch (error) {
console.error(chalk.red("Failed to generate file"), error);
process.exit(1);
}
});
Config Command
// src/commands/config.ts
import { Command } from "commander";
import Conf from "conf";
import chalk from "chalk";
const config = new Conf({ projectName: "my-cli" });
export const configCommand = new Command("config")
.description("Manage configuration");
configCommand
.command("set <key> <value>")
.description("Set a config value")
.action((key: string, value: string) => {
config.set(key, value);
console.log(chalk.green(`Set ${key} = ${value}`));
});
configCommand
.command("get <key>")
.description("Get a config value")
.action((key: string) => {
const value = config.get(key);
if (value === undefined) {
console.log(chalk.yellow(`"${key}" is not set`));
} else {
console.log(`${key} = ${String(value)}`);
}
});
configCommand
.command("list")
.description("List all config values")
.action(() => {
const all = config.store;
if (Object.keys(all).length === 0) {
console.log(chalk.dim("No configuration set"));
return;
}
for (const [key, value] of Object.entries(all)) {
console.log(` ${chalk.cyan(key)}: ${String(value)}`);
}
});
configCommand
.command("reset")
.description("Reset all config values")
.action(() => {
config.clear();
console.log(chalk.green("Configuration cleared"));
});
Make It Executable
// package.json additions
{
"bin": {
"my-cli": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"link": "npm run build && npm link"
}
}
npm run build
npm link
my-cli init
my-cli generate component button
my-cli config set theme dark
Need Custom Developer Tools?
We build CLI tools and developer workflows that save time. Contact us to discuss your needs.