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.
- package/install.js +157 -260
- 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
|
|
720
|
-
fs.mkdirSync(
|
|
721
|
-
|
|
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 "${
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4888
|
+
statusLine("ok", "Staged", `${totalChanged} updated files at ${dim("~/.mover/src/")}`);
|
|
4941
4889
|
barLn();
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
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(
|
|
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
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
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
|
-
|
|
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
|
}
|