Files
harmony/ROADMAP/13-unified-cli.md
Jean-Gabriel Gill-Couture edb62668b6
All checks were successful
Run Check Script / check (pull_request) Successful in 2m19s
doc: Roadmap entry for cli design and implementation
2026-05-31 12:56:36 -04:00

10 KiB

Phase 13: Unified CLI — Extensible, Composable, Subcommand-Driven

Goal

Replace the current landscape of disconnected harmony-* binaries and 60+ example main.rs files with a single, extensible CLI where:

  • The framework provides global concerns (config, SSO, topology selection, Score runner, TUI) as shared infrastructure.
  • Each module (fleet, tenant, okd, …) registers its own subcommands.
  • Third-party MyAppScore authors get harmony myapp deploy with zero framework boilerplate.

The CLI is the user-facing surface of Harmony. Every design decision here shapes the developer experience for the entire ecosystem.

Current State

  • harmony_cli::Args — flat Score-runner flags (--yes, --filter, --list, --number, --interactive). Drives the Maestro loop over a Vec<Score>.
  • harmony_cli::run(Inventory, Topology, Vec<Score>, Option<Args>) — the single entry point consumed by 60+ example binaries.
  • harmony_tui::run() — separate crate, separate run(), same inputs.
  • harmony-fleet-deploy — deploy binary with deploy/publish subcommands (just merged from two separate binaries).
  • harmony_composer — infrastructure composition tool, separate binary.
  • ADR-023 principle 8 describes the staged evolution (B → C) but defers the plugin protocol.

Design

Top-level binary with subcommands

harmony [global flags] <module> <action> [action flags]

harmony --config-namespace fleet-staging fleet deploy --from-tag v0.1.0
harmony --config-namespace fleet-staging fleet publish --from-tag v0.1.0 --no-push
harmony --config-namespace okd-staging okd bootstrap
harmony --config-namespace tenant-c1 tenant create --name c1
harmony --config-namespace harmony myapp deploy --image foo:latest

Global flags (owned by the top-level binary):

  • --config-namespace — maps to ConfigClient::for_namespace()
  • --kubeconfig — topology selection
  • --topology — explicit topology choice (k3d, okd, bare, …)
  • --yes — skip confirmation prompts
  • --interactive — delegate to TUI

Module subcommands (owned by each module):

  • fleet deploy, fleet publish
  • tenant create, tenant list, tenant health, tenant install
  • okd bootstrap, okd add-node
  • User-defined: myapp deploy, myapp publish, …

Two kinds of subcommands

Score-runner subcommands — compose multiple Scores, need --filter/--list/--number. Examples, ad-hoc orchestration, the current harmony_cli::run() use case. The Maestro loop lives here.

Action subcommands — single-purpose (deploy a chart, publish an image, create a tenant). No filter/list/number. Run one Score or a fixed composition.

The distinction matters: forcing action subcommands through the filter/list/number machinery is ceremony; forcing Score-runner subcommands into a rigid single-action shape is constraining.

Deploy crates become library-only

Per ADR-023 principle 5, deploy logic lives in *-deploy crates. The unified CLI absorbs the binaries — deploy crates lose their [[bin]] entries and become libraries consumed by the top-level harmony binary. The crate boundary stays; the binary boundary goes away.

harmony-fleet-deploy/
  Cargo.toml        # [lib] only, no [[bin]]
  src/
    lib.rs          # FleetDeployConfig, FleetDeploySecrets, FleetOperatorScore
    commands.rs     # DeployCommand, PublishCommand (clap Subcommand structs)

The top-level harmony binary imports harmony_fleet_deploy::commands and wires them into its own Command enum.

Publish logic as Scores

Build/push logic (currently imperative Command::new("docker") in harmony-fleet-publish) should be encapsulated in Scores, following the Application trait + feature composition pattern (examples/try_rust_webapp + PackagingDeployment). The publish subcommand becomes a thin CLI wrapper over a Score composition, not a shell-out script.

This is not PackagingDeployment specifically — the operator isn't a RustWebapp. The pattern is the Application trait + feature composition model: a typed application description with composable features (build, push, deploy, monitor).

Plugin discovery (stage C, deferred)

ADR-023 principle 8 envisions harmony discovering harmony-* binaries on $PATH (kubectl-style). This is the third-party extensibility story: a MyAppScore author ships a harmony-myapp binary, and harmony myapp deploy works without rebuilding the framework.

Open question: is the end state a monolithic binary with composable subcommands (first-party modules compiled in), or kubectl-style plugin discovery for everything? Likely both: first-party modules are compiled-in subcommands (tighter integration, shared types), third-party modules are discovered plugins (loose coupling, separate release cycles). The protocol for plugin communication (env vars, stdin JSON, exit codes) is a separate design effort.

TUI integration

harmony_tui is a separate crate with its own run(). The unified CLI's --interactive global flag delegates to harmony_tui::run() for Score-runner subcommands. Action subcommands may or may not have TUI equivalents — that's per-subcommand, not global.

harmony_composer

Stays separate for now. It's an infrastructure composition tool with a different audience (platform engineers building topologies, not operators deploying apps). May become harmony compose later if the use cases converge.

Tasks

13.1 Rewrite harmony_cli — subcommand-aware runner

Replace the flat Args struct with a subcommand-aware Cli struct. Global flags move to the top level. The run() function accepts a Command enum instead of Option<Args>.

#[derive(Parser)]
struct Cli {
    #[arg(long, env = "HARMONY_CONFIG_NAMESPACE", global = true)]
    config_namespace: String,

    #[arg(long, global = true)]
    kubeconfig: Option<String>,

    #[arg(long, global = true)]
    yes: bool,

    #[command(subcommand)]
    command: Command,
}

Files: harmony_cli/src/lib.rs, harmony_cli/src/args.rs (new) Blocked by: Phase 02 (config migration — so the new CLI is born on harmony_config, not retrofitted) Blocks: 13.2, 13.3

13.2 Migrate one deploy binary to subcommand pattern

Proof of concept: harmony-fleet-deploy already has deploy/publish subcommands. Migrate it to the new harmony_cli runner: deploy crate becomes library-only, exports Command enum, top-level binary wires it in.

Files: fleet/harmony-fleet-deploy/, new top-level harmony binary Blocked by: 13.1

13.3 Migrate examples

Each of the 60+ examples currently calls harmony_cli::run() with flat args. Migration: each example becomes a subcommand of the top-level harmony binary, or stays as a standalone binary that imports the new harmony_cli runner.

Migration shape (before/after):

// Before (standalone binary)
fn main() {
    harmony_cli::run(Inventory::autoload(), topology, scores, None).await;
}

// After (subcommand of top-level binary)
// In the example's crate:
pub struct MyExampleCommand { /* clap args */ }
impl Subcommand for MyExampleCommand { ... }

// In the top-level binary:
enum Command {
    MyExample(MyExampleCommand),
    Fleet(FleetCommand),
    ...
}

Files: 60+ example crates Blocked by: 13.2 (prove the pattern works on one)

13.4 Publish-as-Score

Extract build/push logic from harmony-fleet-publish into Scores following the Application trait + feature composition pattern. The publish subcommand becomes a thin wrapper.

Files: harmony/src/modules/application/ (extend), fleet/harmony-fleet-deploy/ Blocked by: 13.2

13.5 Topology selection in the CLI

Global --topology flag or auto-detection. Requires Phase 12.6 (topology proliferation / K8sBareTopology) to land first — the CLI's topology selection is simpler if the topology landscape is clean.

Blocked by: Phase 12.6

13.6 Plugin discovery protocol (stage C)

Design the protocol for third-party harmony-* binaries to communicate with the top-level harmony binary. Env vars for global args? stdin JSON? Exit codes for outcomes?

Status: Research + ADR first. No implementation until the protocol is locked. Blocked by: 13.5 (first-party subcommands working end-to-end)

Dependencies

Phase 02 (config migration)  ──→  13.1 (CLI rewrite)
Phase 12.6 (topology cleanup) ──→  13.5 (topology selection)
13.1  ──→  13.2 (fleet-deploy migration)
13.2  ──→  13.3 (example migration)
13.2  ──→  13.4 (publish-as-Score)
13.5  ──→  13.6 (plugin discovery)

Phase 11 (named config instances) can land after the CLI rewrite — the global --config-namespace flag maps directly to ConfigClient::for_namespace(), and named instances (get_named::<T>("fw-primary")) become a CLI concern too.

ADR-023 Tensions

These need resolution during implementation:

  1. Principle 5 vs. absorbing binaries. Deploy crates keep their crate boundary (library + Scores) but lose their [[bin]]. The unified binary is the sole entry point. This is a refinement of principle 5, not a violation — the deploy logic still lives in the deploy crate.

  2. Principle 8 monolith vs. plugin. First-party modules are compiled-in subcommands. Third-party modules are discovered plugins. The boundary between "first-party" and "third-party" needs a clear doctrine (likely: anything in the harmony repo is first-party; everything else is a plugin).

  3. harmony_composer placement. Stays separate for now. If the use cases converge with the unified CLI, it becomes harmony compose. Not a blocker.

References

  • ADR-023 principle 8 — CLI: hybrid, staged (B → C)
  • ADR-023 principle 5 — deploy logic in *-deploy crates
  • ADR draft 024 §Q5 — runtime tools in the dependency graph
  • examples/try_rust_webappApplication trait + feature composition
  • harmony/src/modules/application/features/packaging_deployment.rs — build/push as a Score feature
  • Phase 02 — config migration (prerequisite)
  • Phase 11 — named config instances (parallel)
  • Phase 12.6 — topology proliferation (prerequisite for 13.5)