diff --git a/src/cli.ts b/src/cli.ts index f4bbeef..3fcf3fe 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -45,6 +45,8 @@ program String(cfg.threshold), ) .option("--scoring ", "Scoring method: copeland (default) or weighted", "copeland") + .option("--no-color", "Disable colored output") + .option("--output-format ", "Output format: text (default) or json", "text") .option("--verbose", "Show detailed output from each agent") .action(async (promptArg: string | undefined, opts) => { const prompt = resolvePrompt(promptArg, opts.file); @@ -79,6 +81,17 @@ program process.exit(1); } + // --no-color: commander parses --no-color as opts.color === false + if (opts.color === false) { + process.env.NO_COLOR = "1"; + } + + const validFormats = ["text", "json"]; + if (!validFormats.includes(opts.outputFormat)) { + console.error(`Error: --output-format must be one of: ${validFormats.join(", ")}`); + process.exit(1); + } + const knownModels = ["sonnet", "opus", "haiku"]; if (!knownModels.includes(opts.model) && !opts.model.startsWith("claude-")) { console.warn( @@ -97,6 +110,7 @@ program runner: opts.runner, scoring: opts.scoring, verbose: opts.verbose ?? false, + outputFormat: opts.outputFormat, }); }); diff --git a/src/commands/run.test.ts b/src/commands/run.test.ts index 0e972c3..64318ce 100644 --- a/src/commands/run.test.ts +++ b/src/commands/run.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { describe, it } from "node:test"; +import { afterEach, describe, it } from "node:test"; import type { RunOptions } from "../types.js"; import { makeResultFilename, preflightValidation } from "./run.js"; @@ -13,6 +13,7 @@ function makeOpts(overrides: Partial = {}): RunOptions { threshold: 0.3, verbose: false, scoring: "weighted", + outputFormat: "text", ...overrides, }; } @@ -78,3 +79,41 @@ describe("makeResultFilename", () => { assert.equal(filename, "run-2026-03-28T18-09-50-100Z.json"); }); }); + +describe("outputFormat option", () => { + it("accepts text as default output format", () => { + const opts = makeOpts({ outputFormat: "text" }); + assert.equal(opts.outputFormat, "text"); + }); + + it("accepts json output format", () => { + const opts = makeOpts({ outputFormat: "json" }); + assert.equal(opts.outputFormat, "json"); + }); +}); + +describe("NO_COLOR environment variable", () => { + const originalNoColor = process.env.NO_COLOR; + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR; + } else { + process.env.NO_COLOR = originalNoColor; + } + }); + + it("can be set to disable colors", () => { + process.env.NO_COLOR = "1"; + assert.equal(process.env.NO_COLOR, "1"); + }); + + it("picocolors respects NO_COLOR", async () => { + process.env.NO_COLOR = "1"; + // picocolors checks NO_COLOR at import time, but its createColors + // function can be used to verify the behavior + const pc = await import("picocolors"); + const colors = pc.createColors(false); + assert.equal(colors.bold("test"), "test"); + }); +}); diff --git a/src/commands/run.ts b/src/commands/run.ts index 0f71c09..7bdd962 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -153,8 +153,12 @@ export async function run(opts: RunOptions): Promise { }; // Display results - displayResults(result); - displayApplyInstructions(result); + if (opts.outputFormat === "json") { + console.log(JSON.stringify(result)); + } else { + displayResults(result); + displayApplyInstructions(result); + } // Save result to .thinktank/ await saveResult(result); diff --git a/src/types.ts b/src/types.ts index 760430c..5faede6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,7 @@ export interface RunOptions { verbose: boolean; runner?: string; scoring: "weighted" | "copeland"; + outputFormat: "text" | "json"; } export interface AgentResult {