Design a comprehensive clap-derive CLI (architecture/cli.md)
Stage 1 of the CLI dev-flow: every binary becomes a clap-derive CLI with --help; no subcommand keeps the current default (TUI / run server / both). A shared cbd-cli crate holds the clap definitions and a feature-gated gRPC executor for the remote library/queue/global commands; server guard/scan and client auth live in their binaries; completions + man pages generate in each build.rs. Includes a d2 component diagram. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
99419dcdcf
commit
56098f7c26
|
|
@ -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 <PARENT> <TITLE>`, `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.
|
||||||
Loading…
Reference in New Issue