Projektordner im ⚙︎-Dialog änderbar: Registry-Eintrag neu zuordnen, autoMemoryDirectory + Edit-Permission ziehen auf den neuen Root um; gesperrt bei laufender Session

This commit is contained in:
marcus.hinz
2026-07-05 15:37:49 +02:00
parent 2075f9aa71
commit 0ebdaf0460
3 changed files with 94 additions and 0 deletions
+75
View File
@@ -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, &reg)?;
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<tauri::Wry> {
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();
+17
View File
@@ -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));
<span class="workdirs">
<span class="icon-row">
<span class="icon-path">{{ settings.path }}</span>
<button @click="changeProjectDir">{{ $t("projects.changeDir") }}</button>
</span>
<span v-for="wd in settings.workDirs" :key="wd" class="icon-row">
<span class="icon-path">{{ wd }}</span>
+2
View File
@@ -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",