Skip to content

cli: build CliContext in main and centralize error rendering#3755

Merged
juan-malbeclabs merged 8 commits into
mainfrom
jo/3-cli-context-and-errors
May 27, 2026
Merged

cli: build CliContext in main and centralize error rendering#3755
juan-malbeclabs merged 8 commits into
mainfrom
jo/3-cli-context-and-errors

Conversation

@juan-malbeclabs
Copy link
Copy Markdown
Contributor

@juan-malbeclabs juan-malbeclabs commented May 22, 2026

RFC-20 implementation stack

This PR is part of a 9-PR chain delivering RFC-20: CLI standardization. Each PR's diff is only its own contribution; reviewers should consume them in order.

# PR Scope
1 #3753 doublezero-cli-core foundation crate + solana_l1_rpc_url
2 #3754 --solana-url + --log-level global flags + tracing init
3 #3755 CliContext built in main + centralized error rendering
4 #3756 rename doublezero_clidoublezero-serviceability-cli
5 #3757 rewrite location get as the async + CliContext reference verb
6 #3758 docs/cli-standard.md + CLAUDE.md pointer
7 #3779 move per-resource subcommand wrappers into the module crate
8 #3760 add ServiceabilityCommand enum + async dispatcher
9 #3761 #[command(flatten)] + collapse binary dispatch

This PR: #3755 — position 3 of 9. Previous: #3754 · Next: #3756


Summary of Changes

  • Builds a CliContext once at binary startup from --env and the per-field global overrides (--url, --ws, --solana-url, --keypair, --sock-file), per RFC-20 (§CliContext). The context resolves to Environment::default() (devnet) when --env is absent. DZClient continues to consume the legacy Option<String> tuple via a thin bridge that forwards None when neither --env nor a per-field override is set, preserving today's fall-back to ~/.config/doublezero/cli/config.yml. Verbs that migrate to the RFC-20 module contract will consume CliContext directly and the bridge shrinks.
  • Centralizes top-level error rendering through doublezero_cli_core::error::render_eyre. Replaces three ad-hoc eprintln!("Error: {e}") sites in client/doublezero/src/main.rs (env-parse failure, env-config resolution failure, top-level command failure) with a single helper that prints Error: <head> followed by the full chain of causes on stderr.

Diff Breakdown

Category Files Lines (+/-) Net
Core logic 1 +55 / -23 +32
Docs 1 +2 / -1 +1
Total 2 +57 / -24 +33

Replaces the ad-hoc env-resolution tuple in main.rs with a CliContextBuilder invocation and unifies error rendering; no change to subcommand dispatch shape.

Key files (click to expand)
  • client/doublezero/src/main.rs - constructs CliContext via CliContextBuilder after App::parse(), bridges back to DZClient::new for the legacy Option<String> signature without changing the no-flags fall-back behavior, and routes the three error-exit sites through doublezero_cli_core::error::render_eyre.

Testing Verification

  • cargo check --workspace clean.
  • doublezero --env totally-bogus address prints a single-line Error: Invalid environment ... on stderr (via the new render_eyre) and exits 1, matching the previous behavior.
  • doublezero --env devnet address resolves and prints the address (CliContext populated from Environment::Devnet); doublezero address with no flags still falls back to ~/.config/solana/cli/config.yml via the bridge.
  • Targets jo/2-cli-global-flags; the diff shown is only this PR's contribution.

@vihu
Copy link
Copy Markdown
Contributor

vihu commented May 22, 2026

The bridge currently sets ws = Some(ctx.ledger_ws_rpc_url.clone()) whenever env_explicit || app.ws.is_some(). That means doublezero --env devnet --url <custom-rpc> with no --ws pairs the custom RPC URL with the env default websocket URL.

DZClient::new only derives WS from the selected RPC URL when websocket_url is None, so this loses the expected derivation behavior for custom RPC overrides. Could we pass None for WS when --url is overridden and --ws is absent, or otherwise derive WS from the chosen URL?

@juan-malbeclabs juan-malbeclabs force-pushed the jo/2-cli-global-flags branch from 7a7f8b6 to 19c7707 Compare May 24, 2026 14:39
@juan-malbeclabs juan-malbeclabs force-pushed the jo/3-cli-context-and-errors branch from 02beec8 to d121d83 Compare May 24, 2026 14:39
@juan-malbeclabs
Copy link
Copy Markdown
Contributor Author

The change was applied to the corresponding PRs.

┌─────────────────────────────┬───────┬──────────────┬─────────────────────────────────────────────────────────────────────────┐
│           Branch            │  PR   │ Local commit │                                 Change                                  │
├─────────────────────────────┼───────┼──────────────┼─────────────────────────────────────────────────────────────────────────┤
│ jo/1-cli-core-foundation    │ #3753 │ 5e6002d64    │ cli/core: derive WS URL from RPC override in CliContextBuilder          │
├─────────────────────────────┼───────┼──────────────┼─────────────────────────────────────────────────────────────────────────┤
│ jo/2-cli-global-flags       │ #3754 │ b0424349e    │ cli: make --env mutually exclusive with per-field URL/program overrides │
├─────────────────────────────┼───────┼──────────────┼─────────────────────────────────────────────────────────────────────────┤
│ jo/3-cli-context-and-errors │ #3755 │ 853a3069a    │ cli: collapse CliContext bridge and fix config-path comment             │
└─────────────────────────────┴───────┴──────────────┴─────────────────────────────────────────────────────────────────────────┘

juan-malbeclabs added a commit that referenced this pull request May 26, 2026
…3753)

## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| **1** | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3759](#3759) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3753** — position **1** of 9. Previous: (none — base is
`main`) · Next:
[#3754](#3754)

---

## Summary of Changes
- Adds `doublezero-cli-core` (`crates/doublezero-cli-core/`), the shared
library crate every `doublezero-<module>-cli` will reuse per
[RFC-20](../rfcs/rfc20-cli-standardization.md). Ships `CliContext` +
`CliContextBuilder`, `RequirementCheck` bitflags (bit values aligned
with the legacy `CHECK_ID_JSON | CHECK_BALANCE |
CHECK_FOUNDATION_ALLOWLIST` constants), the shared validators
(`validate_pubkey`, `validate_pubkey_or_code`, `validate_code`,
`validate_parse_bandwidth`, `validate_parse_delay_ms`,
`validate_parse_jitter_ms`, `validate_parse_delay_override_ms`), the
`DisplayVec` formatter, a `tracing` + `tracing-subscriber`
`init_logging(verbosity)` helper that writes to stderr, and `testing`
helpers (`cli_context_for_tests`, `cli_context_default_for_tests`).
- Adds `solana_l1_rpc_url` to `doublezero-config::NetworkConfig` with
the per-environment defaults from RFC-20 §Environments (mainnet-beta ->
Solana mainnet-beta; testnet and devnet -> Solana testnet; local ->
`http://localhost:8899`) plus a `DZ_SOLANA_RPC_URL` env-var override
mirroring the existing `DZ_LEDGER_RPC_URL` / `DZ_LEDGER_WS_RPC_URL`
overrides.
- Migrates the shared `validators.rs` and `formatters.rs` out of
`smartcontract/cli/` into the new core crate. Existing import paths
continue to compile via thin `pub use` re-export shims in
`smartcontract/cli/src/{validators,formatters}.rs`, so the rest of the
workspace is unaffected.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Core logic   |     7 | +618 / -0   | +618 |
| Tests        |     1 | +64 / -0    |  +64 |
| Scaffolding  |     4 | +50 / -167  | -117 |
| Config/build |     3 | +33 / -0    |  +33 |
| Generated    |     1 | +20 / -0    |  +20 |
| Docs         |     1 | +2 / -1     |   +1 |
| **Total**    |    17 | +787 / -168 | +619 |

Introduces ~620 lines of shared CLI utility code in the new core crate
while shrinking `smartcontract/cli` by replacing its validator and
formatter implementations with thin re-export shims; no behavior change.

<details>
<summary>Key files (click to expand)</summary>

- `crates/doublezero-cli-core/src/context.rs` - `CliContext`,
`OutputFormat`, and the `CliContextBuilder` that resolves `--env`
defaults from `doublezero-config` and applies per-field overrides.
- `crates/doublezero-cli-core/src/validators.rs` - shared `clap`
value-parsers (pubkey, code, pubkey-or-code, bandwidth, delay, jitter,
delay-override) with their unit tests; moved verbatim from
`smartcontract/cli/src/validators.rs`.
- `crates/doublezero-cli-core/src/requirements.rs` - `RequirementCheck`
bitflags type (KEYPAIR / BALANCE / FOUNDATION_ALLOWLIST) with bit values
preserved for ABI continuity with the legacy `u8` constants.
- `crates/doublezero-cli-core/src/testing.rs` -
`cli_context_for_tests()` / `cli_context_default_for_tests()` helpers
for module-crate verb tests.
- `crates/doublezero-cli-core/src/formatters.rs` - `DisplayVec` +
`stringify_vec` moved from `smartcontract/cli`.
- `crates/doublezero-cli-core/src/error.rs` - `Result` alias,
`CliError`, `render_eyre` helper for chain-of-causes rendering.
- `crates/doublezero-cli-core/src/logging.rs` -
`init_logging(verbosity)` via `tracing-subscriber` with stderr writer;
honors `RUST_LOG` when set.
- `config/src/env.rs` - adds `solana_l1_rpc_url` to `NetworkConfig`,
wires it through per environment, and adds `DZ_SOLANA_RPC_URL` override
plus tests.

</details>

## Testing Verification
- `cargo test -p doublezero-cli-core` passes (16 tests across
validators, requirements bitflags, context builder, testing helpers,
formatters).
- `cargo test -p doublezero-config` passes (9 tests including two new
ones covering the Solana L1 URL resolution per environment and the
`DZ_SOLANA_RPC_URL` override).
- `make rust-test` green, including the program-accounts-compat run.
- `make rust-lint` clean.
- Verified existing `smartcontract/cli` consumers continue to compile
against the shim files (`use doublezero_cli::validators::*` and friends
resolve through the re-export).
`--env` now conflicts with `--url`, `--ws`, `--solana-url`,
`--program-id`, and `--geo-program-id` via clap. Combining them used
to be silently inconsistent: per-field overrides won over env defaults
on some fields and not others, so e.g. `--env devnet --url <custom>`
would pair the custom RPC URL with devnet's default WebSocket URL.

Clap now rejects the combination upfront with a clear usage error,
matching the design rule that `--env` selects a full preset wholesale.
Adds clap-parse tests covering each conflict pair and the positive
single-flag cases.
@juan-malbeclabs juan-malbeclabs force-pushed the jo/2-cli-global-flags branch from 19c7707 to 2c8bb40 Compare May 26, 2026 17:06
@juan-malbeclabs juan-malbeclabs force-pushed the jo/3-cli-context-and-errors branch from e34d7e3 to 4f2af4f Compare May 26, 2026 17:06
@ben-dz
Copy link
Copy Markdown
Contributor

ben-dz commented May 26, 2026

From the PR description:

preserving today's fall-back to ~/.config/solana/cli/config.yml.

~/.config/doublezero..., right?

Swap the repeatable --log-verbose flag for --log-level taking one of
off/error/warn/info/debug/trace. init_logging now takes a LogLevel enum
(clap ValueEnum) defined in doublezero-cli-core. Default level remains
warn; RUST_LOG still overrides.
Replaces the three explicit if/else blocks that translated CliContext
into DZClient::new arguments with a flat `any_url_explicit.then(...)`
form. The behavior is identical: when no env or per-field override is
present, all three fields stay None so DZClient falls through to the
on-disk config; otherwise the resolved CliContext values flow through.

Drops the assumption that env_explicit alone resolves URL/program-id
fields: any explicit override (--url, --ws, --program-id) now also
opts into using the resolved context, which keeps the bridge in step
with the builder's WS-from-RPC derivation introduced in jo/1.

Also corrects the stale comment that referenced
`~/.config/solana/cli/config.yml`; DZClient actually reads
`~/.config/doublezero/cli/config.yml`.
Read the persisted ~/.config/doublezero/cli/config.yml (or DOUBLEZERO_CONFIG_FILE)
at binary startup and feed it into CliContextBuilder, so verbs that read from
CliContext directly see the same backend as the legacy DZClient bridge.

Precedence (highest wins): CLI flag > persisted config > env-derived default.
When --env is absent and the persisted config has a serviceability program ID,
the environment is derived via Environment::from_program_id; otherwise it falls
back to Environment::default(). DZClient is now constructed from fully resolved
CliContext values, so the legacy bridge and the new context agree on every path.

File reads remain confined to the binary; module crates do not touch the
filesystem (RFC-20 §67).
@juan-malbeclabs juan-malbeclabs force-pushed the jo/3-cli-context-and-errors branch from 4f2af4f to 140c77b Compare May 26, 2026 21:25
Base automatically changed from jo/2-cli-global-flags to main May 26, 2026 21:35
juan-malbeclabs added a commit that referenced this pull request May 26, 2026
## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| **2** | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3759](#3759) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3754** — position **2** of 9. Previous:
[#3753](#3753) · Next:
[#3755](#3755)

---

## Summary of Changes
- Adds the `--solana-url <SOLANA_RPC_URL>` global flag to `doublezero`
per RFC-20 §Global flags. Distinct from `--url`, which continues to
override the DZ ledger transport; `--solana-url` targets the Solana L1
transport. The flag is parsed and exposed on the binary's `App` struct;
per-verb consumption lands when verbs migrate to construct typed Solana
L1 clients from `CliContext` (follow-up PRs).
- Adds the `--verbose` / `-v` (repeatable) global flag and initializes
the `tracing` subscriber at startup via
`doublezero_cli_core::init_logging(verbosity)`. Default level is `warn`;
`-v` raises to `debug`, `-vv` to `trace`. Honors the `RUST_LOG` env var
when set. Diagnostic logs go to stderr so `--json` output on stdout
remains parseable.
- Replaces the `println!("using keypair: ...")` startup line with a
`tracing::info!` event so the keypair confirmation now appears only at
`-v` or higher and no longer pollutes parseable stdout.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Core logic   |     1 | +13 / -1    |  +12 |
| Config/build |     2 | +3 / -0     |   +3 |
| Docs         |     1 | +2 / -0     |   +2 |
| **Total**    |     4 | +18 / -1    |  +17 |

Three new global flags wired into the binary and the logging facade; no
changes to subcommand dispatch or verb behavior.

<details>
<summary>Key files (click to expand)</summary>

- `client/doublezero/src/main.rs` - adds `solana_url` and `verbose`
fields to the `App` struct, calls
`doublezero_cli_core::init_logging(app.verbose)` first thing in `main`,
and replaces the `println!` keypair confirmation with `tracing::info!`.
- `client/doublezero/Cargo.toml` - depends on `doublezero-cli-core` and
`tracing`.

</details>

## Testing Verification
- `make rust-lint` clean.
- `doublezero --help` shows the new `--solana-url` and `--verbose` /
`-v` global flags with the expected help text.
- `doublezero --keypair /tmp/fake.json --env local -v address` emits a
`tracing::info!` event with the keypair path on stderr while stdout
shows only the address; no `-v` keeps stderr empty.
- Built on top of #3753 (foundation crate); this PR targets
`jo/1-cli-core-foundation` and the diff shown is its own contribution
only.
The CliContext refactor started passing ctx.keypair_path to DZClient::new
as if it were the --keypair CLI flag. When the persisted config.yml
exists, that short-circuits load_keypair's precedence chain and bypasses
the DOUBLEZERO_KEYPAIR env var, which the e2e contributor-auth
negative-authz tests rely on. Pass app.keypair.clone() instead so the
env var continues to override the persisted keypair path; CliContext
still carries the resolved value for other consumers.
@juan-malbeclabs juan-malbeclabs enabled auto-merge (squash) May 26, 2026 22:47
Copy link
Copy Markdown
Contributor

@ben-dz ben-dz left a comment

Choose a reason for hiding this comment

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

LGTM

@juan-malbeclabs juan-malbeclabs merged commit 1962829 into main May 27, 2026
33 checks passed
@juan-malbeclabs juan-malbeclabs deleted the jo/3-cli-context-and-errors branch May 27, 2026 12:16
juan-malbeclabs added a commit that referenced this pull request May 27, 2026
## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| **4** | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3759](#3759) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3756** — position **4** of 9. Previous:
[#3755](#3755) · Next:
[#3757](#3757)

---

## Summary of Changes
- Renames the `smartcontract/cli/` crate from `doublezero_cli` to
`doublezero-serviceability-cli` to satisfy RFC-20's module-crate naming
contract (`doublezero-<module>-cli` in kebab-case). The crate stays at
`smartcontract/cli/`; only the `[package].name` and `[lib].name` change
(lib name is `doublezero_serviceability_cli` because Rust requires
underscores in import paths).
- Updates all in-tree consumers: workspace `Cargo.toml` dep key,
`client/doublezero`, `client/doublezero-geolocation-cli`,
`controlplane/doublezero-admin`, and every `use doublezero_cli::` site
across all `.rs` files swept to `use doublezero_serviceability_cli::`.
- No user-facing command, flag, or output change. External operators who
depend on the workspace crate by its old name must update their own
`Cargo.toml` and `use` statements.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Scaffolding  |    48 | +51 / -51   |   0  |
| Config/build |     5 | +6 / -6     |   0  |
| Generated    |     1 | +51 / -41   |  +10 |
| Docs         |     1 | +1 / -0     |   +1 |
| **Total**    |    53 | +109 / -98  |  +11 |

Pure mechanical rename of imports and dep keys across the workspace;
behavior unchanged.

<details>
<summary>Key files (click to expand)</summary>

- `smartcontract/cli/Cargo.toml` - renames `name = "doublezero_cli"` ->
`"doublezero-serviceability-cli"` and `[lib].name` ->
`"doublezero_serviceability_cli"`.
- `Cargo.toml` (workspace) - renames the workspace dep key.
- `client/doublezero/Cargo.toml`,
`client/doublezero-geolocation-cli/Cargo.toml`,
`controlplane/doublezero-admin/Cargo.toml` - dep + feature-passthrough
rename.
- All `.rs` files containing `use doublezero_cli::` updated to `use
doublezero_serviceability_cli::` (sed sweep across
`client/doublezero/src/cli/**`, `client/doublezero/src/command/**`,
`controlplane/doublezero-admin/src/**`,
`client/doublezero-geolocation-cli/src/**`, and the two binary `main.rs`
files).

</details>

## Testing Verification
- `cargo check --workspace` clean.
- `cargo tree -p doublezero` shows `doublezero-serviceability-cli` (and
no residual `doublezero_cli`).
- `doublezero --version` and `doublezero --env devnet address` continue
to work, confirming the binary is unaffected.
- Targets `jo/3-cli-context-and-errors`; the diff shown is only this
PR's rename.
juan-malbeclabs added a commit that referenced this pull request May 27, 2026
…#3757)

## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| **5** | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3759](#3759) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3757** — position **5** of 9. Previous:
[#3756](#3756) · Next:
[#3758](#3758)

---

## Summary of Changes
- Migrates `location get` to the RFC-20 conforming verb pattern as the
project's reference (`smartcontract/cli/src/location/get.rs`).
`GetLocationCliCommand::execute` is now `async fn`, takes `&CliContext`
as its first non-self argument, and emits a `tracing::debug!` event so
`-v` surfaces what the verb is doing.
- Updates the verb's unit test to consume the shared
`doublezero_cli_core::testing::cli_context_default_for_tests()` helper
and exercise the new async signature via a small `tokio` current-thread
runtime in the existing `#[test]`. Backend stays mocked through
`MockCliCommand` (auto-generated by `#[automock]`).
- Updates the binary dispatch arms in `client/doublezero` and
`controlplane/doublezero-admin` to `.await` the new method. The
`doublezero-admin` binary gets a `doublezero-cli-core` dep plus a small
`CliContext` build at startup (matching the `doublezero` binary's
pattern) so the new verb is callable end-to-end.
- The verb's user-facing args, flags, table layout, and JSON schema are
unchanged. Other location verbs (Create, Update, List, Delete) keep
their current sync signatures and migrate opportunistically per RFC-20's
grandfathering clause.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Core logic   |     3 | +43 / -11   |  +32 |
| Tests        |     1 | +14 / -3    |  +11 |
| Config/build |     2 | +2 / -0     |   +2 |
| Docs         |     1 | +1 / -0     |   +1 |
| Generated    |     1 | +14 / -7    |   +7 |
| **Total**    |     7 | +74 / -21   |  +53 |

One verb migrated end-to-end (async + `CliContext` + tracing + shared
test helper); two binaries updated to `.await` the new dispatch arm.

<details>
<summary>Key files (click to expand)</summary>

- `smartcontract/cli/src/location/get.rs` - converts `execute` to `async
fn (self, ctx, client, out)`, adds the `tracing::debug!` event, rewrites
the unit test to use `cli_context_default_for_tests()` and a small
`tokio` current-thread runtime around the awaited call.
- `client/doublezero/src/main.rs` - one-line change:
`LocationCommands::Get(args) => args.execute(&ctx, &client, &mut
handle).await`.
- `controlplane/doublezero-admin/src/main.rs` - same one-line dispatch
change plus a `CliContextBuilder::new().with_env(...).build()?` block at
startup so `&ctx` is available.
- `controlplane/doublezero-admin/Cargo.toml`,
`smartcontract/cli/Cargo.toml` - add `doublezero-cli-core` and `tracing`
deps respectively.

</details>

## Testing Verification
- `cargo test -p doublezero-serviceability-cli location::get` passes
(the rewritten test exercises pubkey lookup, code lookup, and the
not-found error path through the async signature).
- `make rust-test` green workspace-wide.
- `make rust-lint` clean.
- `doublezero location get --help` shows the same `--code` and `--json`
flags as before; behavior unchanged from the user's perspective.
- Targets `jo/4-cli-rename-serviceability`; the diff shown is only this
PR's contribution.
juan-malbeclabs added a commit that referenced this pull request May 27, 2026
… crate (#3759)

## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| **7** | [#3759](#3759) |
move per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3759** — position **7** of 9. Previous:
[#3758](#3758) · Next:
[#3760](#3760)

---

## Summary of Changes
- Moves the 13 per-resource serviceability subcommand wrapper files
(`accesspass`, `config`, `contributor`, `device`, `exchange`,
`globalconfig`, `link`, `location`, `multicastgroup`, `permission`,
`resource`, `tenant`, `user`) from `client/doublezero/src/cli/` into the
`doublezero-serviceability-cli` module crate at
`smartcontract/cli/src/cli/`, per RFC-20 §Module contract item 2 ("the
module crate exports the subcommand enum").
- Adds `smartcontract/cli/src/cli/mod.rs` and `pub mod cli;` in the
library's `lib.rs` so the new module is reachable.
- Rewrites import paths in the moved files
(`doublezero_serviceability_cli::<resource>::*` ->
`crate::<resource>::*`) and in the binary
(`client/doublezero/src/{cli/command.rs,main.rs}` switch to
`doublezero_serviceability_cli::cli::<resource>::*` for the moved
types).
- `cli/multicast.rs` stays in the binary: its `Subscribe`,
`Unsubscribe`, `Publish`, and `Unpublish` variants are async and their
`execute` impls live in `client/doublezero/src/command/multicast.rs`
(binary-local), depending on `ServiceControllerImpl` and
`crate::command::helpers::resolve_client_ip`. The binary's
`cli/multicast.rs` now imports `MulticastGroupCliCommand` from the
library.
- No `Command` enum reshape, no `main.rs` dispatch change. Pure file
relocation. The next PR adds the top-level `ServiceabilityCommand` enum;
the PR after that wires it into the binary via `#[command(flatten)]` and
collapses the dispatch.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Scaffolding  |    19 | +75 / -32   |  +43 |
| Docs         |     1 | +1 / -0     |   +1 |
| **Total**    |    20 | +76 / -32   |  +44 |

Pure file moves (tracked as renames in git, ~95% similarity each) plus
import-path rewrites in the binary and the new library `cli/mod.rs`.
Behavior unchanged.

<details>
<summary>Key files (click to expand)</summary>

- `smartcontract/cli/src/cli/mod.rs` (new) - declares the 13 moved
modules.
- `smartcontract/cli/src/lib.rs` - adds `pub mod cli;`.
- `client/doublezero/src/cli/mod.rs` - keeps only `command`,
`geolocation`, and `multicast` (the binary-local ones).
- `client/doublezero/src/cli/command.rs` - imports `MulticastCliCommand`
from `crate::cli` and the 13 moved types from
`doublezero_serviceability_cli::cli::*`.
- `client/doublezero/src/cli/multicast.rs` - imports
`MulticastGroupCliCommand` from the library.
- `client/doublezero/src/main.rs` - imports the moved subcommand enums
(`DeviceCommands`, `LinkCommands`, ...) from
`doublezero_serviceability_cli::cli::*`; the per-resource match arms
switch from `cli::<resource>::*Commands::*` to
`doublezero_serviceability_cli::cli::<resource>::*Commands::*`.

</details>

## Testing Verification
- `cargo check --workspace` clean.
- `make rust-test` green workspace-wide (including the `cargo test -p
doublezero-serviceability-cli location::get` reference verb).
- `make rust-lint` clean.
- `cargo run -p doublezero -- --help` and `cargo run -p doublezero --
device list --help` produce the same command tree as before; the
relocation is invisible to users.
- Targets `jo/6-docs-cli-standard`; the diff shown is only this PR's
contribution. Follow-ups #PR8 (add `ServiceabilityCommand` enum) and
#PR9 (flatten in binary + collapse dispatch) complete the RFC-20 §Module
contract item 2 work.
juan-malbeclabs added a commit that referenced this pull request May 27, 2026
## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| **6** | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3779](#3779) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3758** — position **6** of 9. Previous:
[#3757](#3757) · Next:
[#3779](#3779)

---

## Summary of Changes
- Adds `docs/cli-standard.md`, the contributor-facing summary of RFC-20
([rfcs/rfc20-cli-standardization.md](../rfcs/rfc20-cli-standardization.md)).
Covers the module contract, argument and output conventions, the global
flag set, the diagnostic-logging facade, the preflight
`RequirementCheck` bitflags, and walks through
`smartcontract/cli/src/location/get.rs` as the worked example.
- Updates `CLAUDE.md` with a CLI-standard section pointing at RFC-20,
the new contributor doc, and the reference verb so future contributors
land on the standard quickly.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Docs         |     3 | +253 / -0   | +253 |
| **Total**    |     3 | +253 / -0   | +253 |

Pure documentation: a single new contributor doc plus a short pointer
added to `CLAUDE.md` and the CHANGELOG entry recording it.

<details>
<summary>Key files (click to expand)</summary>

- `docs/cli-standard.md` - new file. Roughly 240 lines covering the
module contract, argument/output conventions, global flags, logging, the
`location get` worked example, the `RequirementCheck` bitflag mapping,
authorization rules, and the explicit open follow-ups (Command enum
move, geolocation module crate, daemon-control module crate, JSON schema
versioning, shell completion).
- `CLAUDE.md` - new "CLI Standard (RFC-20)" section between "RFCs and
Documentation" and "Style & Terminology" with four bullets pointing at
the RFC, the contributor doc, the core crate, and the migration cadence.

</details>

## Testing Verification
- `make rust-test` green workspace-wide (no code changes; sanity check
that the docs commit does not break anything).
- Manually previewed the rendered markdown of both
`docs/cli-standard.md` and the updated `CLAUDE.md` section for headings,
lists, and the code block in the worked example.
- Targets `jo/5-cli-location-get-conforming-verb`; the diff shown is
only this PR's docs.
juan-malbeclabs added a commit that referenced this pull request May 27, 2026
…her (#3760)

## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3779](#3779) | move
per-resource subcommand wrappers into the module crate |
| **8** | [#3760](#3760) |
add `ServiceabilityCommand` enum + async dispatcher |
| 9 | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3760** — position **8** of 9. Previous:
[#3779](#3779) · Next:
[#3761](#3761)

---

## Summary of Changes
- Adds `doublezero_serviceability_cli::cli::ServiceabilityCommand`, the
module crate's top-level subcommand enum per RFC-20 §Module contract
item 2.
- Aggregates 17 serviceability variants: `Init`, `Migrate`, `Address`,
`Balance`, `Config`, `GlobalConfig`, `Location`, `Exchange`,
`Contributor`, `Permission`, `Tenant`, `Device`, `Link`, `AccessPass`,
`User`, `Export`, `Keygen`, `Resource`.
- Implements `async fn execute(ctx: &CliContext, client: &impl
CliCommand, out: &mut impl Write)` that owns the full per-resource
dispatch tree currently inlined in `client/doublezero/src/main.rs`. The
`Location::Get` arm forwards `&ctx` and is `.await`ed (matches the
RFC-20 reference verb from #3757); all other resource arms are sync.
- Not yet wired into the unified binary. The next PR (#9) adds
`#[command(flatten)] Serviceability(ServiceabilityCommand)` to the
binary's `Command` enum and collapses `main.rs`'s ~270-line match block
to a single dispatch arm.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Core logic   |     1 | +242 / -0   | +242 |
| Scaffolding  |     1 | +1 / -0     |   +1 |
| Docs         |     1 | +1 / -0     |   +1 |
| **Total**    |     3 | +244 / -1   | +243 |

Single new file in the library plus a one-line `pub mod` in
`cli/mod.rs`. The enum is `pub` and reachable but currently has no
in-tree consumer; it surfaces as the module crate's public mounting
point ready for the binary to flatten.

<details>
<summary>Key files (click to expand)</summary>

- `smartcontract/cli/src/cli/command.rs` (new) - the
`ServiceabilityCommand` enum + `async fn execute` dispatcher. Forwards
`&CliContext` only to verbs that consume it (today only
`LocationCommands::Get` per #3757); other arms ignore `ctx`. The
`AccessPassCommands::Fund` arm preserves the existing stdin-reading
behavior (`std::io::stdin().lock()`) since that interaction is part of
the verb's contract today.
- `smartcontract/cli/src/cli/mod.rs` - adds `pub mod command;`.

</details>

## Testing Verification
- `cargo check --workspace` clean.
- `make rust-test` green workspace-wide (no behavior change — the new
enum is unused by the binary in this PR).
- `make rust-lint` clean.
- `cargo doc --no-deps -p doublezero-serviceability-cli` succeeds and
the new module appears.
- Targets `jo/7-cli-serviceability-move-cli-files`; the diff shown is
only this PR's contribution. Final PR #9 will flatten this enum into the
binary and collapse `main.rs` dispatch.
juan-malbeclabs added a commit that referenced this pull request May 28, 2026
## RFC-20 implementation stack

This PR is part of a 9-PR chain delivering [RFC-20: CLI
standardization](https://github.com/malbeclabs/doublezero/blob/main/rfcs/rfc20-cli-standardization.md).
Each PR's diff is **only its own contribution**; reviewers should
consume them in order.

| # | PR | Scope |
|---|----|-------|
| 1 | [#3753](#3753) |
`doublezero-cli-core` foundation crate + `solana_l1_rpc_url` |
| 2 | [#3754](#3754) |
`--solana-url` + `--log-verbose` global flags + tracing init |
| 3 | [#3755](#3755) |
`CliContext` built in `main` + centralized error rendering |
| 4 | [#3756](#3756) |
rename `doublezero_cli` → `doublezero-serviceability-cli` |
| 5 | [#3757](#3757) |
rewrite `location get` as the async + `CliContext` reference verb |
| 6 | [#3758](#3758) |
`docs/cli-standard.md` + `CLAUDE.md` pointer |
| 7 | [#3779](#3779) | move
per-resource subcommand wrappers into the module crate |
| 8 | [#3760](#3760) | add
`ServiceabilityCommand` enum + async dispatcher |
| **9** | [#3761](#3761) |
`#[command(flatten)]` + collapse binary dispatch |

**This PR: #3761** — position **9** of 9. Previous:
[#3760](#3760) · Next:
(none — tip of stack)

---

## Summary of Changes
- Hoists `doublezero_serviceability_cli::cli::ServiceabilityCommand`
(added in #3760) into the unified `doublezero` binary via
`#[command(flatten)]` on the top-level `Command` enum. RFC-20 §Module
contract item 2 is now complete: the module crate owns the
serviceability subcommand enum, the binary mounts it.
- Drops 17 explicit serviceability variants from the binary's `Command`
enum: `Init`, `Migrate`, `Address`, `Balance`, `Config`, `GlobalConfig`,
`Location`, `Exchange`, `Contributor`, `Permission`, `Tenant`, `Device`,
`Link`, `AccessPass`, `User`, `Export`, `Keygen`, `Resource`. They now
surface at the top level via the flattened
`Serviceability(ServiceabilityCommand)` variant.
- Collapses the `main.rs` dispatch match block from roughly 270 lines
(the original per-resource explicit dispatch) to about 90 lines, with
one new arm doing the work for all serviceability verbs:
`Command::Serviceability(cmd) => cmd.execute(&ctx, &client, &mut
handle).await`. The remaining arms cover binary-local concerns:
daemon-control verbs (`Connect`, `Enable`, `Disable`, `Status`,
`Disconnect`, `Latency`, `Routes`), raw-`DZClient` diagnostics
(`Account`, `Accounts`, `Log`), the binary-local geolocation tree,
`InitGeolocationConfig`, the multicast dispatch (whose
`Subscribe`/`Unsubscribe`/`Publish`/`Unpublish` async arms depend on
daemon-control infrastructure), and the `Completion` generator.
- Re-exports `ServiceabilityCommand` from
`smartcontract/cli/src/cli/mod.rs` (`pub use
command::ServiceabilityCommand;`) so the binary imports through
`doublezero_serviceability_cli::cli::ServiceabilityCommand`.
- Updates the version-check skip list (`matches!`) in `main.rs` to match
against the flattened variants (`ServiceabilityCommand::Address`,
`::Balance`, `::Export`) instead of the now-gone top-level variants.

## Diff Breakdown
| Category     | Files | Lines (+/-) | Net  |
|--------------|-------|-------------|------|
| Core logic   |     2 | +84 / -313  | -229 |
| Scaffolding  |     1 | +2 / -0     |   +2 |
| Docs         |     1 | +1 / -0     |   +1 |
| **Total**    |     4 | +87 / -313  | -226 |

Substantial net deletion in the binary: the 270-line per-resource
dispatch in `main.rs` is replaced by one line, and 17 explicit variants
in `cli/command.rs` are replaced by one `#[command(flatten)]` variant.

<details>
<summary>Key files (click to expand)</summary>

- `client/doublezero/src/cli/command.rs` - top-level `Command` enum
reduces to 14 variants (7 daemon-control + 3 raw-`DZClient` + 1
geolocation + 1 `InitGeolocationConfig` + 1 multicast + 1 completion + 1
flattened serviceability). Adds a doc comment explaining the RFC-20
mounting strategy.
- `client/doublezero/src/main.rs` - replaces the ~270-line per-resource
match block with binary-stay arms + one `Command::Serviceability(cmd) =>
cmd.execute(&ctx, &client, &mut handle).await` arm. Imports for the
moved subcommand enums (`DeviceCommands`, `LinkCommands`, ...) are gone
— only multicast-group enums remain since the binary still dispatches
the Group subtree locally.
- `smartcontract/cli/src/cli/mod.rs` - `pub use
command::ServiceabilityCommand;` so the binary's import path is
`doublezero_serviceability_cli::cli::ServiceabilityCommand` (not
`::cli::command::ServiceabilityCommand`).

</details>

## Testing Verification
- `cargo check --workspace` clean.
- `make rust-test` green workspace-wide.
- `make rust-lint` clean.
- `./target/debug/doublezero --help` shows 29 visible top-level
commands, byte-identical to the pre-refactor output. Hidden variants
(`Init`, `Migrate`, `InitGeolocationConfig`, `Accounts`) remain hidden
via `#[command(hide = true)]`.
- `./target/debug/doublezero device --help`, `./target/debug/doublezero
location --help`, and `./target/debug/doublezero multicast --help`
produce the same nested verb trees.
- The `location get` async ctx-consuming reference verb still works (the
new dispatcher forwards `&ctx` to the relevant arm).
- Targets `jo/8-cli-serviceability-command-enum`; the diff shown is only
this PR's contribution. Once the chain merges to `main`, RFC-20 §Module
contract item 2 is fully delivered for the serviceability module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants