mover-os 4.4.3 → 4.5.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/README.md +1 -1
- package/install.js +863 -627
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -89,8 +89,28 @@ function waveGradient(text, frame, totalFrames) {
|
|
|
89
89
|
}).join("") + S.reset;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// ─── TUI: Alternate screen buffer ────────────────────────────────────────────
|
|
93
|
+
let _inAltScreen = false;
|
|
94
|
+
function enterAltScreen() {
|
|
95
|
+
if (!IS_TTY || _inAltScreen) return;
|
|
96
|
+
w("\x1b[?1049h"); // Enter alternate screen
|
|
97
|
+
w("\x1b[2J\x1b[H"); // Clear + cursor home
|
|
98
|
+
w(S.hide);
|
|
99
|
+
_inAltScreen = true;
|
|
100
|
+
}
|
|
101
|
+
function exitAltScreen() {
|
|
102
|
+
if (!IS_TTY || !_inAltScreen) return;
|
|
103
|
+
w("\x1b[?1049l"); // Restore original screen
|
|
104
|
+
w(S.show);
|
|
105
|
+
_inAltScreen = false;
|
|
106
|
+
}
|
|
107
|
+
function clearContent() {
|
|
108
|
+
w("\x1b[2J\x1b[H"); // Clear screen + cursor to top
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
// ─── Terminal cleanup ────────────────────────────────────────────────────────
|
|
93
112
|
function cleanup() {
|
|
113
|
+
exitAltScreen();
|
|
94
114
|
w(S.show);
|
|
95
115
|
try { if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false); } catch {}
|
|
96
116
|
}
|
|
@@ -252,6 +272,127 @@ function statusLine(icon, label, detail = "") {
|
|
|
252
272
|
barLn(`${iconMap[icon] || icon} ${label}${detail ? ` ${dim(detail)}` : ""}`);
|
|
253
273
|
}
|
|
254
274
|
|
|
275
|
+
// ─── TUI: Compact header ────────────────────────────────────────────────────
|
|
276
|
+
function compactHeader() {
|
|
277
|
+
const pkgVer = require("./package.json").version;
|
|
278
|
+
let info = "";
|
|
279
|
+
try {
|
|
280
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
281
|
+
if (fs.existsSync(cfgPath)) {
|
|
282
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (cfg.vaultPath) parts.push(path.basename(cfg.vaultPath));
|
|
285
|
+
if (cfg.agents?.length) parts.push(`${cfg.agents.length} agents`);
|
|
286
|
+
info = parts.length ? `${S.gray} · ${parts.join(" · ")}${S.reset}` : "";
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
return ` ${S.bold}moveros${S.reset} ${S.dim}v${pkgVer}${S.reset}${info}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── TUI: Screen transitions ────────────────────────────────────────────────
|
|
293
|
+
async function wipeDown(lineCount) {
|
|
294
|
+
if (!IS_TTY) return;
|
|
295
|
+
const rows = process.stdout.rows || 24;
|
|
296
|
+
const count = Math.min(lineCount || rows, rows);
|
|
297
|
+
w("\x1b[H");
|
|
298
|
+
for (let i = 0; i < count; i++) {
|
|
299
|
+
w(`\x1b[${i + 1};1H\x1b[2K`);
|
|
300
|
+
await sleep(8);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function fadeOut() {
|
|
305
|
+
if (!IS_TTY) return;
|
|
306
|
+
const rows = process.stdout.rows || 24;
|
|
307
|
+
for (let i = 1; i <= rows; i++) {
|
|
308
|
+
w(`\x1b[${i};1H\x1b[2K`);
|
|
309
|
+
if (i % 3 === 0) await sleep(6);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function slideIn(lines) {
|
|
314
|
+
if (!IS_TTY) { lines.forEach(l => ln(l)); return; }
|
|
315
|
+
for (const line of lines) {
|
|
316
|
+
ln(line);
|
|
317
|
+
await sleep(12);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function glowPulse(text, x, y) {
|
|
322
|
+
if (!IS_TTY) { ln(text); return; }
|
|
323
|
+
const frames = [255, 252, 255, 250, 248, 246];
|
|
324
|
+
for (const brightness of frames) {
|
|
325
|
+
if (y) w(`\x1b[${y};${x}H`);
|
|
326
|
+
w(`\x1b[2K${S.fg(brightness)}${strip(text)}${S.reset}`);
|
|
327
|
+
await sleep(40);
|
|
328
|
+
}
|
|
329
|
+
if (y) w(`\x1b[${y};${x}H`);
|
|
330
|
+
w(`\x1b[2K${text}\n`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function shimmerText(text, duration = 400) {
|
|
334
|
+
if (!IS_TTY) { ln(text); return; }
|
|
335
|
+
const totalFrames = Math.floor(duration / 20);
|
|
336
|
+
const raw = strip(text);
|
|
337
|
+
for (let f = 0; f <= totalFrames; f++) {
|
|
338
|
+
w(`\r\x1b[2K ${waveGradient(raw, f, totalFrames)}`);
|
|
339
|
+
await sleep(20);
|
|
340
|
+
}
|
|
341
|
+
w(`\r\x1b[2K ${text}\n`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── TUI: waitForEsc ────────────────────────────────────────────────────────
|
|
345
|
+
function waitForEsc() {
|
|
346
|
+
return new Promise((r) => {
|
|
347
|
+
if (!IS_TTY) { r(); return; }
|
|
348
|
+
const { stdin } = process;
|
|
349
|
+
stdin.setRawMode(true);
|
|
350
|
+
stdin.resume();
|
|
351
|
+
stdin.setEncoding("utf8");
|
|
352
|
+
const h = (data) => {
|
|
353
|
+
if (data === "\x1b" || data === "\r" || data === "\n" || data === "\x03") {
|
|
354
|
+
stdin.removeListener("data", h);
|
|
355
|
+
stdin.setRawMode(false);
|
|
356
|
+
stdin.pause();
|
|
357
|
+
if (data === "\x03") { cleanup(); ln(); process.exit(0); }
|
|
358
|
+
r();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
stdin.on("data", h);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── TUI: Animated progress for install/update ──────────────────────────────
|
|
366
|
+
async function installProgress(steps) {
|
|
367
|
+
const total = steps.length;
|
|
368
|
+
for (let i = 0; i < total; i++) {
|
|
369
|
+
const step = steps[i];
|
|
370
|
+
w(`\x1b[2K\r ${progressBar(i, total, 30)} ${S.dim}${step.label}${S.reset}`);
|
|
371
|
+
await step.fn();
|
|
372
|
+
w(`\x1b[2K\r ${progressBar(i + 1, total, 30)} ${S.green}\u2713${S.reset} ${step.label}\n`);
|
|
373
|
+
await sleep(60);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── TUI: Success animation ─────────────────────────────────────────────────
|
|
378
|
+
async function successAnimation(message) {
|
|
379
|
+
if (!IS_TTY) { ln(message); return; }
|
|
380
|
+
const check = [
|
|
381
|
+
" \u2713",
|
|
382
|
+
" \u2713",
|
|
383
|
+
" \u2713",
|
|
384
|
+
" \u2713 \u2713",
|
|
385
|
+
" \u2713",
|
|
386
|
+
];
|
|
387
|
+
ln();
|
|
388
|
+
for (const line of check) {
|
|
389
|
+
await shimmerText(green(line), 200);
|
|
390
|
+
}
|
|
391
|
+
ln();
|
|
392
|
+
await shimmerText(bold(message), 400);
|
|
393
|
+
ln();
|
|
394
|
+
}
|
|
395
|
+
|
|
255
396
|
// ─── Clack-style frame ──────────────────────────────────────────────────────
|
|
256
397
|
const BAR_COLOR = S.cyan;
|
|
257
398
|
const bar = () => w(`${BAR_COLOR}│${S.reset}`);
|
|
@@ -421,10 +562,12 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
|
|
|
421
562
|
|
|
422
563
|
const icon = multi
|
|
423
564
|
? (checked ? green("◼") : dim("◻"))
|
|
424
|
-
: (active ?
|
|
425
|
-
const label = active
|
|
565
|
+
: (active ? `${S.bold}${S.fg(255)}›${S.reset}` : dim(" "));
|
|
566
|
+
const label = active
|
|
567
|
+
? `${S.bold}${S.fg(255)}${item.name}${S.reset}`
|
|
568
|
+
: `${S.fg(245)}${item.name}${S.reset}`;
|
|
426
569
|
const padded = strip(label).padEnd(20);
|
|
427
|
-
const styledPadded = active ? bold(padded
|
|
570
|
+
const styledPadded = active ? `${S.bold}${S.fg(255)}${padded}${S.reset}` : `${S.fg(245)}${padded}${S.reset}`;
|
|
428
571
|
const tag = item._detected ? dim("(detected)") : "";
|
|
429
572
|
|
|
430
573
|
w(`\x1b[2K${BAR_COLOR}│${S.reset} ${icon} ${styledPadded}${tag}\n`);
|
|
@@ -655,14 +798,69 @@ async function downloadPayload(key) {
|
|
|
655
798
|
return tmpDir;
|
|
656
799
|
}
|
|
657
800
|
|
|
801
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
802
|
+
function getSessionDate() {
|
|
803
|
+
const now = new Date();
|
|
804
|
+
if (now.getHours() < 5) return new Date(now.getTime() - 86400000);
|
|
805
|
+
return now;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function resolveDailyNotePath(engineDir, date) {
|
|
809
|
+
if (!date) date = getSessionDate();
|
|
810
|
+
const yyyy = date.getFullYear();
|
|
811
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
812
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
813
|
+
const ymd = `${yyyy}-${mm}-${dd}`;
|
|
814
|
+
// Primary: month subfolder (Mover OS convention)
|
|
815
|
+
const monthPath = path.join(engineDir, "Dailies", `${yyyy}-${mm}`, `Daily - ${ymd}.md`);
|
|
816
|
+
if (fs.existsSync(monthPath)) return monthPath;
|
|
817
|
+
// Fallback: flat Dailies folder (older vaults)
|
|
818
|
+
const flatPath = path.join(engineDir, "Dailies", `Daily - ${ymd}.md`);
|
|
819
|
+
if (fs.existsSync(flatPath)) return flatPath;
|
|
820
|
+
return monthPath; // default for creation
|
|
821
|
+
}
|
|
822
|
+
|
|
658
823
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
824
|
+
// ─── Frecency Tracking ──────────────────────────────────────────────────────
|
|
825
|
+
function loadCliUsage() {
|
|
826
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
827
|
+
if (!fs.existsSync(cfgPath)) return {};
|
|
828
|
+
try {
|
|
829
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
830
|
+
return cfg.cli_usage || {};
|
|
831
|
+
} catch { return {}; }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function recordCliUsage(cmd) {
|
|
835
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
836
|
+
try {
|
|
837
|
+
const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, "utf8")) : {};
|
|
838
|
+
if (!cfg.cli_usage) cfg.cli_usage = {};
|
|
839
|
+
const entry = cfg.cli_usage[cmd] || { count: 0, last: 0 };
|
|
840
|
+
entry.count++;
|
|
841
|
+
entry.last = Date.now();
|
|
842
|
+
cfg.cli_usage[cmd] = entry;
|
|
843
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
844
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
845
|
+
} catch {}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function frecencyScore(usage, cmd) {
|
|
849
|
+
const e = usage[cmd];
|
|
850
|
+
if (!e || !e.count) return 0;
|
|
851
|
+
const ageHours = (Date.now() - (e.last || 0)) / 3600000;
|
|
852
|
+
const recency = ageHours < 1 ? 4 : ageHours < 24 ? 3 : ageHours < 168 ? 2 : 1;
|
|
853
|
+
return e.count * recency;
|
|
854
|
+
}
|
|
855
|
+
|
|
659
856
|
// ─── CLI Commands ────────────────────────────────────────────────────────────
|
|
660
857
|
const CLI_COMMANDS = {
|
|
661
|
-
install:
|
|
662
|
-
update:
|
|
858
|
+
install: { desc: "Fresh install wizard", alias: [] },
|
|
859
|
+
update: { desc: "Comprehensive update — agents, rules, skills", alias: ["-u"] },
|
|
860
|
+
uninstall: { desc: "Remove Mover OS from vault and agents", alias: [] },
|
|
663
861
|
doctor: { desc: "Health check across all installed agents", alias: [] },
|
|
664
862
|
pulse: { desc: "Terminal dashboard — energy, tasks, streaks",alias: [] },
|
|
665
|
-
warm
|
|
863
|
+
// warm removed — hooks + rules + /morning already handle session priming
|
|
666
864
|
capture: { desc: "Quick capture — tasks, links, ideas", alias: [] },
|
|
667
865
|
who: { desc: "Entity memory lookup", alias: [] },
|
|
668
866
|
diff: { desc: "Engine file evolution viewer", alias: [] },
|
|
@@ -743,8 +941,8 @@ function detectObsidianVaults() {
|
|
|
743
941
|
}
|
|
744
942
|
|
|
745
943
|
function compareVersions(a, b) {
|
|
746
|
-
const pa = a.split(".").map(Number);
|
|
747
|
-
const pb = b.split(".").map(Number);
|
|
944
|
+
const pa = String(a).replace(/^[vV]/, "").split(".").map(Number);
|
|
945
|
+
const pb = String(b).replace(/^[vV]/, "").split(".").map(Number);
|
|
748
946
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
749
947
|
const va = pa[i] || 0, vb = pb[i] || 0;
|
|
750
948
|
if (va > vb) return 1;
|
|
@@ -1573,7 +1771,7 @@ Stuck: /debug-resistance
|
|
|
1573
1771
|
|
|
1574
1772
|
## CLI Utility
|
|
1575
1773
|
|
|
1576
|
-
\`moveros\` provides system-level terminal operations: pulse (dashboard),
|
|
1774
|
+
\`moveros\` provides system-level terminal operations: pulse (dashboard), sync (update agents), doctor (health check), who (entity lookup), capture (quick inbox). Use native agent capabilities first — the CLI supplements, never replaces.
|
|
1577
1775
|
`;
|
|
1578
1776
|
}
|
|
1579
1777
|
|
|
@@ -2715,7 +2913,7 @@ function preflight() {
|
|
|
2715
2913
|
// ─── CLI Command Handlers (stubs — implemented progressively) ────────────────
|
|
2716
2914
|
const CLI_HANDLERS = {
|
|
2717
2915
|
pulse: async (opts) => { await cmdPulse(opts); },
|
|
2718
|
-
warm
|
|
2916
|
+
// warm removed
|
|
2719
2917
|
capture: async (opts) => { await cmdCapture(opts); },
|
|
2720
2918
|
who: async (opts) => { await cmdWho(opts); },
|
|
2721
2919
|
diff: async (opts) => { await cmdDiff(opts); },
|
|
@@ -2725,10 +2923,11 @@ const CLI_HANDLERS = {
|
|
|
2725
2923
|
settings: async (opts) => { await cmdSettings(opts); },
|
|
2726
2924
|
backup: async (opts) => { await cmdBackup(opts); },
|
|
2727
2925
|
restore: async (opts) => { await cmdRestore(opts); },
|
|
2728
|
-
doctor:
|
|
2729
|
-
prayer:
|
|
2730
|
-
help:
|
|
2731
|
-
|
|
2926
|
+
doctor: async (opts) => { await cmdDoctor(opts); },
|
|
2927
|
+
prayer: async (opts) => { await cmdPrayer(opts); },
|
|
2928
|
+
help: async (opts) => { await cmdHelp(opts); },
|
|
2929
|
+
uninstall: async (opts) => { await runUninstall(resolveVaultPath(opts.vault)); },
|
|
2930
|
+
test: async (opts) => { await cmdTest(opts); },
|
|
2732
2931
|
};
|
|
2733
2932
|
|
|
2734
2933
|
// ─── moveros doctor ─────────────────────────────────────────────────────────
|
|
@@ -2859,10 +3058,7 @@ async function cmdPulse(opts) {
|
|
|
2859
3058
|
|
|
2860
3059
|
// Today's tasks from Daily Note
|
|
2861
3060
|
barLn();
|
|
2862
|
-
const
|
|
2863
|
-
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2864
|
-
const month = ymd.substring(0, 7);
|
|
2865
|
-
const dailyPath = path.join(engineDir, "Dailies", month, `Daily - ${ymd}.md`);
|
|
3061
|
+
const dailyPath = resolveDailyNotePath(engineDir);
|
|
2866
3062
|
if (fs.existsSync(dailyPath)) {
|
|
2867
3063
|
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
2868
3064
|
const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##|\n---)/i);
|
|
@@ -2917,91 +3113,25 @@ async function cmdPulse(opts) {
|
|
|
2917
3113
|
statusLine(streak >= 3 ? "ok" : "info", "Daily streak", `${streak} day${streak !== 1 ? "s" : ""}`);
|
|
2918
3114
|
}
|
|
2919
3115
|
|
|
2920
|
-
// Last session log time
|
|
2921
|
-
const
|
|
2922
|
-
if (fs.existsSync(
|
|
3116
|
+
// Last session log time — read from Active_Context.md Workflow State
|
|
3117
|
+
const acLogPath = path.join(engineDir, "Active_Context.md");
|
|
3118
|
+
if (fs.existsSync(acLogPath)) {
|
|
2923
3119
|
try {
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3120
|
+
const acContent = fs.readFileSync(acLogPath, "utf8");
|
|
3121
|
+
const logMatch = acContent.match(/log_last_run:\s*(\S+)/);
|
|
3122
|
+
if (logMatch) {
|
|
3123
|
+
const agoMs = Date.now() - new Date(logMatch[1]).getTime();
|
|
3124
|
+
const agoMin = Math.round(agoMs / 60000);
|
|
3125
|
+
const agoH = Math.round(agoMs / 3600000);
|
|
3126
|
+
const agoStr = agoMin < 60 ? `${agoMin}m ago` : `${agoH}h ago`;
|
|
3127
|
+
statusLine(agoH < 8 ? "ok" : agoH < 24 ? "info" : "warn", "Last /log", agoStr);
|
|
2928
3128
|
}
|
|
2929
3129
|
} catch {}
|
|
2930
3130
|
}
|
|
2931
3131
|
barLn();
|
|
2932
3132
|
}
|
|
2933
3133
|
|
|
2934
|
-
//
|
|
2935
|
-
async function cmdWarm(opts) {
|
|
2936
|
-
const vault = resolveVaultPath(opts.vault);
|
|
2937
|
-
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2938
|
-
const agent = opts.rest[0] || "claude";
|
|
2939
|
-
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
2940
|
-
const home = os.homedir();
|
|
2941
|
-
|
|
2942
|
-
// Build context primer
|
|
2943
|
-
const sections = [];
|
|
2944
|
-
sections.push("# Session Context Primer");
|
|
2945
|
-
sections.push(`Generated: ${new Date().toISOString()}\n`);
|
|
2946
|
-
|
|
2947
|
-
// Active Context snapshot
|
|
2948
|
-
const acPath = path.join(engineDir, "Active_Context.md");
|
|
2949
|
-
if (fs.existsSync(acPath)) {
|
|
2950
|
-
const ac = fs.readFileSync(acPath, "utf8");
|
|
2951
|
-
// Extract key sections (first 2000 chars)
|
|
2952
|
-
sections.push("## Active Context\n" + ac.substring(0, 2000));
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
// Current project state
|
|
2956
|
-
const cwd = process.cwd();
|
|
2957
|
-
const planPath = path.join(cwd, "dev", "plan.md");
|
|
2958
|
-
if (fs.existsSync(planPath)) {
|
|
2959
|
-
const plan = fs.readFileSync(planPath, "utf8");
|
|
2960
|
-
// Find last active phase
|
|
2961
|
-
const phases = plan.match(/### Phase \d+[\s\S]*?(?=### Phase|\Z)/g);
|
|
2962
|
-
if (phases) {
|
|
2963
|
-
const active = phases[phases.length - 1].substring(0, 1500);
|
|
2964
|
-
sections.push("## Current Plan (last phase)\n" + active);
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
// Today's daily note tasks
|
|
2969
|
-
const now = new Date();
|
|
2970
|
-
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2971
|
-
const dailyPath = path.join(engineDir, "Dailies", ymd.substring(0, 7), `Daily - ${ymd}.md`);
|
|
2972
|
-
if (fs.existsSync(dailyPath)) {
|
|
2973
|
-
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
2974
|
-
const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##)/i);
|
|
2975
|
-
if (taskSection) sections.push("## Today's Tasks\n" + taskSection[0]);
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
const primer = sections.join("\n\n---\n\n") + "\n";
|
|
2979
|
-
|
|
2980
|
-
// Write to agent-specific location
|
|
2981
|
-
const targets = {
|
|
2982
|
-
claude: path.join(home, ".claude", "tmp", "session-primer.md"),
|
|
2983
|
-
cursor: path.join(vault, ".cursor", "rules", "session-primer.mdc"),
|
|
2984
|
-
gemini: path.join(home, ".gemini", "session-primer.md"),
|
|
2985
|
-
codex: path.join(home, ".codex", "skills", "session-primer", "SKILL.md"),
|
|
2986
|
-
windsurf: path.join(vault, ".windsurf", "rules", "session-primer.md"),
|
|
2987
|
-
cline: path.join(vault, ".clinerules", "session-primer.md"),
|
|
2988
|
-
};
|
|
2989
|
-
|
|
2990
|
-
const dest = targets[agent] || targets.claude;
|
|
2991
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2992
|
-
|
|
2993
|
-
let content = primer;
|
|
2994
|
-
if (agent === "cursor") {
|
|
2995
|
-
content = `---\ndescription: "Session context primer — auto-generated by moveros warm"\nglobs:\nalwaysApply: true\n---\n\n${primer}`;
|
|
2996
|
-
} else if (agent === "codex") {
|
|
2997
|
-
content = `---\nname: session-primer\ndescription: "Auto-generated session context from moveros warm"\n---\n\n${primer}`;
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
fs.writeFileSync(dest, content, "utf8");
|
|
3001
|
-
statusLine("ok", "Warm", `${agent} primer written`);
|
|
3002
|
-
barLn(dim(` ${dest}`));
|
|
3003
|
-
barLn();
|
|
3004
|
-
}
|
|
3134
|
+
// cmdWarm removed — hooks + rules + /morning handle session priming
|
|
3005
3135
|
|
|
3006
3136
|
// ─── moveros capture ────────────────────────────────────────────────────────
|
|
3007
3137
|
async function cmdCapture(opts) {
|
|
@@ -3011,7 +3141,19 @@ async function cmdCapture(opts) {
|
|
|
3011
3141
|
const now = new Date();
|
|
3012
3142
|
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
3013
3143
|
const ts = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
3014
|
-
|
|
3144
|
+
|
|
3145
|
+
// Find existing capture file — prefer today's, fallback to most recent, create new if none
|
|
3146
|
+
const inboxDir = path.join(vault, "00_Inbox");
|
|
3147
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
3148
|
+
const existingCaptures = fs.readdirSync(inboxDir)
|
|
3149
|
+
.filter(f => f.toLowerCase().startsWith("capture") && f.endsWith(".md"))
|
|
3150
|
+
.sort();
|
|
3151
|
+
const todayCapture = existingCaptures.find(f => f.includes(ymd));
|
|
3152
|
+
const capturePath = todayCapture
|
|
3153
|
+
? path.join(inboxDir, todayCapture)
|
|
3154
|
+
: existingCaptures.length > 0
|
|
3155
|
+
? path.join(inboxDir, existingCaptures[existingCaptures.length - 1])
|
|
3156
|
+
: path.join(inboxDir, `Capture - ${ymd}.md`);
|
|
3015
3157
|
|
|
3016
3158
|
// Determine type from flags
|
|
3017
3159
|
let type = null, content = "";
|
|
@@ -3057,7 +3199,6 @@ async function cmdCapture(opts) {
|
|
|
3057
3199
|
else entry = `- ${content} *(${ts})*`;
|
|
3058
3200
|
|
|
3059
3201
|
// Append to capture file
|
|
3060
|
-
fs.mkdirSync(path.dirname(capturePath), { recursive: true });
|
|
3061
3202
|
if (!fs.existsSync(capturePath)) {
|
|
3062
3203
|
fs.writeFileSync(capturePath, `# Capture — ${ymd}\n\n${entry}\n`, "utf8");
|
|
3063
3204
|
} else {
|
|
@@ -3263,13 +3404,20 @@ async function cmdReplay(opts) {
|
|
|
3263
3404
|
const vault = resolveVaultPath(opts.vault);
|
|
3264
3405
|
if (!vault) { barLn(red("No vault found.")); return; }
|
|
3265
3406
|
|
|
3407
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
3266
3408
|
let dateStr = opts.rest.find((a) => a.startsWith("--date="))?.split("=")[1];
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3409
|
+
let targetDate;
|
|
3410
|
+
if (dateStr) {
|
|
3411
|
+
const [y, m, d] = dateStr.split("-").map(Number);
|
|
3412
|
+
targetDate = new Date(y, m - 1, d);
|
|
3413
|
+
} else {
|
|
3414
|
+
targetDate = getSessionDate();
|
|
3415
|
+
const y = targetDate.getFullYear();
|
|
3416
|
+
const m = String(targetDate.getMonth() + 1).padStart(2, "0");
|
|
3417
|
+
const d = String(targetDate.getDate()).padStart(2, "0");
|
|
3418
|
+
dateStr = `${y}-${m}-${d}`;
|
|
3270
3419
|
}
|
|
3271
|
-
const
|
|
3272
|
-
const dailyPath = path.join(vault, "02_Areas", "Engine", "Dailies", month, `Daily - ${dateStr}.md`);
|
|
3420
|
+
const dailyPath = resolveDailyNotePath(engineDir, targetDate);
|
|
3273
3421
|
|
|
3274
3422
|
barLn(bold(` Session Replay — ${dateStr}`));
|
|
3275
3423
|
barLn();
|
|
@@ -3646,6 +3794,19 @@ async function cmdPrayer(opts) {
|
|
|
3646
3794
|
}
|
|
3647
3795
|
|
|
3648
3796
|
// ─── moveros settings ───────────────────────────────────────────────────────
|
|
3797
|
+
const KNOWN_SETTINGS = {
|
|
3798
|
+
review_day: { type: "string", desc: "Weekly review day", defaults: "Sunday" },
|
|
3799
|
+
evening_zone_start: { type: "number", desc: "Evening starts at (hour)", defaults: 18 },
|
|
3800
|
+
midnight_cutoff: { type: "number", desc: "Before this hour = yesterday", defaults: 5 },
|
|
3801
|
+
deep_work_hours: { type: "number", desc: "Daily deep work target (hours)", defaults: 8 },
|
|
3802
|
+
sleep_target: { type: "number", desc: "Sleep target (hours)", defaults: 8 },
|
|
3803
|
+
track_food: { type: "boolean", desc: "Track food in daily notes", defaults: true },
|
|
3804
|
+
track_sleep: { type: "boolean", desc: "Track sleep in daily notes", defaults: true },
|
|
3805
|
+
show_prayer_times: { type: "boolean", desc: "Show prayer times in statusline", defaults: false },
|
|
3806
|
+
backup_retention: { type: "number", desc: "Max backups to keep", defaults: 5 },
|
|
3807
|
+
friction_level: { type: "number", desc: "Max friction level (1-4)", defaults: 3 },
|
|
3808
|
+
};
|
|
3809
|
+
|
|
3649
3810
|
async function cmdSettings(opts) {
|
|
3650
3811
|
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
3651
3812
|
|
|
@@ -3654,17 +3815,15 @@ async function cmdSettings(opts) {
|
|
|
3654
3815
|
return;
|
|
3655
3816
|
}
|
|
3656
3817
|
|
|
3657
|
-
|
|
3818
|
+
let cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
3658
3819
|
|
|
3659
|
-
// moveros settings set <key> <value>
|
|
3820
|
+
// moveros settings set <key> <value> — CLI mode
|
|
3660
3821
|
if (opts.rest[0] === "set" && opts.rest[1]) {
|
|
3661
3822
|
const key = opts.rest[1];
|
|
3662
3823
|
let val = opts.rest.slice(2).join(" ");
|
|
3663
|
-
// Auto-type conversion
|
|
3664
3824
|
if (val === "true") val = true;
|
|
3665
3825
|
else if (val === "false") val = false;
|
|
3666
3826
|
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
3667
|
-
|
|
3668
3827
|
if (!cfg.settings) cfg.settings = {};
|
|
3669
3828
|
cfg.settings[key] = val;
|
|
3670
3829
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
@@ -3672,23 +3831,51 @@ async function cmdSettings(opts) {
|
|
|
3672
3831
|
return;
|
|
3673
3832
|
}
|
|
3674
3833
|
|
|
3675
|
-
//
|
|
3834
|
+
// Interactive mode — show settings as a selectable list
|
|
3676
3835
|
barLn(bold(" Settings"));
|
|
3677
3836
|
barLn();
|
|
3678
3837
|
barLn(` ${dim("Vault:")} ${cfg.vaultPath || dim("not set")}`);
|
|
3679
3838
|
barLn(` ${dim("Key:")} ${cfg.licenseKey ? cyan(cfg.licenseKey.substring(0, 12) + "...") : dim("not set")}`);
|
|
3680
3839
|
barLn(` ${dim("Agents:")} ${(cfg.agents || []).join(", ") || dim("none")}`);
|
|
3681
|
-
barLn(` ${dim("Version:")} ${cfg.version || dim("unknown")}`);
|
|
3682
3840
|
barLn();
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3841
|
+
|
|
3842
|
+
const settings = cfg.settings || {};
|
|
3843
|
+
const items = Object.entries(KNOWN_SETTINGS).map(([key, meta]) => {
|
|
3844
|
+
const current = settings[key] !== undefined ? settings[key] : meta.defaults;
|
|
3845
|
+
const display = typeof current === "boolean" ? (current ? green("on") : red("off")) : String(current);
|
|
3846
|
+
return {
|
|
3847
|
+
id: key,
|
|
3848
|
+
name: `${key.padEnd(22)} ${display.padEnd(8)} ${dim(meta.desc)}`,
|
|
3849
|
+
tier: `Current: ${JSON.stringify(current)} Default: ${JSON.stringify(meta.defaults)}`,
|
|
3850
|
+
};
|
|
3851
|
+
});
|
|
3852
|
+
|
|
3853
|
+
question(dim(" Select a setting to edit (esc to go back)"));
|
|
3854
|
+
barLn();
|
|
3855
|
+
const picked = await interactiveSelect(items);
|
|
3856
|
+
if (!picked) return;
|
|
3857
|
+
|
|
3858
|
+
const meta = KNOWN_SETTINGS[picked];
|
|
3859
|
+
const current = settings[picked] !== undefined ? settings[picked] : meta.defaults;
|
|
3860
|
+
|
|
3861
|
+
if (meta.type === "boolean") {
|
|
3862
|
+
// Toggle immediately
|
|
3863
|
+
const newVal = !current;
|
|
3864
|
+
if (!cfg.settings) cfg.settings = {};
|
|
3865
|
+
cfg.settings[picked] = newVal;
|
|
3866
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
3867
|
+
statusLine("ok", picked, newVal ? "on" : "off");
|
|
3868
|
+
} else {
|
|
3869
|
+
// Prompt for new value
|
|
3870
|
+
const answer = await textInput({ label: `${picked}`, initial: String(current) });
|
|
3871
|
+
if (answer === null || answer.trim() === "" || answer === String(current)) return;
|
|
3872
|
+
let val = answer.trim();
|
|
3873
|
+
if (meta.type === "number") val = parseInt(val, 10);
|
|
3874
|
+
if (!cfg.settings) cfg.settings = {};
|
|
3875
|
+
cfg.settings[picked] = val;
|
|
3876
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
3877
|
+
statusLine("ok", picked, JSON.stringify(val));
|
|
3689
3878
|
}
|
|
3690
|
-
barLn(dim(" Edit: moveros settings set <key> <value>"));
|
|
3691
|
-
barLn(dim(` File: ${cfgPath}`));
|
|
3692
3879
|
barLn();
|
|
3693
3880
|
}
|
|
3694
3881
|
|
|
@@ -4046,9 +4233,6 @@ async function cmdHelp(opts) {
|
|
|
4046
4233
|
` ${cyan("who")} Entity lookup — search People, Orgs, Places`,
|
|
4047
4234
|
` ${dim("moveros who \"Ishaaq\" → everything you know about them")}`,
|
|
4048
4235
|
"",
|
|
4049
|
-
` ${cyan("warm")} Pre-warm an AI session with fresh context`,
|
|
4050
|
-
` ${dim("Run before opening Claude/Cursor. Eliminates cold start.")}`,
|
|
4051
|
-
"",
|
|
4052
4236
|
` ${cyan("prayer")} Mosque timetable — next prayer in your status line`,
|
|
4053
4237
|
` ${dim("Paste or fetch. Shows countdown. Optional.")}`,
|
|
4054
4238
|
],
|
|
@@ -4216,113 +4400,135 @@ async function cmdHelp(opts) {
|
|
|
4216
4400
|
// ─── moveros test (dev only) ────────────────────────────────────────────────
|
|
4217
4401
|
async function cmdTest(opts) { barLn(yellow("moveros test — not yet implemented.")); }
|
|
4218
4402
|
|
|
4219
|
-
// ───
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
"
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4403
|
+
// ─── What's New ──────────────────────────────────────────────────────────────
|
|
4404
|
+
const WHATS_NEW = {
|
|
4405
|
+
"4.2.0": {
|
|
4406
|
+
headline: "Engine Intelligence + Pattern Detection",
|
|
4407
|
+
features: [
|
|
4408
|
+
"Tiered workflow reads — Active_Context as cache, deep reads only when needed",
|
|
4409
|
+
"Active Patterns section in Active_Context — AI-detected behavioral patterns",
|
|
4410
|
+
"Identity Snapshot cached in Active_Context — no full file reads for light workflows",
|
|
4411
|
+
"Monthly review overdue detection in /review-week",
|
|
4412
|
+
"Critical Path Check in /analyse-day (DIRECT/PREREQUISITE/DRIFT)",
|
|
4413
|
+
],
|
|
4414
|
+
},
|
|
4415
|
+
"4.3.0": {
|
|
4416
|
+
headline: "Soul + Friction + Skills",
|
|
4417
|
+
features: [
|
|
4418
|
+
"Soul personality — sharp observations, humor, callbacks, earned respect",
|
|
4419
|
+
"Level 3 friction (Earn It) — AI holds the line on avoidance",
|
|
4420
|
+
"Pre-Escalation Gate — compound work detection, no false positives",
|
|
4421
|
+
"Curated skill packs — marketing, CRO, SEO, design, obsidian",
|
|
4422
|
+
"Skill evaluation system — A/B testing skills with benchmarks",
|
|
4423
|
+
],
|
|
4424
|
+
},
|
|
4425
|
+
"4.5.0": {
|
|
4426
|
+
headline: "Smart Menu + TUI + Comprehensive Update",
|
|
4427
|
+
features: [
|
|
4428
|
+
"Frecency-based menu — surfaces your most-used commands",
|
|
4429
|
+
"Interactive settings — toggle from the menu, no CLI flags",
|
|
4430
|
+
"Full-screen TUI — alternate screen buffer, clean transitions",
|
|
4431
|
+
"Comprehensive update — backup, agent management, What's New",
|
|
4432
|
+
"Install/Update separation — install is fresh only, update is interactive",
|
|
4433
|
+
"Uninstall as standalone command",
|
|
4434
|
+
],
|
|
4435
|
+
},
|
|
4436
|
+
};
|
|
4235
4437
|
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
const
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4438
|
+
function showWhatsNew(fromVer, toVer) {
|
|
4439
|
+
if (!fromVer || !toVer) return;
|
|
4440
|
+
const versions = Object.keys(WHATS_NEW)
|
|
4441
|
+
.filter(v => compareVersions(v, fromVer) > 0 && compareVersions(v, toVer) <= 0)
|
|
4442
|
+
.sort(compareVersions);
|
|
4443
|
+
if (versions.length === 0) return;
|
|
4444
|
+
|
|
4445
|
+
for (const v of versions) {
|
|
4446
|
+
const wn = WHATS_NEW[v];
|
|
4447
|
+
question(bold(`What's New in v${v}`));
|
|
4448
|
+
barLn(dim(wn.headline));
|
|
4449
|
+
barLn();
|
|
4450
|
+
for (const f of wn.features) {
|
|
4451
|
+
barLn(` ${green("+")} ${f}`);
|
|
4452
|
+
}
|
|
4453
|
+
barLn();
|
|
4242
4454
|
}
|
|
4243
|
-
return choice;
|
|
4244
4455
|
}
|
|
4245
4456
|
|
|
4246
|
-
// ───
|
|
4247
|
-
function
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
|
|
4251
|
-
return path.resolve(v);
|
|
4252
|
-
}
|
|
4253
|
-
// Try config.json
|
|
4457
|
+
// ─── Interactive Main Menu (Frecency + Context-Aware) ─────────────────────────
|
|
4458
|
+
async function cmdMainMenu() {
|
|
4459
|
+
const usage = loadCliUsage();
|
|
4460
|
+
const hasVault = !!resolveVaultPath();
|
|
4254
4461
|
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
|
|
4258
|
-
if (v && fs.existsSync(v)) return v;
|
|
4259
|
-
} catch {}
|
|
4260
|
-
}
|
|
4261
|
-
// Try Obsidian detection
|
|
4262
|
-
const obsVaults = detectObsidianVaults();
|
|
4263
|
-
return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
|
|
4264
|
-
}
|
|
4265
|
-
|
|
4266
|
-
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
4267
|
-
async function main() {
|
|
4268
|
-
const opts = parseArgs();
|
|
4269
|
-
let bundleDir = path.resolve(__dirname);
|
|
4270
|
-
const startTime = Date.now();
|
|
4271
|
-
|
|
4272
|
-
// ── Intro ──
|
|
4273
|
-
await printHeader();
|
|
4274
|
-
|
|
4275
|
-
// ── Route: no command → interactive menu (persistent loop) ──
|
|
4276
|
-
const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "test"];
|
|
4462
|
+
const hasConfig = fs.existsSync(cfgPath);
|
|
4463
|
+
const hasPrayer = hasConfig && (() => { try { return !!JSON.parse(fs.readFileSync(cfgPath, "utf8")).settings?.show_prayer_times; } catch { return false; } })();
|
|
4277
4464
|
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
while (true) {
|
|
4281
|
-
opts.command = await cmdMainMenu();
|
|
4282
|
-
if (!opts.command) break; // user cancelled
|
|
4465
|
+
// All non-hidden commands
|
|
4466
|
+
const allCmds = Object.keys(CLI_COMMANDS).filter(c => !CLI_COMMANDS[c].hidden);
|
|
4283
4467
|
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
await new Promise((r) => { process.stdin.resume(); process.stdin.once("data", () => { process.stdin.pause(); r(); }); });
|
|
4291
|
-
continue;
|
|
4292
|
-
}
|
|
4293
|
-
break; // install/update break out of loop into pre-flight
|
|
4294
|
-
}
|
|
4295
|
-
if (!opts.command) return;
|
|
4296
|
-
}
|
|
4468
|
+
// Context filter: hide irrelevant commands
|
|
4469
|
+
const contextFilter = (cmd) => {
|
|
4470
|
+
if (!hasVault && ["pulse", "replay", "diff", "sync", "capture", "who", "context", "backup", "restore", "doctor"].includes(cmd)) return false;
|
|
4471
|
+
if (!hasPrayer && cmd === "prayer") return false;
|
|
4472
|
+
return true;
|
|
4473
|
+
};
|
|
4297
4474
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4475
|
+
const eligible = allCmds.filter(contextFilter);
|
|
4476
|
+
|
|
4477
|
+
// Pinned: always visible
|
|
4478
|
+
const pinned = ["settings", "help"];
|
|
4479
|
+
// Also pin install if no vault, update if vault exists
|
|
4480
|
+
if (!hasVault) pinned.unshift("install");
|
|
4481
|
+
else pinned.unshift("update");
|
|
4482
|
+
|
|
4483
|
+
// Score remaining by frecency
|
|
4484
|
+
const scored = eligible
|
|
4485
|
+
.filter(c => !pinned.includes(c))
|
|
4486
|
+
.map(c => ({ cmd: c, score: frecencyScore(usage, c) }))
|
|
4487
|
+
.sort((a, b) => b.score - a.score);
|
|
4488
|
+
|
|
4489
|
+
// Show top N frecency commands + pinned + "more..."
|
|
4490
|
+
const MAX_SURFACE = 5;
|
|
4491
|
+
const surfaced = scored.slice(0, MAX_SURFACE).map(s => s.cmd);
|
|
4492
|
+
const hidden = scored.slice(MAX_SURFACE).map(s => s.cmd);
|
|
4493
|
+
|
|
4494
|
+
// Build menu: pinned first, then frecency-surfaced, then "more..." if needed
|
|
4495
|
+
const topItems = [...new Set([...pinned, ...surfaced])];
|
|
4496
|
+
const menuItems = topItems.map(cmd => ({
|
|
4497
|
+
id: cmd,
|
|
4498
|
+
name: `${cmd.padEnd(12)} ${dim(CLI_COMMANDS[cmd].desc)}`,
|
|
4499
|
+
}));
|
|
4500
|
+
if (hidden.length > 0) {
|
|
4501
|
+
menuItems.push({ id: "__more__", name: `${"more...".padEnd(12)} ${dim(`${hidden.length} more commands`)}` });
|
|
4307
4502
|
}
|
|
4308
4503
|
|
|
4309
|
-
|
|
4310
|
-
barLn(gray("Pre-flight"));
|
|
4504
|
+
question(`${bold("moveros")} ${dim("— choose a command")}`);
|
|
4311
4505
|
barLn();
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4506
|
+
let choice = await interactiveSelect(menuItems);
|
|
4507
|
+
|
|
4508
|
+
if (choice === "__more__") {
|
|
4509
|
+
// Show full list
|
|
4510
|
+
const fullItems = eligible
|
|
4511
|
+
.filter(c => !c.startsWith("__"))
|
|
4512
|
+
.map(cmd => ({
|
|
4513
|
+
id: cmd,
|
|
4514
|
+
name: `${cmd.padEnd(12)} ${dim(CLI_COMMANDS[cmd].desc)}`,
|
|
4515
|
+
}));
|
|
4516
|
+
barLn();
|
|
4517
|
+
question(`${bold("moveros")} ${dim("— all commands")}`);
|
|
4518
|
+
barLn();
|
|
4519
|
+
choice = await interactiveSelect(fullItems);
|
|
4316
4520
|
}
|
|
4317
|
-
barLn();
|
|
4318
4521
|
|
|
4319
|
-
if (
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
}
|
|
4522
|
+
if (!choice) return null;
|
|
4523
|
+
return choice;
|
|
4524
|
+
}
|
|
4323
4525
|
|
|
4324
|
-
|
|
4325
|
-
|
|
4526
|
+
// ─── Comprehensive Update (10-step interactive) ─────────────────────────────
|
|
4527
|
+
async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
4528
|
+
const isQuick = opts.rest.includes("--quick");
|
|
4529
|
+
|
|
4530
|
+
// Step 1: CLI Self-Update
|
|
4531
|
+
if (!opts._selfUpdated) {
|
|
4326
4532
|
try {
|
|
4327
4533
|
const localVer = require("./package.json").version;
|
|
4328
4534
|
const npmVer = execSync("npm view mover-os version", { encoding: "utf8", timeout: 10000 }).trim();
|
|
@@ -4334,7 +4540,6 @@ async function main() {
|
|
|
4334
4540
|
sp.stop(`CLI updated to ${npmVer}`);
|
|
4335
4541
|
barLn(dim(" Re-running with updated CLI..."));
|
|
4336
4542
|
barLn();
|
|
4337
|
-
// Re-exec with new code — pass args through, add flag to prevent loop
|
|
4338
4543
|
const args = process.argv.slice(2).concat("--_self-updated");
|
|
4339
4544
|
const { spawnSync } = require("child_process");
|
|
4340
4545
|
const result = spawnSync(process.argv[0], [process.argv[1], ...args], {
|
|
@@ -4346,7 +4551,7 @@ async function main() {
|
|
|
4346
4551
|
barLn(dim(" Continuing with current version..."));
|
|
4347
4552
|
}
|
|
4348
4553
|
} else {
|
|
4349
|
-
|
|
4554
|
+
statusLine("ok", "CLI", `up to date (${localVer})`);
|
|
4350
4555
|
}
|
|
4351
4556
|
} catch {
|
|
4352
4557
|
barLn(dim(" Could not check for CLI updates (offline?)"));
|
|
@@ -4354,90 +4559,77 @@ async function main() {
|
|
|
4354
4559
|
barLn();
|
|
4355
4560
|
}
|
|
4356
4561
|
|
|
4357
|
-
//
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
try { updateKey = JSON.parse(fs.readFileSync(cfgPath, "utf8")).licenseKey; } catch {}
|
|
4365
|
-
}
|
|
4366
|
-
}
|
|
4367
|
-
if (!updateKey || !await validateKey(updateKey)) {
|
|
4368
|
-
outro(red("Valid license key required. Use: npx moveros --update --key YOUR_KEY"));
|
|
4369
|
-
process.exit(1);
|
|
4370
|
-
}
|
|
4371
|
-
|
|
4372
|
-
// Download payload if not bundled
|
|
4373
|
-
const hasSrcUpdate = fs.existsSync(path.join(bundleDir, "src", "workflows"));
|
|
4374
|
-
if (!hasSrcUpdate) {
|
|
4375
|
-
barLn(dim("Downloading payload..."));
|
|
4376
|
-
try {
|
|
4377
|
-
bundleDir = await downloadPayload(updateKey);
|
|
4378
|
-
} catch (err) {
|
|
4379
|
-
outro(red(`Download failed: ${err.message}`));
|
|
4380
|
-
process.exit(1);
|
|
4381
|
-
}
|
|
4382
|
-
}
|
|
4562
|
+
// Step 2: Vault Detection
|
|
4563
|
+
let vaultPath = resolveVaultPath(opts.vault);
|
|
4564
|
+
if (!vaultPath) {
|
|
4565
|
+
outro(red("No Mover OS vault found. Use: moveros update --vault /path"));
|
|
4566
|
+
process.exit(1);
|
|
4567
|
+
}
|
|
4568
|
+
statusLine("ok", "Vault", path.basename(vaultPath));
|
|
4383
4569
|
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
);
|
|
4391
|
-
if (!vaultPath) {
|
|
4392
|
-
outro(red("No Mover OS vault found. Use: npx moveros --update --vault /path"));
|
|
4393
|
-
process.exit(1);
|
|
4394
|
-
}
|
|
4570
|
+
// Step 3: Key Validation
|
|
4571
|
+
let updateKey = opts.key;
|
|
4572
|
+
if (!updateKey) {
|
|
4573
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
4574
|
+
if (fs.existsSync(cfgPath)) {
|
|
4575
|
+
try { updateKey = JSON.parse(fs.readFileSync(cfgPath, "utf8")).licenseKey; } catch {}
|
|
4395
4576
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4577
|
+
}
|
|
4578
|
+
const keyFromConfig = !!updateKey;
|
|
4579
|
+
if (!updateKey) {
|
|
4580
|
+
updateKey = await textInput({ label: "License key", mask: "\u25AA", placeholder: "MOVER-XXXX-XXXX" });
|
|
4581
|
+
if (!updateKey) return;
|
|
4582
|
+
}
|
|
4583
|
+
const sp1 = spinner("Validating license");
|
|
4584
|
+
const keyValid = await validateKey(updateKey);
|
|
4585
|
+
if (!keyValid && keyFromConfig) {
|
|
4586
|
+
sp1.stop(dim("License check skipped (offline — using stored key)"));
|
|
4587
|
+
} else if (!keyValid) {
|
|
4588
|
+
sp1.stop(red("Invalid key"));
|
|
4589
|
+
outro(red("Valid license key required."));
|
|
4590
|
+
process.exit(1);
|
|
4591
|
+
} else {
|
|
4592
|
+
sp1.stop(green("License verified"));
|
|
4593
|
+
}
|
|
4594
|
+
barLn();
|
|
4399
4595
|
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4596
|
+
// Download payload if not bundled
|
|
4597
|
+
const hasSrc = fs.existsSync(path.join(bundleDir, "src", "workflows"));
|
|
4598
|
+
if (!hasSrc) {
|
|
4599
|
+
const dlSp = spinner("Downloading Mover OS");
|
|
4600
|
+
try {
|
|
4601
|
+
bundleDir = await downloadPayload(updateKey);
|
|
4602
|
+
dlSp.stop(green("Downloaded"));
|
|
4603
|
+
} catch (err) {
|
|
4604
|
+
dlSp.stop(red("Download failed"));
|
|
4605
|
+
outro(red(err.message));
|
|
4404
4606
|
process.exit(1);
|
|
4405
4607
|
}
|
|
4406
|
-
const selectedIds = detectedAgents.map((a) => a.id);
|
|
4407
|
-
barLn(dim(`Agents: ${detectedAgents.map((a) => a.name).join(", ")}`));
|
|
4408
4608
|
barLn();
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
// Read versions
|
|
4612
|
+
const vfPath = path.join(vaultPath, ".mover-version");
|
|
4613
|
+
const installedVer = fs.existsSync(vfPath) ? fs.readFileSync(vfPath, "utf8").trim() : null;
|
|
4614
|
+
let newVer = `V${VERSION}`;
|
|
4615
|
+
try {
|
|
4616
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(bundleDir, "package.json"), "utf8"));
|
|
4617
|
+
newVer = pkg.version || newVer;
|
|
4618
|
+
} catch {}
|
|
4409
4619
|
|
|
4410
|
-
|
|
4620
|
+
// Quick mode: skip interactive steps
|
|
4621
|
+
if (isQuick) {
|
|
4622
|
+
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
4623
|
+
if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
|
|
4624
|
+
const selectedIds = detectedAgents.map((a) => a.id);
|
|
4411
4625
|
const changes = detectChanges(bundleDir, vaultPath, selectedIds);
|
|
4412
4626
|
const totalChanged = countChanges(changes);
|
|
4413
|
-
|
|
4414
|
-
// Read versions
|
|
4415
|
-
const vfPath = path.join(vaultPath, ".mover-version");
|
|
4416
|
-
const installedVer = fs.existsSync(vfPath) ? fs.readFileSync(vfPath, "utf8").trim() : null;
|
|
4417
|
-
let newVer = `V${VERSION}`;
|
|
4418
|
-
try {
|
|
4419
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(bundleDir, "package.json"), "utf8"));
|
|
4420
|
-
newVer = pkg.version || newVer;
|
|
4421
|
-
} catch {}
|
|
4422
|
-
|
|
4423
4627
|
displayChangeSummary(changes, installedVer, newVer);
|
|
4424
|
-
|
|
4425
|
-
if (totalChanged === 0) {
|
|
4426
|
-
outro(green("Already up to date."));
|
|
4427
|
-
return;
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// Apply all changes
|
|
4628
|
+
if (totalChanged === 0) { outro(green("Already up to date.")); return; }
|
|
4431
4629
|
barLn(bold("Updating..."));
|
|
4432
4630
|
barLn();
|
|
4433
|
-
|
|
4434
|
-
// Vault structure
|
|
4435
4631
|
createVaultStructure(vaultPath);
|
|
4436
|
-
|
|
4437
|
-
// Templates
|
|
4438
4632
|
installTemplateFiles(bundleDir, vaultPath);
|
|
4439
|
-
|
|
4440
|
-
// Per-agent installation
|
|
4441
4633
|
const writtenFiles = new Set();
|
|
4442
4634
|
const skillOpts = { install: true, categories: null, workflows: null };
|
|
4443
4635
|
for (const agent of detectedAgents) {
|
|
@@ -4446,20 +4638,325 @@ async function main() {
|
|
|
4446
4638
|
const sp = spinner(agent.name);
|
|
4447
4639
|
const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, agent.id);
|
|
4448
4640
|
await sleep(200);
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4641
|
+
sp.stop(steps.length > 0 ? `${agent.name} ${dim(steps.join(", "))}` : `${agent.name} ${dim("configured")}`);
|
|
4642
|
+
}
|
|
4643
|
+
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4644
|
+
writeMoverConfig(vaultPath, selectedIds);
|
|
4645
|
+
barLn();
|
|
4646
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4647
|
+
outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// Step 4: What's New
|
|
4652
|
+
showWhatsNew(installedVer, newVer);
|
|
4653
|
+
|
|
4654
|
+
// Step 5: Backup Offer
|
|
4655
|
+
const engine = detectEngineFiles(vaultPath);
|
|
4656
|
+
if (engine.exists) {
|
|
4657
|
+
question("Back up before updating?");
|
|
4658
|
+
barLn(dim(" Select what to save. Esc to skip."));
|
|
4659
|
+
barLn();
|
|
4660
|
+
const backupItems = [
|
|
4661
|
+
{ id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, Context" },
|
|
4662
|
+
];
|
|
4663
|
+
if (fs.existsSync(path.join(vaultPath, "02_Areas"))) {
|
|
4664
|
+
backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/" });
|
|
4665
|
+
}
|
|
4666
|
+
const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
|
|
4667
|
+
if (detectedForBackup.length > 0) {
|
|
4668
|
+
backupItems.push({ id: "agents", name: "Agent configs", tier: `Rules, skills from ${detectedForBackup.length} agent(s)` });
|
|
4669
|
+
}
|
|
4670
|
+
const backupChoices = await interactiveSelect(backupItems, { multi: true, preSelected: ["engine"] });
|
|
4671
|
+
if (backupChoices && backupChoices.length > 0) {
|
|
4672
|
+
const now = new Date();
|
|
4673
|
+
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
4674
|
+
const archivesDir = path.join(vaultPath, "04_Archives");
|
|
4675
|
+
if (backupChoices.includes("engine")) {
|
|
4676
|
+
const engineDir = path.join(vaultPath, "02_Areas", "Engine");
|
|
4677
|
+
const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
|
|
4678
|
+
try {
|
|
4679
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
4680
|
+
let backed = 0;
|
|
4681
|
+
for (const f of fs.readdirSync(engineDir).filter(f => fs.statSync(path.join(engineDir, f)).isFile())) {
|
|
4682
|
+
fs.copyFileSync(path.join(engineDir, f), path.join(backupDir, f));
|
|
4683
|
+
backed++;
|
|
4684
|
+
}
|
|
4685
|
+
statusLine("ok", "Backed up", `${backed} Engine files`);
|
|
4686
|
+
} catch (err) { barLn(yellow(` Backup failed: ${err.message}`)); }
|
|
4687
|
+
}
|
|
4688
|
+
if (backupChoices.includes("areas")) {
|
|
4689
|
+
try {
|
|
4690
|
+
copyDirRecursive(path.join(vaultPath, "02_Areas"), path.join(archivesDir, `Areas_Backup_${ts}`));
|
|
4691
|
+
statusLine("ok", "Backed up", "Full Areas folder");
|
|
4692
|
+
} catch (err) { barLn(yellow(` Areas backup failed: ${err.message}`)); }
|
|
4693
|
+
}
|
|
4694
|
+
if (backupChoices.includes("agents")) {
|
|
4695
|
+
try {
|
|
4696
|
+
const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
|
|
4697
|
+
fs.mkdirSync(agentBackupDir, { recursive: true });
|
|
4698
|
+
let agentsBacked = 0;
|
|
4699
|
+
for (const ag of AGENTS.filter((a) => a.detect())) {
|
|
4700
|
+
const agDir = path.join(agentBackupDir, ag.id);
|
|
4701
|
+
fs.mkdirSync(agDir, { recursive: true });
|
|
4702
|
+
for (const cp of (ag.configPaths || [])) {
|
|
4703
|
+
if (fs.existsSync(cp.src)) {
|
|
4704
|
+
try { fs.copyFileSync(cp.src, path.join(agDir, path.basename(cp.src))); agentsBacked++; } catch {}
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
statusLine("ok", "Backed up", `${agentsBacked} agent config files`);
|
|
4709
|
+
} catch (err) { barLn(yellow(` Agent backup failed: ${err.message}`)); }
|
|
4453
4710
|
}
|
|
4454
4711
|
}
|
|
4712
|
+
barLn();
|
|
4713
|
+
}
|
|
4455
4714
|
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4715
|
+
// Step 6: Agent Management
|
|
4716
|
+
const visibleAgents = AGENTS.filter((a) => !a.hidden);
|
|
4717
|
+
const detectedIds = visibleAgents.filter((a) => a.detect()).map((a) => a.id);
|
|
4718
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
4719
|
+
let currentAgents = [];
|
|
4720
|
+
if (fs.existsSync(cfgPath)) {
|
|
4721
|
+
try { currentAgents = JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || []; } catch {}
|
|
4722
|
+
}
|
|
4723
|
+
const preSelectedAgents = [...new Set([...detectedIds, ...currentAgents])];
|
|
4724
|
+
|
|
4725
|
+
question(`Agents ${dim("(add or remove)")}`);
|
|
4726
|
+
barLn();
|
|
4727
|
+
const agentItems = visibleAgents.map((a) => ({
|
|
4728
|
+
...a,
|
|
4729
|
+
_detected: detectedIds.includes(a.id),
|
|
4730
|
+
}));
|
|
4731
|
+
const selectedIds = await interactiveSelect(agentItems, { multi: true, preSelected: preSelectedAgents });
|
|
4732
|
+
if (!selectedIds || selectedIds.length === 0) return;
|
|
4733
|
+
const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
|
|
4734
|
+
barLn();
|
|
4735
|
+
|
|
4736
|
+
// Step 7: Change Detection
|
|
4737
|
+
const changes = detectChanges(bundleDir, vaultPath, selectedIds);
|
|
4738
|
+
const totalChanged = countChanges(changes);
|
|
4739
|
+
question("Change Summary");
|
|
4740
|
+
barLn();
|
|
4741
|
+
displayChangeSummary(changes, installedVer, newVer);
|
|
4459
4742
|
|
|
4743
|
+
if (totalChanged === 0) {
|
|
4744
|
+
barLn(green(" Already up to date."));
|
|
4460
4745
|
barLn();
|
|
4461
4746
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4462
|
-
outro(`${green("Done.")}
|
|
4747
|
+
outro(`${green("Done.")} No changes needed. ${dim(`(${elapsed}s)`)}`);
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
const applyChoice = await interactiveSelect([
|
|
4752
|
+
{ id: "all", name: "Yes, update all changed files", tier: "" },
|
|
4753
|
+
{ id: "select", name: "Select individually", tier: "" },
|
|
4754
|
+
{ id: "cancel", name: "Cancel", tier: "" },
|
|
4755
|
+
], { multi: false, defaultIndex: 0 });
|
|
4756
|
+
if (!applyChoice || applyChoice === "cancel") { outro("Cancelled."); return; }
|
|
4757
|
+
|
|
4758
|
+
let selectedWorkflows = null;
|
|
4759
|
+
let skipHooks = false, skipRules = false, skipTemplates = false;
|
|
4760
|
+
if (applyChoice === "select") {
|
|
4761
|
+
const changedItems = [];
|
|
4762
|
+
const changedPreSelected = [];
|
|
4763
|
+
for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
|
|
4764
|
+
const id = `wf:${f.file}`;
|
|
4765
|
+
changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new" : "changed") });
|
|
4766
|
+
changedPreSelected.push(id);
|
|
4767
|
+
}
|
|
4768
|
+
for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
|
|
4769
|
+
const id = `hook:${f.file}`;
|
|
4770
|
+
changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
|
|
4771
|
+
changedPreSelected.push(id);
|
|
4772
|
+
}
|
|
4773
|
+
if (changes.rules === "changed") {
|
|
4774
|
+
changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
|
|
4775
|
+
changedPreSelected.push("rules");
|
|
4776
|
+
}
|
|
4777
|
+
for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
|
|
4778
|
+
const id = `tmpl:${f.file}`;
|
|
4779
|
+
changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new" : "changed") });
|
|
4780
|
+
changedPreSelected.push(id);
|
|
4781
|
+
}
|
|
4782
|
+
if (changedItems.length > 0) {
|
|
4783
|
+
question("Select files to update");
|
|
4784
|
+
barLn();
|
|
4785
|
+
const selectedFileIds = await interactiveSelect(changedItems, { multi: true, preSelected: changedPreSelected });
|
|
4786
|
+
if (!selectedFileIds) return;
|
|
4787
|
+
const selectedWfFiles = selectedFileIds.filter((id) => id.startsWith("wf:")).map((id) => id.slice(3));
|
|
4788
|
+
if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
|
|
4789
|
+
selectedWorkflows = new Set(selectedWfFiles);
|
|
4790
|
+
}
|
|
4791
|
+
skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
|
|
4792
|
+
skipRules = !selectedFileIds.includes("rules");
|
|
4793
|
+
skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
// Step 8: Apply Changes (with progress animation)
|
|
4798
|
+
barLn();
|
|
4799
|
+
question(bold("Applying updates"));
|
|
4800
|
+
barLn();
|
|
4801
|
+
|
|
4802
|
+
const installSteps = [];
|
|
4803
|
+
installSteps.push({ label: "Vault structure", fn: async () => { createVaultStructure(vaultPath); await sleep(100); } });
|
|
4804
|
+
if (!skipTemplates) {
|
|
4805
|
+
installSteps.push({ label: "Template files", fn: async () => { installTemplateFiles(bundleDir, vaultPath); await sleep(100); } });
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
const writtenFiles = new Set();
|
|
4809
|
+
const skillOpts = { install: true, categories: null, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
|
|
4810
|
+
for (const agent of selectedAgents) {
|
|
4811
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
4812
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
4813
|
+
for (const targetId of targets) {
|
|
4814
|
+
const fn = AGENT_INSTALLERS[targetId];
|
|
4815
|
+
if (!fn) continue;
|
|
4816
|
+
const targetReg = AGENT_REGISTRY[targetId];
|
|
4817
|
+
const displayName = targetReg ? targetReg.name : agent.name;
|
|
4818
|
+
installSteps.push({ label: displayName, fn: async () => { fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId); await sleep(150); } });
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
await installProgress(installSteps);
|
|
4823
|
+
|
|
4824
|
+
// Step 9: Skills Refresh
|
|
4825
|
+
barLn();
|
|
4826
|
+
const allSkills = findSkills(bundleDir);
|
|
4827
|
+
if (allSkills.length > 0 && selectedAgents.some((a) => a.id !== "aider")) {
|
|
4828
|
+
question("Refresh skill categories?");
|
|
4829
|
+
barLn();
|
|
4830
|
+
const catCounts = {};
|
|
4831
|
+
for (const sk of allSkills) { catCounts[sk.category] = (catCounts[sk.category] || 0) + 1; }
|
|
4832
|
+
const categoryItems = CATEGORY_META.map((c) => ({
|
|
4833
|
+
id: c.id,
|
|
4834
|
+
name: `${c.name} ${dim(`(${catCounts[c.id] || 0})`)}`,
|
|
4835
|
+
tier: dim(c.desc),
|
|
4836
|
+
}));
|
|
4837
|
+
const selectedCatIds = await interactiveSelect(categoryItems, { multi: true, preSelected: ["development", "obsidian"] });
|
|
4838
|
+
if (selectedCatIds && selectedCatIds.length > 0) {
|
|
4839
|
+
const catSet = new Set(selectedCatIds);
|
|
4840
|
+
const refreshOpts = { install: true, categories: catSet, workflows: null };
|
|
4841
|
+
for (const agent of selectedAgents) {
|
|
4842
|
+
const fn = AGENT_INSTALLERS[agent.id];
|
|
4843
|
+
if (fn) fn(bundleDir, vaultPath, refreshOpts, writtenFiles, agent.id);
|
|
4844
|
+
}
|
|
4845
|
+
const skillCount = allSkills.filter((s) => s.category === "tools" || catSet.has(s.category)).length;
|
|
4846
|
+
statusLine("ok", "Skills refreshed", `${skillCount} across ${selectedAgents.length} agent(s)`);
|
|
4847
|
+
}
|
|
4848
|
+
barLn();
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
// Update version marker + config
|
|
4852
|
+
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4853
|
+
writeMoverConfig(vaultPath, selectedIds, updateKey);
|
|
4854
|
+
|
|
4855
|
+
// Step 10: Summary + Success
|
|
4856
|
+
barLn();
|
|
4857
|
+
await successAnimation(`Mover OS updated — ${totalChanged} files`);
|
|
4858
|
+
|
|
4859
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4860
|
+
outro(`${green("Done.")} ${dim(`${elapsed}s`)} Run ${bold("/update")} in your agent to sync Engine.`);
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
// ─── Vault Resolution Helper ─────────────────────────────────────────────────
|
|
4864
|
+
function resolveVaultPath(explicitVault) {
|
|
4865
|
+
if (explicitVault) {
|
|
4866
|
+
let v = explicitVault;
|
|
4867
|
+
if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
|
|
4868
|
+
return path.resolve(v);
|
|
4869
|
+
}
|
|
4870
|
+
// Try config.json
|
|
4871
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
4872
|
+
if (fs.existsSync(cfgPath)) {
|
|
4873
|
+
try {
|
|
4874
|
+
const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
|
|
4875
|
+
if (v && fs.existsSync(v)) return v;
|
|
4876
|
+
} catch {}
|
|
4877
|
+
}
|
|
4878
|
+
// Try Obsidian detection
|
|
4879
|
+
const obsVaults = detectObsidianVaults();
|
|
4880
|
+
return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
4884
|
+
async function main() {
|
|
4885
|
+
const opts = parseArgs();
|
|
4886
|
+
let bundleDir = path.resolve(__dirname);
|
|
4887
|
+
const startTime = Date.now();
|
|
4888
|
+
|
|
4889
|
+
// ── TUI: Enter alternate screen ──
|
|
4890
|
+
enterAltScreen();
|
|
4891
|
+
|
|
4892
|
+
// ── Intro: Logo plays once ──
|
|
4893
|
+
await printHeader();
|
|
4894
|
+
|
|
4895
|
+
// ── Route: no command → interactive menu (persistent loop) ──
|
|
4896
|
+
const lightCommands = ["pulse", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "uninstall", "test"];
|
|
4897
|
+
|
|
4898
|
+
if (!opts.command) {
|
|
4899
|
+
// Interactive loop — stay open like a real app
|
|
4900
|
+
while (true) {
|
|
4901
|
+
clearContent();
|
|
4902
|
+
ln(compactHeader());
|
|
4903
|
+
ln();
|
|
4904
|
+
|
|
4905
|
+
opts.command = await cmdMainMenu();
|
|
4906
|
+
if (!opts.command) break; // user hit Esc — clean exit
|
|
4907
|
+
|
|
4908
|
+
if (lightCommands.includes(opts.command)) {
|
|
4909
|
+
await wipeDown(12);
|
|
4910
|
+
ln(compactHeader());
|
|
4911
|
+
ln();
|
|
4912
|
+
|
|
4913
|
+
recordCliUsage(opts.command);
|
|
4914
|
+
const handler = CLI_HANDLERS[opts.command];
|
|
4915
|
+
if (handler) await handler(opts);
|
|
4916
|
+
else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
|
|
4917
|
+
opts.command = ""; // reset for next loop
|
|
4918
|
+
|
|
4919
|
+
barLn(dim(" esc to go back"));
|
|
4920
|
+
await waitForEsc();
|
|
4921
|
+
continue;
|
|
4922
|
+
}
|
|
4923
|
+
// install/update — record usage and break out into pre-flight
|
|
4924
|
+
recordCliUsage(opts.command);
|
|
4925
|
+
break;
|
|
4926
|
+
}
|
|
4927
|
+
if (!opts.command) return;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
// ── Route: direct CLI command (non-interactive) ──
|
|
4931
|
+
if (lightCommands.includes(opts.command)) {
|
|
4932
|
+
recordCliUsage(opts.command);
|
|
4933
|
+
const handler = CLI_HANDLERS[opts.command];
|
|
4934
|
+
if (handler) {
|
|
4935
|
+
await handler(opts);
|
|
4936
|
+
} else {
|
|
4937
|
+
barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
|
|
4938
|
+
}
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4942
|
+
// ── Pre-flight (install + update only) ──
|
|
4943
|
+
barLn(gray("Pre-flight"));
|
|
4944
|
+
barLn();
|
|
4945
|
+
const checks = preflight();
|
|
4946
|
+
for (const c of checks) {
|
|
4947
|
+
const icon = c.status === "ok" ? green("\u2713") : c.status === "warn" ? yellow("\u25CB") : red("\u2717");
|
|
4948
|
+
barLn(`${icon} ${dim(`${c.label} ${c.detail}`)}`);
|
|
4949
|
+
}
|
|
4950
|
+
barLn();
|
|
4951
|
+
|
|
4952
|
+
if (checks.some((c) => c.status === "fail")) {
|
|
4953
|
+
outro(red("Pre-flight failed. Fix the issues above."));
|
|
4954
|
+
process.exit(1);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
// ── Comprehensive Update ──
|
|
4958
|
+
if (opts.command === "update") {
|
|
4959
|
+
await cmdUpdateComprehensive(opts, bundleDir, startTime);
|
|
4463
4960
|
return;
|
|
4464
4961
|
}
|
|
4465
4962
|
|
|
@@ -4599,213 +5096,22 @@ async function main() {
|
|
|
4599
5096
|
if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
|
|
4600
5097
|
vaultPath = path.resolve(vaultPath);
|
|
4601
5098
|
|
|
4602
|
-
// ──
|
|
5099
|
+
// ── Fresh install only — redirect if existing ──
|
|
4603
5100
|
const engine = detectEngineFiles(vaultPath);
|
|
4604
5101
|
const versionFile = path.join(vaultPath, ".mover-version");
|
|
4605
5102
|
const hasExistingInstall = fs.existsSync(versionFile) || engine.exists;
|
|
4606
5103
|
|
|
4607
|
-
let installMode = "fresh"; // fresh | update | uninstall
|
|
4608
|
-
|
|
4609
5104
|
if (hasExistingInstall) {
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
} else {
|
|
4614
|
-
barLn(yellow("Mover OS installed, but no Engine data yet."));
|
|
4615
|
-
}
|
|
4616
|
-
barLn();
|
|
4617
|
-
|
|
4618
|
-
question("What would you like to do?");
|
|
5105
|
+
question(yellow("Mover OS is already installed in this vault."));
|
|
5106
|
+
barLn(dim(" Use " + bold("moveros update") + " to refresh agents, rules, and skills."));
|
|
5107
|
+
barLn(dim(" Use " + bold("moveros uninstall") + " to remove Mover OS."));
|
|
4619
5108
|
barLn();
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
[
|
|
4623
|
-
{ id: "update", name: "Update", tier: "Refreshes rules, commands, and skills. Your data stays safe." },
|
|
4624
|
-
{ id: "fresh", name: "Fresh Install", tier: "Full setup from scratch. Template files will be overwritten." },
|
|
4625
|
-
{ id: "uninstall", name: "Uninstall", tier: "Remove Mover OS files from your agents and vault." },
|
|
4626
|
-
],
|
|
4627
|
-
{ multi: false, defaultIndex: 0 }
|
|
4628
|
-
);
|
|
4629
|
-
if (!installMode) return;
|
|
4630
|
-
}
|
|
4631
|
-
|
|
4632
|
-
// ── Uninstall flow ──
|
|
4633
|
-
if (installMode === "uninstall") {
|
|
4634
|
-
await runUninstall(vaultPath);
|
|
5109
|
+
barLn(dim(" esc to exit"));
|
|
5110
|
+
await waitForEsc();
|
|
4635
5111
|
return;
|
|
4636
5112
|
}
|
|
4637
5113
|
|
|
4638
|
-
const updateMode =
|
|
4639
|
-
|
|
4640
|
-
// ── Backup (update mode only, if Engine files exist) ──
|
|
4641
|
-
if (updateMode && engine.exists) {
|
|
4642
|
-
barLn();
|
|
4643
|
-
question("Back up before updating?");
|
|
4644
|
-
barLn(dim(" Select what to save. Your data won't be overwritten, but backups are always safer."));
|
|
4645
|
-
barLn();
|
|
4646
|
-
|
|
4647
|
-
const backupItems = [
|
|
4648
|
-
{ id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, and all Engine data" },
|
|
4649
|
-
];
|
|
4650
|
-
|
|
4651
|
-
const areasDir = path.join(vaultPath, "02_Areas");
|
|
4652
|
-
if (fs.existsSync(areasDir)) {
|
|
4653
|
-
backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/ including Dailies and Reviews" });
|
|
4654
|
-
}
|
|
4655
|
-
|
|
4656
|
-
// Only offer agent config backup if any agents are detected
|
|
4657
|
-
const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
|
|
4658
|
-
if (detectedForBackup.length > 0) {
|
|
4659
|
-
backupItems.push({ id: "agents", name: "Agent configs", tier: `Current rules, skills, and commands from ${detectedForBackup.length} detected agent(s)` });
|
|
4660
|
-
}
|
|
4661
|
-
|
|
4662
|
-
backupItems.push({ id: "skip", name: "Skip backup", tier: "Continue without backing up" });
|
|
4663
|
-
|
|
4664
|
-
const backupChoices = await interactiveSelect(backupItems, {
|
|
4665
|
-
multi: true,
|
|
4666
|
-
preSelected: ["engine"],
|
|
4667
|
-
});
|
|
4668
|
-
|
|
4669
|
-
if (backupChoices && backupChoices.length > 0 && !(backupChoices.length === 1 && backupChoices.includes("skip"))) {
|
|
4670
|
-
const now = new Date();
|
|
4671
|
-
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
4672
|
-
const archivesDir = path.join(vaultPath, "04_Archives");
|
|
4673
|
-
|
|
4674
|
-
// Engine files backup
|
|
4675
|
-
if (backupChoices.includes("engine")) {
|
|
4676
|
-
const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
|
|
4677
|
-
const engineDir = path.join(vaultPath, "02_Areas", "Engine");
|
|
4678
|
-
try {
|
|
4679
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
4680
|
-
let backed = 0;
|
|
4681
|
-
for (const file of fs.readdirSync(engineDir)) {
|
|
4682
|
-
const src = path.join(engineDir, file);
|
|
4683
|
-
if (fs.statSync(src).isFile()) {
|
|
4684
|
-
fs.copyFileSync(src, path.join(backupDir, file));
|
|
4685
|
-
backed++;
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
barLn(green(` Backed up ${backed} Engine files to 04_Archives/Engine_Backup_${ts}/`));
|
|
4689
|
-
} catch (err) {
|
|
4690
|
-
barLn(yellow(` Engine backup failed: ${err.message}. Continuing anyway.`));
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
|
|
4694
|
-
// Full Areas folder backup
|
|
4695
|
-
if (backupChoices.includes("areas")) {
|
|
4696
|
-
const backupDir = path.join(archivesDir, `Areas_Backup_${ts}`);
|
|
4697
|
-
try {
|
|
4698
|
-
copyDirRecursive(path.join(vaultPath, "02_Areas"), backupDir);
|
|
4699
|
-
barLn(green(` Backed up full Areas folder to 04_Archives/Areas_Backup_${ts}/`));
|
|
4700
|
-
} catch (err) {
|
|
4701
|
-
barLn(yellow(` Areas backup failed: ${err.message}. Continuing anyway.`));
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
|
|
4705
|
-
// Agent configs backup
|
|
4706
|
-
if (backupChoices.includes("agents")) {
|
|
4707
|
-
const home = os.homedir();
|
|
4708
|
-
const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
|
|
4709
|
-
const AGENT_CONFIG_PATHS = {
|
|
4710
|
-
"claude-code": [
|
|
4711
|
-
{ src: path.join(home, ".claude", "CLAUDE.md"), label: "CLAUDE.md" },
|
|
4712
|
-
{ src: path.join(home, ".claude", "commands"), label: "commands" },
|
|
4713
|
-
{ src: path.join(home, ".claude", "skills"), label: "skills" },
|
|
4714
|
-
{ src: path.join(home, ".claude", "hooks"), label: "hooks" },
|
|
4715
|
-
],
|
|
4716
|
-
cursor: [
|
|
4717
|
-
{ src: path.join(vaultPath, ".cursor", "rules"), label: "rules" },
|
|
4718
|
-
{ src: path.join(home, ".cursor", "commands"), label: "commands" },
|
|
4719
|
-
{ src: path.join(home, ".cursor", "skills"), label: "skills" },
|
|
4720
|
-
],
|
|
4721
|
-
cline: [
|
|
4722
|
-
{ src: path.join(vaultPath, ".clinerules"), label: ".clinerules" },
|
|
4723
|
-
{ src: path.join(vaultPath, ".cline", "skills"), label: "skills" },
|
|
4724
|
-
],
|
|
4725
|
-
windsurf: [
|
|
4726
|
-
{ src: path.join(vaultPath, ".windsurfrules"), label: ".windsurfrules" },
|
|
4727
|
-
{ src: path.join(vaultPath, ".windsurf", "rules"), label: "rules" },
|
|
4728
|
-
{ src: path.join(vaultPath, ".windsurf", "workflows"), label: "workflows" },
|
|
4729
|
-
{ src: path.join(home, ".windsurf", "skills"), label: "skills" },
|
|
4730
|
-
],
|
|
4731
|
-
"gemini-cli": [
|
|
4732
|
-
{ src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
|
|
4733
|
-
{ src: path.join(home, ".gemini", "commands"), label: "commands" },
|
|
4734
|
-
{ src: path.join(home, ".gemini", "skills"), label: "skills" },
|
|
4735
|
-
],
|
|
4736
|
-
antigravity: [
|
|
4737
|
-
{ src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
|
|
4738
|
-
{ src: path.join(home, ".gemini", "antigravity", "global_workflows"), label: "workflows" },
|
|
4739
|
-
{ src: path.join(home, ".gemini", "antigravity", "skills"), label: "skills" },
|
|
4740
|
-
],
|
|
4741
|
-
copilot: [
|
|
4742
|
-
{ src: path.join(vaultPath, ".github", "copilot-instructions.md"), label: "copilot-instructions.md" },
|
|
4743
|
-
{ src: path.join(vaultPath, ".github", "prompts"), label: "prompts" },
|
|
4744
|
-
{ src: path.join(vaultPath, ".github", "skills"), label: "skills" },
|
|
4745
|
-
],
|
|
4746
|
-
codex: [
|
|
4747
|
-
{ src: path.join(home, ".codex", "AGENTS.md"), label: "AGENTS.md" },
|
|
4748
|
-
{ src: path.join(home, ".codex", "skills"), label: "skills" },
|
|
4749
|
-
],
|
|
4750
|
-
"roo-code": [
|
|
4751
|
-
{ src: path.join(vaultPath, ".roo", "rules"), label: "rules" },
|
|
4752
|
-
{ src: path.join(vaultPath, ".roo", "commands"), label: "commands" },
|
|
4753
|
-
{ src: path.join(vaultPath, ".roo", "skills"), label: "skills" },
|
|
4754
|
-
],
|
|
4755
|
-
"amazon-q": [
|
|
4756
|
-
{ src: path.join(vaultPath, ".amazonq", "rules"), label: "rules" },
|
|
4757
|
-
{ src: path.join(home, ".aws", "amazonq", "cli-agents"), label: "cli-agents" },
|
|
4758
|
-
],
|
|
4759
|
-
"kilo-code": [
|
|
4760
|
-
{ src: path.join(vaultPath, ".kilocode", "rules"), label: "rules" },
|
|
4761
|
-
{ src: path.join(vaultPath, ".kilocode", "skills"), label: "skills" },
|
|
4762
|
-
{ src: path.join(vaultPath, ".kilocode", "commands"), label: "commands" },
|
|
4763
|
-
],
|
|
4764
|
-
amp: [
|
|
4765
|
-
{ src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
|
|
4766
|
-
{ src: path.join(vaultPath, ".agents", "skills"), label: "skills" },
|
|
4767
|
-
],
|
|
4768
|
-
"continue": [
|
|
4769
|
-
{ src: path.join(vaultPath, ".continue", "rules"), label: "rules" },
|
|
4770
|
-
{ src: path.join(vaultPath, ".continue", "prompts"), label: "prompts" },
|
|
4771
|
-
],
|
|
4772
|
-
opencode: [
|
|
4773
|
-
{ src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
|
|
4774
|
-
{ src: path.join(vaultPath, ".opencode", "agents"), label: "agents" },
|
|
4775
|
-
],
|
|
4776
|
-
aider: [
|
|
4777
|
-
{ src: path.join(vaultPath, "CONVENTIONS.md"), label: "CONVENTIONS.md" },
|
|
4778
|
-
],
|
|
4779
|
-
};
|
|
4780
|
-
|
|
4781
|
-
let agentsBacked = 0;
|
|
4782
|
-
for (const agentId of detectedForBackup) {
|
|
4783
|
-
const paths = AGENT_CONFIG_PATHS[agentId];
|
|
4784
|
-
if (!paths) continue;
|
|
4785
|
-
const agentDir = path.join(agentBackupDir, agentId);
|
|
4786
|
-
let hasContent = false;
|
|
4787
|
-
for (const { src, label } of paths) {
|
|
4788
|
-
try {
|
|
4789
|
-
if (!fs.existsSync(src)) continue;
|
|
4790
|
-
const stat = fs.statSync(src);
|
|
4791
|
-
if (stat.isDirectory()) {
|
|
4792
|
-
copyDirRecursive(src, path.join(agentDir, label));
|
|
4793
|
-
hasContent = true;
|
|
4794
|
-
} else {
|
|
4795
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
4796
|
-
fs.copyFileSync(src, path.join(agentDir, label));
|
|
4797
|
-
hasContent = true;
|
|
4798
|
-
}
|
|
4799
|
-
} catch { /* skip inaccessible paths */ }
|
|
4800
|
-
}
|
|
4801
|
-
if (hasContent) agentsBacked++;
|
|
4802
|
-
}
|
|
4803
|
-
if (agentsBacked > 0) {
|
|
4804
|
-
barLn(green(` Backed up configs from ${agentsBacked} agent(s) to 04_Archives/Agent_Backup_${ts}/`));
|
|
4805
|
-
}
|
|
4806
|
-
}
|
|
4807
|
-
}
|
|
4808
|
-
}
|
|
5114
|
+
const updateMode = false;
|
|
4809
5115
|
|
|
4810
5116
|
if (!fs.existsSync(vaultPath)) fs.mkdirSync(vaultPath, { recursive: true });
|
|
4811
5117
|
|
|
@@ -4838,102 +5144,9 @@ async function main() {
|
|
|
4838
5144
|
return;
|
|
4839
5145
|
}
|
|
4840
5146
|
|
|
4841
|
-
//
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
let skipRules = false;
|
|
4845
|
-
let skipTemplates = false;
|
|
4846
|
-
|
|
4847
|
-
if (updateMode) {
|
|
4848
|
-
const changes = detectChanges(bundleDir, vaultPath, selectedIds);
|
|
4849
|
-
const totalChanged = countChanges(changes);
|
|
4850
|
-
|
|
4851
|
-
// Read versions for display
|
|
4852
|
-
const versionFilePath = path.join(vaultPath, ".mover-version");
|
|
4853
|
-
const installedVersion = fs.existsSync(versionFilePath)
|
|
4854
|
-
? fs.readFileSync(versionFilePath, "utf8").trim()
|
|
4855
|
-
: null;
|
|
4856
|
-
let newVersion = `V${VERSION}`;
|
|
4857
|
-
try {
|
|
4858
|
-
const pkgPath = path.join(bundleDir, "package.json");
|
|
4859
|
-
if (fs.existsSync(pkgPath)) {
|
|
4860
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
4861
|
-
newVersion = pkg.version || newVersion;
|
|
4862
|
-
}
|
|
4863
|
-
} catch {}
|
|
4864
|
-
|
|
4865
|
-
barLn();
|
|
4866
|
-
question("Change Summary");
|
|
4867
|
-
barLn();
|
|
4868
|
-
displayChangeSummary(changes, installedVersion, newVersion);
|
|
4869
|
-
|
|
4870
|
-
if (totalChanged === 0) {
|
|
4871
|
-
barLn(green(" Already up to date."));
|
|
4872
|
-
barLn();
|
|
4873
|
-
} else {
|
|
4874
|
-
const applyChoice = await interactiveSelect(
|
|
4875
|
-
[
|
|
4876
|
-
{ id: "all", name: "Yes, update all changed files", tier: "" },
|
|
4877
|
-
{ id: "select", name: "Select individually", tier: "" },
|
|
4878
|
-
{ id: "cancel", name: "Cancel", tier: "" },
|
|
4879
|
-
],
|
|
4880
|
-
{ multi: false, defaultIndex: 0 }
|
|
4881
|
-
);
|
|
4882
|
-
|
|
4883
|
-
if (!applyChoice || applyChoice === "cancel") {
|
|
4884
|
-
outro("Cancelled.");
|
|
4885
|
-
return;
|
|
4886
|
-
}
|
|
4887
|
-
|
|
4888
|
-
if (applyChoice === "select") {
|
|
4889
|
-
// Build list of only changed/new files for individual selection
|
|
4890
|
-
const changedItems = [];
|
|
4891
|
-
const changedPreSelected = [];
|
|
4892
|
-
for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
|
|
4893
|
-
const id = `wf:${f.file}`;
|
|
4894
|
-
changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new workflow" : "workflow") });
|
|
4895
|
-
changedPreSelected.push(id);
|
|
4896
|
-
}
|
|
4897
|
-
for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
|
|
4898
|
-
const id = `hook:${f.file}`;
|
|
4899
|
-
changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
|
|
4900
|
-
changedPreSelected.push(id);
|
|
4901
|
-
}
|
|
4902
|
-
if (changes.rules === "changed") {
|
|
4903
|
-
changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
|
|
4904
|
-
changedPreSelected.push("rules");
|
|
4905
|
-
}
|
|
4906
|
-
for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
|
|
4907
|
-
const id = `tmpl:${f.file}`;
|
|
4908
|
-
changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new template" : "template") });
|
|
4909
|
-
changedPreSelected.push(id);
|
|
4910
|
-
}
|
|
4911
|
-
|
|
4912
|
-
if (changedItems.length > 0) {
|
|
4913
|
-
question("Select files to update");
|
|
4914
|
-
barLn();
|
|
4915
|
-
const selectedFileIds = await interactiveSelect(changedItems, {
|
|
4916
|
-
multi: true,
|
|
4917
|
-
preSelected: changedPreSelected,
|
|
4918
|
-
});
|
|
4919
|
-
if (!selectedFileIds) return;
|
|
4920
|
-
|
|
4921
|
-
// Build workflow filter Set
|
|
4922
|
-
const selectedWfFiles = selectedFileIds
|
|
4923
|
-
.filter((id) => id.startsWith("wf:"))
|
|
4924
|
-
.map((id) => id.slice(3));
|
|
4925
|
-
if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
|
|
4926
|
-
selectedWorkflows = new Set(selectedWfFiles);
|
|
4927
|
-
}
|
|
4928
|
-
// Check if hooks/rules/templates were deselected
|
|
4929
|
-
skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
|
|
4930
|
-
skipRules = !selectedFileIds.includes("rules");
|
|
4931
|
-
skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
// "all" = selectedWorkflows stays null, skip flags stay false
|
|
4935
|
-
}
|
|
4936
|
-
}
|
|
5147
|
+
// Fresh install — no change detection needed
|
|
5148
|
+
const selectedWorkflows = null;
|
|
5149
|
+
const skipHooks = false, skipRules = false, skipTemplates = false;
|
|
4937
5150
|
|
|
4938
5151
|
// ── Skills ──
|
|
4939
5152
|
const allSkills = findSkills(bundleDir);
|
|
@@ -5100,9 +5313,44 @@ async function main() {
|
|
|
5100
5313
|
}
|
|
5101
5314
|
}
|
|
5102
5315
|
|
|
5316
|
+
// ── Settings step — let user configure before install ──
|
|
5317
|
+
{
|
|
5318
|
+
barLn();
|
|
5319
|
+
question("Configure settings " + dim("(esc to use defaults)"));
|
|
5320
|
+
barLn();
|
|
5321
|
+
const settingsItems = [
|
|
5322
|
+
{ id: "review_day", name: "review_day Sunday Weekly review day" },
|
|
5323
|
+
{ id: "track_food", name: "track_food on Track food in daily notes" },
|
|
5324
|
+
{ id: "track_sleep", name: "track_sleep on Track sleep in daily notes" },
|
|
5325
|
+
{ id: "friction_level", name: "friction_level 3 Max friction level (1-4)" },
|
|
5326
|
+
];
|
|
5327
|
+
const settingsPick = await interactiveSelect(settingsItems);
|
|
5328
|
+
if (settingsPick) {
|
|
5329
|
+
const meta = KNOWN_SETTINGS[settingsPick];
|
|
5330
|
+
if (meta) {
|
|
5331
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
5332
|
+
let cfg = {};
|
|
5333
|
+
if (fs.existsSync(cfgPath)) { try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {} }
|
|
5334
|
+
if (!cfg.settings) cfg.settings = {};
|
|
5335
|
+
if (meta.type === "boolean") {
|
|
5336
|
+
cfg.settings[settingsPick] = !(cfg.settings[settingsPick] !== undefined ? cfg.settings[settingsPick] : meta.defaults);
|
|
5337
|
+
statusLine("ok", settingsPick, cfg.settings[settingsPick] ? "on" : "off");
|
|
5338
|
+
} else {
|
|
5339
|
+
const answer = await textInput({ label: settingsPick, initial: String(meta.defaults) });
|
|
5340
|
+
if (answer !== null && answer.trim() !== "") {
|
|
5341
|
+
cfg.settings[settingsPick] = meta.type === "number" ? parseInt(answer.trim(), 10) : answer.trim();
|
|
5342
|
+
statusLine("ok", settingsPick, JSON.stringify(cfg.settings[settingsPick]));
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
5346
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5103
5351
|
// ── Install with animated spinners ──
|
|
5104
5352
|
barLn();
|
|
5105
|
-
question(
|
|
5353
|
+
question(bold("Installing..."));
|
|
5106
5354
|
barLn();
|
|
5107
5355
|
|
|
5108
5356
|
let totalSteps = 0;
|
|
@@ -5134,17 +5382,15 @@ async function main() {
|
|
|
5134
5382
|
|
|
5135
5383
|
// 2. Template files (runs in both modes — only creates missing files, never overwrites)
|
|
5136
5384
|
if (!skipTemplates) {
|
|
5137
|
-
sp = spinner(
|
|
5385
|
+
sp = spinner("Engine templates");
|
|
5138
5386
|
const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
|
|
5139
5387
|
await sleep(200);
|
|
5140
|
-
sp.stop(
|
|
5141
|
-
? `New template files${templatesInstalled > 0 ? dim(` ${templatesInstalled} added`) : dim(" all present")}`
|
|
5142
|
-
: `Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
|
|
5388
|
+
sp.stop(`Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
|
|
5143
5389
|
totalSteps++;
|
|
5144
5390
|
}
|
|
5145
5391
|
|
|
5146
|
-
// 3. CLAUDE.md
|
|
5147
|
-
|
|
5392
|
+
// 3. CLAUDE.md
|
|
5393
|
+
{
|
|
5148
5394
|
const vaultClaudeMd = path.join(vaultPath, "CLAUDE.md");
|
|
5149
5395
|
const bundleClaudeMd = path.join(bundleDir, "CLAUDE.md");
|
|
5150
5396
|
if (!fs.existsSync(vaultClaudeMd) && fs.existsSync(bundleClaudeMd)) {
|
|
@@ -5207,8 +5453,8 @@ async function main() {
|
|
|
5207
5453
|
}
|
|
5208
5454
|
}
|
|
5209
5455
|
|
|
5210
|
-
// 7. Git init Engine folder
|
|
5211
|
-
|
|
5456
|
+
// 7. Git init Engine folder
|
|
5457
|
+
{
|
|
5212
5458
|
const hasGit = cmdExists("git");
|
|
5213
5459
|
if (hasGit) {
|
|
5214
5460
|
const engineDir = path.join(vaultPath, "02_Areas", "Engine");
|
|
@@ -5234,9 +5480,9 @@ async function main() {
|
|
|
5234
5480
|
}
|
|
5235
5481
|
}
|
|
5236
5482
|
|
|
5237
|
-
// 8. Version stamp
|
|
5238
|
-
|
|
5239
|
-
fs.writeFileSync(path.join(vaultPath, ".mover-version"),
|
|
5483
|
+
// 8. Version stamp
|
|
5484
|
+
{
|
|
5485
|
+
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
5240
5486
|
}
|
|
5241
5487
|
|
|
5242
5488
|
// 9. Write ~/.mover/config.json (both fresh + update)
|
|
@@ -5246,8 +5492,7 @@ async function main() {
|
|
|
5246
5492
|
|
|
5247
5493
|
// ── Done ──
|
|
5248
5494
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
5249
|
-
|
|
5250
|
-
outro(`${green("Done.")} Mover OS v${VERSION} ${verb}. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
|
|
5495
|
+
outro(`${green("Done.")} Mover OS v${VERSION} installed. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
|
|
5251
5496
|
|
|
5252
5497
|
// Size check on installed rules files
|
|
5253
5498
|
const rulesFile = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
@@ -5282,30 +5527,22 @@ async function main() {
|
|
|
5282
5527
|
ln();
|
|
5283
5528
|
ln(` ${bold("Next steps")}`);
|
|
5284
5529
|
ln();
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
ln(` ${cyan("4")} Run ${bold("/setup")}`);
|
|
5302
|
-
ln(` ${dim("Builds your Identity, Strategy, and Goals")}`);
|
|
5303
|
-
ln();
|
|
5304
|
-
ln(gray(" ─────────────────────────────────────────────"));
|
|
5305
|
-
ln();
|
|
5306
|
-
ln(` ${dim("Obsidian = view your files")}`);
|
|
5307
|
-
ln(` ${dim("Your AI agent = where you work")}`);
|
|
5308
|
-
}
|
|
5530
|
+
ln(` ${cyan("1")} Open your vault in ${bold("Obsidian")}`);
|
|
5531
|
+
ln(` ${dim("This is where you view and browse your files")}`);
|
|
5532
|
+
ln();
|
|
5533
|
+
ln(` ${cyan("2")} Open the vault folder in your AI agent`);
|
|
5534
|
+
ln(` ${dim("Installed: " + agentNames.join(", "))}`);
|
|
5535
|
+
ln();
|
|
5536
|
+
ln(` ${cyan("3")} Enable the Obsidian theme`);
|
|
5537
|
+
ln(` ${dim("Settings → Appearance → CSS snippets → minimal-theme")}`);
|
|
5538
|
+
ln();
|
|
5539
|
+
ln(` ${cyan("4")} Run ${bold("/setup")}`);
|
|
5540
|
+
ln(` ${dim("Builds your Identity, Strategy, and Goals")}`);
|
|
5541
|
+
ln();
|
|
5542
|
+
ln(gray(" ─────────────────────────────────────────────"));
|
|
5543
|
+
ln();
|
|
5544
|
+
ln(` ${dim("Obsidian = view your files")}`);
|
|
5545
|
+
ln(` ${dim("Your AI agent = where you work")}`);
|
|
5309
5546
|
ln();
|
|
5310
5547
|
ln(` ${dim("/morning → [work] → /log → /analyse-day → /plan-tomorrow")}`);
|
|
5311
5548
|
ln();
|
|
@@ -5316,8 +5553,7 @@ async function main() {
|
|
|
5316
5553
|
ln(` ${green("▸")} ${bold("moveros pulse")} ${dim("Dashboard — energy, tasks, streaks")}`);
|
|
5317
5554
|
ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
|
|
5318
5555
|
ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
|
|
5319
|
-
ln(` ${green("▸")} ${bold("moveros
|
|
5320
|
-
ln(` ${green("▸")} ${bold("moveros sync")} ${dim("Update all agents to latest")}`);
|
|
5556
|
+
ln(` ${green("▸")} ${bold("moveros update")} ${dim("Update agents, rules, and skills")}`);
|
|
5321
5557
|
ln(` ${green("▸")} ${bold("moveros")} ${dim("Full menu with all commands")}`);
|
|
5322
5558
|
ln();
|
|
5323
5559
|
}
|