feat(cli): cashctl-style errors and help, a network spinner, and ncli profile - #55
Merged
Merged
Conversation
Align ncli's command UX with cashctl's. A failure now takes one of three text shapes instead of a timestamped zerolog line plus a help dump on the wrong stream: bare invocation help alone, no error line wrong invocation "Error: <msg>", blank line, then help runtime failure "Error: <msg>" alone All three go to stderr; help used to land on stdout, which contradicted "a command's result goes to stdout only" and could pollute a pipe into jq. "Error:" is red only on a real terminal, and never under NO_COLOR -- which ncli did not honour anywhere before. Exit codes are unchanged. A bare group command still exits 2, so the friendlier output never turns a mistyped subcommand into apparent success, and --json still emits exactly one structured line and never help. Help is opt-in, mirroring cashctl's split: UsageError stays help-free for a usage-coded runtime refusal (a relay answering 501 because membership is off, a vault password prompt that can't run), InvocationError adds the error-then-help shape, and HelpError prints help alone. InvocationOrHelp picks between the last two from the args a validator already has, since the same check fires both for "you gave me nothing" and "you gave me the wrong thing". Fixes two contract violations found while verifying: - An unknown flag was reported twice -- cobra's own "Error:" plus usage dump, and ncli's own line stacked under it -- and exited 1 instead of 2. Silencing cobra on the root and classifying ExecuteC's own errors in main fixes that, and with it every unwrapped MarkFlagRequired. - "bunker sessions revoke-grant" with no --method exited 1 as internal rather than 2 as usage; it was the one command with MarkFlagRequired and no ValidateRequiredFlags. Commands that block on the network now animate a spinner on stderr. Log records go through a writer that wipes the in-flight frame first, so the existing per-relay narration still prints and the next tick redraws beneath it. Off under --json, -q, NO_COLOR and a non-terminal stderr, and never wired into the commands that own the terminal (apply, ping --tui, bunker, the delegate wizard) or into the long-running relay server. EmitError keeps recording failures to ncli.log through a file-only logger, since it no longer prints via zerolog. Help text: trimmed the bulkiest Long blocks and tightened the Short lines over 60 characters. That is not cosmetic any more -- help is printed on every mis-invocation now, so find's 17-line Long was about to become the common case (find --help: 43 lines -> 35).
Looking someone up was "ncli find npub1..." returning raw JSON. "ncli profile <identifier>" answers the same question as a readable card, from a single subscription covering the four records that describe an identity: kind:0 metadata, kind:3 (the following count), kind:10002 (NIP-65 relays, with read/write markers) and kind:10063 (Blossom servers). It shows the lightning address from lud16, falling back to lud06. It takes whatever ResolveIdentifier already takes -- npub, hex, nprofile, a nip-05 address, or a vault label, so "ncli profile <label>" shows your own. Two things it deliberately does differently from find: - It aggregates across every target rather than stopping at the first one with a match, using QueryTargets rather than Find. A relay list very often lives on a different relay than the profile, so find's first-match-wins would miss it. - An identity that published nothing is a successful, sparse render at exit 0, not not_found. The identity exists; it just published nothing. Every section still prints, reading "not published", so the card's shape never moves and absent stays distinguishable from zero -- in --json those fields are omitted rather than zeroed. The claimed nip-05 is verified against its domain by default, one HTTPS round trip: a lookup that can't complete reports as unverifiable rather than as a failed verification, since an unreachable domain says nothing either way. --no-verify skips it. The kind:0 content document moves to cli/common as ProfileMetadata, replacing bunker's private three-field copy, so both agree on what a profile is. NIP-65 entries are re-serialized through a local struct because nip65.RelayEntry carries no JSON tags and would otherwise put Go field names in the --json contract. Verified end to end against relay.primal.net: real metadata, a genuine nip-05 verification, and a six-relay NIP-65 list. Docs: AGENTS.md's failure section rewritten for the three new shapes and the spinner, plus the three skills that claimed a bare group command was "not a silent help dump" -- it now does print help, and still exits 2.
Two CI failures from the spinner commit: - errcheck flagged the three fmt.Fprint/Fprintf calls that draw and erase the spinner frame. Discard the returns explicitly, as the rest of the tree already does for best-effort writes -- there is nothing useful to do if writing a progress frame to stderr fails. - IsBareInvocation imports spf13/pflag directly for the VisitAll callback signature, which promotes it from an indirect to a direct requirement; go.mod hadn't been re-tidied to match. Verified against CI's own steps: gofmt, build, vet, tidy, `go test -short -race ./...`, and golangci-lint v2.13.2 (the version the workflow pins) now reports 0 issues.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings ncli's command UX in line with
cashctl, and addsncli profile.1. Failures read like cashctl
Three text shapes instead of a timestamped zerolog line plus a help dump on the wrong stream:
ncli decode(missing arg)2026-09-23T12:27:33Z ERR accepts 1 arg(s), received 0on stderr, exit 2ncli miner mnie(typo)ERRError: unknown command "mnie", blank line, help — all stderr, exit 2ncli decode --bogusflagncli decode notavalidERR ...Error: ...alone, no help, exit 3Help moved off stdout, where it contradicted AGENTS.md's "a command's result goes to stdout only" and could pollute a pipe into
jq.Error:is red only on a real terminal, and never underNO_COLOR— which ncli honoured nowhere before.Exit codes are unchanged. A bare group command still exits 2, so the friendlier output never turns a mistyped subcommand into apparent success, and
--jsonstill emits exactly one structured line and never help. The three skills that said a bare group was "not a silent help dump" are reworded: it does print help now, and still exits 2.Help is opt-in, mirroring cashctl:
UsageErrorstays help-free for a usage-coded runtime refusal (a relay answering 501 because membership is off; a vault password prompt with no TTY),InvocationErroradds error-then-help,HelpErrorprints help alone.InvocationOrHelppicks between the last two from the args a validator already has.Two contract violations fixed on the way
Error:+ usage dump, with ncli's line stacked under it — and exited 1. Silencing cobra on the root and classifyingExecuteC's own errors inmainfixes that, and with it every unwrappedMarkFlagRequired.bunker sessions revoke-grantwith no--methodexited 1 asinternalinstead of 2 asusage— the one command withMarkFlagRequiredand noValidateRequiredFlags.2. Spinner on network-facing commands
Log records go through a writer that wipes the in-flight frame first, so the existing per-relay narration still prints and the next tick redraws beneath it — the spinner doesn't cost you the narration.
Off under
--json,-q,NO_COLORand a non-terminal stderr (verified: zero ANSI bytes on captured stderr). Never wired into the commands that own the terminal —apply,ping --tui,bunker, the delegate wizard — or into the long-running relay server.3.
ncli profile <identifier>One subscription over kind:0, kind:3 (following count), kind:10002 (NIP-65) and kind:10063 (Blossom). Takes npub, hex, nprofile, a nip-05 address, or a vault label.
Two deliberate differences from
find:QueryTargets, notFind) — a relay list very often lives on a different relay than the profile.not_found. Every section still prints as "not published", so absent stays distinguishable from zero; in--jsonthose fields are omitted rather than zeroed.nip-05 is verified against its domain by default; a lookup that can't complete reports as unverifiable (
⚠) rather than as a failed verification.--no-verifyskips it.Verification
go build ./...,go vet ./...,go test -short ./...all clean (GOWORK=off).HelpModeshapes with stdout asserted empty,--jsonnever dumping help,errorPrefix/NO_COLOR,IsBareInvocation, and the spinner (frames written, nothing when disabled, goroutine joined before return, no nesting, error propagated).IsBareInvocationusedVisitinstead ofVisitAll+Changed, which made every flags-only invocation look bare.ncli profileverified end to end againstrelay.primal.net— real metadata, a genuine nip-05 HTTPS verification, a six-relay NIP-65 list.Note
origin/worktree-trim-id-helpis no longer a conflict:satoshiis already the vault-label placeholder onmain, contrary to an earlier reading.