From 297c56c72d5af9ae14213413a1b40afb8b299c3b Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 29 Jul 2026 18:26:22 +0900 Subject: [PATCH] cli: configure target defaults --- crates/workspace-server/src/config.rs | 2 +- crates/workspace-server/src/identity.rs | 2 +- crates/yoi/src/cli_connection.rs | 52 ++-- crates/yoi/src/main.rs | 376 +++++++++++------------ resources/workspace-backend.default.toml | 6 +- 5 files changed, 220 insertions(+), 218 deletions(-) diff --git a/crates/workspace-server/src/config.rs b/crates/workspace-server/src/config.rs index 1912474b..dc5b3d97 100644 --- a/crates/workspace-server/src/config.rs +++ b/crates/workspace-server/src/config.rs @@ -306,7 +306,7 @@ impl WorkspaceBackendConfigFile { match fs::read_to_string(&path) { Ok(local) => Ok(ConfigDiff::new(WORKSPACE_BACKEND_CONFIG_TEMPLATE, &local)), Err(error) if error.kind() == io::ErrorKind::NotFound => Err(Error::Config(format!( - "workspace backend local config `{}` does not exist; run `yoi workspace init --workspace {}` first", + "workspace backend local config `{}` does not exist; run `yoi-workspace-server init --workspace {}` first", path.display(), workspace_root.display() ))), diff --git a/crates/workspace-server/src/identity.rs b/crates/workspace-server/src/identity.rs index 3ff7e555..f41a778c 100644 --- a/crates/workspace-server/src/identity.rs +++ b/crates/workspace-server/src/identity.rs @@ -46,7 +46,7 @@ impl WorkspaceIdentity { 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 is not initialized at {}; run `yoi-workspace-server init --workspace {}` first", workspace_root.as_ref().display(), workspace_root.as_ref().display() ))) diff --git a/crates/yoi/src/cli_connection.rs b/crates/yoi/src/cli_connection.rs index 22f07677..352637ed 100644 --- a/crates/yoi/src/cli_connection.rs +++ b/crates/yoi/src/cli_connection.rs @@ -1,7 +1,7 @@ use client::{BackendTarget, LocalTarget, Target, TargetKind}; use serde::Deserialize; -use super::{ParseError, resolve_backend_url}; +use super::{ParseError, read_client_default_connection, resolve_backend_url}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CliConnectionRequirement { @@ -26,7 +26,6 @@ pub(crate) enum CliCommand { Mcp, MemoryLint, Session, - WorkspaceServer, Login, } @@ -47,7 +46,6 @@ impl CliCommand { CliCommand::Mcp => "yoi mcp", CliCommand::MemoryLint => "yoi memory lint", CliCommand::Session => "yoi session", - CliCommand::WorkspaceServer => "yoi workspace/server", CliCommand::Login => "yoi login", } } @@ -68,15 +66,13 @@ impl CliCommand { | CliCommand::Plugin | CliCommand::Mcp | CliCommand::MemoryLint - | CliCommand::Session - | CliCommand::WorkspaceServer => CliConnectionRequirement::LocalOnly, + | CliCommand::Session => CliConnectionRequirement::LocalOnly, } } } #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] -#[allow(dead_code)] pub(crate) enum ClientDefaultConnection { Local, Backend, @@ -90,7 +86,10 @@ impl Default for ClientDefaultConnection { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CliConnectionInput<'a> { - LocalDefault, + DefaultTarget { + workspace_id: Option<&'a str>, + }, + LocalTarget, BackendTarget { explicit_backend_url: Option, workspace_id: Option<&'a str>, @@ -115,21 +114,29 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver { input: CliConnectionInput<'_>, ) -> Result, ParseError> { match (command.connection_requirement(), input) { - (CliConnectionRequirement::LocalOnly, CliConnectionInput::LocalDefault) => { - Ok(Box::new(LocalTarget::new())) - } + ( + CliConnectionRequirement::LocalOnly, + CliConnectionInput::DefaultTarget { .. } | CliConnectionInput::LocalTarget, + ) => Ok(Box::new(LocalTarget::new())), (CliConnectionRequirement::LocalOnly, CliConnectionInput::BackendTarget { .. }) => { Err(ParseError(format!( "{} uses a local connection target and cannot accept Backend target options", command.display_name() ))) } - (CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalDefault) => { + (CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalTarget) => { Err(ParseError(format!( "{} requires a Backend connection target", command.display_name() ))) } + ( + CliConnectionRequirement::BackendOnly, + CliConnectionInput::DefaultTarget { workspace_id }, + ) => Ok(Box::new(BackendTarget::new( + resolve_backend_url(None, workspace_id)?, + workspace_id.map(str::to_string), + ))), ( CliConnectionRequirement::BackendOnly | CliConnectionRequirement::ConnectionAware, CliConnectionInput::BackendTarget { @@ -140,9 +147,19 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver { resolve_backend_url(explicit_backend_url, workspace_id)?, workspace_id.map(str::to_string), ))), - (CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalDefault) => { + (CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalTarget) => { Ok(Box::new(LocalTarget::new())) } + ( + CliConnectionRequirement::ConnectionAware, + CliConnectionInput::DefaultTarget { workspace_id }, + ) => match read_client_default_connection()? { + ClientDefaultConnection::Local => Ok(Box::new(LocalTarget::new())), + ClientDefaultConnection::Backend => Ok(Box::new(BackendTarget::new( + resolve_backend_url(None, workspace_id)?, + workspace_id.map(str::to_string), + ))), + }, } } } @@ -151,7 +168,7 @@ pub(crate) fn resolve_local_cli_connection( resolver: &R, command: CliCommand, ) -> Result, ParseError> { - let target = resolver.resolve_connection(command, CliConnectionInput::LocalDefault)?; + let target = resolver.resolve_connection(command, CliConnectionInput::LocalTarget)?; match target.kind() { TargetKind::Local => Ok(target), TargetKind::Backend => Err(ParseError(format!( @@ -196,7 +213,7 @@ pub(crate) fn resolve_connection_aware_cli_connection, - }, Login { backend_url: String, no_wait: bool, @@ -93,11 +87,6 @@ async fn main() -> ExitCode { print_memory_lint_help(); ExitCode::SUCCESS } - Mode::WorkspaceHelp => { - print_workspace_help(); - ExitCode::SUCCESS - } - Mode::WorkspaceServer { subcommand, args } => run_workspace_server(&subcommand, args), Mode::Login { backend_url, no_wait, @@ -332,7 +321,7 @@ fn parse_args_slice_with_connection_resolver( &target_selection, &workspace_root, )?; - let mode = if target_selection.explicit_backend() { + let mode = if target.kind() == client::TargetKind::Backend { LaunchMode::Workers { runtime_id: None, include_stopped: false, @@ -390,16 +379,6 @@ fn parse_args_slice_with_connection_resolver( let plugin_cli = parse_plugin_args(&args[1..])?; return Ok(Mode::Plugin(plugin_cli)); } - "workspace" => { - let _target = - resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?; - return parse_workspace_args(&args[1..]); - } - "server" => { - let _target = - resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?; - return parse_workspace_args(&args[1..]); - } "login" => { return parse_login_args(&args[1..], connection_resolver); } @@ -937,28 +916,65 @@ struct WorkspaceIdentityFile { id: String, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default)] struct ClientConfigFile { default_backend: Option, - // Parsed as part of the target model; applying configured connection defaults is left to - // the later user-facing connection-mode work so current `yoi` default launch stays local. - #[allow(dead_code)] - #[serde(default)] default_connection: ClientDefaultConnection, - #[serde(default)] backends: BTreeMap, - #[serde(default)] workspaces: BTreeMap, } -#[derive(Debug, Deserialize)] -struct ClientBackendConfig { - url: String, +#[derive(Debug, Default, Deserialize)] +struct ClientConfigOverlay { + default_backend: Option, + default_connection: Option, + #[serde(default)] + backends: BTreeMap, + #[serde(default)] + workspaces: BTreeMap, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default)] +struct ClientBackendConfig { + url: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct ClientBackendConfigOverlay { + url: Option, +} + +#[derive(Debug, Default)] struct ClientWorkspaceConfig { - backend: String, + backend: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct ClientWorkspaceConfigOverlay { + backend: Option, +} + +impl ClientConfigFile { + fn apply_overlay(&mut self, overlay: ClientConfigOverlay) { + if let Some(default_backend) = overlay.default_backend { + self.default_backend = Some(default_backend); + } + if let Some(default_connection) = overlay.default_connection { + self.default_connection = default_connection; + } + for (name, backend) in overlay.backends { + let entry = self.backends.entry(name).or_default(); + if let Some(url) = backend.url { + entry.url = Some(url); + } + } + for (id, workspace) in overlay.workspaces { + let entry = self.workspaces.entry(id).or_default(); + if let Some(backend) = workspace.backend { + entry.backend = Some(backend); + } + } + } } fn resolve_workspace_id_from_root(workspace_root: &Path) -> Result, ParseError> { @@ -1004,7 +1020,7 @@ fn resolve_backend_url( config .workspaces .get(id) - .map(|workspace| workspace.backend.as_str()) + .and_then(|workspace| workspace.backend.as_deref()) }) .or(config.default_backend.as_deref()) .ok_or_else(|| ParseError(client_config_missing_message(workspace_id)))?; @@ -1013,7 +1029,11 @@ fn resolve_backend_url( "client config references backend `{backend_name}`, but [backends.{backend_name}] is not defined" )) })?; - let url = backend.url.trim(); + let Some(url) = backend.url.as_deref().map(str::trim) else { + return Err(ParseError(format!( + "client config backend `{backend_name}` must contain a url" + ))); + }; if url.is_empty() { return Err(ParseError(format!( "client config backend `{backend_name}` must contain a non-empty url" @@ -1022,30 +1042,67 @@ fn resolve_backend_url( Ok(url.to_string()) } +fn read_client_default_connection() -> Result { + Ok(read_client_config()? + .map(|config| config.default_connection) + .unwrap_or_default()) +} + fn read_client_config() -> Result, ParseError> { - let Some(path) = client_config_path() else { - return Ok(None); - }; + let mut config = ClientConfigFile::default(); + let mut found = false; + + if let Some(path) = client_global_config_path() { + if let Some(overlay) = read_client_config_overlay(&path)? { + config.apply_overlay(overlay); + found = true; + } + } + + let cwd_path = client_cwd_config_path()?; + if let Some(overlay) = read_client_config_overlay(&cwd_path)? { + config.apply_overlay(overlay); + found = true; + } + + Ok(found.then_some(config)) +} + +fn read_client_config_overlay(path: &Path) -> Result, ParseError> { if !path.is_file() { return Ok(None); } - let contents = fs::read_to_string(&path) + let contents = fs::read_to_string(path) .map_err(|e| ParseError(format!("failed to read {}: {e}", path.display())))?; - toml::from_str::(&contents) + toml::from_str::(&contents) .map(Some) .map_err(|e| ParseError(format!("failed to parse {}: {e}", path.display()))) } -fn client_config_path() -> Option { - yoi_config_dir().map(|dir| dir.join("client.toml")) +fn client_global_config_path() -> Option { + manifest::paths::data_dir().map(|dir| dir.join("client").join("config.toml")) +} + +fn client_cwd_config_path() -> Result { + Ok(current_dir()?.join(".yoi").join("client.config.toml")) +} + +fn client_config_location_message() -> String { + match client_global_config_path() { + Some(path) => format!("{} or /.yoi/client.config.toml", path.display()), + None => "/client/config.toml or /.yoi/client.config.toml".to_string(), + } } fn client_config_missing_message(workspace_id: Option<&str>) -> String { + let locations = client_config_location_message(); match workspace_id { Some(workspace_id) => format!( - "Backend URL is required. Pass --backend or configure $XDG_CONFIG_HOME/yoi/client.toml with [workspaces.{workspace_id}] backend = and [backends.].url" + "Backend URL is required. Pass --backend or configure {locations} with [workspaces.{workspace_id}] backend = and [backends.].url" + ), + None => format!( + "Backend URL is required. Pass --backend or configure default_backend in {locations}" ), - None => "Backend URL is required. Pass --backend or configure default_backend in $XDG_CONFIG_HOME/yoi/client.toml".to_string(), } } @@ -1189,94 +1246,6 @@ fn yoi_config_dir() -> Option { std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config").join("yoi")) } -fn parse_workspace_args(args: &[String]) -> Result { - let Some((subcommand, rest)) = args.split_first() else { - return Err(ParseError( - "yoi workspace requires `init`, `config`, `identity`, `trust-runtime`, or `serve` (try `yoi workspace --help`)" - .to_string(), - )); - }; - 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(), - }) - } - "config" => { - if rest.iter().any(|arg| arg == "--help" || arg == "-h") { - return Ok(Mode::WorkspaceHelp); - } - Ok(Mode::WorkspaceServer { - subcommand: "config".to_string(), - args: rest.to_vec(), - }) - } - "identity" | "trust-runtime" => { - if rest.iter().any(|arg| arg == "--help" || arg == "-h") { - return Ok(Mode::WorkspaceHelp); - } - Ok(Mode::WorkspaceServer { - subcommand: subcommand.clone(), - args: rest.to_vec(), - }) - } - "serve" => { - if rest.iter().any(|arg| arg == "--help" || arg == "-h") { - return Ok(Mode::WorkspaceHelp); - } - Ok(Mode::WorkspaceServer { - subcommand: "serve".to_string(), - args: rest.to_vec(), - }) - } - "--help" | "-h" => Ok(Mode::WorkspaceHelp), - other => Err(ParseError(format!( - "unknown yoi workspace subcommand `{other}`; expected `init`, `config`, `identity`, `trust-runtime`, or `serve`" - ))), - } -} - -fn run_workspace_server(subcommand: &str, args: Vec) -> ExitCode { - let command = match resolve_workspace_server_command() { - Ok(command) => command, - Err(error) => { - eprintln!("yoi workspace: {error}"); - return ExitCode::FAILURE; - } - }; - - let mut child = Command::new(&command); - child.arg(subcommand); - child.args(args); - match child.status() { - Ok(status) if status.success() => ExitCode::SUCCESS, - Ok(status) => ExitCode::from(status.code().unwrap_or(1).min(255) as u8), - Err(error) => { - eprintln!( - "yoi workspace: failed to launch `{}`: {error}", - command.to_string_lossy() - ); - ExitCode::FAILURE - } - } -} - -fn resolve_workspace_server_command() -> Result { - if let Some(value) = std::env::var_os("YOI_WORKSPACE_SERVER_COMMAND") { - if !value.is_empty() { - return Ok(value); - } - } - let current = std::env::current_exe() - .map_err(|error| ParseError(format!("failed to resolve current executable: {error}")))?; - let sibling = current.with_file_name("yoi-workspace-server"); - Ok(sibling.into_os_string()) -} - fn parse_plugin_args(args: &[String]) -> Result { let Some((subcommand, rest)) = args.split_first() else { return Err(ParseError( @@ -1622,20 +1591,7 @@ fn parse_session_id(value: &str) -> Result { fn print_help() { println!( - "yoi\n\nUsage:\n yoi [TARGET_OPTIONS] [OPTIONS]\n yoi [TARGET_OPTIONS] resume [--workspace ] [--all] [--runtime-id ]\n yoi [TARGET_OPTIONS] workers [-r|--stopped] [--workspace ] [--runtime-id ]\n yoi [TARGET_OPTIONS] panel [--workspace ]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete [--force] [--dry-run]\n yoi worker prune --older-than [--force] [--dry-run]\n yoi objective [OPTIONS]\n yoi session analyze --json\n yoi session prune --unreferenced [--older-than ] [--force] [--dry-run]\n yoi ticket [OPTIONS]\n yoi workspace init [OPTIONS] - yoi workspace config [OPTIONS] - yoi workspace identity [OPTIONS] - yoi workspace trust-runtime [OPTIONS] - yoi workspace serve [OPTIONS] - yoi server identity [OPTIONS] - yoi server trust-runtime [OPTIONS] - yoi plugin new [--json]\n yoi plugin check [--json]\n yoi plugin pack [--output ] [--json]\n yoi plugin list [--workspace ] [--profile ] [--json]\n yoi plugin show [--workspace ] [--profile ] [--json]\n yoi mcp list [--workspace ] [--profile ] [--json]\n yoi mcp show [--workspace ] [--profile ] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace ] [--profile ] [--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 Runtime workspace root for default Console/--worker/workers (defaults to cwd)\n --workspace-id Workspace identity for Backend scoped routes\n --backend Workspace Backend API URL for Backend Runtime attach/list\n --runtime-id Backend Runtime identity for attach/list\n --worker Open the Worker Console by name (attach/restore/create)\n --socket Attach a Worker Console to a specific socket with --worker\n --session Resume a specific session segment in the Worker Console\n --profile Select a reusable Profile recipe\n -h, --help Print help\n" - ); -} - -fn print_workspace_help() { - println!( - "yoi workspace\n\nUsage:\n yoi workspace init [OPTIONS]\n yoi workspace config [OPTIONS]\n yoi workspace identity [OPTIONS]\n yoi workspace trust-runtime [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi server identity [OPTIONS]\n yoi server trust-runtime [OPTIONS]\n\nDescription:\n Launches the separate yoi-workspace-server executable. The yoi binary does not link the workspace server crate. `serve` reads Workspace records from the Yoi server DB.\n\nSubcommands:\n init Initialize .yoi/workspace.toml, .yoi/workspace-backend.local.toml, and a server DB Workspace record\n config default Print the latest packaged Backend config template\n config diff Compare workspace local config with the packaged template\n identity Initialize/show the Server signing identity\n trust-runtime Register/list/revoke trusted remote Runtime records\n serve Serve the Workspace recorded in the server DB\n\nOptions forwarded to init/config diff:\n --workspace Workspace root (defaults to cwd)\n\nOptions forwarded to serve:\n --listen Listen address override\n -h, --help Print help\n\nEnvironment:\n YOI_WORKSPACE_SERVER_COMMAND Path to yoi-workspace-server executable override\n" + "yoi\n\nUsage:\n yoi [TARGET_OPTIONS] [OPTIONS]\n yoi [TARGET_OPTIONS] resume [--workspace ] [--all] [--runtime-id ]\n yoi [TARGET_OPTIONS] workers [-r|--stopped] [--workspace ] [--runtime-id ]\n yoi [TARGET_OPTIONS] panel [--workspace ]\n yoi keys\n yoi setup-model\n yoi worker [WORKER_OPTIONS]\n yoi worker delete [--force] [--dry-run]\n yoi worker prune --older-than [--force] [--dry-run]\n yoi objective [OPTIONS]\n yoi session analyze --json\n yoi session prune --unreferenced [--older-than ] [--force] [--dry-run]\n yoi ticket [OPTIONS]\n yoi plugin new [--json]\n yoi plugin check [--json]\n yoi plugin pack [--output ] [--json]\n yoi plugin list [--workspace ] [--profile ] [--json]\n yoi plugin show [--workspace ] [--profile ] [--json]\n yoi mcp list [--workspace ] [--profile ] [--json]\n yoi mcp show [--workspace ] [--profile ] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace ] [--profile ] [--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 Runtime workspace root for default Console/--worker/workers (defaults to cwd)\n --workspace-id Workspace identity for Backend scoped routes\n --backend Workspace Backend API URL for Backend Runtime attach/list\n --runtime-id Backend Runtime identity for attach/list\n --worker Open the Worker Console by name (attach/restore/create)\n --socket Attach a Worker Console to a specific socket with --worker\n --session Resume a specific session segment in the Worker Console\n --profile Select a reusable Profile recipe\n -h, --help Print help\n" ); } @@ -1668,7 +1624,9 @@ mod tests { input: CliConnectionInput<'_>, ) -> Result, ParseError> { match input { - CliConnectionInput::LocalDefault => Ok(Box::new(LocalTarget::new())), + CliConnectionInput::DefaultTarget { .. } | CliConnectionInput::LocalTarget => { + Ok(Box::new(LocalTarget::new())) + } CliConnectionInput::BackendTarget { workspace_id, .. } => Ok(Box::new( BackendTarget::new(self.backend_url, workspace_id.map(str::to_string)), )), @@ -1706,6 +1664,74 @@ mod tests { assert_eq!(config.default_connection, ClientDefaultConnection::Local); } + #[test] + fn client_config_overlay_merges_property_wise() { + let mut config = ClientConfigFile::default(); + config.apply_overlay( + toml::from_str( + r#" +default_backend = "global" +default_connection = "backend" + +[backends.global] +url = "http://global.example" + +[backends.shared] +url = "http://shared-global.example" + +[workspaces.workspace-a] +backend = "global" +"#, + ) + .unwrap(), + ); + config.apply_overlay( + toml::from_str( + r#" +default_backend = "shared" + +[backends.shared] +url = "http://shared-cwd.example" + +[workspaces.workspace-b] +backend = "shared" +"#, + ) + .unwrap(), + ); + + assert_eq!(config.default_connection, ClientDefaultConnection::Backend); + assert_eq!(config.default_backend.as_deref(), Some("shared")); + assert_eq!( + config + .backends + .get("global") + .and_then(|backend| backend.url.as_deref()), + Some("http://global.example") + ); + assert_eq!( + config + .backends + .get("shared") + .and_then(|backend| backend.url.as_deref()), + Some("http://shared-cwd.example") + ); + assert_eq!( + config + .workspaces + .get("workspace-a") + .and_then(|workspace| workspace.backend.as_deref()), + Some("global") + ); + assert_eq!( + config + .workspaces + .get("workspace-b") + .and_then(|workspace| workspace.backend.as_deref()), + Some("shared") + ); + } + #[test] fn parse_local_only_commands_reject_backend_target_options() { let err = parse_args_from(["panel", "--runtime-id", "runtime-a"]).unwrap_err(); @@ -2009,55 +2035,15 @@ mod tests { } #[test] - fn parse_workspace_serve_passthrough() { - match parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap() { - Mode::WorkspaceServer { subcommand, args } => { - assert_eq!(subcommand, "serve"); - assert_eq!(args, vec!["--listen", "127.0.0.1:0"]); - } - other => panic!("unexpected mode: {other:?}"), - } + fn parse_workspace_command_is_removed_from_yoi_surface() { + let err = parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap_err(); + assert_eq!(err.to_string(), "unknown command `workspace`"); } #[test] - fn parse_workspace_serve_leaves_legacy_flags_to_server() { - match parse_args_from(["workspace", "serve", "--workspace", "/tmp/ws"]).unwrap() { - Mode::WorkspaceServer { subcommand, args } => { - assert_eq!(subcommand, "serve"); - assert_eq!(args, vec!["--workspace", "/tmp/ws"]); - } - 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:?}"), - } - } - - #[test] - fn parse_workspace_config_passthrough() { - match parse_args_from(["workspace", "config", "diff", "--workspace", "/tmp/ws"]).unwrap() { - Mode::WorkspaceServer { subcommand, args } => { - assert_eq!(subcommand, "config"); - assert_eq!(args, vec!["diff", "--workspace", "/tmp/ws"]); - } - other => panic!("unexpected mode: {other:?}"), - } - } - - #[test] - fn parse_workspace_help() { - assert!(matches!( - parse_args_from(["workspace", "--help"]).unwrap(), - Mode::WorkspaceHelp - )); + fn parse_server_command_is_removed_from_yoi_surface() { + let err = parse_args_from(["server", "identity", "show"]).unwrap_err(); + assert_eq!(err.to_string(), "unknown command `server`"); } #[test] diff --git a/resources/workspace-backend.default.toml b/resources/workspace-backend.default.toml index 23676092..49707930 100644 --- a/resources/workspace-backend.default.toml +++ b/resources/workspace-backend.default.toml @@ -1,14 +1,14 @@ # Workspace Backend local config template. # -# `yoi workspace init` copies this packaged template to +# `yoi-workspace-server init` copies this packaged template to # `.yoi/workspace-backend.local.toml` without overwriting an existing file. # The `.local` file is intentionally git-ignored. # # Print the latest packaged template with: -# yoi workspace config default +# yoi-workspace-server config default # # Compare the local config with the latest packaged template with: -# yoi workspace config diff +# yoi-workspace-server config diff # # Omit a key to use the built-in fallback. TOML has no `null`, so optional # settings are represented by leaving the key commented out.