Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
62 changes: 34 additions & 28 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createCLI } from "@outfitter/cli/command";
import { Command, Option } from "commander";

import { version } from "../package.json";
Expand Down Expand Up @@ -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 <name>", "Filter to specific repository", validateRepoSlug)
.option("--all", "Include all cached repos")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -192,5 +198,5 @@ program
export { program };

export async function run(): Promise<void> {
await program.parseAsync();
await cli.parse();
}
31 changes: 22 additions & 9 deletions apps/cli/src/utils/tty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/

Expand All @@ -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;
}
Expand Down