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.
- package/install.js +207 -253
- 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,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
|
-
|
|
4654
|
-
|
|
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("
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
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
|
-
//
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
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
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"));
|
|
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
|
-
|
|
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
|
}
|