use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::config::StorageTarget;
use crate::manifest::TaskKind;
use crate::output::Format;
#[derive(Debug, Parser)]
#[command(
name = "workspace-mgr",
version,
about = "Fixed-policy repository workspace manager for coding agents",
subcommand_required = true,
arg_required_else_help = true
)]
pub struct Cli {
#[arg(
long,
global = true,
value_enum,
default_value = "human",
env = "WORKSPACE_MGR_FORMAT"
)]
pub format: Format,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Setup(SetupArgs),
Init(InitArgs),
Instructions(InstructionsArgs),
Doctor(RepoArgs),
Config(ConfigArgs),
Task(TaskArgs),
Plan(PlanArgs),
Publish(PublishCommandArgs),
Storage(StorageArgs),
Move(MoveArgs),
Refresh(RefreshArgs),
}
#[derive(Debug, Args)]
pub struct SetupArgs {
#[arg(long)]
pub runtime_dir: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct RepoArgs {
#[arg(long, default_value = ".")]
pub repo: PathBuf,
}
#[derive(Debug, Args)]
pub struct InitArgs {
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub s3_url: Option<String>,
#[arg(long, requires = "s3_url")]
pub s3_endpoint_url: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct InstructionsArgs {
pub topic: Option<String>,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
}
#[derive(Debug, Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommand,
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Show(RepoArgs),
}
#[derive(Debug, Args)]
pub struct TaskArgs {
#[command(subcommand)]
pub command: TaskCommand,
}
#[derive(Debug, Subcommand)]
pub enum TaskCommand {
Create(TaskCreateArgs),
Rename(TaskRenameArgs),
Status(TaskStatusArgs),
Discard(TaskDiscardArgs),
}
#[derive(Debug, Args)]
pub struct TaskRenameArgs {
pub new_slug: String,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct TaskCreateArgs {
pub slug: String,
#[arg(long)]
pub title: String,
#[arg(long)]
pub purpose: String,
#[arg(long, value_enum, default_value = "deliverable")]
pub kind: TaskKind,
#[arg(long = "scope")]
pub scopes: Vec<String>,
#[arg(long, requires = "scopes")]
pub scope_note: Option<String>,
#[arg(long, hide = true)]
pub timestamp: Option<String>,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct TaskStatusArgs {
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub manifest: Option<PathBuf>,
}
#[derive(Debug, Args)]
pub struct TaskDiscardArgs {
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(long, conflicts_with = "confirm")]
pub dry_run: bool,
#[arg(long, value_name = "TASK_ID", conflicts_with = "dry_run")]
pub confirm: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct PlanArgs {
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(long = "include")]
pub include: Vec<String>,
#[arg(long)]
pub scope_note: Option<String>,
#[arg(long)]
pub allow_non_shared_head: bool,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
}
#[derive(Debug, Clone, Args)]
pub struct PublishCommandArgs {
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(short = 'm', long)]
pub message: String,
#[arg(long = "include")]
pub include: Vec<String>,
#[arg(long)]
pub scope_note: Option<String>,
#[arg(long)]
pub allow_non_shared_head: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
}
#[derive(Debug, Args)]
pub struct StorageArgs {
#[command(subcommand)]
pub command: StorageCommand,
}
#[derive(Debug, Subcommand)]
pub enum StorageCommand {
Status(StorageStatusArgs),
Set(StorageSetArgs),
Reset(StorageResetArgs),
Hydrate(StorageHydrateArgs),
}
#[derive(Debug, Clone, Args)]
pub struct ScopedArgs {
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(long = "include")]
pub include: Vec<String>,
#[arg(long)]
pub scope_note: Option<String>,
#[arg(long, default_value = ".")]
pub repo: PathBuf,
}
#[derive(Debug, Args)]
pub struct StorageStatusArgs {
#[command(flatten)]
pub scoped: ScopedArgs,
pub paths: Vec<String>,
}
#[derive(Debug, Args)]
pub struct StorageSetArgs {
#[command(flatten)]
pub scoped: ScopedArgs,
#[arg(required = true)]
pub paths: Vec<String>,
#[arg(long, value_enum)]
pub to: StorageTarget,
#[arg(long)]
pub reason: String,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct StorageResetArgs {
#[command(flatten)]
pub scoped: ScopedArgs,
#[arg(required = true)]
pub paths: Vec<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct StorageHydrateArgs {
#[command(flatten)]
pub scoped: ScopedArgs,
pub paths: Vec<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct MoveArgs {
#[command(flatten)]
pub scoped: ScopedArgs,
pub old_path: String,
pub new_path: String,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct RefreshArgs {
#[arg(long, default_value = ".")]
pub repo: PathBuf,
#[arg(long)]
pub dry_run: bool,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::Cli;
#[test]
fn documented_command_shapes_are_accepted_by_clap() {
let examples: &[&[&str]] = &[
&["setup", "--dry-run"],
&[
"setup",
"--runtime-dir",
"/tmp/workspace-mgr-runtime",
"--dry-run",
],
&["init"],
&[
"init",
"--s3-url",
"s3://example-bucket/workspace",
"--s3-endpoint-url",
"https://s3.example.invalid",
"--dry-run",
],
&["instructions"],
&["instructions", "model"],
&["instructions", "storage", "--repo", "/tmp/repository"],
&["--format", "json", "instructions", "publish"],
&["doctor", "--repo", "/tmp/repository"],
&["config", "show", "--repo", "/tmp/repository"],
&[
"task",
"create",
"training-report",
"--title",
"Training report",
"--purpose",
"Produce the final training report",
"--dry-run",
],
&[
"task",
"create",
"shared-policy",
"--kind",
"infrastructure",
"--title",
"Shared policy",
"--purpose",
"Update repository policy",
"--scope",
"AGENTS.md",
"--scope",
".github/workflows/ci.yml",
"--scope-note",
"The user requested this infrastructure change",
],
&["task", "status", "--manifest", "task/manifest.toml"],
&[
"task",
"rename",
"current-topic",
"--manifest",
"task/manifest.toml",
"--dry-run",
],
&["storage", "status"],
&[
"storage",
"status",
"task/results/model.bin",
"--manifest",
"task/manifest.toml",
],
&[
"storage",
"set",
"task/report.pdf",
"task/summary.txt",
"--to",
"git",
"--reason",
"Review these files directly",
],
&[
"storage",
"set",
"task/data",
"--to",
"s3",
"--reason",
"Retain the dataset",
"--dry-run",
],
&["storage", "reset", "task/data"],
&["storage", "hydrate", "task/data/example.csv"],
&["move", "task/old.bin", "task/new.bin", "--dry-run"],
&[
"plan",
"--include",
"docs/shared.md",
"--scope-note",
"The user requested this shared documentation update",
],
&[
"plan",
"--allow-non-shared-head",
"--scope-note",
"The user selected an alternate checkout",
],
&["publish", "-m", "Publish the training report"],
&[
"publish",
"--message",
"Publish shared documentation",
"--include",
"docs/shared.md",
"--scope-note",
"The user requested this shared documentation update",
"--dry-run",
],
&["refresh", "--dry-run"],
];
for args in examples {
let argv = std::iter::once("workspace-mgr").chain(args.iter().copied());
Cli::try_parse_from(argv).unwrap_or_else(|error| {
panic!("documented command failed to parse: {args:?}\n{error}")
});
}
}
#[test]
fn read_only_commands_do_not_expose_meaningless_mutation_flags() {
assert!(Cli::try_parse_from(["workspace-mgr", "plan", "--dry-run"]).is_err());
assert!(Cli::try_parse_from(["workspace-mgr", "plan", "-m", "unused"]).is_err());
assert!(Cli::try_parse_from(["workspace-mgr", "storage", "status", "--dry-run"]).is_err());
}
}