# A comprehensive CLI for every binary ## Context and problem statement Today the command line is thin and inconsistent: - `crabidy-server` uses clap-derive but exposes only `hash-password` and no flags (it always binds the fixed `LISTEN_ADDR`). - `cbd-tui` and `cbd` share a ClapSerde `Config` (`-a/-u/-p`, `--spectrum`) that doubles as the TOML schema; neither has subcommands. - Every user operation is reachable *only* through the interactive TUI. There is no way to script the player, set up auth, or index a music folder from the shell. The goal: **every binary is a clap-derive CLI with `--help`; every operation the TUI can do is also a subcommand; there are shell completions and man pages; and running a binary with no subcommand behaves exactly as today** (TUI, run the server, or `cbd`'s server+TUI). New capabilities requested: - **`guard`** (server): hash a role password and, by default, write it into `crabidy-server.toml`'s `[auth]`. - **`scan`** (server): walk a path, drop a `.cbd-track.toml` beside every playable file; `--capture` copies the audio into the content store (toml points there), `--move` moves it instead of copying. - **`auth`** (client): write a role + cleartext password into the client config. - **`library` / `queue` / `global`** (server *and* client): the full set of library, queue, and playback operations, run against a server over gRPC. ## Assumptions - The `library`/`queue`/`global` commands are **remote**: they connect to a running server at `--address` (default localhost) with the same basic-auth as the TUI, reusing the generated gRPC client. A "server" binary running them is just acting as a client to whatever server is up — including its own. - One config file per client binary stays the source of truth for connection defaults (`cbd-tui.toml`, `cbd.toml`); flags override it. - Passwords may be given as an argument *or* on stdin. Argv is visible in the process list — the help text says so, and omitting the argument reads stdin (pipe-friendly, the current `hash-password` behavior). - No tag extraction in `scan` v1 (title = file stem); no machine-readable output format v1 (human-readable text). Both are noted as future work. ## D1 — A shared `cbd-cli` crate A new library crate **`cbd-cli`** holds the clap definitions so all three binaries share one command surface and each binary's `build.rs` can generate assets from it. It is feature-split to keep `build.rs` light: - **default features (clap only)**: the `Parser`/`Subcommand` types (`LibraryCmd`, `QueueCmd`, `GlobalCmd`, `GuardCmd`, `ScanArgs`, `AuthCmd`, the per-binary top-level `ServerCli`/`TuiCli`/`CbdCli`), a `Role` enum, a `RemoteArgs` flatten (`--address/--user/--password`), and `generate_assets(cmd, out_dir)` (wrapping `clap_complete` + `clap_mangen`). Depends only on `clap`, `clap_complete`, `clap_mangen`. - **feature `client`**: `run_remote(remote: &RemoteArgs, cmd: RemoteCmd)` — the executor that connects a `crabidy_core` gRPC client (with a standalone basic-auth interceptor) and runs a `library`/`queue`/`global` subcommand, printing results. Adds `tonic`, `crabidy-core`, `tokio`. `guard`/`scan`/`auth` *definitions* live in `cbd-cli` (so completions and man pages cover them) but their *execution* lives in the owning binary, which has the server config / store / client config internals `cbd-cli` must not depend on. ```d2 direction: right cbd_cli: cbd-cli (clap defs + asset gen) { defs: "Parser/Subcommand types\nRemoteArgs, Role\ngenerate_assets()" client: "feature=client:\nrun_remote() gRPC executor" } server: crabidy-server { guard_scan: "guard + scan\n(config writer, store)" } tui: cbd-tui { auth: "auth (client config writer)" } cbd: cbd (bundle) core: crabidy-core (generated client) cbd_cli.defs -> server.guard_scan: defines cbd_cli.defs -> tui.auth: defines cbd_cli.client -> core: gRPC to a running server server -> cbd_cli.client: library/queue/global tui -> cbd_cli.client: library/queue/global cbd -> server: reuses guard/scan cbd -> tui: reuses auth cbd -> cbd_cli.client: library/queue/global ``` ## D2 — Per-binary CLI and the no-subcommand default Each binary defines a top-level clap `Parser` (in `cbd-cli`) with global connection flags and an **optional** subcommand: - `ServerCli`: `[guard|scan|library|queue|global|completions]`; none → run the server (as today). - `TuiCli`: `RemoteArgs` + `--spectrum` + `[auth|library|queue|global| completions]`; none → run the TUI. - `CbdCli`: the union — `[guard|scan|auth|library|queue|global|completions]`; none → server + TUI (as today). This is "cbd has the commands from both." **Config merge.** The no-subcommand path must keep today's behavior: read the TOML (writing defaults on first run) and let flags override. ClapSerde did this implicitly; a top-level `Parser` with a subcommand does not compose cleanly with ClapSerde's `Opt`. Decision: replace ClapSerde for the client binaries with a plain `serde` config load plus an explicit override step — the `RemoteArgs` fields are `Option`, and a provided flag overrides the file value. `init_config` keeps writing a defaults file on first run. This is a small, well-contained change to `cbd-tui/src/config.rs` and the two `main.rs` files. Alternative considered: sniff argv for a known subcommand before ClapSerde parsing and branch. Rejected — it forfeits a unified `--help`/completions and is fragile. ## D3 — `library` / `queue` / `global` (remote) The executor (`cbd-cli` feature `client`) maps subcommands to the existing RPCs (`RpcClient` already wraps all but `Stop`, which gains a wrapper): - `library list [PATH]` (default `/`) → `GetLibraryNode`; prints child nodes and tracks (path, title; captured rows marked, per architecture/crabidy-store.md). - `library create `, `rename <PATH> <TITLE>`, `delete <PATH>` → the matching library RPCs. - `library save <PATH> <NAME>` → `CaptureLibraryNode{download:false}`; `library capture <PATH> <NAME>` → `{download:true}`. - `queue show` → `Queue`; `queue append|insert|replace <PATH>…`, `queue remove <POS>…`, `queue clear [--keep-current]`, `queue set-current <POS>`, `queue save <NAME>`, `queue capture <NAME>` (→ `CaptureLibraryNode` on `/crabidy/current`), `queue shuffle`, `queue repeat` (the `QueueModifiers` toggles). - `global play|stop|next|prev|restart|mute`, `global volume <DELTA>` (or `up|down` sugar around `ChangeVolume`). Connection: `RemoteArgs` → a lazily-connected `CrabidyServiceClient` with a basic-auth interceptor built from `--user/--password` (empty user = no header, i.e. an open server). Each command is a one-shot: connect, call, print, exit. `RpcClient::connect`'s `&'static ServerConfig` signature is loosened (or the executor builds the client directly) so a CLI can pass an owned config. Output is human-readable text; a `--json` flag is future work (D9). ## D4 — `guard` (server) `crabidy-server guard <ROLE> [PASSWORD] [--no-config]` where `ROLE` ∈ `owner|queue-owner|appender`: 1. Read the password from the argument, or from stdin if omitted. 2. Hash it (`auth::hash_password`, argon2id) and **print the PHC string** to stdout (so it stays pipeable and `--no-config` reproduces today's `hash-password`). 3. Unless `--no-config`: load `crabidy-server.toml` (or defaults), set the role's field (`owner`/`queue_owner`/`queue_appender`) to the hash, and write it back, preserving the flat `[auth]` shape and the other roles. This needs a **config writer** (D8) — none exists today. `hash-password` is removed in favor of `guard` (`guard owner <pw> --no-config` is the exact replacement). The role names match the basic-auth user names. ## D5 — `scan` (server) `crabidy-server scan <PATH> [--capture] [--move]`: - Walk `<PATH>` recursively (bounded, skipping hidden entries), selecting files by audio extension (flac/mp3/m4a/ogg/opus/wav/webm/aac…). - For each audio file, write a `<stem>.cbd-track.toml` **beside it** using `fsdy::TrackFile` (`to_toml`, the shared naming), so the folder becomes a browsable `/fs` tree. Title defaults to the file stem (tag extraction is future work). - Default (neither flag): the toml's playable is `Playable::File` pointing at the audio's own file name (relative) — the audio stays where it is. - `--capture`: ingest the audio into the content store (hash + de-dup + sidecar, reusing `CrabidyStore`), and the beside-file toml gets a `Playable::Store` entry instead. Idempotent: an already-stored file de-dups. - `--move`: like `--capture` but the source audio is moved into the store, not copied — the original location keeps only the toml. - `--capture`/`--move` require the content store (a state/data dir); `scan` opens a `CrabidyStore` directly and calls a new `ingest_file(path, move) -> StoreName` helper (the local-source half of the D4 capture flow, factored out). Existing `.cbd-track.toml` files are left untouched (scan never clobbers a hand-edited toml); a warning notes skips. ## D6 — `auth` (client) `cbd-tui auth <ROLE> [PASSWORD] [--address ADDR]` (and the same on `cbd`): loads the client config (`cbd-tui.toml` / `cbd.toml`), sets `user` to the role name and `password` to the cleartext (optionally `address`), and writes it back via the **client config writer** (D8). The file is chmod-private-friendly; the help text repeats that the client config holds a plaintext password. ## D7 — Shell completions and man pages Add `clap_complete` and `clap_mangen`. Each binary's `build.rs` build-depends on `cbd-cli` (default features — clap only, cheap) and, from its top-level `Command`, writes bash/zsh/fish completions and a `man` page into `OUT_DIR` every build (a genuine build step). When `CBD_ASSET_DIR` is set, `build.rs` also copies them into that stable directory. A devenv `gen-cli-assets` script sets `CBD_ASSET_DIR=$PWD/dist` and builds, so `dist/completions/**` and `dist/man/*.1` are produced on demand. A hidden `completions <shell>` subcommand on each binary prints a completion script to stdout for ad-hoc use. Alternative considered: an `xtask` generator binary. Rejected — a `build.rs` keeps generation automatic and in lockstep with the CLI definition; the `CBD_ASSET_DIR` copy covers the "get them into the repo" need. ## D8 — Config writers (new) Two small, careful serializers, both load-modify-write preserving shape: - **Server** (`crabidy-server`): `ServerSettings` gains a `store(config_dir)` that serializes the current `[auth]` (round-tripping the existing file so unknown-field rejection stays satisfiable) — used by `guard`. - **Client** (`cbd-tui`): the `Config`/`ServerConfig` is already `Serialize`; `auth` loads it, sets the fields, and writes `toml::to_string_pretty` back to the config path — used by `auth`. Both write to `dirs::config_dir()/crabidy/<file>` and create it if missing. ## D9 — Out of scope / future - Tag extraction in `scan` (a `lofty`-backed title/artist/album/duration). - `--json` machine-readable output for `library list` / `queue show`. - Watching/streaming (`global watch` over `GetUpdateStream`). - Bulk auth (multiple roles in one `guard` invocation). ## Risks - **Password in argv** is visible process-wide; mitigated by the stdin fallback and documented in help. Accepted per the explicit request. - **ClapSerde → clap migration** for the client config changes the parse path; the first-run-writes-defaults and flag-overrides-file behaviors must be preserved by tests. - **`build.rs` writing outside `OUT_DIR`** (the `CBD_ASSET_DIR` copy) is unconventional; gated on the env var so ordinary builds only touch `OUT_DIR`. - **`cbd-cli` as a build-dependency** must stay clap-only by default, or every build drags in tonic — enforced by the feature split.