mover-os 4.5.6 → 4.6.1
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 +494 -39
- 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);
|
|
@@ -1469,9 +1670,14 @@ async function runUninstall(vaultPath) {
|
|
|
1469
1670
|
barLn(`${dim("Could not reach Polar — license not deactivated")}`);
|
|
1470
1671
|
}
|
|
1471
1672
|
}
|
|
1472
|
-
// Remove config file
|
|
1473
|
-
|
|
1474
|
-
|
|
1673
|
+
// Remove config file but preserve license key for reinstall
|
|
1674
|
+
if (cfg.licenseKey) {
|
|
1675
|
+
fs.writeFileSync(configPath, JSON.stringify({ licenseKey: cfg.licenseKey }, null, 2), "utf8");
|
|
1676
|
+
barLn(`${green("\u2713")} ${dim("~/.mover/config.json (license key preserved for reinstall)")}`);
|
|
1677
|
+
} else {
|
|
1678
|
+
fs.unlinkSync(configPath);
|
|
1679
|
+
barLn(`${green("\u2713")} ${dim("~/.mover/config.json")}`);
|
|
1680
|
+
}
|
|
1475
1681
|
removed++;
|
|
1476
1682
|
} catch {}
|
|
1477
1683
|
}
|
|
@@ -1679,13 +1885,14 @@ const AGENT_REGISTRY = {
|
|
|
1679
1885
|
},
|
|
1680
1886
|
};
|
|
1681
1887
|
|
|
1682
|
-
// User-selectable agents (
|
|
1888
|
+
// User-selectable agents (15 selections). Each maps to 1+ install targets.
|
|
1683
1889
|
const AGENT_SELECTIONS = [
|
|
1684
1890
|
{ id: "claude-code", targets: ["claude-code"], name: "Claude Code" },
|
|
1685
1891
|
{ id: "cursor", targets: ["cursor"], name: "Cursor" },
|
|
1686
1892
|
{ id: "cline", targets: ["cline"], name: "Cline" },
|
|
1687
1893
|
{ id: "windsurf", targets: ["windsurf"], name: "Windsurf" },
|
|
1688
|
-
{ id: "gemini-cli", targets: ["gemini-cli"
|
|
1894
|
+
{ id: "gemini-cli", targets: ["gemini-cli"], name: "Gemini CLI" },
|
|
1895
|
+
{ id: "antigravity", targets: ["antigravity"], name: "Antigravity" },
|
|
1689
1896
|
{ id: "copilot", targets: ["copilot"], name: "GitHub Copilot" },
|
|
1690
1897
|
{ id: "codex", targets: ["codex"], name: "Codex" },
|
|
1691
1898
|
{ id: "amazon-q", targets: ["amazon-q"], name: "Amazon Q Developer" },
|
|
@@ -2256,7 +2463,10 @@ function installWorkflows(bundleDir, destDir, selectedWorkflows) {
|
|
|
2256
2463
|
let count = 0;
|
|
2257
2464
|
for (const file of srcFiles) {
|
|
2258
2465
|
if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
|
|
2259
|
-
if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file)))
|
|
2466
|
+
if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file))) {
|
|
2467
|
+
savePristineCopy("workflows", path.join(srcDir, file));
|
|
2468
|
+
count++;
|
|
2469
|
+
}
|
|
2260
2470
|
}
|
|
2261
2471
|
|
|
2262
2472
|
// Clean orphaned workflows (renamed/removed in updates)
|
|
@@ -2311,6 +2521,9 @@ function installRules(bundleDir, destPath, agentId) {
|
|
|
2311
2521
|
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2312
2522
|
if (!fs.existsSync(src)) return false;
|
|
2313
2523
|
|
|
2524
|
+
// Save pristine copy of source rules (pre-customization) for future hash comparison
|
|
2525
|
+
savePristineCopy("system", src);
|
|
2526
|
+
|
|
2314
2527
|
// Extract any user customizations from existing file before overwriting
|
|
2315
2528
|
const customizations = extractCustomizations(destPath);
|
|
2316
2529
|
|
|
@@ -2588,12 +2801,14 @@ function installHooksForClaude(bundleDir, vaultPath) {
|
|
|
2588
2801
|
// Read, strip \r (CRLF→LF), write — prevents "command not found" on macOS/Linux
|
|
2589
2802
|
const content = fs.readFileSync(path.join(hooksSrc, file), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2590
2803
|
fs.writeFileSync(dst, content, { mode: 0o755 });
|
|
2804
|
+
savePristineCopy("hooks", path.join(hooksSrc, file));
|
|
2591
2805
|
count++;
|
|
2592
2806
|
}
|
|
2593
2807
|
// Copy hook template files (.md) and scripts (.js)
|
|
2594
2808
|
for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".md") || f.endsWith(".js"))) {
|
|
2595
2809
|
const dst = path.join(hooksDst, file);
|
|
2596
2810
|
fs.copyFileSync(path.join(hooksSrc, file), dst);
|
|
2811
|
+
savePristineCopy("hooks", path.join(hooksSrc, file));
|
|
2597
2812
|
count++;
|
|
2598
2813
|
}
|
|
2599
2814
|
|
|
@@ -4884,6 +5099,21 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4884
5099
|
return;
|
|
4885
5100
|
}
|
|
4886
5101
|
|
|
5102
|
+
// ── Confirmation gate ──
|
|
5103
|
+
barLn();
|
|
5104
|
+
const updateConfirm = await interactiveSelect(
|
|
5105
|
+
[
|
|
5106
|
+
{ id: "yes", name: "Apply updates", tier: `${totalChanged} file${totalChanged > 1 ? "s" : ""} will be updated` },
|
|
5107
|
+
{ id: "no", name: "Cancel", tier: "No changes will be made" },
|
|
5108
|
+
],
|
|
5109
|
+
{ multi: false, defaultIndex: 0 }
|
|
5110
|
+
);
|
|
5111
|
+
if (!updateConfirm || updateConfirm === "no") {
|
|
5112
|
+
outro("Update cancelled.");
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
barLn();
|
|
5116
|
+
|
|
4887
5117
|
// ── Apply safe system files directly (no user customizations in these) ──
|
|
4888
5118
|
const home = os.homedir();
|
|
4889
5119
|
let appliedCount = 0;
|
|
@@ -4894,7 +5124,9 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4894
5124
|
const hooksDest = path.join(home, ".claude", "hooks");
|
|
4895
5125
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
4896
5126
|
for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh"))) {
|
|
4897
|
-
fs.
|
|
5127
|
+
const hookContent = fs.readFileSync(path.join(hooksSrc, file), "utf8")
|
|
5128
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5129
|
+
fs.writeFileSync(path.join(hooksDest, file), hookContent, { mode: 0o755 });
|
|
4898
5130
|
try { fs.chmodSync(path.join(hooksDest, file), 0o755); } catch {}
|
|
4899
5131
|
appliedCount++;
|
|
4900
5132
|
}
|
|
@@ -4905,7 +5137,8 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4905
5137
|
const slSrc = path.join(bundleDir, "src", "hooks", "statusline.js");
|
|
4906
5138
|
const slDest = path.join(home, ".claude", "statusline.js");
|
|
4907
5139
|
if (fs.existsSync(slSrc)) {
|
|
4908
|
-
fs.
|
|
5140
|
+
const slContent = fs.readFileSync(slSrc, "utf8").replace(/\r\n/g, "\n");
|
|
5141
|
+
fs.writeFileSync(slDest, slContent, "utf8");
|
|
4909
5142
|
// Ensure settings.json has statusLine config
|
|
4910
5143
|
const globalSettings = path.join(home, ".claude", "settings.json");
|
|
4911
5144
|
try {
|
|
@@ -4945,26 +5178,189 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4945
5178
|
// Update version marker
|
|
4946
5179
|
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4947
5180
|
|
|
4948
|
-
//
|
|
4949
|
-
const
|
|
4950
|
-
|
|
4951
|
-
|
|
5181
|
+
// ── Smart Update: hash-based change detection + auto-merge ──
|
|
5182
|
+
const manifest = loadUpdateManifest();
|
|
5183
|
+
const newManifest = { version: require("./package.json").version, installedAt: new Date().toISOString(), files: {} };
|
|
5184
|
+
const updated = [], skippedFiles = [], autoMerged = [], conflicts = [], userOnly = [];
|
|
5185
|
+
let needsUpdate = false;
|
|
4952
5186
|
|
|
5187
|
+
if (!manifest) {
|
|
5188
|
+
barLn(dim(" First update with smart change detection — setting up file tracking."));
|
|
5189
|
+
barLn(dim(" Future updates will preserve your customizations automatically."));
|
|
5190
|
+
barLn();
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
// ── Workflows: smart merge per file ──
|
|
5194
|
+
const wfSrcDir = path.join(bundleDir, "src", "workflows");
|
|
5195
|
+
const wfDestDir = path.join(home, ".claude", "commands");
|
|
5196
|
+
if (fs.existsSync(wfSrcDir)) {
|
|
5197
|
+
for (const file of fs.readdirSync(wfSrcDir).filter((f) => f.endsWith(".md"))) {
|
|
5198
|
+
if (file === "update.md") continue; // already synced above
|
|
5199
|
+
const srcFile = path.join(wfSrcDir, file);
|
|
5200
|
+
const destFile = path.join(wfDestDir, file);
|
|
5201
|
+
const result = compareForUpdate("workflows", srcFile, destFile, manifest);
|
|
5202
|
+
|
|
5203
|
+
switch (result.action) {
|
|
5204
|
+
case "install":
|
|
5205
|
+
case "safe-overwrite":
|
|
5206
|
+
linkOrCopy(srcFile, destFile);
|
|
5207
|
+
savePristineCopy("workflows", srcFile);
|
|
5208
|
+
updated.push(file);
|
|
5209
|
+
break;
|
|
5210
|
+
case "user-only-skip":
|
|
5211
|
+
userOnly.push(file);
|
|
5212
|
+
break;
|
|
5213
|
+
case "skip":
|
|
5214
|
+
skippedFiles.push(file);
|
|
5215
|
+
break;
|
|
5216
|
+
case "legacy-overwrite": {
|
|
5217
|
+
const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
|
|
5218
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
5219
|
+
if (fs.existsSync(destFile)) fs.copyFileSync(destFile, path.join(backupDir, file));
|
|
5220
|
+
linkOrCopy(srcFile, destFile);
|
|
5221
|
+
savePristineCopy("workflows", srcFile);
|
|
5222
|
+
updated.push(file);
|
|
5223
|
+
break;
|
|
5224
|
+
}
|
|
5225
|
+
case "conflict": {
|
|
5226
|
+
const mergeResult = tryAutoMerge("workflows", srcFile, destFile);
|
|
5227
|
+
if (mergeResult.success) {
|
|
5228
|
+
fs.writeFileSync(destFile, mergeResult.content, "utf8");
|
|
5229
|
+
savePristineCopy("workflows", srcFile);
|
|
5230
|
+
autoMerged.push(file);
|
|
5231
|
+
} else {
|
|
5232
|
+
conflicts.push({ name: file, reason: mergeResult.reason, conflictCount: mergeResult.conflictCount });
|
|
5233
|
+
}
|
|
5234
|
+
break;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
newManifest.files[`workflows/${file}`] = result.newHash;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5241
|
+
// ── Rules: keep sentinel merge + track pristine ──
|
|
5242
|
+
if (changes.rules === "changed") {
|
|
5243
|
+
const rulesSrc = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
5244
|
+
// installRules already handles sentinel merge + pristine save
|
|
5245
|
+
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
5246
|
+
for (const agent of detectedAgents) {
|
|
5247
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5248
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5249
|
+
for (const tid of targets) {
|
|
5250
|
+
const reg = AGENT_REGISTRY[tid];
|
|
5251
|
+
if (reg && reg.rules) {
|
|
5252
|
+
const destPath = typeof reg.rules === "function" ? reg.rules() : reg.rules;
|
|
5253
|
+
installRules(bundleDir, destPath, tid);
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
if (fs.existsSync(rulesSrc)) newManifest.files["system/Mover_Global_Rules.md"] = fileContentHash(rulesSrc);
|
|
5258
|
+
updated.push("Global Rules");
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
// ── Skills: use existing hash-based install (already has .skill-manifest.json) ──
|
|
5262
|
+
const skillChanges = (changes.skills || []).filter((f) => f.status !== "unchanged");
|
|
5263
|
+
if (skillChanges.length > 0) {
|
|
5264
|
+
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
5265
|
+
const writtenFiles = new Set();
|
|
5266
|
+
for (const agent of detectedAgents) {
|
|
5267
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5268
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5269
|
+
for (const tid of targets) {
|
|
5270
|
+
const reg = AGENT_REGISTRY[tid];
|
|
5271
|
+
if (reg && reg.skills) {
|
|
5272
|
+
const destDir = typeof reg.skills === "function" ? reg.skills() : reg.skills;
|
|
5273
|
+
if (destDir && !writtenFiles.has(destDir)) {
|
|
5274
|
+
installSkills(bundleDir, destDir, null, null);
|
|
5275
|
+
writtenFiles.add(destDir);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
updated.push(`${skillChanges.length} skills`);
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
// ── Handle true conflicts → write conflicts.json for /update ──
|
|
5284
|
+
if (conflicts.length > 0) {
|
|
5285
|
+
const conflictData = conflicts.map(({ name }) => ({
|
|
5286
|
+
file: name,
|
|
5287
|
+
pristine: path.join(home, ".mover", "installed", "workflows", name),
|
|
5288
|
+
userVersion: path.join(home, ".claude", "commands", name),
|
|
5289
|
+
newVersion: path.join(bundleDir, "src", "workflows", name),
|
|
5290
|
+
}));
|
|
5291
|
+
const conflictPath = path.join(home, ".mover", "conflicts.json");
|
|
5292
|
+
fs.writeFileSync(conflictPath, JSON.stringify({
|
|
5293
|
+
version: require("./package.json").version,
|
|
5294
|
+
timestamp: new Date().toISOString(),
|
|
5295
|
+
files: conflictData,
|
|
5296
|
+
}, null, 2), "utf8");
|
|
5297
|
+
|
|
5298
|
+
// Backup conflicted user files
|
|
5299
|
+
const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
|
|
5300
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
5301
|
+
for (const { name } of conflicts) {
|
|
5302
|
+
const dest = path.join(home, ".claude", "commands", name);
|
|
5303
|
+
if (fs.existsSync(dest)) fs.copyFileSync(dest, path.join(backupDir, name));
|
|
5304
|
+
}
|
|
5305
|
+
needsUpdate = true;
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
// ── Multi-agent propagation (mirrors --quick path) ──
|
|
5309
|
+
const detectedAgentsForProp = AGENTS.filter((a) => a.detect());
|
|
5310
|
+
const propWritten = new Set();
|
|
5311
|
+
for (const agent of detectedAgentsForProp) {
|
|
5312
|
+
if (agent.id === "claude-code") continue;
|
|
5313
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
5314
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
5315
|
+
for (const tid of targets) {
|
|
5316
|
+
const fn = AGENT_INSTALLERS[tid];
|
|
5317
|
+
if (!fn || propWritten.has(tid)) continue;
|
|
5318
|
+
try {
|
|
5319
|
+
const sp = spinner(AGENT_REGISTRY[tid]?.name || agent.name);
|
|
5320
|
+
const steps = fn(bundleDir, vaultPath, { install: true, categories: null, workflows: null, statusLine: true }, propWritten, tid);
|
|
5321
|
+
sp.stop(`${AGENT_REGISTRY[tid]?.name || agent.name} ${dim(steps.join(", "))}`);
|
|
5322
|
+
} catch {}
|
|
5323
|
+
propWritten.add(tid);
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
// ── Save updated manifest ──
|
|
5328
|
+
// Carry forward entries for files we didn't process (hooks, statusline, /update itself)
|
|
5329
|
+
if (manifest && manifest.files) {
|
|
5330
|
+
for (const [key, hash] of Object.entries(manifest.files)) {
|
|
5331
|
+
if (!newManifest.files[key]) newManifest.files[key] = hash;
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
const hooksDir = path.join(bundleDir, "src", "hooks");
|
|
5335
|
+
if (fs.existsSync(hooksDir)) {
|
|
5336
|
+
for (const f of fs.readdirSync(hooksDir).filter((x) => x.endsWith(".sh") || x.endsWith(".js") || x.endsWith(".md"))) {
|
|
5337
|
+
newManifest.files[`hooks/${f}`] = fileContentHash(path.join(hooksDir, f));
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
saveUpdateManifest(newManifest);
|
|
5341
|
+
writeMoverConfig(vaultPath, selectedIds);
|
|
5342
|
+
|
|
5343
|
+
// ── Summary ──
|
|
4953
5344
|
barLn();
|
|
4954
|
-
if (
|
|
4955
|
-
|
|
5345
|
+
if (updated.length > 0) statusLine("ok", "Updated", `${updated.length} files`);
|
|
5346
|
+
if (autoMerged.length > 0) statusLine("ok", "Auto-merged", `${autoMerged.length} files (your changes + our updates combined)`);
|
|
5347
|
+
if (userOnly.length > 0) statusLine("ok", "Preserved", `${userOnly.length} customized files`);
|
|
5348
|
+
if (skippedFiles.length > 0) statusLine("ok", "Current", `${skippedFiles.length} files unchanged`);
|
|
5349
|
+
|
|
5350
|
+
if (conflicts.length > 0) {
|
|
4956
5351
|
barLn();
|
|
4957
|
-
barLn(
|
|
4958
|
-
barLn(`
|
|
4959
|
-
barLn(
|
|
4960
|
-
barLn(dim("
|
|
4961
|
-
|
|
4962
|
-
|
|
5352
|
+
barLn(yellow(` ${conflicts.length} file${conflicts.length > 1 ? "s" : ""} need intelligent merge:`));
|
|
5353
|
+
for (const { name } of conflicts) barLn(` ${yellow("!")} ${name}`);
|
|
5354
|
+
barLn();
|
|
5355
|
+
barLn(dim(" You and we both changed the same sections."));
|
|
5356
|
+
barLn(dim(" Run /update — the AI will merge your changes with ours."));
|
|
5357
|
+
barLn(dim(" Your current versions are backed up."));
|
|
4963
5358
|
}
|
|
5359
|
+
|
|
4964
5360
|
barLn();
|
|
4965
5361
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4966
|
-
if (
|
|
4967
|
-
outro(`System updated. Run ${bold("/update")}
|
|
5362
|
+
if (needsUpdate) {
|
|
5363
|
+
outro(`System updated. Run ${bold("/update")} to resolve ${conflicts.length} conflict${conflicts.length > 1 ? "s" : ""}. ${dim(`(${elapsed}s)`)}`);
|
|
4968
5364
|
} else {
|
|
4969
5365
|
outro(`${green("Done.")} All files up to date. ${dim(`(${elapsed}s)`)}`);
|
|
4970
5366
|
}
|
|
@@ -5302,8 +5698,9 @@ async function main() {
|
|
|
5302
5698
|
if (selectedIds.includes("claude-code")) {
|
|
5303
5699
|
barLn();
|
|
5304
5700
|
question("Install Claude Code status line?");
|
|
5305
|
-
barLn(dim("
|
|
5306
|
-
barLn(dim(" Example:
|
|
5701
|
+
barLn(dim(" Live status bar with model, context %, project, session cost, and Mover OS data."));
|
|
5702
|
+
barLn(dim(" Example: Opus 4.6 · 24% · my-project (main) · 2h14m · $12.50"));
|
|
5703
|
+
barLn(dim(" ▸ next task · 2/5 done · Sleep by 22:00 · logged 30m ago"));
|
|
5307
5704
|
barLn();
|
|
5308
5705
|
|
|
5309
5706
|
const slChoice = await interactiveSelect(
|
|
@@ -5430,30 +5827,33 @@ async function main() {
|
|
|
5430
5827
|
}
|
|
5431
5828
|
}
|
|
5432
5829
|
|
|
5433
|
-
// ── Settings step — let user configure before install ──
|
|
5830
|
+
// ── Settings step — let user configure before install (loops until esc) ──
|
|
5434
5831
|
{
|
|
5435
5832
|
barLn();
|
|
5436
|
-
question("Configure settings " + dim("(esc to
|
|
5833
|
+
question("Configure settings " + dim("(esc to continue with defaults)"));
|
|
5437
5834
|
barLn();
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5835
|
+
let editingSettings = true;
|
|
5836
|
+
while (editingSettings) {
|
|
5837
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
5838
|
+
let cfg = {};
|
|
5839
|
+
if (fs.existsSync(cfgPath)) { try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {} }
|
|
5840
|
+
if (!cfg.settings) cfg.settings = {};
|
|
5841
|
+
|
|
5842
|
+
const settingsItems = [
|
|
5843
|
+
{ id: "review_day", name: `review_day ${(cfg.settings.review_day || "Sunday").toString().padEnd(12)}Weekly review day` },
|
|
5844
|
+
{ id: "track_food", name: `track_food ${(cfg.settings.track_food !== undefined ? (cfg.settings.track_food ? "on" : "off") : "on").padEnd(12)}Track food in daily notes` },
|
|
5845
|
+
{ id: "track_sleep", name: `track_sleep ${(cfg.settings.track_sleep !== undefined ? (cfg.settings.track_sleep ? "on" : "off") : "on").padEnd(12)}Track sleep in daily notes` },
|
|
5846
|
+
{ id: "friction_level", name: `friction_level ${(cfg.settings.friction_level || 3).toString().padEnd(12)}Max friction level (1-4)` },
|
|
5847
|
+
];
|
|
5848
|
+
const settingsPick = await interactiveSelect(settingsItems);
|
|
5849
|
+
if (!settingsPick) { editingSettings = false; break; }
|
|
5446
5850
|
const meta = KNOWN_SETTINGS[settingsPick];
|
|
5447
5851
|
if (meta) {
|
|
5448
|
-
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
5449
|
-
let cfg = {};
|
|
5450
|
-
if (fs.existsSync(cfgPath)) { try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {} }
|
|
5451
|
-
if (!cfg.settings) cfg.settings = {};
|
|
5452
5852
|
if (meta.type === "boolean") {
|
|
5453
5853
|
cfg.settings[settingsPick] = !(cfg.settings[settingsPick] !== undefined ? cfg.settings[settingsPick] : meta.defaults);
|
|
5454
5854
|
statusLine("ok", settingsPick, cfg.settings[settingsPick] ? "on" : "off");
|
|
5455
5855
|
} else {
|
|
5456
|
-
const answer = await textInput({ label: settingsPick, initial: String(meta.defaults) });
|
|
5856
|
+
const answer = await textInput({ label: settingsPick, initial: String(cfg.settings[settingsPick] !== undefined ? cfg.settings[settingsPick] : meta.defaults) });
|
|
5457
5857
|
if (answer !== null && answer.trim() !== "") {
|
|
5458
5858
|
cfg.settings[settingsPick] = meta.type === "number" ? parseInt(answer.trim(), 10) : answer.trim();
|
|
5459
5859
|
statusLine("ok", settingsPick, JSON.stringify(cfg.settings[settingsPick]));
|
|
@@ -5465,6 +5865,39 @@ async function main() {
|
|
|
5465
5865
|
}
|
|
5466
5866
|
}
|
|
5467
5867
|
|
|
5868
|
+
// ── Confirmation gate — show what will be installed ──
|
|
5869
|
+
{
|
|
5870
|
+
barLn();
|
|
5871
|
+
question(bold("Review your selections:"));
|
|
5872
|
+
barLn();
|
|
5873
|
+
barLn(` ${bold("Vault:")} ${vaultPath}`);
|
|
5874
|
+
barLn(` ${bold("Agents:")} ${selectedAgents.map((a) => a.name).join(", ")}`);
|
|
5875
|
+
if (installSkills && selectedCategories) {
|
|
5876
|
+
barLn(` ${bold("Skills:")} ${[...selectedCategories].join(", ")}`);
|
|
5877
|
+
} else if (installSkills) {
|
|
5878
|
+
barLn(` ${bold("Skills:")} all categories`);
|
|
5879
|
+
} else {
|
|
5880
|
+
barLn(` ${bold("Skills:")} none`);
|
|
5881
|
+
}
|
|
5882
|
+
if (selectedIds.includes("claude-code")) {
|
|
5883
|
+
barLn(` ${bold("Status line:")} ${installStatusLine ? "yes" : "no"}`);
|
|
5884
|
+
}
|
|
5885
|
+
barLn(` ${bold("Prayer:")} ${prayerSetup ? "yes" : "no"}`);
|
|
5886
|
+
barLn();
|
|
5887
|
+
|
|
5888
|
+
const confirmChoice = await interactiveSelect(
|
|
5889
|
+
[
|
|
5890
|
+
{ id: "yes", name: "Install", tier: "Proceed with the selections above" },
|
|
5891
|
+
{ id: "no", name: "Cancel", tier: "Go back and start over" },
|
|
5892
|
+
],
|
|
5893
|
+
{ multi: false, defaultIndex: 0 }
|
|
5894
|
+
);
|
|
5895
|
+
if (!confirmChoice || confirmChoice === "no") {
|
|
5896
|
+
outro("Cancelled.");
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5468
5901
|
// ── Install with animated spinners ──
|
|
5469
5902
|
barLn();
|
|
5470
5903
|
question(bold("Installing..."));
|
|
@@ -5605,6 +6038,28 @@ async function main() {
|
|
|
5605
6038
|
// 9. Write ~/.mover/config.json (both fresh + update)
|
|
5606
6039
|
writeMoverConfig(vaultPath, selectedIds, key, { prayerSetup });
|
|
5607
6040
|
|
|
6041
|
+
// 10. Build update manifest (pristine hashes for smart future updates)
|
|
6042
|
+
try {
|
|
6043
|
+
const mfst = { version: VERSION, installedAt: new Date().toISOString(), files: {} };
|
|
6044
|
+
const wfDir = path.join(bundleDir, "src", "workflows");
|
|
6045
|
+
if (fs.existsSync(wfDir)) {
|
|
6046
|
+
for (const f of fs.readdirSync(wfDir).filter((x) => x.endsWith(".md"))) {
|
|
6047
|
+
mfst.files[`workflows/${f}`] = fileContentHash(path.join(wfDir, f));
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
const rulesPath = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
6051
|
+
if (fs.existsSync(rulesPath)) {
|
|
6052
|
+
mfst.files["system/Mover_Global_Rules.md"] = fileContentHash(rulesPath);
|
|
6053
|
+
}
|
|
6054
|
+
const hooksDir = path.join(bundleDir, "src", "hooks");
|
|
6055
|
+
if (fs.existsSync(hooksDir)) {
|
|
6056
|
+
for (const f of fs.readdirSync(hooksDir).filter((x) => x.endsWith(".sh") || x.endsWith(".js") || x.endsWith(".md"))) {
|
|
6057
|
+
mfst.files[`hooks/${f}`] = fileContentHash(path.join(hooksDir, f));
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
saveUpdateManifest(mfst);
|
|
6061
|
+
} catch {}
|
|
6062
|
+
|
|
5608
6063
|
barLn();
|
|
5609
6064
|
|
|
5610
6065
|
// ── Done ──
|