server: use single XDG server database
This commit is contained in:
parent
19d5396897
commit
4ff599c011
|
|
@ -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"
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
[server]
|
|
||||||
|
|
||||||
[data]
|
|
||||||
|
|
||||||
[limits]
|
|
||||||
|
|
||||||
[[repositories]]
|
|
||||||
id = "main"
|
|
||||||
provider = "git"
|
|
||||||
uri = "."
|
|
||||||
display_name = "Yoi"
|
|
||||||
default_selector = "HEAD"
|
|
||||||
|
|
||||||
|
|
@ -378,7 +378,7 @@ impl WorkspaceBackendConfigFile {
|
||||||
.workspace_database_path
|
.workspace_database_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|path| resolve_workspace_path(workspace_root, path))
|
.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
|
let embedded_runtime_store_root = self
|
||||||
.data
|
.data
|
||||||
.embedded_runtime_store_root
|
.embedded_runtime_store_root
|
||||||
|
|
@ -570,7 +570,7 @@ mod tests {
|
||||||
assert_eq!(resolved.listen, "127.0.0.1:8787".parse().unwrap());
|
assert_eq!(resolved.listen, "127.0.0.1:8787".parse().unwrap());
|
||||||
assert_eq!(resolved.server.frontend_url, DEFAULT_FRONTEND_URL);
|
assert_eq!(resolved.server.frontend_url, DEFAULT_FRONTEND_URL);
|
||||||
assert_eq!(resolved.server.max_records, DEFAULT_MAX_RECORDS);
|
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!(
|
assert!(
|
||||||
resolved
|
resolved
|
||||||
.server
|
.server
|
||||||
|
|
@ -643,7 +643,7 @@ embedded_runtime_store_root = "/tmp/yoi-runtime"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn data_root_derives_database_and_runtime_store_paths() {
|
fn data_root_derives_runtime_store_path_only() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let config = WorkspaceBackendConfigFile::parse_str(
|
let config = WorkspaceBackendConfigFile::parse_str(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -655,10 +655,7 @@ root = ".local-data"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let resolved = config.resolve(dir.path(), identity()).unwrap();
|
let resolved = config.resolve(dir.path(), identity()).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(resolved.database_path.ends_with("server.db"));
|
||||||
resolved.database_path,
|
|
||||||
dir.path().join(".local-data/workspace.db")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.server.embedded_runtime_store_root,
|
resolved.server.embedded_runtime_store_root,
|
||||||
dir.path().join(".local-data/embedded-runtime")
|
dir.path().join(".local-data/embedded-runtime")
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ use std::process::ExitCode;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use yoi_workspace_server::store::RepositoryRecord;
|
||||||
use yoi_workspace_server::{
|
use yoi_workspace_server::{
|
||||||
BackendRuntimesConfigFile, SqliteWorkspaceStore, WORKSPACE_BACKEND_CONFIG_TEMPLATE,
|
BackendRuntimesConfigFile, ControlPlaneStore, ServerConfig, SqliteWorkspaceStore,
|
||||||
WorkspaceBackendConfigFile, WorkspaceIdentity, serve,
|
WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, WorkspaceIdentity,
|
||||||
|
WorkspaceRecord, serve,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -21,9 +23,6 @@ enum Command {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ServeOptions {
|
struct ServeOptions {
|
||||||
workspace: PathBuf,
|
|
||||||
db: Option<PathBuf>,
|
|
||||||
frontend: Option<PathBuf>,
|
|
||||||
listen: Option<SocketAddr>,
|
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<_>>();
|
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||||
match parse_command(&args)? {
|
match parse_command(&args)? {
|
||||||
Command::Serve(options) => run_serve(options).await,
|
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::ConfigDefault => run_config_default(),
|
||||||
Command::ConfigDiff(options) => run_config_diff(options),
|
Command::ConfigDiff(options) => run_config_diff(options),
|
||||||
Command::Skills(command) => run_skills(command),
|
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)?;
|
let identity = WorkspaceIdentity::load_or_init(&options.workspace)?;
|
||||||
WorkspaceBackendConfigFile::ensure_local_config_for_workspace(&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!(
|
eprintln!(
|
||||||
"yoi-workspace-server: initialized workspace `{}` ({})",
|
"yoi-workspace-server: initialized workspace `{}` ({}) in server DB `{}`",
|
||||||
options.workspace.display(),
|
options.workspace.display(),
|
||||||
identity.workspace_id
|
identity.workspace_id,
|
||||||
|
database_path.display()
|
||||||
);
|
);
|
||||||
Ok(())
|
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>> {
|
async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let identity = WorkspaceIdentity::load_required(&options.workspace)?;
|
let database_path = ServerConfig::default_server_database_path();
|
||||||
let config_file = WorkspaceBackendConfigFile::load_for_workspace(&options.workspace)?;
|
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 runtime_config = BackendRuntimesConfigFile::load_default()?;
|
||||||
let mut resolved =
|
let mut resolved = WorkspaceBackendConfigFile::default().resolve_with_runtime_config(
|
||||||
config_file.resolve_with_runtime_config(&options.workspace, identity, &runtime_config)?;
|
&workspace_root,
|
||||||
if let Some(db) = options.db {
|
identity,
|
||||||
resolved = resolved.with_database_path(db);
|
&runtime_config,
|
||||||
}
|
)?;
|
||||||
if let Some(frontend) = options.frontend {
|
resolved.database_path = database_path.clone();
|
||||||
resolved = resolved.with_static_assets_dir(Some(frontend));
|
resolved.server.database_path = database_path.clone();
|
||||||
}
|
|
||||||
if let Some(listen) = options.listen {
|
if let Some(listen) = options.listen {
|
||||||
resolved = resolved.with_listen(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?;
|
let listener = TcpListener::bind(resolved.listen).await?;
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"yoi-workspace-server: serving workspace `{}` on http://{}",
|
"yoi-workspace-server: serving workspace `{}` from server DB `{}` on http://{}",
|
||||||
options.workspace.display(),
|
workspace.workspace_id,
|
||||||
|
database_path.display(),
|
||||||
listener.local_addr()?
|
listener.local_addr()?
|
||||||
);
|
);
|
||||||
serve(resolved.server, store, listener).await?;
|
serve(resolved.server, store, listener).await?;
|
||||||
Ok(())
|
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> {
|
fn parse_config_command(args: &[String]) -> Result<Command, CliError> {
|
||||||
let Some((subcommand, rest)) = args.split_first() else {
|
let Some((subcommand, rest)) = args.split_first() else {
|
||||||
print_config_help();
|
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> {
|
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 listen = None;
|
||||||
|
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
while index < args.len() {
|
while index < args.len() {
|
||||||
let arg = &args[index];
|
let arg = &args[index];
|
||||||
match arg.as_str() {
|
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" => {
|
"--listen" => {
|
||||||
index += 1;
|
index += 1;
|
||||||
let value = args
|
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()))?;
|
.ok_or_else(|| CliError("--listen requires a value".to_string()))?;
|
||||||
listen = Some(parse_listen(value)?);
|
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=") => {
|
_ if arg.starts_with("--listen=") => {
|
||||||
listen = Some(parse_listen(value_after_equals(arg, "--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!(
|
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;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace = workspace
|
Ok(ServeOptions { listen })
|
||||||
.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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_after_equals<'a>(arg: &'a str, flag: &str) -> Result<&'a str, CliError> {
|
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() {
|
fn print_init_help() {
|
||||||
println!(
|
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() {
|
fn print_serve_help() {
|
||||||
println!(
|
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]
|
#[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 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();
|
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());
|
assert_eq!(options.listen.unwrap(), "127.0.0.1:0".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_serve_accepts_equals_workspace() {
|
fn parse_serve_rejects_legacy_workspace_flag() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
let args = vec![format!("--workspace={}", temp.path().display())];
|
let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
|
||||||
let options = parse_serve_options(&args).unwrap();
|
let error = parse_serve_options(&args).unwrap_err();
|
||||||
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
|
assert_eq!(error.to_string(), "unknown serve option `--workspace`");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
run_init(InitOptions {
|
let database_path = temp.path().join("data").join("server").join("server.db");
|
||||||
workspace: temp.path().canonicalize().unwrap(),
|
run_init_with_database_path(
|
||||||
})
|
InitOptions {
|
||||||
|
workspace: temp.path().canonicalize().unwrap(),
|
||||||
|
},
|
||||||
|
database_path.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists());
|
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/workspace.db").exists());
|
||||||
assert!(!temp.path().join(".yoi/embedded-runtime").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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,7 @@ impl ServerConfig {
|
||||||
let workspace_root = workspace_root.into();
|
let workspace_root = workspace_root.into();
|
||||||
let workspace_id = identity.workspace_id;
|
let workspace_id = identity.workspace_id;
|
||||||
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
|
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
|
||||||
let database_path =
|
let database_path = Self::default_server_database_path();
|
||||||
Self::default_workspace_backend_data_root(&workspace_id).join("workspace.db");
|
|
||||||
Self {
|
Self {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
workspace_display_name: identity.display_name,
|
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(
|
pub fn workspace_backend_data_root_for_data_dir(
|
||||||
data_dir: impl Into<PathBuf>,
|
data_dir: impl Into<PathBuf>,
|
||||||
workspace_id: impl AsRef<str>,
|
workspace_id: impl AsRef<str>,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
data_dir
|
Self::server_data_root_for_data_dir(data_dir)
|
||||||
.into()
|
.join("workspaces")
|
||||||
.join("workspace-server")
|
|
||||||
.join(workspace_id.as_ref())
|
.join(workspace_id.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,7 +198,8 @@ impl ServerConfig {
|
||||||
}
|
}
|
||||||
None => std::env::temp_dir()
|
None => std::env::temp_dir()
|
||||||
.join("yoi")
|
.join("yoi")
|
||||||
.join("workspace-server")
|
.join("server")
|
||||||
|
.join("workspaces")
|
||||||
.join(workspace_id.as_ref()),
|
.join(workspace_id.as_ref()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6846,6 +6864,7 @@ mod tests {
|
||||||
let store_root = workspace_root.join(".test-embedded-runtime-store");
|
let store_root = workspace_root.join(".test-embedded-runtime-store");
|
||||||
let mut config = ServerConfig::local_dev(workspace_root.clone(), test_identity())
|
let mut config = ServerConfig::local_dev(workspace_root.clone(), test_identity())
|
||||||
.with_embedded_runtime_store_root(store_root);
|
.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.runtime_config_path = Some(workspace_root.join(".test-config/runtimes.toml"));
|
||||||
config.repositories = vec![ConfiguredRepository {
|
config.repositories = vec![ConfiguredRepository {
|
||||||
id: TEST_REPOSITORY_ID.to_string(),
|
id: TEST_REPOSITORY_ID.to_string(),
|
||||||
|
|
@ -7952,7 +7971,6 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serves_bounded_read_apis_and_static_spa_separately() {
|
async fn serves_bounded_read_apis_and_static_spa_separately() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
write_ticket(dir.path(), "00000000001J2", "API Ticket", "ready");
|
|
||||||
write_objective(dir.path(), "00000000001J3", "API Objective", "active");
|
write_objective(dir.path(), "00000000001J3", "API Objective", "active");
|
||||||
let static_dir = dir.path().join("static");
|
let static_dir = dir.path().join("static");
|
||||||
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
|
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
|
||||||
|
|
@ -7961,6 +7979,12 @@ mod tests {
|
||||||
|
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
let mut config = test_server_config(dir.path());
|
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);
|
config.static_assets_dir = Some(static_dir);
|
||||||
let api = WorkspaceApi::new_with_execution_backend(
|
let api = WorkspaceApi::new_with_execution_backend(
|
||||||
config,
|
config,
|
||||||
|
|
@ -8044,7 +8068,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let tickets = get_json(app.clone(), "/api/tickets").await;
|
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");
|
assert_eq!(tickets["items"][0]["state"], "ready");
|
||||||
|
|
||||||
let objectives = get_json(app.clone(), "/api/objectives").await;
|
let objectives = get_json(app.clone(), "/api/objectives").await;
|
||||||
|
|
@ -8098,7 +8122,7 @@ mod tests {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|column| column["state"] == "ready")
|
.find(|column| column["state"] == "ready")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(ready_column["items"][0]["id"], "00000000001J2");
|
assert_eq!(ready_column["items"][0]["title"], "API Ticket");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repository_tickets["diagnostics"][0]["code"],
|
repository_tickets["diagnostics"][0]["code"],
|
||||||
"repository_ticket_target_metadata_absent"
|
"repository_ticket_target_metadata_absent"
|
||||||
|
|
@ -8456,7 +8480,8 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
default_root,
|
default_root,
|
||||||
data_dir
|
data_dir
|
||||||
.join("workspace-server")
|
.join("server")
|
||||||
|
.join("workspaces")
|
||||||
.join(TEST_WORKSPACE_ID)
|
.join(TEST_WORKSPACE_ID)
|
||||||
.join("embedded-runtime")
|
.join("embedded-runtime")
|
||||||
);
|
);
|
||||||
|
|
@ -8941,25 +8966,18 @@ mod tests {
|
||||||
serde_json::from_slice(&bytes).unwrap()
|
serde_json::from_slice(&bytes).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_ticket(root: &Path, id: &str, title: &str, state: &str) {
|
fn write_ticket(
|
||||||
let ticket_dir = root.join(".yoi/tickets").join(id);
|
database_path: &Path,
|
||||||
std::fs::create_dir_all(&ticket_dir).unwrap();
|
workspace_id: &str,
|
||||||
std::fs::write(
|
title: &str,
|
||||||
ticket_dir.join("item.md"),
|
state: ticket::TicketWorkflowState,
|
||||||
format!(
|
) {
|
||||||
r#"---
|
use ticket::TicketBackend as _;
|
||||||
title: "{title}"
|
|
||||||
state: "{state}"
|
|
||||||
created_at: "2026-01-01T00:00:00Z"
|
|
||||||
updated_at: "2026-01-02T00:00:00Z"
|
|
||||||
---
|
|
||||||
|
|
||||||
Ticket body.
|
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();
|
||||||
.unwrap();
|
|
||||||
std::fs::write(ticket_dir.join("thread.md"), "").unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
|
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
async fn schema_version(&self) -> Result<i64>;
|
async fn schema_version(&self) -> Result<i64>;
|
||||||
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
|
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
|
||||||
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>>;
|
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 upsert_repository(&self, record: &RepositoryRecord) -> Result<()>;
|
||||||
fn list_repositories(&self, workspace_id: &str) -> Result<Vec<RepositoryRecord>>;
|
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<()> {
|
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
|
||||||
|
|
@ -1053,12 +1053,6 @@ fn parse_workspace_args(args: &[String]) -> Result<Mode, ParseError> {
|
||||||
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||||
return Ok(Mode::WorkspaceHelp);
|
return Ok(Mode::WorkspaceHelp);
|
||||||
}
|
}
|
||||||
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 {
|
Ok(Mode::WorkspaceServer {
|
||||||
subcommand: "serve".to_string(),
|
subcommand: "serve".to_string(),
|
||||||
args: rest.to_vec(),
|
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 {
|
fn run_workspace_server(subcommand: &str, args: Vec<String>) -> ExitCode {
|
||||||
let command = match resolve_workspace_server_command() {
|
let command = match resolve_workspace_server_command() {
|
||||||
Ok(command) => command,
|
Ok(command) => command,
|
||||||
|
|
@ -1453,13 +1442,13 @@ fn print_help() {
|
||||||
println!(
|
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\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 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() {
|
fn print_workspace_help() {
|
||||||
println!(
|
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]
|
#[test]
|
||||||
fn parse_workspace_serve_passthrough() {
|
fn parse_workspace_serve_passthrough() {
|
||||||
match parse_args_from([
|
match parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap() {
|
||||||
"workspace",
|
|
||||||
"serve",
|
|
||||||
"--workspace",
|
|
||||||
"/tmp/ws",
|
|
||||||
"--listen",
|
|
||||||
"127.0.0.1:0",
|
|
||||||
])
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Mode::WorkspaceServer { subcommand, args } => {
|
Mode::WorkspaceServer { subcommand, args } => {
|
||||||
assert_eq!(subcommand, "serve");
|
assert_eq!(subcommand, "serve");
|
||||||
assert_eq!(
|
assert_eq!(args, vec!["--listen", "127.0.0.1:0"]);
|
||||||
args,
|
|
||||||
vec!["--workspace", "/tmp/ws", "--listen", "127.0.0.1:0"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
other => panic!("unexpected mode: {other:?}"),
|
other => panic!("unexpected mode: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_workspace_serve_requires_explicit_workspace() {
|
fn parse_workspace_serve_leaves_legacy_flags_to_server() {
|
||||||
let err = parse_args_from(["workspace", "serve", "--listen", "127.0.0.1:0"]).unwrap_err();
|
match parse_args_from(["workspace", "serve", "--workspace", "/tmp/ws"]).unwrap() {
|
||||||
assert_eq!(
|
Mode::WorkspaceServer { subcommand, args } => {
|
||||||
err.to_string(),
|
assert_eq!(subcommand, "serve");
|
||||||
"yoi workspace serve requires --workspace <PATH>; cwd is no longer forwarded as an implicit workspace"
|
assert_eq!(args, vec!["--workspace", "/tmp/ws"]);
|
||||||
);
|
}
|
||||||
|
other => panic!("unexpected mode: {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ fn workspace_settings_uuid_v7(workspace: &Path) -> String {
|
||||||
fn backend_for_workspace(workspace: &Path) -> Result<Box<dyn TicketBackend>, TicketCliError> {
|
fn backend_for_workspace(workspace: &Path) -> Result<Box<dyn TicketBackend>, TicketCliError> {
|
||||||
let config = TicketConfig::load_workspace(workspace)?;
|
let config = TicketConfig::load_workspace(workspace)?;
|
||||||
let workspace_id = workspace_id_for_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(
|
Ok(Box::new(
|
||||||
SqliteTicketBackend::new(db_path, workspace_id)
|
SqliteTicketBackend::new(db_path, workspace_id)
|
||||||
.with_record_language(config.ticket_record_language()),
|
.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())
|
let local = LocalTicketBackend::new(config.backend_root().to_path_buf())
|
||||||
.with_record_language(config.ticket_record_language());
|
.with_record_language(config.ticket_record_language());
|
||||||
let workspace_id = workspace_id_for_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)?;
|
||||||
let sqlite = SqliteTicketBackend::new(db_path.clone(), workspace_id)
|
let sqlite = SqliteTicketBackend::new(db_path.clone(), workspace_id)
|
||||||
.with_record_language(config.ticket_record_language());
|
.with_record_language(config.ticket_record_language());
|
||||||
sqlite.import_from_local_backend(&local)?;
|
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> {
|
fn workspace_id_for_workspace(workspace: &Path) -> Result<String, TicketCliError> {
|
||||||
let settings_path = workspace.join(WORKSPACE_SETTINGS_RELATIVE_PATH);
|
let settings_path = workspace.join(WORKSPACE_SETTINGS_RELATIVE_PATH);
|
||||||
let raw = fs::read_to_string(&settings_path).map_err(|error| {
|
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(
|
fn create(
|
||||||
backend: &dyn TicketBackend,
|
backend: &dyn TicketBackend,
|
||||||
options: CreateOptions,
|
options: CreateOptions,
|
||||||
|
|
|
||||||
|
|
@ -262,14 +262,13 @@ in
|
||||||
Entrypoint = [ "/bin/yoi-workspace-server" ];
|
Entrypoint = [ "/bin/yoi-workspace-server" ];
|
||||||
Cmd = [
|
Cmd = [
|
||||||
"serve"
|
"serve"
|
||||||
"--workspace"
|
|
||||||
"/workspace"
|
|
||||||
"--db"
|
|
||||||
"/server-data/workspace.db"
|
|
||||||
"--listen"
|
"--listen"
|
||||||
"0.0.0.0:8787"
|
"0.0.0.0:8787"
|
||||||
];
|
];
|
||||||
Env = [ "PATH=/bin" ] ++ commonEnv;
|
Env = [
|
||||||
|
"PATH=/bin"
|
||||||
|
"YOI_DATA_DIR=/server-data"
|
||||||
|
] ++ commonEnv;
|
||||||
ExposedPorts = {
|
ExposedPorts = {
|
||||||
"8787/tcp" = { };
|
"8787/tcp" = { };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,15 @@ frontend_url = "http://127.0.0.1:5173"
|
||||||
# static_assets_dir = "web/workspace/dist"
|
# static_assets_dir = "web/workspace/dist"
|
||||||
|
|
||||||
[data]
|
[data]
|
||||||
# Backend data root override. Leave commented to use the user-data fallback:
|
# Workspace-scoped runtime/data root override. Leave commented to use the user-data fallback:
|
||||||
# <data_dir>/workspace-server/<workspace_id>/
|
# <data_dir>/server/workspaces/<workspace_id>/
|
||||||
# Relative paths are resolved from the workspace root.
|
# Relative paths are resolved from the workspace root.
|
||||||
# root = ".yoi/workspace-backend.data"
|
# root = ".yoi/workspace-backend.data"
|
||||||
|
|
||||||
# Explicit control-plane SQLite DB path override.
|
# Explicit control-plane SQLite DB path override. Normal `serve` uses the Yoi
|
||||||
# If omitted, this falls back to `<data.root>/workspace.db`.
|
# server DB at `<data_dir>/server/server.db`; keep this commented unless a
|
||||||
# workspace_database_path = ".yoi/workspace-backend.data/workspace.db"
|
# local test needs a custom DB path.
|
||||||
|
# workspace_database_path = ".yoi/workspace-backend.data/server.db"
|
||||||
|
|
||||||
# Explicit embedded Runtime fs-store root override.
|
# Explicit embedded Runtime fs-store root override.
|
||||||
# If omitted, this falls back to `<data.root>/embedded-runtime`.
|
# If omitted, this falls back to `<data.root>/embedded-runtime`.
|
||||||
|
|
|
||||||
23
scripts/dev-workspace.sh
Executable file → Normal file
23
scripts/dev-workspace.sh
Executable file → Normal file
|
|
@ -21,7 +21,22 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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"
|
PID_DIR="$RUNTIME_DIR/pids"
|
||||||
LOG_DIR="$RUNTIME_DIR/logs"
|
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:
|
Manage the local Yoi development stack for this checkout:
|
||||||
runtime target/debug/worker-runtime-rest-server --bind $RUNTIME_BIND
|
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)
|
frontend deno run -A npm:vite@7.2.7 dev --host $FRONTEND_HOST --port $FRONTEND_PORT (cwd: web/workspace)
|
||||||
|
|
||||||
Actions:
|
Actions:
|
||||||
|
|
@ -64,7 +79,7 @@ Environment overrides:
|
||||||
YOI_DEV_RUNTIME_ENABLED=1 set to 0 to skip the standalone runtime process
|
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_HOST=0.0.0.0
|
||||||
YOI_DEV_FRONTEND_PORT=5173
|
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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,7 +345,7 @@ start_backend() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
start_service backend "$ROOT_DIR" "$port" \
|
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() {
|
start_frontend() {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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."
|
|
||||||
|
|
@ -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."
|
|
||||||
|
|
@ -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:
|
If you want to run the backend from the repository root instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -p yoi-workspace-server -- serve \
|
cargo run -p yoi-workspace-server -- serve --listen 127.0.0.1:8787
|
||||||
--workspace . \
|
|
||||||
--db .yoi/workspace.db \
|
|
||||||
--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:
|
Build the SPA:
|
||||||
|
|
||||||
|
|
@ -41,25 +40,7 @@ Build the SPA:
|
||||||
deno task build
|
deno task build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then serve the static build and API from the Rust backend:
|
Static asset packaging is a deployment concern. Local development normally uses the Vite dev server proxy plus the Rust backend command above.
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"install": "deno install",
|
"install": "deno install",
|
||||||
"dev": "deno run -A npm:vite@7.2.7 dev",
|
"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",
|
"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",
|
"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",
|
"build": "deno run -A npm:vite@7.2.7 build",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user