From 6657728316929142cc8e9821621cf6ef5b20a1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E4=BA=8E=E6=99=8F?= <3146347095@qq.com> Date: Sun, 28 Jun 2026 15:49:00 +0800 Subject: [PATCH] feat(cli): add no-color output controls --- packages/cli/README.md | 2 ++ packages/cli/src/commands/account.ts | 4 ++-- packages/cli/src/commands/explain.ts | 3 ++- packages/cli/src/commands/health.ts | 4 ++-- packages/cli/src/commands/tx.ts | 4 ++-- packages/cli/src/index.ts | 34 ++++------------------------ packages/cli/src/lib/config.ts | 11 +++------ packages/cli/src/utils/color.ts | 16 +++++++++++++ packages/cli/tests/color.test.ts | 31 +++++++++++++++++++++++++ 9 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 packages/cli/src/utils/color.ts create mode 100644 packages/cli/tests/color.test.ts diff --git a/packages/cli/README.md b/packages/cli/README.md index e482990..a69926f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -8,6 +8,8 @@ CLI tool for the Stellar Explain API. npx @stellar-explain/cli ``` +Disable ANSI colors with `--no-color` or by setting the `NO_COLOR` environment variable. + ## Configuration File You can create a `.stellar-explain.json` file in your project directory (or home directory) to set default options. The CLI will automatically pick it up — no flags required. diff --git a/packages/cli/src/commands/account.ts b/packages/cli/src/commands/account.ts index 63c58ba..98816ce 100644 --- a/packages/cli/src/commands/account.ts +++ b/packages/cli/src/commands/account.ts @@ -12,7 +12,7 @@ export function registerAccount(program: Command): void { .command("account
") .description("Explain a Stellar account") .action(async (address: string) => { - const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean; cache: boolean }>(); + const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean; cache: boolean; color: boolean }>(); validateAddress(address); if (opts.cache !== false) { @@ -25,7 +25,7 @@ export function registerAccount(program: Command): void { const client = createClient({ baseUrl: opts.url, timeout: opts.timeout, retries: opts.retries, verbose: opts.verbose }); const acc = await client.getAccount(address); - const useColor = shouldUseColorOutput() && !opts.json; + const useColor = shouldUseColorOutput({ noColor: opts.color === false }) && !opts.json; const output = opts.json ? JSON.stringify(acc, null, 2) : formatAccount(acc, useColor); console.log(output); diff --git a/packages/cli/src/commands/explain.ts b/packages/cli/src/commands/explain.ts index e19493f..06d9701 100644 --- a/packages/cli/src/commands/explain.ts +++ b/packages/cli/src/commands/explain.ts @@ -22,6 +22,7 @@ export function registerExplain(program: Command): void { verbose: boolean; json: boolean; cache: boolean; + color: boolean; }>(); const type = detectInputType(input); @@ -46,7 +47,7 @@ export function registerExplain(program: Command): void { verbose: opts.verbose, }); - const useColor = shouldUseColorOutput() && !opts.json; + const useColor = shouldUseColorOutput({ noColor: opts.color === false }) && !opts.json; if (type === "hash") { validateHash(input); diff --git a/packages/cli/src/commands/health.ts b/packages/cli/src/commands/health.ts index c853d60..8ecde10 100644 --- a/packages/cli/src/commands/health.ts +++ b/packages/cli/src/commands/health.ts @@ -8,10 +8,10 @@ export function registerHealth(program: Command): void { .command("health") .description("Check API health status") .action(async () => { - const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean }>(); + const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean; color: boolean }>(); const client = createClient({ baseUrl: opts.url, timeout: opts.timeout, retries: opts.retries, verbose: opts.verbose }); const h = await client.getHealth(); if (opts.json) { console.log(JSON.stringify(h, null, 2)); return; } - console.log(formatHealth(h, shouldUseColorOutput())); + console.log(formatHealth(h, shouldUseColorOutput({ noColor: opts.color === false }))); }); } diff --git a/packages/cli/src/commands/tx.ts b/packages/cli/src/commands/tx.ts index 026d25e..d26b262 100644 --- a/packages/cli/src/commands/tx.ts +++ b/packages/cli/src/commands/tx.ts @@ -12,7 +12,7 @@ export function registerTx(program: Command): void { .command("tx ") .description("Explain a Stellar transaction") .action(async (hash: string) => { - const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean; cache: boolean }>(); + const opts = program.opts<{ url: string; timeout: number; retries: number; verbose: boolean; json: boolean; cache: boolean; color: boolean }>(); validateHash(hash); if (opts.cache !== false) { @@ -25,7 +25,7 @@ export function registerTx(program: Command): void { const client = createClient({ baseUrl: opts.url, timeout: opts.timeout, retries: opts.retries, verbose: opts.verbose }); const tx = await client.getTransaction(hash); - const useColor = shouldUseColorOutput() && !opts.json; + const useColor = shouldUseColorOutput({ noColor: opts.color === false }) && !opts.json; const output = opts.json ? JSON.stringify(tx, null, 2) : formatTransaction(tx, useColor); console.log(output); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 168856b..523107e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -8,7 +8,7 @@ import { registerHealth } from "./commands/health.js"; import { registerBatch } from "./commands/batch.js"; import { registerExplain } from "./commands/explain.js"; import { registerWatch } from "./commands/watch.js"; -import { registerCompletion } from "./commands/completion.js"; +import { registerCompletionCommand } from "./commands/completion.js"; import { registerConfigSet } from "./commands/configSet.js"; import { registerConfigGet } from "./commands/configGet.js"; import { registerConfigList } from "./commands/configList.js"; @@ -19,36 +19,9 @@ import { readConfigFile } from "./lib/configFile.js"; import { getCliVersion } from "./lib/pkgVersion.js"; const program = new Command(); +const version = getCliVersion(); program - .name('stellar-explain') - .description('CLI for exploring and explaining Stellar transactions and accounts') - .version('0.1.0'); - -// ── Existing commands (stubs shown; replace with real implementations) ──────── - -program - .command('tx ') - .description('Look up and explain a Stellar transaction') - .action((hash: string) => { - addEntry('tx', hash); - // TODO: delegate to tx command handler - console.log(`Looking up transaction: ${hash}`); - }); - -program - .command('account ') - .description('Look up and explain a Stellar account') - .action((id: string) => { - addEntry('account', id); - // TODO: delegate to account command handler - console.log(`Looking up account: ${id}`); - }); - -registerHistoryCommand(program); // #441 / #442 / #443 -registerCompletionCommand(program); // #440 - -program.parse(process.argv); .name(BIN_NAME) .version(version) .option("--url ", "API base URL") @@ -59,6 +32,7 @@ program.parse(process.argv); .option("--timeout ", "Request timeout in ms", parseMs, 10000) .option("--verbose", "Log request details to stderr", false) .option("--no-cache", "Skip reading from and writing to the local response cache") + .option("--no-color", "Disable ANSI color output") .option("--json", "Output raw JSON", false); program.hook("preAction", (thisCommand) => { @@ -75,7 +49,7 @@ registerHealth(program); registerBatch(program); registerExplain(program); registerWatch(program); -registerCompletion(program); +registerCompletionCommand(program); registerConfigSet(program); registerConfigGet(program); registerConfigList(program); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 3b49aec..6efa06e 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -1,16 +1,11 @@ import { readConfigFile } from "./configFile"; +import { colorize, shouldUseColorOutput } from "../utils/color"; + +export { colorize, shouldUseColorOutput }; const DEFAULT_URL = "http://localhost:8080"; const DEFAULT_TIMEOUT = 5000; -export function shouldUseColorOutput(stdout: Pick = process.stdout): boolean { - return Boolean(stdout.isTTY) && process.env.NO_COLOR !== "1"; -} - -export function colorize(text: string, code: number, enabled: boolean): string { - return enabled ? `\u001b[${code}m${text}\u001b[0m` : text; -} - function isLocalhost(url: string): boolean { try { const { hostname } = new URL(url); diff --git a/packages/cli/src/utils/color.ts b/packages/cli/src/utils/color.ts new file mode 100644 index 0000000..fe6ff93 --- /dev/null +++ b/packages/cli/src/utils/color.ts @@ -0,0 +1,16 @@ +export interface ColorOptions { + noColor?: boolean; + stdout?: Pick; + env?: NodeJS.ProcessEnv; +} + +export function shouldUseColorOutput(options: ColorOptions = {}): boolean { + const stdout = options.stdout ?? process.stdout; + const env = options.env ?? process.env; + + return options.noColor !== true && env.NO_COLOR === undefined && Boolean(stdout.isTTY); +} + +export function colorize(text: string, code: number, enabled: boolean): string { + return enabled ? `\u001b[${code}m${text}\u001b[0m` : text; +} diff --git a/packages/cli/tests/color.test.ts b/packages/cli/tests/color.test.ts new file mode 100644 index 0000000..c8d06ba --- /dev/null +++ b/packages/cli/tests/color.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { colorize, shouldUseColorOutput } from "../src/utils/color.js"; + +describe("shouldUseColorOutput", () => { + it("uses color when stdout is a TTY", () => { + expect(shouldUseColorOutput({ stdout: { isTTY: true }, env: {} })).toBe(true); + }); + + it("disables color when --no-color is passed", () => { + expect(shouldUseColorOutput({ noColor: true, stdout: { isTTY: true }, env: {} })).toBe(false); + }); + + it("disables color when NO_COLOR is set", () => { + expect(shouldUseColorOutput({ stdout: { isTTY: true }, env: { NO_COLOR: "" } })).toBe(false); + expect(shouldUseColorOutput({ stdout: { isTTY: true }, env: { NO_COLOR: "1" } })).toBe(false); + }); + + it("does not use color when stdout is not a TTY", () => { + expect(shouldUseColorOutput({ stdout: { isTTY: false }, env: {} })).toBe(false); + }); +}); + +describe("colorize", () => { + it("wraps text with ANSI codes when enabled", () => { + expect(colorize("ok", 32, true)).toBe("\u001b[32mok\u001b[0m"); + }); + + it("returns plain text when disabled", () => { + expect(colorize("ok", 32, false)).toBe("ok"); + }); +});