mover-os 4.5.3 → 4.5.5

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 (2) hide show
  1. package/install.js +207 -253
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -716,9 +716,14 @@ const DOWNLOAD_URL = "https://moveros.dev/api/download";
716
716
 
717
717
  async function downloadPayload(key) {
718
718
  const https = require("https");
719
- const tmpDir = path.join(os.tmpdir(), `moveros-${Date.now()}`);
720
- fs.mkdirSync(tmpDir, { recursive: true });
721
- const tarPath = path.join(tmpDir, "payload.tar.gz");
719
+ const moverDir = path.join(os.homedir(), ".mover");
720
+ fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
721
+ // Clean old staged src/ before extracting fresh
722
+ const stagedSrc = path.join(moverDir, "src");
723
+ if (fs.existsSync(stagedSrc)) {
724
+ fs.rmSync(stagedSrc, { recursive: true, force: true });
725
+ }
726
+ const tarPath = path.join(moverDir, "payload.tar.gz");
722
727
 
723
728
  // Download tarball
724
729
  await new Promise((resolve, reject) => {
@@ -778,9 +783,9 @@ async function downloadPayload(key) {
778
783
  req.end();
779
784
  });
780
785
 
781
- // Extract tarball
786
+ // Extract tarball into ~/.mover/ (produces ~/.mover/src/)
782
787
  try {
783
- execSync(`tar -xzf "${tarPath}" -C "${tmpDir}"`, { stdio: "ignore" });
788
+ execSync(`tar -xzf "${tarPath}" -C "${moverDir}"`, { stdio: "ignore" });
784
789
  } catch {
785
790
  throw new Error("Failed to extract payload. Ensure 'tar' is available.");
786
791
  }
@@ -788,7 +793,7 @@ async function downloadPayload(key) {
788
793
  // Clean up tarball
789
794
  try { fs.unlinkSync(tarPath); } catch {}
790
795
 
791
- return tmpDir;
796
+ return moverDir;
792
797
  }
793
798
 
794
799
  // ─── Path helpers ────────────────────────────────────────────────────────────
@@ -1088,6 +1093,83 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
1088
1093
  return result;
1089
1094
  }
1090
1095
 
1096
+ function autoBackupBeforeUpdate(changes, selectedAgentIds, vaultPath) {
1097
+ const home = os.homedir();
1098
+ const targetIds = expandTargetIds(selectedAgentIds);
1099
+ const now = new Date();
1100
+ 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")}`;
1101
+ const backupRoot = path.join(home, ".mover", "backups", `pre-update-${ts}`);
1102
+ let backed = 0;
1103
+
1104
+ const backup = (src, relPath) => {
1105
+ if (!fs.existsSync(src)) return;
1106
+ const dest = path.join(backupRoot, relPath);
1107
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1108
+ try { fs.copyFileSync(src, dest); backed++; } catch {}
1109
+ };
1110
+
1111
+ // Workflows
1112
+ const wfDests = [
1113
+ targetIds.includes("claude-code") && path.join(home, ".claude", "commands"),
1114
+ targetIds.includes("cursor") && path.join(home, ".cursor", "commands"),
1115
+ targetIds.includes("antigravity") && path.join(home, ".gemini", "antigravity", "global_workflows"),
1116
+ ].filter(Boolean);
1117
+ for (const f of changes.workflows.filter((x) => x.status === "changed")) {
1118
+ for (const dir of wfDests) {
1119
+ backup(path.join(dir, f.file), path.join("workflows", path.basename(dir), f.file));
1120
+ }
1121
+ }
1122
+
1123
+ // Hooks
1124
+ if (targetIds.includes("claude-code")) {
1125
+ for (const f of changes.hooks.filter((x) => x.status === "changed")) {
1126
+ backup(path.join(home, ".claude", "hooks", f.file), path.join("hooks", f.file));
1127
+ }
1128
+ }
1129
+
1130
+ // Rules
1131
+ if (changes.rules === "changed") {
1132
+ const rulesDests = [
1133
+ targetIds.includes("claude-code") && path.join(home, ".claude", "CLAUDE.md"),
1134
+ targetIds.includes("gemini-cli") && path.join(home, ".gemini", "GEMINI.md"),
1135
+ targetIds.includes("cursor") && path.join(home, ".cursor", "rules", "mover-os.mdc"),
1136
+ ].filter(Boolean);
1137
+ for (const p of rulesDests) {
1138
+ backup(p, path.join("rules", path.basename(p)));
1139
+ }
1140
+ }
1141
+
1142
+ // Skills
1143
+ const skillsDests = [
1144
+ targetIds.includes("claude-code") && path.join(home, ".claude", "skills"),
1145
+ targetIds.includes("cursor") && path.join(home, ".cursor", "skills"),
1146
+ targetIds.includes("cline") && path.join(home, ".cline", "skills"),
1147
+ ].filter(Boolean);
1148
+ for (const f of (changes.skills || []).filter((x) => x.status === "changed")) {
1149
+ for (const dir of skillsDests) {
1150
+ const skillFile = path.join(dir, f.file, "SKILL.md");
1151
+ if (fs.existsSync(skillFile)) {
1152
+ backup(skillFile, path.join("skills", f.file, path.basename(dir), "SKILL.md"));
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // Statusline
1158
+ if (changes.statusline === "changed" && targetIds.includes("claude-code")) {
1159
+ backup(path.join(home, ".claude", "statusline.js"), "statusline.js");
1160
+ }
1161
+
1162
+ // Templates
1163
+ for (const f of changes.templates.filter((x) => x.status === "changed")) {
1164
+ backup(path.join(vaultPath, f.file), path.join("templates", f.file));
1165
+ }
1166
+
1167
+ if (backed > 0) {
1168
+ statusLine("ok", "Auto-backup", `${backed} files → ${dim(backupRoot)}`);
1169
+ }
1170
+ return backed;
1171
+ }
1172
+
1091
1173
  function countChanges(changes) {
1092
1174
  let n = 0;
1093
1175
  n += changes.workflows.filter((f) => f.status !== "unchanged").length;
@@ -1998,19 +2080,19 @@ async function fetchPrayerTimes(city, country) {
1998
2080
  const https = require("https");
1999
2081
  const year = new Date().getFullYear();
2000
2082
  const allTimes = {};
2083
+ let failedMonths = 0;
2001
2084
 
2002
2085
  for (let month = 1; month <= 12; month++) {
2003
2086
  try {
2004
2087
  const url = `https://api.aladhan.com/v1/calendarByCity/${year}/${month}?city=${encodeURIComponent(city)}&country=${encodeURIComponent(country)}&method=15`;
2005
2088
  const body = await new Promise((resolve, reject) => {
2006
- const req = https.request(url, { method: "GET", timeout: 10000 }, (res) => {
2089
+ const req = https.get(url, { timeout: 10000 }, (res) => {
2007
2090
  let data = "";
2008
2091
  res.on("data", (c) => (data += c));
2009
2092
  res.on("end", () => resolve(data));
2010
2093
  });
2011
2094
  req.on("error", reject);
2012
2095
  req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
2013
- req.end();
2014
2096
  });
2015
2097
 
2016
2098
  const json = JSON.parse(body);
@@ -2027,9 +2109,11 @@ async function fetchPrayerTimes(city, country) {
2027
2109
  isha: t.Isha.replace(/\s*\(.*\)/, ""),
2028
2110
  };
2029
2111
  }
2112
+ } else {
2113
+ failedMonths++;
2030
2114
  }
2031
2115
  } catch {
2032
- // Skip failed months — partial data is still useful
2116
+ failedMonths++;
2033
2117
  }
2034
2118
  }
2035
2119
 
@@ -3776,27 +3860,29 @@ async function cmdPrayer(opts) {
3776
3860
  barLn();
3777
3861
  const city = await textInput({ label: "City", placeholder: "London" });
3778
3862
  if (city === null) return;
3863
+ if (!city.trim()) { barLn(yellow(" City cannot be empty.")); return; }
3779
3864
  const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
3780
3865
  if (country === null) return;
3866
+ if (!country.trim()) { barLn(yellow(" Country cannot be empty.")); return; }
3781
3867
  barLn();
3782
3868
 
3783
- if (city && country) {
3784
- const sp = spinner("Fetching prayer times (12 months)");
3785
- const result = await fetchPrayerTimes(city.trim(), country.trim());
3786
- if (result && Object.keys(result.times).length > 0) {
3787
- fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3788
- sp.stop(`Saved ${Object.keys(result.times).length} days`);
3789
- barLn(dim(" These are calculated adhan times, not mosque jama'ah times."));
3790
- barLn(dim(" For your mosque's specific times, choose 'Paste mosque timetable'."));
3791
- } else {
3792
- sp.stop(yellow("Could not fetch. Check city/country spelling."));
3793
- }
3869
+ const sp = spinner("Fetching prayer times (12 months)");
3870
+ const result = await fetchPrayerTimes(city.trim(), country.trim());
3871
+ if (result && Object.keys(result.times).length > 0) {
3872
+ fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3873
+ sp.stop(`Saved ${Object.keys(result.times).length} days`);
3874
+ barLn(dim(" These are calculated adhan times, not mosque jama'ah times."));
3875
+ barLn(dim(" For your mosque's specific times, choose 'Paste mosque timetable'."));
3876
+
3877
+ // Auto-enable
3878
+ if (!cfg.settings) cfg.settings = {};
3879
+ cfg.settings.show_prayer_times = true;
3880
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3881
+ } else {
3882
+ sp.stop(yellow("Could not fetch prayer times."));
3883
+ barLn(yellow(" Check the city/country spelling and try again."));
3884
+ barLn(dim(" Example: City = London, Country = United Kingdom"));
3794
3885
  }
3795
-
3796
- // Auto-enable
3797
- if (!cfg.settings) cfg.settings = {};
3798
- cfg.settings.show_prayer_times = true;
3799
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3800
3886
  } else if (choice === "paste") {
3801
3887
  barLn();
3802
3888
  barLn(dim(" Paste your mosque's timetable below."));
@@ -4650,8 +4736,14 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4650
4736
  barLn(`${yellow("CLI update available:")} ${dim(localVer)} ${dim("\u2192")} ${green(npmVer)}`);
4651
4737
  const sp = spinner("Updating CLI");
4652
4738
  try {
4653
- execSync("npm i -g mover-os", { stdio: "ignore", timeout: 60000 });
4654
- sp.stop(`CLI updated to ${npmVer}`);
4739
+ try {
4740
+ execSync("npm i -g mover-os", { stdio: "ignore", timeout: 60000 });
4741
+ } catch {
4742
+ // Retry with sudo — use inherit so password prompt shows
4743
+ sp.stop(dim("Needs elevated permissions..."));
4744
+ execSync("sudo npm i -g mover-os", { stdio: "inherit", timeout: 120000 });
4745
+ }
4746
+ barLn(`${green("\u2713")} CLI updated to ${npmVer}`);
4655
4747
  barLn(dim(" Re-running with updated CLI..."));
4656
4748
  barLn();
4657
4749
  const args = process.argv.slice(2).concat("--_self-updated");
@@ -4662,7 +4754,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4662
4754
  process.exit(result.status || 0);
4663
4755
  } catch (e) {
4664
4756
  sp.stop(yellow(`CLI self-update failed: ${e.message}`));
4665
- barLn(dim(" Continuing with current version..."));
4757
+ barLn(dim(" Try: sudo npm i -g mover-os"));
4666
4758
  }
4667
4759
  } else {
4668
4760
  statusLine("ok", "CLI", `up to date (${localVer})`);
@@ -4731,15 +4823,21 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4731
4823
  newVer = pkg.version || newVer;
4732
4824
  } catch {}
4733
4825
 
4734
- // Quick mode: skip interactive steps
4826
+ // ── Stage only no direct file overwrite ──
4827
+ // Files are staged at ~/.mover/src/. The /update AI workflow handles
4828
+ // the actual apply with merge support for user customizations.
4829
+
4830
+ const detectedAgents = AGENTS.filter((a) => a.detect());
4831
+ const selectedIds = detectedAgents.map((a) => a.id);
4832
+
4833
+ // Quick mode: force-apply (CI/headless — no user customizations to protect)
4735
4834
  if (isQuick) {
4736
- const detectedAgents = AGENTS.filter((a) => a.detect());
4737
4835
  if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
4738
- const selectedIds = detectedAgents.map((a) => a.id);
4739
4836
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4740
4837
  const totalChanged = countChanges(changes);
4741
4838
  displayChangeSummary(changes, installedVer, newVer);
4742
4839
  if (totalChanged === 0) { outro(green("Already up to date.")); return; }
4840
+ autoBackupBeforeUpdate(changes, selectedIds, vaultPath);
4743
4841
  barLn(bold("Updating..."));
4744
4842
  barLn();
4745
4843
  createVaultStructure(vaultPath);
@@ -4764,117 +4862,15 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4764
4862
  writeMoverConfig(vaultPath, selectedIds);
4765
4863
  barLn();
4766
4864
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4767
- outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
4865
+ outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s.`);
4768
4866
  return;
4769
4867
  }
4770
4868
 
4869
+ // ── Interactive mode: stage + summary ──
4771
4870
  // Step 4: What's New
4772
4871
  showWhatsNew(installedVer, newVer);
4773
4872
 
4774
- // Step 5: Backup Offer
4775
- const engine = detectEngineFiles(vaultPath);
4776
- if (engine.exists) {
4777
- question("Back up before updating?");
4778
- barLn(dim(" Select what to save. Esc to skip."));
4779
- barLn();
4780
- const backupItems = [
4781
- { id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, Context" },
4782
- ];
4783
- if (fs.existsSync(path.join(vaultPath, "02_Areas"))) {
4784
- backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/" });
4785
- }
4786
- const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
4787
- if (detectedForBackup.length > 0) {
4788
- backupItems.push({ id: "agents", name: "Agent configs", tier: `Rules, skills from ${detectedForBackup.length} agent(s)` });
4789
- }
4790
- const backupChoices = await interactiveSelect(backupItems, { multi: true, preSelected: ["engine"] });
4791
- if (backupChoices && backupChoices.length > 0) {
4792
- const now = new Date();
4793
- 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")}`;
4794
- const archivesDir = path.join(vaultPath, "04_Archives");
4795
- if (backupChoices.includes("engine")) {
4796
- const engineDir = path.join(vaultPath, "02_Areas", "Engine");
4797
- const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
4798
- try {
4799
- fs.mkdirSync(backupDir, { recursive: true });
4800
- let backed = 0;
4801
- for (const f of fs.readdirSync(engineDir).filter(f => fs.statSync(path.join(engineDir, f)).isFile())) {
4802
- fs.copyFileSync(path.join(engineDir, f), path.join(backupDir, f));
4803
- backed++;
4804
- }
4805
- statusLine("ok", "Backed up", `${backed} Engine files`);
4806
- } catch (err) { barLn(yellow(` Backup failed: ${err.message}`)); }
4807
- }
4808
- if (backupChoices.includes("areas")) {
4809
- try {
4810
- copyDirRecursive(path.join(vaultPath, "02_Areas"), path.join(archivesDir, `Areas_Backup_${ts}`));
4811
- statusLine("ok", "Backed up", "Full Areas folder");
4812
- } catch (err) { barLn(yellow(` Areas backup failed: ${err.message}`)); }
4813
- }
4814
- if (backupChoices.includes("agents")) {
4815
- try {
4816
- const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
4817
- fs.mkdirSync(agentBackupDir, { recursive: true });
4818
- let agentsBacked = 0;
4819
- for (const ag of AGENTS.filter((a) => a.detect())) {
4820
- const sel = AGENT_SELECTIONS.find((s) => s.id === ag.id);
4821
- const targets = sel ? sel.targets : [ag.id];
4822
- for (const targetId of targets) {
4823
- const reg = AGENT_REGISTRY[targetId];
4824
- if (!reg) continue;
4825
- const agDir = path.join(agentBackupDir, targetId);
4826
- fs.mkdirSync(agDir, { recursive: true });
4827
- // Back up rules file
4828
- if (reg.rules && reg.rules.dest) {
4829
- try {
4830
- const rulesPath = reg.rules.dest(vaultPath);
4831
- if (fs.existsSync(rulesPath)) {
4832
- fs.copyFileSync(rulesPath, path.join(agDir, path.basename(rulesPath)));
4833
- agentsBacked++;
4834
- }
4835
- } catch {}
4836
- }
4837
- // Back up skills directory
4838
- if (reg.skills && reg.skills.dest) {
4839
- try {
4840
- const skillsDir = reg.skills.dest(vaultPath);
4841
- if (fs.existsSync(skillsDir)) {
4842
- copyDirRecursive(skillsDir, path.join(agDir, "skills"));
4843
- agentsBacked++;
4844
- }
4845
- } catch {}
4846
- }
4847
- }
4848
- }
4849
- statusLine("ok", "Backed up", `${agentsBacked} agent config items`);
4850
- } catch (err) { barLn(yellow(` Agent backup failed: ${err.message}`)); }
4851
- }
4852
- }
4853
- barLn();
4854
- }
4855
-
4856
- // Step 6: Agent Management
4857
- const visibleAgents = AGENTS.filter((a) => !a.hidden);
4858
- const detectedIds = visibleAgents.filter((a) => a.detect()).map((a) => a.id);
4859
- const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4860
- let currentAgents = [];
4861
- if (fs.existsSync(cfgPath)) {
4862
- try { currentAgents = JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || []; } catch {}
4863
- }
4864
- const preSelectedAgents = [...new Set([...detectedIds, ...currentAgents])];
4865
-
4866
- question(`Agents ${dim("(add or remove)")}`);
4867
- barLn();
4868
- const agentItems = visibleAgents.map((a) => ({
4869
- ...a,
4870
- _detected: detectedIds.includes(a.id),
4871
- }));
4872
- const selectedIds = await interactiveSelect(agentItems, { multi: true, preSelected: preSelectedAgents });
4873
- if (!selectedIds || selectedIds.length === 0) return;
4874
- const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
4875
- barLn();
4876
-
4877
- // Step 7: Change Detection
4873
+ // Step 5: Change Detection (compare staged vs installed)
4878
4874
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4879
4875
  const totalChanged = countChanges(changes);
4880
4876
  question("Change Summary");
@@ -4884,125 +4880,77 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4884
4880
  if (totalChanged === 0) {
4885
4881
  barLn(green(" Already up to date."));
4886
4882
  barLn();
4887
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4888
- outro(`${green("Done.")} No changes needed. ${dim(`(${elapsed}s)`)}`);
4883
+ outro(green("No changes needed. Engine files may still need migration — run /update."));
4889
4884
  return;
4890
4885
  }
4891
4886
 
4892
- const applyChoice = await interactiveSelect([
4893
- { id: "all", name: "Yes, update all changed files", tier: "" },
4894
- { id: "select", name: "Select individually", tier: "" },
4895
- { id: "cancel", name: "Cancel", tier: "" },
4896
- ], { multi: false, defaultIndex: 0 });
4897
- if (!applyChoice || applyChoice === "cancel") { outro("Cancelled."); return; }
4898
-
4899
- let selectedWorkflows = null;
4900
- let skipHooks = false, skipRules = false, skipTemplates = false;
4901
- if (applyChoice === "select") {
4902
- const changedItems = [];
4903
- const changedPreSelected = [];
4904
- for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
4905
- const id = `wf:${f.file}`;
4906
- changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new" : "changed") });
4907
- changedPreSelected.push(id);
4908
- }
4909
- for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
4910
- const id = `hook:${f.file}`;
4911
- changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
4912
- changedPreSelected.push(id);
4913
- }
4914
- if (changes.rules === "changed") {
4915
- changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
4916
- changedPreSelected.push("rules");
4917
- }
4918
- for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
4919
- const id = `tmpl:${f.file}`;
4920
- changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new" : "changed") });
4921
- changedPreSelected.push(id);
4922
- }
4923
- if (changedItems.length > 0) {
4924
- question("Select files to update");
4925
- barLn();
4926
- const selectedFileIds = await interactiveSelect(changedItems, { multi: true, preSelected: changedPreSelected });
4927
- if (!selectedFileIds) return;
4928
- const selectedWfFiles = selectedFileIds.filter((id) => id.startsWith("wf:")).map((id) => id.slice(3));
4929
- if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
4930
- selectedWorkflows = new Set(selectedWfFiles);
4931
- }
4932
- skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
4933
- skipRules = !selectedFileIds.includes("rules");
4934
- skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
4887
+ // ── Apply safe system files directly (no user customizations in these) ──
4888
+ const home = os.homedir();
4889
+ let appliedCount = 0;
4890
+
4891
+ // Hooks (shell scripts users don't customize)
4892
+ const hooksSrc = path.join(bundleDir, "src", "hooks");
4893
+ if (fs.existsSync(hooksSrc) && selectedIds.some((id) => expandTargetIds([id]).includes("claude-code"))) {
4894
+ const hooksDest = path.join(home, ".claude", "hooks");
4895
+ fs.mkdirSync(hooksDest, { recursive: true });
4896
+ for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh"))) {
4897
+ fs.copyFileSync(path.join(hooksSrc, file), path.join(hooksDest, file));
4898
+ try { fs.chmodSync(path.join(hooksDest, file), 0o755); } catch {}
4899
+ appliedCount++;
4935
4900
  }
4901
+ statusLine("ok", "Hooks", `${appliedCount} updated`);
4936
4902
  }
4937
4903
 
4938
- // Step 8: Apply Changes (with progress animation)
4939
- barLn();
4940
- question(bold("Applying updates"));
4941
- barLn();
4942
-
4943
- const installSteps = [];
4944
- installSteps.push({ label: "Vault structure", fn: async () => { createVaultStructure(vaultPath); await sleep(100); } });
4945
- if (!skipTemplates) {
4946
- installSteps.push({ label: "Template files", fn: async () => { installTemplateFiles(bundleDir, vaultPath); await sleep(100); } });
4904
+ // Statusline (single JS file no user data)
4905
+ const slSrc = path.join(bundleDir, "src", "hooks", "statusline.js");
4906
+ const slDest = path.join(home, ".claude", "statusline.js");
4907
+ if (fs.existsSync(slSrc)) {
4908
+ fs.copyFileSync(slSrc, slDest);
4909
+ // Ensure settings.json has statusLine config
4910
+ const globalSettings = path.join(home, ".claude", "settings.json");
4911
+ try {
4912
+ const settings = fs.existsSync(globalSettings)
4913
+ ? JSON.parse(fs.readFileSync(globalSettings, "utf8"))
4914
+ : {};
4915
+ if (!settings.statusLine) {
4916
+ settings.statusLine = { type: "command", command: "node ~/.claude/statusline.js" };
4917
+ fs.writeFileSync(globalSettings, JSON.stringify(settings, null, 2), "utf8");
4918
+ }
4919
+ } catch {}
4920
+ statusLine("ok", "Statusline", "updated");
4921
+ appliedCount++;
4947
4922
  }
4948
4923
 
4949
- const writtenFiles = new Set();
4950
- const skillOpts = { install: true, categories: null, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates, statusLine: changes.statusline !== "unchanged" };
4951
- for (const agent of selectedAgents) {
4952
- const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
4953
- const targets = sel ? sel.targets : [agent.id];
4954
- for (const targetId of targets) {
4955
- const fn = AGENT_INSTALLERS[targetId];
4956
- if (!fn) continue;
4957
- const targetReg = AGENT_REGISTRY[targetId];
4958
- const displayName = targetReg ? targetReg.name : agent.name;
4959
- installSteps.push({ label: displayName, fn: async () => { fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId); await sleep(150); } });
4960
- }
4961
- }
4924
+ // Vault structure + templates (safe scaffolding)
4925
+ createVaultStructure(vaultPath);
4926
+ installTemplateFiles(bundleDir, vaultPath);
4962
4927
 
4963
- await installProgress(installSteps);
4928
+ // Update version marker
4929
+ fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
4930
+
4931
+ // Stage confirmation for user-customizable files
4932
+ const userFileChanges = changes.workflows.filter((f) => f.status !== "unchanged").length
4933
+ + (changes.rules === "changed" ? 1 : 0)
4934
+ + (changes.skills || []).filter((f) => f.status !== "unchanged").length;
4964
4935
 
4965
- // Step 9: Skills Refresh
4966
4936
  barLn();
4967
- const allSkills = findSkills(bundleDir);
4968
- if (allSkills.length > 0 && selectedAgents.some((a) => a.id !== "aider")) {
4969
- question("Refresh skill categories?");
4970
- barLn();
4971
- const catCounts = {};
4972
- for (const sk of allSkills) { catCounts[sk.category] = (catCounts[sk.category] || 0) + 1; }
4973
- const categoryItems = CATEGORY_META.map((c) => ({
4974
- id: c.id,
4975
- name: `${c.name} ${dim(`(${catCounts[c.id] || 0})`)}`,
4976
- tier: dim(c.desc),
4977
- }));
4978
- const selectedCatIds = await interactiveSelect(categoryItems, { multi: true, preSelected: ["development", "obsidian"] });
4979
- if (selectedCatIds && selectedCatIds.length > 0) {
4980
- const catSet = new Set(selectedCatIds);
4981
- const refreshOpts = { install: true, categories: catSet, workflows: null };
4982
- for (const agent of selectedAgents) {
4983
- const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
4984
- const targets = sel ? sel.targets : [agent.id];
4985
- for (const targetId of targets) {
4986
- const fn = AGENT_INSTALLERS[targetId];
4987
- if (fn) fn(bundleDir, vaultPath, refreshOpts, writtenFiles, targetId);
4988
- }
4989
- }
4990
- const skillCount = allSkills.filter((s) => s.category === "tools" || catSet.has(s.category)).length;
4991
- statusLine("ok", "Skills refreshed", `${skillCount} across ${selectedAgents.length} agent(s)`);
4992
- }
4937
+ if (userFileChanges > 0) {
4938
+ statusLine("ok", "Staged", `${userFileChanges} workflow/rule/skill updates at ${dim("~/.mover/src/")}`);
4993
4939
  barLn();
4940
+ barLn(bold(" Next step:"));
4941
+ barLn(` Run ${bold("/update")} in your AI agent to apply workflow & rule changes.`);
4942
+ barLn(dim(" Your customizations will be preserved — new version is the base,"));
4943
+ barLn(dim(" your additions get carried forward."));
4944
+ } else {
4945
+ statusLine("ok", "System files", "all updated");
4994
4946
  }
4995
-
4996
- // Update version marker + config
4997
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
4998
- writeMoverConfig(vaultPath, selectedIds, updateKey);
4999
-
5000
- // Step 10: Summary + Success
5001
4947
  barLn();
5002
- await successAnimation(`Mover OS updated — ${totalChanged} files`);
5003
-
5004
4948
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5005
- outro(`${green("Done.")} ${dim(`${elapsed}s`)} Run ${bold("/update")} in your agent to sync Engine.`);
4949
+ if (userFileChanges > 0) {
4950
+ outro(`System updated. Run ${bold("/update")} for workflows & rules. ${dim(`(${elapsed}s)`)}`);
4951
+ } else {
4952
+ outro(`${green("Done.")} All files up to date. ${dim(`(${elapsed}s)`)}`);
4953
+ }
5006
4954
  }
5007
4955
 
5008
4956
  // ─── Vault Resolution Helper ─────────────────────────────────────────────────
@@ -5437,20 +5385,26 @@ async function main() {
5437
5385
  } else if (method === "fetch") {
5438
5386
  barLn();
5439
5387
  const city = await textInput({ label: "City (e.g. London, Watford, Istanbul)", placeholder: "London" });
5440
- const country = city ? await textInput({ label: "Country", placeholder: "United Kingdom" }) : null;
5441
- barLn();
5442
-
5443
- if (city && country) {
5444
- const sp = spinner("Fetching prayer times");
5445
- const tt = await fetchPrayerTimes(city.trim(), country.trim());
5446
- if (tt && Object.keys(tt.times).length > 0) {
5447
- const ttPath = path.join(moverDir, "prayer-timetable.json");
5448
- fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
5449
- sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
5450
- barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
5451
- barLn(dim(" For your mosque's specific times, run: moveros prayer"));
5388
+ if (!city || !city.trim()) {
5389
+ barLn(yellow(" City cannot be empty. Run moveros prayer later to set up."));
5390
+ } else {
5391
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
5392
+ if (!country || !country.trim()) {
5393
+ barLn(yellow(" Country cannot be empty. Run moveros prayer later to set up."));
5452
5394
  } else {
5453
- sp.stop(yellow("Could not fetch. Run moveros prayer later."));
5395
+ barLn();
5396
+ const sp = spinner("Fetching prayer times");
5397
+ const tt = await fetchPrayerTimes(city.trim(), country.trim());
5398
+ if (tt && Object.keys(tt.times).length > 0) {
5399
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
5400
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
5401
+ sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
5402
+ barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
5403
+ barLn(dim(" For your mosque's specific times, run: moveros prayer"));
5404
+ } else {
5405
+ sp.stop(yellow("Could not fetch prayer times."));
5406
+ barLn(yellow(" Check city/country spelling. Run moveros prayer later to retry."));
5407
+ }
5454
5408
  }
5455
5409
  }
5456
5410
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.5.3",
3
+ "version": "4.5.5",
4
4
  "description": "The self-improving OS for AI agents. Turns Obsidian into an execution engine.",
5
5
  "bin": {
6
6
  "moveros": "install.js"