poolSyncDir konfigurierbar: Pool-Runtime-Verlinkung nur bei gesetztem Sync-Ordner (settings.json), Ziel <poolSyncDir>/<pool> statt hartem ~/claude-projects/pool; ungesetzt bleibt alles lokal

This commit is contained in:
marcus.hinz
2026-07-05 15:51:31 +02:00
parent 8a831af8a8
commit 387a9e964a
2 changed files with 47 additions and 12 deletions
+1
View File
@@ -90,6 +90,7 @@ Prerequisites: Rust (stable), Node.js/npm, macOS. `dev.sh`/`build.sh` expect `CA
|--------------------|----------|---------| |--------------------|----------|---------|
| `claudeCommand` | `claude` | Command launched in the project terminal (via `zsh -ic`, so your `.zshrc` PATH applies). | | `claudeCommand` | `claude` | Command launched in the project terminal (via `zsh -ic`, so your `.zshrc` PATH applies). |
| `syncOnSessionEnd` | `false` | After a session ends, commit and push the Git repository containing the project. | | `syncOnSessionEnd` | `false` | After a session ends, commit and push the Git repository containing the project. |
| `poolSyncDir` | unset | Directory to sync pool runtime data into (session transcripts, todos, prompt history — what `/resume` needs). When set, a pool's runtime files are replaced by symlinks into `<poolSyncDir>/<pool>`, so sessions travel with however that directory is synced (git, Syncthing, …). Unset: everything stays local in the pool directory. |
## Claude and Anthropic licensing ## Claude and Anthropic licensing
+46 -12
View File
@@ -1205,18 +1205,30 @@ fn init_pool_config(
const SYNCED_RUNTIME: [(&str, bool); 3] = const SYNCED_RUNTIME: [(&str, bool); 3] =
[("projects", true), ("todos", true), ("history.jsonl", false)]; [("projects", true), ("todos", true), ("history.jsonl", false)];
/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo. /// Sync-Ziel für Pool-Laufzeitdaten (settings.json: poolSyncDir).
fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf { /// Ungesetzt = Feature aus, alle Daten bleiben lokal im Pool-Ordner.
paths.projects_dir().join("pool").join(pool) fn pool_sync_dir(paths: &Paths) -> Option<PathBuf> {
fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| v["poolSyncDir"].as_str().map(|d| expand_home(paths, d)))
} }
/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das /// Zielort der synced Runtime-Daten eines Pools unterhalb von poolSyncDir.
/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git. fn pool_data_dir(paths: &Paths, pool: &str) -> Result<PathBuf, String> {
pool_sync_dir(paths)
.map(|d| d.join(pool))
.ok_or_else(|| "poolSyncDir ist nicht konfiguriert".to_string())
}
/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf den
/// konfigurierten Sync-Ordner. Die Symlinks sind maschinenlokal, die Daten
/// reisen über den Sync des Zielordners (z. B. git).
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst). /// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> { fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
check_name(pool)?; check_name(pool)?;
let src = paths.pool_dir(pool); let src = paths.pool_dir(pool);
let data = pool_data_dir(paths, pool); let data = pool_data_dir(paths, pool)?;
for (name, is_dir) in SYNCED_RUNTIME { for (name, is_dir) in SYNCED_RUNTIME {
let target = data.join(name); let target = data.join(name);
if is_dir { if is_dir {
@@ -1266,7 +1278,9 @@ fn create_apikey_pool_in(
serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }), serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),
)?; )?;
write_pool_json(&dir, name, "apikey")?; write_pool_json(&dir, name, "apikey")?;
link_pool_runtime_in(paths, &id)?; if pool_sync_dir(paths).is_some() {
link_pool_runtime_in(paths, &id)?;
}
Ok(id) Ok(id)
} }
@@ -1279,7 +1293,9 @@ fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
init_pool_config(&dir, serde_json::json!({}))?; init_pool_config(&dir, serde_json::json!({}))?;
write_pool_json(&dir, name, "oauth")?; write_pool_json(&dir, name, "oauth")?;
let id = dir.file_name().unwrap().to_string_lossy().into_owned(); let id = dir.file_name().unwrap().to_string_lossy().into_owned();
link_pool_runtime_in(paths, &id)?; if pool_sync_dir(paths).is_some() {
link_pool_runtime_in(paths, &id)?;
}
Ok(id) Ok(id)
} }
@@ -2611,18 +2627,36 @@ mod tests {
#[test] #[test]
fn pool_anlegen_verlinkt_synced_runtime() { fn pool_anlegen_verlinkt_synced_runtime() {
let p = tmp_paths(); let p = tmp_paths();
fs::create_dir_all(p.config_dir()).unwrap();
fs::write(
p.config_dir().join(APP_SETTINGS_FILE),
"{ \"poolSyncDir\": \"~/claude-projects/pool\" }\n",
)
.unwrap();
let id = make_oauth_pool(&p, "privat"); let id = make_oauth_pool(&p, "privat");
let pooldir = p.pool_dir(&id); let pooldir = p.pool_dir(&id);
for (name, is_dir) in SYNCED_RUNTIME { for (name, is_dir) in SYNCED_RUNTIME {
let link = pooldir.join(name); let link = pooldir.join(name);
assert!(link.is_symlink(), "{name} sollte Symlink sein"); assert!(link.is_symlink(), "{name} sollte Symlink sein");
assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name)); assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).unwrap().join(name));
let target = pool_data_dir(&p, &id).join(name); let target = pool_data_dir(&p, &id).unwrap().join(name);
assert_eq!(target.is_dir(), is_dir); assert_eq!(target.is_dir(), is_dir);
assert!(target.exists()); assert!(target.exists());
} }
// Zieldaten liegen im claude-projects-Repo unter pool/<id>/ // Zieldaten liegen unter dem konfigurierten poolSyncDir/<id>/
assert!(p.projects_dir().join("pool").join(&id).is_dir()); assert!(p.home.join("claude-projects/pool").join(&id).is_dir());
}
/// Ohne poolSyncDir bleibt alles lokal: keine Symlinks, Verlinken scheitert laut.
#[test]
fn pool_anlegen_ohne_sync_dir_bleibt_lokal() {
let p = tmp_paths();
let id = make_oauth_pool(&p, "privat");
for (name, _) in SYNCED_RUNTIME {
assert!(!p.pool_dir(&id).join(name).is_symlink(), "{name} darf kein Symlink sein");
}
let err = link_pool_runtime_in(&p, &id).unwrap_err();
assert!(err.contains("poolSyncDir"));
} }
#[test] #[test]