crabidy/architecture/cli.md

12 KiB

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.

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 showQueue; 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 ROLEowner|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.