diff --git a/packages/cli/src/cli/commands/auth.ts b/packages/cli/src/cli/commands/auth.ts index c077b42..50759d8 100644 --- a/packages/cli/src/cli/commands/auth.ts +++ b/packages/cli/src/cli/commands/auth.ts @@ -24,30 +24,12 @@ import type { const DEFAULT_ACCOUNT_POINTER_FILE = ".default"; const KEYSTORE_FILE_EXTENSION = ".json"; +import { getMergedOptions } from "./jsonHelpers"; + const MNEMONIC_PROMPT = "mnemonic"; const KEY_URI_PROMPT = "key-uri"; const AUTH_TYPE_UNKNOWN: AuthType = "unknown"; -function getMergedOptions(command: Command | undefined, fallback: CommandOptions): CommandOptions { - const mergedOptions: CommandOptions = { ...(fallback ?? {}) }; - - let currentCommand: Command | null | undefined = command?.parent; - while (currentCommand) { - if (typeof currentCommand.opts === "function") { - const parentOptions = currentCommand.opts() as CommandOptions; - for (const key in parentOptions) { - const optionKey = key as keyof CommandOptions; - if (!(optionKey in mergedOptions) && parentOptions[optionKey] !== undefined) { - mergedOptions[optionKey] = parentOptions[optionKey]; - } - } - } - currentCommand = currentCommand.parent; - } - - return mergedOptions; -} - function resolvePasswordForCreate(options: CommandOptions): Promise | string { const fromCli = String(options.password ?? "").trim(); if (fromCli) return fromCli; diff --git a/packages/cli/src/cli/commands/bulletin.ts b/packages/cli/src/cli/commands/bulletin.ts index 66a7a90..16d047f 100644 --- a/packages/cli/src/cli/commands/bulletin.ts +++ b/packages/cli/src/cli/commands/bulletin.ts @@ -10,7 +10,6 @@ import type { BulletinReporterMode, BulletinRetryHandler, BulletinUploadOptions, - CommandOptions, UploadProfiler, UploadProfilerOptions, UploadProfileReport, @@ -68,7 +67,7 @@ import { DEFAULT_AUTHORIZATION_TRANSACTIONS, DEFAULT_AUTHORIZATION_BYTES, } from "../../utils/constants"; -import { getJsonFlag } from "./lookup"; +import { getJsonFlag, getMergedOptions, withCapturedConsole } from "./jsonHelpers"; import { clampChunkSizeBytes } from "../../bulletin/store"; import { createCliReporter, @@ -79,28 +78,6 @@ import { type ResolvedReporterMode, } from "../reporter"; -function getMergedOptions( - command: Command | undefined, - fallback: BulletinUploadOptions, -): CommandOptions & BulletinUploadOptions { - const mergedOptions: any = { ...(fallback ?? {}) }; - - let currentCommand: Command | null | undefined = command?.parent; - while (currentCommand) { - if (typeof currentCommand.opts === "function") { - const parentOptions = currentCommand.opts(); - for (const key in parentOptions) { - if (!(key in mergedOptions) && parentOptions[key] !== undefined) { - mergedOptions[key] = parentOptions[key]; - } - } - } - currentCommand = currentCommand.parent; - } - - return mergedOptions; -} - const PROFILE_SAMPLE_INTERVAL_MS = 2_000; function addReporterOption(command: Command): Command { @@ -236,58 +213,6 @@ export function createUploadProfiler(options: UploadProfilerOptions): UploadProf }; } -export async function withCapturedConsole(callback: () => Promise): Promise { - const MAX_CAPTURED_ENTRIES = 400; - const captured: string[] = []; - const pushCaptured = (value: string) => { - captured.push(value); - if (captured.length > MAX_CAPTURED_ENTRIES) { - captured.splice(0, captured.length - MAX_CAPTURED_ENTRIES); - } - }; - const capture = (...args: any[]) => { - pushCaptured(args.map(String).join(" ")); - }; - const captureWrite = (chunk: any) => { - pushCaptured(String(chunk)); - return true; - }; - - const saved = { - log: console.log, - error: console.error, - warn: console.warn, - info: console.info, - stdoutWrite: process.stdout.write.bind(process.stdout), - stderrWrite: process.stderr.write.bind(process.stderr), - }; - - console.log = capture; - console.error = capture; - console.warn = capture; - console.info = capture; - process.stdout.write = captureWrite as any; - process.stderr.write = captureWrite as any; - - try { - return await callback(); - } catch (error) { - saved.error("[captured output before failure]\n" + captured.join("\n")); - throw error; - } finally { - console.log = saved.log; - console.error = saved.error; - console.warn = saved.warn; - console.info = saved.info; - process.stdout.write = saved.stdoutWrite; - process.stderr.write = saved.stderrWrite; - } -} - -export function maybeQuiet(jsonOutput: boolean, callback: () => Promise): Promise { - return jsonOutput ? withCapturedConsole(callback) : callback(); -} - async function withBulletinHumanOutput( reporterMode: ResolvedReporterMode, callback: () => Promise, diff --git a/packages/cli/src/cli/commands/content.ts b/packages/cli/src/cli/commands/content.ts index 6a3f5c0..4964d8b 100644 --- a/packages/cli/src/cli/commands/content.ts +++ b/packages/cli/src/cli/commands/content.ts @@ -1,11 +1,16 @@ import { Command } from "commander"; import chalk from "chalk"; -import type { CommandOptions } from "../../types/types"; import { viewDomainContentHash, setDomainContentHash } from "../../commands/contentHash"; import { addAuthOptions } from "./authOptions"; import { prepareContext } from "../context"; import { prepareReadOnlyContext } from "./lookup"; -import { formatErrorMessage } from "../../utils/formatting"; +import { + getMergedOptions, + getJsonFlag, + maybeQuiet, + emitJsonResult, + handleCommandError, +} from "./jsonHelpers"; import ora from "ora"; export interface ContentViewOptions { @@ -16,25 +21,6 @@ export interface ContentSetOptions { rpc?: string; } -function getMergedOptions(command: Command | undefined, fallback: T): CommandOptions & T { - const mergedOptions: any = { ...(fallback ?? {}) }; - - let currentCommand: Command | null | undefined = command?.parent; - while (currentCommand) { - if (typeof currentCommand.opts === "function") { - const parentOptions = currentCommand.opts(); - for (const key in parentOptions) { - if (!(key in mergedOptions) && parentOptions[key] !== undefined) { - mergedOptions[key] = parentOptions[key]; - } - } - } - currentCommand = currentCommand.parent; - } - - return mergedOptions; -} - export function attachContentCommands(root: Command) { const contentCommand = root.command("content").description("Manage domain content hashes"); @@ -42,34 +28,43 @@ export function attachContentCommands(root: Command) { const viewContentCommand = contentCommand .command("view ") - .description("View domain content hash"); + .description("View domain content hash") + .option("--json", "Output result as JSON (suppresses all other output)", false); addAuthOptions(viewContentCommand).action( async (name: string, options: ContentViewOptions, command: Command) => { + const jsonOutput = getJsonFlag(command); try { const mergedOptions = getMergedOptions(command, options); - const context = await prepareReadOnlyContext(mergedOptions as any); + const context = await maybeQuiet(jsonOutput, () => + prepareReadOnlyContext(mergedOptions as any), + ); - console.log(chalk.bold("\n▶ Content View\n")); + if (!jsonOutput) console.log(chalk.bold("\n▶ Content View\n")); const spinner = ora(); - await viewDomainContentHash(context.clientWrapper!, context.account.address, name, spinner); + const result = await maybeQuiet(jsonOutput, () => + viewDomainContentHash(context.clientWrapper!, context.account.address, name, spinner), + ); - console.log(chalk.green("\n✓ Complete\n")); + if (!emitJsonResult(jsonOutput, result)) { + console.log(chalk.green("\n✓ Complete\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); - process.exit(1); + handleCommandError(jsonOutput, error); } }, ); const setContentCommand = contentCommand .command("set ") - .description("Set domain content hash (IPFS CID)"); + .description("Set domain content hash (IPFS CID)") + .option("--json", "Output result as JSON (suppresses all other output)", false); addAuthOptions(setContentCommand).action( async (name: string, cid: string, options: ContentSetOptions, command: Command) => { + const jsonOutput = getJsonFlag(command); try { const mergedOptions = getMergedOptions(command, options); @@ -77,25 +72,30 @@ export function attachContentCommands(root: Command) { throw new Error("Cannot specify both --mnemonic and --key-uri"); } - const context = await prepareContext({ ...mergedOptions, useRevive: true }); + const context = await maybeQuiet(jsonOutput, () => + prepareContext({ ...mergedOptions, useRevive: true }), + ); - console.log(chalk.bold("\n▶ Content Set\n")); + if (!jsonOutput) console.log(chalk.bold("\n▶ Content Set\n")); const spinner = ora(); - await setDomainContentHash( - context.clientWrapper!, - context.substrateAddress, - context.signer, - name, - cid, - spinner, + const result = await maybeQuiet(jsonOutput, () => + setDomainContentHash( + context.clientWrapper!, + context.substrateAddress, + context.signer, + name, + cid, + spinner, + ), ); - console.log(chalk.green("\n✓ Complete\n")); + if (!emitJsonResult(jsonOutput, result)) { + console.log(chalk.green("\n✓ Complete\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); - process.exit(1); + handleCommandError(jsonOutput, error); } }, ); diff --git a/packages/cli/src/cli/commands/info.ts b/packages/cli/src/cli/commands/info.ts index 6aa519f..ca95615 100644 --- a/packages/cli/src/cli/commands/info.ts +++ b/packages/cli/src/cli/commands/info.ts @@ -11,33 +11,14 @@ import { resolveRpc, resolveKeystorePath } from "../env"; import { formatErrorMessage } from "../../utils/formatting"; import { resolveAuthSource, createAccountFromSource } from "../../commands/auth"; import { step } from "../ui"; -import { getJsonFlag, prepareReadOnlyContext } from "./lookup"; -import { maybeQuiet } from "./bulletin"; +import { prepareReadOnlyContext } from "./lookup"; +import { getJsonFlag, getMergedOptions, maybeQuiet } from "./jsonHelpers"; import { checkAccountMapped, checkWhitelisted, whitelistAddress, } from "../../commands/accountChecks"; -function getMergedOptions(command: Command | undefined, fallback: T): CommandOptions & T { - const mergedOptions: any = { ...(fallback ?? {}) }; - - let currentCommand: Command | null | undefined = command?.parent; - while (currentCommand) { - if (typeof currentCommand.opts === "function") { - const parentOptions = currentCommand.opts(); - for (const key in parentOptions) { - if (!(key in mergedOptions) && parentOptions[key] !== undefined) { - mergedOptions[key] = parentOptions[key]; - } - } - } - currentCommand = currentCommand.parent; - } - - return mergedOptions; -} - export function attachAccountCommands(root: Command) { const accountCommand = root.command("account").description("Account management utilities"); diff --git a/packages/cli/src/cli/commands/jsonHelpers.ts b/packages/cli/src/cli/commands/jsonHelpers.ts new file mode 100644 index 0000000..ad10e18 --- /dev/null +++ b/packages/cli/src/cli/commands/jsonHelpers.ts @@ -0,0 +1,144 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import { formatErrorMessage } from "../../utils/formatting"; +import type { CommandOptions } from "../../types/types"; + +/** + * Shared helpers for --json output and Commander option merging. + * + * JSON envelope convention: + * - Mutations (register, set, etc.) return { ok: true, ...fields } + * - Reads (view, info, lookup) return the data directly (no ok field) + * - Errors always return { error: "message" } on stderr with exit code 1 + */ + +/** + * Walk up the Commander parent chain and merge options from ancestor + * commands into a single object. Child options take precedence. + */ +export function getMergedOptions(command: Command | undefined, fallback: T): CommandOptions & T { + const mergedOptions: any = { ...(fallback ?? {}) }; + + let currentCommand: Command | null | undefined = command?.parent; + while (currentCommand) { + if (typeof currentCommand.opts === "function") { + const parentOptions = currentCommand.opts(); + for (const key in parentOptions) { + if (!(key in mergedOptions) && parentOptions[key] !== undefined) { + mergedOptions[key] = parentOptions[key]; + } + } + } + currentCommand = currentCommand.parent; + } + + return mergedOptions; +} + +/** + * Read the --json flag from a Commander command, checking optsWithGlobals + * first (covers parent-level flags), then local opts. + */ +export function getJsonFlag(command: any): boolean { + if (command && typeof command.optsWithGlobals === "function") { + const options = command.optsWithGlobals(); + if (typeof options?.json === "boolean") return options.json; + } + + const localOptions = + command && typeof command.opts === "function" ? (command.opts() as any) : undefined; + if (typeof localOptions?.json === "boolean") return localOptions.json; + + return false; +} + +/** + * Emit a JSON result to stdout if --json is active. + * Returns true if JSON was emitted, false otherwise (so the caller + * can provide human-readable output in the else branch). + */ +export function emitJsonResult(jsonOutput: boolean, result: unknown): boolean { + if (jsonOutput) { + console.log(JSON.stringify(result)); + return true; + } + return false; +} + +/** + * Handle a command error, formatting as JSON or human-readable depending + * on the --json flag. Always exits with code 1. + */ +export function handleCommandError(jsonOutput: boolean, error: unknown): never { + const message = formatErrorMessage(error); + + if (jsonOutput) { + console.error(JSON.stringify({ error: message })); + } else { + console.error(chalk.red(`\n✗ Error: ${message}\n`)); + } + + process.exit(1); +} + +/** + * Capture all console and stream output during a callback, suppressing it. + * On error, the captured output is dumped to stderr before re-throwing. + */ +export async function withCapturedConsole(callback: () => Promise): Promise { + const MAX_CAPTURED_ENTRIES = 400; + const captured: string[] = []; + const pushCaptured = (value: string) => { + captured.push(value); + if (captured.length > MAX_CAPTURED_ENTRIES) { + captured.splice(0, captured.length - MAX_CAPTURED_ENTRIES); + } + }; + const capture = (...args: any[]) => { + pushCaptured(args.map(String).join(" ")); + }; + const captureWrite = (chunk: any) => { + pushCaptured(String(chunk)); + return true; + }; + + const saved = { + log: console.log, + error: console.error, + warn: console.warn, + info: console.info, + stdoutWrite: process.stdout.write.bind(process.stdout), + stderrWrite: process.stderr.write.bind(process.stderr), + }; + + console.log = capture; + console.error = capture; + console.warn = capture; + console.info = capture; + process.stdout.write = captureWrite as any; + process.stderr.write = captureWrite as any; + + try { + return await callback(); + } catch (error) { + saved.error("[captured output before failure]\n" + captured.join("\n")); + throw error; + } finally { + console.log = saved.log; + console.error = saved.error; + console.warn = saved.warn; + console.info = saved.info; + process.stdout.write = saved.stdoutWrite; + process.stderr.write = saved.stderrWrite; + } +} + +/** + * When --json is active, suppress human-readable output by capturing it. + * Ora caches process.stderr (the object), not .write (the method). + * withCapturedConsole replaces .write on the object, so spinner + * output is captured as long as start/succeed/fail run inside maybeQuiet. + */ +export function maybeQuiet(jsonOutput: boolean, callback: () => Promise): Promise { + return jsonOutput ? withCapturedConsole(callback) : callback(); +} diff --git a/packages/cli/src/cli/commands/lookup.ts b/packages/cli/src/cli/commands/lookup.ts index 4b290f7..30fd6c3 100644 --- a/packages/cli/src/cli/commands/lookup.ts +++ b/packages/cli/src/cli/commands/lookup.ts @@ -24,7 +24,7 @@ import type { LookupActionOptions, ResolvedReadOnlyAuth, } from "../../types/types"; -import { maybeQuiet } from "./bulletin"; +import { getJsonFlag, maybeQuiet } from "./jsonHelpers"; import type { Address } from "viem"; function createClientWrapper(rpc: string) { @@ -32,19 +32,6 @@ function createClientWrapper(rpc: string) { return new ReviveClientWrapper(client as PolkadotApiClient); } -export function getJsonFlag(command: any): boolean { - if (command && typeof (command as any).optsWithGlobals === "function") { - const options = (command as any).optsWithGlobals(); - if (typeof options?.json === "boolean") return options.json; - } - - const localOptions = - command && typeof command.opts === "function" ? (command.opts() as any) : undefined; - if (typeof localOptions?.json === "boolean") return localOptions.json; - - return process.argv.includes("--json"); -} - function hasAnyAuthHint(opts: AuthSource): boolean { return ( (opts.mnemonic != null && String(opts.mnemonic).length > 0) || diff --git a/packages/cli/src/cli/commands/pop.ts b/packages/cli/src/cli/commands/pop.ts index c5e5eb8..bd8d234 100644 --- a/packages/cli/src/cli/commands/pop.ts +++ b/packages/cli/src/cli/commands/pop.ts @@ -8,9 +8,15 @@ import { parseProofOfPersonhoodStatus } from "../labels"; import { prepareContext } from "../context"; import { addAuthOptions } from "./authOptions"; import type { CommandOptions } from "../../types/types"; -import { formatErrorMessage } from "../../utils/formatting"; import { ProofOfPersonhoodStatus } from "../../types/types"; import { prepareReadOnlyContext } from "./lookup"; +import { + getMergedOptions, + getJsonFlag, + maybeQuiet, + emitJsonResult, + handleCommandError, +} from "./jsonHelpers"; import type { Address } from "viem"; export type PopInfoResult = { @@ -19,26 +25,6 @@ export type PopInfoResult = { status: ProofOfPersonhoodStatus; }; -function getMergedOptions(command: Command | undefined, fallback: CommandOptions): CommandOptions { - const mergedOptions: CommandOptions = { ...(fallback ?? {}) }; - - let currentCommand: Command | null | undefined = command?.parent; - while (currentCommand) { - if (typeof currentCommand.opts === "function") { - const parentOptions = currentCommand.opts() as CommandOptions; - for (const key in parentOptions) { - const optionKey = key as keyof CommandOptions; - if (!(optionKey in mergedOptions) && parentOptions[optionKey] !== undefined) { - mergedOptions[optionKey] = parentOptions[optionKey]; - } - } - } - currentCommand = currentCommand.parent; - } - - return mergedOptions; -} - async function readPopInfo(options: CommandOptions): Promise { const context = await prepareReadOnlyContext(options as any); const status = await getUserProofOfPersonhoodStatus( @@ -61,51 +47,76 @@ export function attachPopCommands(root: Command): void { const setPopCommand = popCommand .command("set ") - .description("Set ProofOfPersonhood status (none, lite, or full)"); + .description("Set ProofOfPersonhood status (none, lite, or full)") + .option("--json", "Output result as JSON (suppresses all other output)", false); addAuthOptions(setPopCommand).action( async (status: string, options: CommandOptions, command: Command) => { + const jsonOutput = getJsonFlag(command); try { const mergedOptions = getMergedOptions(command, options); - const context = await prepareContext(mergedOptions); + const context = await maybeQuiet(jsonOutput, () => prepareContext(mergedOptions)); const parsedStatus = parseProofOfPersonhoodStatus(status); - await setUserProofOfPersonhoodStatus( - context.clientWrapper!, - context.substrateAddress, - context.signer, - context.evmAddress!, - "", - parsedStatus, + await maybeQuiet(jsonOutput, () => + setUserProofOfPersonhoodStatus( + context.clientWrapper!, + context.substrateAddress, + context.signer, + context.evmAddress!, + "", + parsedStatus, + ), ); - console.log(chalk.green("\n✓ PoP Status Updated\n")); + if ( + !emitJsonResult(jsonOutput, { + ok: true, + status: ProofOfPersonhoodStatus[parsedStatus].toLowerCase(), + statusCode: parsedStatus, + }) + ) { + console.log(chalk.green("\n✓ PoP Status Updated\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); - process.exit(1); + handleCommandError(jsonOutput, error); } }, ); - const infoCommand = popCommand.command("info").description("Display ProofOfPersonhood status"); + const infoCommand = popCommand + .command("info") + .description("Display ProofOfPersonhood status") + .option("--json", "Output result as JSON (suppresses all other output)", false); addAuthOptions(infoCommand).action(async (options: CommandOptions, command: Command) => { + const jsonOutput = getJsonFlag(command); try { const mergedOptions = getMergedOptions(command, options); - const info = await readPopInfo(mergedOptions); - - console.log(chalk.bold("\n📋 ProofOfPersonhood Status\n")); - console.log(chalk.gray(" substrate: ") + chalk.white(info.substrate)); - console.log(chalk.gray(" evm: ") + chalk.white(info.evm)); - console.log(chalk.gray(" status: ") + chalk.white(ProofOfPersonhoodStatus[info.status])); - console.log(chalk.green("\n✓ PoP Status Retrieved\n")); + const info = await maybeQuiet(jsonOutput, () => readPopInfo(mergedOptions)); + + if ( + !emitJsonResult(jsonOutput, { + substrate: info.substrate, + evm: info.evm, + status: ProofOfPersonhoodStatus[info.status].toLowerCase(), + statusCode: info.status, + }) + ) { + console.log(chalk.bold("\n📋 ProofOfPersonhood Status\n")); + console.log(chalk.gray(" substrate: ") + chalk.white(info.substrate)); + console.log(chalk.gray(" evm: ") + chalk.white(info.evm)); + console.log( + chalk.gray(" status: ") + chalk.white(ProofOfPersonhoodStatus[info.status]), + ); + console.log(chalk.green("\n✓ PoP Status Retrieved\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); - process.exit(1); + handleCommandError(jsonOutput, error); } }); } diff --git a/packages/cli/src/cli/commands/register.ts b/packages/cli/src/cli/commands/register.ts index 00cdeac..13e964c 100644 --- a/packages/cli/src/cli/commands/register.ts +++ b/packages/cli/src/cli/commands/register.ts @@ -77,7 +77,24 @@ export function isValidTransferDestination(destination: string): boolean { ); } -export async function executeRegistration(options: Partial = {}) { +export type DomainRegistrationResult = { + ok: true; + label: string; + domain: string; + owner: string; +}; + +export type SubnameRegistrationResult = { + ok: true; + label: string; + parent: string; + domain: string; + owner: string; +}; + +export async function executeRegistration( + options: Partial = {}, +): Promise { if (options.mnemonic && options.keyUri) { throw new Error("Cannot specify both --mnemonic and --key-uri"); } @@ -152,11 +169,13 @@ export async function executeRegistration(options: Partial, -): Promise { +): Promise { if (!options.name) { throw new Error("Missing subname: use --name "); } @@ -196,6 +215,14 @@ export async function executeSubnameRegistration( console.log(`${chalk.bold.green(" ✓ Subname Registered ")}`); console.log(`${chalk.bold.green("═══════════════════════════════════════")}\n`); console.log(chalk.gray(" Domain: ") + chalk.cyan(fullName)); + + return { + ok: true as const, + label: sublabel, + parent: parentLabel, + domain: fullName, + owner: ownerAddress, + }; } async function executeGovernanceRegistration( diff --git a/packages/cli/src/cli/commands/registerCommand.ts b/packages/cli/src/cli/commands/registerCommand.ts index a524963..11e6109 100644 --- a/packages/cli/src/cli/commands/registerCommand.ts +++ b/packages/cli/src/cli/commands/registerCommand.ts @@ -1,10 +1,9 @@ import { Command } from "commander"; -import chalk from "chalk"; import { executeRegistration, executeSubnameRegistration } from "./register"; import { type RegistrationCommandOptions } from "../../types/types"; import { addAuthOptions, getAuthOptions } from "./authOptions"; -import { formatErrorMessage } from "../../utils/formatting"; import { DEFAULT_COMMITMENT_BUFFER_SECONDS } from "../../utils/constants"; +import { getJsonFlag, maybeQuiet, emitJsonResult, handleCommandError } from "./jsonHelpers"; export type RegisterActionOptions = RegistrationCommandOptions & { __statusProvided?: boolean; @@ -41,7 +40,9 @@ export function attachRegisterCommand(root: Command) { "--cb, --commitment-buffer ", `Extra seconds to wait after minCommitmentAge (default: ${DEFAULT_COMMITMENT_BUFFER_SECONDS}, env: DOTNS_COMMITMENT_BUFFER)`, ) + .option("--json", "Output result as JSON (suppresses all other output)", false) .action(async (options: RegistrationCommandOptions, cmd: any) => { + const jsonOutput = getJsonFlag(cmd); try { const merged = { ...options, ...getAuthOptions(cmd) } as RegisterActionOptions; @@ -55,11 +56,12 @@ export function attachRegisterCommand(root: Command) { throw new Error("Missing transfer destination: use --to "); } - await executeRegistration(merged); + const result = await maybeQuiet(jsonOutput, () => executeRegistration(merged)); + + emitJsonResult(jsonOutput, result); process.exit(0); } catch (error) { - console.error(`\n${chalk.red.bold("✗ Error:")} ${formatErrorMessage(error)}\n`); - process.exit(1); + handleCommandError(jsonOutput, error); } }); @@ -71,15 +73,18 @@ export function attachRegisterCommand(root: Command) { .requiredOption("-n, --name