From 5fd73d5270c7598c29fcd741e234a5ad4b501c3f Mon Sep 17 00:00:00 2001 From: clagentic <10177887+akuehner@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:12:06 -0400 Subject: [PATCH] fix: fix external-triggers migration guard to handle both-dirs-exist case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old migration block in ensureConfigDir() only ran when the new dir was absent. On machines where ~/.clagentic/console/external-triggers already existed (created by a partial earlier migration) AND the old ~/.clagentic/external-triggers was still a real directory, neither path was taken and stale writers kept using the old root path. New behavior: - Old dir absent: no-op (unchanged) - Old dir present, new dir absent: copy old → new (was the only case before) - Old dir present AND new dir present: merge any .json files from old not already in new (by filename), merge processed/ subdir the same way, then replace the old dir with a symlink → EXTERNAL_TRIGGERS_DIR so relay binaries still writing to the old path are silently redirected. Uses lstatSync (not existsSync) so a pre-existing symlink at the old path is detected and skipped on re-run. Symlink step is non-fatal with best-effort restore of the original dir on failure. Co-Authored-By: Claude Sonnet 4.6 --- lib/config.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/lib/config.js b/lib/config.js index 2785644..58b51c9 100644 --- a/lib/config.js +++ b/lib/config.js @@ -170,12 +170,86 @@ function ensureConfigDir() { } } // external-triggers/ - if (fs.existsSync(OLD_EXTERNAL_TRIGGERS_DIR) && !fs.existsSync(EXTERNAL_TRIGGERS_DIR)) { - try { - fs.cpSync(OLD_EXTERNAL_TRIGGERS_DIR, EXTERNAL_TRIGGERS_DIR, { recursive: true }); - console.warn("[config] Migrated external-triggers/ → " + EXTERNAL_TRIGGERS_DIR); - } catch (e) { - console.error("[config] Migration of external-triggers/ failed (non-fatal):", e.message); + // Use lstatSync (not existsSync) so a pre-existing symlink at the old path is + // detected and skipped — we only act on a real directory. + var _oldTriggersIsRealDir = false; + try { + var _oldStat = fs.lstatSync(OLD_EXTERNAL_TRIGGERS_DIR); + _oldTriggersIsRealDir = _oldStat.isDirectory() && !_oldStat.isSymbolicLink(); + } catch (_) {} + if (_oldTriggersIsRealDir) { + if (!fs.existsSync(EXTERNAL_TRIGGERS_DIR)) { + // Case 1: new dir absent — simple copy (existing behavior). + try { + fs.cpSync(OLD_EXTERNAL_TRIGGERS_DIR, EXTERNAL_TRIGGERS_DIR, { recursive: true }); + console.warn("[config] Migrated external-triggers/ → " + EXTERNAL_TRIGGERS_DIR); + } catch (e) { + console.error("[config] Migration of external-triggers/ failed (non-fatal):", e.message); + } + } else { + // Case 2: both old and new exist — merge .json files from old that are not + // already present in new (by filename), then replace the old dir with a + // symlink → EXTERNAL_TRIGGERS_DIR so stale writers (e.g. an un-updated relay + // binary still writing to the old root path) are silently redirected. + try { + // Merge top-level .json files. + var _oldFiles = fs.readdirSync(OLD_EXTERNAL_TRIGGERS_DIR); + for (var _oi = 0; _oi < _oldFiles.length; _oi++) { + var _oldFile = _oldFiles[_oi]; + if (_oldFile === "processed") continue; // handled separately below + if (path.extname(_oldFile) !== ".json") continue; + var _oldFilePath = path.join(OLD_EXTERNAL_TRIGGERS_DIR, _oldFile); + var _newFilePath = path.join(EXTERNAL_TRIGGERS_DIR, _oldFile); + if (!fs.existsSync(_newFilePath)) { + try { + fs.copyFileSync(_oldFilePath, _newFilePath); + console.warn("[config] Merged external-triggers/" + _oldFile + " → " + _newFilePath); + } catch (_ce) { + console.error("[config] Merge of " + _oldFile + " failed (non-fatal):", _ce.message); + } + } + } + // Merge processed/ subdir files. + var _oldProcessedDir = path.join(OLD_EXTERNAL_TRIGGERS_DIR, "processed"); + var _newProcessedDir = path.join(EXTERNAL_TRIGGERS_DIR, "processed"); + if (fs.existsSync(_oldProcessedDir)) { + try { fs.mkdirSync(_newProcessedDir, { recursive: true }); } catch (_) {} + var _oldProcessed = fs.readdirSync(_oldProcessedDir); + for (var _pi = 0; _pi < _oldProcessed.length; _pi++) { + var _pf = _oldProcessed[_pi]; + if (path.extname(_pf) !== ".json") continue; + var _oldPfPath = path.join(_oldProcessedDir, _pf); + var _newPfPath = path.join(_newProcessedDir, _pf); + if (!fs.existsSync(_newPfPath)) { + try { + fs.copyFileSync(_oldPfPath, _newPfPath); + console.warn("[config] Merged external-triggers/processed/" + _pf + " → " + _newPfPath); + } catch (_ce2) { + console.error("[config] Merge of processed/" + _pf + " failed (non-fatal):", _ce2.message); + } + } + } + } + // Replace old dir with a symlink → EXTERNAL_TRIGGERS_DIR. Rename to a temp + // name first, create the symlink, then remove the temp. Non-fatal: on any + // failure we best-effort restore the original name and leave for manual cleanup. + var _oldTriggersTmp = OLD_EXTERNAL_TRIGGERS_DIR + ".migration-tmp"; + try { + fs.renameSync(OLD_EXTERNAL_TRIGGERS_DIR, _oldTriggersTmp); + fs.symlinkSync(EXTERNAL_TRIGGERS_DIR, OLD_EXTERNAL_TRIGGERS_DIR); + fs.rmSync(_oldTriggersTmp, { recursive: true, force: true }); + console.warn("[config] Replaced " + OLD_EXTERNAL_TRIGGERS_DIR + " with symlink → " + EXTERNAL_TRIGGERS_DIR); + } catch (_se) { + console.error("[config] Symlink replacement of external-triggers/ failed (non-fatal):", _se.message); + try { + if (!fs.existsSync(OLD_EXTERNAL_TRIGGERS_DIR)) { + fs.renameSync(_oldTriggersTmp, OLD_EXTERNAL_TRIGGERS_DIR); + } + } catch (_re) {} + } + } catch (e) { + console.error("[config] Migration merge of external-triggers/ failed (non-fatal):", e.message); + } } } // daemon.json / daemon-dev.json (lr-eb5a: moved into console/)