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, yargs, oclif, Ink), Python (Click, Typer, argparse, Rich), Go (cobra, urfave/cli, bubbletea), or Rust (clap), argument parsing and validation, interactive prompts and TUI (terminal UI), output formatting (tables, colors, progress bars, spinners), configuration file management, shell completions, man pages, packaging and distribution (npm, PyPI, Homebrew, goreleaser, single-binary builds), plugin systems, and CLI testing strategies. Also activate for questions about terminal colors, ANSI escape codes, stdin/stdout piping, or cross-platform CLI behavior.
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.