server: use single XDG server database

This commit is contained in:
Keisuke Hirata 2026-07-24 08:45:51 +09:00
parent 19d5396897
commit 4ff599c011
No known key found for this signature in database
16 changed files with 293 additions and 485 deletions

View File

@ -1,22 +0,0 @@
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
[ticket]
language = "Japanese"
[roles.intake]
profile = "builtin:intake"
workflow = "ticket-intake-workflow"
[roles.orchestrator]
profile = "builtin:orchestrator"
workflow = "ticket-orchestrator-routing"
[roles.coder]
profile = "builtin:coder"
workflow = "multi-agent-workflow"
[roles.reviewer]
profile = "builtin:reviewer"
workflow = "multi-agent-workflow"

View File

@ -1,13 +0,0 @@
[server]
[data]
[limits]
[[repositories]]
id = "main"
provider = "git"
uri = "."
display_name = "Yoi"
default_selector = "HEAD"

View File

@ -378,7 +378,7 @@ impl WorkspaceBackendConfigFile {
.workspace_database_path
.as_ref()
.map(|path| resolve_workspace_path(workspace_root, path))
.unwrap_or_else(|| data_root.join("workspace.db"));
.unwrap_or_else(ServerConfig::default_server_database_path);
let embedded_runtime_store_root = self
.data
.embedded_runtime_store_root
@ -570,7 +570,7 @@ mod tests {
assert_eq!(resolved.listen, "127.0.0.1:8787".parse().unwrap());
assert_eq!(resolved.server.frontend_url, DEFAULT_FRONTEND_URL);
assert_eq!(resolved.server.max_records, DEFAULT_MAX_RECORDS);
assert!(resolved.database_path.ends_with("workspace.db"));
assert!(resolved.database_path.ends_with("server.db"));
assert!(
resolved
.server
@ -643,7 +643,7 @@ embedded_runtime_store_root = "/tmp/yoi-runtime"
}
#[test]
fn data_root_derives_database_and_runtime_store_paths() {
fn data_root_derives_runtime_store_path_only() {
let dir = tempfile::tempdir().unwrap();
let config = WorkspaceBackendConfigFile::parse_str(
r#"
@ -655,10 +655,7 @@ root = ".local-data"
.unwrap();
let resolved = config.resolve(dir.path(), identity()).unwrap();
assert_eq!(
resolved.database_path,
dir.path().join(".local-data/workspace.db")
);
assert!(resolved.database_path.ends_with("server.db"));
assert_eq!(
resolved.server.embedded_runtime_store_root,
dir.path().join(".local-data/embedded-runtime")

View File

@ -4,9 +4,11 @@ use std::process::ExitCode;
use std::sync::Arc;
use tokio::net::TcpListener;
use yoi_workspace_server::store::RepositoryRecord;
use yoi_workspace_server::{
BackendRuntimesConfigFile, SqliteWorkspaceStore, WORKSPACE_BACKEND_CONFIG_TEMPLATE,
WorkspaceBackendConfigFile, WorkspaceIdentity, serve,
BackendRuntimesConfigFile, ControlPlaneStore, ServerConfig, SqliteWorkspaceStore,
WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, WorkspaceIdentity,
WorkspaceRecord, serve,
};
#[derive(Debug)]
@ -21,9 +23,6 @@ enum Command {
#[derive(Debug)]
struct ServeOptions {
workspace: PathBuf,
db: Option<PathBuf>,
frontend: Option<PathBuf>,
listen: Option<SocketAddr>,
}
@ -70,7 +69,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
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::Init(options) => run_init(options).await,
Command::ConfigDefault => run_config_default(),
Command::ConfigDiff(options) => run_config_diff(options),
Command::Skills(command) => run_skills(command),
@ -111,13 +110,50 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
}
}
fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
async fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
run_init_with_database_path(options, ServerConfig::default_server_database_path()).await
}
async fn run_init_with_database_path(
options: InitOptions,
database_path: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
let identity = WorkspaceIdentity::load_or_init(&options.workspace)?;
WorkspaceBackendConfigFile::ensure_local_config_for_workspace(&options.workspace)?;
if let Some(parent) = database_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let store = SqliteWorkspaceStore::open(&database_path)?;
store
.upsert_workspace(&WorkspaceRecord {
workspace_id: identity.workspace_id.clone(),
owner_account_id: None,
display_name: identity.display_name.clone(),
state: "active".to_string(),
created_at: identity.created_at.clone(),
updated_at: identity.created_at.clone(),
})
.await?;
store.upsert_repository(&RepositoryRecord {
workspace_id: identity.workspace_id.clone(),
repository_id: "main".to_string(),
name: "Main repository".to_string(),
kind: "git".to_string(),
provider: Some("git".to_string()),
uri: options.workspace.display().to_string(),
default_ref: Some("HEAD".to_string()),
auth_ref_kind: None,
auth_ref_key: None,
created_at: identity.created_at.clone(),
updated_at: identity.created_at.clone(),
})?;
eprintln!(
"yoi-workspace-server: initialized workspace `{}` ({})",
"yoi-workspace-server: initialized workspace `{}` ({}) in server DB `{}`",
options.workspace.display(),
identity.workspace_id
identity.workspace_id,
database_path.display()
);
Ok(())
}
@ -167,36 +203,90 @@ fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>>
}
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 database_path = ServerConfig::default_server_database_path();
if let Some(parent) = database_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let store = Arc::new(SqliteWorkspaceStore::open(&database_path)?);
let workspace = select_serve_workspace(store.as_ref())?;
let workspace_root = infer_workspace_root_from_repositories(store.as_ref(), &workspace)?;
let identity = WorkspaceIdentity {
workspace_id: workspace.workspace_id.clone(),
created_at: workspace.created_at.clone(),
display_name: workspace.display_name.clone(),
};
let runtime_config = BackendRuntimesConfigFile::load_default()?;
let mut resolved =
config_file.resolve_with_runtime_config(&options.workspace, identity, &runtime_config)?;
if let Some(db) = options.db {
resolved = resolved.with_database_path(db);
}
if let Some(frontend) = options.frontend {
resolved = resolved.with_static_assets_dir(Some(frontend));
}
let mut resolved = WorkspaceBackendConfigFile::default().resolve_with_runtime_config(
&workspace_root,
identity,
&runtime_config,
)?;
resolved.database_path = database_path.clone();
resolved.server.database_path = database_path.clone();
if let Some(listen) = options.listen {
resolved = resolved.with_listen(listen);
}
if let Some(parent) = resolved.database_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let store = Arc::new(SqliteWorkspaceStore::open(&resolved.database_path)?);
let listener = TcpListener::bind(resolved.listen).await?;
eprintln!(
"yoi-workspace-server: serving workspace `{}` on http://{}",
options.workspace.display(),
"yoi-workspace-server: serving workspace `{}` from server DB `{}` on http://{}",
workspace.workspace_id,
database_path.display(),
listener.local_addr()?
);
serve(resolved.server, store, listener).await?;
Ok(())
}
fn select_serve_workspace(store: &SqliteWorkspaceStore) -> Result<WorkspaceRecord, CliError> {
let workspaces = store
.list_workspaces()
.map_err(|error| CliError(format!("failed to list workspaces from server DB: {error}")))?;
match workspaces.as_slice() {
[] => Err(CliError(
"server DB has no workspace records; run `yoi-workspace-server init --workspace <PATH>`".to_string(),
)),
[workspace] => Ok(workspace.clone()),
_ => Err(CliError(format!(
"server DB contains {} workspaces; serve workspace selection is not implemented yet",
workspaces.len()
))),
}
}
fn infer_workspace_root_from_repositories(
store: &SqliteWorkspaceStore,
workspace: &WorkspaceRecord,
) -> Result<PathBuf, CliError> {
let repositories = store
.list_repositories(&workspace.workspace_id)
.map_err(|error| {
CliError(format!(
"failed to list repositories from server DB: {error}"
))
})?;
let Some(repository) = repositories
.iter()
.find(|repository| repository.repository_id == "main")
.or_else(|| repositories.first())
else {
return Err(CliError(format!(
"workspace `{}` has no repository records; cannot derive a workspace root",
workspace.workspace_id
)));
};
let repository_path = PathBuf::from(&repository.uri);
if !repository_path.is_absolute() {
return Err(CliError(format!(
"repository `{}` has relative URI `{}`; repository records used by serve must be absolute paths",
repository.repository_id, repository.uri
)));
}
Ok(repository_path)
}
fn parse_config_command(args: &[String]) -> Result<Command, CliError> {
let Some((subcommand, rest)) = args.split_first() else {
print_config_help();
@ -313,36 +403,12 @@ fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
}
fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
let mut workspace = None;
let mut db = None;
let mut frontend = None;
let mut listen = None;
let mut index = 0;
while index < args.len() {
let arg = &args[index];
match arg.as_str() {
"--workspace" => {
index += 1;
let value = args
.get(index)
.ok_or_else(|| CliError("--workspace requires a value".to_string()))?;
workspace = Some(PathBuf::from(value));
}
"--db" => {
index += 1;
let value = args
.get(index)
.ok_or_else(|| CliError("--db requires a value".to_string()))?;
db = Some(PathBuf::from(value));
}
"--frontend" => {
index += 1;
let value = args
.get(index)
.ok_or_else(|| CliError("--frontend requires a value".to_string()))?;
frontend = Some(PathBuf::from(value));
}
"--listen" => {
index += 1;
let value = args
@ -350,15 +416,6 @@ fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
.ok_or_else(|| CliError("--listen requires a value".to_string()))?;
listen = Some(parse_listen(value)?);
}
_ if arg.starts_with("--workspace=") => {
workspace = Some(PathBuf::from(value_after_equals(arg, "--workspace")?));
}
_ if arg.starts_with("--db=") => {
db = Some(PathBuf::from(value_after_equals(arg, "--db")?));
}
_ if arg.starts_with("--frontend=") => {
frontend = Some(PathBuf::from(value_after_equals(arg, "--frontend")?));
}
_ if arg.starts_with("--listen=") => {
listen = Some(parse_listen(value_after_equals(arg, "--listen")?)?);
}
@ -367,28 +424,14 @@ fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
}
_ => {
return Err(CliError(format!(
"unexpected positional argument `{arg}`; use --workspace <PATH>"
"unexpected positional argument `{arg}`; serve reads the workspace from the server DB"
)));
}
}
index += 1;
}
let workspace = workspace
.ok_or_else(|| CliError("serve requires --workspace <path>; the current directory is no longer used as an implicit workspace".to_string()))?;
let workspace = workspace.canonicalize().map_err(|error| {
CliError(format!(
"failed to canonicalize workspace `{}`: {error}",
workspace.display()
))
})?;
Ok(ServeOptions {
workspace,
db,
frontend,
listen,
})
Ok(ServeOptions { listen })
}
fn value_after_equals<'a>(arg: &'a str, flag: &str) -> Result<&'a str, CliError> {
@ -416,7 +459,7 @@ fn 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 packaged Backend config template to .yoi/workspace-backend.local.toml. Does not create Backend data stores.\n\nOptions:\n --workspace <PATH> 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, copies the packaged Backend config template to .yoi/workspace-backend.local.toml, and registers the Workspace in the Yoi server DB.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
);
}
@ -434,7 +477,7 @@ fn print_skills_help() {
fn print_serve_help() {
println!(
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve --workspace <PATH> [OPTIONS]\n\nDescription:\n Serves an already initialized Workspace. Run `yoi workspace init --workspace <PATH>` first. Backend serve no longer treats the process current directory as an implicit Workspace.\n\nOptions:\n --workspace <PATH> Workspace root containing .yoi project records (required)\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"
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nDescription:\n Serves the Workspace recorded in the Yoi server DB. Workspace records are stored in the XDG/Yoi data directory, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n -h, --help Print help"
);
}
@ -455,43 +498,42 @@ mod tests {
}
#[test]
fn parse_serve_rejects_missing_workspace() {
fn parse_serve_accepts_listen_only() {
let args = vec!["--listen".to_string(), "127.0.0.1:0".to_string()];
let error = parse_serve_options(&args).unwrap_err();
assert_eq!(
error.to_string(),
"serve requires --workspace <path>; the current directory is no longer used as an implicit workspace"
);
}
#[test]
fn parse_serve_requires_explicit_workspace() {
let temp = tempfile::tempdir().unwrap();
let args = vec![
"--workspace".to_string(),
temp.path().display().to_string(),
"--listen".to_string(),
"127.0.0.1:0".to_string(),
];
let options = parse_serve_options(&args).unwrap();
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
assert_eq!(options.listen.unwrap(), "127.0.0.1:0".parse().unwrap());
}
#[test]
fn parse_serve_accepts_equals_workspace() {
fn parse_serve_rejects_legacy_workspace_flag() {
let temp = tempfile::tempdir().unwrap();
let args = vec![format!("--workspace={}", temp.path().display())];
let options = parse_serve_options(&args).unwrap();
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
let error = parse_serve_options(&args).unwrap_err();
assert_eq!(error.to_string(), "unknown serve option `--workspace`");
}
#[test]
fn init_creates_identity_and_local_config_only() {
fn parse_serve_rejects_legacy_db_and_frontend_flags() {
let error = parse_serve_options(&["--db=/tmp/yoi.db".to_string()]).unwrap_err();
assert_eq!(error.to_string(), "unknown serve option `--db=/tmp/yoi.db`");
let error = parse_serve_options(&["--frontend=/tmp/web".to_string()]).unwrap_err();
assert_eq!(
error.to_string(),
"unknown serve option `--frontend=/tmp/web`"
);
}
#[tokio::test]
async fn init_creates_identity_local_config_and_server_records() {
let temp = tempfile::tempdir().unwrap();
run_init(InitOptions {
workspace: temp.path().canonicalize().unwrap(),
})
let database_path = temp.path().join("data").join("server").join("server.db");
run_init_with_database_path(
InitOptions {
workspace: temp.path().canonicalize().unwrap(),
},
database_path.clone(),
)
.await
.unwrap();
assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists());
@ -509,5 +551,19 @@ mod tests {
);
assert!(!temp.path().join(".yoi/workspace.db").exists());
assert!(!temp.path().join(".yoi/embedded-runtime").exists());
assert!(database_path.exists());
let store = SqliteWorkspaceStore::open(&database_path).unwrap();
let workspaces = store.list_workspaces().unwrap();
assert_eq!(workspaces.len(), 1);
let repositories = store
.list_repositories(&workspaces[0].workspace_id)
.unwrap();
assert_eq!(repositories.len(), 1);
assert_eq!(repositories[0].repository_id, "main");
assert_eq!(
repositories[0].uri,
temp.path().canonicalize().unwrap().display().to_string()
);
}
}

View File

@ -138,8 +138,7 @@ impl ServerConfig {
let workspace_root = workspace_root.into();
let workspace_id = identity.workspace_id;
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
let database_path =
Self::default_workspace_backend_data_root(&workspace_id).join("workspace.db");
let database_path = Self::default_server_database_path();
Self {
workspace_id,
workspace_display_name: identity.display_name,
@ -164,13 +163,31 @@ impl ServerConfig {
}
}
pub fn server_data_root_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
data_dir.into().join("server")
}
pub fn default_server_data_root() -> PathBuf {
match manifest::paths::data_dir() {
Some(data_dir) => Self::server_data_root_for_data_dir(data_dir),
None => std::env::temp_dir().join("yoi").join("server"),
}
}
pub fn server_database_path_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
Self::server_data_root_for_data_dir(data_dir).join("server.db")
}
pub fn default_server_database_path() -> PathBuf {
Self::default_server_data_root().join("server.db")
}
pub fn workspace_backend_data_root_for_data_dir(
data_dir: impl Into<PathBuf>,
workspace_id: impl AsRef<str>,
) -> PathBuf {
data_dir
.into()
.join("workspace-server")
Self::server_data_root_for_data_dir(data_dir)
.join("workspaces")
.join(workspace_id.as_ref())
}
@ -181,7 +198,8 @@ impl ServerConfig {
}
None => std::env::temp_dir()
.join("yoi")
.join("workspace-server")
.join("server")
.join("workspaces")
.join(workspace_id.as_ref()),
}
}
@ -6846,6 +6864,7 @@ mod tests {
let store_root = workspace_root.join(".test-embedded-runtime-store");
let mut config = ServerConfig::local_dev(workspace_root.clone(), test_identity())
.with_embedded_runtime_store_root(store_root);
config.database_path = workspace_root.join(".test-yoi-server.db");
config.runtime_config_path = Some(workspace_root.join(".test-config/runtimes.toml"));
config.repositories = vec![ConfiguredRepository {
id: TEST_REPOSITORY_ID.to_string(),
@ -7952,7 +7971,6 @@ mod tests {
#[tokio::test]
async fn serves_bounded_read_apis_and_static_spa_separately() {
let dir = tempfile::tempdir().unwrap();
write_ticket(dir.path(), "00000000001J2", "API Ticket", "ready");
write_objective(dir.path(), "00000000001J3", "API Objective", "active");
let static_dir = dir.path().join("static");
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
@ -7961,6 +7979,12 @@ mod tests {
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = test_server_config(dir.path());
write_ticket(
&config.database_path,
TEST_WORKSPACE_ID,
"API Ticket",
ticket::TicketWorkflowState::Ready,
);
config.static_assets_dir = Some(static_dir);
let api = WorkspaceApi::new_with_execution_backend(
config,
@ -8044,7 +8068,7 @@ mod tests {
);
let tickets = get_json(app.clone(), "/api/tickets").await;
assert_eq!(tickets["items"][0]["id"], "00000000001J2");
assert_eq!(tickets["items"][0]["title"], "API Ticket");
assert_eq!(tickets["items"][0]["state"], "ready");
let objectives = get_json(app.clone(), "/api/objectives").await;
@ -8098,7 +8122,7 @@ mod tests {
.iter()
.find(|column| column["state"] == "ready")
.unwrap();
assert_eq!(ready_column["items"][0]["id"], "00000000001J2");
assert_eq!(ready_column["items"][0]["title"], "API Ticket");
assert_eq!(
repository_tickets["diagnostics"][0]["code"],
"repository_ticket_target_metadata_absent"
@ -8456,7 +8480,8 @@ mod tests {
assert_eq!(
default_root,
data_dir
.join("workspace-server")
.join("server")
.join("workspaces")
.join(TEST_WORKSPACE_ID)
.join("embedded-runtime")
);
@ -8941,25 +8966,18 @@ mod tests {
serde_json::from_slice(&bytes).unwrap()
}
fn write_ticket(root: &Path, id: &str, title: &str, state: &str) {
let ticket_dir = root.join(".yoi/tickets").join(id);
std::fs::create_dir_all(&ticket_dir).unwrap();
std::fs::write(
ticket_dir.join("item.md"),
format!(
r#"---
title: "{title}"
state: "{state}"
created_at: "2026-01-01T00:00:00Z"
updated_at: "2026-01-02T00:00:00Z"
---
fn write_ticket(
database_path: &Path,
workspace_id: &str,
title: &str,
state: ticket::TicketWorkflowState,
) {
use ticket::TicketBackend as _;
Ticket body.
"#,
),
)
.unwrap();
std::fs::write(ticket_dir.join("thread.md"), "").unwrap();
let backend = ticket::SqliteTicketBackend::new(database_path, workspace_id);
let mut input = ticket::NewTicket::new(title);
input.workflow_state = Some(state);
backend.create(input).unwrap();
}
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {

View File

@ -226,6 +226,7 @@ pub trait ControlPlaneStore: Send + Sync {
async fn schema_version(&self) -> Result<i64>;
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>>;
fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>>;
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()>;
fn list_repositories(&self, workspace_id: &str) -> Result<Vec<RepositoryRecord>>;
@ -416,6 +417,19 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
})
}
fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>> {
self.with_conn(|conn| {
let mut stmt = conn.prepare(
r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at
FROM workspaces
ORDER BY workspace_id ASC"#,
)?;
let rows = stmt.query_map([], read_workspace_record)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)
})
}
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
self.with_conn(|conn| {
conn.execute(

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]

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,

View File

@ -262,14 +262,13 @@ in
Entrypoint = [ "/bin/yoi-workspace-server" ];
Cmd = [
"serve"
"--workspace"
"/workspace"
"--db"
"/server-data/workspace.db"
"--listen"
"0.0.0.0:8787"
];
Env = [ "PATH=/bin" ] ++ commonEnv;
Env = [
"PATH=/bin"
"YOI_DATA_DIR=/server-data"
] ++ commonEnv;
ExposedPorts = {
"8787/tcp" = { };
};

View File

@ -25,14 +25,15 @@ 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 fallback:
# <data_dir>/workspace-server/<workspace_id>/
# Workspace-scoped runtime/data root override. Leave commented to use the user-data fallback:
# <data_dir>/server/workspaces/<workspace_id>/
# Relative paths are resolved from the workspace root.
# root = ".yoi/workspace-backend.data"
# Explicit control-plane SQLite DB path override.
# If omitted, this falls back to `<data.root>/workspace.db`.
# workspace_database_path = ".yoi/workspace-backend.data/workspace.db"
# Explicit control-plane SQLite DB path override. Normal `serve` uses the Yoi
# server DB at `<data_dir>/server/server.db`; keep this commented unless a
# local test needs a custom DB path.
# workspace_database_path = ".yoi/workspace-backend.data/server.db"
# Explicit embedded Runtime fs-store root override.
# If omitted, this falls back to `<data.root>/embedded-runtime`.

23
scripts/dev-workspace.sh Executable file → Normal file
View File

@ -21,7 +21,22 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_DIR="${YOI_DEV_RUNTIME_DIR:-$ROOT_DIR/.yoi/dev}"
default_dev_runtime_dir() {
if [[ -n "${YOI_DEV_RUNTIME_DIR:-}" ]]; then
printf '%s\n' "$YOI_DEV_RUNTIME_DIR"
elif [[ -n "${YOI_RUNTIME_DIR:-}" ]]; then
printf '%s/yoi-dev\n' "$YOI_RUNTIME_DIR"
elif [[ -n "${YOI_HOME:-}" ]]; then
printf '%s/run/yoi-dev\n' "$YOI_HOME"
elif [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then
printf '%s/yoi/dev\n' "$XDG_RUNTIME_DIR"
else
printf '%s/.local/share/yoi/run/dev\n' "${HOME:?HOME is required when XDG_RUNTIME_DIR is unset}"
fi
}
RUNTIME_DIR="$(default_dev_runtime_dir)"
PID_DIR="$RUNTIME_DIR/pids"
LOG_DIR="$RUNTIME_DIR/logs"
@ -43,7 +58,7 @@ Usage: $(basename "$0") <start|stop|restart|status>
Manage the local Yoi development stack for this checkout:
runtime target/debug/worker-runtime-rest-server --bind $RUNTIME_BIND
backend target/debug/yoi-workspace-server serve --workspace $ROOT_DIR --db $ROOT_DIR/.yoi/workspace.db --listen $BACKEND_LISTEN
backend target/debug/yoi-workspace-server serve --listen $BACKEND_LISTEN
frontend deno run -A npm:vite@7.2.7 dev --host $FRONTEND_HOST --port $FRONTEND_PORT (cwd: web/workspace)
Actions:
@ -64,7 +79,7 @@ Environment overrides:
YOI_DEV_RUNTIME_ENABLED=1 set to 0 to skip the standalone runtime process
YOI_DEV_FRONTEND_HOST=0.0.0.0
YOI_DEV_FRONTEND_PORT=5173
YOI_DEV_RUNTIME_DIR=$ROOT_DIR/.yoi/dev
YOI_DEV_RUNTIME_DIR=$RUNTIME_DIR pid/log directory override (defaults to XDG runtime/data fallback)
EOF
}
@ -330,7 +345,7 @@ start_backend() {
return 1
fi
start_service backend "$ROOT_DIR" "$port" \
"$backend_bin" serve --workspace "$ROOT_DIR" --db "$ROOT_DIR/.yoi/workspace.db" --listen "$BACKEND_LISTEN"
"$backend_bin" serve --listen "$BACKEND_LISTEN"
}
start_frontend() {

View File

@ -1,77 +0,0 @@
#!/usr/bin/env sh
set -eu
# Move the legacy home data root (`$HOME/.yoi`) to the current Yoi data dir.
#
# IMPORTANT:
# This moves the directory that currently contains live sessions, worker
# runtime state, workdirs, and this checkout's parent path. Do not run while
# Yoi backend/runtime/workers/TUI/web sessions are running.
#
# Recommended usage after shutdown:
# cd /tmp
# RUN_YOI_HOME_MIGRATION=1 sh /path/to/tmp-migrate-home-yoi-data-dir.sh
#
# Destination resolution matches manifest::paths::data_dir():
# 1. YOI_DATA_DIR
# 2. YOI_HOME
# 3. XDG_DATA_HOME/yoi
# 4. HOME/.local/share/yoi
#
# This script intentionally refuses to merge into a non-empty destination. If
# the destination already has data, inspect and merge manually.
if [ "${RUN_YOI_HOME_MIGRATION:-}" != "1" ]; then
echo "Refusing to migrate. Set RUN_YOI_HOME_MIGRATION=1 after stopping all Yoi processes." >&2
exit 1
fi
HOME_DIR=${HOME:?HOME is required}
OLD_DIR=${OLD_YOI_DATA_DIR:-"$HOME_DIR/.yoi"}
if [ -n "${YOI_DATA_DIR:-}" ]; then
NEW_DIR=$YOI_DATA_DIR
elif [ -n "${YOI_HOME:-}" ]; then
NEW_DIR=$YOI_HOME
elif [ -n "${XDG_DATA_HOME:-}" ]; then
NEW_DIR=$XDG_DATA_HOME/yoi
else
NEW_DIR=$HOME_DIR/.local/share/yoi
fi
if [ ! -d "$OLD_DIR" ]; then
echo "Legacy data dir does not exist: $OLD_DIR" >&2
exit 1
fi
case "$NEW_DIR" in
"$OLD_DIR"|"$OLD_DIR"/*)
echo "Refusing to move into the old data dir or one of its children: $NEW_DIR" >&2
exit 1
;;
esac
CURRENT_DIR=$(pwd -P)
case "$CURRENT_DIR" in
"$OLD_DIR"|"$OLD_DIR"/*)
echo "Refusing to run from inside $OLD_DIR. cd outside it first, e.g. cd /tmp." >&2
exit 1
;;
esac
if [ -e "$NEW_DIR" ]; then
if [ -d "$NEW_DIR" ] && [ -z "$(find "$NEW_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
rmdir "$NEW_DIR"
else
echo "Destination already exists and is not empty: $NEW_DIR" >&2
echo "Inspect and merge manually; this script will not overwrite or merge data." >&2
exit 1
fi
fi
mkdir -p "$(dirname "$NEW_DIR")"
mv "$OLD_DIR" "$NEW_DIR"
echo "Moved legacy Yoi data dir:"
echo " from: $OLD_DIR"
echo " to: $NEW_DIR"

View File

@ -1,88 +0,0 @@
#!/usr/bin/env sh
set -eu
# Migration helper for workspace-local runtime config and legacy Ticket files.
#
# It performs:
# 1. Extract `[[runtimes.remote]]` blocks from `.yoi/workspace-backend.local.toml`.
# 2. Write them to the resolved config-dir `runtimes.toml`.
# 3. Remove those runtime blocks from `.yoi/workspace-backend.local.toml` so the
# updated backend no longer rejects the workspace config.
# 4. Run `cargo run --bin yoi -- ticket import-local` to import `.yoi/tickets`
# into the workspace SQLite DB.
#
# It intentionally does NOT delete legacy files. Run
# `tmp-remove-legacy-yoi-files.sh` only after repository DB import, ticket import,
# and DB location migration have all been verified.
#
# Preconditions:
# - Run from the repository/workspace root.
# - Stop Yoi backend/runtime/workers before running.
# - Use the updated code in this worktree.
#
# Run explicitly with:
# RUN_YOI_WORKSPACE_RUNTIME_TICKET_MIGRATION=1 sh ./tmp-migrate-workspace-runtime-and-tickets.sh
if [ "${RUN_YOI_WORKSPACE_RUNTIME_TICKET_MIGRATION:-}" != "1" ]; then
echo "Refusing to migrate. Set RUN_YOI_WORKSPACE_RUNTIME_TICKET_MIGRATION=1 after stopping Yoi processes." >&2
exit 1
fi
if [ ! -f ".yoi/workspace.toml" ]; then
echo "Refusing to run outside a Yoi workspace root: .yoi/workspace.toml not found." >&2
exit 1
fi
CONFIG_FILE=.yoi/workspace-backend.local.toml
if [ ! -f "$CONFIG_FILE" ]; then
echo "Workspace backend local config not found: $CONFIG_FILE" >&2
exit 1
fi
if [ -n "${YOI_CONFIG_DIR:-}" ]; then
CONFIG_DIR=$YOI_CONFIG_DIR
elif [ -n "${YOI_HOME:-}" ]; then
CONFIG_DIR=$YOI_HOME/config
elif [ -n "${XDG_CONFIG_HOME:-}" ]; then
CONFIG_DIR=$XDG_CONFIG_HOME/yoi
else
HOME_DIR=${HOME:?HOME is required when YOI_CONFIG_DIR, YOI_HOME, and XDG_CONFIG_HOME are unset}
CONFIG_DIR=$HOME_DIR/.config/yoi
fi
RUNTIMES_TOML=$CONFIG_DIR/runtimes.toml
RUNTIME_BLOCKS=$(mktemp)
STRIPPED_CONFIG=$(mktemp)
trap 'rm -f "$RUNTIME_BLOCKS" "$STRIPPED_CONFIG"' EXIT
awk '
/^\[\[runtimes\.remote\]\]/ { in_runtime=1; print; next }
in_runtime && /^\[/ && $0 !~ /^\[\[runtimes\.remote\]\]/ { in_runtime=0 }
in_runtime { print }
' "$CONFIG_FILE" > "$RUNTIME_BLOCKS"
if [ -s "$RUNTIME_BLOCKS" ]; then
mkdir -p "$CONFIG_DIR"
if [ -e "$RUNTIMES_TOML" ] && [ -s "$RUNTIMES_TOML" ]; then
echo "Refusing to overwrite or merge existing non-empty runtimes config: $RUNTIMES_TOML" >&2
echo "Move runtime entries manually or empty the file after inspection." >&2
exit 1
fi
cp "$RUNTIME_BLOCKS" "$RUNTIMES_TOML"
echo "Wrote runtime registrations to $RUNTIMES_TOML"
awk '
/^\[\[runtimes\.remote\]\]/ { skip=1; next }
skip && /^\[/ && $0 !~ /^\[\[runtimes\.remote\]\]/ { skip=0 }
!skip { print }
' "$CONFIG_FILE" > "$STRIPPED_CONFIG"
cp "$STRIPPED_CONFIG" "$CONFIG_FILE"
echo "Removed runtime registrations from $CONFIG_FILE"
else
echo "No [[runtimes.remote]] blocks found in $CONFIG_FILE; leaving runtimes config unchanged."
fi
cargo run --bin yoi -- ticket import-local
echo "Imported legacy .yoi/tickets into the workspace SQLite Ticket backend."
echo "Next: start the backend once to import repositories, verify data, then run tmp-remove-legacy-yoi-files.sh if safe."

View File

@ -1,43 +0,0 @@
#!/usr/bin/env sh
set -eu
# Temporary cleanup script for files that should stop being used after the
# workspace DB/runtime-config migration.
#
# Preconditions before running:
# 1. The backend/server process is stopped.
# 2. `[[runtimes.remote]]` from `.yoi/workspace-backend.local.toml` has been
# copied to the resolved config-dir `runtimes.toml`, e.g.
# `$XDG_CONFIG_HOME/yoi/runtimes.toml` when no YOI_CONFIG_DIR/YOI_HOME is set.
# 3. Repository records from `.yoi/workspace-backend.local.toml` have been
# imported into the workspace control-plane DB.
# 4. Legacy `.yoi/tickets` has been imported with `yoi ticket import-local`.
# 5. `.yoi/workspace.db` has been copied/moved to the resolved XDG data path.
#
# This intentionally does NOT remove:
# - `.yoi/workspace.toml`
# - `.yoi/workflow/**`
#
# Run explicitly with:
# RUN_YOI_LEGACY_CLEANUP=1 sh ./tmp-remove-legacy-yoi-files.sh
if [ "${RUN_YOI_LEGACY_CLEANUP:-}" != "1" ]; then
echo "Refusing to remove files. Set RUN_YOI_LEGACY_CLEANUP=1 after completing migration preconditions." >&2
exit 1
fi
if [ ! -f ".yoi/workspace.toml" ]; then
echo "Refusing to run outside a Yoi workspace root: .yoi/workspace.toml not found." >&2
exit 1
fi
rm -rf \
.yoi/tickets
rm -f \
.yoi/workspace-backend.local.toml \
.yoi/workspace.db \
.yoi/workspace.db-shm \
.yoi/workspace.db-wal
echo "Removed legacy workspace-local backend config, Ticket files, and workspace DB files."

View File

@ -27,13 +27,12 @@ The Vite dev server proxies `/api/*` to `http://127.0.0.1:8787`, so frontend hot
If you want to run the backend from the repository root instead:
```bash
cargo run -p yoi-workspace-server -- serve \
--workspace . \
--db .yoi/workspace.db \
--listen 127.0.0.1:8787
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
```
## Static build served by Rust backend
The backend reads Workspace records from the Yoi server DB at `<data_dir>/server/server.db`. Run `cargo run -p yoi-workspace-server -- init --workspace .` first when the server DB has not been initialized.
## Static build
Build the SPA:
@ -41,25 +40,7 @@ Build the SPA:
deno task build
```
Then serve the static build and API from the Rust backend:
```bash
cargo run -p yoi-workspace-server -- serve \
--workspace ../.. \
--db ../../.yoi/workspace.db \
--frontend build \
--listen 127.0.0.1:8787
```
From the repository root, the equivalent command is:
```bash
cargo run -p yoi-workspace-server -- serve \
--workspace . \
--db .yoi/workspace.db \
--frontend web/workspace/build \
--listen 127.0.0.1:8787
```
Static asset packaging is a deployment concern. Local development normally uses the Vite dev server proxy plus the Rust backend command above.
## Checks

View File

@ -4,7 +4,7 @@
"tasks": {
"install": "deno install",
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --workspace . --db .yoi/workspace.db --listen 127.0.0.1:8787",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts",
"build": "deno run -A npm:vite@7.2.7 build",