diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6e0da56..ae2746f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -559,6 +559,53 @@ fn create_project_full_in( Ok(()) } +/// Verlegt den Projektordner: Registry-Eintrag auf den neuen Pfad; Verweise +/// auf den alten Root in der settings.json (autoMemoryDirectory, +/// Edit-Permission) ziehen mit. Verschoben wird nichts — der neue Ordner +/// muss existieren. +fn set_project_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> { + check_name(name)?; + let new_dir = expand_home(paths, dir); + if !new_dir.is_dir() { + return Err(format!("Ordner nicht gefunden: {}", new_dir.display())); + } + let mut reg = load_registry(paths)?; + let old_dir = reg + .get(name) + .cloned() + .ok_or_else(|| format!("Projekt nicht registriert: {name}"))?; + if old_dir == new_dir { + return Ok(()); + } + reg.insert(name.to_string(), new_dir.clone()); + save_registry(paths, ®)?; + + let sp = settings_path(&new_dir); + if !sp.is_file() { + return Ok(()); // importierte Projekte ohne settings.json + } + let old_c = contract_home(paths, &old_dir); + let new_c = contract_home(paths, &new_dir); + let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?; + let mut v: serde_json::Value = + serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?; + if let Some(mem) = v["autoMemoryDirectory"].as_str() { + if let Some(rest) = mem.strip_prefix(&old_c) { + v["autoMemoryDirectory"] = serde_json::json!(format!("{new_c}{rest}")); + } + } + if let Some(allow) = v["permissions"]["allow"].as_array_mut() { + let old_edit = format!("Edit({old_c}/**)"); + for e in allow.iter_mut() { + if e.as_str() == Some(&old_edit) { + *e = serde_json::json!(format!("Edit({new_c}/**)")); + } + } + } + let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?; + fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display())) +} + /// Nimmt einen bestehenden Ordner als Projekt auf; der Name ist der Ordnername. fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> { let dir = expand_home(paths, path); @@ -1544,6 +1591,15 @@ fn remove_project(name: String) -> Result<(), String> { unregister_project(&Paths::real(), &name) } +/// Projektordner neu zuordnen; bei laufender Session gesperrt. +#[tauri::command] +fn set_project_dir(project: String, dir: String) -> Result<(), String> { + if is_running(&project) { + return Err(format!("{project} läuft noch — erst beenden")); + } + set_project_dir_in(&Paths::real(), &project, &dir) +} + #[tauri::command] fn add_work_dir(project: String, dir: String) -> Result<(), String> { add_work_dir_in(&Paths::real(), &project, &dir) @@ -1906,6 +1962,7 @@ fn main_builder() -> tauri::Builder { remove_project, delete_project, project_work_dirs, + set_project_dir, add_work_dir, remove_work_dir, list_pools, @@ -2753,6 +2810,24 @@ mod tests { assert!(p.home.join("projects/extra").is_dir()); } + #[test] + fn projektordner_verlegen() { + let p = tmp_paths(); + create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false) + .unwrap(); + // Nutzer hat den Ordner selbst verschoben; die App ordnet nur neu zu. + let new_dir = p.home.join("elsewhere").join("proj"); + fs::create_dir_all(p.home.join("elsewhere")).unwrap(); + fs::rename(p.projects_dir().join("proj"), &new_dir).unwrap(); + set_project_dir_in(&p, "proj", &new_dir.to_string_lossy()).unwrap(); + + assert_eq!(project_dir(&p, "proj").unwrap(), new_dir); + let raw = fs::read_to_string(settings_path(&new_dir)).unwrap(); + assert!(raw.contains("~/elsewhere/proj/memory")); + assert!(raw.contains("Edit(~/elsewhere/proj/**)")); + assert!(!raw.contains("claude-projects/proj")); + } + #[test] fn projekt_loeschen_unbekannt_scheitert() { let p = tmp_paths(); diff --git a/src/components/ProjectList.vue b/src/components/ProjectList.vue index 57a3934..be0a594 100644 --- a/src/components/ProjectList.vue +++ b/src/components/ProjectList.vue @@ -182,6 +182,22 @@ async function openSettings(p: Project) { } } +// Projektordner neu zuordnen (Root, in dem die Session startet); verschoben +// wird nichts — der gewählte Ordner ist der neue Ort des Projekts. +async function changeProjectDir() { + const s = settings.value!; + const dir = await open({ directory: true, multiple: false }); + if (typeof dir !== "string") return; + try { + await invoke("set_project_dir", { project: s.name, dir }); + await refresh(); + const p = projects.value.find((x) => x.name === s.name); + if (p) s.path = p.path; + } catch (e) { + error.value = String(e); + } +} + // Arbeitsordner schreiben direkt in die Projekt-settings.json // (additionalDirectories + Edit-Permission) — kein Speichern-Schritt. async function addWorkDir() { @@ -624,6 +640,7 @@ onUnmounted(() => window.clearInterval(timer)); {{ settings.path }} + {{ wd }} diff --git a/src/i18n.ts b/src/i18n.ts index 05c4253..0273141 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -54,6 +54,7 @@ const de = { addFolder: "Projekt importieren …", addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen", addWorkDir: "Ordner hinzufügen …", + changeDir: "Ändern …", wizardTitle: "Neues Projekt", name: "Name", projectDir: "Projektordner", @@ -177,6 +178,7 @@ const en: typeof de = { addFolder: "Import project …", addFolderTitle: "Add an existing folder as a project", addWorkDir: "Add folder …", + changeDir: "Change …", wizardTitle: "New project", name: "Name", projectDir: "Project folder",