feat: add explicit workspace init

This commit is contained in:
Keisuke Hirata 2026-07-02 18:20:38 +09:00
parent 5002be498f
commit da631029cb
No known key found for this signature in database
6 changed files with 327 additions and 37 deletions

View File

@ -1,9 +1,11 @@
--- ---
title: 'Workspace初期化をinitコマンドに切り出しserveの副作用をなくす' title: 'Workspace初期化をinitコマンドに切り出しserveの副作用をなくす'
state: 'planning' state: 'closed'
created_at: '2026-07-02T07:02:02Z' created_at: '2026-07-02T07:02:02Z'
updated_at: '2026-07-02T08:31:22Z' updated_at: '2026-07-02T09:20:26Z'
assignee: null assignee: null
queued_by: 'yoi ticket'
queued_at: '2026-07-02T09:03:56Z'
--- ---
## 背景 ## 背景

View File

@ -0,0 +1,16 @@
Implemented explicit Workspace initialization command and removed implicit initialization from serve.
Delivered:
- `yoi workspace init [--workspace <PATH>]`.
- `yoi-workspace-server init [--workspace <PATH>]`.
- `init` creates `.yoi/workspace.toml` and `.yoi/workspace-backend.default.toml` only.
- `serve` requires `.yoi/workspace.toml` to already exist and fails with an init diagnostic otherwise.
- `serve` no longer creates workspace identity or default config template.
- Help text and focused tests updated.
Validation:
- `cargo test -p yoi-workspace-server`
- `cargo test -p yoi`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`

View File

@ -38,4 +38,110 @@ Workspace init Ticket に storage / 正本境界を追記した。
- 将来 ProjectRecordBackend / TicketBackend / ObjectiveBackend 相当の provider を差し替える余地を残す。 - 将来 ProjectRecordBackend / TicketBackend / ObjectiveBackend 相当の provider を差し替える余地を残す。
---
<!-- event: intake_summary author: hare at: 2026-07-02T09:03:56Z -->
## Intake summary
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-02T09:03:56Z from: planning to: ready reason: cli_state field: state -->
## State changed
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-02T09:03:56Z from: ready to: queued reason: queued field: state -->
## State changed
Ticket を `yoi ticket` が queued にしました。
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-02T09:03:56Z from: queued to: inprogress reason: cli_state field: state -->
## State changed
State changed to `inprogress`.
---
<!-- event: implementation_report author: hare at: 2026-07-02T09:20:10Z -->
## Implementation report
Implemented explicit Workspace init and removed serve-time initialization side effects.
Changes:
- Added `yoi workspace init [--workspace <PATH>]` passthrough to `yoi-workspace-server init`.
- Added `yoi-workspace-server init [--workspace <PATH>]`.
- `init` creates only `.yoi/workspace.toml` and `.yoi/workspace-backend.default.toml`.
- `init` does not create `.local` config, SQLite DB, embedded Runtime store, logs, Worker data, or project record layout.
- Added `WorkspaceIdentity::load_required(...)` for serve-time load-only behavior.
- `serve` now requires an initialized workspace and no longer calls `load_or_init` or copies the default config template.
- Uninitialized `serve` fails with a diagnostic telling the user to run `yoi workspace init --workspace <path>`.
- Updated help text for `yoi workspace` and `yoi-workspace-server`.
- Added focused tests for init parsing, init output files, load_required no-create behavior, and yoi CLI passthrough.
Validation:
- `cargo test -p yoi-workspace-server`
- `cargo test -p yoi`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- manual smoke: uninitialized `yoi-workspace-server serve` fails without creating identity.
- manual smoke: `yoi-workspace-server init` creates only workspace identity and default template.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-02T09:20:10Z from: inprogress to: done reason: cli_state field: state -->
## State changed
State changed to `done`.
---
<!-- event: state_changed author: hare at: 2026-07-02T09:20:26Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-07-02T09:20:26Z status: closed -->
## 完了
Implemented explicit Workspace initialization command and removed implicit initialization from serve.
Delivered:
- `yoi workspace init [--workspace <PATH>]`.
- `yoi-workspace-server init [--workspace <PATH>]`.
- `init` creates `.yoi/workspace.toml` and `.yoi/workspace-backend.default.toml` only.
- `serve` requires `.yoi/workspace.toml` to already exist and fails with an init diagnostic otherwise.
- `serve` no longer creates workspace identity or default config template.
- Help text and focused tests updated.
Validation:
- `cargo test -p yoi-workspace-server`
- `cargo test -p yoi`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
--- ---

View File

@ -38,6 +38,21 @@ impl WorkspaceIdentity {
}) })
} }
pub fn load_required(workspace_root: impl AsRef<Path>) -> Result<Self> {
let path = Self::path(workspace_root.as_ref());
match fs::read_to_string(&path) {
Ok(raw) => Self::parse_str(&raw, &path),
Err(error) if error.kind() == ErrorKind::NotFound => {
Err(Error::WorkspaceIdentity(format!(
"workspace is not initialized at {}; run `yoi workspace init --workspace {}` first",
workspace_root.as_ref().display(),
workspace_root.as_ref().display()
)))
}
Err(error) => Err(Error::Io(error)),
}
}
pub fn path(workspace_root: impl AsRef<Path>) -> PathBuf { pub fn path(workspace_root: impl AsRef<Path>) -> PathBuf {
workspace_root workspace_root
.as_ref() .as_ref()
@ -192,6 +207,21 @@ mod tests {
const FIXED_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001"; const FIXED_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
const FIXED_CREATED_AT: &str = "2026-06-23T06:43:28Z"; const FIXED_CREATED_AT: &str = "2026-06-23T06:43:28Z";
#[test]
fn load_required_rejects_uninitialized_workspace_without_creating_identity() {
let temp = tempfile::tempdir().unwrap();
let workspace_root = temp.path().join("uninitialized-workspace");
fs::create_dir_all(&workspace_root).unwrap();
let error = WorkspaceIdentity::load_required(&workspace_root).unwrap_err();
assert!(
error.to_string().contains("workspace is not initialized"),
"unexpected error: {error}"
);
assert!(!WorkspaceIdentity::path(&workspace_root).exists());
}
#[test] #[test]
fn missing_identity_file_is_created_with_safe_fields() { fn missing_identity_file_is_created_with_safe_fields() {
let temp = tempfile::tempdir().unwrap(); let temp = tempfile::tempdir().unwrap();

View File

@ -8,6 +8,13 @@ use yoi_workspace_server::{
SqliteWorkspaceStore, WorkspaceBackendConfigFile, WorkspaceIdentity, serve, SqliteWorkspaceStore, WorkspaceBackendConfigFile, WorkspaceIdentity, serve,
}; };
#[derive(Debug)]
enum Command {
Serve(ServeOptions),
Init(InitOptions),
Help,
}
#[derive(Debug)] #[derive(Debug)]
struct ServeOptions { struct ServeOptions {
workspace: PathBuf, workspace: PathBuf,
@ -16,6 +23,11 @@ struct ServeOptions {
listen: Option<SocketAddr>, listen: Option<SocketAddr>,
} }
#[derive(Debug)]
struct InitOptions {
workspace: PathBuf,
}
#[derive(Debug)] #[derive(Debug)]
struct CliError(String); struct CliError(String);
@ -40,34 +52,57 @@ async fn main() -> ExitCode {
async fn run() -> Result<(), Box<dyn std::error::Error>> { async fn run() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().skip(1).collect::<Vec<_>>(); let args = std::env::args().skip(1).collect::<Vec<_>>();
match parse_command(&args)? {
Command::Serve(options) => run_serve(options).await,
Command::Init(options) => run_init(options),
Command::Help => Ok(()),
}
}
fn parse_command(args: &[String]) -> Result<Command, CliError> {
let Some((command, rest)) = args.split_first() else { let Some((command, rest)) = args.split_first() else {
print_help(); print_help();
return Ok(()); return Ok(Command::Help);
}; };
match command.as_str() { match command.as_str() {
"init" => {
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
print_init_help();
return Ok(Command::Help);
}
Ok(Command::Init(parse_init_options(rest)?))
}
"serve" => { "serve" => {
if rest.iter().any(|arg| arg == "--help" || arg == "-h") { if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
print_serve_help(); print_serve_help();
return Ok(()); return Ok(Command::Help);
} }
let options = parse_serve_options(rest)?; Ok(Command::Serve(parse_serve_options(rest)?))
run_serve(options).await?;
Ok(())
} }
"--help" | "-h" => { "--help" | "-h" => {
print_help(); print_help();
Ok(()) Ok(Command::Help)
} }
other => Err(Box::new(CliError(format!( other => Err(CliError(format!(
"unknown command `{other}`; expected `serve`" "unknown command `{other}`; expected `init` or `serve`"
)))), ))),
} }
} }
async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Error>> { fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
let identity = WorkspaceIdentity::load_or_init(&options.workspace)?; let identity = WorkspaceIdentity::load_or_init(&options.workspace)?;
WorkspaceBackendConfigFile::ensure_default_template_for_workspace(&options.workspace)?; WorkspaceBackendConfigFile::ensure_default_template_for_workspace(&options.workspace)?;
eprintln!(
"yoi-workspace-server: initialized workspace `{}` ({})",
options.workspace.display(),
identity.workspace_id
);
Ok(())
}
async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Error>> {
let identity = WorkspaceIdentity::load_required(&options.workspace)?;
let config_file = WorkspaceBackendConfigFile::load_for_workspace(&options.workspace)?; let config_file = WorkspaceBackendConfigFile::load_for_workspace(&options.workspace)?;
let mut resolved = config_file.resolve(&options.workspace, identity)?; let mut resolved = config_file.resolve(&options.workspace, identity)?;
if let Some(db) = options.db { if let Some(db) = options.db {
@ -95,6 +130,31 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Erro
Ok(()) Ok(())
} }
fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
let mut workspace = std::env::current_dir()
.map_err(|error| CliError(format!("failed to read current dir: {error}")))?;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--workspace" => {
let value = iter
.next()
.ok_or_else(|| CliError("--workspace requires a path".to_string()))?;
workspace = PathBuf::from(value);
}
value if value.starts_with("--workspace=") => {
workspace = PathBuf::from(value_after_equals(arg, "--workspace")?);
}
other => return Err(CliError(format!("unknown init option `{other}`"))),
}
}
let workspace = workspace
.canonicalize()
.map_err(|error| CliError(format!("failed to canonicalize workspace: {error}")))?;
Ok(InitOptions { workspace })
}
fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> { fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
let mut workspace = std::env::current_dir() let mut workspace = std::env::current_dir()
.map_err(|error| CliError(format!("failed to resolve current directory: {error}")))?; .map_err(|error| CliError(format!("failed to resolve current directory: {error}")))?;
@ -192,12 +252,58 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
fn print_help() { fn print_help() {
println!( println!(
"yoi-workspace-server\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help" "yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
);
}
fn print_init_help() {
println!(
"yoi-workspace-server init\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity and copies the default Backend config template. Does not create Backend data stores.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
); );
} }
fn print_serve_help() { fn print_serve_help() {
println!( println!(
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n --workspace <PATH> Workspace root containing .yoi project records (defaults to cwd)\n --db <PATH> SQLite database path (defaults to <workspace>/.yoi/workspace.db)\n --frontend <PATH> Static SPA build directory to serve\n --listen <ADDR> Listen address (defaults to 127.0.0.1:8787)\n -h, --help Print help" "yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nDescription:\n Serves an already initialized Workspace. Run `yoi workspace init` first.\n\nOptions:\n --workspace <PATH> Workspace root containing .yoi project records (defaults to cwd)\n --db <PATH> SQLite database path (legacy dev override)\n --frontend <PATH> Static SPA build directory to serve (legacy dev override)\n --listen <ADDR> Listen address (legacy dev override; default 127.0.0.1:8787)\n -h, --help Print help"
); );
} }
#[cfg(test)]
mod tests {
use super::*;
use yoi_workspace_server::{
WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH, WORKSPACE_IDENTITY_RELATIVE_PATH,
};
#[test]
fn parse_init_defaults_workspace_to_cwd_or_flag() {
let temp = tempfile::tempdir().unwrap();
let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
let options = parse_init_options(&args).unwrap();
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
}
#[test]
fn init_creates_identity_and_default_template_only() {
let temp = tempfile::tempdir().unwrap();
run_init(InitOptions {
workspace: temp.path().canonicalize().unwrap(),
})
.unwrap();
assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists());
assert!(
temp.path()
.join(WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH)
.exists()
);
assert!(
!temp
.path()
.join(".yoi/workspace-backend.local.toml")
.exists()
);
assert!(!temp.path().join(".yoi/workspace.db").exists());
assert!(!temp.path().join(".yoi/embedded-runtime").exists());
}
}

View File

@ -29,7 +29,10 @@ enum Mode {
WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli), WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli),
Ticket(ticket_cli::TicketCli), Ticket(ticket_cli::TicketCli),
WorkspaceHelp, WorkspaceHelp,
WorkspaceServe(Vec<String>), WorkspaceServer {
subcommand: String,
args: Vec<String>,
},
WorkerRuntime(Vec<String>), WorkerRuntime(Vec<String>),
Keys, Keys,
SetupModel, SetupModel,
@ -78,7 +81,7 @@ async fn main() -> ExitCode {
print_workspace_help(); print_workspace_help();
ExitCode::SUCCESS ExitCode::SUCCESS
} }
Mode::WorkspaceServe(args) => run_workspace_server(args), Mode::WorkspaceServer { subcommand, args } => run_workspace_server(&subcommand, args),
Mode::MemoryLint(options) => match memory_lint::run(&options) { Mode::MemoryLint(options) => match memory_lint::run(&options) {
Ok(LintStatus::Clean) => ExitCode::SUCCESS, Ok(LintStatus::Clean) => ExitCode::SUCCESS,
Ok(LintStatus::Failed) => ExitCode::FAILURE, Ok(LintStatus::Failed) => ExitCode::FAILURE,
@ -607,24 +610,36 @@ fn current_dir() -> Result<PathBuf, ParseError> {
fn parse_workspace_args(args: &[String]) -> Result<Mode, ParseError> { fn parse_workspace_args(args: &[String]) -> Result<Mode, ParseError> {
let Some((subcommand, rest)) = args.split_first() else { let Some((subcommand, rest)) = args.split_first() else {
return Err(ParseError( return Err(ParseError(
"yoi workspace requires `serve` (try `yoi workspace --help`)".to_string(), "yoi workspace requires `init` or `serve` (try `yoi workspace --help`)".to_string(),
)); ));
}; };
match subcommand.as_str() { match subcommand.as_str() {
"init" => {
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Mode::WorkspaceHelp);
}
Ok(Mode::WorkspaceServer {
subcommand: "init".to_string(),
args: rest.to_vec(),
})
}
"serve" => { "serve" => {
if rest.iter().any(|arg| arg == "--help" || arg == "-h") { if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Mode::WorkspaceHelp); return Ok(Mode::WorkspaceHelp);
} }
Ok(Mode::WorkspaceServe(rest.to_vec())) Ok(Mode::WorkspaceServer {
subcommand: "serve".to_string(),
args: rest.to_vec(),
})
} }
"--help" | "-h" => Ok(Mode::WorkspaceHelp), "--help" | "-h" => Ok(Mode::WorkspaceHelp),
other => Err(ParseError(format!( other => Err(ParseError(format!(
"unknown yoi workspace subcommand `{other}`" "unknown yoi workspace subcommand `{other}`; expected `init` or `serve`"
))), ))),
} }
} }
fn run_workspace_server(args: Vec<String>) -> ExitCode { fn run_workspace_server(subcommand: &str, args: Vec<String>) -> ExitCode {
let command = match resolve_workspace_server_command() { let command = match resolve_workspace_server_command() {
Ok(command) => command, Ok(command) => command,
Err(error) => { Err(error) => {
@ -634,7 +649,7 @@ fn run_workspace_server(args: Vec<String>) -> ExitCode {
}; };
let mut child = Command::new(&command); let mut child = Command::new(&command);
child.arg("serve"); child.arg(subcommand);
child.args(args); child.args(args);
match child.status() { match child.status() {
Ok(status) if status.success() => ExitCode::SUCCESS, Ok(status) if status.success() => ExitCode::SUCCESS,
@ -999,13 +1014,14 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
fn print_help() { fn print_help() {
println!( println!(
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Worker chat/client surface (default, --worker, yoi resume, Backend Runtime target)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--worker (defaults to cwd)\n --worker <NAME> Open the Worker Console by name (attach/restore/create)\n --socket <PATH> Attach a Worker Console to a specific socket with --worker\n --session <UUID> Resume a specific session segment in the Worker Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n" "yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete <NAME> [--force] [--dry-run]\n yoi worker prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace init [OPTIONS]
yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Worker chat/client surface (default, --worker, yoi resume, Backend Runtime target)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--worker (defaults to cwd)\n --worker <NAME> Open the Worker Console by name (attach/restore/create)\n --socket <PATH> Attach a Worker Console to a specific socket with --worker\n --session <UUID> Resume a specific session segment in the Worker Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
); );
} }
fn print_workspace_help() { fn print_workspace_help() {
println!( println!(
"yoi workspace\n\nUsage:\n yoi workspace serve [OPTIONS]\n\nDescription:\n Launches the separate yoi-workspace-server executable. The yoi binary does not link the workspace server crate.\n\nOptions forwarded to yoi-workspace-server serve:\n --workspace <PATH> Workspace root containing .yoi project records (defaults to cwd)\n --db <PATH> SQLite database path (defaults to <workspace>/.yoi/workspace.db)\n --frontend <PATH> Static SPA build directory to serve\n --listen <ADDR> Listen address (defaults to 127.0.0.1:8787)\n -h, --help Print help\n\nEnvironment:\n YOI_WORKSPACE_SERVER_COMMAND Path to yoi-workspace-server executable override\n" "yoi workspace\n\nUsage:\n yoi workspace init [OPTIONS]\n yoi workspace serve [OPTIONS]\n\nDescription:\n Launches the separate yoi-workspace-server executable. The yoi binary does not link the workspace server crate.\n\nSubcommands:\n init Initialize .yoi/workspace.toml and the default Backend config template\n serve Serve an already initialized Workspace\n\nOptions forwarded to init/serve:\n --workspace <PATH> Workspace root (defaults to cwd)\n\nLegacy dev options forwarded to serve:\n --db <PATH> SQLite database path override\n --frontend <PATH> Static SPA build directory to serve\n --listen <ADDR> Listen address override\n -h, --help Print help\n\nEnvironment:\n YOI_WORKSPACE_SERVER_COMMAND Path to yoi-workspace-server executable override\n"
); );
} }
@ -1209,7 +1225,21 @@ mod tests {
#[test] #[test]
fn parse_workspace_serve_passthrough() { fn parse_workspace_serve_passthrough() {
match parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap() { match parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap() {
Mode::WorkspaceServe(args) => assert_eq!(args, vec!["--listen", "127.0.0.1:0"]), Mode::WorkspaceServer { subcommand, args } => {
assert_eq!(subcommand, "serve");
assert_eq!(args, vec!["--listen", "127.0.0.1:0"]);
}
other => panic!("unexpected mode: {other:?}"),
}
}
#[test]
fn parse_workspace_init_passthrough() {
match parse_args_from(["workspace", "init", "--workspace", "/tmp/ws"]).unwrap() {
Mode::WorkspaceServer { subcommand, args } => {
assert_eq!(subcommand, "init");
assert_eq!(args, vec!["--workspace", "/tmp/ws"]);
}
other => panic!("unexpected mode: {other:?}"), other => panic!("unexpected mode: {other:?}"),
} }
} }