diff --git a/.yoi/tickets/00001KWGT4WYB/item.md b/.yoi/tickets/00001KWGT4WYB/item.md index 22a78d6a..b9b1a1e5 100644 --- a/.yoi/tickets/00001KWGT4WYB/item.md +++ b/.yoi/tickets/00001KWGT4WYB/item.md @@ -2,7 +2,7 @@ title: 'Workspace初期化をinitコマンドに切り出しserveの副作用をなくす' state: 'closed' created_at: '2026-07-02T07:02:02Z' -updated_at: '2026-07-02T09:20:26Z' +updated_at: '2026-07-02T11:59:35Z' assignee: null queued_by: 'yoi ticket' queued_at: '2026-07-02T09:03:56Z' @@ -14,30 +14,32 @@ queued_at: '2026-07-02T09:03:56Z' - `WorkspaceIdentity::load_or_init(workspace_root)` - `.yoi/workspace.toml` が無ければ作る。 -- `WorkspaceBackendConfigFile::ensure_default_template_for_workspace(workspace_root)` - - `.yoi/workspace-backend.default.toml` が無ければ `resources/workspace-backend.default.toml` からコピーする。 +- `WorkspaceBackendConfigFile::ensure_local_config_for_workspace(workspace_root)` + - `.yoi/workspace-backend.local.toml` が無ければ `resources/workspace-backend.default.toml` からコピーする。 -1Workspace=1Backend の現状では、Backend の実行 directory / `--workspace` が workspace root になっている。この前提自体はよいが、`serve` が初期化副作用を持つと、間違った cwd で起動しただけで `.yoi/workspace.toml` や default config template が作られる。 +1Workspace=1Backend の現状では、Backend の実行 directory / `--workspace` が workspace root になっている。この前提自体はよいが、`serve` が初期化副作用を持つと、間違った cwd で起動しただけで `.yoi/workspace.toml` や local config が作られる。 Workspace の一回だけ実行されるべき初期化は、明示的な `init` コマンドへ切り出し、`serve` は既に初期化済みの workspace を起動するだけにする。 ## 目的 - Workspace 初期化を明示コマンドに切り出す。 -- `serve` 起動時に workspace identity / default config template を作らない。 +- `serve` 起動時に workspace identity / local config を作らない。 - `serve` は未初期化 workspace に対して typed diagnostic で失敗する。 - 1Workspace=1Backend の前提を保ち、workspace root は Backend 起動対象として明示する。 -- 初期化で作る record / template と、serve/runtime が生成する data を分ける。 +- 初期化で作る record / local config と、serve/runtime が生成する data を分ける。 - 現状の local filesystem 保存を採用しつつ、Workspace / Project record の将来的な provider 可換性を妨げない。 ## コマンド設計 ### Product CLI -`yoi` 側に workspace init subcommand を追加する。 +`yoi` 側に workspace init と config subcommand を追加する。 ```text yoi workspace init [--workspace ] +yoi workspace config default +yoi workspace config diff [--workspace ] yoi workspace serve [OPTIONS] ``` @@ -49,6 +51,8 @@ yoi workspace serve [OPTIONS] ```text yoi-workspace-server init [--workspace ] +yoi-workspace-server config default +yoi-workspace-server config diff [--workspace ] yoi-workspace-server serve [OPTIONS] ``` @@ -60,7 +64,7 @@ yoi-workspace-server serve [OPTIONS] ```text .yoi/workspace.toml -.yoi/workspace-backend.default.toml +.yoi/workspace-backend.local.toml ``` ### `.yoi/workspace.toml` @@ -70,17 +74,16 @@ yoi-workspace-server serve [OPTIONS] - 既存ファイルがある場合は parse/validate し、上書きしない。 - 作成は `create_new` semantics を維持し、race 時は既存 record を読み直す。 -### `.yoi/workspace-backend.default.toml` +### `.yoi/workspace-backend.local.toml` -- `resources/workspace-backend.default.toml` からコピーする workspace-local template。 +- `resources/workspace-backend.default.toml` からコピーする workspace-local config。 - 既存ファイルがある場合は上書きしない。 -- `.local` config ではないため、ユーザーが実設定として使う場合は `.yoi/workspace-backend.local.toml` にコピーして編集する。 +- packaged default の最新版は `yoi workspace config default` で参照し、local との差分は `yoi workspace config diff` で確認する。 ## 初期化で作らないもの `init` は Backend / Runtime data を作らない。 -- `.yoi/workspace-backend.local.toml` - control-plane SQLite DB - embedded Runtime fs-store - logs / pid files @@ -131,7 +134,7 @@ Ticket / Objective などの project record は、現状では `.yoi/tickets` / 1. workspace root を決める。 2. `.yoi/workspace.toml` を load する。 -3. `.yoi/workspace-backend.default.toml` は作らない。 +3. `.yoi/workspace-backend.local.toml` は作らない。 4. `.yoi/workspace-backend.local.toml` があれば読む。無ければ defaults。 5. resolved `ServerConfig` で Backend を起動する。 @@ -143,7 +146,7 @@ Diagnostic 例: workspace is not initialized at ; run `yoi workspace init --workspace ` first ``` -`serve` は `.yoi/workspace-backend.default.toml` が無いだけでは失敗しない方針とする。これは template であり、runtime config の authority ではないため。ただし init 済み workspace で default template が欠けている場合に warning を出すかは実装時に判断する。 +`serve` は `.yoi/workspace-backend.local.toml` が無いだけでは失敗しない方針とする。config file が欠けている場合は code fallback を使う。ただし `init` は通常 `.local` config を作るため、欠落は診断対象にしてよい。 ## 内部 API 整理 @@ -161,7 +164,7 @@ WorkspaceInitialization::load_required(workspace_root) -> WorkspaceIdentity ```rust WorkspaceIdentity::init_if_missing(...) WorkspaceIdentity::load_required(...) -WorkspaceBackendConfigFile::ensure_default_template_for_workspace(...) +WorkspaceBackendConfigFile::ensure_local_config_for_workspace(...) ``` 重要なのは、`serve` が `load_or_init` を呼ばないこと。 @@ -171,20 +174,23 @@ WorkspaceBackendConfigFile::ensure_default_template_for_workspace(...) この Ticket は Workspace Backend config schema の方針を維持し、新規 Backend 設定項目を CLI flag として増やさない。 - `init` に必要なのは `--workspace` だけ。 +- `config diff` に必要なのも `--workspace` だけ。 - `serve` の既存 legacy dev flags をこの Ticket で全面削除するかは別判断にする。 - ただし `serve` の初期化副作用は必ずなくす。 ## 実装要件 - `yoi workspace init [--workspace ]` を追加する。 +- `yoi workspace config default` / `yoi workspace config diff [--workspace ]` を追加する。 +- `yoi-workspace-server config default` / `yoi-workspace-server config diff [--workspace ]` を追加する。 - `yoi-workspace-server init [--workspace ]` を追加する。 - workspace-server 側の help に init を追加する。 - `WorkspaceIdentity` に load-only path を追加する。 - 既存 `load_or_init` は init command 内部用に残してよいが、serve からは呼ばない。 - `serve` から `WorkspaceIdentity::load_or_init(...)` を外す。 -- `serve` から `WorkspaceBackendConfigFile::ensure_default_template_for_workspace(...)` を外す。 +- `serve` から `WorkspaceBackendConfigFile::ensure_local_config_for_workspace(...)` を外す。 - 未初期化 workspace の `serve` は typed diagnostic で失敗する。 -- `init` は existing workspace identity / default template を上書きしない。 +- `init` は existing workspace identity / local config を上書きしない。 - `init` は data root / DB / embedded Runtime store を作らない。 - `init` は Ticket / Objective など provider-specific project record layout を作らない。 - `serve` / Browser-facing API / Runtime create path に workspace-local filesystem path を正本識別子として漏らさない。 @@ -192,19 +198,21 @@ WorkspaceBackendConfigFile::ensure_default_template_for_workspace(...) ## 受け入れ条件 - `yoi workspace init [--workspace ]` が使える。 +- `yoi workspace config default` が packaged template を表示する。 +- `yoi workspace config diff [--workspace ]` が `.local` config と packaged template を比較する。 - `yoi-workspace-server init [--workspace ]` が使える。 -- `init` が `.yoi/workspace.toml` と `.yoi/workspace-backend.default.toml` を作る。 -- `init` が既存 `.yoi/workspace.toml` / `.yoi/workspace-backend.default.toml` を上書きしない。 -- `init` が `.yoi/workspace-backend.local.toml`、DB、embedded Runtime fs-store、logs を作らない。 +- `init` が `.yoi/workspace.toml` と `.yoi/workspace-backend.local.toml` を作る。 +- `init` が既存 `.yoi/workspace.toml` / `.yoi/workspace-backend.local.toml` を上書きしない。 +- `init` が DB、embedded Runtime fs-store、logs を作らない. - `init` が Ticket / Objective body や provider-specific project record layout を作らない。 - `serve` が `.yoi/workspace.toml` を新規作成しない。 -- `serve` が `.yoi/workspace-backend.default.toml` を新規作成しない。 +- `serve` が `.yoi/workspace-backend.local.toml` を新規作成しない。 - 未初期化 workspace で `serve` すると、`workspace init` を促す diagnostic で失敗する。 -- 初期化済み workspace では config file が無くても defaults で `serve` が起動できる。 +- 初期化済み workspace では `.local` config が存在し、欠けた key は code fallback で解決される。 - Browser-facing API / Runtime create path / Worker conversation context に `.yoi/workspace.toml`、`.yoi/tickets/...`、data root、DB path、Runtime store path が正本識別子として漏れない。 - Project record provider 可換性を妨げないことが code/docs/tests 上で明確になっている。 -- Help text が `workspace init` と `workspace serve` の責務差を説明している。 -- Focused tests が init 作成、init idempotency、serve 未初期化拒否、serve 初期化済み起動、data 非作成を確認する。 +- Help text が `workspace init`、`workspace config`、`workspace serve` の責務差を説明している。 +- Focused tests が init 作成、init idempotency、serve 未初期化拒否、serve 初期化済み起動、config default/diff、data 非作成を確認する。 - `cargo test -p yoi-workspace-server` が通る。 - `cargo test -p yoi` が通る、または CLI parser tests が通る。 - `cargo check -p yoi` が通る。 @@ -213,7 +221,7 @@ WorkspaceBackendConfigFile::ensure_default_template_for_workspace(...) ## 対象外 -- Backend config schema の追加変更。 +- Backend config schema の追加変更。ただし packaged template の参照・diff CLI はこの Ticket の correction として扱う。 - frontend Vite config の管理。 - remote Runtime supervisor。 - secret store 実装。 diff --git a/.yoi/tickets/00001KWGT4WYB/thread.md b/.yoi/tickets/00001KWGT4WYB/thread.md index 1f3c965d..54f4530b 100644 --- a/.yoi/tickets/00001KWGT4WYB/thread.md +++ b/.yoi/tickets/00001KWGT4WYB/thread.md @@ -144,4 +144,29 @@ Validation: - `nix build .#yoi --no-link` +--- + + + +## Implementation report + +Adjusted Workspace Backend config handling so no workspace-local `.default.toml` file is created. + +Decision: +- `resources/workspace-backend.default.toml` remains the packaged template source. +- `yoi workspace init` copies that template directly to `.yoi/workspace-backend.local.toml` with create-new semantics and never overwrites an existing local config. +- `.yoi/workspace-backend.default.toml` is no longer created. +- `yoi workspace config default` / `yoi-workspace-server config default` print the latest packaged template. +- `yoi workspace config diff` / `yoi-workspace-server config diff` compare the workspace-local config with the packaged template. +- Built-in code fallback remains separate from the generated local config file. + +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: `yoi-workspace-server init` creates only `.yoi/workspace.toml` and `.yoi/workspace-backend.local.toml`; `config default` prints the packaged template; `config diff` reports a match for fresh init. + + --- diff --git a/crates/workspace-server/src/config.rs b/crates/workspace-server/src/config.rs index 1f3465cf..28d20f20 100644 --- a/crates/workspace-server/src/config.rs +++ b/crates/workspace-server/src/config.rs @@ -10,9 +10,7 @@ use crate::server::{AuthConfig, ServerConfig}; use crate::{Error, Result}; pub const WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH: &str = ".yoi/workspace-backend.local.toml"; -pub const WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH: &str = - ".yoi/workspace-backend.default.toml"; -pub const WORKSPACE_BACKEND_DEFAULT_CONFIG_TEMPLATE: &str = +pub const WORKSPACE_BACKEND_CONFIG_TEMPLATE: &str = include_str!("../../../resources/workspace-backend.default.toml"); const DEFAULT_LISTEN: &str = "127.0.0.1:8787"; const DEFAULT_FRONTEND_URL: &str = "http://127.0.0.1:5173"; @@ -78,6 +76,61 @@ pub struct RemoteRuntimeConfigFile { pub token_ref: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConfigDiff { + pub differs: bool, + pub text: String, +} + +impl ConfigDiff { + fn new(default: &str, local: &str) -> Self { + if default == local { + return Self { + differs: false, + text: "workspace backend local config matches the packaged default\n".to_string(), + }; + } + + let mut text = String::from("--- packaged default\n+++ workspace local\n"); + let default_lines = default.lines().collect::>(); + let local_lines = local.lines().collect::>(); + let max = default_lines.len().max(local_lines.len()); + for index in 0..max { + match (default_lines.get(index), local_lines.get(index)) { + (Some(left), Some(right)) if left == right => { + text.push(' '); + text.push_str(left); + text.push('\n'); + } + (Some(left), Some(right)) => { + text.push('-'); + text.push_str(left); + text.push('\n'); + text.push('+'); + text.push_str(right); + text.push('\n'); + } + (Some(left), None) => { + text.push('-'); + text.push_str(left); + text.push('\n'); + } + (None, Some(right)) => { + text.push('+'); + text.push_str(right); + text.push('\n'); + } + (None, None) => {} + } + } + + Self { + differs: true, + text, + } + } +} + #[derive(Clone)] pub struct ResolvedWorkspaceBackendConfig { pub server: ServerConfig, @@ -92,14 +145,8 @@ impl WorkspaceBackendConfigFile { .join(WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH) } - pub fn default_template_path_for_workspace(workspace_root: impl AsRef) -> PathBuf { - workspace_root - .as_ref() - .join(WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH) - } - - pub fn ensure_default_template_for_workspace(workspace_root: impl AsRef) -> Result<()> { - let path = Self::default_template_path_for_workspace(workspace_root); + pub fn ensure_local_config_for_workspace(workspace_root: impl AsRef) -> Result<()> { + let path = Self::path_for_workspace(workspace_root); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } @@ -110,7 +157,7 @@ impl WorkspaceBackendConfigFile { { Ok(mut file) => { use std::io::Write; - file.write_all(WORKSPACE_BACKEND_DEFAULT_CONFIG_TEMPLATE.as_bytes())?; + file.write_all(WORKSPACE_BACKEND_CONFIG_TEMPLATE.as_bytes())?; file.sync_all()?; Ok(()) } @@ -119,6 +166,20 @@ impl WorkspaceBackendConfigFile { } } + pub fn local_config_diff_for_workspace(workspace_root: impl AsRef) -> Result { + let workspace_root = workspace_root.as_ref(); + let path = Self::path_for_workspace(workspace_root); + 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", + path.display(), + workspace_root.display() + ))), + Err(error) => Err(Error::Io(error)), + } + } + pub fn load_for_workspace(workspace_root: impl AsRef) -> Result { let path = Self::path_for_workspace(workspace_root); match fs::read_to_string(&path) { @@ -367,17 +428,39 @@ root = ".local-data" } #[test] - fn copies_default_template_without_overwriting() { + fn copies_local_config_without_overwriting() { let dir = tempfile::tempdir().unwrap(); - WorkspaceBackendConfigFile::ensure_default_template_for_workspace(dir.path()).unwrap(); - let path = WorkspaceBackendConfigFile::default_template_path_for_workspace(dir.path()); + WorkspaceBackendConfigFile::ensure_local_config_for_workspace(dir.path()).unwrap(); + let path = WorkspaceBackendConfigFile::path_for_workspace(dir.path()); let raw = fs::read_to_string(&path).unwrap(); - assert_eq!(raw, WORKSPACE_BACKEND_DEFAULT_CONFIG_TEMPLATE); + assert_eq!(raw, WORKSPACE_BACKEND_CONFIG_TEMPLATE); WorkspaceBackendConfigFile::parse_str(&raw, &path).unwrap(); - fs::write(&path, "# custom template\n").unwrap(); - WorkspaceBackendConfigFile::ensure_default_template_for_workspace(dir.path()).unwrap(); - assert_eq!(fs::read_to_string(&path).unwrap(), "# custom template\n"); + fs::write(&path, "# custom local config\n").unwrap(); + WorkspaceBackendConfigFile::ensure_local_config_for_workspace(dir.path()).unwrap(); + assert_eq!( + fs::read_to_string(&path).unwrap(), + "# custom local config\n" + ); + } + + #[test] + fn local_config_diff_reports_match_and_difference() { + let dir = tempfile::tempdir().unwrap(); + WorkspaceBackendConfigFile::ensure_local_config_for_workspace(dir.path()).unwrap(); + let matched = + WorkspaceBackendConfigFile::local_config_diff_for_workspace(dir.path()).unwrap(); + assert!(!matched.differs); + + fs::write( + WorkspaceBackendConfigFile::path_for_workspace(dir.path()), + "[server]\nlisten = \"127.0.0.1:9999\"\n", + ) + .unwrap(); + let diff = WorkspaceBackendConfigFile::local_config_diff_for_workspace(dir.path()).unwrap(); + assert!(diff.differs); + assert!(diff.text.contains("+++ workspace local")); + assert!(diff.text.contains("127.0.0.1:9999")); } #[test] diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 160ba976..88073049 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -15,8 +15,8 @@ pub mod server; pub mod store; pub use config::{ - ResolvedWorkspaceBackendConfig, WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH, - WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH, WorkspaceBackendConfigFile, + ConfigDiff, ResolvedWorkspaceBackendConfig, WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH, + WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, }; pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity}; pub use records::{ diff --git a/crates/workspace-server/src/main.rs b/crates/workspace-server/src/main.rs index 5247dcb3..8010761f 100644 --- a/crates/workspace-server/src/main.rs +++ b/crates/workspace-server/src/main.rs @@ -5,13 +5,16 @@ use std::sync::Arc; use tokio::net::TcpListener; use yoi_workspace_server::{ - SqliteWorkspaceStore, WorkspaceBackendConfigFile, WorkspaceIdentity, serve, + SqliteWorkspaceStore, WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, + WorkspaceIdentity, serve, }; #[derive(Debug)] enum Command { Serve(ServeOptions), Init(InitOptions), + ConfigDefault, + ConfigDiff(WorkspacePathOptions), Help, } @@ -28,6 +31,11 @@ struct InitOptions { workspace: PathBuf, } +#[derive(Debug)] +struct WorkspacePathOptions { + workspace: PathBuf, +} + #[derive(Debug)] struct CliError(String); @@ -55,6 +63,8 @@ async fn run() -> Result<(), Box> { match parse_command(&args)? { Command::Serve(options) => run_serve(options).await, Command::Init(options) => run_init(options), + Command::ConfigDefault => run_config_default(), + Command::ConfigDiff(options) => run_config_diff(options), Command::Help => Ok(()), } } @@ -73,6 +83,7 @@ fn parse_command(args: &[String]) -> Result { } Ok(Command::Init(parse_init_options(rest)?)) } + "config" => parse_config_command(rest), "serve" => { if rest.iter().any(|arg| arg == "--help" || arg == "-h") { print_serve_help(); @@ -85,14 +96,14 @@ fn parse_command(args: &[String]) -> Result { Ok(Command::Help) } other => Err(CliError(format!( - "unknown command `{other}`; expected `init` or `serve`" + "unknown command `{other}`; expected `init`, `config`, or `serve`" ))), } } fn run_init(options: InitOptions) -> Result<(), Box> { let identity = WorkspaceIdentity::load_or_init(&options.workspace)?; - WorkspaceBackendConfigFile::ensure_default_template_for_workspace(&options.workspace)?; + WorkspaceBackendConfigFile::ensure_local_config_for_workspace(&options.workspace)?; eprintln!( "yoi-workspace-server: initialized workspace `{}` ({})", options.workspace.display(), @@ -101,6 +112,17 @@ fn run_init(options: InitOptions) -> Result<(), Box> { Ok(()) } +fn run_config_default() -> Result<(), Box> { + print!("{WORKSPACE_BACKEND_CONFIG_TEMPLATE}"); + Ok(()) +} + +fn run_config_diff(options: WorkspacePathOptions) -> Result<(), Box> { + let diff = WorkspaceBackendConfigFile::local_config_diff_for_workspace(&options.workspace)?; + print!("{}", diff.text); + Ok(()) +} + async fn run_serve(options: ServeOptions) -> Result<(), Box> { let identity = WorkspaceIdentity::load_required(&options.workspace)?; let config_file = WorkspaceBackendConfigFile::load_for_workspace(&options.workspace)?; @@ -130,6 +152,65 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box Result { + let Some((subcommand, rest)) = args.split_first() else { + print_config_help(); + return Ok(Command::Help); + }; + match subcommand.as_str() { + "default" => { + if rest.iter().any(|arg| arg == "--help" || arg == "-h") { + print_config_help(); + return Ok(Command::Help); + } + if !rest.is_empty() { + return Err(CliError( + "config default does not accept options".to_string(), + )); + } + Ok(Command::ConfigDefault) + } + "diff" => { + if rest.iter().any(|arg| arg == "--help" || arg == "-h") { + print_config_help(); + return Ok(Command::Help); + } + Ok(Command::ConfigDiff(parse_workspace_path_options(rest)?)) + } + "--help" | "-h" => { + print_config_help(); + Ok(Command::Help) + } + other => Err(CliError(format!( + "unknown config subcommand `{other}`; expected `default` or `diff`" + ))), + } +} + +fn parse_workspace_path_options(args: &[String]) -> Result { + 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 workspace option `{other}`"))), + } + } + let workspace = workspace + .canonicalize() + .map_err(|error| CliError(format!("failed to canonicalize workspace: {error}")))?; + Ok(WorkspacePathOptions { workspace }) +} + fn parse_init_options(args: &[String]) -> Result { let mut workspace = std::env::current_dir() .map_err(|error| CliError(format!("failed to read current dir: {error}")))?; @@ -252,13 +333,19 @@ fn parse_listen(value: &str) -> Result { fn print_help() { println!( - "yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\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 config [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 Workspace root to initialize (defaults to cwd)\n -h, --help Print help" + "yoi-workspace-server init\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity and copies the packaged Backend config template to .yoi/workspace-backend.local.toml. Does not create Backend data stores.\n\nOptions:\n --workspace Workspace root to initialize (defaults to cwd)\n -h, --help Print help" + ); +} + +fn print_config_help() { + println!( + "yoi-workspace-server config\n\nUsage:\n yoi-workspace-server config default\n yoi-workspace-server config diff [OPTIONS]\n\nDescription:\n Prints the packaged Workspace Backend config template or compares it with the workspace-local config.\n\nOptions for diff:\n --workspace Workspace root (defaults to cwd)\n -h, --help Print help" ); } @@ -272,7 +359,8 @@ fn print_serve_help() { mod tests { use super::*; use yoi_workspace_server::{ - WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH, WORKSPACE_IDENTITY_RELATIVE_PATH, + WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH, WORKSPACE_BACKEND_CONFIG_TEMPLATE, + WORKSPACE_IDENTITY_RELATIVE_PATH, }; #[test] @@ -284,7 +372,7 @@ mod tests { } #[test] - fn init_creates_identity_and_default_template_only() { + fn init_creates_identity_and_local_config_only() { let temp = tempfile::tempdir().unwrap(); run_init(InitOptions { workspace: temp.path().canonicalize().unwrap(), @@ -292,15 +380,16 @@ mod tests { .unwrap(); assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists()); - assert!( - temp.path() - .join(WORKSPACE_BACKEND_DEFAULT_CONFIG_RELATIVE_PATH) - .exists() + let local_config_path = temp.path().join(WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH); + assert!(local_config_path.exists()); + assert_eq!( + std::fs::read_to_string(local_config_path).unwrap(), + WORKSPACE_BACKEND_CONFIG_TEMPLATE ); assert!( !temp .path() - .join(".yoi/workspace-backend.local.toml") + .join(".yoi/workspace-backend.default.toml") .exists() ); assert!(!temp.path().join(".yoi/workspace.db").exists()); diff --git a/crates/yoi/src/main.rs b/crates/yoi/src/main.rs index 3d01f2ea..0ff2a40f 100644 --- a/crates/yoi/src/main.rs +++ b/crates/yoi/src/main.rs @@ -610,7 +610,8 @@ fn current_dir() -> Result { fn parse_workspace_args(args: &[String]) -> Result { let Some((subcommand, rest)) = args.split_first() else { return Err(ParseError( - "yoi workspace requires `init` or `serve` (try `yoi workspace --help`)".to_string(), + "yoi workspace requires `init`, `config`, or `serve` (try `yoi workspace --help`)" + .to_string(), )); }; match subcommand.as_str() { @@ -623,6 +624,15 @@ fn parse_workspace_args(args: &[String]) -> Result { 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(), + }) + } "serve" => { if rest.iter().any(|arg| arg == "--help" || arg == "-h") { return Ok(Mode::WorkspaceHelp); @@ -634,7 +644,7 @@ fn parse_workspace_args(args: &[String]) -> Result { } "--help" | "-h" => Ok(Mode::WorkspaceHelp), other => Err(ParseError(format!( - "unknown yoi workspace subcommand `{other}`; expected `init` or `serve`" + "unknown yoi workspace subcommand `{other}`; expected `init`, `config`, or `serve`" ))), } } @@ -1015,13 +1025,14 @@ fn parse_session_id(value: &str) -> Result { fn print_help() { println!( "yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace ] [--all]\n yoi 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 serve [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 (defaults to cwd)\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 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 Workspace root (defaults to cwd)\n\nLegacy dev options forwarded to serve:\n --db SQLite database path override\n --frontend Static SPA build directory 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 workspace\n\nUsage:\n yoi workspace init [OPTIONS]\n yoi workspace config [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 .yoi/workspace-backend.local.toml\n config default Print the latest packaged Backend config template\n config diff Compare workspace local config with the packaged template\n serve Serve an already initialized Workspace\n\nOptions forwarded to init/config diff/serve:\n --workspace Workspace root (defaults to cwd)\n\nLegacy dev options forwarded to serve:\n --db SQLite database path override\n --frontend Static SPA build directory 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" ); } @@ -1244,6 +1255,17 @@ mod tests { } } + #[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!( diff --git a/resources/workspace-backend.default.toml b/resources/workspace-backend.default.toml index 44234d90..96cfedc2 100644 --- a/resources/workspace-backend.default.toml +++ b/resources/workspace-backend.default.toml @@ -1,10 +1,16 @@ # Workspace Backend local config template. # -# Copied to `.yoi/workspace-backend.default.toml` during workspace init. -# Copy that file to `.yoi/workspace-backend.local.toml` and edit it. +# `yoi workspace init` copies this packaged template to +# `.yoi/workspace-backend.local.toml` without overwriting an existing file. # The `.local` file is intentionally git-ignored. # -# Omit a key to use the built-in default. TOML has no `null`, so optional +# Print the latest packaged template with: +# yoi workspace config default +# +# Compare the local config with the latest packaged template with: +# yoi workspace 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. [server] @@ -19,17 +25,17 @@ frontend_url = "http://127.0.0.1:5173" # static_assets_dir = "web/workspace/dist" [data] -# Backend data root override. Leave commented to use the user-data default: +# Backend data root override. Leave commented to use the user-data fallback: # /workspace-server// # Relative paths are resolved from the workspace root. # root = ".yoi/workspace-backend.data" # Explicit control-plane SQLite DB path override. -# If omitted, this defaults to `/workspace.db`. +# If omitted, this falls back to `/workspace.db`. # workspace_database_path = ".yoi/workspace-backend.data/workspace.db" # Explicit embedded Runtime fs-store root override. -# If omitted, this defaults to `/embedded-runtime`. +# If omitted, this falls back to `/embedded-runtime`. # embedded_runtime_store_root = ".yoi/workspace-backend.data/embedded-runtime" [limits]