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.
Files changed (2) hide show
  1. package/install.js +91 -14
  2. 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
- const destContent = fs.readFileSync(rulesDest, "utf8");
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
- // Update version marker
5344
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
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
- const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
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.2",
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",