12 KiB
A comprehensive CLI for every binary
Context and problem statement
Today the command line is thin and inconsistent:
crabidy-serveruses clap-derive but exposes onlyhash-passwordand no flags (it always binds the fixedLISTEN_ADDR).cbd-tuiandcbdshare a ClapSerdeConfig(-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 intocrabidy-server.toml's[auth].scan(server): walk a path, drop a.cbd-track.tomlbeside every playable file;--capturecopies the audio into the content store (toml points there),--movemoves 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/globalcommands 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-passwordbehavior). - No tag extraction in
scanv1 (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/Subcommandtypes (LibraryCmd,QueueCmd,GlobalCmd,GuardCmd,ScanArgs,AuthCmd, the per-binary top-levelServerCli/TuiCli/CbdCli), aRoleenum, aRemoteArgsflatten (--address/--user/--password), andgenerate_assets(cmd, out_dir)(wrappingclap_complete+clap_mangen). Depends only onclap,clap_complete,clap_mangen. - feature
client:run_remote(remote: &RemoteArgs, cmd: RemoteCmd)— the executor that connects acrabidy_coregRPC client (with a standalone basic-auth interceptor) and runs alibrary/queue/globalsubcommand, printing results. Addstonic,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 show→Queue;queue append|insert|replace <PATH>…,queue remove <POS>…,queue clear [--keep-current],queue set-current <POS>,queue save <NAME>,queue capture <NAME>(→CaptureLibraryNodeon/crabidy/current),queue shuffle,queue repeat(theQueueModifierstoggles).global play|stop|next|prev|restart|mute,global volume <DELTA>(orup|downsugar aroundChangeVolume).
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:
- Read the password from the argument, or from stdin if omitted.
- Hash it (
auth::hash_password, argon2id) and print the PHC string to stdout (so it stays pipeable and--no-configreproduces today'shash-password). - Unless
--no-config: loadcrabidy-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.tomlbeside it usingfsdy::TrackFile(to_toml, the shared naming), so the folder becomes a browsable/fstree. Title defaults to the file stem (tag extraction is future work). - Default (neither flag): the toml's playable is
Playable::Filepointing 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, reusingCrabidyStore), and the beside-file toml gets aPlayable::Storeentry instead. Idempotent: an already-stored file de-dups.--move: like--capturebut the source audio is moved into the store, not copied — the original location keeps only the toml.--capture/--moverequire the content store (a state/data dir);scanopens aCrabidyStoredirectly and calls a newingest_file(path, move) -> StoreNamehelper (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):ServerSettingsgains astore(config_dir)that serializes the current[auth](round-tripping the existing file so unknown-field rejection stays satisfiable) — used byguard. - Client (
cbd-tui): theConfig/ServerConfigis alreadySerialize;authloads it, sets the fields, and writestoml::to_string_prettyback to the config path — used byauth.
Both write to dirs::config_dir()/crabidy/<file> and create it if missing.
D9 — Out of scope / future
- Tag extraction in
scan(alofty-backed title/artist/album/duration). --jsonmachine-readable output forlibrary list/queue show.- Watching/streaming (
global watchoverGetUpdateStream). - Bulk auth (multiple roles in one
guardinvocation).
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.rswriting outsideOUT_DIR(theCBD_ASSET_DIRcopy) is unconventional; gated on the env var so ordinary builds only touchOUT_DIR.cbd-clias a build-dependency must stay clap-only by default, or every build drags in tonic — enforced by the feature split.