Build beautiful CLIs with TypeScript
Learn how to build a complete CLI application using EasyCLI. We'll create a project manager CLI that can create, list, and deploy projects.
project-cli create my-app --template react
project-cli list
project-cli deploy my-app --env production --force
Create a new project:
npx create-easycli project-cli
cd project-cli
Or manually:
mkdir project-cli && cd project-cli
pnpm init
pnpm add easycli-core easycli-ui
pnpm add -D typescript tsup tsx
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
Create tsup.config.ts:
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
target: "node18",
clean: true,
dts: true,
banner: {
js: "#!/usr/bin/env node"
}
});
Create src/index.ts:
import { defineCLI } from "easycli-core";
import { colors } from "easycli-ui";
const cli = defineCLI({
name: "project-cli",
version: "1.0.0",
description: "Manage your projects",
commands: {}
});
cli.run();
Test it:
pnpm tsx src/index.ts --help
Add the create command:
import { defineCLI } from "easycli-core";
import { colors, spinner, box } from "easycli-ui";
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";
const cli = defineCLI({
name: "project-cli",
version: "1.0.0",
description: "Manage your projects",
commands: {
create: {
description: "Create a new project",
args: {
name: "string"
},
flags: {
template: {
type: "string",
alias: "t",
default: "basic",
description: "Project template (basic, react, api)"
}
},
async run({ name, template }) {
const s = spinner(`Creating project "${name}"...`);
s.start();
const projectDir = join(process.cwd(), name);
await mkdir(projectDir, { recursive: true });
await mkdir(join(projectDir, "src"), { recursive: true });
const pkg = {
name,
version: "1.0.0",
type: "module",
scripts: {
dev: "tsx src/index.ts",
build: "tsup"
}
};
await writeFile(
join(projectDir, "package.json"),
JSON.stringify(pkg, null, 2)
);
await writeFile(
join(projectDir, "src/index.ts"),
`console.log("Hello from ${name}!");`
);
s.success(`Project "${name}" created!`);
console.log();
console.log(box([
colors.bold("Next steps:"),
"",
` cd ${name}`,
" pnpm install",
" pnpm dev"
], {
borderStyle: "rounded",
borderColor: "green",
padding: 1
}));
}
}
}
});
cli.run();
Test it:
pnpm tsx src/index.ts create my-app
Add project listing:
import { readdir, stat, readFile } from "fs/promises";
commands: {
// ... create command ...
list: {
description: "List all projects",
flags: {
dir: {
type: "string",
alias: "d",
default: ".",
description: "Directory to scan"
}
},
async run({ dir }) {
const { table } = await import("easycli-ui");
const entries = await readdir(dir, { withFileTypes: true });
const projects = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const pkgPath = join(dir, entry.name, "package.json");
try {
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
projects.push({
Name: entry.name,
Version: pkg.version || "-",
Type: pkg.type || "commonjs"
});
} catch {
continue;
}
}
if (projects.length === 0) {
console.log(colors.yellow("No projects found."));
return;
}
console.log(colors.bold(`Found ${projects.length} project(s):`));
console.log();
table(projects);
}
}
}
Add deployment with interactive confirmation:
commands: {
// ... other commands ...
deploy: {
description: "Deploy a project",
args: {
name: "string"
},
flags: {
env: {
type: "string",
alias: "e",
default: "staging",
description: "Target environment"
},
force: {
type: "boolean",
alias: "f",
description: "Skip confirmation"
}
},
async run({ name, env, force }, ctx) {
const { progress } = await import("easycli-ui");
if (!force) {
const proceed = await ctx.ask.confirm(
`Deploy "${name}" to ${env}?`
);
if (!proceed) {
console.log(colors.yellow("Deployment cancelled."));
return;
}
}
console.log();
console.log(colors.cyan(`Deploying ${name} to ${env}...`));
console.log();
const steps = ["Building", "Testing", "Uploading", "Verifying"];
const bar = progress(100);
for (let i = 0; i < steps.length; i++) {
console.log(colors.dim(` ${steps[i]}...`));
await new Promise(r => setTimeout(r, 500));
bar.update((i + 1) * 25);
}
bar.complete();
console.log();
console.log(colors.green(`Deployed ${name} to ${env}!`));
console.log();
console.log(box([
`Project: ${colors.cyan(name)}`,
`Environment: ${colors.yellow(env)}`,
`URL: https://${name}.${env}.example.com`
], {
title: " Deployment Complete ",
borderStyle: "double",
borderColor: "green"
}));
}
}
}
Add database management subcommands:
commands: {
// ... other commands ...
db: {
description: "Database commands",
commands: {
migrate: {
description: "Run database migrations",
flags: {
dry: { type: "boolean", description: "Dry run" }
},
async run({ dry }) {
const s = spinner("Running migrations...");
s.start();
await new Promise(r => setTimeout(r, 1000));
if (dry) {
s.success("Dry run complete - no changes made");
} else {
s.success("Migrations complete!");
}
}
},
seed: {
description: "Seed the database",
async run() {
const s = spinner("Seeding database...");
s.start();
await new Promise(r => setTimeout(r, 800));
s.success("Database seeded!");
}
},
reset: {
description: "Reset the database",
async run(_, ctx) {
const confirm = await ctx.ask.confirm(
"This will DELETE all data. Continue?"
);
if (!confirm) {
console.log(colors.yellow("Cancelled."));
return;
}
const s = spinner("Resetting database...");
s.start();
await new Promise(r => setTimeout(r, 1200));
s.success("Database reset!");
}
}
}
}
}
Usage:
project-cli db migrate --dry
project-cli db seed
project-cli db reset
Add rich error handling:
commands: {
deploy: {
// ...
async run({ name, env, force }, ctx) {
const projectDir = join(process.cwd(), name);
try {
await stat(projectDir);
} catch {
throw ctx.error(`Project "${name}" not found`, {
hint: `Run "project-cli create ${name}" first`,
exitCode: 1
});
}
if (env === "production" && !force) {
throw ctx.error("Production deployments require --force", {
hint: "Add --force flag to confirm production deployment"
});
}
// ... rest of deploy logic
}
}
}
Collect multiple values:
commands: {
build: {
description: "Build project files",
flags: {
file: {
type: "string",
array: true,
alias: "f",
description: "Files to build"
},
exclude: {
type: "string",
array: true,
description: "Patterns to exclude"
}
},
run({ file, exclude }) {
console.log("Building files:");
(file || []).forEach(f => console.log(` + ${f}`));
if (exclude?.length) {
console.log("Excluding:");
exclude.forEach(e => console.log(` - ${e}`));
}
}
}
}
Usage:
project-cli build --file src/a.ts --file src/b.ts --exclude "*.test.ts"
Add scripts to package.json:
{
"name": "project-cli",
"version": "1.0.0",
"type": "module",
"bin": {
"project-cli": "./dist/index.js"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsup",
"start": "node dist/index.js"
}
}
Build:
pnpm build
Test the built version:
node dist/index.js --help
Link globally for testing:
pnpm link --global
project-cli --help
Here's the complete src/index.ts:
import { defineCLI } from "easycli-core";
import { colors, spinner, box, progress, table } from "easycli-ui";
import { mkdir, writeFile, readdir, readFile, stat } from "fs/promises";
import { join } from "path";
const cli = defineCLI({
name: "project-cli",
version: "1.0.0",
description: "Manage your projects",
commands: {
create: {
description: "Create a new project",
args: { name: "string" },
flags: {
template: { type: "string", alias: "t", default: "basic" }
},
async run({ name, template }) {
const s = spinner(`Creating "${name}"...`);
s.start();
const dir = join(process.cwd(), name);
await mkdir(dir, { recursive: true });
await mkdir(join(dir, "src"), { recursive: true });
await writeFile(
join(dir, "package.json"),
JSON.stringify({ name, version: "1.0.0", type: "module" }, null, 2)
);
await writeFile(
join(dir, "src/index.ts"),
`console.log("Hello from ${name}!");`
);
s.success("Created!");
console.log(box([
`cd ${name}`,
"pnpm install",
"pnpm dev"
], { borderStyle: "rounded", borderColor: "green" }));
}
},
list: {
description: "List projects",
async run() {
const entries = await readdir(".", { withFileTypes: true });
const projects = [];
for (const e of entries) {
if (!e.isDirectory()) continue;
try {
const pkg = JSON.parse(
await readFile(join(e.name, "package.json"), "utf-8")
);
projects.push({ Name: e.name, Version: pkg.version || "-" });
} catch {}
}
if (projects.length === 0) {
console.log(colors.yellow("No projects found."));
return;
}
table(projects);
}
},
deploy: {
description: "Deploy a project",
args: { name: "string" },
flags: {
env: { type: "string", alias: "e", default: "staging" },
force: { type: "boolean", alias: "f" }
},
async run({ name, env, force }, ctx) {
if (!force) {
const ok = await ctx.ask.confirm(`Deploy to ${env}?`);
if (!ok) return;
}
const bar = progress(100);
for (let i = 0; i <= 100; i += 25) {
await new Promise(r => setTimeout(r, 300));
bar.update(i);
}
bar.complete();
console.log(colors.green(`Deployed ${name} to ${env}!`));
}
},
db: {
description: "Database commands",
commands: {
migrate: {
description: "Run migrations",
async run() {
const s = spinner("Migrating...");
s.start();
await new Promise(r => setTimeout(r, 800));
s.success("Done!");
}
},
seed: {
description: "Seed database",
async run() {
const s = spinner("Seeding...");
s.start();
await new Promise(r => setTimeout(r, 600));
s.success("Done!");
}
}
}
}
}
});
cli.run();
easycli-configCheck out the API Reference for complete documentation.