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