diff --git a/apps/cli/package.json b/apps/cli/package.json index e37ede0..036b8e9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -21,11 +21,16 @@ "check": "bunx tsc -b" }, "dependencies": { + "@outfitter/cli": "^0.5.2", + "@outfitter/config": "^0.3.3", "@outfitter/firewatch-core": "workspace:*", "@outfitter/firewatch-mcp": "workspace:*", "@outfitter/firewatch-shared": "workspace:*", - "commander": "^13.0.0", - "ora": "^8.1.1" + "@outfitter/schema": "^0.2.2", + "@outfitter/types": "^0.2.3", + "commander": "^14.0.0", + "ora": "^8.1.1", + "zod": "^4.3.6" }, "devDependencies": { "@types/bun": "^1.3.6" diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 24d6dff..bb2096b 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,3 +1,4 @@ +import { createCLI } from "@outfitter/cli/command"; import { Command, Option } from "commander"; import { version } from "../package.json"; @@ -34,16 +35,19 @@ import { } from "./query-helpers"; import { emitAliasHint } from "./utils/alias-hint"; -const program = new Command(); +const cli = createCLI({ + name: "fw", + version, + description: + "GitHub PR activity logger with pure JSONL output for jq-based workflows", +}); + +const { program } = cli; program.enablePositionalOptions(); program.showSuggestionAfterError(true); +// Root command acts as query shorthand: `fw --since 24h` === `fw query --since 24h` program - .name("fw") - .description( - "GitHub PR activity logger with pure JSONL output for jq-based workflows" - ) - .version(version) .option("--pr [numbers]", "Filter to PR domain, optionally specific PRs") .option("--repo ", "Filter to specific repository", validateRepoSlug) .option("--all", "Include all cached repos") @@ -80,7 +84,6 @@ program .option("--summary", "Aggregate entries into per-PR summary") .option("-j, --jsonl", "Force structured output") .option("--no-jsonl", "Force human-readable output") - .addOption(new Option("--json").hideHelp()) .option("--debug", "Enable debug logging") .option("--no-color", "Disable color output") .addHelpText( @@ -129,16 +132,18 @@ Query options on root 'fw' are supported but 'fw query' is preferred.` } }); -program.addCommand(queryCommand); -program.addCommand(syncCommand); -program.addCommand(editCommand); -program.addCommand(ackCommand); -program.addCommand(closeCommand); -program.addCommand(commentCommand); -program.addCommand(approveCommand); -program.addCommand(rejectCommand); -program.addCommand(freezeCommand); -program.addCommand(unfreezeCommand); +// Register subcommands +cli + .register(queryCommand) + .register(syncCommand) + .register(editCommand) + .register(ackCommand) + .register(closeCommand) + .register(commentCommand) + .register(approveCommand) + .register(rejectCommand) + .register(freezeCommand) + .register(unfreezeCommand); // Hidden alias: `fw resolve` -> `fw close` const resolveCommand = new Command("resolve") @@ -156,16 +161,17 @@ const resolveCommand = new Command("resolve") }); program.addCommand(resolveCommand, { hidden: true }); -program.addCommand(replyCommand); -program.addCommand(listCommand); -program.addCommand(viewCommand); -program.addCommand(claudePluginCommand); -program.addCommand(statusCommand); -program.addCommand(configCommand); -program.addCommand(doctorCommand); -program.addCommand(schemaCommand); -program.addCommand(examplesCommand); -program.addCommand(mcpCommand); +cli + .register(replyCommand) + .register(listCommand) + .register(viewCommand) + .register(claudePluginCommand) + .register(statusCommand) + .register(configCommand) + .register(doctorCommand) + .register(schemaCommand) + .register(examplesCommand) + .register(mcpCommand); // Explicit help command since root action intercepts unknown args program @@ -192,5 +198,5 @@ program export { program }; export async function run(): Promise { - await program.parseAsync(); + await cli.parse(); } diff --git a/apps/cli/src/utils/tty.ts b/apps/cli/src/utils/tty.ts index 0c256a4..ca82845 100644 --- a/apps/cli/src/utils/tty.ts +++ b/apps/cli/src/utils/tty.ts @@ -3,7 +3,8 @@ * * Determines output format (structured vs human-readable) based on: * 1. Explicit --jsonl/--no-jsonl flags - * 2. FIREWATCH_JSONL (preferred) or FIREWATCH_JSON environment variable + * 2. Environment variables: OUTFITTER_JSON (set by --json via createCLI), + * FIREWATCH_JSONL, FIREWATCH_JSON * 3. TTY detection (non-TTY defaults to JSON for piping) */ @@ -15,31 +16,43 @@ export interface OutputModeOptions { /** * Determine if output should be structured based on: * 1. --jsonl/--no-jsonl flags (explicit) - * 2. FIREWATCH_JSONL (preferred) or FIREWATCH_JSON env var - * 3. TTY detection (non-TTY defaults to JSON) + * 2. --json flag (only when explicitly true, not default false) + * 3. OUTFITTER_JSON (set by createCLI --json), FIREWATCH_JSONL, FIREWATCH_JSON + * 4. TTY detection (non-TTY defaults to JSON) * - * Note: Commander's --no-jsonl sets options.jsonl = false (not noJsonl = true) + * Note: createCLI defines --json with default false, so options.json is always + * false when not passed. We only check for options.json === true (explicitly + * passed) and never treat false as "explicitly disabled". The --no-jsonl flag + * is the way to explicitly disable structured output. + * + * Commander's --no-jsonl sets options.jsonl = false (not noJsonl = true) */ export function shouldOutputJson( options: OutputModeOptions, defaultFormat?: "human" | "json" ): boolean { - // Explicit flag takes precedence - // --jsonl sets jsonl=true, --no-jsonl sets jsonl=false + // Explicit --jsonl/--no-jsonl flags take precedence if (options.jsonl === true) { return true; } if (options.jsonl === false) { return false; } + + // --json flag (only truthy check — false may be createCLI's default) if (options.json === true) { return true; } - if (options.json === false) { + + // Environment variables + // OUTFITTER_JSON is set by createCLI's --json preAction hook + if (process.env.OUTFITTER_JSON === "1") { + return true; + } + if (process.env.OUTFITTER_JSON === "0") { return false; } - - // Environment variable (prefer JSONL) + // Firewatch-specific env vars (legacy, still supported) if (process.env.FIREWATCH_JSONL === "1") { return true; }