diff --git a/experimental/silverwood/CLI_SPEC.jsonnet b/experimental/silverwood/CLI_SPEC.jsonnet index 305579f..1e14102 100644 --- a/experimental/silverwood/CLI_SPEC.jsonnet +++ b/experimental/silverwood/CLI_SPEC.jsonnet @@ -355,29 +355,22 @@ name: "spawn", intentHint: "Launch a session kind directly without looking up a recorded session.", generatedDescriptionHint: "Run a session kind in the chosen working directory, or inspect its shell plan with --json.", + options: [ + { + longName: "override-working-directory", + intentHint: "Working directory to run in. Default: this process's own cwd.", + domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, + }, + ], subcommands: { choices: [ { name: "plain-shell", intentHint: "Launch a plain interactive shell.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], }, { name: "disk-space", intentHint: "Launch a disk-space monitor.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], }, { name: "claude-code", @@ -387,13 +380,6 @@ { name: "first-run", generatedDescriptionHint: "First launch of this Claude session id.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], positionals: [ { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], @@ -401,13 +387,6 @@ { name: "resume", generatedDescriptionHint: "Resume an existing Claude conversation.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], positionals: [ { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], @@ -431,13 +410,6 @@ { name: "first-run", generatedDescriptionHint: "First launch of this Claude session id.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], positionals: [ { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], @@ -445,13 +417,6 @@ { name: "resume", generatedDescriptionHint: "Resume an existing Claude conversation.", - options: [ - { - longName: "override-working-directory", - intentHint: "Working directory to run in. Default: this process's own cwd.", - domain: { semanticName: "PATH", validation: "rustType:std::path::Path" }, - }, - ], positionals: [ { generatedDescriptionHint: "The session id.", domain: { semanticName: "SESSION_ID" } }, ], diff --git a/experimental/silverwood/crates/silverwood-cli/src/main.rs b/experimental/silverwood/crates/silverwood-cli/src/main.rs index 5dda870..738e563 100644 --- a/experimental/silverwood/crates/silverwood-cli/src/main.rs +++ b/experimental/silverwood/crates/silverwood-cli/src/main.rs @@ -116,6 +116,8 @@ enum Command { /// Intent: launch a direct session in the intended working directory; /// `--json` reveals the plan. Spawn { + #[command(flatten)] + wd: WorkingDir, #[command(subcommand)] what: SpawnCommand, }, @@ -130,16 +132,10 @@ enum Command { #[derive(Subcommand)] enum SpawnCommand { /// A plain interactive login shell. - PlainShell { - #[command(flatten)] - wd: WorkingDir, - }, + PlainShell, /// A disk-space monitor (a `df` refresh loop), run via the same interactive-shell /// mechanism as `plain-shell`. - DiskSpace { - #[command(flatten)] - wd: WorkingDir, - }, + DiskSpace, /// A Claude Code session, run as a thin wrapper over your login-interactive shell. ClaudeCode { #[command(subcommand)] @@ -156,42 +152,37 @@ enum SpawnCommand { }, } -/// First-run vs resume for a claude-code spawn — carries the claude session id and the -/// per-leaf working directory. +/// First-run vs resume for a claude-code spawn — carries the Claude session id. #[derive(Subcommand)] enum ClaudeRunArg { /// First launch of this session id (`claude --session-id`, which creates the conversation). FirstRun { /// The Claude session id to run. session_id: String, - #[command(flatten)] - wd: WorkingDir, }, /// Resume an existing conversation (`claude --resume`). Resume { /// The Claude session id to run. session_id: String, - #[command(flatten)] - wd: WorkingDir, }, } impl ClaudeRunArg { - /// Decompose into (session id, run mode, working dir). - fn parts(self) -> (String, ClaudeRun, WorkingDir) { + /// Decompose into (session id, run mode). + fn parts(self) -> (String, ClaudeRun) { match self { - ClaudeRunArg::FirstRun { session_id, wd } => (session_id, ClaudeRun::FirstRun, wd), - ClaudeRunArg::Resume { session_id, wd } => (session_id, ClaudeRun::Resume, wd), + ClaudeRunArg::FirstRun { session_id } => (session_id, ClaudeRun::FirstRun), + ClaudeRunArg::Resume { session_id } => (session_id, ClaudeRun::Resume), } } } -/// The working directory for a cwd-derived spawn kind: `--override-working-directory` if -/// given, else this process's own cwd. Recorded sessions use their checkout instead. +/// Working directory shared by every `spawn` kind: the override if given, else +/// this process's cwd. Recorded sessions use their checkout instead. #[derive(clap::Args)] struct WorkingDir { /// Working directory to run in (default: this process's own cwd). - #[arg(long, value_name = "PATH")] + #[arg(long, global = true, value_name = "PATH")] override_working_directory: Option, } @@ -753,7 +744,7 @@ fn run(cli: Cli) -> CliResult { Command::Kv(cmd) => run_kv(&forest, json, cmd), Command::Session(cmd) => run_session(&forest, json, cmd), - Command::Spawn { what } => run_spawn(json, what), + Command::Spawn { wd, what } => run_spawn(json, wd, what), Command::RegisterCodexSession => register_codex_session(&forest), @@ -789,6 +780,7 @@ fn validate_required_options(cli: &Cli) -> CliResult { run_direnv_exec: None, .. }, + .. } => Err("`--run-direnv-exec ` is required".into()), _ => Ok(()), } @@ -1071,24 +1063,25 @@ fn shell_quote(value: &str) -> String { /// [`exec_plan`]). Each subcommand calls its matching plan builder with a resolved /// working directory. Reading the seed from the environment is frontend policy /// (like [`resolve_forest_dir`]), so it stays here. -fn run_spawn(json: bool, what: SpawnCommand) -> CliResult { +fn run_spawn(json: bool, wd: WorkingDir, what: SpawnCommand) -> CliResult { let seed = spawn_seed()?; + let wd = wd.resolve()?; let plan = match what { - SpawnCommand::PlainShell { wd } => plain_shell_plan(&wd.resolve()?, &seed), - SpawnCommand::DiskSpace { wd } => disk_space_plan(&wd.resolve()?, &seed), + SpawnCommand::PlainShell => plain_shell_plan(&wd, &seed), + SpawnCommand::DiskSpace => disk_space_plan(&wd, &seed), SpawnCommand::ClaudeCode { run } => { - let (session_id, run, wd) = run.parts(); - claude_code_plan(&wd.resolve()?, &session_id, run, &seed) + let (session_id, run) = run.parts(); + claude_code_plan(&wd, &session_id, run, &seed) } SpawnCommand::ClaudeCodeNoninteractiveshell { run_direnv_exec, run, } => { - let (session_id, run, wd) = run.parts(); + let (session_id, run) = run.parts(); let run_direnv_exec = run_direnv_exec.ok_or("`--run-direnv-exec ` is required")?; claude_code_noninteractiveshell_plan( - &wd.resolve()?, + &wd, &session_id, run, run_direnv_exec, diff --git a/experimental/silverwood/crates/silverwood-cli/tests/cli.rs b/experimental/silverwood/crates/silverwood-cli/tests/cli.rs index c0ec68d..1b1811e 100644 --- a/experimental/silverwood/crates/silverwood-cli/tests/cli.rs +++ b/experimental/silverwood/crates/silverwood-cli/tests/cli.rs @@ -472,8 +472,9 @@ fn session_run_rejects_bad_and_absent_ids() { ); } -/// The direct spawn kinds build their plan from `--override-working-directory` (no -/// checkout needed), so their `--json` plan shape is testable in the sandbox. We assert on +/// The direct spawn kinds build their plan from the shared `spawn` +/// `--override-working-directory` option (no checkout needed), so their `--json` +/// plan shape is testable in the sandbox. We assert on /// shell-agnostic parts only — the interactive kinds' `program` is the login shell, which /// varies by environment. #[test] @@ -499,9 +500,9 @@ fn spawn_direct_variants_reflect_the_kind() { &[ "--json", "spawn", - "plain-shell", "--override-working-directory", "/tmp/x", + "plain-shell", ], ); assert_eq!(plan["args"], serde_json::json!(["-l"]));