mover-os 4.5.3 → 4.5.4

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 +157 -260
  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,7 +4736,12 @@ 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 });
4739
+ try {
4740
+ execSync("npm i -g mover-os", { stdio: "ignore", timeout: 60000 });
4741
+ } catch {
4742
+ // Retry with sudo (macOS/Linux where global npm needs root)
4743
+ execSync("sudo npm i -g mover-os", { stdio: "ignore", timeout: 60000 });
4744
+ }
4654
4745
  sp.stop(`CLI updated to ${npmVer}`);
4655
4746
  barLn(dim(" Re-running with updated CLI..."));
4656
4747
  barLn();
@@ -4662,7 +4753,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4662
4753
  process.exit(result.status || 0);
4663
4754
  } catch (e) {
4664
4755
  sp.stop(yellow(`CLI self-update failed: ${e.message}`));
4665
- barLn(dim(" Continuing with current version..."));
4756
+ barLn(dim(" Try: sudo npm i -g mover-os"));
4666
4757
  }
4667
4758
  } else {
4668
4759
  statusLine("ok", "CLI", `up to date (${localVer})`);
@@ -4731,15 +4822,21 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4731
4822
  newVer = pkg.version || newVer;
4732
4823
  } catch {}
4733
4824
 
4734
- // Quick mode: skip interactive steps
4825
+ // ── Stage only no direct file overwrite ──
4826
+ // Files are staged at ~/.mover/src/. The /update AI workflow handles
4827
+ // the actual apply with merge support for user customizations.
4828
+
4829
+ const detectedAgents = AGENTS.filter((a) => a.detect());
4830
+ const selectedIds = detectedAgents.map((a) => a.id);
4831
+
4832
+ // Quick mode: force-apply (CI/headless — no user customizations to protect)
4735
4833
  if (isQuick) {
4736
- const detectedAgents = AGENTS.filter((a) => a.detect());
4737
4834
  if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
4738
- const selectedIds = detectedAgents.map((a) => a.id);
4739
4835
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4740
4836
  const totalChanged = countChanges(changes);
4741
4837
  displayChangeSummary(changes, installedVer, newVer);
4742
4838
  if (totalChanged === 0) { outro(green("Already up to date.")); return; }
4839
+ autoBackupBeforeUpdate(changes, selectedIds, vaultPath);
4743
4840
  barLn(bold("Updating..."));
4744
4841
  barLn();
4745
4842
  createVaultStructure(vaultPath);
@@ -4764,117 +4861,15 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4764
4861
  writeMoverConfig(vaultPath, selectedIds);
4765
4862
  barLn();
4766
4863
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4767
- outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
4864
+ outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s.`);
4768
4865
  return;
4769
4866
  }
4770
4867
 
4868
+ // ── Interactive mode: stage + summary ──
4771
4869
  // Step 4: What's New
4772
4870
  showWhatsNew(installedVer, newVer);
4773
4871
 
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
4872
+ // Step 5: Change Detection (compare staged vs installed)
4878
4873
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4879
4874
  const totalChanged = countChanges(changes);
4880
4875
  question("Change Summary");
@@ -4884,125 +4879,21 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4884
4879
  if (totalChanged === 0) {
4885
4880
  barLn(green(" Already up to date."));
4886
4881
  barLn();
4887
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4888
- outro(`${green("Done.")} No changes needed. ${dim(`(${elapsed}s)`)}`);
4882
+ outro(green("No changes needed. Engine files may still need migration — run /update."));
4889
4883
  return;
4890
4884
  }
4891
4885
 
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:"));
4935
- }
4936
- }
4937
-
4938
- // Step 8: Apply Changes (with progress animation)
4886
+ // Stage confirmation
4939
4887
  barLn();
4940
- question(bold("Applying updates"));
4888
+ statusLine("ok", "Staged", `${totalChanged} updated files at ${dim("~/.mover/src/")}`);
4941
4889
  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); } });
4947
- }
4948
-
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
- }
4962
-
4963
- await installProgress(installSteps);
4964
-
4965
- // Step 9: Skills Refresh
4966
- 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
- }
4993
- barLn();
4994
- }
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
4890
+ barLn(bold(" Next step:"));
4891
+ barLn(` Run ${bold("/update")} in your AI agent to apply changes.`);
4892
+ barLn(dim(" The AI will compare each file against your local version,"));
4893
+ barLn(dim(" preserve your customizations, and merge new content."));
5001
4894
  barLn();
5002
- await successAnimation(`Mover OS updated — ${totalChanged} files`);
5003
-
5004
4895
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5005
- outro(`${green("Done.")} ${dim(`${elapsed}s`)} Run ${bold("/update")} in your agent to sync Engine.`);
4896
+ outro(`Files staged. Run ${bold("/update")} to apply. ${dim(`(${elapsed}s)`)}`);
5006
4897
  }
5007
4898
 
5008
4899
  // ─── Vault Resolution Helper ─────────────────────────────────────────────────
@@ -5437,20 +5328,26 @@ async function main() {
5437
5328
  } else if (method === "fetch") {
5438
5329
  barLn();
5439
5330
  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"));
5331
+ if (!city || !city.trim()) {
5332
+ barLn(yellow(" City cannot be empty. Run moveros prayer later to set up."));
5333
+ } else {
5334
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
5335
+ if (!country || !country.trim()) {
5336
+ barLn(yellow(" Country cannot be empty. Run moveros prayer later to set up."));
5452
5337
  } else {
5453
- sp.stop(yellow("Could not fetch. Run moveros prayer later."));
5338
+ barLn();
5339
+ const sp = spinner("Fetching prayer times");
5340
+ const tt = await fetchPrayerTimes(city.trim(), country.trim());
5341
+ if (tt && Object.keys(tt.times).length > 0) {
5342
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
5343
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
5344
+ sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
5345
+ barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
5346
+ barLn(dim(" For your mosque's specific times, run: moveros prayer"));
5347
+ } else {
5348
+ sp.stop(yellow("Could not fetch prayer times."));
5349
+ barLn(yellow(" Check city/country spelling. Run moveros prayer later to retry."));
5350
+ }
5454
5351
  }
5455
5352
  }
5456
5353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.5.3",
3
+ "version": "4.5.4",
4
4
  "description": "The self-improving OS for AI agents. Turns Obsidian into an execution engine.",
5
5
  "bin": {
6
6
  "moveros": "install.js"