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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/install.js +863 -627
  3. 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 ? cyan("●") : dim(""));
425
- const label = active ? bold(item.name) : item.name;
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) : 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: { desc: "Full interactive install", alias: [] },
662
- update: { desc: "Update all agents", alias: ["-u"] },
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: { desc: "Pre-warm an AI session with context", alias: [] },
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), warm (pre-warm sessions), sync (update agents), doctor (health check), who (entity lookup), capture (quick inbox). Use native agent capabilities first — the CLI supplements, never replaces.
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: async (opts) => { await cmdWarm(opts); },
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: async (opts) => { await cmdDoctor(opts); },
2729
- prayer: async (opts) => { await cmdPrayer(opts); },
2730
- help: async (opts) => { await cmdHelp(opts); },
2731
- test: async (opts) => { await cmdTest(opts); },
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 now = new Date();
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 cfgPath = path.join(os.homedir(), ".mover", "config.json");
2922
- if (fs.existsSync(cfgPath)) {
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 cfgData = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
2925
- if (cfgData.lastLog) {
2926
- const ago = Math.round((Date.now() - new Date(cfgData.lastLog).getTime()) / 3600000);
2927
- statusLine(ago < 8 ? "ok" : ago < 24 ? "info" : "warn", "Last /log", `${ago}h ago`);
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
- // ─── moveros warm ───────────────────────────────────────────────────────────
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
- const capturePath = path.join(vault, "00_Inbox", `Capture - ${ymd}.md`);
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
- if (!dateStr) {
3268
- const now = new Date();
3269
- dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
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 month = dateStr.substring(0, 7);
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
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
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
- // Show all settings
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
- if (cfg.settings) {
3684
- barLn(dim(" Custom:"));
3685
- for (const [k, v] of Object.entries(cfg.settings)) {
3686
- barLn(` ${k}: ${JSON.stringify(v)}`);
3687
- }
3688
- barLn();
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
- // ─── Interactive Main Menu ────────────────────────────────────────────────────
4220
- async function cmdMainMenu() {
4221
- const menuOrder = [
4222
- "install", "update",
4223
- "pulse", "replay", "diff",
4224
- "doctor", "sync", "context", "warm",
4225
- "capture", "who",
4226
- "settings", "prayer", "backup", "restore", "help",
4227
- ];
4228
-
4229
- const menuItems = menuOrder
4230
- .filter(cmd => { const m = CLI_COMMANDS[cmd]; return m && !m.hidden; })
4231
- .map(cmd => ({
4232
- id: cmd,
4233
- name: `${cmd.padEnd(12)} ${dim(CLI_COMMANDS[cmd].desc)}`,
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
- question(`${bold("moveros")} ${dim("— choose a command")}`);
4237
- barLn();
4238
- const choice = await interactiveSelect(menuItems);
4239
- if (!choice) {
4240
- outro(dim("Cancelled."));
4241
- process.exit(0);
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
- // ─── Vault Resolution Helper ─────────────────────────────────────────────────
4247
- function resolveVaultPath(explicitVault) {
4248
- if (explicitVault) {
4249
- let v = explicitVault;
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
- if (fs.existsSync(cfgPath)) {
4256
- try {
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
- if (!opts.command) {
4279
- // Interactive loop stay open like a real app
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
- if (lightCommands.includes(opts.command)) {
4285
- const handler = CLI_HANDLERS[opts.command];
4286
- if (handler) await handler(opts);
4287
- else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4288
- opts.command = ""; // reset for next loop
4289
- barLn(dim(" Press enter to return to menu..."));
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
- // ── Route: direct CLI command (non-interactive) ──
4299
- if (lightCommands.includes(opts.command)) {
4300
- const handler = CLI_HANDLERS[opts.command];
4301
- if (handler) {
4302
- await handler(opts);
4303
- } else {
4304
- barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4305
- }
4306
- return;
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
- // ── Pre-flight (install + update only) ──
4310
- barLn(gray("Pre-flight"));
4504
+ question(`${bold("moveros")} ${dim("— choose a command")}`);
4311
4505
  barLn();
4312
- const checks = preflight();
4313
- for (const c of checks) {
4314
- const icon = c.status === "ok" ? green("\u2713") : c.status === "warn" ? yellow("\u25CB") : red("\u2717");
4315
- barLn(`${icon} ${dim(`${c.label} ${c.detail}`)}`);
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 (checks.some((c) => c.status === "fail")) {
4320
- outro(red("Pre-flight failed. Fix the issues above."));
4321
- process.exit(1);
4322
- }
4522
+ if (!choice) return null;
4523
+ return choice;
4524
+ }
4323
4525
 
4324
- // ── CLI self-update check ──
4325
- if (opts.command === "update" && !opts._selfUpdated) {
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
- barLn(`${green("\u2713")} ${dim("CLI is up to date")} ${dim(`(${localVer})`)}`);
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
- // ── Headless quick update ──
4358
- if (opts.command === "update") {
4359
- // Validate stored key
4360
- let updateKey = opts.key;
4361
- if (!updateKey) {
4362
- const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4363
- if (fs.existsSync(cfgPath)) {
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
- // Auto-detect vault
4385
- let vaultPath = opts.vault;
4386
- if (!vaultPath) {
4387
- const obsVaults = detectObsidianVaults();
4388
- vaultPath = obsVaults.find((p) =>
4389
- fs.existsSync(path.join(p, ".mover-version"))
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
- if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
4397
- vaultPath = path.resolve(vaultPath);
4398
- barLn(dim(`Vault: ${vaultPath}`));
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
- // Auto-detect agents
4401
- const detectedAgents = AGENTS.filter((a) => a.detect());
4402
- if (detectedAgents.length === 0) {
4403
- outro(red("No AI agents detected."));
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
- // Detect changes
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
- if (steps.length > 0) {
4450
- sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
4451
- } else {
4452
- sp.stop(`${agent.name} ${dim("configured")}`);
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
- // Update version marker + config
4457
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `V${VERSION}\n`, "utf8");
4458
- writeMoverConfig(vaultPath, selectedIds);
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.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
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
- // ── Detect existing install show mode menu ──
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
- if (engine.exists) {
4611
- barLn(yellow("Existing Mover OS vault detected."));
4612
- barLn(dim(` Engine files: ${engine.files.join(", ")}`));
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
- installMode = await interactiveSelect(
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 = installMode === "update";
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
- // ── Change detection + selection (update mode only) ──
4842
- let selectedWorkflows = null; // null = install all
4843
- let skipHooks = false;
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(updateMode ? bold("Updating...") : bold("Installing..."));
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(updateMode ? "New template files" : "Engine templates");
5385
+ sp = spinner("Engine templates");
5138
5386
  const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
5139
5387
  await sleep(200);
5140
- sp.stop(updateMode
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 (skip on update)
5147
- if (!updateMode) {
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 only (fresh install only)
5211
- if (!updateMode) {
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 (fresh install only — update mode lets /update workflow stamp after migrations)
5238
- if (!updateMode) {
5239
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `V${VERSION}\n`, "utf8");
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
- const verb = updateMode ? "updated" : "installed";
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
- if (updateMode) {
5286
- ln(` ${cyan("1")} Open the vault folder in your AI agent`);
5287
- ln(` ${dim("Updated: " + agentNames.join(", "))}`);
5288
- ln();
5289
- ln(` ${cyan("2")} Run ${bold("/update")}`);
5290
- ln(` ${dim("Syncs your Engine with the latest version")}`);
5291
- } else {
5292
- ln(` ${cyan("1")} Open your vault in ${bold("Obsidian")}`);
5293
- ln(` ${dim("This is where you view and browse your files")}`);
5294
- ln();
5295
- ln(` ${cyan("2")} Open the vault folder in your AI agent`);
5296
- ln(` ${dim("Installed: " + agentNames.join(", "))}`);
5297
- ln();
5298
- ln(` ${cyan("3")} Enable the Obsidian theme`);
5299
- ln(` ${dim("Settings Appearance CSS snippets → minimal-theme")}`);
5300
- ln();
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 warm")} ${dim("Pre-warm your next AI session")}`);
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
  }