From 95a9d0700668d125b92d54d4b93491d56007e7bd Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Mon, 16 Mar 2026 16:49:02 -0400 Subject: [PATCH 1/6] feat: add plaintext output mode with --output, -p, -j, GODADDY_CLI_OUTPUT Co-Authored-By: Claude --- src/cli-entry.ts | 62 ++++++++------ src/cli/services/cli-config.ts | 4 + src/cli/services/envelope-writer.ts | 124 +++++++++++++++++++++++----- 3 files changed, 147 insertions(+), 43 deletions(-) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index 0cca914..cb431ef 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -9,7 +9,10 @@ import * as Layer from "effect/Layer"; import packageJson from "../package.json"; import { mapRuntimeError, mapValidationError } from "./cli/agent/errors"; import type { NextAction } from "./cli/agent/types"; -import { makeCliConfigLayer } from "./cli/services/cli-config"; +import { + type OutputFormat, + makeCliConfigLayer, +} from "./cli/services/cli-config"; import { EnvelopeWriter, EnvelopeWriterLive, @@ -222,6 +225,7 @@ const API_SUBCOMMANDS = new Set(["list", "describe", "search", "call"]); const ROOT_FLAG_WITH_VALUE = new Set([ "--env", "-e", + "--output", "--log-level", "--completions", ]); @@ -229,6 +233,8 @@ const ROOT_BOOLEAN_FLAGS = new Set([ "--pretty", "--verbose", "-v", + "-p", + "-j", "--info", "--debug", "--help", @@ -386,6 +392,12 @@ export function runCli(rawArgv: ReadonlyArray): Promise { let verbosity = 0; let envOverride: Environment | null = null; + const envVarOutput = process.env.GODADDY_CLI_OUTPUT; + let outputFormat: OutputFormat = + envVarOutput === "plaintext" || envVarOutput === "json" + ? envVarOutput + : "json"; + const stripIndices = new Set(); for (let i = 0; i < normalized.length; i++) { const token = normalized[i]; @@ -403,34 +415,35 @@ export function runCli(rawArgv: ReadonlyArray): Promise { stripIndices.add(i + 1); i++; // skip value } + if (token === "-p") { + outputFormat = "plaintext"; + stripIndices.add(i); + } + if (token === "-j") { + outputFormat = "json"; + stripIndices.add(i); + } + if (token === "--output" && i + 1 < normalized.length) { + const val = normalized[i + 1]; + if (val === "plaintext" || val === "json") { + outputFormat = val; + } + stripIndices.add(i); + stripIndices.add(i + 1); + i++; // skip value + } + if (token.startsWith("--output=")) { + const val = token.slice("--output=".length); + if (val === "plaintext" || val === "json") { + outputFormat = val; + } + stripIndices.add(i); + } } const frameworkArgs = normalized.filter((_, i) => !stripIndices.has(i)); const rewrittenFrameworkArgs = rewriteLegacyApiEndpointArgs(frameworkArgs); - // Detect unsupported --output option before handing to framework - const outputIdx = normalized.indexOf("--output"); - if (outputIdx !== -1) { - const outputValue = normalized[outputIdx + 1] ?? "unknown"; - const commandStr = - `godaddy ${rawArgv.join(" ").replace(/\s+/g, " ")}`.trim(); - const envelope = { - ok: false, - command: commandStr, - error: { - message: `Unsupported option: --output ${outputValue}. All output is JSON by default.`, - code: "UNSUPPORTED_OPTION", - }, - fix: "Remove --output; all godaddy CLI output is JSON envelopes by default.", - next_actions: rootNextActions, - }; - process.stdout.write( - `${prettyPrint ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)}\n`, - ); - process.exitCode = 1; - return Promise.resolve(); - } - // Side-effects for compatibility with existing core/ code that reads globals if (verbosity > 0) { setVerbosityLevel(verbosity); @@ -441,6 +454,7 @@ export function runCli(rawArgv: ReadonlyArray): Promise { prettyPrint, verbosity, environmentOverride: envOverride, + outputFormat, }); const envelopeWriterLayer = EnvelopeWriterLive; diff --git a/src/cli/services/cli-config.ts b/src/cli/services/cli-config.ts index a99618b..d8267da 100644 --- a/src/cli/services/cli-config.ts +++ b/src/cli/services/cli-config.ts @@ -9,10 +9,13 @@ import type { Environment } from "../../core/environment"; * --env) so that every layer in the dependency graph can access them without * mutable module-level state. */ +export type OutputFormat = "json" | "plaintext"; + export interface CliConfigShape { readonly prettyPrint: boolean; readonly verbosity: number; // 0 = silent, 1 = basic, 2 = full readonly environmentOverride: Environment | null; + readonly outputFormat: OutputFormat; } export class CliConfig extends Context.Tag("CliConfig")< @@ -25,6 +28,7 @@ export const defaultCliConfig: CliConfigShape = { prettyPrint: false, verbosity: 0, environmentOverride: null, + outputFormat: "json", }; /** Build a layer from parsed global flags. */ diff --git a/src/cli/services/envelope-writer.ts b/src/cli/services/envelope-writer.ts index a905279..99a35ae 100644 --- a/src/cli/services/envelope-writer.ts +++ b/src/cli/services/envelope-writer.ts @@ -10,6 +10,76 @@ import type { } from "../agent/types"; import { CliConfig } from "./cli-config"; +// --------------------------------------------------------------------------- +// Plaintext renderer +// --------------------------------------------------------------------------- + +function renderValue(value: unknown, indent = 0): string { + const pad = " ".repeat(indent); + if (value === null || value === undefined) return ""; + if (typeof value !== "object") return String(value); + if (Array.isArray(value)) { + if (value.length === 0) return "(none)"; + return value + .map((item) => { + const rendered = renderValue(item, indent); + return `${pad}- ${rendered.trimStart()}`; + }) + .join("\n"); + } + return Object.entries(value as Record) + .filter(([, v]) => v !== undefined && v !== null) + .map(([k, v]) => { + const label = k.replace(/_/g, " "); + if (typeof v === "object" && v !== null) { + const inner = renderValue(v, indent + 1); + if (!inner) return null; + return `${pad}${label}:\n${inner}`; + } + return `${pad}${label}: ${v}`; + }) + .filter(Boolean) + .join("\n"); +} + +function renderSuccessPlaintext(result: unknown): string { + return renderValue(result); +} + +function renderErrorPlaintext( + error: { message: string; code: string }, + fix: string, +): string { + return `Error: ${error.message}\nFix: ${fix}`; +} + +function renderStreamEventPlaintext(event: StreamEvent): string { + switch (event.type) { + case "start": + return `Running ${event.command}...`; + case "step": { + const prefix = + event.status === "completed" + ? "[ok]" + : event.status === "failed" + ? "[FAILED]" + : "[ .. ]"; + const suffix = + event.status === "failed" && event.message ? `: ${event.message}` : ""; + return `${prefix} ${event.name}${suffix}`; + } + case "progress": { + const pct = event.percent !== undefined ? ` ${event.percent}%` : ""; + const msg = event.message ? ` ${event.message}` : ""; + return `${event.name}:${pct}${msg}`.trim(); + } + case "result": + return renderSuccessPlaintext(event.result); + case "error": + return renderErrorPlaintext(event.error, event.fix); + } +} + // --------------------------------------------------------------------------- // Service interface // --------------------------------------------------------------------------- @@ -80,7 +150,9 @@ export const EnvelopeWriterLive: Layer.Layer = const config = yield* CliConfig; const written = yield* Ref.make(false); - function serialize(value: unknown): string { + const plaintext = config.outputFormat === "plaintext"; + + function serializeJson(value: unknown): string { return config.prettyPrint ? JSON.stringify(value, null, 2) : JSON.stringify(value); @@ -106,7 +178,11 @@ export const EnvelopeWriterLive: Layer.Layer = next_actions: nextActions, }; if (!alreadyWritten) { - yield* writeLine(serialize(envelope)); + yield* writeLine( + plaintext + ? renderSuccessPlaintext(result) + : serializeJson(envelope), + ); yield* Ref.set(written, true); } return envelope; @@ -128,7 +204,11 @@ export const EnvelopeWriterLive: Layer.Layer = next_actions: nextActions, }; if (!alreadyWritten) { - yield* writeLine(serialize(envelope)); + yield* writeLine( + plaintext + ? renderErrorPlaintext(error, fix) + : serializeJson(envelope), + ); yield* Ref.set(written, true); yield* Effect.sync(() => { process.exitCode = 1; @@ -138,7 +218,9 @@ export const EnvelopeWriterLive: Layer.Layer = }); const emitStreamEvent = (event: StreamEvent): Effect.Effect => - writeLine(serialize(event)); + writeLine( + plaintext ? renderStreamEventPlaintext(event) : serializeJson(event), + ); const emitStreamResult = ( command: string, @@ -147,13 +229,15 @@ export const EnvelopeWriterLive: Layer.Layer = ): Effect.Effect => Effect.gen(function* () { yield* writeLine( - serialize({ - type: "result" as const, - ok: true, - command, - result, - next_actions: nextActions, - }), + plaintext + ? renderSuccessPlaintext(result) + : serializeJson({ + type: "result" as const, + ok: true, + command, + result, + next_actions: nextActions, + }), ); yield* Ref.set(written, true); }); @@ -166,14 +250,16 @@ export const EnvelopeWriterLive: Layer.Layer = ): Effect.Effect => Effect.gen(function* () { yield* writeLine( - serialize({ - type: "error" as const, - ok: false, - command, - error, - fix, - next_actions: nextActions, - }), + plaintext + ? renderErrorPlaintext(error, fix) + : serializeJson({ + type: "error" as const, + ok: false, + command, + error, + fix, + next_actions: nextActions, + }), ); yield* Ref.set(written, true); yield* Effect.sync(() => { From 163988f4317d390cf2fcf362280b8432302c9470 Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Tue, 17 Mar 2026 13:12:24 -0400 Subject: [PATCH 2/6] feat: use Effect's built-in help and log-level system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace custom help subcommands with Effect's --help (via `help` → `--help` positional rewrite) - Add Console layer that collapses 3+ blank lines in Effect's help output - Add Effect Logger that routes Effect.log* to stderr, formatted as JSON or plaintext based on outputFormat - Wire -v/-vv/-vvv to --log-level info/debug/trace so Effect's runtime logger respects verbosity - Remove makeHelpSubcommand from all command files and delete help.ts Co-Authored-By: Claude --- src/cli-entry.ts | 153 +++++++++++++++++++++++--------- src/cli/commands/actions.ts | 1 - src/cli/commands/api.ts | 1 - src/cli/commands/application.ts | 1 - src/cli/commands/auth.ts | 1 - src/cli/commands/env.ts | 1 - src/cli/commands/webhook.ts | 1 - 7 files changed, 109 insertions(+), 50 deletions(-) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index cb431ef..e57684d 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -3,8 +3,10 @@ import * as Command from "@effect/cli/Command"; import * as Options from "@effect/cli/Options"; import * as NodeContext from "@effect/platform-node/NodeContext"; +import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Logger from "effect/Logger"; import packageJson from "../package.json"; import { mapRuntimeError, mapValidationError } from "./cli/agent/errors"; @@ -53,7 +55,7 @@ const rootNextActions: NextAction[] = [ // --------------------------------------------------------------------------- const ROOT_DESCRIPTION = - "GoDaddy Developer Platform CLI - Agent-first JSON interface for platform operations"; + "GoDaddy Developer Platform CLI - Agent-first interface for platform operations"; interface CommandNode { id: string; @@ -186,7 +188,8 @@ const COMMAND_TREE: CommandNode = { // Global option pre-processing // // @effect/cli doesn't support -vv style stacking, so we normalize argv -// before handing it to the framework. +// before handing it to the framework. Verbosity flags are converted to +// --log-level=X so the Effect runtime logger respects the level too. // --------------------------------------------------------------------------- function isShortVerboseCluster(token: string): boolean { @@ -215,9 +218,10 @@ function normalizeVerbosityArgs(argv: readonly string[]): string[] { } retained.push(token); } - const norm = Math.min(verbosity, 2); - if (norm >= 2) return ["--debug", ...retained]; - if (norm === 1) return ["--verbose", ...retained]; + const norm = Math.min(verbosity, 3); + if (norm >= 3) return ["--log-level", "trace", ...retained]; + if (norm === 2) return ["--log-level", "debug", ...retained]; + if (norm === 1) return ["--log-level", "info", ...retained]; return retained; } @@ -231,12 +235,8 @@ const ROOT_FLAG_WITH_VALUE = new Set([ ]); const ROOT_BOOLEAN_FLAGS = new Set([ "--pretty", - "--verbose", - "-v", "-p", "-j", - "--info", - "--debug", "--help", "-h", "--version", @@ -285,6 +285,71 @@ function rewriteLegacyApiEndpointArgs(argv: readonly string[]): string[] { return rewritten; } +// --------------------------------------------------------------------------- +// Console override — collapses 3+ consecutive blank lines in help output +// --------------------------------------------------------------------------- + +function makeCleanConsole(): Console.Console { + const c = globalThis.console; + const s = Effect.sync; + return { + [Console.TypeId]: Console.TypeId as Console.TypeId, + log: (...args: ReadonlyArray) => + s(() => { + const text = args.map(String).join(" "); + process.stdout.write(`${text.replace(/\n{3,}/g, "\n\n")}\n`); + }), + error: (...args: ReadonlyArray) => + s(() => c.error(...(args as unknown[]))), + warn: (...args: ReadonlyArray) => + s(() => c.warn(...(args as unknown[]))), + info: (...args: ReadonlyArray) => + s(() => c.info(...(args as unknown[]))), + debug: (...args: ReadonlyArray) => + s(() => c.debug(...(args as unknown[]))), + trace: (...args: ReadonlyArray) => + s(() => c.trace(...(args as unknown[]))), + assert: (condition: boolean, ...args: ReadonlyArray) => + s(() => c.assert(condition, ...(args as unknown[]))), + clear: s(() => c.clear()), + count: (label?: string) => s(() => c.count(label)), + countReset: (label?: string) => s(() => c.countReset(label)), + dir: (item: unknown, options?: unknown) => s(() => c.dir(item, options)), + dirxml: (...args: ReadonlyArray) => + s(() => c.dirxml(...(args as unknown[]))), + group: (options?: { label?: string; collapsed?: boolean }) => + options?.collapsed + ? s(() => c.groupCollapsed(options.label)) + : s(() => c.group(options?.label)), + groupEnd: s(() => c.groupEnd()), + table: (tabularData: unknown, properties?: ReadonlyArray) => + s(() => c.table(tabularData, properties)), + time: (label?: string) => s(() => c.time(label)), + timeEnd: (label?: string) => s(() => c.timeEnd(label)), + timeLog: (label?: string, ...args: ReadonlyArray) => + s(() => c.timeLog(label, ...(args as unknown[]))), + unsafe: c, + }; +} + +// --------------------------------------------------------------------------- +// Effect logger — routes Effect.log* output to stderr with format awareness +// --------------------------------------------------------------------------- + +function makeEffectLogger(outputFormat: OutputFormat) { + return Logger.make(({ logLevel, message, date }) => { + const msg = typeof message === "string" ? message : String(message); + if (outputFormat === "json") { + process.stderr.write( + `${JSON.stringify({ ts: date.toISOString(), level: logLevel.label.toLowerCase(), msg })}\n`, + ); + } else { + const time = date.toISOString().substring(11, 23); + process.stderr.write(`[${time}] ${logLevel.label.padEnd(5)} ${msg}\n`); + } + }); +} + // --------------------------------------------------------------------------- // Root command // --------------------------------------------------------------------------- @@ -297,17 +362,15 @@ const rootCommand = Command.make( "Pretty-print JSON envelopes with 2-space indentation", ), ), - verbose: Options.boolean("verbose").pipe( - Options.withAlias("v"), + output: Options.text("output").pipe( Options.withDescription( - "Enable basic verbose output for HTTP requests and responses", + "Output format: json (default) or plaintext. Use -p for plaintext shorthand.", ), + Options.optional, ), - info: Options.boolean("info").pipe( - Options.withDescription("Enable basic verbose output (same as -v)"), - ), - debug: Options.boolean("debug").pipe( - Options.withDescription("Enable full verbose output (same as -vv)"), + json: Options.boolean("json").pipe( + Options.withAlias("j"), + Options.withDescription("Shorthand for --output=json"), ), env: Options.text("env").pipe( Options.withAlias("e"), @@ -320,7 +383,6 @@ const rootCommand = Command.make( (_config) => Effect.gen(function* () { const writer = yield* EnvelopeWriter; - // Reconstruct the command string from raw argv for traceability const rawArgs = process.argv.slice(2); const commandStr = rawArgs.length > 0 ? `godaddy ${rawArgs.join(" ")}` : "godaddy"; @@ -383,7 +445,8 @@ const cliRunner = Command.run(rootCommand, { // --------------------------------------------------------------------------- export function runCli(rawArgv: ReadonlyArray): Promise { - // Normalize -vv, --info, --debug before the framework sees them + // Normalize -vv, --info, --debug before the framework sees them. + // They become --log-level=X so Effect's runtime logger also respects the level. const normalized = normalizeVerbosityArgs(rawArgv); // Pre-parse global flags to build layers BEFORE Command.run, then strip @@ -405,15 +468,23 @@ export function runCli(rawArgv: ReadonlyArray): Promise { prettyPrint = true; stripIndices.add(i); } - if (token === "--verbose" || token === "-v") - verbosity = Math.max(verbosity, 1); - if (token === "--debug") verbosity = 2; - if (token === "--info") verbosity = Math.max(verbosity, 1); + if (token.startsWith("--log-level=")) { + const level = token.slice("--log-level=".length); + if (level === "info") verbosity = Math.max(verbosity, 1); + if (level === "debug") verbosity = Math.max(verbosity, 2); + if (level === "trace") verbosity = Math.max(verbosity, 3); + } + if (token === "--log-level" && i + 1 < normalized.length) { + const level = normalized[i + 1]; + if (level === "info") verbosity = Math.max(verbosity, 1); + if (level === "debug") verbosity = Math.max(verbosity, 2); + if (level === "trace") verbosity = Math.max(verbosity, 3); + } if ((token === "--env" || token === "-e") && i + 1 < normalized.length) { envOverride = validateEnvironment(normalized[i + 1]); stripIndices.add(i); stripIndices.add(i + 1); - i++; // skip value + i++; } if (token === "-p") { outputFormat = "plaintext"; @@ -430,7 +501,7 @@ export function runCli(rawArgv: ReadonlyArray): Promise { } stripIndices.add(i); stripIndices.add(i + 1); - i++; // skip value + i++; } if (token.startsWith("--output=")) { const val = token.slice("--output=".length); @@ -444,12 +515,17 @@ export function runCli(rawArgv: ReadonlyArray): Promise { const frameworkArgs = normalized.filter((_, i) => !stripIndices.has(i)); const rewrittenFrameworkArgs = rewriteLegacyApiEndpointArgs(frameworkArgs); - // Side-effects for compatibility with existing core/ code that reads globals + // Replace trailing positional 'help' with '--help' so Effect's built-in + // help handler fires. Only replaces the last token to avoid false positives + // on flag values (e.g. --label help). + const finalArgs = rewrittenFrameworkArgs.map((arg, i, arr) => + arg === "help" && i === arr.length - 1 ? "--help" : arg, + ); + if (verbosity > 0) { setVerbosityLevel(verbosity); - if (verbosity === 1) process.stderr.write("(verbose output enabled)\n"); - else process.stderr.write("(verbose output enabled: full details)\n"); } + const cliConfigLayer = makeCliConfigLayer({ prettyPrint, verbosity, @@ -459,30 +535,21 @@ export function runCli(rawArgv: ReadonlyArray): Promise { const envelopeWriterLayer = EnvelopeWriterLive; - // Full layer: platform (FileSystem, Path, Terminal) + custom services + CLI services const fullLayer = Layer.mergeAll( NodeContext.layer, NodeLiveLayer, cliConfigLayer, - ).pipe( - // EnvelopeWriter depends on CliConfig, so provide after merging - (base) => - Layer.merge(base, Layer.provide(envelopeWriterLayer, cliConfigLayer)), + Console.setConsole(makeCleanConsole()), + Logger.replace(Logger.defaultLogger, makeEffectLogger(outputFormat)), + ).pipe((base) => + Layer.merge(base, Layer.provide(envelopeWriterLayer, cliConfigLayer)), ); - const program = cliRunner( - // Command.run expects the full process.argv (node + script + args) - // We pass a synthetic prefix so the framework strips the first two. - // Use frameworkArgs (global flags already stripped) so @effect/cli - // doesn't reject them as unknown options on subcommands. - ["node", "godaddy", ...rewrittenFrameworkArgs], - ).pipe( - // Centralized error boundary: catch ALL errors, emit JSON envelope + const program = cliRunner(["node", "godaddy", ...finalArgs]).pipe( Effect.catchAll((error) => Effect.gen(function* () { const writer = yield* EnvelopeWriter; - // Check if it's an @effect/cli ValidationError (not a custom CliError) const CLI_VALIDATION_TAGS = new Set([ "CommandMismatch", "CorrectedFlag", @@ -517,7 +584,6 @@ export function runCli(rawArgv: ReadonlyArray): Promise { const cmdStr = `godaddy ${normalized.join(" ")}`.trim(); - // If this is a streaming command (--follow), emit stream error const isStreaming = normalized.includes("--follow"); if (isStreaming) { yield* writer.emitStreamError( @@ -548,7 +614,6 @@ export function runCli(rawArgv: ReadonlyArray): Promise { const args = process.argv.slice(2); runCli(args).catch((error) => { - // Last-resort catch for truly unexpected failures process.stderr.write(`Fatal: ${error}\n`); process.exitCode = 1; }); diff --git a/src/cli/commands/actions.ts b/src/cli/commands/actions.ts index 1f360df..2319748 100644 --- a/src/cli/commands/actions.ts +++ b/src/cli/commands/actions.ts @@ -9,7 +9,6 @@ import { loadActionInterface, } from "../schemas/actions/index"; import { EnvelopeWriter } from "../services/envelope-writer"; - // --------------------------------------------------------------------------- // Colocated next_actions // --------------------------------------------------------------------------- diff --git a/src/cli/commands/api.ts b/src/cli/commands/api.ts index 550ab9e..9e5a088 100644 --- a/src/cli/commands/api.ts +++ b/src/cli/commands/api.ts @@ -26,7 +26,6 @@ import { } from "../schemas/api/index"; import { CliConfig } from "../services/cli-config"; import { EnvelopeWriter } from "../services/envelope-writer"; - const VALID_METHODS: readonly HttpMethod[] = [ "GET", "POST", diff --git a/src/cli/commands/application.ts b/src/cli/commands/application.ts index bb0ab2c..73c940a 100644 --- a/src/cli/commands/application.ts +++ b/src/cli/commands/application.ts @@ -40,7 +40,6 @@ import { import { protectPayload, truncateList } from "../agent/truncation"; import type { NextAction } from "../agent/types"; import { EnvelopeWriter } from "../services/envelope-writer"; - // --------------------------------------------------------------------------- // Helpers (pure, no global state) // --------------------------------------------------------------------------- diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 7eb53fc..ee2d31a 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -9,7 +9,6 @@ import { import { envGetEffect } from "../../core/environment"; import type { NextAction } from "../agent/types"; import { EnvelopeWriter } from "../services/envelope-writer"; - // --------------------------------------------------------------------------- // Colocated next_actions // --------------------------------------------------------------------------- diff --git a/src/cli/commands/env.ts b/src/cli/commands/env.ts index ec792fe..7fdded7 100644 --- a/src/cli/commands/env.ts +++ b/src/cli/commands/env.ts @@ -11,7 +11,6 @@ import { } from "../../core/environment"; import type { NextAction } from "../agent/types"; import { EnvelopeWriter } from "../services/envelope-writer"; - // --------------------------------------------------------------------------- // Colocated next_actions // --------------------------------------------------------------------------- diff --git a/src/cli/commands/webhook.ts b/src/cli/commands/webhook.ts index 39ce992..5e1d6ea 100644 --- a/src/cli/commands/webhook.ts +++ b/src/cli/commands/webhook.ts @@ -4,7 +4,6 @@ import { webhookEventsEffect } from "../../core/webhooks"; import { truncateList } from "../agent/truncation"; import type { NextAction } from "../agent/types"; import { EnvelopeWriter } from "../services/envelope-writer"; - // --------------------------------------------------------------------------- // Colocated next_actions // --------------------------------------------------------------------------- From 4b0003397d94ee83116072208de31578afa49fb6 Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Tue, 17 Mar 2026 14:25:49 -0400 Subject: [PATCH 3/6] fix: strip Effect CLI boilerplate lines from help output Remove the verbose per-option metadata lines Effect's renderer injects ("A true or false value.", "This setting is optional.", "One of the following: ...") and collapse the orphaned blank lines that remain, producing tight two-line option blocks (name + description). Co-Authored-By: Claude --- src/cli-entry.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index e57684d..5b34f04 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -286,9 +286,33 @@ function rewriteLegacyApiEndpointArgs(argv: readonly string[]): string[] { } // --------------------------------------------------------------------------- -// Console override — collapses 3+ consecutive blank lines in help output +// Console override — strips boilerplate lines from Effect's help renderer +// and collapses orphaned blank lines that remain. // --------------------------------------------------------------------------- +// Effect's CLI renderer injects per-option metadata lines that add no value +// for users: type description ("A true or false value."), optionality status +// ("This setting is optional."), and enum listing ("One of the following: …"). +// Strip those lines, then collapse any runs of 3+ blank lines that result. +const HELP_NOISE_RE = + /^\s*(A true or false value\.|A user-defined piece of text\.|This setting is optional\.|This setting is required\.|One of the following:)/; + +function cleanHelpText(text: string): string { + return ( + text + .split("\n") + .filter((line) => !HELP_NOISE_RE.test(line)) + .join("\n") + // Collapse runs of 3+ blank lines that appear where noisy lines were removed. + .replace(/\n{3,}/g, "\n\n") + // Remove blank lines immediately before indented content (option descriptions, + // command table rows) so each entry is a tight two-line block. Blank lines + // between top-level entries (option names, section headers) are unaffected + // because they are not followed by whitespace. + .replace(/\n\n(?=\s)/g, "\n") + ); +} + function makeCleanConsole(): Console.Console { const c = globalThis.console; const s = Effect.sync; @@ -297,7 +321,7 @@ function makeCleanConsole(): Console.Console { log: (...args: ReadonlyArray) => s(() => { const text = args.map(String).join(" "); - process.stdout.write(`${text.replace(/\n{3,}/g, "\n\n")}\n`); + process.stdout.write(`${cleanHelpText(text)}\n`); }), error: (...args: ReadonlyArray) => s(() => c.error(...(args as unknown[]))), From 62c2376e740228c70ed09743340a3a5fa8cd97e5 Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Tue, 17 Mar 2026 16:11:50 -0400 Subject: [PATCH 4/6] feat: print log-level message to stderr when verbosity is set When -v, -vv, -vvv, or --log-level is passed, emit a human-friendly line to stderr before command output indicating the active log level. Co-Authored-By: Claude --- src/cli-entry.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index 5b34f04..532052b 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -548,6 +548,9 @@ export function runCli(rawArgv: ReadonlyArray): Promise { if (verbosity > 0) { setVerbosityLevel(verbosity); + const levelName = + verbosity >= 3 ? "trace" : verbosity === 2 ? "debug" : "info"; + process.stderr.write(`Log level has been set to: ${levelName}\n`); } const cliConfigLayer = makeCliConfigLayer({ From 56bbc00790b46149a8f416af01e57ce786fee049 Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Tue, 17 Mar 2026 17:05:11 -0400 Subject: [PATCH 5/6] fix: normalize --log-level=X and remove redundant help title - Expand --log-level=X (equals form) to two-token form in normalizeVerbosityArgs; @effect/cli only accepts the space-separated form, so the equals form was being passed through unparsed and producing different output than running without a log-level flag. Verbosity levels are absorbed into the counter to avoid duplication. - Remove the standalone command-name title line Effect renders at the top of help output (e.g. "godaddy\n\ngodaddy 0.2.3"), leaving just the version line as the first thing shown. Co-Authored-By: Claude --- src/cli-entry.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index 532052b..5b071de 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -216,6 +216,22 @@ function normalizeVerbosityArgs(argv: readonly string[]): string[] { verbosity += token.length - 1; continue; } + // Expand --log-level=X to two-token form; @effect/cli only accepts the + // space-separated form. Verbosity levels are absorbed into the counter so + // the end-of-function prepend deduplicates them. Other levels pass through. + if (token.startsWith("--log-level=")) { + const level = token.slice("--log-level=".length); + if (level === "info") { + verbosity = Math.max(verbosity, 1); + } else if (level === "debug") { + verbosity = Math.max(verbosity, 2); + } else if (level === "trace") { + verbosity = Math.max(verbosity, 3); + } else { + retained.push("--log-level", level); + } + continue; + } retained.push(token); } const norm = Math.min(verbosity, 3); @@ -300,6 +316,11 @@ const HELP_NOISE_RE = function cleanHelpText(text: string): string { return ( text + // Effect renders a standalone title line (just the command name, possibly + // with ANSI codes) before the "name version" line. Remove it so the version + // line is the first thing shown. The lookahead (?=\S+ \d) targets the + // "godaddy 0.2.3" pattern and keeps it anchored to the very first line. + .replace(/^.*\n\n(?=\S+ \d)/, "") .split("\n") .filter((line) => !HELP_NOISE_RE.test(line)) .join("\n") From 7999d25771ee612a86802e515514f0261183180d Mon Sep 17 00:00:00 2001 From: Jun-Dai Date: Wed, 18 Mar 2026 17:45:37 -0400 Subject: [PATCH 6/6] feat: structured JSON help output and --plaintext alias - Parse Effect's help text into {version, usage, description, options, commands} when output format is json, instead of a raw help string - Add --plaintext as the long-form alias for -p (mirrors --json/-j symmetry) Co-Authored-By: Claude --- src/cli-entry.ts | 119 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 7 deletions(-) diff --git a/src/cli-entry.ts b/src/cli-entry.ts index 5b071de..1ecb80c 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -252,6 +252,7 @@ const ROOT_FLAG_WITH_VALUE = new Set([ const ROOT_BOOLEAN_FLAGS = new Set([ "--pretty", "-p", + "--plaintext", "-j", "--help", "-h", @@ -334,7 +335,94 @@ function cleanHelpText(text: string): string { ); } -function makeCleanConsole(): Console.Console { +// ESC character (0x1B) cannot appear as a literal in regex; build dynamically. +const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); + +interface ParsedHelpOption { + flags: string; + description: string; +} + +interface ParsedHelpCommand { + command: string; + description: string; +} + +interface ParsedHelp { + version?: string; + usage?: string; + description?: string; + options: ParsedHelpOption[]; + commands: ParsedHelpCommand[]; +} + +function parseHelpText(plain: string): ParsedHelp { + const result: ParsedHelp = { options: [], commands: [] }; + const lines = plain.split("\n"); + + const firstLine = lines.find((l) => l.trim().length > 0) ?? ""; + const versionMatch = firstLine.match(/^\S+\s+(\S+)/); + if (versionMatch) result.version = versionMatch[1]; + + const sections = new Map(); + let currentSection: string | null = null; + let sectionLines: string[] = []; + + for (const line of lines) { + if (/^[A-Z][A-Z ]+$/.test(line.trim()) && line.trim().length > 1) { + if (currentSection !== null) sections.set(currentSection, sectionLines); + currentSection = line.trim(); + sectionLines = []; + } else if (currentSection !== null) { + sectionLines.push(line); + } + } + if (currentSection !== null) sections.set(currentSection, sectionLines); + + const usageLine = (sections.get("USAGE") ?? []).find((l) => + l.trim().startsWith("$"), + ); + if (usageLine) result.usage = usageLine.replace(/^\s*\$\s*/, "").trim(); + + const descLines = (sections.get("DESCRIPTION") ?? []).filter( + (l) => l.trim().length > 0, + ); + if (descLines.length > 0) result.description = descLines.join(" ").trim(); + + const optLines = sections.get("OPTIONS") ?? []; + for (let i = 0; i < optLines.length; i++) { + const line = optLines[i]; + if (line.trim().length > 0 && !/^\s/.test(line)) { + const flags = line.trim(); + let description = ""; + if (i + 1 < optLines.length && /^\s/.test(optLines[i + 1])) { + description = optLines[i + 1].trim(); + i++; + } + result.options.push({ flags, description }); + } + } + + for (const line of sections.get("COMMANDS") ?? []) { + const match = line.match(/^\s+-\s+(\S(?:.*\S)?)\s{2,}(\S.*)$/); + if (match) { + result.commands.push({ + command: match[1].trim(), + description: match[2].trim(), + }); + } + } + + return result; +} + +function makeCleanConsole( + outputFormat: OutputFormat, + prettyPrint: boolean, + finalArgs: string[], +): Console.Console { + const cmdStr = + finalArgs.length > 0 ? `godaddy ${finalArgs.join(" ")}` : "godaddy"; const c = globalThis.console; const s = Effect.sync; return { @@ -342,7 +430,22 @@ function makeCleanConsole(): Console.Console { log: (...args: ReadonlyArray) => s(() => { const text = args.map(String).join(" "); - process.stdout.write(`${cleanHelpText(text)}\n`); + const cleaned = cleanHelpText(text); + if (outputFormat === "json") { + const plain = cleaned.replace(ANSI_RE, ""); + const envelope = { + ok: true, + command: cmdStr, + result: parseHelpText(plain), + next_actions: [], + }; + const serialized = prettyPrint + ? JSON.stringify(envelope, null, 2) + : JSON.stringify(envelope); + process.stdout.write(`${serialized}\n`); + } else { + process.stdout.write(`${cleaned}\n`); + } }), error: (...args: ReadonlyArray) => s(() => c.error(...(args as unknown[]))), @@ -408,15 +511,17 @@ const rootCommand = Command.make( ), ), output: Options.text("output").pipe( - Options.withDescription( - "Output format: json (default) or plaintext. Use -p for plaintext shorthand.", - ), + Options.withDescription("Output format: json (default) or plaintext"), Options.optional, ), json: Options.boolean("json").pipe( Options.withAlias("j"), Options.withDescription("Shorthand for --output=json"), ), + plaintext: Options.boolean("plaintext").pipe( + Options.withAlias("p"), + Options.withDescription("Shorthand for --output=plaintext"), + ), env: Options.text("env").pipe( Options.withAlias("e"), Options.withDescription( @@ -531,7 +636,7 @@ export function runCli(rawArgv: ReadonlyArray): Promise { stripIndices.add(i + 1); i++; } - if (token === "-p") { + if (token === "-p" || token === "--plaintext") { outputFormat = "plaintext"; stripIndices.add(i); } @@ -587,7 +692,7 @@ export function runCli(rawArgv: ReadonlyArray): Promise { NodeContext.layer, NodeLiveLayer, cliConfigLayer, - Console.setConsole(makeCleanConsole()), + Console.setConsole(makeCleanConsole(outputFormat, prettyPrint, finalArgs)), Logger.replace(Logger.defaultLogger, makeEffectLogger(outputFormat)), ).pipe((base) => Layer.merge(base, Layer.provide(envelopeWriterLayer, cliConfigLayer)),