| name | outfitter-cli |
| version | 0.1.0 |
| description | Deep patterns for @outfitter/cli including output modes, pagination, input parsing, and Commander.js integration. Use when building CLI commands, handling output formatting, implementing pagination, or when "CLI output", "pagination", "exitWithError", or "@outfitter/cli" are mentioned. |
| allowed-tools | Read Write Edit Glob Grep |
CLI Patterns
Deep dive into @outfitter/cli patterns.
Creating a CLI
import { createCLI } from "@outfitter/cli";
const cli = createCLI({
name: "myapp",
version: "1.0.0",
description: "My CLI application",
});
cli.program.addCommand(listCommand);
cli.program.addCommand(getCommand);
cli.program.parse();
Command Builder
Type-safe command construction:
import { command } from "@outfitter/cli";
export const myCommand = command("my-command")
.description("What this command does")
.argument("<id>", "Required resource ID")
.argument("[name]", "Optional name")
.option("-l, --limit <n>", "Limit results", parseInt)
.option("-v, --verbose", "Enable verbose output")
.option("-t, --tags <tags...>", "Filter by tags")
.action(async ({ args, flags }) => {
})
.build();
Output Modes
Automatic Detection
import { output } from "@outfitter/cli";
await output(data);
Mode Priority
- Explicit
mode option
OUTFITTER_JSONL=1 env var
OUTFITTER_JSON=1 env var
OUTFITTER_JSON=0 forces human
- Default fallback: human mode
Forcing Modes
await output(data, { mode: "json" });
await output(data, { mode: "human" });
for await (const item of items) {
await output(item, { mode: "jsonl" });
}
await output(errorData, { stream: process.stderr });
Custom Formatters
await output(data, {
formatters: {
human: (data) => formatTable(data),
json: (data) => JSON.stringify(data, null, 2),
},
});
Error Handling
Exit with Error
import { exitWithError } from "@outfitter/cli";
const result = await handler(input, ctx);
if (result.isErr()) {
exitWithError(result.error);
}
Exit Code Mapping
| Category | Exit Code |
|---|
| validation | 1 |
| not_found | 2 |
| conflict | 3 |
| permission | 4 |
| timeout | 5 |
| rate_limit | 6 |
| network | 7 |
| internal | 8 |
| auth | 9 |
| cancelled | 130 |
Custom Error Output
import { formatError, getExitCode } from "@outfitter/cli";
if (result.isErr()) {
const formatted = formatError(result.error, { verbose: flags.verbose });
await output(formatted, { stream: process.stderr });
process.exit(getExitCode(result.error.category));
}
Pagination
Cursor State
Cursors persist in XDG state directory:
$XDG_STATE_HOME/{toolName}/cursors/{command}/cursor.json
Using Pagination
import { loadCursor, saveCursor, clearCursor } from "@outfitter/cli";
const options = { command: "list", toolName: "myapp" };
const state = loadCursor(options);
const results = await listItems({
cursor: state?.cursor,
limit: 20,
});
if (results.hasMore) {
saveCursor(results.nextCursor, options);
}
if (flags.reset) {
clearCursor(options);
}
Cursor Expiration
const state = loadCursor({
...options,
maxAgeMs: 30 * 60 * 1000,
});
Pagination Command Pattern
export const listCommand = command("list")
.option("-n, --next", "Continue from previous position")
.option("--reset", "Reset pagination cursor")
.option("-l, --limit <n>", "Results per page", parseInt, 20)
.action(async ({ flags }) => {
const paginationOpts = { command: "list", toolName: "myapp" };
if (flags.reset) {
clearCursor(paginationOpts);
console.log("Cursor reset");
return;
}
const cursor = flags.next ? loadCursor(paginationOpts)?.cursor : undefined;
const result = await listHandler({ cursor, limit: flags.limit }, ctx);
if (result.isErr()) {
exitWithError(result.error);
}
await output(result.value.items);
if (result.value.nextCursor) {
saveCursor(result.value.nextCursor, paginationOpts);
console.log("\nUse --next for more results");
}
})
.build();
Input Parsing
Stdin Reading
import { readStdin } from "@outfitter/cli";
const input = await readStdin();
Piped Detection
import { isPiped } from "@outfitter/cli";
if (isPiped()) {
const data = await readStdin();
} else {
}
Progress Indicators
import { createSpinner, createProgressBar } from "@outfitter/tui/render";
const spinner = createSpinner("Loading...");
spinner.start();
spinner.succeed("Done!");
const progress = createProgressBar({ total: 100 });
for (let i = 0; i <= 100; i++) {
progress.update(i);
}
progress.stop();
Best Practices
- Handler first — Business logic in handler, CLI is thin adapter
- Output modes — Support both human and JSON output
- Exit codes — Use
exitWithError for consistent codes
- Pagination — Use cursor state for
--next functionality
- Stdin support — Handle piped input gracefully
- TTY detection — Adapt behavior for interactive vs piped
Related Skills
stack:patterns — Handler contract
stack:scaffold — CLI command template
stack:debug — Troubleshooting CLI issues