From c81af0a3d6112e35ae08bdf479d985d91d546b53 Mon Sep 17 00:00:00 2001 From: Ender <59183375+EnderOfWorlds007@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:29:16 +0200 Subject: [PATCH 1/6] feat(cli): add --json output to register, content, and pop commands Add structured JSON output support to all CLI commands that were missing it: register domain, register subname, content set, content view, pop set, and pop info. This follows the same pattern already used by the bulletin and lookup commands -- the --json flag suppresses human-readable output via maybeQuiet() and writes a single JSON line to stdout. The underlying contentHash functions now return result objects (domain, cid, contenthash, txHash) and the registration functions return result objects (label, domain, owner) so the CLI layer can serialize them. Errors are written as JSON to stderr when --json is active. --- packages/cli/src/cli/commands/content.ts | 69 ++++++++++----- packages/cli/src/cli/commands/pop.ts | 84 ++++++++++++++----- packages/cli/src/cli/commands/register.ts | 31 ++++++- .../cli/src/cli/commands/registerCommand.ts | 32 ++++++- packages/cli/src/commands/contentHash.ts | 35 +++++++- 5 files changed, 202 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/cli/commands/content.ts b/packages/cli/src/cli/commands/content.ts index 6a3f5c0..c4dbf9b 100644 --- a/packages/cli/src/cli/commands/content.ts +++ b/packages/cli/src/cli/commands/content.ts @@ -4,7 +4,8 @@ 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 { prepareReadOnlyContext, getJsonFlag } from "./lookup"; +import { maybeQuiet } from "./bulletin"; import { formatErrorMessage } from "../../utils/formatting"; import ora from "ora"; @@ -42,23 +43,38 @@ 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 (jsonOutput) { + process.stdout.write(JSON.stringify(result) + "\n"); + } else { + console.log(chalk.green("\n✓ Complete\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); + const errorMessage = formatErrorMessage(error); + if (jsonOutput) { + process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n"); + process.exit(1); + } + console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`)); process.exit(1); } }, @@ -66,10 +82,12 @@ export function attachContentCommands(root: Command) { 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,24 +95,37 @@ 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 (jsonOutput) { + process.stdout.write(JSON.stringify(result) + "\n"); + } else { + console.log(chalk.green("\n✓ Complete\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); + const errorMessage = formatErrorMessage(error); + if (jsonOutput) { + process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n"); + process.exit(1); + } + console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`)); process.exit(1); } }, diff --git a/packages/cli/src/cli/commands/pop.ts b/packages/cli/src/cli/commands/pop.ts index c5e5eb8..c004245 100644 --- a/packages/cli/src/cli/commands/pop.ts +++ b/packages/cli/src/cli/commands/pop.ts @@ -10,7 +10,8 @@ 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 { prepareReadOnlyContext, getJsonFlag } from "./lookup"; +import { maybeQuiet } from "./bulletin"; import type { Address } from "viem"; export type PopInfoResult = { @@ -61,50 +62,91 @@ 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 (jsonOutput) { + process.stdout.write( + JSON.stringify({ + ok: true, + status: ProofOfPersonhoodStatus[parsedStatus].toLowerCase(), + statusCode: parsedStatus, + }) + "\n", + ); + } else { + console.log(chalk.green("\n✓ PoP Status Updated\n")); + } process.exit(0); } catch (error) { - console.error(chalk.red(`\n✗ Error: ${formatErrorMessage(error)}\n`)); + const errorMessage = formatErrorMessage(error); + if (jsonOutput) { + process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n"); + process.exit(1); + } + console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`)); process.exit(1); } }, ); - 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 (jsonOutput) { + process.stdout.write( + JSON.stringify({ + substrate: info.substrate, + evm: info.evm, + status: ProofOfPersonhoodStatus[info.status].toLowerCase(), + statusCode: info.status, + }) + "\n", + ); + } else { + 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`)); + const errorMessage = formatErrorMessage(error); + if (jsonOutput) { + process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n"); + process.exit(1); + } + console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`)); process.exit(1); } }); 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..d4dab3f 100644 --- a/packages/cli/src/cli/commands/registerCommand.ts +++ b/packages/cli/src/cli/commands/registerCommand.ts @@ -5,6 +5,8 @@ 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 } from "./lookup"; +import { maybeQuiet } from "./bulletin"; export type RegisterActionOptions = RegistrationCommandOptions & { __statusProvided?: boolean; @@ -41,7 +43,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,10 +59,19 @@ export function attachRegisterCommand(root: Command) { throw new Error("Missing transfer destination: use --to "); } - await executeRegistration(merged); + const result = await maybeQuiet(jsonOutput, () => executeRegistration(merged)); + + if (jsonOutput) { + process.stdout.write(JSON.stringify(result) + "\n"); + } process.exit(0); } catch (error) { - console.error(`\n${chalk.red.bold("✗ Error:")} ${formatErrorMessage(error)}\n`); + const errorMessage = formatErrorMessage(error); + if (jsonOutput) { + process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n"); + process.exit(1); + } + console.error(`\n${chalk.red.bold("✗ Error:")} ${errorMessage}\n`); process.exit(1); } }); @@ -71,14 +84,25 @@ export function attachRegisterCommand(root: Command) { .requiredOption("-n, --name