server: use single XDG server database

This commit is contained in:
2026-07-24 08:45:51 +09:00
parent 19d5396897
commit 4ff599c011
16 changed files with 293 additions and 485 deletions
+12 -33
View File
@@ -1053,12 +1053,6 @@ fn parse_workspace_args(args: &[String]) -> Result<Mode, ParseError> {
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Mode::WorkspaceHelp);
}
if !has_workspace_option(rest) {
return Err(ParseError(
"yoi workspace serve requires --workspace <PATH>; cwd is no longer forwarded as an implicit workspace"
.to_string(),
));
}
Ok(Mode::WorkspaceServer {
subcommand: "serve".to_string(),
args: rest.to_vec(),
@@ -1071,11 +1065,6 @@ fn parse_workspace_args(args: &[String]) -> Result<Mode, ParseError> {
}
}
fn has_workspace_option(args: &[String]) -> bool {
args.iter()
.any(|arg| arg == "--workspace" || arg.starts_with("--workspace="))
}
fn run_workspace_server(subcommand: &str, args: Vec<String>) -> ExitCode {
let command = match resolve_workspace_server_command() {
Ok(command) => command,
@@ -1453,13 +1442,13 @@ fn print_help() {
println!(
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi workers [--workspace <PATH>] [--workspace-id <ID>] [--backend <URL>] [--runtime-id <ID>]\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 config <COMMAND> [OPTIONS]
yoi workspace serve --workspace <PATH> [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/workers (defaults to cwd)\n --workspace-id <ID> Workspace identity for Backend scoped routes\n --backend <URL> Workspace Backend API URL for Backend Runtime attach/list\n --runtime-id <ID> Backend Runtime identity for attach/list\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 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/workers (defaults to cwd)\n --workspace-id <ID> Workspace identity for Backend scoped routes\n --backend <URL> Workspace Backend API URL for Backend Runtime attach/list\n --runtime-id <ID> Backend Runtime identity for attach/list\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() {
println!(
"yoi workspace\n\nUsage:\n yoi workspace init [OPTIONS]\n yoi workspace config <COMMAND> [OPTIONS]\n yoi workspace serve --workspace <PATH> [OPTIONS]\n\nDescription:\n Launches the separate yoi-workspace-server executable. The yoi binary does not link the workspace server crate. `serve` requires an explicit workspace path and never forwards cwd as an implicit backend workspace.\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 explicitly selected Workspace\n\nOptions forwarded to init/config diff:\n --workspace <PATH> Workspace root (defaults to cwd)\n\nOptions forwarded to serve:\n --workspace <PATH> Workspace root (required)\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"
"yoi workspace\n\nUsage:\n yoi workspace init [OPTIONS]\n yoi workspace config <COMMAND> [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. `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 serve Serve the Workspace recorded in the server DB\n\nOptions forwarded to init/config diff:\n --workspace <PATH> Workspace root (defaults to cwd)\n\nOptions forwarded 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"
);
}
@@ -1700,34 +1689,24 @@ mod tests {
#[test]
fn parse_workspace_serve_passthrough() {
match parse_args_from([
"workspace",
"serve",
"--workspace",
"/tmp/ws",
"--listen",
"127.0.0.1:0",
])
.unwrap()
{
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!["--workspace", "/tmp/ws", "--listen", "127.0.0.1:0"]
);
assert_eq!(args, vec!["--listen", "127.0.0.1:0"]);
}
other => panic!("unexpected mode: {other:?}"),
}
}
#[test]
fn parse_workspace_serve_requires_explicit_workspace() {
let err = parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap_err();
assert_eq!(
err.to_string(),
"yoi workspace serve requires --workspace <PATH>; cwd is no longer forwarded as an implicit workspace"
);
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]
+23 -32
View File
@@ -368,7 +368,7 @@ fn workspace_settings_uuid_v7(workspace: &Path) -> String {
fn backend_for_workspace(workspace: &Path) -> Result<Box<dyn TicketBackend>, TicketCliError> {
let config = TicketConfig::load_workspace(workspace)?;
let workspace_id = workspace_id_for_workspace(workspace)?;
let db_path = workspace_ticket_database_path(workspace, &workspace_id)?;
let db_path = server_database_path(workspace)?;
Ok(Box::new(
SqliteTicketBackend::new(db_path, workspace_id)
.with_record_language(config.ticket_record_language()),
@@ -380,7 +380,7 @@ fn import_local(workspace: &Path) -> Result<TicketCliOutput, TicketCliError> {
let local = LocalTicketBackend::new(config.backend_root().to_path_buf())
.with_record_language(config.ticket_record_language());
let workspace_id = workspace_id_for_workspace(workspace)?;
let db_path = workspace_ticket_database_path(workspace, &workspace_id)?;
let db_path = server_database_path(workspace)?;
let sqlite = SqliteTicketBackend::new(db_path.clone(), workspace_id)
.with_record_language(config.ticket_record_language());
sqlite.import_from_local_backend(&local)?;
@@ -391,6 +391,27 @@ fn import_local(workspace: &Path) -> Result<TicketCliOutput, TicketCliError> {
)))
}
fn server_database_path(workspace: &Path) -> Result<PathBuf, TicketCliError> {
#[cfg(test)]
{
return Ok(workspace
.join(".test-yoi-data")
.join("server")
.join("server.db"));
}
#[cfg(not(test))]
{
let _ = workspace;
let data_dir = manifest::paths::data_dir().ok_or_else(|| {
TicketCliError::new(
"could not resolve Yoi data directory for SQLite Ticket backend (set YOI_DATA_DIR, YOI_HOME, XDG_DATA_HOME, or HOME)",
)
})?;
Ok(data_dir.join("server").join("server.db"))
}
}
fn workspace_id_for_workspace(workspace: &Path) -> Result<String, TicketCliError> {
let settings_path = workspace.join(WORKSPACE_SETTINGS_RELATIVE_PATH);
let raw = fs::read_to_string(&settings_path).map_err(|error| {
@@ -419,36 +440,6 @@ fn workspace_id_for_workspace(workspace: &Path) -> Result<String, TicketCliError
})
}
fn workspace_ticket_database_path(
workspace: &Path,
workspace_id: &str,
) -> Result<PathBuf, TicketCliError> {
#[cfg(not(test))]
let _ = workspace;
#[cfg(test)]
{
return Ok(workspace
.join(".test-yoi-data")
.join("workspace-server")
.join(workspace_id)
.join("workspace.db"));
}
#[cfg(not(test))]
{
let data_dir = manifest::paths::data_dir().ok_or_else(|| {
TicketCliError::new(
"could not resolve Yoi data directory for SQLite Ticket backend (set YOI_DATA_DIR, YOI_HOME, XDG_DATA_HOME, or HOME)",
)
})?;
Ok(data_dir
.join("workspace-server")
.join(workspace_id)
.join("workspace.db"))
}
}
fn create(
backend: &dyn TicketBackend,
options: CreateOptions,