mover-os 4.5.5 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install.js +431 -17
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -1093,6 +1093,207 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1093
1093
|
return result;
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
+
// ── Pristine Manifest Helpers (dpkg-style hash detection) ──────────────────
|
|
1097
|
+
|
|
1098
|
+
function fileContentHash(filePath) {
|
|
1099
|
+
const content = fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n");
|
|
1100
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function loadUpdateManifest() {
|
|
1104
|
+
const p = path.join(os.homedir(), ".mover", "manifest.json");
|
|
1105
|
+
if (!fs.existsSync(p)) return null;
|
|
1106
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function saveUpdateManifest(manifest) {
|
|
1110
|
+
const p = path.join(os.homedir(), ".mover", "manifest.json");
|
|
1111
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1112
|
+
fs.writeFileSync(p, JSON.stringify(manifest, null, 2), "utf8");
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function savePristineCopy(category, srcFile) {
|
|
1116
|
+
const pristineDir = path.join(os.homedir(), ".mover", "installed", category);
|
|
1117
|
+
fs.mkdirSync(pristineDir, { recursive: true });
|
|
1118
|
+
const content = fs.readFileSync(srcFile, "utf8").replace(/\r\n/g, "\n");
|
|
1119
|
+
fs.writeFileSync(path.join(pristineDir, path.basename(srcFile)), content, "utf8");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function compareForUpdate(category, srcFile, destFile, manifest) {
|
|
1123
|
+
const fileName = `${category}/${path.basename(srcFile)}`;
|
|
1124
|
+
const newHash = fileContentHash(srcFile);
|
|
1125
|
+
const pristineHash = manifest?.files?.[fileName];
|
|
1126
|
+
|
|
1127
|
+
if (!fs.existsSync(destFile)) return { action: "install", newHash };
|
|
1128
|
+
|
|
1129
|
+
if (!pristineHash) {
|
|
1130
|
+
const destHash = fileContentHash(destFile);
|
|
1131
|
+
return destHash === newHash
|
|
1132
|
+
? { action: "skip", newHash }
|
|
1133
|
+
: { action: "legacy-overwrite", newHash };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const destHash = fileContentHash(destFile);
|
|
1137
|
+
const userChanged = destHash !== pristineHash;
|
|
1138
|
+
const upstreamChanged = newHash !== pristineHash;
|
|
1139
|
+
|
|
1140
|
+
if (!userChanged && !upstreamChanged) return { action: "skip", newHash };
|
|
1141
|
+
if (!userChanged && upstreamChanged) return { action: "safe-overwrite", newHash };
|
|
1142
|
+
if (userChanged && !upstreamChanged) return { action: "user-only-skip", newHash };
|
|
1143
|
+
return { action: "conflict", newHash };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ── Three-Way Merge (inlined from node-diff3, MIT license) ─────────────────
|
|
1147
|
+
// bhousel/node-diff3 v3.2.0 — Hunt-McIlroy LCS + diff3 merge
|
|
1148
|
+
// Only the functions needed for three-way file merge are included.
|
|
1149
|
+
|
|
1150
|
+
function _lcs(buffer1, buffer2) {
|
|
1151
|
+
let eq = {};
|
|
1152
|
+
for (let j = 0; j < buffer2.length; j++) {
|
|
1153
|
+
const item = buffer2[j];
|
|
1154
|
+
if (eq[item]) eq[item].push(j); else eq[item] = [j];
|
|
1155
|
+
}
|
|
1156
|
+
const NULL = { buffer1index: -1, buffer2index: -1, chain: null };
|
|
1157
|
+
let cands = [NULL];
|
|
1158
|
+
for (let i = 0; i < buffer1.length; i++) {
|
|
1159
|
+
const b2 = eq[buffer1[i]] || [];
|
|
1160
|
+
let r = 0, c = cands[0];
|
|
1161
|
+
for (let jx = 0; jx < b2.length; jx++) {
|
|
1162
|
+
const j = b2[jx];
|
|
1163
|
+
let s;
|
|
1164
|
+
for (s = r; s < cands.length; s++) {
|
|
1165
|
+
if (cands[s].buffer2index < j && (s === cands.length - 1 || cands[s + 1].buffer2index > j)) break;
|
|
1166
|
+
}
|
|
1167
|
+
if (s < cands.length) {
|
|
1168
|
+
const nc = { buffer1index: i, buffer2index: j, chain: cands[s] };
|
|
1169
|
+
if (r === cands.length) cands.push(c); else cands[r] = c;
|
|
1170
|
+
r = s + 1; c = nc;
|
|
1171
|
+
if (r === cands.length) break;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
cands[r] = c;
|
|
1175
|
+
}
|
|
1176
|
+
return cands[cands.length - 1];
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function _diffIndices(buffer1, buffer2) {
|
|
1180
|
+
const lcs = _lcs(buffer1, buffer2);
|
|
1181
|
+
let result = [], tail1 = buffer1.length, tail2 = buffer2.length;
|
|
1182
|
+
for (let c = lcs; c !== null; c = c.chain) {
|
|
1183
|
+
const m1 = tail1 - c.buffer1index - 1, m2 = tail2 - c.buffer2index - 1;
|
|
1184
|
+
tail1 = c.buffer1index; tail2 = c.buffer2index;
|
|
1185
|
+
if (m1 || m2) {
|
|
1186
|
+
result.push({
|
|
1187
|
+
buffer1: [tail1 + 1, m1], buffer1Content: buffer1.slice(tail1 + 1, tail1 + 1 + m1),
|
|
1188
|
+
buffer2: [tail2 + 1, m2], buffer2Content: buffer2.slice(tail2 + 1, tail2 + 1 + m2)
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
result.reverse();
|
|
1193
|
+
return result;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function _diff3MergeRegions(a, o, b) {
|
|
1197
|
+
let hunks = [];
|
|
1198
|
+
function addHunk(h, ab) {
|
|
1199
|
+
hunks.push({ ab, oStart: h.buffer1[0], oLength: h.buffer1[1], abStart: h.buffer2[0], abLength: h.buffer2[1] });
|
|
1200
|
+
}
|
|
1201
|
+
_diffIndices(o, a).forEach(i => addHunk(i, "a"));
|
|
1202
|
+
_diffIndices(o, b).forEach(i => addHunk(i, "b"));
|
|
1203
|
+
hunks.sort((x, y) => x.oStart - y.oStart);
|
|
1204
|
+
|
|
1205
|
+
let results = [], curr = 0;
|
|
1206
|
+
function advanceTo(end) {
|
|
1207
|
+
if (end > curr) {
|
|
1208
|
+
results.push({ stable: true, buffer: "o", bufferStart: curr, bufferLength: end - curr, bufferContent: o.slice(curr, end) });
|
|
1209
|
+
curr = end;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
while (hunks.length) {
|
|
1214
|
+
let hunk = hunks.shift();
|
|
1215
|
+
let rStart = hunk.oStart, rEnd = hunk.oStart + hunk.oLength;
|
|
1216
|
+
let rHunks = [hunk];
|
|
1217
|
+
advanceTo(rStart);
|
|
1218
|
+
while (hunks.length && hunks[0].oStart <= rEnd) {
|
|
1219
|
+
rEnd = Math.max(rEnd, hunks[0].oStart + hunks[0].oLength);
|
|
1220
|
+
rHunks.push(hunks.shift());
|
|
1221
|
+
}
|
|
1222
|
+
if (rHunks.length === 1) {
|
|
1223
|
+
if (hunk.abLength > 0) {
|
|
1224
|
+
const buf = hunk.ab === "a" ? a : b;
|
|
1225
|
+
results.push({ stable: true, buffer: hunk.ab, bufferStart: hunk.abStart, bufferLength: hunk.abLength, bufferContent: buf.slice(hunk.abStart, hunk.abStart + hunk.abLength) });
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
let bounds = { a: [a.length, -1, o.length, -1], b: [b.length, -1, o.length, -1] };
|
|
1229
|
+
while (rHunks.length) {
|
|
1230
|
+
hunk = rHunks.shift();
|
|
1231
|
+
let bd = bounds[hunk.ab];
|
|
1232
|
+
bd[0] = Math.min(hunk.abStart, bd[0]);
|
|
1233
|
+
bd[1] = Math.max(hunk.abStart + hunk.abLength, bd[1]);
|
|
1234
|
+
bd[2] = Math.min(hunk.oStart, bd[2]);
|
|
1235
|
+
bd[3] = Math.max(hunk.oStart + hunk.oLength, bd[3]);
|
|
1236
|
+
}
|
|
1237
|
+
results.push({
|
|
1238
|
+
stable: false,
|
|
1239
|
+
aStart: bounds.a[0] + (rStart - bounds.a[2]), aLength: (bounds.a[1] + (rEnd - bounds.a[3])) - (bounds.a[0] + (rStart - bounds.a[2])),
|
|
1240
|
+
aContent: a.slice(bounds.a[0] + (rStart - bounds.a[2]), bounds.a[1] + (rEnd - bounds.a[3])),
|
|
1241
|
+
oStart: rStart, oLength: rEnd - rStart, oContent: o.slice(rStart, rEnd),
|
|
1242
|
+
bStart: bounds.b[0] + (rStart - bounds.b[2]), bLength: (bounds.b[1] + (rEnd - bounds.b[3])) - (bounds.b[0] + (rStart - bounds.b[2])),
|
|
1243
|
+
bContent: b.slice(bounds.b[0] + (rStart - bounds.b[2]), bounds.b[1] + (rEnd - bounds.b[3]))
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
curr = rEnd;
|
|
1247
|
+
}
|
|
1248
|
+
advanceTo(o.length);
|
|
1249
|
+
return results;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function diff3Merge(a, o, b) {
|
|
1253
|
+
let results = [], okBuf = [];
|
|
1254
|
+
function flushOk() { if (okBuf.length) { results.push({ ok: okBuf }); okBuf = []; } }
|
|
1255
|
+
function isFalse(x, y) {
|
|
1256
|
+
if (x.length !== y.length) return false;
|
|
1257
|
+
for (let i = 0; i < x.length; i++) { if (x[i] !== y[i]) return false; }
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
_diff3MergeRegions(a, o, b).forEach(r => {
|
|
1261
|
+
if (r.stable) { okBuf.push(...r.bufferContent); }
|
|
1262
|
+
else if (isFalse(r.aContent, r.bContent)) { okBuf.push(...r.aContent); }
|
|
1263
|
+
else { flushOk(); results.push({ conflict: { a: r.aContent, o: r.oContent, b: r.bContent } }); }
|
|
1264
|
+
});
|
|
1265
|
+
flushOk();
|
|
1266
|
+
return results;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ── Auto-Merge (uses diff3 + pristine snapshots) ──────────────────────────
|
|
1270
|
+
|
|
1271
|
+
function tryAutoMerge(category, srcFile, destFile) {
|
|
1272
|
+
const pristineDir = path.join(os.homedir(), ".mover", "installed", category);
|
|
1273
|
+
const pristineFile = path.join(pristineDir, path.basename(srcFile));
|
|
1274
|
+
if (!fs.existsSync(pristineFile)) return { success: false, reason: "no-pristine" };
|
|
1275
|
+
|
|
1276
|
+
const base = fs.readFileSync(pristineFile, "utf8").replace(/\r\n/g, "\n");
|
|
1277
|
+
const theirs = fs.readFileSync(srcFile, "utf8").replace(/\r\n/g, "\n");
|
|
1278
|
+
const yours = fs.readFileSync(destFile, "utf8").replace(/\r\n/g, "\n");
|
|
1279
|
+
|
|
1280
|
+
const result = diff3Merge(yours.split("\n"), base.split("\n"), theirs.split("\n"));
|
|
1281
|
+
const hasConflicts = result.some(block => block.conflict);
|
|
1282
|
+
|
|
1283
|
+
if (!hasConflicts) {
|
|
1284
|
+
const merged = result.flatMap(block => block.ok || []);
|
|
1285
|
+
return { success: true, content: merged.join("\n") };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
success: false,
|
|
1290
|
+
reason: "conflict",
|
|
1291
|
+
conflictCount: result.filter(block => block.conflict).length
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ── End Three-Way Merge ────────────────────────────────────────────────────
|
|
1296
|
+
|
|
1096
1297
|
function autoBackupBeforeUpdate(changes, selectedAgentIds, vaultPath) {
|
|
1097
1298
|
const home = os.homedir();
|
|
1098
1299
|
const targetIds = expandTargetIds(selectedAgentIds);
|
|
@@ -2256,7 +2457,10 @@ function installWorkflows(bundleDir, destDir, selectedWorkflows) {
|
|
|
2256
2457
|
let count = 0;
|
|
2257
2458
|
for (const file of srcFiles) {
|
|
2258
2459
|
if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
|
|
2259
|
-
if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file)))
|
|
2460
|
+
if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file))) {
|
|
2461
|
+
savePristineCopy("workflows", path.join(srcDir, file));
|
|
2462
|
+
count++;
|
|
2463
|
+
}
|
|
2260
2464
|
}
|
|
2261
2465
|
|
|
2262
2466
|
// Clean orphaned workflows (renamed/removed in updates)
|
|
@@ -2311,6 +2515,9 @@ function installRules(bundleDir, destPath, agentId) {
|
|
|
2311
2515
|
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2312
2516
|
if (!fs.existsSync(src)) return false;
|
|
2313
2517
|
|
|
2518
|
+
// Save pristine copy of source rules (pre-customization) for future hash comparison
|
|
2519
|
+
savePristineCopy("system", src);
|
|
2520
|
+
|
|
2314
2521
|
// Extract any user customizations from existing file before overwriting
|
|
2315
2522
|
const customizations = extractCustomizations(destPath);
|
|
2316
2523
|
|
|
@@ -2588,12 +2795,14 @@ function installHooksForClaude(bundleDir, vaultPath) {
|
|
|
2588
2795
|
// Read, strip \r (CRLF→LF), write — prevents "command not found" on macOS/Linux
|
|
2589
2796
|
const content = fs.readFileSync(path.join(hooksSrc, file), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2590
2797
|
fs.writeFileSync(dst, content, { mode: 0o755 });
|
|
2798
|
+
savePristineCopy("hooks", path.join(hooksSrc, file));
|
|
2591
2799
|
count++;
|
|
2592
2800
|
}
|
|
2593
2801
|
// Copy hook template files (.md) and scripts (.js)
|
|
2594
2802
|
for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".md") || f.endsWith(".js"))) {
|
|
2595
2803
|
const dst = path.join(hooksDst, file);
|
|
2596
2804
|
fs.copyFileSync(path.join(hooksSrc, file), dst);
|
|
2805
|
+
savePristineCopy("hooks", path.join(hooksSrc, file));
|
|
2597
2806
|
count++;
|
|
2598
2807
|
}
|
|
2599
2808
|
|
|
@@ -4894,7 +5103,9 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4894
5103
|
const hooksDest = path.join(home, ".claude", "hooks");
|
|
4895
5104
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
4896
5105
|
for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh"))) {
|
|
4897
|
-
fs.
|
|
5106
|
+
const hookContent = fs.readFileSync(path.join(hooksSrc, file), "utf8")
|
|
5107
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5108
|
+
fs.writeFileSync(path.join(hooksDest, file), hookContent, { mode: 0o755 });
|
|
4898
5109
|
try { fs.chmodSync(path.join(hooksDest, file), 0o755); } catch {}
|
|
4899
5110
|
appliedCount++;
|
|
4900
5111
|
}
|
|
@@ -4905,7 +5116,8 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4905
5116
|
const slSrc = path.join(bundleDir, "src", "hooks", "statusline.js");
|
|
4906
5117
|
const slDest = path.join(home, ".claude", "statusline.js");
|
|
4907
5118
|
if (fs.existsSync(slSrc)) {
|
|
4908
|
-
fs.
|
|
5119
|
+
const slContent = fs.readFileSync(slSrc, "utf8").replace(/\r\n/g, "\n");
|
|
5120
|
+
fs.writeFileSync(slDest, slContent, "utf8");
|
|
4909
5121
|
// Ensure settings.json has statusLine config
|
|
4910
5122
|
const globalSettings = path.join(home, ".claude", "settings.json");
|
|
4911
5123
|
try {
|
|
@@ -4921,6 +5133,23 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4921
5133
|
appliedCount++;
|
|
4922
5134
|
}
|
|
4923
5135
|
|
|
5136
|
+
// /update workflow (must always be current — it drives the rest of the update)
|
|
5137
|
+
const updateSrc = path.join(bundleDir, "src", "workflows", "update.md");
|
|
5138
|
+
if (fs.existsSync(updateSrc)) {
|
|
5139
|
+
const cmdDirs = [
|
|
5140
|
+
path.join(home, ".claude", "commands"),
|
|
5141
|
+
path.join(home, ".cursor", "commands"),
|
|
5142
|
+
path.join(home, ".gemini", "antigravity", "global_workflows"),
|
|
5143
|
+
];
|
|
5144
|
+
for (const dir of cmdDirs) {
|
|
5145
|
+
const dest = path.join(dir, "update.md");
|
|
5146
|
+
if (fs.existsSync(dir)) {
|
|
5147
|
+
fs.copyFileSync(updateSrc, dest);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
statusLine("ok", "/update", "workflow synced");
|
|
5151
|
+
}
|
|
5152
|
+
|
|
4924
5153
|
// Vault structure + templates (safe scaffolding)
|
|
4925
5154
|
createVaultStructure(vaultPath);
|
|
4926
5155
|
installTemplateFiles(bundleDir, vaultPath);
|
|
@@ -4928,26 +5157,189 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4928
5157
|
// Update version marker
|
|
4929
5158
|
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4930
5159
|
|
|
4931
|
-
//
|
|
4932
|
-
const
|
|
4933
|
-
|
|
4934
|
-
|
|
5160
|
+
// ── Smart Update: hash-based change detection + auto-merge ──
|
|
5161
|
+
const manifest = loadUpdateManifest();
|
|
5162
|
+
const newManifest = { version: require("./package.json").version, installedAt: new Date().toISOString(), files: {} };
|
|
5163
|
+
const updated = [], skippedFiles = [], autoMerged = [], conflicts = [], userOnly = [];
|
|
5164
|
+
let needsUpdate = false;
|
|
5165
|
+
|
|
5166
|
+
if (!manifest) {
|
|
5167
|
+
barLn(dim(" First update with smart change detection — setting up file tracking."));
|
|
5168
|
+
barLn(dim(" Future updates will preserve your customizations automatically."));
|
|
5169
|
+
barLn();
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
// ── Workflows: smart merge per file ──
|
|
5173
|
+
const wfSrcDir = path.join(bundleDir, "src", "workflows");
|
|
5174
|
+
const wfDestDir = path.join(home, ".claude", "commands");
|
|
5175
|
+
if (fs.existsSync(wfSrcDir)) {
|
|
5176
|
+
for (const file of fs.readdirSync(wfSrcDir).filter((f) => f.endsWith(".md"))) {
|
|
5177
|
+
if (file === "update.md") continue; // already synced above
|
|
5178
|
+
const srcFile = path.join(wfSrcDir, file);
|
|
5179
|
+
const destFile = path.join(wfDestDir, file);
|
|
5180
|
+
const result = compareForUpdate("workflows", srcFile, destFile, manifest);
|
|
5181
|
+
|
|
5182
|
+
switch (result.action) {
|
|
5183
|
+
case "install":
|
|
5184
|
+
case "safe-overwrite":
|
|
5185
|
+
linkOrCopy(srcFile, destFile);
|
|
5186
|
+
savePristineCopy("workflows", srcFile);
|
|
5187
|
+
updated.push(file);
|
|
5188
|
+
break;
|
|
5189
|
+
case "user-only-skip":
|
|
5190
|
+
userOnly.push(file);
|
|
5191
|
+
break;
|
|
5192
|
+
case "skip":
|
|
5193
|
+
skippedFiles.push(file);
|
|
5194
|
+
break;
|
|
5195
|
+
case "legacy-overwrite": {
|
|
5196
|
+
const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
|
|
5197
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
5198
|
+
if (fs.existsSync(destFile)) fs.copyFileSync(destFile, path.join(backupDir, file));
|
|
5199
|
+
linkOrCopy(srcFile, destFile);
|
|
5200
|
+
savePristineCopy("workflows", srcFile);
|
|
5201
|
+
updated.push(file);
|
|
5202
|
+
break;
|
|
5203
|
+
}
|
|
5204
|
+
case "conflict": {
|
|
5205
|
+
const mergeResult = tryAutoMerge("workflows", srcFile, destFile);
|
|
5206
|
+
if (mergeResult.success) {
|
|
5207
|
+
fs.writeFileSync(destFile, mergeResult.content, "utf8");
|
|
5208
|
+
savePristineCopy("workflows", srcFile);
|
|
5209
|
+
autoMerged.push(file);
|
|
5210
|
+
} else {
|
|
5211
|
+
conflicts.push({ name: file, reason: mergeResult.reason, conflictCount: mergeResult.conflictCount });
|
|
5212
|
+
}
|
|
5213
|
+
break;
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
newManifest.files[`workflows/${file}`] = result.newHash;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
// ── Rules: keep sentinel merge + track pristine ──
|
|
5221
|
+
if (changes.rules === "changed") {
|
|
5222
|
+
const rulesSrc = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
5223
|
+
// installRules already handles sentinel merge + pristine save
|
|
5224
|
+
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
5225
|
+
for (const agent of detectedAgents) {
|
|
5226
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5227
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5228
|
+
for (const tid of targets) {
|
|
5229
|
+
const reg = AGENT_REGISTRY[tid];
|
|
5230
|
+
if (reg && reg.rules) {
|
|
5231
|
+
const destPath = typeof reg.rules === "function" ? reg.rules() : reg.rules;
|
|
5232
|
+
installRules(bundleDir, destPath, tid);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
if (fs.existsSync(rulesSrc)) newManifest.files["system/Mover_Global_Rules.md"] = fileContentHash(rulesSrc);
|
|
5237
|
+
updated.push("Global Rules");
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
// ── Skills: use existing hash-based install (already has .skill-manifest.json) ──
|
|
5241
|
+
const skillChanges = (changes.skills || []).filter((f) => f.status !== "unchanged");
|
|
5242
|
+
if (skillChanges.length > 0) {
|
|
5243
|
+
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
5244
|
+
const writtenFiles = new Set();
|
|
5245
|
+
for (const agent of detectedAgents) {
|
|
5246
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5247
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5248
|
+
for (const tid of targets) {
|
|
5249
|
+
const reg = AGENT_REGISTRY[tid];
|
|
5250
|
+
if (reg && reg.skills) {
|
|
5251
|
+
const destDir = typeof reg.skills === "function" ? reg.skills() : reg.skills;
|
|
5252
|
+
if (destDir && !writtenFiles.has(destDir)) {
|
|
5253
|
+
installSkills(bundleDir, destDir, null, null);
|
|
5254
|
+
writtenFiles.add(destDir);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
updated.push(`${skillChanges.length} skills`);
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
// ── Handle true conflicts → write conflicts.json for /update ──
|
|
5263
|
+
if (conflicts.length > 0) {
|
|
5264
|
+
const conflictData = conflicts.map(({ name }) => ({
|
|
5265
|
+
file: name,
|
|
5266
|
+
pristine: path.join(home, ".mover", "installed", "workflows", name),
|
|
5267
|
+
userVersion: path.join(home, ".claude", "commands", name),
|
|
5268
|
+
newVersion: path.join(bundleDir, "src", "workflows", name),
|
|
5269
|
+
}));
|
|
5270
|
+
const conflictPath = path.join(home, ".mover", "conflicts.json");
|
|
5271
|
+
fs.writeFileSync(conflictPath, JSON.stringify({
|
|
5272
|
+
version: require("./package.json").version,
|
|
5273
|
+
timestamp: new Date().toISOString(),
|
|
5274
|
+
files: conflictData,
|
|
5275
|
+
}, null, 2), "utf8");
|
|
5276
|
+
|
|
5277
|
+
// Backup conflicted user files
|
|
5278
|
+
const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
|
|
5279
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
5280
|
+
for (const { name } of conflicts) {
|
|
5281
|
+
const dest = path.join(home, ".claude", "commands", name);
|
|
5282
|
+
if (fs.existsSync(dest)) fs.copyFileSync(dest, path.join(backupDir, name));
|
|
5283
|
+
}
|
|
5284
|
+
needsUpdate = true;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
// ── Multi-agent propagation (mirrors --quick path) ──
|
|
5288
|
+
const detectedAgentsForProp = AGENTS.filter((a) => a.detect());
|
|
5289
|
+
const propWritten = new Set();
|
|
5290
|
+
for (const agent of detectedAgentsForProp) {
|
|
5291
|
+
if (agent.id === "claude-code") continue;
|
|
5292
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5293
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5294
|
+
for (const tid of targets) {
|
|
5295
|
+
const fn = AGENT_INSTALLERS[tid];
|
|
5296
|
+
if (!fn || propWritten.has(tid)) continue;
|
|
5297
|
+
try {
|
|
5298
|
+
const sp = spinner(AGENT_REGISTRY[tid]?.name || agent.name);
|
|
5299
|
+
const steps = fn(bundleDir, vaultPath, { install: true, categories: null, workflows: null, statusLine: true }, propWritten, tid);
|
|
5300
|
+
sp.stop(`${AGENT_REGISTRY[tid]?.name || agent.name} ${dim(steps.join(", "))}`);
|
|
5301
|
+
} catch {}
|
|
5302
|
+
propWritten.add(tid);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
4935
5305
|
|
|
5306
|
+
// ── Save updated manifest ──
|
|
5307
|
+
// Carry forward entries for files we didn't process (hooks, statusline, /update itself)
|
|
5308
|
+
if (manifest && manifest.files) {
|
|
5309
|
+
for (const [key, hash] of Object.entries(manifest.files)) {
|
|
5310
|
+
if (!newManifest.files[key]) newManifest.files[key] = hash;
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
const hooksDir = path.join(bundleDir, "src", "hooks");
|
|
5314
|
+
if (fs.existsSync(hooksDir)) {
|
|
5315
|
+
for (const f of fs.readdirSync(hooksDir).filter((x) => x.endsWith(".sh") || x.endsWith(".js") || x.endsWith(".md"))) {
|
|
5316
|
+
newManifest.files[`hooks/${f}`] = fileContentHash(path.join(hooksDir, f));
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
saveUpdateManifest(newManifest);
|
|
5320
|
+
writeMoverConfig(vaultPath, selectedIds);
|
|
5321
|
+
|
|
5322
|
+
// ── Summary ──
|
|
4936
5323
|
barLn();
|
|
4937
|
-
if (
|
|
4938
|
-
|
|
5324
|
+
if (updated.length > 0) statusLine("ok", "Updated", `${updated.length} files`);
|
|
5325
|
+
if (autoMerged.length > 0) statusLine("ok", "Auto-merged", `${autoMerged.length} files (your changes + our updates combined)`);
|
|
5326
|
+
if (userOnly.length > 0) statusLine("ok", "Preserved", `${userOnly.length} customized files`);
|
|
5327
|
+
if (skippedFiles.length > 0) statusLine("ok", "Current", `${skippedFiles.length} files unchanged`);
|
|
5328
|
+
|
|
5329
|
+
if (conflicts.length > 0) {
|
|
4939
5330
|
barLn();
|
|
4940
|
-
barLn(
|
|
4941
|
-
barLn(`
|
|
4942
|
-
barLn(
|
|
4943
|
-
barLn(dim("
|
|
4944
|
-
|
|
4945
|
-
|
|
5331
|
+
barLn(yellow(` ${conflicts.length} file${conflicts.length > 1 ? "s" : ""} need intelligent merge:`));
|
|
5332
|
+
for (const { name } of conflicts) barLn(` ${yellow("!")} ${name}`);
|
|
5333
|
+
barLn();
|
|
5334
|
+
barLn(dim(" You and we both changed the same sections."));
|
|
5335
|
+
barLn(dim(" Run /update — the AI will merge your changes with ours."));
|
|
5336
|
+
barLn(dim(" Your current versions are backed up."));
|
|
4946
5337
|
}
|
|
5338
|
+
|
|
4947
5339
|
barLn();
|
|
4948
5340
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4949
|
-
if (
|
|
4950
|
-
outro(`System updated. Run ${bold("/update")}
|
|
5341
|
+
if (needsUpdate) {
|
|
5342
|
+
outro(`System updated. Run ${bold("/update")} to resolve ${conflicts.length} conflict${conflicts.length > 1 ? "s" : ""}. ${dim(`(${elapsed}s)`)}`);
|
|
4951
5343
|
} else {
|
|
4952
5344
|
outro(`${green("Done.")} All files up to date. ${dim(`(${elapsed}s)`)}`);
|
|
4953
5345
|
}
|
|
@@ -5588,6 +5980,28 @@ async function main() {
|
|
|
5588
5980
|
// 9. Write ~/.mover/config.json (both fresh + update)
|
|
5589
5981
|
writeMoverConfig(vaultPath, selectedIds, key, { prayerSetup });
|
|
5590
5982
|
|
|
5983
|
+
// 10. Build update manifest (pristine hashes for smart future updates)
|
|
5984
|
+
try {
|
|
5985
|
+
const mfst = { version: VERSION, installedAt: new Date().toISOString(), files: {} };
|
|
5986
|
+
const wfDir = path.join(bundleDir, "src", "workflows");
|
|
5987
|
+
if (fs.existsSync(wfDir)) {
|
|
5988
|
+
for (const f of fs.readdirSync(wfDir).filter((x) => x.endsWith(".md"))) {
|
|
5989
|
+
mfst.files[`workflows/${f}`] = fileContentHash(path.join(wfDir, f));
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
const rulesPath = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
5993
|
+
if (fs.existsSync(rulesPath)) {
|
|
5994
|
+
mfst.files["system/Mover_Global_Rules.md"] = fileContentHash(rulesPath);
|
|
5995
|
+
}
|
|
5996
|
+
const hooksDir = path.join(bundleDir, "src", "hooks");
|
|
5997
|
+
if (fs.existsSync(hooksDir)) {
|
|
5998
|
+
for (const f of fs.readdirSync(hooksDir).filter((x) => x.endsWith(".sh") || x.endsWith(".js") || x.endsWith(".md"))) {
|
|
5999
|
+
mfst.files[`hooks/${f}`] = fileContentHash(path.join(hooksDir, f));
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
saveUpdateManifest(mfst);
|
|
6003
|
+
} catch {}
|
|
6004
|
+
|
|
5591
6005
|
barLn();
|
|
5592
6006
|
|
|
5593
6007
|
// ── Done ──
|