Projekt-Identität und -Config neu: .ai-control/config.json mit UUID und Name

- Projekt-Config zieht von ai-control.json nach .ai-control/config.json um,
  das Icon liegt daneben und synct mit dem Projekt.
- Jedes Projekt trägt eine UUID (id) und den Anzeigenamen (name) in der
  Config; die UUID ist Registry-Schlüssel und project-Parameter aller
  Commands, der Name reine Darstellung.
- Pool-Zuordnung ist maschinenlokal und liegt in der projects.json-Registry,
  nicht mehr in der syncbaren Projekt-Config.
- open_terminal prüft die ID im Ordner gegen die Registry (verschobene oder
  ersetzte Ordner scheitern laut).
- Import übernimmt eine mitgebrachte ID, legt Arbeitsordner und Archiv-Home
  an und zieht die Archiv-Permission nach.
- Migration beim App-Start: Alt-Datei umziehen, Pool in die Registry,
  Icons aus ~/.config/ai-control/icons ins Projekt, Registry auf UUIDs.
- ProjectConfig reicht unbekannte Keys per serde(flatten) durch den
  Round-Trip (Regressionstest config_roundtrip_erhaelt_fremde_keys).
This commit is contained in:
marcus hinz
2026-07-20 10:55:21 +02:00
parent 6d44f77671
commit b9d006184a
20 changed files with 830 additions and 346 deletions
+26 -16
View File
@@ -14,9 +14,9 @@ use crate::domain::pool::{
};
use crate::domain::project::{
add_project_in, add_work_dir_in, assign_pool_in, create_project_full_in, delete_project_in,
is_running, kill_terminals, list_projects_in, project_work_dirs_in, read_project_config_in,
remove_work_dir_in, resolve_icon_path, running_projects_using_pool, set_project_dir_in,
set_terminal_config_in, unassign_pool_in, Project, TerminalConfig,
display_name_in, is_running, kill_terminals, list_projects_in, project_work_dirs_in,
read_project_config_in, remove_work_dir_in, resolve_icon_path, running_projects_using_pool,
set_project_dir_in, set_terminal_config_in, unassign_pool_in, Project, TerminalConfig,
};
use crate::domain::registry::unregister_project;
use crate::domain::settings;
@@ -41,7 +41,7 @@ pub(crate) fn create_project_full(
create_work_dir: bool,
terminal: TerminalConfig,
todo: bool,
) -> Result<(), String> {
) -> Result<String, String> {
create_project_full_in(
&Paths::real(),
&name,
@@ -62,18 +62,20 @@ pub(crate) fn add_project(path: String) -> Result<(), String> {
/// Projekt aus der Registry nehmen; der Ordner bleibt unangetastet.
#[tauri::command]
pub(crate) fn remove_project(name: String) -> Result<(), String> {
if is_running(&name) {
pub(crate) fn remove_project(project: String) -> Result<(), String> {
if is_running(&project) {
let name = display_name_in(&Paths::real(), &project)?;
return Err(format!("{name} läuft noch — erst beenden"));
}
unregister_project(&Paths::real(), &name)
unregister_project(&Paths::real(), &project)
}
/// Projektordner neu zuordnen; bei laufender Session gesperrt.
#[tauri::command]
pub(crate) fn set_project_dir(project: String, dir: String) -> Result<(), String> {
if is_running(&project) {
return Err(format!("{project} läuft noch — erst beenden"));
let name = display_name_in(&Paths::real(), &project)?;
return Err(format!("{name} läuft noch — erst beenden"));
}
set_project_dir_in(&Paths::real(), &project, &dir)
}
@@ -89,11 +91,12 @@ pub(crate) fn remove_work_dir(project: String, dir: String) -> Result<(), String
}
#[tauri::command]
pub(crate) fn delete_project(name: String, delete_work_dirs: bool) -> Result<(), String> {
if is_running(&name) {
pub(crate) fn delete_project(project: String, delete_work_dirs: bool) -> Result<(), String> {
if is_running(&project) {
let name = display_name_in(&Paths::real(), &project)?;
return Err(format!("{name} läuft noch — erst beenden"));
}
delete_project_in(&Paths::real(), &name, delete_work_dirs)
delete_project_in(&Paths::real(), &project, delete_work_dirs)
}
#[tauri::command]
@@ -133,7 +136,7 @@ pub(crate) fn project_icon(project: String) -> Result<Option<String>, String> {
let Some(icon) = read_project_config_in(&paths, &project)?.terminal.icon else {
return Ok(None);
};
let path = resolve_icon_path(&paths, &icon);
let path = resolve_icon_path(&paths, &project, &icon)?;
let mime = match path.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("svg") => "image/svg+xml",
_ => "image/png",
@@ -168,6 +171,11 @@ pub(crate) fn rename_pool(pool: String, name: String) -> Result<(), String> {
rename_pool_in(&Paths::real(), &pool, &name)
}
/// Anzeige-Namen der Projekte (für Fehlermeldungen mit Projektliste).
fn display_names(paths: &Paths, ids: &[String]) -> Result<Vec<String>, String> {
ids.iter().map(|id| display_name_in(paths, id)).collect()
}
#[tauri::command]
pub(crate) fn delete_pool(pool: String) -> Result<(), String> {
let paths = Paths::real();
@@ -175,7 +183,7 @@ pub(crate) fn delete_pool(pool: String) -> Result<(), String> {
if !running.is_empty() {
return Err(format!(
"Pool wird noch benutzt — läuft: {}",
running.join(", ")
display_names(&paths, &running)?.join(", ")
));
}
delete_pool_in(&paths, &KeychainStore, &pool)
@@ -219,7 +227,7 @@ pub(crate) fn oauth_login(pool: String) -> Result<(), String> {
if !running.is_empty() {
return Err(format!(
"Neuanmeldung nur bei ungenutztem Pool möglich — läuft: {}",
running.join(", ")
display_names(&paths, &running)?.join(", ")
));
}
@@ -246,7 +254,8 @@ pub(crate) fn link_pool_runtime(pool: String) -> Result<(), String> {
#[tauri::command]
pub(crate) fn stop_project(project: String) -> Result<(), String> {
if crate::platform::terminal_pids(&project).is_empty() {
return Err(format!("{project} läuft nicht"));
let name = display_name_in(&Paths::real(), &project)?;
return Err(format!("{name} läuft nicht"));
}
kill_terminals(&project)
}
@@ -259,7 +268,8 @@ pub(crate) fn restart_project(app: tauri::AppHandle, project: String) -> Result<
let deadline = Instant::now() + Duration::from_secs(30);
while is_running(&project) {
if Instant::now() > deadline {
return Err(format!("{project} hat sich nach 30 s nicht beendet"));
let name = display_name_in(&Paths::real(), &project)?;
return Err(format!("{name} hat sich nach 30 s nicht beendet"));
}
std::thread::sleep(Duration::from_millis(250));
}