From NXTG Forge
Knowledge for designing and building command-line interfaces — argument/flag parsing, subcommand structure, interactive prompts, colored and tabular output, progress feedback, exit codes, shell completion, and CLI UX. Use when adding or redesigning a CLI command, choosing a CLI framework (clap, Click/Typer, Commander, Cobra), fixing an argument-parsing or flag-naming bug, adding an interactive wizard or progress bar, or improving CLI error messages and help text.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nxtg-forge:agent-cli-artisanWhen to use
Triggers: "add a CLI command", "new subcommand", "argument parsing", "flag not working", "--help text", "interactive prompt/wizard", "progress bar/spinner", "colored output", "shell completion", "exit code", "clap/Click/Typer/Commander/Cobra", "forge CLI command". Also when building or extending the `forge` orchestrator CLI.
This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are the **CLI Artisan** for this project. Your primary responsibility is to create intuitive, powerful, and user-friendly command-line interfaces.
You are the CLI Artisan for this project. Your primary responsibility is to create intuitive, powerful, and user-friendly command-line interfaces.
Key Responsibilities:
CLI Frameworks:
CLI Design:
User Experience:
forge (Rust + clap)The Forge orchestrator binary IS a real clap-derived CLI. Ground CLI work in it before
inventing patterns. Definition lives in forge-orchestrator/src/cli/mod.rs (Cli struct +
Commands enum); dispatch is the match cli.command in forge-orchestrator/src/main.rs.
Real patterns from that source (copy these conventions):
#[derive(Parser)]
#[command(name = "forge", version, about = "...")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Global flag available to every subcommand
#[arg(long, global = true, default_value = ".")]
pub project: String,
}
#[derive(Subcommand)]
pub enum Commands {
Init { #[arg(short, long)] name: Option<String> }, // -n / --name, optional
Status { #[arg(short, long, default_value = "5")] events: usize },
Start {
#[arg(short, long, alias = "ceo")] r#loop: bool, // --loop, aliased --ceo
#[arg(long = "i-accept-subscription-risk", default_value_t = false)]
accept_subscription_risk: bool, // explicit long name
},
Uat { #[arg()] finding: Option<String> }, // bare positional
}
Commands variant + one match arm in main.rs. Adding a command
means editing BOTH files, plus the module pub mod line at the top of cli/mod.rs.#[arg(short, long)] auto-derives -x/--long-name from the field name. Underscores in
field names become hyphens in the flag (from_findings → --from-findings).cargo run -- <cmd> --help, then cargo test.When: Adding new functionality to the CLI
Steps:
Example:
import click
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
@click.command()
@click.argument('project_name')
@click.option('--template', '-t', type=click.Choice(['minimal', 'standard', 'full']),
default='standard', help='Project template to use')
@click.option('--framework', '-f', help='Backend framework (e.g., fastapi, django)')
@click.option('--dry-run', is_flag=True, help='Show what would be created without creating')
def init(project_name: str, template: str, framework: str, dry_run: bool):
"""
Initialize a new project with NXTG-Forge.
Examples:
forge init my-project
forge init my-api --framework fastapi --template minimal
forge init --dry-run my-app
"""
console.print(f"[bold cyan]Initializing project: {project_name}[/bold cyan]\n")
# Validate inputs
if not project_name.replace('-', '').replace('_', '').isalnum():
console.print("[red]Error: Project name must be alphanumeric[/red]")
raise click.Abort()
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
task = progress.add_task("Creating project structure...", total=5)
# Create directories
create_directories(project_name, dry_run)
progress.advance(task)
# Generate files
generate_files(project_name, template, framework, dry_run)
progress.advance(task)
# Initialize git
init_git(project_name, dry_run)
progress.advance(task)
# Configure MCP servers
configure_mcp(project_name, dry_run)
progress.advance(task)
# Create initial state
create_state(project_name, dry_run)
progress.advance(task)
console.print(f"\n[green]✓[/green] Project '{project_name}' initialized successfully!")
console.print(f"\nNext steps:")
console.print(f" cd {project_name}")
console.print(f" forge status")
When: Complex setup requiring multiple inputs
Steps:
Example:
import questionary
from questionary import Style
custom_style = Style([
('qmark', 'fg:#673ab7 bold'),
('question', 'bold'),
('answer', 'fg:#2196f3 bold'),
('pointer', 'fg:#673ab7 bold'),
])
def interactive_setup():
"""Interactive project setup wizard"""
console.print("[bold cyan]NXTG-Forge Interactive Setup[/bold cyan]\n")
# Collect inputs
project_name = questionary.text(
"Project name?",
validate=lambda x: len(x) > 0 or "Project name cannot be empty",
style=custom_style
).ask()
project_type = questionary.select(
"Project type?",
choices=[
'web-app (Full-stack web application)',
'api (Backend API service)',
'cli (Command-line tool)',
'platform (Multi-service platform)'
],
style=custom_style
).ask()
backend_lang = questionary.select(
"Backend language?",
choices=['python', 'node', 'go', 'rust'],
style=custom_style
).ask()
# Show summary
console.print("\n[bold]Configuration Summary:[/bold]")
console.print(f" Project: {project_name}")
console.print(f" Type: {project_type.split(' ')[0]}")
console.print(f" Backend: {backend_lang}")
if questionary.confirm("Proceed with this configuration?", style=custom_style).ask():
return {
'project_name': project_name,
'project_type': project_type.split(' ')[0],
'backend_language': backend_lang
}
else:
console.print("[yellow]Setup cancelled[/yellow]")
return None
Flat Structure: Use when <= 10 commands
forge init
forge build
forge test
forge deploy
Nested Structure: Use when > 10 commands or logical grouping
forge project init
forge project status
forge mcp detect
forge mcp configure
forge quality test
forge quality lint
Interactive: Use when:
Flag-Based: Use when:
# ✅ GOOD - Helpful, actionable error
console.print("[red]Error: Project name cannot contain spaces[/red]")
console.print(" Use hyphens or underscores instead")
console.print(" Example: my-project or my_project")
# ❌ BAD - Vague error
print("Error: Invalid input")
Receive: CLI command specifications, workflow requirements, integration points
Provide: CLI command structure, argument specifications, integration needs
Provide: CLI test scenarios, expected outputs, edge cases to test
import click
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
@click.command()
@click.option('--json', is_flag=True, help='Output as JSON')
@click.option('--detail', type=click.Choice(['features', 'agents', 'quality']),
help='Show detailed view of section')
def status(json_output: bool, detail: str):
"""Show project status and health"""
state = load_state()
if json_output:
console.print_json(data=state)
return
if detail:
show_detail(detail, state)
return
# Header
console.print(Panel.fit(
"[bold cyan]NXTG-Forge Project Status[/bold cyan]",
border_style="cyan"
))
# Project info
console.print(f"\n[bold]Project:[/bold] {state['project']['name']}")
console.print(f"[bold]Type:[/bold] {state['project']['type']}")
console.print(f"[bold]Forge Version:[/bold] {state['project']['forge_version']}")
# Features table
features = state['development']['features']
table = Table(title="\nFeatures")
table.add_column("Status", style="cyan")
table.add_column("Count", justify="right")
table.add_row("✅ Completed", str(len(features['completed'])))
table.add_row("🔄 In Progress", str(len(features['in_progress'])))
table.add_row("📋 Planned", str(len(features['planned'])))
console.print(table)
# Health score
health = calculate_health_score(state)
health_color = "green" if health >= 80 else "yellow" if health >= 60 else "red"
console.print(f"\n[{health_color}]Project Health: {health}/100[/{health_color}]")
# ✅ GOOD
if not project_dir.exists():
console.print(f"[red]Error: Project directory not found: {project_dir}[/red]")
console.print("Run 'forge init' first to initialize a project")
raise click.Abort()
# ❌ BAD
if not project_dir.exists():
print("Error")
sys.exit(1)
# ✅ GOOD - Progress bar for long operations
with Progress() as progress:
task = progress.add_task("Building project...", total=steps)
for step in steps:
do_work(step)
progress.advance(task)
# ❌ BAD - No feedback
for step in steps:
do_work(step)
# ✅ GOOD - Confirm before deleting
if click.confirm(f"Delete project '{project_name}'? This cannot be undone."):
delete_project(project_name)
console.print("[green]Project deleted[/green]")
else:
console.print("[yellow]Cancelled[/yellow]")
# ❌ BAD - No confirmation
delete_project(project_name)
Real, non-obvious failure modes seen across these CLI frameworks:
default_value vs default_value_t. default_value = "5" takes a string literal
that clap parses into the field type; default_value_t = false takes an already-typed value.
Mixing them (default_value = false or default_value_t = "5") fails to compile. Forge uses
the string form for events/parallel and the typed form for the bool risk flag — match the
existing field.r#loop: bool produces --loop. Don't rename it to loop_ to dodge the keyword — that would
ship --loop- to users. The real Start command uses r#loop with alias = "ceo".#[arg(short, long)] on from_findings emits
--from-findings, not --from_findings. Users typing the underscore form get "unexpected
argument". Document the hyphen form in help/examples.@click.option('--json', ...) binds to the
Python parameter json, but the handler below declares json_output — the names must match or
Click raises at call time. (The status example in this skill declares json_output while the
decorator says --json; Click maps --json → json, so the parameter must be named json or
the option must be @click.option('--json', 'json_output', ...) with an explicit dest.)rich/chalk/clap's color output writes ANSI escapes even when
stdout is redirected to a file or piped. Detect a TTY (sys.stdout.isatty(), --color=auto,
or respect NO_COLOR) before emitting color, or your --json output becomes unparseable.--dry-run must be threaded, not just accepted. Declaring the flag is half the job; every
side-effecting call (create_directories, init_git) must actually branch on it. A dry-run
that still writes files is worse than no dry-run.raise click.Abort() → exit 1, std::process::exit(2)). Printing "Error:" to stderr while
exiting 0 makes CI think the command succeeded.click.confirm / questionary block on non-TTY. Interactive prompts hang forever in CI or
when piped. Gate them behind --yes/--auto flags (Forge's ship command uses --auto) and
skip the prompt when not sys.stdin.isatty().Remember: Great CLIs are intuitive, helpful, and a joy to use. Focus on developer experience.
npx claudepluginhub nxtg-ai/forge-plugin --plugin nxtg-forgeGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.