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
2 changes: 2 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ CLI tool for the Stellar Explain API.
npx @stellar-explain/cli <command>
```

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.
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function registerAccount(program: Command): void {
.command("account <address>")
.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) {
Expand All @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/explain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function registerExplain(program: Command): void {
verbose: boolean;
json: boolean;
cache: boolean;
color: boolean;
}>();

const type = detectInputType(input);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })));
});
}
4 changes: 2 additions & 2 deletions packages/cli/src/commands/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function registerTx(program: Command): void {
.command("tx <hash>")
.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) {
Expand All @@ -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);

Expand Down
34 changes: 4 additions & 30 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 <hash>')
.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 <id>')
.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 <url>", "API base URL")
Expand All @@ -59,6 +32,7 @@ program.parse(process.argv);
.option("--timeout <ms>", "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) => {
Expand All @@ -75,7 +49,7 @@ registerHealth(program);
registerBatch(program);
registerExplain(program);
registerWatch(program);
registerCompletion(program);
registerCompletionCommand(program);
registerConfigSet(program);
registerConfigGet(program);
registerConfigList(program);
Expand Down
11 changes: 3 additions & 8 deletions packages/cli/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -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<NodeJS.WriteStream, "isTTY"> = 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);
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/utils/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface ColorOptions {
noColor?: boolean;
stdout?: Pick<NodeJS.WriteStream, "isTTY">;
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;
}
31 changes: 31 additions & 0 deletions packages/cli/tests/color.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});