Skip to content

feat(cli): add --json output to register, content, and pop commands#76

Merged
EnderOfWorlds007 merged 6 commits into
mainfrom
feat/json-output-all-commands
Apr 16, 2026
Merged

feat(cli): add --json output to register, content, and pop commands#76
EnderOfWorlds007 merged 6 commits into
mainfrom
feat/json-output-all-commands

Conversation

@EnderOfWorlds007
Copy link
Copy Markdown
Contributor

Summary

Adds --json flag to 6 commands that lacked it (content view, content set, pop set, pop info, register domain, register subname). The bulletin and lookup commands already had --json.

Replaces #74 (fork PR → upstream branch to fix CI permissions).

Changes from #74

  • Switched JSON output from process.stdout.write to console.log (and process.stderr.write to console.error) to align with the existing --json pattern in lookup.ts. The test harness only intercepts console.log/console.error, so the previous approach wouldn't have been captured in tests.
  • Added code comment explaining why ora spinners are safe inside maybeQuiet: withCapturedConsole replaces .write on the process.stderr object, and ora caches the stream object reference (not the method), so spinner output is captured when the spinner lifecycle runs inside maybeQuiet.
  • Added unit tests (8 tests): --json flag in help output, JSON error formatting for pre-chain validation errors.
  • Added integration tests (7 tests): happy-path JSON output for all 6 commands plus error path for content set.

JSON output shapes

dotns register domain -n <label> --json
→ { "ok": true, "label": "...", "domain": "....dot", "owner": "0x..." }

dotns register subname -n <sub> -p <parent> --json
→ { "ok": true, "label": "...", "parent": "...", "domain": "sub.parent.dot", "owner": "0x..." }

dotns content view <name> --json
→ { "domain": "....dot", "contenthash": "0x...", "cid": "bafy..." }

dotns content set <name> <cid> --json
→ { "ok": true, "domain": "....dot", "cid": "bafy...", "contenthash": "0x...", "txHash": "0x..." }

dotns pop set <status> --json
→ { "ok": true, "status": "full", "statusCode": 2 }

dotns pop info --json
→ { "substrate": "5D...", "evm": "0x...", "status": "proofofpersonhoodfull", "statusCode": 2 }

Errors (all commands):
→ stderr: { "error": "..." }
→ exit code 1

Reviewer feedback addressed

  1. Console leak in contentHash.ts — Verified that withCapturedConsole intercepts process.stderr.write (not just console.log), so ora spinner output is captured when maybeQuiet wraps the call scope. Added code comment explaining the mechanism.
  2. Automated tests — Added 8 unit tests and 7 integration tests covering all --json commands.
  3. Push to upstream — This PR comes from an in-repo branch, fixing the fork CI permissions issue.

Test plan

  • Unit tests pass: bun test tests/unit/ (142 tests, 0 failures)
  • Integration tests pass against Paseo testnet
  • CI passes (no longer fork-blocked)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 3, 2026

CI Summary

Check Result
Lint Passed
Format Passed
Typecheck Passed
Build Passed
Release Passed
Deploy Example Passed
PR Title Passed
Labels Passed
Test Passed - 142 passed, 0 failed

Release - Passed

Test this PR

Download artifact (GitHub CLI required):

gh run download 24500185212 -n cli-release-0.0.0-pr.76 -R paritytech/dotns-sdk

Install globally:

npm install -g ./parity-dotns-cli-0.0.0-pr.76.tgz

Verify:

dotns --help
Deploy Example — Passed
Stage Status
✓ Site validation Site validated
✓ Deploy Deployed (cached)
Property Value
Domain pr76.dotns-example-site.dot
CID bafybeicx555prsudvmqsdrap7keoilryrudwycdx3ody2rejwqwjwp52u4 (cached)
URL (dot.li) https://dot.li/pr76.dotns-example-site.dot
URL (direct) https://pr76.dotns-example-site.dot
Duration 37s

View run

Labels

pkg: cli, type: test

Test - Passed

142 passed, 0 failed across 142 tests.

View run

$ bun test tests/unit/
bun test v1.2.6 (8ebd5d53)

::group::tests/unit/text/textHelp.test.ts:
(pass) text --help lists subcommands and auth options [15.00ms]
(pass) text view --help shows name and key arguments [3.00ms]
(pass) text set --help shows name, key, and value arguments [2.00ms]

::endgroup::

::group::tests/unit/content/contentJson.test.ts:
(pass) content view --help shows --json option [3.00ms]
(pass) content set --help shows --json option [2.00ms]
(pass) content set --json emits JSON error when both mnemonic and key-uri provided [3.00ms]

::endgroup::

::group::tests/unit/content/contentHelp.test.ts:
result:  {
  exitCode: 0,
  standardOutput: "Usage: dotns content [options] [command]\n\nManage domain content hashes\n\nOptions:\n  --rpc <wsUrl>               WebSocket RPC endpoint (env: DOTNS_RPC)\n  --keystore-path <path>      Keystore path (env: DOTNS_KEYSTORE_PATH)\n  --min-balance <pas>         Minimum balance in PAS (env:\n                              DOTNS_MIN_BALANCE_PAS)\n  --account <name>            Keystore account name (default: keystore default)\n  --password <pw>             Keystore password (env: DOTNS_KEYSTORE_PASSWORD)\n  -m, --mnemonic <phrase>     BIP39 mnemonic phrase (env: DOTNS_MNEMONIC)\n  -k, --key-uri <uri>         Substrate key URI (env: DOTNS_KEY_URI)\n  -h, --help                  display help for command\n\nCommands:\n  view [options] <name>       View domain content hash\n  set [options] <name> <cid>  Set domain content hash (IPFS CID)\n  help [command]              display help for command\n",
  standardError: "",
  combinedOutput: "Usage: dotns content [options] [command]\n\nManage domain content hashes\n\nOptions:\n  --rpc <wsUrl>               WebSocket RPC endpoint (env: DOTNS_RPC)\n  --keystore-path <path>      Keystore path (env: DOTNS_KEYSTORE_PATH)\n  --min-balance <pas>         Minimum balance in PAS (env:\n                              DOTNS_MIN_BALANCE_PAS)\n  --account <name>            Keystore account name (default: keystore default)\n  --password <pw>             Keystore password (env: DOTNS_KEYSTORE_PASSWORD)\n  -m, --mnemonic <phrase>     BIP39 mnemonic phrase (env: DOTNS_MNEMONIC)\n  -k, --key-uri <uri>         Substrate key URI (env: DOTNS_KEY_URI)\n  -h, --help                  display help for command\n\nCommands:\n  view [options] <name>       View domain content hash\n  set [options] <name> <cid>  Set domain content hash (IPFS CID)\n  help [command]              display help for command\n",
}
(pass) content --help lists subcommands and auth options [2.00ms]
(pass) content view --help shows name argument and options [1.00ms]
(pass) content set --help shows name and cid arguments [1.00ms]

::endgroup::

::group::tests/unit/auth/auth.test.ts:
(pass) auth set creates keystore and stores multiple accounts [318.01ms]
(pass) auth set accepts account names with special characters [451.01ms]
(pass) auth list reports missing keystore [3.00ms]
(pass) auth list shows all accounts and auth types [297.01ms]
(pass) auth use switches default account [464.01ms]
(pass) auth remove last account clears default [77.00ms]
(pass) auth remove preserves remaining accounts and reassigns default [235.01ms]
(pass) auth clear deletes all accounts [93.00ms]

::endgroup::

::group::tests/unit/auth/authHelp.test.ts:
(pass) root help lists auth command [2.00ms]
(pass) auth help shows options and subcommands [2.00ms]
(pass) auth set help shows all options [1.00ms]
(pass) auth list help shows options [1.00ms]
(pass) auth use help shows options [2.00ms]
(pass) auth remove help shows options [1.00ms]
(pass) auth clear help shows options [2.00ms]
(pass) auth parses keystore-path option [1.00ms]
(pass) auth parses password option [2.00ms]
(pass) auth set parses account option [2.00ms]
(pass) auth set parses mnemonic option [2.00ms]
(pass) auth set parses key-uri option [1.00ms]
(pass) auth help command shows help [2.00ms]
(pass) auth help set shows set command help [1.00ms]
(pass) auth help list shows list command help [2.00ms]
(pass) auth help use shows use command help [1.00ms]
(pass) auth help remove shows remove command help [1.00ms]
(pass) auth help clear shows clear command help [1.00ms]

::endgroup::

::group::tests/unit/auth/authRevert.test.ts:
(pass) auth set rejects account name with forward slash [2.00ms]
(pass) auth set rejects account name with backslash [1.00ms]
(pass) auth set rejects account name that is just a dot [2.00ms]
(pass) auth set rejects account name that is double dots [1.00ms]
(pass) auth set rejects account name starting with dot [2.00ms]
(pass) auth set rejects account name ending with dot [2.00ms]
(pass) auth set rejects account name with special characters [11.00ms]
(pass) auth set rejects account name that is too long [1.00ms]
(pass) auth use rejects non-existent account [75.00ms]
(pass) auth remove rejects non-existent account [93.00ms]

::endgroup::

::group::tests/unit/store/storeHelp.test.ts:
(pass) store --help lists subcommands [2.00ms]
(pass) store info --help shows auth options [1.00ms]
(pass) store list --help shows options [1.00ms]
(pass) store get --help shows key argument [2.00ms]
(pass) store set --help shows key and value arguments with auth [1.00ms]
(pass) store delete --help shows key argument with auth [1.00ms]
(pass) store check --help shows address argument [1.00ms]
(pass) store authorize --help shows address argument with auth [1.00ms]
(pass) store unauthorize --help shows address argument [1.00ms]
(pass) store authorize-controller --help shows address argument [1.00ms]
(pass) store unauthorize-controller --help shows address argument [1.00ms]
(pass) store ensure-auth --help shows description and auth options [1.00ms]

::endgroup::

::group::tests/unit/pop/popJson.test.ts:
(pass) pop set --help shows --json option [1.00ms]
(pass) pop info --help shows --json option [2.00ms]

::endgroup::

::group::tests/unit/pop/setPopHelp.test.ts:
(pass) root help lists pop command [2.00ms]
(pass) pop help shows commands and description [1.00ms]
(pass) pop help shows auth options [1.00ms]
(pass) pop set help shows status parameter [1.00ms]
(pass) pop set help shows auth options [2.00ms]
(pass) pop info help shows description [1.00ms]
(pass) pop info help shows auth options [1.00ms]
(pass) pop set parses rpc option at pop level [1.00ms]
(pass) pop set parses rpc option at set level [1.00ms]
(pass) pop set parses keystore-path option [1.00ms]
(pass) pop set parses account option at pop level [1.00ms]
(pass) pop set parses account option at set level [1.00ms]
(pass) pop set parses password option [1.00ms]
(pass) pop set parses mnemonic option [1.00ms]
(pass) pop set parses key-uri option [1.00ms]
(pass) pop info parses auth options at pop level [1.00ms]
(pass) pop info parses auth options at info level [1.00ms]
(pass) pop set parses mixed options across levels [1.00ms]
(pass) pop help command shows pop help [2.00ms]
(pass) pop help set shows set help [1.00ms]
(pass) pop help info shows info help [1.00ms]

::endgroup::

::group::tests/unit/account/accountHelp.test.ts:
(pass) account --help lists subcommands including is-mapped, is-whitelisted, whitelist [2.00ms]
(pass) account is-mapped --help shows address argument and --json [1.00ms]
(pass) account is-whitelisted --help shows address argument and --json [1.00ms]
(pass) account whitelist --help shows address argument, --remove, and --json [1.00ms]
(pass) account is alias works for is-mapped [1.00ms]
(pass) account iw alias works for is-whitelisted [1.00ms]

::endgroup::

::group::tests/unit/cli/reporter.test.ts:
(pass) cli reporter > stream reporter emits durable progress lines
(pass) cli reporter > withConsoleToStderr redirects console and stdout writes [1.00ms]

::endgroup::

::group::tests/unit/bulletin/uploadManifest.test.ts:
(pass) upload manifest resume behavior > returns stale manifest when fingerprint does not match [2.00ms]
(pass) upload manifest resume behavior > deduplicates completed blocks by index

::endgroup::

::group::tests/unit/bulletin/uploadProfiling.test.ts:
(pass) upload profiler > writes schema-complete profile report with peak aggregation [17.00ms]
(pass) upload profiler > default profile path is deterministic for a given fingerprint

::endgroup::

::group::tests/unit/bulletin/bulletinHelp.test.ts:
(pass) root help lists bulletin command [2.00ms]
(pass) bulletin help shows commands and description [1.00ms]
(pass) bulletin upload help shows all options [1.00ms]
(pass) bulletin upload help shows default values [2.00ms]
(pass) bulletin authorize help shows all options [1.00ms]
(pass) bulletin authorize help shows default values [2.00ms]
(pass) bulletin history help shows options [1.00ms]
(pass) bulletin history:remove help shows usage [1.00ms]
(pass) bulletin history:clear help shows description [2.00ms]
(pass) bulletin help command shows bulletin help [1.00ms]
(pass) bulletin help upload shows upload help [1.00ms]
(pass) bulletin help authorize shows authorize help [1.00ms]
(pass) bulletin status help shows all options [1.00ms]
(pass) bulletin help status shows status help [1.00ms]
(pass) bulletin list alias works [1.00ms]
(pass) bulletin verify help shows usage [1.00ms]
(pass) bulletin help verify shows verify help [2.00ms]

::endgroup::

::group::tests/unit/register/registerHelp.test.ts:
(pass) root help lists register command [2.00ms]
(pass) register help shows subcommands [1.00ms]
(pass) register domain help shows options [1.00ms]
(pass) register subname help shows options [1.00ms]
(pass) register domain parses status none [1.00ms]
(pass) register domain parses status lite [1.00ms]
(pass) register domain parses status full [1.00ms]
(pass) register domain parses reverse flag [1.00ms]
(pass) register domain parses governance flag [2.00ms]
(pass) register domain parses owner option [1.00ms]
(pass) register domain parses transfer with destination [1.00ms]
(pass) register domain parses account option [1.00ms]
(pass) register domain parses keystore-path option [1.00ms]
(pass) register domain parses password option [1.00ms]
(pass) register domain parses mnemonic option [1.00ms]
(pass) register domain parses key-uri option [1.00ms]
(pass) register domain parses commitment-buffer option [1.00ms]
(pass) register domain parses commitment-buffer alias --cb [2.00ms]
(pass) register subname parses name and parent [1.00ms]
(pass) register subname parses owner option [1.00ms]
(pass) getCommitmentBufferSeconds defaults to 6 when env is not set
(pass) getCommitmentBufferSeconds reads from DOTNS_COMMITMENT_BUFFER env variable
(pass) COMMITMENT_POLL_INTERVAL_MS is 2000
(pass) COMMITMENT_POLL_TIMEOUT_MS is 30000

::endgroup::

::group::tests/unit/register/registerJson.test.ts:
(pass) register domain --help shows --json option [1.00ms]
(pass) register subname --help shows --json option [1.00ms]
(pass) register domain --json emits JSON error when --transfer without --to [1.00ms]

::endgroup::

::group::tests/unit/lookup/lookupHelp.test.ts:
(pass) lookup --help lists subcommands and auth options [2.00ms]
(pass) lookup name --help shows label argument and options [1.00ms]
(pass) lookup owner-of --help shows label argument and options [1.00ms]
(pass) lookup transfer --help shows label argument and destination option [1.00ms]
(pass) lookup transfer parses destination at transfer level [1.00ms]
(pass) lookup transfer parses auth options at lookup level [1.00ms]

::endgroup::

 142 pass
 0 fail
 533 expect() calls
Ran 142 tests across 17 files. [2.70s]

@EnderOfWorlds007
Copy link
Copy Markdown
Contributor Author

@andrew-ifrita @sphamjoli — This replaces #74 with the reviewer feedback addressed:

Re: contentHash.ts console leak (@andrew-ifrita) — Investigated this thoroughly. withCapturedConsole already intercepts process.stderr.write (not just console.log), and ora caches process.stderr as a stream object reference, then calls this.stream.write(). Since withCapturedConsole replaces .write on that same object, ora output is captured when the spinner's active lifecycle runs inside maybeQuiet. Added a code comment in content.ts explaining this. No functional fix was needed — the original wrapping was correct.

Re: automated tests (@andrew-ifrita @sphamjoli) — Added both unit and integration tests:

  • 8 unit tests: --json in help output, JSON error formatting for pre-chain errors
  • 7 integration tests: happy-path JSON for all 6 commands + error path

Re: CI permissions (@andrew-ifrita) — Pushed directly to the repo instead of from fork. CI should work now.

Additional fix: Switched JSON output from process.stdout.write to console.log/console.error to match the pattern in lookup.ts. The test harness (runDotnsCli) only intercepts console methods, so process.stdout.write output wasn't being captured in tests.

@EnderOfWorlds007 EnderOfWorlds007 force-pushed the feat/json-output-all-commands branch from ae158d2 to ce753f8 Compare April 3, 2026 15:01
Copy link
Copy Markdown
Member

@sphamjoli sphamjoli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Howsit, nice work getting --json across all the commands. A few things to tighten up before merge:

A quick run with claude produces

  1. The error handling block is copy-pasted ~10 times

Every command handler has the same catch block (format error, check jsonOutput, write to stderr/stdout, exit). This should be a single shared function. Right now if we need to change the error shape, we'd have to touch 10+ places.

Locations: content.ts, pop.ts, registerCommand.ts, lookup.ts , every .action() handler.

  1. The success output pattern is also duplicated

The if (jsonOutput) console.log(JSON.stringify(result)) else console.log(chalk.green(...)); process.exit(0) block repeats in every handler too. Same fix, Ideally extract once, call everywhere.

  1. getMergedOptions() is identical in two files

content.ts:20-37 and pop.ts:23-41 have the same function. Should live in one place.

  1. getJsonFlag() has a suspicious triple fallback (lookup.ts:35-46)

It tries optsWithGlobals(), then opts(), then falls back to process.argv.includes("--json"). If Commander options are wired correctly, the process.argv fallback shouldn't be needed. Worth investigating whether this is papering over an option propagation issue.

  1. maybeQuiet() lives in bulletin.ts

It's a general-purpose utility imported by content, pop, register, and lookup, but it's defined in the bulletin command module. If we're keeping it, it should live in a shared location.

  1. Error format is inconsistent

content.ts and pop.ts use chalk.red("\n✗ Error: ...") while registerCommand.ts uses chalk.red.bold("✗ Error:") with different string structure. Extracting the error handler (point 1) would fix this automatically.

  1. JSON envelope shapes are inconsistent

Some responses include ok: true (register, pop set, content set), others don't (content view, pop info). It wpuld be be good to pick a convention, e.g., mutations return ok, reads don't, and document it.

  1. Test files repeat the same patterns

The --json help tests in contentJson.test.ts, popJson.test.ts, and registerJson.test.ts are identical except for the command name. A shared helper would reduce the boilerplate. Same for the keystore setup/teardown block that's duplicated across all integration test files.

@EnderOfWorlds007
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @sphamjoli — good observations across the board. Let me separate what's in scope for this PR vs what predates it:

Will fix in this PR (items 1, 2, 7):

  • Extract a shared handleCommandError and emitJsonResult for the 3 command files we touched (content.ts, pop.ts, registerCommand.ts). This also fixes the inconsistent error formatting (fix(cli): fix patched workflow files and examples #6) in our files.
  • Document the JSON envelope convention: mutations return { ok: true, ... }, reads return the data directly. This is already consistent — just needs to be explicit.

Pre-existing — suggest a follow-up PR (items 3, 4, 5, 6, 8):

  • getMergedOptions duplication (6 files), getJsonFlag fallback chain, maybeQuiet living in bulletin.ts, and the test boilerplate all predate this PR — we only import them. Happy to open a dedicated refactoring PR to address these across the codebase, but bundling a cross-file refactor into a feature PR would make it harder to review and increases risk.

Does that split work for you, or would you prefer we tackle everything in one go?

@sphamjoli
Copy link
Copy Markdown
Member

Thanks for the thorough review @sphamjoli — good observations across the board. Let me separate what's in scope for this PR vs what predates it:

Will fix in this PR (items 1, 2, 7):

  • Extract a shared handleCommandError and emitJsonResult for the 3 command files we touched (content.ts, pop.ts, registerCommand.ts). This also fixes the inconsistent error formatting (fix(cli): fix patched workflow files and examples #6) in our files.
  • Document the JSON envelope convention: mutations return { ok: true, ... }, reads return the data directly. This is already consistent — just needs to be explicit.

Pre-existing — suggest a follow-up PR (items 3, 4, 5, 6, 8):

  • getMergedOptions duplication (6 files), getJsonFlag fallback chain, maybeQuiet living in bulletin.ts, and the test boilerplate all predate this PR — we only import them. Happy to open a dedicated refactoring PR to address these across the codebase, but bundling a cross-file refactor into a feature PR would make it harder to review and increases risk.

Does that split work for you, or would you prefer we tackle everything in one go?

We can give it a go in one i dont see the benefit of multiple PRs in this case cc: @andrew-ifrita WDYT?

@andrew-ifrita
Copy link
Copy Markdown
Collaborator

We can give it a go in one i dont see the benefit of multiple PRs in this case cc: @andrew-ifrita WDYT?

One PR is fine in order to include getMergedOptions.

I suspect a good amount of AI gen here (which is fine), but I encourage having your model review the PR first before pulling in humans.

@sphamjoli
Copy link
Copy Markdown
Member

@EnderOfWorlds007 whats the status on this PR?

EnderOfWorlds007 added a commit that referenced this pull request Apr 16, 2026
Move getMergedOptions (6 copies), getJsonFlag, withCapturedConsole, and
maybeQuiet into jsonHelpers.ts as the single shared location. Remove the
process.argv fallback from getJsonFlag — Commander option propagation
handles this correctly. Extract expectJsonHelpOption test helper to
reduce boilerplate across --json unit tests.

Addresses review items 3-5, 8 from PR #76.
@EnderOfWorlds007
Copy link
Copy Markdown
Contributor Author

@sphamjoli @andrew-ifrita — All review items addressed in a single PR as requested. Here's the breakdown:

Items 1 & 2 (error/success duplication): Extracted handleCommandError and emitJsonResult into shared jsonHelpers.ts. All 3 command files we touched (content.ts, pop.ts, registerCommand.ts) use them consistently.

Item 3 (getMergedOptions duplication): Removed from all 6 files (content.ts, pop.ts, bulletin.ts, text.ts, auth.ts, info.ts) and consolidated into jsonHelpers.ts.

Item 4 (getJsonFlag process.argv fallback): Removed the process.argv.includes("--json") fallback. Commander's optsWithGlobals() and opts() handle option propagation correctly — the fallback was unnecessary.

Item 5 (maybeQuiet in bulletin.ts): Moved maybeQuiet and withCapturedConsole from bulletin.ts into jsonHelpers.ts. All importers now reference the shared location.

Item 7 (JSON envelope convention): Documented in jsonHelpers.ts: mutations return { ok: true, ... }, reads return data directly, errors return { error } on stderr.

Item 8 (test boilerplate): Extracted expectJsonHelpOption() helper into cliHelpers.ts.

Net result of the refactoring commit: -103 lines (150 added, 253 removed) across 14 files. All checks pass locally (typecheck, 142 unit tests, lint, prettier).

EnderOfWorlds007 added a commit that referenced this pull request Apr 16, 2026
Move getMergedOptions (6 copies), getJsonFlag, withCapturedConsole, and
maybeQuiet into jsonHelpers.ts as the single shared location. Remove the
process.argv fallback from getJsonFlag — Commander option propagation
handles this correctly. Extract expectJsonHelpOption test helper to
reduce boilerplate across --json unit tests.

Addresses review items 3-5, 8 from PR #76.
@EnderOfWorlds007 EnderOfWorlds007 force-pushed the feat/json-output-all-commands branch from 335e5b9 to 0f79ee8 Compare April 16, 2026 08:20
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.
Switch --json output from process.stdout.write/process.stderr.write to
console.log/console.error, aligning with the existing pattern in
lookup.ts. This ensures the test harness captures JSON output correctly.

Add unit tests verifying --json flag in help output and JSON error
formatting for pre-chain validation errors.

Add integration tests for content view/set, pop info/set, and register
domain in --json mode, validating JSON shape and absence of human output
markers.

Add code comment explaining why ora spinners are safe inside maybeQuiet
(withCapturedConsole replaces .write on the stream object that ora
caches by reference).
Move getMergedOptions (6 copies), getJsonFlag, withCapturedConsole, and
maybeQuiet into jsonHelpers.ts as the single shared location. Remove the
process.argv fallback from getJsonFlag — Commander option propagation
handles this correctly. Extract expectJsonHelpOption test helper to
reduce boilerplate across --json unit tests.

Addresses review items 3-5, 8 from PR #76.
…edicate

The empty-contenthash check (0x, 0x0, length < 6) was duplicated
between decodeContenthashToCid and the viewDomainContentHash return.
Derive hasContent from the decoded string instead.
@EnderOfWorlds007 EnderOfWorlds007 force-pushed the feat/json-output-all-commands branch from 0f79ee8 to be48ae3 Compare April 16, 2026 08:28
@andrew-ifrita
Copy link
Copy Markdown
Collaborator

andrew-ifrita commented Apr 16, 2026

(with help of AI) I believe executeRegistration still logs human output which will get picked up and throw off the json output. This isn't a blocker as it is easy to address in a followup PR.

edit: This would be with the console.log banners

edit edit: I think these are suppressed by maybeQuiet. A little weird organizationally but not a blocker for this PR

Copy link
Copy Markdown
Collaborator

@andrew-ifrita andrew-ifrita left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sufficient. Thanks for putting this together

@sphamjoli sphamjoli self-requested a review April 16, 2026 12:09
@EnderOfWorlds007 EnderOfWorlds007 merged commit d625c53 into main Apr 16, 2026
19 checks passed
@EnderOfWorlds007 EnderOfWorlds007 deleted the feat/json-output-all-commands branch April 16, 2026 12:11
andrew-ifrita pushed a commit that referenced this pull request Apr 28, 2026
## Summary

Mirrors #76 for the `text` namespace — the last on-chain CLI commands
that didn't have machine-readable output. Unblocks `bulletin-deploy`
#173 (set name + description text records at deploy time).

Follows the exact pattern that #76 established for
`register`/`content`/`pop`:
- CLI wrapper uses `getJsonFlag` / `maybeQuiet` / `emitJsonResult` /
`handleCommandError` from `jsonHelpers.ts`.
- Underlying `viewDomainText` / `setDomainText` now return structured
results (`TextViewResult` / `TextSetResult`) — same shift
`viewDomainContentHash` / `setDomainContentHash` did in #76.
- `--json` flag suppresses chalk/ora output via `withCapturedConsole`.
- Banner is already gated by `process.argv.includes("--json")` in
`program.ts`, so it's silent under `--json`.

## JSON output shapes

```
dotns text view <name> <key> --json
→ { "domain": "....dot", "key": "...", "exists": true, "owner": "0x...", "value": "..." | null }

# unregistered domain:
→ { "domain": "....dot", "key": "...", "exists": false, "owner": null, "value": null }

dotns text set <name> <key> <value> --json
→ { "ok": true, "domain": "....dot", "key": "...", "value": "...", "txHash": "0x..." }

# any error path:
→ stderr: { "error": "<message>" }, exit 1
```

Reads return data directly (no `ok` field); writes return `{ ok: true,
... }`. Matches the envelope convention documented in `jsonHelpers.ts`.

## Behaviour change worth flagging

`text view` previously had a manual piped-stdout fallback: when stdout
was a pipe, all chalk output got rerouted to stderr and the bare value
was written to stdout, so `dotns text view foo bar | xargs ...` would
just receive the value. That hack is removed — it predated `--json` and
conflicted with the new flag (the redirect would hijack the JSON
envelope). Scripted users should now use `--json` and parse the `value`
field. The non-piped, non-json TTY experience is unchanged.

## Tests

- New unit file `tests/unit/text/textJson.test.ts` (3 tests): `--json`
flag in help output for both subcommands + JSON error envelope for the
pre-chain `--mnemonic` + `--key-uri` mutex. Mirrors
`contentJson.test.ts`.
- New `--json` cases appended to `tests/integration/text/text.test.ts`
(4 tests): registered-domain happy path for view/set,
unregistered-domain returning `exists: false` (view) and JSON error
(set). Mirrors the `--json` block at the bottom of `content.test.ts`.
- `bun test tests/unit/` → **163/163 pass** (was 159; +4 from this PR —
3 new + the 1 textHelp test count is unchanged but textJson adds 3,
totaling +4? actually +4 unit assertions across the 3 tests; verified
locally).
- `bun run build` → green.
- `bun run lint` → clean.
- `bun run format` → clean.
- `bun run typecheck` → 47 pre-existing errors (same count on `main`);
zero introduced by this PR.

## Scope

Deliberately excluded from this PR — should be follow-ups:
- `account address`, `account info`, `account map` — self-contained
namespace, separate concern from text records.
- `auth set/list/use/remove/clear` — keystore management, involves
interactive password prompts; needs its own design pass.

Closes the upstream gap noted in bulletin-deploy PR #180 (`### #173 /
text records — NOT in this PR`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants