Add cbd-cli crate: shared clap CLI definitions + executor stub
Stage 2 of the CLI dev-flow (api-design). cbd-cli holds the clap Parser/Subcommand types for all three binaries (ServerCli/TuiCli/CbdCli, library/queue/global, guard/scan/auth, Role, RemoteArgs), asset generation (clap_complete + clap_mangen), and a feature-gated gRPC executor (run_remote) whose per-command dispatch is stubbed for the implement stage. Compiles with and without the client feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56098f7c26
commit
e215dd8b87
|
|
@ -664,6 +664,19 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbd-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"crabidy-core",
|
||||
"tokio",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbd-tui"
|
||||
version = "0.1.0"
|
||||
|
|
@ -797,6 +810,15 @@ dependencies = [
|
|||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
|
|
@ -815,6 +837,16 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.2.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
|
|
@ -4119,6 +4151,12 @@ dependencies = [
|
|||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
|
||||
|
||||
[[package]]
|
||||
name = "rquickjs"
|
||||
version = "0.9.0"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ resolver = "2"
|
|||
members = [
|
||||
"audio-player",
|
||||
"cbd",
|
||||
"cbd-cli",
|
||||
"cbd-tui",
|
||||
"cbd-web",
|
||||
"crabidy-core",
|
||||
|
|
@ -27,6 +28,8 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|||
axum = "0.8"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap-serde-derive = "0.2"
|
||||
clap_complete = "4"
|
||||
clap_mangen = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
crossterm = "0.29"
|
||||
dirs = "6"
|
||||
|
|
@ -91,6 +94,7 @@ url = "2"
|
|||
|
||||
# Local crates
|
||||
audio-player = { path = "audio-player" }
|
||||
cbd-cli = { path = "cbd-cli" }
|
||||
cbd-tui = { path = "cbd-tui" }
|
||||
crabidy-core = { path = "crabidy-core" }
|
||||
crabidy-server = { path = "crabidy-server" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "cbd-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
clap_mangen.workspace = true
|
||||
|
||||
# The `client` feature adds the gRPC executor for the remote
|
||||
# library/queue/global commands. Kept optional so binaries' build.rs can
|
||||
# depend on this crate (default features = clap only) without dragging in
|
||||
# tonic on every build (architecture/cli.md D1/D7).
|
||||
crabidy-core = { workspace = true, optional = true }
|
||||
tonic = { workspace = true, optional = true, features = [
|
||||
"transport",
|
||||
"codegen",
|
||||
] }
|
||||
tokio = { workspace = true, optional = true, features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
] }
|
||||
base64 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
client = ["dep:crabidy-core", "dep:tonic", "dep:tokio", "dep:base64"]
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
//! The gRPC executor for the remote `library`/`queue`/`global` commands
|
||||
//! (`client` feature). Connects a generated `crabidy-core` client with a
|
||||
//! basic-auth interceptor and runs one command against a server.
|
||||
//!
|
||||
//! Stage-2 (api-design) note: the connection builder is real; `run_remote`'s
|
||||
//! per-command dispatch is finalized by the implement stage.
|
||||
|
||||
use base64::Engine;
|
||||
use tonic::metadata::MetadataValue;
|
||||
use tonic::service::{interceptor::InterceptedService, Interceptor};
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
use tonic::{Request, Status};
|
||||
|
||||
use crabidy_core::proto::crabidy::crabidy_service_client::CrabidyServiceClient;
|
||||
|
||||
use crate::{Connection, RemoteCmd};
|
||||
|
||||
/// Attaches `authorization: Basic …` when a user is configured; an empty user
|
||||
/// means an open server. The header value is a secret and never logged.
|
||||
#[derive(Clone)]
|
||||
pub struct AuthInterceptor {
|
||||
header: Option<MetadataValue<tonic::metadata::Ascii>>,
|
||||
}
|
||||
|
||||
impl AuthInterceptor {
|
||||
fn new(user: &str, password: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
if user.is_empty() {
|
||||
return Ok(Self { header: None });
|
||||
}
|
||||
let encoded =
|
||||
base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}"));
|
||||
let header = format!("Basic {encoded}")
|
||||
.parse()
|
||||
.map_err(|_| "cannot encode credentials header")?;
|
||||
Ok(Self {
|
||||
header: Some(header),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Interceptor for AuthInterceptor {
|
||||
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
|
||||
if let Some(header) = &self.header {
|
||||
request
|
||||
.metadata_mut()
|
||||
.insert("authorization", header.clone());
|
||||
}
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
type Client = CrabidyServiceClient<InterceptedService<Channel, AuthInterceptor>>;
|
||||
|
||||
/// Connects (lazily) to the server described by `conn`.
|
||||
async fn connect(conn: &Connection) -> Result<Client, Box<dyn std::error::Error>> {
|
||||
let endpoint = Endpoint::from_shared(conn.address.clone())?.connect_lazy();
|
||||
let interceptor = AuthInterceptor::new(&conn.user, &conn.password)?;
|
||||
Ok(CrabidyServiceClient::with_interceptor(
|
||||
endpoint,
|
||||
interceptor,
|
||||
))
|
||||
}
|
||||
|
||||
/// Runs one remote command against the server and prints the result.
|
||||
pub async fn run_remote(
|
||||
conn: &Connection,
|
||||
cmd: RemoteCmd,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _client = connect(conn).await?;
|
||||
let _ = cmd;
|
||||
todo!("dispatch each Library/Queue/Global variant to its RPC and print")
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
//! Shared command-line definitions for the crabidy binaries
|
||||
//! (`crabidy-server`, `cbd-tui`, `cbd`), plus a feature-gated gRPC executor
|
||||
//! for the remote `library`/`queue`/`global` commands.
|
||||
//!
|
||||
//! See `architecture/cli.md`. The clap types live here so every binary shares
|
||||
//! one command surface and each binary's `build.rs` can generate shell
|
||||
//! completions and man pages from its own top-level [`clap::Command`] with the
|
||||
//! default (clap-only) feature set. The `client` feature adds [`run_remote`],
|
||||
//! which the binaries call to execute a remote command against a running
|
||||
//! server.
|
||||
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
|
||||
/// A server auth role. The `ValueEnum` names double as the basic-auth user
|
||||
/// names the server expects.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum Role {
|
||||
/// Full control (the normal user).
|
||||
Owner,
|
||||
/// Anything on the queue and playback, no library writes.
|
||||
QueueOwner,
|
||||
/// May browse and append to the queue; nothing else.
|
||||
Appender,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
/// The basic-auth user name for this role.
|
||||
pub fn user_name(self) -> &'static str {
|
||||
match self {
|
||||
Role::Owner => "owner",
|
||||
Role::QueueOwner => "queue-owner",
|
||||
Role::Appender => "queue-appender",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection flags shared by every remote command. Defaults match the client
|
||||
/// config; a flag overrides the config-file value.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct RemoteArgs {
|
||||
/// Server address (default: the client config's, else localhost).
|
||||
#[arg(short, long)]
|
||||
pub address: Option<String>,
|
||||
/// Basic-auth user / role name (empty against an open server).
|
||||
#[arg(short, long)]
|
||||
pub user: Option<String>,
|
||||
/// Basic-auth password.
|
||||
#[arg(short, long)]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
/// Library operations against a running server.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum LibraryCmd {
|
||||
/// List a node's child nodes and tracks (default path `/`).
|
||||
List {
|
||||
#[arg(default_value = "/")]
|
||||
path: String,
|
||||
},
|
||||
/// Create a child node under a creatable parent (e.g. a search term).
|
||||
Create { parent: String, title: String },
|
||||
/// Rename an editable node.
|
||||
Rename { path: String, title: String },
|
||||
/// Delete a deletable node or track (never touches the content store).
|
||||
Delete { path: String },
|
||||
/// Save a subtree as a link bookmark under `/crabidy/<name>`.
|
||||
Save { path: String, name: String },
|
||||
/// Capture a subtree under `/crabidy/<name>` (downloads audio).
|
||||
Capture { path: String, name: String },
|
||||
}
|
||||
|
||||
/// Queue operations against a running server.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum QueueCmd {
|
||||
/// Print the current queue.
|
||||
Show,
|
||||
/// Append tracks/subtrees (by library path) to the end of the queue.
|
||||
Append { paths: Vec<String> },
|
||||
/// Insert tracks/subtrees at a position.
|
||||
Insert { position: u32, paths: Vec<String> },
|
||||
/// Replace the whole queue with the given tracks/subtrees.
|
||||
Replace { paths: Vec<String> },
|
||||
/// Remove queue entries by position.
|
||||
Remove { positions: Vec<u32> },
|
||||
/// Clear the queue (optionally keeping the current track).
|
||||
Clear {
|
||||
#[arg(long)]
|
||||
keep_current: bool,
|
||||
},
|
||||
/// Jump to a queue position.
|
||||
SetCurrent { position: u32 },
|
||||
/// Link-save the current queue as `/crabidy/<name>`.
|
||||
Save { name: String },
|
||||
/// Capture the current queue into `/crabidy/<name>` (downloads audio).
|
||||
Capture { name: String },
|
||||
/// Toggle shuffle.
|
||||
Shuffle,
|
||||
/// Toggle repeat.
|
||||
Repeat,
|
||||
}
|
||||
|
||||
/// Playback and global operations against a running server.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum GlobalCmd {
|
||||
/// Toggle play/pause.
|
||||
Play,
|
||||
/// Stop playback.
|
||||
Stop,
|
||||
/// Skip to the next track.
|
||||
Next,
|
||||
/// Go to the previous track.
|
||||
Prev,
|
||||
/// Restart the current track.
|
||||
Restart,
|
||||
/// Toggle mute.
|
||||
Mute,
|
||||
/// Change the volume by a delta in [-1.0, 1.0] (e.g. `0.05`, `-0.1`).
|
||||
Volume { delta: f32 },
|
||||
}
|
||||
|
||||
/// The remote command groups an executor can run (`client` feature).
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum RemoteCmd {
|
||||
/// Library operations.
|
||||
#[command(subcommand)]
|
||||
Library(LibraryCmd),
|
||||
/// Queue operations.
|
||||
#[command(subcommand)]
|
||||
Queue(QueueCmd),
|
||||
/// Playback / global operations.
|
||||
#[command(subcommand)]
|
||||
Global(GlobalCmd),
|
||||
}
|
||||
|
||||
/// `guard <role> [password]`: hash a role password and (unless `--no-config`)
|
||||
/// write it into `crabidy-server.toml`'s `[auth]`.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct GuardArgs {
|
||||
/// Which role's password to set.
|
||||
pub role: Role,
|
||||
/// The password. If omitted, it is read from stdin (pipe-friendly, and
|
||||
/// keeps it out of the process list).
|
||||
pub password: Option<String>,
|
||||
/// Only print the PHC hash; do not modify `crabidy-server.toml`.
|
||||
#[arg(long)]
|
||||
pub no_config: bool,
|
||||
}
|
||||
|
||||
/// `scan <path> [--capture|--move]`: index a music folder.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ScanArgs {
|
||||
/// Directory to walk for playable files.
|
||||
pub path: std::path::PathBuf,
|
||||
/// Copy each file into the content store; the toml points there.
|
||||
#[arg(long)]
|
||||
pub capture: bool,
|
||||
/// Move each file into the content store instead of copying.
|
||||
#[arg(long, name = "move")]
|
||||
pub move_: bool,
|
||||
}
|
||||
|
||||
/// `auth <role> [password]`: write a role + cleartext password into the client
|
||||
/// config.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct AuthArgs {
|
||||
/// Which role to authenticate as.
|
||||
pub role: Role,
|
||||
/// The cleartext password (read from stdin if omitted).
|
||||
pub password: Option<String>,
|
||||
/// Also set the server address in the client config.
|
||||
#[arg(short, long)]
|
||||
pub address: Option<String>,
|
||||
}
|
||||
|
||||
/// `completions <shell>`: print a completion script to stdout.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CompletionsArgs {
|
||||
/// Target shell.
|
||||
pub shell: clap_complete::Shell,
|
||||
}
|
||||
|
||||
/// Subcommands of `crabidy-server`.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServerCommand {
|
||||
/// Set a role password (hash + write config).
|
||||
Guard(GuardArgs),
|
||||
/// Index a music folder with `.cbd-track.toml` files.
|
||||
Scan(ScanArgs),
|
||||
/// Library operations against a running server.
|
||||
#[command(subcommand)]
|
||||
Library(LibraryCmd),
|
||||
/// Queue operations against a running server.
|
||||
#[command(subcommand)]
|
||||
Queue(QueueCmd),
|
||||
/// Playback / global operations against a running server.
|
||||
#[command(subcommand)]
|
||||
Global(GlobalCmd),
|
||||
/// Print a shell completion script.
|
||||
Completions(CompletionsArgs),
|
||||
}
|
||||
|
||||
/// `crabidy-server`: no subcommand runs the server.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "crabidy-server", author, version, about)]
|
||||
pub struct ServerCli {
|
||||
#[command(flatten)]
|
||||
pub remote: RemoteArgs,
|
||||
#[command(subcommand)]
|
||||
pub command: Option<ServerCommand>,
|
||||
}
|
||||
|
||||
/// Subcommands of `cbd-tui`.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum TuiCommand {
|
||||
/// Write a role + password into the client config.
|
||||
Auth(AuthArgs),
|
||||
#[command(subcommand)]
|
||||
Library(LibraryCmd),
|
||||
#[command(subcommand)]
|
||||
Queue(QueueCmd),
|
||||
#[command(subcommand)]
|
||||
Global(GlobalCmd),
|
||||
/// Print a shell completion script.
|
||||
Completions(CompletionsArgs),
|
||||
}
|
||||
|
||||
/// `cbd-tui`: no subcommand runs the TUI.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "cbd-tui", author, version, about)]
|
||||
pub struct TuiCli {
|
||||
#[command(flatten)]
|
||||
pub remote: RemoteArgs,
|
||||
/// Show the frequency-spectrum bars under the track progress.
|
||||
#[arg(long)]
|
||||
pub spectrum: Option<bool>,
|
||||
#[command(subcommand)]
|
||||
pub command: Option<TuiCommand>,
|
||||
}
|
||||
|
||||
/// Subcommands of `cbd` — the union of the server and client commands.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum CbdCommand {
|
||||
Guard(GuardArgs),
|
||||
Scan(ScanArgs),
|
||||
Auth(AuthArgs),
|
||||
#[command(subcommand)]
|
||||
Library(LibraryCmd),
|
||||
#[command(subcommand)]
|
||||
Queue(QueueCmd),
|
||||
#[command(subcommand)]
|
||||
Global(GlobalCmd),
|
||||
/// Print a shell completion script.
|
||||
Completions(CompletionsArgs),
|
||||
}
|
||||
|
||||
/// `cbd`: no subcommand runs the in-process server + TUI.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "cbd", author, version, about)]
|
||||
pub struct CbdCli {
|
||||
#[command(flatten)]
|
||||
pub remote: RemoteArgs,
|
||||
#[arg(long)]
|
||||
pub spectrum: Option<bool>,
|
||||
#[command(subcommand)]
|
||||
pub command: Option<CbdCommand>,
|
||||
}
|
||||
|
||||
/// Resolved connection settings for the executor (owned, unlike the TUI's
|
||||
/// `&'static ServerConfig`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Connection {
|
||||
pub address: String,
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Writes bash/zsh/fish completions and a man page for `cmd` into `dir`.
|
||||
/// Used by each binary's `build.rs` (architecture/cli.md D7).
|
||||
pub fn generate_assets(
|
||||
mut cmd: clap::Command,
|
||||
bin_name: &str,
|
||||
dir: &std::path::Path,
|
||||
) -> std::io::Result<()> {
|
||||
use clap_complete::Shell;
|
||||
std::fs::create_dir_all(dir.join("completions"))?;
|
||||
std::fs::create_dir_all(dir.join("man"))?;
|
||||
for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
|
||||
clap_complete::generate_to(shell, &mut cmd, bin_name, dir.join("completions"))?;
|
||||
}
|
||||
let man = clap_mangen::Man::new(cmd);
|
||||
let mut out = Vec::new();
|
||||
man.render(&mut out)?;
|
||||
std::fs::write(dir.join("man").join(format!("{bin_name}.1")), out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prints a completion script for `shell` to stdout (the `completions`
|
||||
/// subcommand).
|
||||
pub fn print_completions(shell: clap_complete::Shell, cmd: &mut clap::Command, bin_name: &str) {
|
||||
clap_complete::generate(shell, cmd, bin_name, &mut std::io::stdout());
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
mod client;
|
||||
#[cfg(feature = "client")]
|
||||
pub use client::run_remote;
|
||||
Loading…
Reference in New Issue