diff --git a/architecture/cli.md b/architecture/cli.md new file mode 100644 index 0000000..5ccb9c3 --- /dev/null +++ b/architecture/cli.md @@ -0,0 +1,238 @@ +# 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.