mover-os 4.7.2 → 4.7.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 +91 -14
- package/package.json +8 -1
package/install.js
CHANGED
|
@@ -17,6 +17,18 @@ const { execSync } = require("child_process");
|
|
|
17
17
|
|
|
18
18
|
const VERSION = "4";
|
|
19
19
|
|
|
20
|
+
// ─── Windows path normalization ─────────────────────────────────────────────
|
|
21
|
+
// Git Bash / MSYS / WSL surface paths like "/c/Users/foo" inside terminals
|
|
22
|
+
// that Node treats as drive-less when run from PowerShell or cmd. path.resolve
|
|
23
|
+
// then prepends the *current* drive, producing "C:\c\Users\foo". Convert
|
|
24
|
+
// "/c/Users/foo" → "C:\Users\foo" before any path.join(vaultPath, ...) call.
|
|
25
|
+
function normalizeWinPath(p) {
|
|
26
|
+
if (process.platform !== "win32" || !p) return p;
|
|
27
|
+
const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
|
|
28
|
+
if (m) return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
// ─── JSON output helper ──────────────────────────────────────────────────────
|
|
21
33
|
function jsonOut(command, data, ok = true) {
|
|
22
34
|
const envelope = {
|
|
@@ -749,10 +761,32 @@ const POLAR_ORG_ID = process.env.POLAR_ORG_ID || "ba863394-6bca-4965-952a-06b7c0
|
|
|
749
761
|
// ─── Payload Download ────────────────────────────────────────────────────────
|
|
750
762
|
const DOWNLOAD_URL = "https://moveros.dev/api/download";
|
|
751
763
|
|
|
764
|
+
// Generates or reads a stable machine ID. Hardware-derived hash so the same
|
|
765
|
+
// physical machine produces the same ID even if the file is deleted (defeats
|
|
766
|
+
// casual sharing where someone copies just the license key but not the file).
|
|
767
|
+
function getMachineId(moverDir) {
|
|
768
|
+
const idPath = path.join(moverDir, ".machine-id");
|
|
769
|
+
// If file exists and is well-formed, reuse it
|
|
770
|
+
try {
|
|
771
|
+
const existing = fs.readFileSync(idPath, "utf8").trim();
|
|
772
|
+
if (existing && /^[a-f0-9]{32,}$/i.test(existing)) return existing;
|
|
773
|
+
} catch { /* file doesn't exist, fall through */ }
|
|
774
|
+
// Generate hardware-derived ID: hash(cpu_model + hostname + os_release + platform)
|
|
775
|
+
const crypto = require("crypto");
|
|
776
|
+
const cpuModel = (os.cpus()[0] && os.cpus()[0].model) || "unknown-cpu";
|
|
777
|
+
const fingerprint = `${cpuModel}|${os.hostname()}|${os.release()}|${os.platform()}|${os.arch()}`;
|
|
778
|
+
const id = crypto.createHash("sha256").update(fingerprint).digest("hex");
|
|
779
|
+
try {
|
|
780
|
+
fs.writeFileSync(idPath, id, { mode: 0o600 });
|
|
781
|
+
} catch { /* non-fatal — we'll just regenerate next run */ }
|
|
782
|
+
return id;
|
|
783
|
+
}
|
|
784
|
+
|
|
752
785
|
async function downloadPayload(key) {
|
|
753
786
|
const https = require("https");
|
|
754
787
|
const moverDir = path.join(os.homedir(), ".mover");
|
|
755
788
|
fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
|
|
789
|
+
const machineId = getMachineId(moverDir);
|
|
756
790
|
// Clean old staged src/ before extracting fresh
|
|
757
791
|
const stagedSrc = path.join(moverDir, "src");
|
|
758
792
|
if (fs.existsSync(stagedSrc)) {
|
|
@@ -767,12 +801,16 @@ async function downloadPayload(key) {
|
|
|
767
801
|
hostname: url.hostname,
|
|
768
802
|
path: url.pathname,
|
|
769
803
|
method: "GET",
|
|
770
|
-
headers: { "X-License-Key": key.trim() },
|
|
804
|
+
headers: { "X-License-Key": key.trim(), "X-Machine-Id": machineId },
|
|
771
805
|
timeout: 60000,
|
|
772
806
|
}, (res) => {
|
|
773
807
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
774
808
|
// Follow redirect — only to trusted domains
|
|
775
809
|
const redirectUrl = new URL(res.headers.location);
|
|
810
|
+
if (redirectUrl.protocol !== 'https:') {
|
|
811
|
+
reject(new Error('Refusing non-HTTPS redirect'));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
776
814
|
const trusted = ["github.com", "objects.githubusercontent.com", "moveros.dev"];
|
|
777
815
|
if (!trusted.some((d) => redirectUrl.hostname === d || redirectUrl.hostname.endsWith("." + d))) {
|
|
778
816
|
reject(new Error("Untrusted redirect domain"));
|
|
@@ -802,6 +840,12 @@ async function downloadPayload(key) {
|
|
|
802
840
|
res.on("end", () => reject(new Error("License key rejected by server")));
|
|
803
841
|
return;
|
|
804
842
|
}
|
|
843
|
+
if (res.statusCode === 403) {
|
|
844
|
+
let body = "";
|
|
845
|
+
res.on("data", (c) => body += c);
|
|
846
|
+
res.on("end", () => reject(new Error("License at activation cap. Email support@moveros.dev to free a slot.")));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
805
849
|
if (res.statusCode !== 200) {
|
|
806
850
|
reject(new Error(`Download failed (HTTP ${res.statusCode})`));
|
|
807
851
|
return;
|
|
@@ -818,6 +862,15 @@ async function downloadPayload(key) {
|
|
|
818
862
|
req.end();
|
|
819
863
|
});
|
|
820
864
|
|
|
865
|
+
// Validate tar contents before extraction (zip-slip prevention)
|
|
866
|
+
const listing = execSync(`tar -tzf "${tarPath}"`, { encoding: 'utf8' });
|
|
867
|
+
const entries = listing.split('\n').filter(Boolean);
|
|
868
|
+
const badPaths = entries.filter(e => e.startsWith('/') || e.includes('..'));
|
|
869
|
+
if (badPaths.length > 0) {
|
|
870
|
+
fs.unlinkSync(tarPath);
|
|
871
|
+
throw new Error('Payload contains unsafe paths: ' + badPaths.join(', '));
|
|
872
|
+
}
|
|
873
|
+
|
|
821
874
|
// Extract tarball into ~/.mover/ (produces ~/.mover/src/)
|
|
822
875
|
try {
|
|
823
876
|
execSync(`tar -xzf "${tarPath}" -C "${moverDir}"`, { stdio: "ignore" });
|
|
@@ -1012,12 +1065,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1012
1065
|
|
|
1013
1066
|
if (fs.existsSync(wfSrc)) {
|
|
1014
1067
|
for (const file of fs.readdirSync(wfSrc).filter((f) => f.endsWith(".md"))) {
|
|
1015
|
-
const srcContent = fs.readFileSync(path.join(wfSrc, file), "utf8")
|
|
1068
|
+
const srcContent = fs.readFileSync(path.join(wfSrc, file), "utf8")
|
|
1069
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1016
1070
|
const destFile = wfDest && path.join(wfDest, file);
|
|
1017
1071
|
if (!destFile || !fs.existsSync(destFile)) {
|
|
1018
1072
|
result.workflows.push({ file, status: "new" });
|
|
1019
1073
|
} else {
|
|
1020
|
-
const destContent = fs.readFileSync(destFile, "utf8")
|
|
1074
|
+
const destContent = fs.readFileSync(destFile, "utf8")
|
|
1075
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1021
1076
|
result.workflows.push({
|
|
1022
1077
|
file,
|
|
1023
1078
|
status: srcContent === destContent ? "unchanged" : "changed",
|
|
@@ -1056,8 +1111,10 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1056
1111
|
].filter(Boolean);
|
|
1057
1112
|
const rulesDest = rulesDests.find((d) => fs.existsSync(d));
|
|
1058
1113
|
if (fs.existsSync(rulesSrc) && rulesDest) {
|
|
1059
|
-
const srcContent = fs.readFileSync(rulesSrc, "utf8")
|
|
1060
|
-
|
|
1114
|
+
const srcContent = fs.readFileSync(rulesSrc, "utf8")
|
|
1115
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1116
|
+
const destContent = fs.readFileSync(rulesDest, "utf8")
|
|
1117
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1061
1118
|
result.rules = srcContent === destContent ? "unchanged" : "changed";
|
|
1062
1119
|
} else {
|
|
1063
1120
|
result.rules = "unchanged";
|
|
@@ -1074,12 +1131,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1074
1131
|
} else {
|
|
1075
1132
|
const relNorm = entryRel.replace(/\\/g, "/");
|
|
1076
1133
|
if (relNorm.includes("02_Areas") && relNorm.includes("Engine")) continue;
|
|
1077
|
-
const srcContent = fs.readFileSync(path.join(dir, entry.name), "utf8")
|
|
1134
|
+
const srcContent = fs.readFileSync(path.join(dir, entry.name), "utf8")
|
|
1135
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1078
1136
|
const destFile = path.join(vaultPath, entryRel);
|
|
1079
1137
|
if (!fs.existsSync(destFile)) {
|
|
1080
1138
|
result.templates.push({ file: entryRel, status: "new" });
|
|
1081
1139
|
} else {
|
|
1082
|
-
const destContent = fs.readFileSync(destFile, "utf8")
|
|
1140
|
+
const destContent = fs.readFileSync(destFile, "utf8")
|
|
1141
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1083
1142
|
result.templates.push({
|
|
1084
1143
|
file: entryRel,
|
|
1085
1144
|
status: srcContent === destContent ? "unchanged" : "changed",
|
|
@@ -1106,12 +1165,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
|
|
|
1106
1165
|
if (entry.isDirectory()) {
|
|
1107
1166
|
const skillFile = path.join(full, "SKILL.md");
|
|
1108
1167
|
if (fs.existsSync(skillFile)) {
|
|
1109
|
-
const srcContent = fs.readFileSync(skillFile, "utf8")
|
|
1168
|
+
const srcContent = fs.readFileSync(skillFile, "utf8")
|
|
1169
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1110
1170
|
const destSkill = path.join(skillsDest, entry.name, "SKILL.md");
|
|
1111
1171
|
if (!fs.existsSync(destSkill)) {
|
|
1112
1172
|
result.skills.push({ file: entry.name, status: "new" });
|
|
1113
1173
|
} else {
|
|
1114
|
-
const destContent = fs.readFileSync(destSkill, "utf8")
|
|
1174
|
+
const destContent = fs.readFileSync(destSkill, "utf8")
|
|
1175
|
+
.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1115
1176
|
result.skills.push({ file: entry.name, status: srcContent === destContent ? "unchanged" : "changed" });
|
|
1116
1177
|
}
|
|
1117
1178
|
} else {
|
|
@@ -5239,8 +5300,12 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
5239
5300
|
}
|
|
5240
5301
|
}
|
|
5241
5302
|
fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
|
|
5242
|
-
writeMoverConfig(vaultPath, selectedIds);
|
|
5303
|
+
writeMoverConfig(vaultPath, selectedIds, updateKey);
|
|
5243
5304
|
barLn();
|
|
5305
|
+
if (totalChanged > 0) {
|
|
5306
|
+
barLn(dim(" Restart your AI session to load updated rules and skills."));
|
|
5307
|
+
barLn();
|
|
5308
|
+
}
|
|
5244
5309
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
5245
5310
|
await successAnimation(`${totalChanged} files updated in ${elapsed}s.`);
|
|
5246
5311
|
return;
|
|
@@ -5340,8 +5405,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
5340
5405
|
createVaultStructure(vaultPath);
|
|
5341
5406
|
// NOTE: templates are now handled via smart merge below (after manifest load), NOT installTemplateFiles()
|
|
5342
5407
|
|
|
5343
|
-
//
|
|
5344
|
-
|
|
5408
|
+
// Version marker is NOT stamped here during interactive update.
|
|
5409
|
+
// /update workflow stamps it after migrations complete.
|
|
5410
|
+
// Stamping early would make /update see current-version == target-version
|
|
5411
|
+
// and skip all migrations (the "V4→V4" bug).
|
|
5345
5412
|
|
|
5346
5413
|
// ── Smart Update: hash-based change detection + auto-merge ──
|
|
5347
5414
|
const manifest = loadUpdateManifest();
|
|
@@ -5567,7 +5634,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
5567
5634
|
}
|
|
5568
5635
|
}
|
|
5569
5636
|
saveUpdateManifest(newManifest);
|
|
5570
|
-
writeMoverConfig(vaultPath, selectedIds);
|
|
5637
|
+
writeMoverConfig(vaultPath, selectedIds, updateKey);
|
|
5571
5638
|
|
|
5572
5639
|
// ── Summary ──
|
|
5573
5640
|
barLn();
|
|
@@ -5587,6 +5654,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
5587
5654
|
}
|
|
5588
5655
|
|
|
5589
5656
|
barLn();
|
|
5657
|
+
if (updated.length > 0 || autoMerged.length > 0) {
|
|
5658
|
+
barLn(dim(" Restart your AI session to load updated rules and skills."));
|
|
5659
|
+
barLn();
|
|
5660
|
+
}
|
|
5590
5661
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
5591
5662
|
if (needsUpdate) {
|
|
5592
5663
|
outro(`System updated. Run ${bold("/update")} to resolve ${conflicts.length} conflict${conflicts.length > 1 ? "s" : ""}. ${dim(`(${elapsed}s)`)}`);
|
|
@@ -5600,13 +5671,15 @@ function resolveVaultPath(explicitVault) {
|
|
|
5600
5671
|
if (explicitVault) {
|
|
5601
5672
|
let v = explicitVault;
|
|
5602
5673
|
if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
|
|
5674
|
+
v = normalizeWinPath(v);
|
|
5603
5675
|
return path.resolve(v);
|
|
5604
5676
|
}
|
|
5605
5677
|
// Try config.json
|
|
5606
5678
|
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
5607
5679
|
if (fs.existsSync(cfgPath)) {
|
|
5608
5680
|
try {
|
|
5609
|
-
|
|
5681
|
+
let v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
|
|
5682
|
+
v = normalizeWinPath(v);
|
|
5610
5683
|
if (v && fs.existsSync(v)) return v;
|
|
5611
5684
|
} catch {}
|
|
5612
5685
|
}
|
|
@@ -5796,6 +5869,9 @@ async function main() {
|
|
|
5796
5869
|
} else if (msg.includes("401") || msg.includes("rejected")) {
|
|
5797
5870
|
barLn(red(" License key rejected by server."));
|
|
5798
5871
|
barLn(dim(" Has your key expired? Check polar.sh or email support@moveros.dev."));
|
|
5872
|
+
} else if (msg.includes("activation cap") || msg.includes("403")) {
|
|
5873
|
+
barLn(red(" License is registered to other devices."));
|
|
5874
|
+
barLn(dim(" Email support@moveros.dev to deactivate one and free a slot."));
|
|
5799
5875
|
} else if (msg.includes("500") || msg.includes("502") || msg.includes("503")) {
|
|
5800
5876
|
barLn(red(" Server error."));
|
|
5801
5877
|
barLn(dim(" Try again in a few minutes. If it persists, email support@moveros.dev."));
|
|
@@ -5867,6 +5943,7 @@ async function main() {
|
|
|
5867
5943
|
}
|
|
5868
5944
|
|
|
5869
5945
|
if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
|
|
5946
|
+
vaultPath = normalizeWinPath(vaultPath);
|
|
5870
5947
|
vaultPath = path.resolve(vaultPath);
|
|
5871
5948
|
|
|
5872
5949
|
// ── Fresh install only — redirect if existing ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mover-os",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.4",
|
|
4
4
|
"description": "Your AI co-founder. Remembers your goals, pushes back when you drift. Works with 15 AI agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"moveros": "install.js"
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"install.js"
|
|
10
10
|
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node test/runner.mjs",
|
|
13
|
+
"test:smoke": "node test/smoke.mjs",
|
|
14
|
+
"test:structural": "node test/structural.mjs",
|
|
15
|
+
"test:machineid": "node test/getMachineId.test.mjs",
|
|
16
|
+
"test:full": "node test/runner.mjs --include-slow"
|
|
17
|
+
},
|
|
11
18
|
"keywords": [
|
|
12
19
|
"obsidian",
|
|
13
20
|
"productivity",
|