Install
openclaw skills install ai-cli-builderActivate this skill whenever a user asks to build, design, or improve a command-line interface (CLI) tool. This includes: building CLIs in Node.js (Commander...
openclaw skills install ai-cli-builderBuild professional command-line tools that users love. Follow the sections relevant to your current task.
tool <command> [subcommand] [flags] [arguments]
# Examples:
git commit -m "message"
docker compose up -d
npm install --save-dev typescript
-v, -f, -n 5--verbose, --force, --count 5--verbose / --no-verbose--help, --version, --verbose, --quiet#!/usr/bin/env node
import { Command } from "commander";
import { version } from "../package.json";
const program = new Command();
program
.name("mytool")
.description("My awesome CLI tool")
.version(version);
program
.command("init")
.description("Initialize a new project")
.argument("<name>", "Project name")
.option("-t, --template <template>", "Template to use", "default")
.option("--no-git", "Skip git initialization")
.action(async (name, options) => {
console.log(`Creating project: ${name}`);
console.log(`Template: ${options.template}`);
console.log(`Git: ${options.git}`);
// ... implementation
});
program
.command("build")
.description("Build the project")
.option("-w, --watch", "Watch for changes")
.option("-o, --outdir <dir>", "Output directory", "dist")
.action(async (options) => {
// ... implementation
});
program.parse();
{
"name": "mytool",
"version": "1.0.0",
"bin": {
"mytool": "./dist/cli.js"
},
"type": "module",
"scripts": {
"build": "tsup src/cli.ts --format esm",
"dev": "tsx src/cli.ts"
}
}
import { input, select, confirm } from "@inquirer/prompts";
const name = await input({ message: "Project name:" });
const template = await select({
message: "Choose a template:",
choices: [
{ name: "Minimal", value: "minimal" },
{ name: "Full", value: "full" },
{ name: "API", value: "api" },
],
});
const proceed = await confirm({ message: "Create project?" });
import typer
from typing import Optional
from typing_extensions import Annotated
app = typer.Typer(help="My awesome CLI tool")
@app.command()
def init(
name: str,
template: Annotated[str, typer.Option("--template", "-t", help="Template")] = "default",
git: Annotated[bool, typer.Option("--git/--no-git", help="Init git")] = True,
):
"""Initialize a new project."""
typer.echo(f"Creating project: {name}")
typer.echo(f"Template: {template}")
@app.command()
def build(
watch: Annotated[bool, typer.Option("--watch", "-w")] = False,
outdir: Annotated[str, typer.Option("--outdir", "-o")] = "dist",
):
"""Build the project."""
if watch:
typer.echo("Watching for changes...")
if __name__ == "__main__":
app()
import click
@click.group()
@click.version_option()
def cli():
"""My awesome CLI tool."""
pass
@cli.command()
@click.argument("name")
@click.option("--template", "-t", default="default", help="Template to use")
@click.option("--git/--no-git", default=True, help="Initialize git")
def init(name, template, git):
"""Initialize a new project."""
click.echo(f"Creating project: {name}")
if __name__ == "__main__":
cli()
from rich.console import Console
from rich.table import Table
from rich.progress import track
console = Console()
# Styled output
console.print("[bold green]Success![/] Project created.")
console.print("[red]Error:[/] File not found.", style="bold")
# Tables
table = Table(title="Dependencies")
table.add_column("Package", style="cyan")
table.add_column("Version", style="green")
table.add_column("Status")
table.add_row("react", "18.3.0", "[green]Up to date[/]")
table.add_row("webpack", "5.91.0", "[yellow]Update available[/]")
console.print(table)
# Progress
for item in track(range(100), description="Processing..."):
process(item)
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "My awesome CLI tool",
}
var initCmd = &cobra.Command{
Use: "init <name>",
Short: "Initialize a new project",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
template, _ := cmd.Flags().GetString("template")
fmt.Printf("Creating project: %s (template: %s)\n", name, template)
return nil
},
}
func init() {
initCmd.Flags().StringP("template", "t", "default", "Template to use")
initCmd.Flags().Bool("no-git", false, "Skip git initialization")
rootCmd.AddCommand(initCmd)
}
func Execute() error {
return rootCmd.Execute()
}
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 { m.cursor-- }
case "down", "j":
if m.cursor < len(m.choices)-1 { m.cursor++ }
case "enter", " ":
if _, ok := m.selected[m.cursor]; ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "Select features:\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i { cursor = ">" }
checked := " "
if _, ok := m.selected[i]; ok { checked = "x" }
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
import chalk from "chalk";
console.log(chalk.green("Success!"));
console.log(chalk.red.bold("Error:"), "Something went wrong");
console.log(chalk.yellow("Warning:"), "Deprecated feature");
console.log(chalk.cyan("Info:"), "Processing 42 files...");
import ora from "ora";
const spinner = ora("Installing dependencies...").start();
await installDeps();
spinner.succeed("Dependencies installed");
// Or on failure:
spinner.fail("Installation failed");
import { table } from "table";
const data = [
["Name", "Version", "Status"],
["react", "18.3.0", "OK"],
["webpack", "5.91.0", "Update available"],
];
console.log(table(data));
Support both human and machine-readable output:
program
.option("--json", "Output as JSON")
.option("--quiet", "Minimal output");
function output(data: unknown, opts: { json?: boolean; quiet?: boolean }) {
if (opts.json) {
console.log(JSON.stringify(data, null, 2));
} else if (opts.quiet) {
console.log(data.id); // Just the essential value
} else {
// Pretty human-readable output
printTable(data);
}
}
import os from "os";
import path from "path";
function getConfigDir(appName: string): string {
const platform = process.platform;
if (platform === "win32") {
return path.join(process.env.APPDATA || "", appName);
}
if (platform === "darwin") {
return path.join(os.homedir(), "Library", "Application Support", appName);
}
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), appName);
}
.mytoolrc, mytool.config.js)~/.config/mytool/config.json)import { cosmiconfig } from "cosmiconfig";
const explorer = cosmiconfig("mytool");
const result = await explorer.search();
// Searches: .mytoolrc, .mytoolrc.json, .mytoolrc.yaml,
// mytool.config.js, package.json "mytool" field
# Generate completion script
mytool completion bash > /etc/bash_completion.d/mytool
# Or for user-local:
mytool completion bash >> ~/.bashrc
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate shell completion script",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
}
return fmt.Errorf("unsupported shell: %s", args[0])
},
})
npm publish # Publish to npm registry
npx mytool # Users can run without installing
pip install build twine
python -m build
twine upload dist/*
# .goreleaser.yaml
builds:
- env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
brews:
- repository:
owner: myorg
name: homebrew-tap
homepage: https://github.com/myorg/mytool
description: My awesome CLI tool
# Using pkg
npx pkg . --targets node20-linux-x64,node20-macos-x64,node20-win-x64
# Using bun
bun build ./src/cli.ts --compile --outfile mytool
class Mytool < Formula
desc "My awesome CLI tool"
homepage "https://github.com/myorg/mytool"
url "https://github.com/myorg/mytool/releases/download/v1.0.0/mytool-1.0.0.tar.gz"
sha256 "abc123..."
depends_on "node@20"
def install
bin.install "mytool"
end
test do
assert_match "1.0.0", shell_output("#{bin}/mytool --version")
end
end
import { execSync } from "child_process";
describe("mytool CLI", () => {
it("shows help", () => {
const output = execSync("node dist/cli.js --help").toString();
expect(output).toContain("My awesome CLI tool");
expect(output).toContain("init");
expect(output).toContain("build");
});
it("shows version", () => {
const output = execSync("node dist/cli.js --version").toString();
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
});
it("exits with code 2 on unknown command", () => {
try {
execSync("node dist/cli.js unknown 2>&1");
} catch (err) {
expect(err.status).toBe(2);
}
});
});
func TestInitCommand(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"init", "myproject"})
err := rootCmd.Execute()
assert.NoError(t, err)
assert.Contains(t, buf.String(), "Creating project: myproject")
}
--help — every command needs help text.$HOME.cat file | mytool process).--version — users need to know what they're running.