Dopplungen konsolidiert: settings.json-Permissions-Helfer, pool.json-Lesen über read_pool, gemeinsame Helfer für Pool-Anlage, Projekt-Config, Icon-Übernahme und Running-Guard
This commit is contained in:
@@ -93,15 +93,18 @@ pub(crate) fn settings_path(dir: &std::path::Path) -> PathBuf {
|
||||
dir.join(".claude").join("settings.json")
|
||||
}
|
||||
|
||||
pub(crate) fn project_config_path(paths: &Paths, project: &str) -> Result<PathBuf, String> {
|
||||
Ok(project_dir(paths, project)?.join(PROJECT_CONFIG_DIR).join(PROJECT_FILE))
|
||||
}
|
||||
|
||||
pub(crate) fn read_project_config_in(
|
||||
paths: &Paths,
|
||||
project: &str,
|
||||
) -> Result<ProjectConfig, String> {
|
||||
let cfg_path = project_config_path(paths, project)?;
|
||||
read_config_at(&project_dir(paths, project)?)
|
||||
}
|
||||
|
||||
/// config.json unter `<dir>/.ai-control` lesen; ohne Datei der Default.
|
||||
/// Für Fälle, in denen der Ordner vor der Registrierung steht (Import,
|
||||
/// Migration) — sonst über `read_project_config_in`.
|
||||
pub(crate) fn read_config_at(dir: &std::path::Path) -> Result<ProjectConfig, String> {
|
||||
let cfg_path = dir.join(PROJECT_CONFIG_DIR).join(PROJECT_FILE);
|
||||
if !cfg_path.is_file() {
|
||||
return Ok(ProjectConfig::default());
|
||||
}
|
||||
@@ -384,13 +387,7 @@ pub(crate) fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> {
|
||||
.into_owned();
|
||||
check_name(&dirname)?;
|
||||
|
||||
let cfg_path = dir.join(PROJECT_CONFIG_DIR).join(PROJECT_FILE);
|
||||
let mut cfg: ProjectConfig = if cfg_path.is_file() {
|
||||
let raw = fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?
|
||||
} else {
|
||||
ProjectConfig::default()
|
||||
};
|
||||
let mut cfg = read_config_at(&dir)?;
|
||||
if let Some(id) = cfg.id.as_deref() {
|
||||
if load_registry(paths)?.contains_key(id) {
|
||||
return Err(format!("Projekt ist schon registriert: {id}"));
|
||||
@@ -425,37 +422,16 @@ pub(crate) fn add_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<()
|
||||
}
|
||||
let dir = contract_home(paths, &wd_path);
|
||||
let sp = settings_path(&project_dir(paths, name)?);
|
||||
let mut v: serde_json::Value = if sp.is_file() {
|
||||
let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
|
||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
};
|
||||
let root = v.as_object_mut().ok_or("settings.json ist kein Objekt")?;
|
||||
let perms = root
|
||||
.entry("permissions")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.ok_or("permissions ist kein Objekt")?;
|
||||
let dirs = perms
|
||||
.entry("additionalDirectories")
|
||||
.or_insert_with(|| serde_json::json!([]))
|
||||
.as_array_mut()
|
||||
.ok_or("additionalDirectories ist kein Array")?;
|
||||
if dirs.iter().any(|d| d.as_str() == Some(&dir)) {
|
||||
return Err(format!("schon eingetragen: {dir}"));
|
||||
}
|
||||
dirs.push(serde_json::json!(dir));
|
||||
let allow = perms
|
||||
.entry("allow")
|
||||
.or_insert_with(|| serde_json::json!([]))
|
||||
.as_array_mut()
|
||||
.ok_or("allow ist kein Array")?;
|
||||
allow.insert(0, serde_json::json!(format!("Edit({dir}/**)")));
|
||||
let parent = sp.parent().ok_or("settings.json ohne Elternordner")?;
|
||||
fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
|
||||
let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
|
||||
crate::domain::write_atomic(&sp, &(raw + "\n"))
|
||||
crate::domain::update_settings_permissions(&sp, true, |perms| {
|
||||
let dirs = crate::domain::perm_array(perms, "additionalDirectories")?;
|
||||
if dirs.iter().any(|d| d.as_str() == Some(&dir)) {
|
||||
return Err(format!("schon eingetragen: {dir}"));
|
||||
}
|
||||
dirs.push(serde_json::json!(dir));
|
||||
let allow = crate::domain::perm_array(perms, "allow")?;
|
||||
allow.insert(0, serde_json::json!(format!("Edit({dir}/**)")));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Nimmt einen Arbeitsordner wieder raus: additionalDirectories-Eintrag und
|
||||
@@ -463,25 +439,18 @@ pub(crate) fn add_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<()
|
||||
pub(crate) fn remove_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
|
||||
check_name(name)?;
|
||||
let sp = settings_path(&project_dir(paths, name)?);
|
||||
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()))?;
|
||||
let perms = v["permissions"]
|
||||
.as_object_mut()
|
||||
.ok_or("permissions ist kein Objekt")?;
|
||||
let dirs = perms["additionalDirectories"]
|
||||
.as_array_mut()
|
||||
.ok_or("additionalDirectories ist kein Array")?;
|
||||
let before = dirs.len();
|
||||
dirs.retain(|d| d.as_str() != Some(dir));
|
||||
if dirs.len() == before {
|
||||
return Err(format!("nicht eingetragen: {dir}"));
|
||||
}
|
||||
if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
|
||||
allow.retain(|p| p.as_str() != Some(&format!("Edit({dir}/**)")));
|
||||
}
|
||||
let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
|
||||
crate::domain::write_atomic(&sp, &(raw + "\n"))
|
||||
crate::domain::update_settings_permissions(&sp, false, |perms| {
|
||||
let dirs = crate::domain::perm_array(perms, "additionalDirectories")?;
|
||||
let before = dirs.len();
|
||||
dirs.retain(|d| d.as_str() != Some(dir));
|
||||
if dirs.len() == before {
|
||||
return Err(format!("nicht eingetragen: {dir}"));
|
||||
}
|
||||
if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
|
||||
allow.retain(|p| p.as_str() != Some(&format!("Edit({dir}/**)")));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
|
||||
@@ -653,20 +622,8 @@ pub(crate) fn set_terminal_config_in(
|
||||
// Dateiname speichern — das Icon synct damit mit dem Projekt.
|
||||
if let Some(icon) = terminal.icon.as_deref() {
|
||||
if icon.starts_with('/') {
|
||||
let src = PathBuf::from(icon);
|
||||
let ext = src
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("png")
|
||||
.to_lowercase();
|
||||
let name = format!("icon.{ext}");
|
||||
let cfg_dir = project_dir(paths, project)?.join(PROJECT_CONFIG_DIR);
|
||||
fs::create_dir_all(&cfg_dir).map_err(|e| format!("{}: {e}", cfg_dir.display()))?;
|
||||
let dest = cfg_dir.join(&name);
|
||||
if src != dest {
|
||||
fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
|
||||
}
|
||||
terminal.icon = Some(name);
|
||||
terminal.icon = Some(adopt_icon(std::path::Path::new(icon), &cfg_dir)?);
|
||||
}
|
||||
}
|
||||
// Die Terminal-Config kommt aus der Oberfläche und kennt nur die drei Felder;
|
||||
@@ -678,6 +635,23 @@ pub(crate) fn set_terminal_config_in(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Icon-Datei als `icon.<ext>` (kleingeschrieben, Default png) in den
|
||||
/// Projekt-Config-Ordner kopieren; liefert den gespeicherten Dateinamen.
|
||||
fn adopt_icon(src: &std::path::Path, cfg_dir: &std::path::Path) -> Result<String, String> {
|
||||
let ext = src
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("png")
|
||||
.to_lowercase();
|
||||
let name = format!("icon.{ext}");
|
||||
fs::create_dir_all(cfg_dir).map_err(|e| format!("{}: {e}", cfg_dir.display()))?;
|
||||
let dest = cfg_dir.join(&name);
|
||||
if src != dest {
|
||||
fs::copy(src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
|
||||
}
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
|
||||
/// Projekt-Config-Ordner (.ai-control) des Projekts.
|
||||
pub(crate) fn resolve_icon_path(
|
||||
@@ -771,13 +745,7 @@ pub(crate) fn migrate_layout_in(paths: &Paths) -> Result<(), String> {
|
||||
// war der Anzeigename.
|
||||
let cfg_dir = entry.dir.join(PROJECT_CONFIG_DIR);
|
||||
let cfg_path = cfg_dir.join(PROJECT_FILE);
|
||||
let mut cfg: ProjectConfig = if cfg_path.is_file() {
|
||||
let raw =
|
||||
fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?
|
||||
} else {
|
||||
ProjectConfig::default()
|
||||
};
|
||||
let mut cfg = read_config_at(&entry.dir)?;
|
||||
let mut cfg_dirty = cfg.id.is_none() || cfg.name.is_none();
|
||||
let id = cfg.id.get_or_insert_with(|| uuid::Uuid::new_v4().to_string()).clone();
|
||||
cfg.name.get_or_insert_with(|| key.clone());
|
||||
@@ -789,16 +757,8 @@ pub(crate) fn migrate_layout_in(paths: &Paths) -> Result<(), String> {
|
||||
if !icon.starts_with('/') {
|
||||
let old = paths.icons_dir().join(&icon);
|
||||
if old.is_file() {
|
||||
let ext = std::path::Path::new(&icon)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("png")
|
||||
.to_lowercase();
|
||||
let name = format!("icon.{ext}");
|
||||
fs::create_dir_all(&cfg_dir).map_err(|e| format!("{}: {e}", cfg_dir.display()))?;
|
||||
fs::copy(&old, cfg_dir.join(&name)).map_err(|e| format!("{}: {e}", old.display()))?;
|
||||
cfg.terminal.icon = Some(adopt_icon(&old, &cfg_dir)?);
|
||||
fs::remove_file(&old).map_err(|e| format!("{}: {e}", old.display()))?;
|
||||
cfg.terminal.icon = Some(name);
|
||||
cfg_dirty = true;
|
||||
}
|
||||
}
|
||||
@@ -826,6 +786,10 @@ mod tests {
|
||||
use crate::domain::testutil::{create_project, make_apikey_pool, make_oauth_pool, map_store, tmp_paths};
|
||||
use crate::domain::todo::TODO_FILE;
|
||||
|
||||
fn project_config_path(paths: &Paths, project: &str) -> Result<PathBuf, String> {
|
||||
Ok(project_dir(paths, project)?.join(PROJECT_CONFIG_DIR).join(PROJECT_FILE))
|
||||
}
|
||||
|
||||
/// Ein Pool-Eintrag mit Pfad-Bestandteilen — etwa per Migration aus der
|
||||
/// mitgeklonten Alt-Datei in die Registry gelangt — darf kein beliebiges
|
||||
/// Verzeichnis zum CLAUDE_CONFIG_DIR machen.
|
||||
|
||||
Reference in New Issue
Block a user