mover-os 4.5.2 → 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 +271 -289
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -92,17 +92,10 @@ function waveGradient(text, frame, totalFrames) {
|
|
|
92
92
|
// ─── TUI: Alternate screen buffer ────────────────────────────────────────────
|
|
93
93
|
let _inAltScreen = false;
|
|
94
94
|
function enterAltScreen() {
|
|
95
|
-
|
|
96
|
-
w("\x1b[?1049h"); // Enter alternate screen
|
|
97
|
-
w("\x1b[2J\x1b[H"); // Clear + cursor home
|
|
98
|
-
w(S.hide);
|
|
99
|
-
_inAltScreen = true;
|
|
95
|
+
// Disabled: keep output in terminal scrollback history
|
|
100
96
|
}
|
|
101
97
|
function exitAltScreen() {
|
|
102
|
-
|
|
103
|
-
w("\x1b[?1049l"); // Restore original screen
|
|
104
|
-
w(S.show);
|
|
105
|
-
_inAltScreen = false;
|
|
98
|
+
// Disabled: no alt screen to exit
|
|
106
99
|
}
|
|
107
100
|
function clearContent() {
|
|
108
101
|
w("\x1b[2J\x1b[H"); // Clear screen + cursor to top
|
|
@@ -723,9 +716,14 @@ const DOWNLOAD_URL = "https://moveros.dev/api/download";
|
|
|
723
716
|
|
|
724
717
|
async function downloadPayload(key) {
|
|
725
718
|
const https = require("https");
|
|
726
|
-
const
|
|
727
|
-
fs.mkdirSync(
|
|
728
|
-
|
|
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");
|
|
729
727
|
|
|
730
728
|
// Download tarball
|
|
731
729
|
await new Promise((resolve, reject) => {
|
|
@@ -785,9 +783,9 @@ async function downloadPayload(key) {
|
|
|
785
783
|
req.end();
|
|
786
784
|
});
|
|
787
785
|
|
|
788
|
-
// Extract tarball
|
|
786
|
+
// Extract tarball into ~/.mover/ (produces ~/.mover/src/)
|
|
789
787
|
try {
|
|
790
|
-
execSync(`tar -xzf "${tarPath}" -C "${
|
|
788
|
+
execSync(`tar -xzf "${tarPath}" -C "${moverDir}"`, { stdio: "ignore" });
|
|
791
789
|
} catch {
|
|
792
790
|
throw new Error("Failed to extract payload. Ensure 'tar' is available.");
|
|
793
791
|
}
|
|
@@ -795,7 +793,7 @@ async function downloadPayload(key) {
|
|
|
795
793
|
// Clean up tarball
|
|
796
794
|
try { fs.unlinkSync(tarPath); } catch {}
|
|
797
795
|
|
|
798
|
-
return
|
|
796
|
+
return moverDir;
|
|
799
797
|
}
|
|
800
798
|
|
|
801
799
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
@@ -956,12 +954,15 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
956
954
|
const home = os.homedir();
|
|
957
955
|
const result = { workflows: [], hooks: [], rules: null, templates: [], skills: [], statusline: "unchanged" };
|
|
958
956
|
|
|
957
|
+
// Expand selection IDs to target IDs (e.g. "gemini-cli" → includes "antigravity")
|
|
958
|
+
const targetIds = expandTargetIds(selectedAgentIds);
|
|
959
|
+
|
|
959
960
|
// --- Workflows: compare source vs first installed destination ---
|
|
960
961
|
const wfSrc = path.join(bundleDir, "src", "workflows");
|
|
961
962
|
const wfDests = [
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
963
|
+
targetIds.includes("claude-code") && path.join(home, ".claude", "commands"),
|
|
964
|
+
targetIds.includes("cursor") && path.join(home, ".cursor", "commands"),
|
|
965
|
+
targetIds.includes("antigravity") && path.join(home, ".gemini", "antigravity", "global_workflows"),
|
|
965
966
|
].filter(Boolean);
|
|
966
967
|
const wfDest = wfDests.find((d) => fs.existsSync(d));
|
|
967
968
|
|
|
@@ -984,7 +985,7 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
984
985
|
// --- Hooks: compare with CRLF normalization ---
|
|
985
986
|
const hooksSrc = path.join(bundleDir, "src", "hooks");
|
|
986
987
|
const hooksDest = path.join(home, ".claude", "hooks");
|
|
987
|
-
if (fs.existsSync(hooksSrc) &&
|
|
988
|
+
if (fs.existsSync(hooksSrc) && targetIds.includes("claude-code")) {
|
|
988
989
|
for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh") || f.endsWith(".md"))) {
|
|
989
990
|
const srcContent = fs.readFileSync(path.join(hooksSrc, file), "utf8")
|
|
990
991
|
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -1005,9 +1006,9 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1005
1006
|
// --- Rules: compare source vs first installed destination ---
|
|
1006
1007
|
const rulesSrc = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
1007
1008
|
const rulesDests = [
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1009
|
+
targetIds.includes("claude-code") && path.join(home, ".claude", "CLAUDE.md"),
|
|
1010
|
+
targetIds.includes("cursor") && path.join(home, ".cursor", "rules", "mover-os.mdc"),
|
|
1011
|
+
targetIds.includes("gemini-cli") && path.join(home, ".gemini", "GEMINI.md"),
|
|
1011
1012
|
].filter(Boolean);
|
|
1012
1013
|
const rulesDest = rulesDests.find((d) => fs.existsSync(d));
|
|
1013
1014
|
if (fs.existsSync(rulesSrc) && rulesDest) {
|
|
@@ -1049,9 +1050,9 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1049
1050
|
// --- Skills: compare source vs installed ---
|
|
1050
1051
|
const skillsSrc = path.join(bundleDir, "src", "skills");
|
|
1051
1052
|
const skillsDests = [
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1053
|
+
targetIds.includes("claude-code") && path.join(home, ".claude", "skills"),
|
|
1054
|
+
targetIds.includes("cursor") && path.join(home, ".cursor", "skills"),
|
|
1055
|
+
targetIds.includes("cline") && path.join(home, ".cline", "skills"),
|
|
1055
1056
|
].filter(Boolean);
|
|
1056
1057
|
const skillsDest = skillsDests.find((d) => fs.existsSync(d));
|
|
1057
1058
|
if (fs.existsSync(skillsSrc) && skillsDest) {
|
|
@@ -1081,17 +1082,94 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1081
1082
|
// --- Statusline ---
|
|
1082
1083
|
const slSrc = path.join(bundleDir, "src", "hooks", "statusline.js");
|
|
1083
1084
|
const slDest = path.join(home, ".claude", "statusline.js");
|
|
1084
|
-
if (fs.existsSync(slSrc) && fs.existsSync(slDest) &&
|
|
1085
|
+
if (fs.existsSync(slSrc) && fs.existsSync(slDest) && targetIds.includes("claude-code")) {
|
|
1085
1086
|
const srcSl = fs.readFileSync(slSrc, "utf8").replace(/\r\n/g, "\n");
|
|
1086
1087
|
const destSl = fs.readFileSync(slDest, "utf8").replace(/\r\n/g, "\n");
|
|
1087
1088
|
result.statusline = srcSl === destSl ? "unchanged" : "changed";
|
|
1088
|
-
} else if (fs.existsSync(slSrc) && !fs.existsSync(slDest) &&
|
|
1089
|
+
} else if (fs.existsSync(slSrc) && !fs.existsSync(slDest) && targetIds.includes("claude-code")) {
|
|
1089
1090
|
result.statusline = "new";
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1092
1093
|
return result;
|
|
1093
1094
|
}
|
|
1094
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
|
+
|
|
1095
1173
|
function countChanges(changes) {
|
|
1096
1174
|
let n = 0;
|
|
1097
1175
|
n += changes.workflows.filter((f) => f.status !== "unchanged").length;
|
|
@@ -1627,6 +1705,17 @@ const AGENTS = AGENT_SELECTIONS.map((s) => ({
|
|
|
1627
1705
|
detect: AGENT_REGISTRY[s.targets[0]].detect,
|
|
1628
1706
|
}));
|
|
1629
1707
|
|
|
1708
|
+
// Expand selection IDs to install target IDs (e.g. "gemini-cli" → ["gemini-cli", "antigravity"])
|
|
1709
|
+
function expandTargetIds(selectionIds) {
|
|
1710
|
+
const result = [];
|
|
1711
|
+
for (const id of selectionIds) {
|
|
1712
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === id);
|
|
1713
|
+
if (sel) { for (const t of sel.targets) result.push(t); }
|
|
1714
|
+
else result.push(id);
|
|
1715
|
+
}
|
|
1716
|
+
return [...new Set(result)];
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1630
1719
|
// ─── Utility functions ──────────────────────────────────────────────────────
|
|
1631
1720
|
function cmdExists(cmd) {
|
|
1632
1721
|
try {
|
|
@@ -1991,19 +2080,19 @@ async function fetchPrayerTimes(city, country) {
|
|
|
1991
2080
|
const https = require("https");
|
|
1992
2081
|
const year = new Date().getFullYear();
|
|
1993
2082
|
const allTimes = {};
|
|
2083
|
+
let failedMonths = 0;
|
|
1994
2084
|
|
|
1995
2085
|
for (let month = 1; month <= 12; month++) {
|
|
1996
2086
|
try {
|
|
1997
2087
|
const url = `https://api.aladhan.com/v1/calendarByCity/${year}/${month}?city=${encodeURIComponent(city)}&country=${encodeURIComponent(country)}&method=15`;
|
|
1998
2088
|
const body = await new Promise((resolve, reject) => {
|
|
1999
|
-
const req = https.
|
|
2089
|
+
const req = https.get(url, { timeout: 10000 }, (res) => {
|
|
2000
2090
|
let data = "";
|
|
2001
2091
|
res.on("data", (c) => (data += c));
|
|
2002
2092
|
res.on("end", () => resolve(data));
|
|
2003
2093
|
});
|
|
2004
2094
|
req.on("error", reject);
|
|
2005
2095
|
req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
|
|
2006
|
-
req.end();
|
|
2007
2096
|
});
|
|
2008
2097
|
|
|
2009
2098
|
const json = JSON.parse(body);
|
|
@@ -2020,9 +2109,11 @@ async function fetchPrayerTimes(city, country) {
|
|
|
2020
2109
|
isha: t.Isha.replace(/\s*\(.*\)/, ""),
|
|
2021
2110
|
};
|
|
2022
2111
|
}
|
|
2112
|
+
} else {
|
|
2113
|
+
failedMonths++;
|
|
2023
2114
|
}
|
|
2024
2115
|
} catch {
|
|
2025
|
-
|
|
2116
|
+
failedMonths++;
|
|
2026
2117
|
}
|
|
2027
2118
|
}
|
|
2028
2119
|
|
|
@@ -2113,6 +2204,8 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey, opts = {}) {
|
|
|
2113
2204
|
if (existing.settings) config.settings = { ...existing.settings };
|
|
2114
2205
|
// Preserve prayer_times fallback
|
|
2115
2206
|
if (existing.prayer_times) config.prayer_times = existing.prayer_times;
|
|
2207
|
+
// Preserve frecency data
|
|
2208
|
+
if (existing.cli_usage) config.cli_usage = existing.cli_usage;
|
|
2116
2209
|
config.updatedAt = new Date().toISOString();
|
|
2117
2210
|
} catch {}
|
|
2118
2211
|
}
|
|
@@ -3031,7 +3124,8 @@ async function cmdDoctor(opts) {
|
|
|
3031
3124
|
// Per-agent checks
|
|
3032
3125
|
barLn();
|
|
3033
3126
|
barLn(dim(" Agents:"));
|
|
3034
|
-
|
|
3127
|
+
let cfgData = {};
|
|
3128
|
+
if (fs.existsSync(cfg)) { try { cfgData = JSON.parse(fs.readFileSync(cfg, "utf8")); } catch { statusLine("warn", "Config", "corrupt JSON"); } }
|
|
3035
3129
|
const installedAgents = cfgData.agents || [];
|
|
3036
3130
|
if (installedAgents.length === 0) {
|
|
3037
3131
|
barLn(dim(" No agents recorded in config."));
|
|
@@ -3057,7 +3151,7 @@ async function cmdDoctor(opts) {
|
|
|
3057
3151
|
const hasCmds = fs.existsSync(cp) && fs.readdirSync(cp).length > 0;
|
|
3058
3152
|
checks.push(hasCmds ? "commands" : dim("no commands"));
|
|
3059
3153
|
}
|
|
3060
|
-
const allOk = checks.every((c) =>
|
|
3154
|
+
const allOk = checks.every((c) => c === strip(c)); // styled text = problem
|
|
3061
3155
|
statusLine(allOk ? "ok" : "warn", ` ${reg.name}`, checks.join(", "));
|
|
3062
3156
|
}
|
|
3063
3157
|
|
|
@@ -3102,7 +3196,7 @@ async function cmdPulse(opts) {
|
|
|
3102
3196
|
const focusMatch = ac.match(/\*\*Focus:\*\*\s*(.+)/i) || ac.match(/Single Test:\s*(.+)/i);
|
|
3103
3197
|
if (focusMatch) focus = focusMatch[1].trim();
|
|
3104
3198
|
// Blockers
|
|
3105
|
-
const blockerSection = ac.match(/##.*Blocker[s]?[\s\S]*?(?=\n##|\n
|
|
3199
|
+
const blockerSection = ac.match(/##.*Blocker[s]?[\s\S]*?(?=\n##|\n---|$)/i);
|
|
3106
3200
|
if (blockerSection) {
|
|
3107
3201
|
const lines = blockerSection[0].split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*"));
|
|
3108
3202
|
blockers = lines.map((l) => l.replace(/^[\s*-]+/, "").trim()).filter(Boolean).slice(0, 3);
|
|
@@ -3491,7 +3585,7 @@ async function cmdReplay(opts) {
|
|
|
3491
3585
|
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
3492
3586
|
|
|
3493
3587
|
// Extract session log entries
|
|
3494
|
-
const logMatch = daily.match(/##\s*Session Log[\s\S]*?(?=\n## [^#]
|
|
3588
|
+
const logMatch = daily.match(/##\s*Session Log[\s\S]*?(?=\n## [^#]|$)/i);
|
|
3495
3589
|
if (!logMatch) {
|
|
3496
3590
|
barLn(dim(" No session log found in this Daily Note."));
|
|
3497
3591
|
return;
|
|
@@ -3766,27 +3860,29 @@ async function cmdPrayer(opts) {
|
|
|
3766
3860
|
barLn();
|
|
3767
3861
|
const city = await textInput({ label: "City", placeholder: "London" });
|
|
3768
3862
|
if (city === null) return;
|
|
3863
|
+
if (!city.trim()) { barLn(yellow(" City cannot be empty.")); return; }
|
|
3769
3864
|
const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
|
|
3770
3865
|
if (country === null) return;
|
|
3866
|
+
if (!country.trim()) { barLn(yellow(" Country cannot be empty.")); return; }
|
|
3771
3867
|
barLn();
|
|
3772
3868
|
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
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"));
|
|
3784
3885
|
}
|
|
3785
|
-
|
|
3786
|
-
// Auto-enable
|
|
3787
|
-
if (!cfg.settings) cfg.settings = {};
|
|
3788
|
-
cfg.settings.show_prayer_times = true;
|
|
3789
|
-
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
3790
3886
|
} else if (choice === "paste") {
|
|
3791
3887
|
barLn();
|
|
3792
3888
|
barLn(dim(" Paste your mosque's timetable below."));
|
|
@@ -4046,10 +4142,11 @@ async function cmdRestore(opts) {
|
|
|
4046
4142
|
const archivesDir = path.join(vault, "04_Archives");
|
|
4047
4143
|
if (!fs.existsSync(archivesDir)) { barLn(yellow(" No archives found.")); return; }
|
|
4048
4144
|
|
|
4145
|
+
const backupPrefixes = ["Backup_", "Engine_Backup_", "Areas_Backup_", "Agent_Backup_"];
|
|
4049
4146
|
const backups = fs.readdirSync(archivesDir)
|
|
4050
4147
|
.filter((d) => {
|
|
4051
4148
|
const full = path.join(archivesDir, d);
|
|
4052
|
-
return fs.statSync(full).isDirectory() &&
|
|
4149
|
+
return fs.statSync(full).isDirectory() && backupPrefixes.some((p) => d.startsWith(p));
|
|
4053
4150
|
})
|
|
4054
4151
|
.sort()
|
|
4055
4152
|
.reverse();
|
|
@@ -4077,25 +4174,67 @@ async function cmdRestore(opts) {
|
|
|
4077
4174
|
|
|
4078
4175
|
const backupPath = path.join(archivesDir, selected);
|
|
4079
4176
|
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
if (
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4177
|
+
let totalRestored = 0;
|
|
4178
|
+
|
|
4179
|
+
if (selected.startsWith("Areas_Backup_")) {
|
|
4180
|
+
// Areas backup: restore entire 02_Areas folder
|
|
4181
|
+
const areasTarget = path.join(vault, "02_Areas");
|
|
4182
|
+
copyDirRecursive(backupPath, areasTarget);
|
|
4183
|
+
statusLine("ok", "Areas", "folder restored");
|
|
4184
|
+
totalRestored++;
|
|
4185
|
+
} else if (selected.startsWith("Agent_Backup_")) {
|
|
4186
|
+
// Agent backup: restore rules/skills per agent
|
|
4187
|
+
let agentCount = 0;
|
|
4188
|
+
for (const agDir of fs.readdirSync(backupPath).filter((d) => fs.statSync(path.join(backupPath, d)).isDirectory())) {
|
|
4189
|
+
const reg = AGENT_REGISTRY[agDir];
|
|
4190
|
+
if (!reg) continue;
|
|
4191
|
+
const srcDir = path.join(backupPath, agDir);
|
|
4192
|
+
// Restore rules file
|
|
4193
|
+
if (reg.rules && reg.rules.dest) {
|
|
4194
|
+
const rulesFile = fs.readdirSync(srcDir).find((f) => !f.startsWith("skills") && fs.statSync(path.join(srcDir, f)).isFile());
|
|
4195
|
+
if (rulesFile) {
|
|
4196
|
+
try {
|
|
4197
|
+
const dest = reg.rules.dest(vault);
|
|
4198
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
4199
|
+
fs.copyFileSync(path.join(srcDir, rulesFile), dest);
|
|
4200
|
+
agentCount++;
|
|
4201
|
+
} catch {}
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
// Restore skills directory
|
|
4205
|
+
const skillsSrc = path.join(srcDir, "skills");
|
|
4206
|
+
if (fs.existsSync(skillsSrc) && reg.skills && reg.skills.dest) {
|
|
4207
|
+
try {
|
|
4208
|
+
copyDirRecursive(skillsSrc, reg.skills.dest(vault));
|
|
4209
|
+
agentCount++;
|
|
4210
|
+
} catch {}
|
|
4211
|
+
}
|
|
4088
4212
|
}
|
|
4089
|
-
statusLine("ok", "
|
|
4213
|
+
statusLine("ok", "Agents", `${agentCount} items restored`);
|
|
4214
|
+
totalRestored = agentCount;
|
|
4090
4215
|
} else {
|
|
4091
|
-
//
|
|
4092
|
-
const
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4216
|
+
// Engine backup (Engine_Backup_ or legacy Backup_)
|
|
4217
|
+
const engPath = path.join(backupPath, "engine");
|
|
4218
|
+
if (fs.existsSync(engPath)) {
|
|
4219
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
4220
|
+
let restored = 0;
|
|
4221
|
+
for (const f of fs.readdirSync(engPath).filter((f) => fs.statSync(path.join(engPath, f)).isFile())) {
|
|
4222
|
+
fs.copyFileSync(path.join(engPath, f), path.join(engineDir, f));
|
|
4223
|
+
restored++;
|
|
4224
|
+
}
|
|
4225
|
+
statusLine("ok", "Engine", `${restored} files restored`);
|
|
4226
|
+
totalRestored = restored;
|
|
4227
|
+
} else {
|
|
4228
|
+
// Legacy backup format (files directly in backup dir)
|
|
4229
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
4230
|
+
let restored = 0;
|
|
4231
|
+
for (const f of fs.readdirSync(backupPath).filter((f) => f.endsWith(".md") && f !== ".backup-manifest.json")) {
|
|
4232
|
+
fs.copyFileSync(path.join(backupPath, f), path.join(engineDir, f));
|
|
4233
|
+
restored++;
|
|
4234
|
+
}
|
|
4235
|
+
if (restored > 0) statusLine("ok", "Engine", `${restored} files restored`);
|
|
4236
|
+
totalRestored = restored;
|
|
4097
4237
|
}
|
|
4098
|
-
if (restored > 0) statusLine("ok", "Engine", `${restored} files restored`);
|
|
4099
4238
|
}
|
|
4100
4239
|
|
|
4101
4240
|
barLn();
|
|
@@ -4597,7 +4736,12 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4597
4736
|
barLn(`${yellow("CLI update available:")} ${dim(localVer)} ${dim("\u2192")} ${green(npmVer)}`);
|
|
4598
4737
|
const sp = spinner("Updating CLI");
|
|
4599
4738
|
try {
|
|
4600
|
-
|
|
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
|
+
}
|
|
4601
4745
|
sp.stop(`CLI updated to ${npmVer}`);
|
|
4602
4746
|
barLn(dim(" Re-running with updated CLI..."));
|
|
4603
4747
|
barLn();
|
|
@@ -4609,7 +4753,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4609
4753
|
process.exit(result.status || 0);
|
|
4610
4754
|
} catch (e) {
|
|
4611
4755
|
sp.stop(yellow(`CLI self-update failed: ${e.message}`));
|
|
4612
|
-
barLn(dim("
|
|
4756
|
+
barLn(dim(" Try: sudo npm i -g mover-os"));
|
|
4613
4757
|
}
|
|
4614
4758
|
} else {
|
|
4615
4759
|
statusLine("ok", "CLI", `up to date (${localVer})`);
|
|
@@ -4678,123 +4822,54 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4678
4822
|
newVer = pkg.version || newVer;
|
|
4679
4823
|
} catch {}
|
|
4680
4824
|
|
|
4681
|
-
//
|
|
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)
|
|
4682
4833
|
if (isQuick) {
|
|
4683
|
-
const detectedAgents = AGENTS.filter((a) => a.detect());
|
|
4684
4834
|
if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
|
|
4685
|
-
const selectedIds = detectedAgents.map((a) => a.id);
|
|
4686
4835
|
const changes = detectChanges(bundleDir, vaultPath, selectedIds);
|
|
4687
4836
|
const totalChanged = countChanges(changes);
|
|
4688
4837
|
displayChangeSummary(changes, installedVer, newVer);
|
|
4689
4838
|
if (totalChanged === 0) { outro(green("Already up to date.")); return; }
|
|
4839
|
+
autoBackupBeforeUpdate(changes, selectedIds, vaultPath);
|
|
4690
4840
|
barLn(bold("Updating..."));
|
|
4691
4841
|
barLn();
|
|
4692
4842
|
createVaultStructure(vaultPath);
|
|
4693
4843
|
installTemplateFiles(bundleDir, vaultPath);
|
|
4694
4844
|
const writtenFiles = new Set();
|
|
4695
|
-
const skillOpts = { install: true, categories: null, workflows: null };
|
|
4845
|
+
const skillOpts = { install: true, categories: null, workflows: null, statusLine: changes.statusline !== "unchanged" };
|
|
4696
4846
|
for (const agent of detectedAgents) {
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
const
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4847
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
4848
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
4849
|
+
for (const targetId of targets) {
|
|
4850
|
+
const fn = AGENT_INSTALLERS[targetId];
|
|
4851
|
+
if (!fn) continue;
|
|
4852
|
+
const targetReg = AGENT_REGISTRY[targetId];
|
|
4853
|
+
const displayName = targetReg ? targetReg.name : agent.name;
|
|
4854
|
+
const sp = spinner(displayName);
|
|
4855
|
+
const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId);
|
|
4856
|
+
await sleep(200);
|
|
4857
|
+
sp.stop(steps.length > 0 ? `${displayName} ${dim(steps.join(", "))}` : `${displayName} ${dim("configured")}`);
|
|
4858
|
+
}
|
|
4703
4859
|
}
|
|
4704
4860
|
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4705
4861
|
writeMoverConfig(vaultPath, selectedIds);
|
|
4706
4862
|
barLn();
|
|
4707
4863
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4708
|
-
outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s
|
|
4864
|
+
outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s.`);
|
|
4709
4865
|
return;
|
|
4710
4866
|
}
|
|
4711
4867
|
|
|
4868
|
+
// ── Interactive mode: stage + summary ──
|
|
4712
4869
|
// Step 4: What's New
|
|
4713
4870
|
showWhatsNew(installedVer, newVer);
|
|
4714
4871
|
|
|
4715
|
-
// Step 5:
|
|
4716
|
-
const engine = detectEngineFiles(vaultPath);
|
|
4717
|
-
if (engine.exists) {
|
|
4718
|
-
question("Back up before updating?");
|
|
4719
|
-
barLn(dim(" Select what to save. Esc to skip."));
|
|
4720
|
-
barLn();
|
|
4721
|
-
const backupItems = [
|
|
4722
|
-
{ id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, Context" },
|
|
4723
|
-
];
|
|
4724
|
-
if (fs.existsSync(path.join(vaultPath, "02_Areas"))) {
|
|
4725
|
-
backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/" });
|
|
4726
|
-
}
|
|
4727
|
-
const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
|
|
4728
|
-
if (detectedForBackup.length > 0) {
|
|
4729
|
-
backupItems.push({ id: "agents", name: "Agent configs", tier: `Rules, skills from ${detectedForBackup.length} agent(s)` });
|
|
4730
|
-
}
|
|
4731
|
-
const backupChoices = await interactiveSelect(backupItems, { multi: true, preSelected: ["engine"] });
|
|
4732
|
-
if (backupChoices && backupChoices.length > 0) {
|
|
4733
|
-
const now = new Date();
|
|
4734
|
-
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")}`;
|
|
4735
|
-
const archivesDir = path.join(vaultPath, "04_Archives");
|
|
4736
|
-
if (backupChoices.includes("engine")) {
|
|
4737
|
-
const engineDir = path.join(vaultPath, "02_Areas", "Engine");
|
|
4738
|
-
const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
|
|
4739
|
-
try {
|
|
4740
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
4741
|
-
let backed = 0;
|
|
4742
|
-
for (const f of fs.readdirSync(engineDir).filter(f => fs.statSync(path.join(engineDir, f)).isFile())) {
|
|
4743
|
-
fs.copyFileSync(path.join(engineDir, f), path.join(backupDir, f));
|
|
4744
|
-
backed++;
|
|
4745
|
-
}
|
|
4746
|
-
statusLine("ok", "Backed up", `${backed} Engine files`);
|
|
4747
|
-
} catch (err) { barLn(yellow(` Backup failed: ${err.message}`)); }
|
|
4748
|
-
}
|
|
4749
|
-
if (backupChoices.includes("areas")) {
|
|
4750
|
-
try {
|
|
4751
|
-
copyDirRecursive(path.join(vaultPath, "02_Areas"), path.join(archivesDir, `Areas_Backup_${ts}`));
|
|
4752
|
-
statusLine("ok", "Backed up", "Full Areas folder");
|
|
4753
|
-
} catch (err) { barLn(yellow(` Areas backup failed: ${err.message}`)); }
|
|
4754
|
-
}
|
|
4755
|
-
if (backupChoices.includes("agents")) {
|
|
4756
|
-
try {
|
|
4757
|
-
const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
|
|
4758
|
-
fs.mkdirSync(agentBackupDir, { recursive: true });
|
|
4759
|
-
let agentsBacked = 0;
|
|
4760
|
-
for (const ag of AGENTS.filter((a) => a.detect())) {
|
|
4761
|
-
const agDir = path.join(agentBackupDir, ag.id);
|
|
4762
|
-
fs.mkdirSync(agDir, { recursive: true });
|
|
4763
|
-
for (const cp of (ag.configPaths || [])) {
|
|
4764
|
-
if (fs.existsSync(cp.src)) {
|
|
4765
|
-
try { fs.copyFileSync(cp.src, path.join(agDir, path.basename(cp.src))); agentsBacked++; } catch {}
|
|
4766
|
-
}
|
|
4767
|
-
}
|
|
4768
|
-
}
|
|
4769
|
-
statusLine("ok", "Backed up", `${agentsBacked} agent config files`);
|
|
4770
|
-
} catch (err) { barLn(yellow(` Agent backup failed: ${err.message}`)); }
|
|
4771
|
-
}
|
|
4772
|
-
}
|
|
4773
|
-
barLn();
|
|
4774
|
-
}
|
|
4775
|
-
|
|
4776
|
-
// Step 6: Agent Management
|
|
4777
|
-
const visibleAgents = AGENTS.filter((a) => !a.hidden);
|
|
4778
|
-
const detectedIds = visibleAgents.filter((a) => a.detect()).map((a) => a.id);
|
|
4779
|
-
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
4780
|
-
let currentAgents = [];
|
|
4781
|
-
if (fs.existsSync(cfgPath)) {
|
|
4782
|
-
try { currentAgents = JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || []; } catch {}
|
|
4783
|
-
}
|
|
4784
|
-
const preSelectedAgents = [...new Set([...detectedIds, ...currentAgents])];
|
|
4785
|
-
|
|
4786
|
-
question(`Agents ${dim("(add or remove)")}`);
|
|
4787
|
-
barLn();
|
|
4788
|
-
const agentItems = visibleAgents.map((a) => ({
|
|
4789
|
-
...a,
|
|
4790
|
-
_detected: detectedIds.includes(a.id),
|
|
4791
|
-
}));
|
|
4792
|
-
const selectedIds = await interactiveSelect(agentItems, { multi: true, preSelected: preSelectedAgents });
|
|
4793
|
-
if (!selectedIds || selectedIds.length === 0) return;
|
|
4794
|
-
const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
|
|
4795
|
-
barLn();
|
|
4796
|
-
|
|
4797
|
-
// Step 7: Change Detection
|
|
4872
|
+
// Step 5: Change Detection (compare staged vs installed)
|
|
4798
4873
|
const changes = detectChanges(bundleDir, vaultPath, selectedIds);
|
|
4799
4874
|
const totalChanged = countChanges(changes);
|
|
4800
4875
|
question("Change Summary");
|
|
@@ -4804,121 +4879,21 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
4804
4879
|
if (totalChanged === 0) {
|
|
4805
4880
|
barLn(green(" Already up to date."));
|
|
4806
4881
|
barLn();
|
|
4807
|
-
|
|
4808
|
-
outro(`${green("Done.")} No changes needed. ${dim(`(${elapsed}s)`)}`);
|
|
4882
|
+
outro(green("No changes needed. Engine files may still need migration — run /update."));
|
|
4809
4883
|
return;
|
|
4810
4884
|
}
|
|
4811
4885
|
|
|
4812
|
-
|
|
4813
|
-
{ id: "all", name: "Yes, update all changed files", tier: "" },
|
|
4814
|
-
{ id: "select", name: "Select individually", tier: "" },
|
|
4815
|
-
{ id: "cancel", name: "Cancel", tier: "" },
|
|
4816
|
-
], { multi: false, defaultIndex: 0 });
|
|
4817
|
-
if (!applyChoice || applyChoice === "cancel") { outro("Cancelled."); return; }
|
|
4818
|
-
|
|
4819
|
-
let selectedWorkflows = null;
|
|
4820
|
-
let skipHooks = false, skipRules = false, skipTemplates = false;
|
|
4821
|
-
if (applyChoice === "select") {
|
|
4822
|
-
const changedItems = [];
|
|
4823
|
-
const changedPreSelected = [];
|
|
4824
|
-
for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
|
|
4825
|
-
const id = `wf:${f.file}`;
|
|
4826
|
-
changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new" : "changed") });
|
|
4827
|
-
changedPreSelected.push(id);
|
|
4828
|
-
}
|
|
4829
|
-
for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
|
|
4830
|
-
const id = `hook:${f.file}`;
|
|
4831
|
-
changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
|
|
4832
|
-
changedPreSelected.push(id);
|
|
4833
|
-
}
|
|
4834
|
-
if (changes.rules === "changed") {
|
|
4835
|
-
changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
|
|
4836
|
-
changedPreSelected.push("rules");
|
|
4837
|
-
}
|
|
4838
|
-
for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
|
|
4839
|
-
const id = `tmpl:${f.file}`;
|
|
4840
|
-
changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new" : "changed") });
|
|
4841
|
-
changedPreSelected.push(id);
|
|
4842
|
-
}
|
|
4843
|
-
if (changedItems.length > 0) {
|
|
4844
|
-
question("Select files to update");
|
|
4845
|
-
barLn();
|
|
4846
|
-
const selectedFileIds = await interactiveSelect(changedItems, { multi: true, preSelected: changedPreSelected });
|
|
4847
|
-
if (!selectedFileIds) return;
|
|
4848
|
-
const selectedWfFiles = selectedFileIds.filter((id) => id.startsWith("wf:")).map((id) => id.slice(3));
|
|
4849
|
-
if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
|
|
4850
|
-
selectedWorkflows = new Set(selectedWfFiles);
|
|
4851
|
-
}
|
|
4852
|
-
skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
|
|
4853
|
-
skipRules = !selectedFileIds.includes("rules");
|
|
4854
|
-
skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
|
|
4855
|
-
}
|
|
4856
|
-
}
|
|
4857
|
-
|
|
4858
|
-
// Step 8: Apply Changes (with progress animation)
|
|
4886
|
+
// Stage confirmation
|
|
4859
4887
|
barLn();
|
|
4860
|
-
|
|
4888
|
+
statusLine("ok", "Staged", `${totalChanged} updated files at ${dim("~/.mover/src/")}`);
|
|
4861
4889
|
barLn();
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
installSteps.push({ label: "Template files", fn: async () => { installTemplateFiles(bundleDir, vaultPath); await sleep(100); } });
|
|
4867
|
-
}
|
|
4868
|
-
|
|
4869
|
-
const writtenFiles = new Set();
|
|
4870
|
-
const skillOpts = { install: true, categories: null, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
|
|
4871
|
-
for (const agent of selectedAgents) {
|
|
4872
|
-
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
4873
|
-
const targets = sel ? sel.targets : [agent.id];
|
|
4874
|
-
for (const targetId of targets) {
|
|
4875
|
-
const fn = AGENT_INSTALLERS[targetId];
|
|
4876
|
-
if (!fn) continue;
|
|
4877
|
-
const targetReg = AGENT_REGISTRY[targetId];
|
|
4878
|
-
const displayName = targetReg ? targetReg.name : agent.name;
|
|
4879
|
-
installSteps.push({ label: displayName, fn: async () => { fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId); await sleep(150); } });
|
|
4880
|
-
}
|
|
4881
|
-
}
|
|
4882
|
-
|
|
4883
|
-
await installProgress(installSteps);
|
|
4884
|
-
|
|
4885
|
-
// Step 9: Skills Refresh
|
|
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."));
|
|
4886
4894
|
barLn();
|
|
4887
|
-
const allSkills = findSkills(bundleDir);
|
|
4888
|
-
if (allSkills.length > 0 && selectedAgents.some((a) => a.id !== "aider")) {
|
|
4889
|
-
question("Refresh skill categories?");
|
|
4890
|
-
barLn();
|
|
4891
|
-
const catCounts = {};
|
|
4892
|
-
for (const sk of allSkills) { catCounts[sk.category] = (catCounts[sk.category] || 0) + 1; }
|
|
4893
|
-
const categoryItems = CATEGORY_META.map((c) => ({
|
|
4894
|
-
id: c.id,
|
|
4895
|
-
name: `${c.name} ${dim(`(${catCounts[c.id] || 0})`)}`,
|
|
4896
|
-
tier: dim(c.desc),
|
|
4897
|
-
}));
|
|
4898
|
-
const selectedCatIds = await interactiveSelect(categoryItems, { multi: true, preSelected: ["development", "obsidian"] });
|
|
4899
|
-
if (selectedCatIds && selectedCatIds.length > 0) {
|
|
4900
|
-
const catSet = new Set(selectedCatIds);
|
|
4901
|
-
const refreshOpts = { install: true, categories: catSet, workflows: null };
|
|
4902
|
-
for (const agent of selectedAgents) {
|
|
4903
|
-
const fn = AGENT_INSTALLERS[agent.id];
|
|
4904
|
-
if (fn) fn(bundleDir, vaultPath, refreshOpts, writtenFiles, agent.id);
|
|
4905
|
-
}
|
|
4906
|
-
const skillCount = allSkills.filter((s) => s.category === "tools" || catSet.has(s.category)).length;
|
|
4907
|
-
statusLine("ok", "Skills refreshed", `${skillCount} across ${selectedAgents.length} agent(s)`);
|
|
4908
|
-
}
|
|
4909
|
-
barLn();
|
|
4910
|
-
}
|
|
4911
|
-
|
|
4912
|
-
// Update version marker + config
|
|
4913
|
-
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
4914
|
-
writeMoverConfig(vaultPath, selectedIds, updateKey);
|
|
4915
|
-
|
|
4916
|
-
// Step 10: Summary + Success
|
|
4917
|
-
barLn();
|
|
4918
|
-
await successAnimation(`Mover OS updated — ${totalChanged} files`);
|
|
4919
|
-
|
|
4920
4895
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
4921
|
-
outro(
|
|
4896
|
+
outro(`Files staged. Run ${bold("/update")} to apply. ${dim(`(${elapsed}s)`)}`);
|
|
4922
4897
|
}
|
|
4923
4898
|
|
|
4924
4899
|
// ─── Vault Resolution Helper ─────────────────────────────────────────────────
|
|
@@ -5284,8 +5259,8 @@ async function main() {
|
|
|
5284
5259
|
],
|
|
5285
5260
|
{ multi: false, defaultIndex: 1 }
|
|
5286
5261
|
);
|
|
5287
|
-
if (!ptChoice)
|
|
5288
|
-
if (ptChoice === "yes") {
|
|
5262
|
+
if (!ptChoice || ptChoice === "no") { /* skip prayer setup */ }
|
|
5263
|
+
else if (ptChoice === "yes") {
|
|
5289
5264
|
prayerSetup = true;
|
|
5290
5265
|
barLn();
|
|
5291
5266
|
question("How would you like to set up prayer times?");
|
|
@@ -5299,7 +5274,9 @@ async function main() {
|
|
|
5299
5274
|
],
|
|
5300
5275
|
{ multi: false, defaultIndex: 0 }
|
|
5301
5276
|
);
|
|
5302
|
-
if (!method || method === "later") {
|
|
5277
|
+
if (!method || method === "later") {
|
|
5278
|
+
// User cancelled method pick — still enable the setting, no timetable
|
|
5279
|
+
} else {
|
|
5303
5280
|
|
|
5304
5281
|
const moverDir = path.join(os.homedir(), ".mover");
|
|
5305
5282
|
if (!fs.existsSync(moverDir)) fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
|
|
@@ -5351,26 +5328,31 @@ async function main() {
|
|
|
5351
5328
|
} else if (method === "fetch") {
|
|
5352
5329
|
barLn();
|
|
5353
5330
|
const city = await textInput({ label: "City (e.g. London, Watford, Istanbul)", placeholder: "London" });
|
|
5354
|
-
if (city
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
const sp = spinner("Fetching prayer times");
|
|
5361
|
-
const tt = await fetchPrayerTimes(city.trim(), country.trim());
|
|
5362
|
-
if (tt && Object.keys(tt.times).length > 0) {
|
|
5363
|
-
const ttPath = path.join(moverDir, "prayer-timetable.json");
|
|
5364
|
-
fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
|
|
5365
|
-
sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
|
|
5366
|
-
barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
|
|
5367
|
-
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."));
|
|
5368
5337
|
} else {
|
|
5369
|
-
|
|
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
|
+
}
|
|
5370
5351
|
}
|
|
5371
5352
|
}
|
|
5372
5353
|
}
|
|
5373
5354
|
// method === "later" → just enable the setting, no timetable yet
|
|
5355
|
+
} // end if method !== "later"
|
|
5374
5356
|
}
|
|
5375
5357
|
}
|
|
5376
5358
|
|