From 09488ae5ea22a1c53a87b6697f3da038074d9e8d Mon Sep 17 00:00:00 2001 From: "marcus.hinz" Date: Sat, 4 Jul 2026 20:28:12 +0200 Subject: [PATCH] Pool-Runtime (projects/todos/history) als Symlinks ins synced Repo, auch bei Pool-Anlage --- src-tauri/src/lib.rs | 73 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ea88558..63551b2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -642,6 +642,44 @@ fn init_pool_config( Ok(()) } +/// Runtime, die pro Pool ins synced Repo gelinkt wird: Transkripte, Todos, +/// Prompt-Historie. (name, ist_ordner) +const SYNCED_RUNTIME: [(&str, bool); 3] = + [("projects", true), ("todos", true), ("history.jsonl", false)]; + +/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo. +fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf { + paths.projects_dir().join("pool").join(pool) +} + +/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das +/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git. +/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst). +fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> { + check_name(pool)?; + let src = paths.pool_dir(pool); + let data = pool_data_dir(paths, pool); + for (name, is_dir) in SYNCED_RUNTIME { + let target = data.join(name); + if is_dir { + fs::create_dir_all(&target).map_err(|e| e.to_string())?; + } else { + fs::create_dir_all(&data).map_err(|e| e.to_string())?; + if !target.exists() { + fs::write(&target, "").map_err(|e| e.to_string())?; + } + } + let link = src.join(name); + if link.is_dir() && !link.is_symlink() { + fs::remove_dir_all(&link).map_err(|e| e.to_string())?; + } else if link.is_symlink() || link.exists() { + fs::remove_file(&link).map_err(|e| e.to_string())?; + } + std::os::unix::fs::symlink(&target, &link).map_err(|e| e.to_string())?; + } + Ok(()) +} + /// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit /// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> { @@ -656,7 +694,8 @@ fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), Str &dir, serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }), )?; - write_pool_json(&dir, name, "apikey") + write_pool_json(&dir, name, "apikey")?; + link_pool_runtime_in(paths, name) } /// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md) @@ -665,7 +704,8 @@ fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), Str fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> { let dir = check_new_pool(paths, name)?; init_pool_config(&dir, serde_json::json!({}))?; - write_pool_json(&dir, name, "oauth") + write_pool_json(&dir, name, "oauth")?; + link_pool_runtime_in(paths, name) } /// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte @@ -1096,6 +1136,17 @@ fn set_sync_setting(enabled: bool) -> Result<(), String> { set_sync_on_session_end_in(&Paths::real(), enabled) } +/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool — +/// sonst würde das Transkript der laufenden Session ersetzt. +#[tauri::command] +fn link_pool_runtime(pool: String) -> Result<(), String> { + let paths = Paths::real(); + if !running_projects_using_pool(&paths, &pool)?.is_empty() { + return Err(format!("{pool} wird benutzt — erst Session beenden")); + } + link_pool_runtime_in(&paths, &pool) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST). @@ -1213,6 +1264,7 @@ fn main_builder() -> tauri::Builder { restart_project, sync_setting, set_sync_setting, + link_pool_runtime, oauth_login, keychain_status, set_apikey, @@ -1647,6 +1699,23 @@ mod tests { assert!(!p.project_config("proj").exists()); } + #[test] + fn pool_anlegen_verlinkt_synced_runtime() { + let p = tmp_paths(); + create_oauth_pool_in(&p, "privat").unwrap(); + let pooldir = p.pool_dir("privat"); + for (name, is_dir) in SYNCED_RUNTIME { + let link = pooldir.join(name); + assert!(link.is_symlink(), "{name} sollte Symlink sein"); + assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name)); + let target = pool_data_dir(&p, "privat").join(name); + assert_eq!(target.is_dir(), is_dir); + assert!(target.exists()); + } + // Zieldaten liegen im claude-projects-Repo unter pool// + assert!(p.projects_dir().join("pool").join("privat").is_dir()); + } + #[test] fn sync_optin_default_aus_und_umschaltbar() { let p = tmp_paths();