diff --git a/CHANGELOG.md b/CHANGELOG.md index 432b001b79..4a9a9652df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ All notable changes to this project will be documented in this file. - Migrate `location get` to the RFC-20 conforming verb pattern as the project's reference. `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. The verb's user-facing args, flags, table layout, and JSON schema are unchanged. The unit test consumes the shared `doublezero_cli_core::testing::cli_context_default_for_tests()` helper and continues to use the existing `MockCliCommand` (auto-generated by `#[automock]`) as the backend. Binary dispatch arms in `client/doublezero` and `controlplane/doublezero-admin` are updated to `.await` the new method; other location verbs (Create, Update, List, Delete) keep their current sync signatures and migrate opportunistically. - Add `docs/cli-standard.md`, the contributor-facing summary of RFC-20 with the `location get` worked example and pointers to the shared validators, formatters, logging facade, and test helpers in `doublezero-cli-core`. - Update `CLAUDE.md` with a CLI-standard section pointing at RFC-20, the contributor doc, and the reference verb so future contributors land on the standard quickly. + - Move the 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 module crate at `smartcontract/cli/src/cli/` per RFC-20 §Module contract item 2. Internal imports in the moved files switch from `doublezero_serviceability_cli::::*` to `crate::::*`. Binary import paths in `client/doublezero/src/{cli/command.rs,main.rs}` switch to `doublezero_serviceability_cli::cli::::*`. `cli/multicast.rs` stays in the binary because its `Subscribe`/`Unsubscribe`/`Publish`/`Unpublish` variants are async and depend on binary-local daemon-control infrastructure (`ServiceControllerImpl`, `crate::command::helpers::resolve_client_ip`); the binary now imports `MulticastGroupCliCommand` from the library. + - Introduce `doublezero_serviceability_cli::cli::ServiceabilityCommand`, the module crate's top-level subcommand enum + `async fn execute(ctx, client, out)` dispatcher 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`) and owns the full dispatch tree currently inlined in `client/doublezero/src/main.rs`. Defined but not yet wired into the unified binary; the next PR adds `#[command(flatten)] Serviceability(ServiceabilityCommand)` to the binary's `Command` enum and collapses `main.rs` to a single dispatch arm. Binary `Command` enum and `main.rs` dispatch are unchanged; this is pure file relocation. ## [v0.24.0](https://github.com/malbeclabs/doublezero/compare/client/v0.23.0...client/v0.24.0) - 2026-05-22 diff --git a/client/doublezero/src/cli/command.rs b/client/doublezero/src/cli/command.rs index d0ae55cd01..97332e210b 100644 --- a/client/doublezero/src/cli/command.rs +++ b/client/doublezero/src/cli/command.rs @@ -1,12 +1,5 @@ -use super::multicast::MulticastCliCommand; use crate::{ - cli::{ - accesspass::AccessPassCliCommand, config::ConfigCliCommand, - contributor::ContributorCliCommand, device::DeviceCliCommand, exchange::ExchangeCliCommand, - geolocation::GeolocationCliCommand, globalconfig::GlobalConfigCliCommand, - link::LinkCliCommand, location::LocationCliCommand, permission::PermissionCliCommand, - resource::ResourceCliCommand, tenant::TenantCliCommand, user::UserCliCommand, - }, + cli::{geolocation::GeolocationCliCommand, multicast::MulticastCliCommand}, command::{ connect::ProvisioningCliCommand, disable::DisableCliCommand, disconnect::DecommissioningCliCommand, enable::EnableCliCommand, @@ -16,10 +9,23 @@ use crate::{ use clap::{Args, Subcommand}; use clap_complete::Shell; use doublezero_serviceability_cli::{ - account::GetAccountCliCommand, accounts::GetAccountsCliCommand, address::AddressCliCommand, - balance::BalanceCliCommand, export::ExportCliCommand, - geolocation::programconfig::init::InitProgramConfigCliCommand, init::InitCliCommand, - keygen::KeyGenCliCommand, logcommand::LogCliCommand, migrate::MigrateCliCommand, + account::GetAccountCliCommand, + accounts::GetAccountsCliCommand, + address::AddressCliCommand, + balance::BalanceCliCommand, + cli::{ + accesspass::AccessPassCliCommand, config::ConfigCliCommand, + contributor::ContributorCliCommand, device::DeviceCliCommand, exchange::ExchangeCliCommand, + globalconfig::GlobalConfigCliCommand, link::LinkCliCommand, location::LocationCliCommand, + permission::PermissionCliCommand, resource::ResourceCliCommand, tenant::TenantCliCommand, + user::UserCliCommand, + }, + export::ExportCliCommand, + geolocation::programconfig::init::InitProgramConfigCliCommand, + init::InitCliCommand, + keygen::KeyGenCliCommand, + logcommand::LogCliCommand, + migrate::MigrateCliCommand, }; #[derive(Subcommand, Debug)] diff --git a/client/doublezero/src/cli/mod.rs b/client/doublezero/src/cli/mod.rs index 4661143e46..9b9c38b536 100644 --- a/client/doublezero/src/cli/mod.rs +++ b/client/doublezero/src/cli/mod.rs @@ -1,16 +1,3 @@ -pub mod accesspass; pub mod command; -pub mod config; -pub mod contributor; -pub mod device; -pub mod exchange; pub mod geolocation; -pub mod globalconfig; -pub mod link; -pub mod location; pub mod multicast; -pub mod multicastgroup; -pub mod permission; -pub mod resource; -pub mod tenant; -pub mod user; diff --git a/client/doublezero/src/cli/multicast.rs b/client/doublezero/src/cli/multicast.rs index 55e2335408..c7064a6956 100644 --- a/client/doublezero/src/cli/multicast.rs +++ b/client/doublezero/src/cli/multicast.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use super::multicastgroup::MulticastGroupCliCommand; +use doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCliCommand; #[derive(Args, Debug)] pub struct MulticastCliCommand { diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index ba63dfd3ec..d6e8cf2a34 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -10,26 +10,30 @@ mod requirements; mod servicecontroller; use crate::cli::{ command::Command, - config::ConfigCommands, - device::{DeviceCommands, InterfaceCommands}, - exchange::ExchangeCommands, geolocation::{ probe::ProbeCommands, user::UserCommands as GeoUserCommands, GeolocationCommands, }, - globalconfig::{ - AirdropCommands, AuthorityCommands, FeatureFlagsCommands, FoundationAllowlistCommands, - GlobalConfigCommands, QaAllowlistCommands, - }, - link::{LinkCommands, TopologyCommands}, - location::LocationCommands, - user::UserCommands, }; use doublezero_cli_core::LogLevel; use doublezero_sdk::{geolocation::client::GeoClient, DZClient, ProgramVersion}; use doublezero_serviceability::pda::get_globalstate_pda; use doublezero_serviceability_cli::{ - checkversion::check_version, doublezerocommand::CliCommandImpl, - geoclicommand::GeoCliCommandImpl, version::VersionCliCommand, + checkversion::check_version, + cli::{ + config::ConfigCommands, + device::{DeviceCommands, InterfaceCommands}, + exchange::ExchangeCommands, + globalconfig::{ + AirdropCommands, AuthorityCommands, FeatureFlagsCommands, FoundationAllowlistCommands, + GlobalConfigCommands, QaAllowlistCommands, + }, + link::{LinkCommands, TopologyCommands}, + location::LocationCommands, + user::UserCommands, + }, + doublezerocommand::CliCommandImpl, + geoclicommand::GeoCliCommandImpl, + version::VersionCliCommand, }; use servicecontroller::ServiceControllerImpl; @@ -332,37 +336,37 @@ async fn main() -> eyre::Result<()> { ExchangeCommands::Delete(args) => args.execute(&client, &mut handle), }, Command::Contributor(command) => match command.command { - cli::contributor::ContributorCommands::Create(args) => { + doublezero_serviceability_cli::cli::contributor::ContributorCommands::Create(args) => { args.execute(&client, &mut handle) } - cli::contributor::ContributorCommands::Update(args) => { + doublezero_serviceability_cli::cli::contributor::ContributorCommands::Update(args) => { args.execute(&client, &mut handle) } - cli::contributor::ContributorCommands::List(args) => args.execute(&client, &mut handle), - cli::contributor::ContributorCommands::Get(args) => args.execute(&client, &mut handle), - cli::contributor::ContributorCommands::Delete(args) => { + doublezero_serviceability_cli::cli::contributor::ContributorCommands::List(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::contributor::ContributorCommands::Get(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::contributor::ContributorCommands::Delete(args) => { args.execute(&client, &mut handle) } }, Command::Permission(command) => match command.command { - cli::permission::PermissionCommands::Set(args) => args.execute(&client, &mut handle), - cli::permission::PermissionCommands::Suspend(args) => { + doublezero_serviceability_cli::cli::permission::PermissionCommands::Set(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::permission::PermissionCommands::Suspend(args) => { args.execute(&client, &mut handle) } - cli::permission::PermissionCommands::Resume(args) => args.execute(&client, &mut handle), - cli::permission::PermissionCommands::Delete(args) => args.execute(&client, &mut handle), - cli::permission::PermissionCommands::Get(args) => args.execute(&client, &mut handle), - cli::permission::PermissionCommands::List(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::permission::PermissionCommands::Resume(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::permission::PermissionCommands::Delete(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::permission::PermissionCommands::Get(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::permission::PermissionCommands::List(args) => args.execute(&client, &mut handle), }, Command::Tenant(command) => match command.command { - cli::tenant::TenantCommands::Create(args) => args.execute(&client, &mut handle), - cli::tenant::TenantCommands::Update(args) => args.execute(&client, &mut handle), - cli::tenant::TenantCommands::List(args) => args.execute(&client, &mut handle), - cli::tenant::TenantCommands::Get(args) => args.execute(&client, &mut handle), - cli::tenant::TenantCommands::Delete(args) => args.execute(&client, &mut handle), - cli::tenant::TenantCommands::Administrator(command) => match command.command { - cli::tenant::AdministratorCommands::Add(args) => args.execute(&client, &mut handle), - cli::tenant::AdministratorCommands::Remove(args) => { + doublezero_serviceability_cli::cli::tenant::TenantCommands::Create(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::TenantCommands::Update(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::TenantCommands::List(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::TenantCommands::Get(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::TenantCommands::Delete(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::TenantCommands::Administrator(command) => match command.command { + doublezero_serviceability_cli::cli::tenant::AdministratorCommands::Add(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::tenant::AdministratorCommands::Remove(args) => { args.execute(&client, &mut handle) } }, @@ -384,8 +388,8 @@ async fn main() -> eyre::Result<()> { }, Command::Link(command) => match command.command { LinkCommands::Create(args) => match args.command { - cli::link::CreateLinkCommands::Wan(args) => args.execute(&client, &mut handle), - cli::link::CreateLinkCommands::Dzx(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::link::CreateLinkCommands::Wan(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::link::CreateLinkCommands::Dzx(args) => args.execute(&client, &mut handle), }, LinkCommands::Accept(args) => args.execute(&client, &mut handle), LinkCommands::Update(args) => args.execute(&client, &mut handle), @@ -403,14 +407,14 @@ async fn main() -> eyre::Result<()> { }, }, Command::AccessPass(command) => match command.command { - cli::accesspass::AccessPassCommands::Set(args) => args.execute(&client, &mut handle), - cli::accesspass::AccessPassCommands::Close(args) => args.execute(&client, &mut handle), - cli::accesspass::AccessPassCommands::List(args) => args.execute(&client, &mut handle), - cli::accesspass::AccessPassCommands::Get(args) => args.execute(&client, &mut handle), - cli::accesspass::AccessPassCommands::UserBalances(args) => { + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::Set(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::Close(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::List(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::Get(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::UserBalances(args) => { args.execute(&client, &mut handle) } - cli::accesspass::AccessPassCommands::Fund(args) => { + doublezero_serviceability_cli::cli::accesspass::AccessPassCommands::Fund(args) => { args.execute(&client, &mut handle, &mut std::io::stdin().lock()) } }, @@ -426,49 +430,49 @@ async fn main() -> eyre::Result<()> { }, Command::Multicast(args) => match args.command { cli::multicast::MulticastCommands::Group(args) => match args.command { - cli::multicastgroup::MulticastGroupCommands::Allowlist(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::Allowlist(args) => { match args.command { - cli::multicastgroup::MulticastGroupAllowlistCommands::Publisher(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupAllowlistCommands::Publisher(args) => { match args.command { - cli::multicastgroup::MulticastGroupPubAllowlistCommands::List( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupPubAllowlistCommands::List( args, ) => args.execute(&client, &mut handle), - cli::multicastgroup::MulticastGroupPubAllowlistCommands::Add( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupPubAllowlistCommands::Add( args, ) => args.execute(&client, &mut handle), - cli::multicastgroup::MulticastGroupPubAllowlistCommands::Remove( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupPubAllowlistCommands::Remove( args, ) => args.execute(&client, &mut handle), } } - cli::multicastgroup::MulticastGroupAllowlistCommands::Subscriber(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupAllowlistCommands::Subscriber(args) => { match args.command { - cli::multicastgroup::MulticastGroupSubAllowlistCommands::List( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupSubAllowlistCommands::List( args, ) => args.execute(&client, &mut handle), - cli::multicastgroup::MulticastGroupSubAllowlistCommands::Add( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupSubAllowlistCommands::Add( args, ) => args.execute(&client, &mut handle), - cli::multicastgroup::MulticastGroupSubAllowlistCommands::Remove( + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupSubAllowlistCommands::Remove( args, ) => args.execute(&client, &mut handle), } } } } - cli::multicastgroup::MulticastGroupCommands::Create(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::Create(args) => { args.execute(&client, &mut handle) } - cli::multicastgroup::MulticastGroupCommands::Update(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::Update(args) => { args.execute(&client, &mut handle) } - cli::multicastgroup::MulticastGroupCommands::List(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::List(args) => { args.execute(&client, &mut handle) } - cli::multicastgroup::MulticastGroupCommands::Get(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::Get(args) => { args.execute(&client, &mut handle) } - cli::multicastgroup::MulticastGroupCommands::Delete(args) => { + doublezero_serviceability_cli::cli::multicastgroup::MulticastGroupCommands::Delete(args) => { args.execute(&client, &mut handle) } }, @@ -511,12 +515,12 @@ async fn main() -> eyre::Result<()> { } Command::Resource(command) => match command.command { - cli::resource::ResourceCommands::Allocate(args) => args.execute(&client, &mut handle), - cli::resource::ResourceCommands::Create(args) => args.execute(&client, &mut handle), - cli::resource::ResourceCommands::Deallocate(args) => args.execute(&client, &mut handle), - cli::resource::ResourceCommands::Get(args) => args.execute(&client, &mut handle), - cli::resource::ResourceCommands::Close(args) => args.execute(&client, &mut handle), - cli::resource::ResourceCommands::Verify(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Allocate(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Create(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Deallocate(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Get(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Close(args) => args.execute(&client, &mut handle), + doublezero_serviceability_cli::cli::resource::ResourceCommands::Verify(args) => args.execute(&client, &mut handle), }, Command::Export(args) => args.execute(&client, &mut handle), diff --git a/client/doublezero/src/cli/accesspass.rs b/smartcontract/cli/src/cli/accesspass.rs similarity index 95% rename from client/doublezero/src/cli/accesspass.rs rename to smartcontract/cli/src/cli/accesspass.rs index 8dc90b753e..c32b9e2f3c 100644 --- a/client/doublezero/src/cli/accesspass.rs +++ b/smartcontract/cli/src/cli/accesspass.rs @@ -1,9 +1,9 @@ -use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::accesspass::{ +use crate::accesspass::{ close::CloseAccessPassCliCommand, fund::FundAccessPassCliCommand, get::GetAccessPassCliCommand, list::ListAccessPassCliCommand, set::SetAccessPassCliCommand, user_balances::UserBalancesAccessPassCliCommand, }; +use clap::{Args, Subcommand}; #[derive(Args, Debug)] pub struct AccessPassCliCommand { diff --git a/smartcontract/cli/src/cli/command.rs b/smartcontract/cli/src/cli/command.rs new file mode 100644 index 0000000000..05ba197330 --- /dev/null +++ b/smartcontract/cli/src/cli/command.rs @@ -0,0 +1,375 @@ +//! Top-level serviceability subcommand enum per RFC-20 §Module contract. +//! +//! The unified `doublezero` binary mounts this enum via `#[command(flatten)]` +//! so its variants surface as top-level commands (`doublezero device list`, +//! `doublezero location get`, ...). The binary keeps its own `Command` enum +//! for binary-local verbs (daemon-control, geolocation, completion, and the +//! raw-`DZClient` diagnostics like `Account`, `Accounts`, `Log`). + +use clap::Subcommand; +use doublezero_cli_core::CliContext; +use std::io::Write; + +use crate::{ + address::AddressCliCommand, + balance::BalanceCliCommand, + cli::{ + accesspass::{AccessPassCliCommand, AccessPassCommands}, + config::{ConfigCliCommand, ConfigCommands}, + contributor::{ContributorCliCommand, ContributorCommands}, + device::{DeviceCliCommand, DeviceCommands, InterfaceCommands}, + exchange::{ExchangeCliCommand, ExchangeCommands}, + globalconfig::{ + AirdropCommands, AuthorityCommands, FeatureFlagsCommands, FoundationAllowlistCommands, + GlobalConfigCliCommand, GlobalConfigCommands, QaAllowlistCommands, + }, + link::{CreateLinkCommands, LinkCliCommand, LinkCommands, TopologyCommands}, + location::{LocationCliCommand, LocationCommands}, + permission::{PermissionCliCommand, PermissionCommands}, + resource::{ResourceCliCommand, ResourceCommands}, + tenant::{AdministratorCommands, TenantCliCommand, TenantCommands}, + user::{UserCliCommand, UserCommands}, + }, + doublezerocommand::CliCommand, + export::ExportCliCommand, + init::InitCliCommand, + keygen::KeyGenCliCommand, + migrate::MigrateCliCommand, +}; + +#[derive(Subcommand, Debug)] +pub enum ServiceabilityCommand { + #[command(hide = true)] + Init(InitCliCommand), + #[command(hide = true)] + Migrate(MigrateCliCommand), + + /// Get your public key + Address(AddressCliCommand), + /// Get your balance + Balance(BalanceCliCommand), + + /// local configuration + Config(ConfigCliCommand), + /// Global network configuration + GlobalConfig(GlobalConfigCliCommand), + + /// Manage locations + Location(LocationCliCommand), + /// Manage exchanges + Exchange(ExchangeCliCommand), + /// Manage contributors + Contributor(ContributorCliCommand), + /// Manage permissions + Permission(PermissionCliCommand), + /// Manage tenants + Tenant(TenantCliCommand), + /// Manage devices + Device(DeviceCliCommand), + /// Manage tunnels between devices + Link(LinkCliCommand), + /// Manage access passes + AccessPass(AccessPassCliCommand), + /// Manage users + User(UserCliCommand), + + /// Export all data to files + Export(ExportCliCommand), + /// Create a new user identity + Keygen(KeyGenCliCommand), + + /// IP/ID Resource Management + Resource(ResourceCliCommand), +} + +impl ServiceabilityCommand { + /// Dispatch a serviceability verb to its implementation. + /// + /// `ctx` is forwarded to verbs that consume it (today only + /// `location get` per RFC-20 PR 5; other verbs are sync and ignore it). + pub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()> + where + C: CliCommand, + W: Write, + { + match self { + Self::Init(args) => args.execute(client, out), + Self::Migrate(args) => args.execute(client, out), + Self::Address(args) => args.execute(client, out), + Self::Balance(args) => args.execute(client, out), + Self::Export(args) => args.execute(client, out), + Self::Keygen(args) => args.execute(client, out), + + Self::Config(cmd) => match cmd.command { + ConfigCommands::Get(args) => args.execute(client, out), + ConfigCommands::Set(args) => args.execute(client, out), + }, + Self::GlobalConfig(cmd) => match cmd.command { + GlobalConfigCommands::Set(args) => args.execute(client, out), + GlobalConfigCommands::Get(args) => args.execute(client, out), + GlobalConfigCommands::Airdrop(c) => match c.command { + AirdropCommands::Set(args) => args.execute(client, out), + AirdropCommands::Get(args) => args.execute(client, out), + }, + GlobalConfigCommands::Authority(c) => match c.command { + AuthorityCommands::Set(args) => args.execute(client, out), + AuthorityCommands::Get(args) => args.execute(client, out), + }, + GlobalConfigCommands::Allowlist(c) => match c.command { + FoundationAllowlistCommands::List(args) => args.execute(client, out), + FoundationAllowlistCommands::Add(args) => args.execute(client, out), + FoundationAllowlistCommands::Remove(args) => args.execute(client, out), + }, + GlobalConfigCommands::QaAllowlist(c) => match c.command { + QaAllowlistCommands::List(args) => args.execute(client, out), + QaAllowlistCommands::Add(args) => args.execute(client, out), + QaAllowlistCommands::Remove(args) => args.execute(client, out), + }, + GlobalConfigCommands::SetVersion(args) => args.execute(client, out), + GlobalConfigCommands::FeatureFlags(c) => match c.command { + FeatureFlagsCommands::Get(args) => args.execute(client, out), + FeatureFlagsCommands::Set(args) => args.execute(client, out), + }, + }, + + Self::Location(cmd) => match cmd.command { + LocationCommands::Create(args) => args.execute(client, out), + LocationCommands::Update(args) => args.execute(client, out), + LocationCommands::List(args) => args.execute(client, out), + LocationCommands::Get(args) => args.execute(ctx, client, out).await, + LocationCommands::Delete(args) => args.execute(client, out), + }, + Self::Exchange(cmd) => match cmd.command { + ExchangeCommands::Create(args) => args.execute(client, out), + ExchangeCommands::SetDevice(args) => args.execute(client, out), + ExchangeCommands::Update(args) => args.execute(client, out), + ExchangeCommands::List(args) => args.execute(client, out), + ExchangeCommands::Get(args) => args.execute(client, out), + ExchangeCommands::Delete(args) => args.execute(client, out), + }, + Self::Contributor(cmd) => match cmd.command { + ContributorCommands::Create(args) => args.execute(client, out), + ContributorCommands::Update(args) => args.execute(client, out), + ContributorCommands::List(args) => args.execute(client, out), + ContributorCommands::Get(args) => args.execute(client, out), + ContributorCommands::Delete(args) => args.execute(client, out), + }, + Self::Permission(cmd) => match cmd.command { + PermissionCommands::Set(args) => args.execute(client, out), + PermissionCommands::Suspend(args) => args.execute(client, out), + PermissionCommands::Resume(args) => args.execute(client, out), + PermissionCommands::Delete(args) => args.execute(client, out), + PermissionCommands::Get(args) => args.execute(client, out), + PermissionCommands::List(args) => args.execute(client, out), + }, + Self::Tenant(cmd) => match cmd.command { + TenantCommands::Create(args) => args.execute(client, out), + TenantCommands::Update(args) => args.execute(client, out), + TenantCommands::List(args) => args.execute(client, out), + TenantCommands::Get(args) => args.execute(client, out), + TenantCommands::Delete(args) => args.execute(client, out), + TenantCommands::Administrator(c) => match c.command { + AdministratorCommands::Add(args) => args.execute(client, out), + AdministratorCommands::Remove(args) => args.execute(client, out), + }, + }, + Self::Device(cmd) => match cmd.command { + DeviceCommands::Create(args) => args.execute(client, out), + DeviceCommands::Update(args) => args.execute(client, out), + DeviceCommands::List(args) => args.execute(client, out), + DeviceCommands::Get(args) => args.execute(client, out), + DeviceCommands::Delete(args) => args.execute(client, out), + DeviceCommands::Interface(c) => match c.command { + InterfaceCommands::Create(args) => args.execute(client, out), + InterfaceCommands::Update(args) => args.execute(client, out), + InterfaceCommands::List(args) => args.execute(client, out), + InterfaceCommands::Get(args) => args.execute(client, out), + InterfaceCommands::Delete(args) => args.execute(client, out), + }, + DeviceCommands::SetHealth(args) => args.execute(client, out), + }, + Self::Link(cmd) => match cmd.command { + LinkCommands::Create(args) => match args.command { + CreateLinkCommands::Wan(args) => args.execute(client, out), + CreateLinkCommands::Dzx(args) => args.execute(client, out), + }, + LinkCommands::Accept(args) => args.execute(client, out), + LinkCommands::Update(args) => args.execute(client, out), + LinkCommands::List(args) => args.execute(client, out), + LinkCommands::Get(args) => args.execute(client, out), + LinkCommands::Latency(args) => args.execute(client, out), + LinkCommands::Delete(args) => args.execute(client, out), + LinkCommands::SetHealth(args) => args.execute(client, out), + LinkCommands::Topology(t) => match t.command { + TopologyCommands::Create(args) => args.execute(client, out), + TopologyCommands::Delete(args) => args.execute(client, out), + TopologyCommands::Clear(args) => args.execute(client, out), + TopologyCommands::AssignNodeSegments(args) => args.execute(client, out), + TopologyCommands::List(args) => args.execute(client, out), + }, + }, + Self::AccessPass(cmd) => match cmd.command { + AccessPassCommands::Set(args) => args.execute(client, out), + AccessPassCommands::Close(args) => args.execute(client, out), + AccessPassCommands::List(args) => args.execute(client, out), + AccessPassCommands::Get(args) => args.execute(client, out), + AccessPassCommands::UserBalances(args) => args.execute(client, out), + AccessPassCommands::Fund(args) => { + args.execute(client, out, &mut std::io::stdin().lock()) + } + }, + Self::User(cmd) => match cmd.command { + UserCommands::Create(args) => args.execute(client, out), + UserCommands::CreateSubscribe(args) => args.execute(client, out), + UserCommands::Subscribe(args) => args.execute(client, out), + UserCommands::Update(args) => args.execute(client, out), + UserCommands::List(args) => args.execute(client, out), + UserCommands::Get(args) => args.execute(client, out), + UserCommands::Delete(args) => args.execute(client, out), + UserCommands::RequestBan(args) => args.execute(client, out), + }, + Self::Resource(cmd) => match cmd.command { + ResourceCommands::Allocate(args) => args.execute(client, out), + ResourceCommands::Create(args) => args.execute(client, out), + ResourceCommands::Deallocate(args) => args.execute(client, out), + ResourceCommands::Get(args) => args.execute(client, out), + ResourceCommands::Close(args) => args.execute(client, out), + ResourceCommands::Verify(args) => args.execute(client, out), + }, + } + } +} + +#[cfg(test)] +mod tests { + //! Parse-level parity tests for `ServiceabilityCommand`. + //! + //! Once the unified `doublezero` binary mounts this enum via + //! `#[command(flatten)]`, the variant tree below becomes the user-facing + //! parse tree for every serviceability verb. A wrong `Subcommand` attribute + //! or a misrouted nested enum here would land directly in production, so we + //! pin the representative chains to specific variants. + //! + //! The tests cover parse-time routing only - they do not invoke `execute`. + //! Per-verb behavior is covered by inline tests next to each leaf command. + use super::*; + use crate::cli::{device::InterfaceCliCommand, link::CreateLinkCommand}; + use clap::Parser; + + #[derive(Parser, Debug)] + struct TestCli { + #[command(subcommand)] + command: ServiceabilityCommand, + } + + /// The system program id: a syntactically valid 32-byte base58 pubkey. + /// Used wherever a verb takes an identifier validated by + /// `validate_pubkey_or_code`. + const TEST_PUBKEY: &str = "11111111111111111111111111111111"; + + #[test] + fn parses_location_get() { + let parsed = + TestCli::try_parse_from(["test", "location", "get", "--code", TEST_PUBKEY]).unwrap(); + assert!(matches!( + parsed.command, + ServiceabilityCommand::Location(LocationCliCommand { + command: LocationCommands::Get(_), + }) + )); + } + + #[test] + fn parses_device_interface_get() { + let parsed = TestCli::try_parse_from([ + "test", + "device", + "interface", + "get", + TEST_PUBKEY, + "Ethernet1", + ]) + .unwrap(); + assert!(matches!( + parsed.command, + ServiceabilityCommand::Device(DeviceCliCommand { + command: DeviceCommands::Interface(InterfaceCliCommand { + command: InterfaceCommands::Get(_), + }), + }) + )); + } + + #[test] + fn parses_link_create_wan() { + let parsed = TestCli::try_parse_from([ + "test", + "link", + "create", + "wan", + "--code", + "test-link", + "--contributor", + TEST_PUBKEY, + "--side-a", + TEST_PUBKEY, + "--side-a-interface", + "Ethernet1", + "--side-z", + TEST_PUBKEY, + "--side-z-interface", + "Ethernet2", + "--bandwidth", + "1Gbps", + "--delay-ms", + "5", + "--jitter-ms", + "1", + ]) + .unwrap(); + assert!(matches!( + parsed.command, + ServiceabilityCommand::Link(LinkCliCommand { + command: LinkCommands::Create(CreateLinkCommand { + command: CreateLinkCommands::Wan(_), + }), + }) + )); + } + + #[test] + fn parses_access_pass_fund() { + let parsed = TestCli::try_parse_from(["test", "access-pass", "fund"]).unwrap(); + assert!(matches!( + parsed.command, + ServiceabilityCommand::AccessPass(AccessPassCliCommand { + command: AccessPassCommands::Fund(_), + }) + )); + } + + #[test] + fn parses_resource_verify() { + let parsed = TestCli::try_parse_from(["test", "resource", "verify"]).unwrap(); + assert!(matches!( + parsed.command, + ServiceabilityCommand::Resource(ResourceCliCommand { + command: ResourceCommands::Verify(_), + }) + )); + } + + // `hide = true` must not gate parsing - operators and automation rely on + // these verbs being reachable even though they do not appear in --help. + #[test] + fn parses_hidden_init() { + let parsed = TestCli::try_parse_from(["test", "init"]).unwrap(); + assert!(matches!(parsed.command, ServiceabilityCommand::Init(_))); + } + + #[test] + fn parses_hidden_migrate() { + let parsed = TestCli::try_parse_from(["test", "migrate"]).unwrap(); + assert!(matches!(parsed.command, ServiceabilityCommand::Migrate(_))); + } +} diff --git a/client/doublezero/src/cli/config.rs b/smartcontract/cli/src/cli/config.rs similarity index 78% rename from client/doublezero/src/cli/config.rs rename to smartcontract/cli/src/cli/config.rs index 47021ded09..58f2288535 100644 --- a/client/doublezero/src/cli/config.rs +++ b/smartcontract/cli/src/cli/config.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::config::{get::GetConfigCliCommand, set::SetConfigCliCommand}; +use crate::config::{get::GetConfigCliCommand, set::SetConfigCliCommand}; #[derive(Args, Debug)] pub struct ConfigCliCommand { diff --git a/client/doublezero/src/cli/contributor.rs b/smartcontract/cli/src/cli/contributor.rs similarity index 86% rename from client/doublezero/src/cli/contributor.rs rename to smartcontract/cli/src/cli/contributor.rs index 5254911278..91f61349d7 100644 --- a/client/doublezero/src/cli/contributor.rs +++ b/smartcontract/cli/src/cli/contributor.rs @@ -1,8 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::contributor::{ - create::*, delete::*, get::*, list::*, update::*, -}; +use crate::contributor::{create::*, delete::*, get::*, list::*, update::*}; #[derive(Args, Debug)] pub struct ContributorCliCommand { diff --git a/client/doublezero/src/cli/device.rs b/smartcontract/cli/src/cli/device.rs similarity index 97% rename from client/doublezero/src/cli/device.rs rename to smartcontract/cli/src/cli/device.rs index 08921a8cc8..14e0e8d92d 100644 --- a/client/doublezero/src/cli/device.rs +++ b/smartcontract/cli/src/cli/device.rs @@ -1,5 +1,4 @@ -use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::device::{ +use crate::device::{ create::CreateDeviceCliCommand, delete::DeleteDeviceCliCommand, get::GetDeviceCliCommand, @@ -12,6 +11,7 @@ use doublezero_serviceability_cli::device::{ sethealth::SetDeviceHealthCliCommand, update::UpdateDeviceCliCommand, }; +use clap::{Args, Subcommand}; #[derive(Debug, Subcommand)] pub enum InterfaceCommands { diff --git a/client/doublezero/src/cli/exchange.rs b/smartcontract/cli/src/cli/exchange.rs similarity index 94% rename from client/doublezero/src/cli/exchange.rs rename to smartcontract/cli/src/cli/exchange.rs index a468a4b526..9af2f1e044 100644 --- a/client/doublezero/src/cli/exchange.rs +++ b/smartcontract/cli/src/cli/exchange.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::exchange::{ +use crate::exchange::{ create::*, delete::*, get::*, list::*, setdevice::SetDeviceExchangeCliCommand, update::*, }; diff --git a/client/doublezero/src/cli/globalconfig.rs b/smartcontract/cli/src/cli/globalconfig.rs similarity index 99% rename from client/doublezero/src/cli/globalconfig.rs rename to smartcontract/cli/src/cli/globalconfig.rs index 2dfb4b6aaa..1ba1660456 100644 --- a/client/doublezero/src/cli/globalconfig.rs +++ b/smartcontract/cli/src/cli/globalconfig.rs @@ -1,5 +1,4 @@ -use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::{ +use crate::{ allowlist::{ foundation::{ add::AddFoundationAllowlistCliCommand, list::ListFoundationAllowlistCliCommand, @@ -16,6 +15,7 @@ use doublezero_serviceability_cli::{ setversion::SetVersionCliCommand, }, }; +use clap::{Args, Subcommand}; #[derive(Args, Debug)] pub struct GlobalConfigCliCommand { diff --git a/client/doublezero/src/cli/link.rs b/smartcontract/cli/src/cli/link.rs similarity index 98% rename from client/doublezero/src/cli/link.rs rename to smartcontract/cli/src/cli/link.rs index d67ba4f9d2..fe075fcad5 100644 --- a/client/doublezero/src/cli/link.rs +++ b/smartcontract/cli/src/cli/link.rs @@ -1,5 +1,4 @@ -use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::{ +use crate::{ link::{ accept::AcceptLinkCliCommand, delete::*, dzx_create::CreateDZXLinkCliCommand, get::*, latency::LinkLatencyCliCommand, list::*, sethealth::SetLinkHealthCliCommand, update::*, @@ -11,6 +10,7 @@ use doublezero_serviceability_cli::{ list::ListTopologyCliCommand, }, }; +use clap::{Args, Subcommand}; #[derive(Args, Debug)] pub struct LinkCliCommand { diff --git a/client/doublezero/src/cli/location.rs b/smartcontract/cli/src/cli/location.rs similarity index 86% rename from client/doublezero/src/cli/location.rs rename to smartcontract/cli/src/cli/location.rs index d14c7dd1a2..5f6aa2232e 100644 --- a/client/doublezero/src/cli/location.rs +++ b/smartcontract/cli/src/cli/location.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::location::{create::*, delete::*, get::*, list::*, update::*}; +use crate::location::{create::*, delete::*, get::*, list::*, update::*}; #[derive(Args, Debug)] pub struct LocationCliCommand { diff --git a/smartcontract/cli/src/cli/mod.rs b/smartcontract/cli/src/cli/mod.rs new file mode 100644 index 0000000000..8662eb9027 --- /dev/null +++ b/smartcontract/cli/src/cli/mod.rs @@ -0,0 +1,23 @@ +//! Module-crate-owned subcommand enums per RFC-20 (§Module contract item 2). +//! +//! Each file in this module defines one resource's clap `Subcommand` enum +//! (`DeviceCommands`, `LinkCommands`, ...) wrapping the per-verb args types +//! that live next to the verbs themselves (`crate::device::create::*`, +//! `crate::link::create::*`, ...). A future PR adds a top-level +//! `ServiceabilityCommand` aggregator and hoists these variants into the +//! unified `doublezero` binary via `#[command(flatten)]`. + +pub mod accesspass; +pub mod command; +pub mod config; +pub mod contributor; +pub mod device; +pub mod exchange; +pub mod globalconfig; +pub mod link; +pub mod location; +pub mod multicastgroup; +pub mod permission; +pub mod resource; +pub mod tenant; +pub mod user; diff --git a/client/doublezero/src/cli/multicastgroup.rs b/smartcontract/cli/src/cli/multicastgroup.rs similarity index 98% rename from client/doublezero/src/cli/multicastgroup.rs rename to smartcontract/cli/src/cli/multicastgroup.rs index ba135239b2..8e77e66b51 100644 --- a/client/doublezero/src/cli/multicastgroup.rs +++ b/smartcontract/cli/src/cli/multicastgroup.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::multicastgroup::{ +use crate::multicastgroup::{ allowlist::{ publisher::{ add::AddMulticastGroupPubAllowlistCliCommand, diff --git a/client/doublezero/src/cli/permission.rs b/smartcontract/cli/src/cli/permission.rs similarity index 87% rename from client/doublezero/src/cli/permission.rs rename to smartcontract/cli/src/cli/permission.rs index f773ce4580..d258d66e12 100644 --- a/client/doublezero/src/cli/permission.rs +++ b/smartcontract/cli/src/cli/permission.rs @@ -1,8 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::permission::{ - delete::*, get::*, list::*, resume::*, set::*, suspend::*, -}; +use crate::permission::{delete::*, get::*, list::*, resume::*, set::*, suspend::*}; #[derive(Args, Debug)] pub struct PermissionCliCommand { diff --git a/client/doublezero/src/cli/resource.rs b/smartcontract/cli/src/cli/resource.rs similarity index 95% rename from client/doublezero/src/cli/resource.rs rename to smartcontract/cli/src/cli/resource.rs index 7fc8f679fb..bb7886edbd 100644 --- a/client/doublezero/src/cli/resource.rs +++ b/smartcontract/cli/src/cli/resource.rs @@ -1,9 +1,9 @@ -use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::resource::{ +use crate::resource::{ allocate::AllocateResourceCliCommand, close::CloseResourceCliCommand, create::CreateResourceCliCommand, deallocate::DeallocateResourceCliCommand, get::GetResourceCliCommand, verify::VerifyResourceCliCommand, }; +use clap::{Args, Subcommand}; #[derive(Args, Debug)] pub struct ResourceCliCommand { diff --git a/client/doublezero/src/cli/tenant.rs b/smartcontract/cli/src/cli/tenant.rs similarity index 96% rename from client/doublezero/src/cli/tenant.rs rename to smartcontract/cli/src/cli/tenant.rs index 627b937618..c6bcd76211 100644 --- a/client/doublezero/src/cli/tenant.rs +++ b/smartcontract/cli/src/cli/tenant.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::tenant::{ +use crate::tenant::{ add_administrator::*, create::*, delete::*, get::*, list::*, remove_administrator::*, update::*, }; diff --git a/client/doublezero/src/cli/user.rs b/smartcontract/cli/src/cli/user.rs similarity index 96% rename from client/doublezero/src/cli/user.rs rename to smartcontract/cli/src/cli/user.rs index 84394874d3..10e3610afb 100644 --- a/client/doublezero/src/cli/user.rs +++ b/smartcontract/cli/src/cli/user.rs @@ -1,6 +1,6 @@ use clap::{Args, Subcommand}; -use doublezero_serviceability_cli::user::{ +use crate::user::{ create::CreateUserCliCommand, create_subscribe::CreateSubscribeUserCliCommand, delete::DeleteUserCliCommand, get::GetUserCliCommand, list::ListUserCliCommand, request_ban::RequestBanUserCliCommand, subscribe::SubscribeUserCliCommand, diff --git a/smartcontract/cli/src/lib.rs b/smartcontract/cli/src/lib.rs index 14f5d11498..5ef51ebf92 100644 --- a/smartcontract/cli/src/lib.rs +++ b/smartcontract/cli/src/lib.rs @@ -5,6 +5,7 @@ pub mod address; pub mod allowlist; pub mod balance; pub mod checkversion; +pub mod cli; pub mod config; pub mod contributor; pub mod device;