@fudrouter/fsrouter 0.6.93 → 0.6.95

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.
@@ -72,13 +72,51 @@ export async function POST_handler(req, res) {
72
72
  if (db && typeof db.close === "function")
73
73
  db.close();
74
74
  }
75
- // 2. Remove active -wal and -shm files to prevent corruption/mismatch
75
+ // 2. Remove active -wal and -shm files to prevent corruption/mismatch.
76
+ // On Windows these files can be locked by the OS/AV/SQLite even after
77
+ // db.close() (EBUSY). Use a retry + rename fallback so restore never
78
+ // hard-fails on a transient lock.
76
79
  const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
77
80
  const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
78
- if (fs.existsSync(walFile))
79
- fs.unlinkSync(walFile);
80
- if (fs.existsSync(shmFile))
81
- fs.unlinkSync(shmFile);
81
+ const safeUnlink = (file) => {
82
+ if (!fs.existsSync(file))
83
+ return;
84
+ // Give the DB a moment to fully release the file handle.
85
+ const attempts = process.platform === "win32" ? 12 : 3;
86
+ for (let i = 0; i < attempts; i++) {
87
+ try {
88
+ fs.unlinkSync(file);
89
+ return;
90
+ }
91
+ catch (e) {
92
+ if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
93
+ // Fallback: rename out of the way (atomic on NTFS) then retry unlink.
94
+ try {
95
+ const tmp = `${file}.old-${Date.now()}`;
96
+ fs.renameSync(file, tmp);
97
+ try {
98
+ fs.unlinkSync(tmp);
99
+ }
100
+ catch { /* ignore */ }
101
+ return;
102
+ }
103
+ catch {
104
+ // Wait and retry — the lock may be released shortly.
105
+ try {
106
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
107
+ }
108
+ catch { /* noop */ }
109
+ continue;
110
+ }
111
+ }
112
+ throw e;
113
+ }
114
+ }
115
+ // Last resort: leave the file; SQLite rebuilds -wal/-shm on next open.
116
+ console.warn(`[Restore] Could not remove ${file} (locked) — continuing.`);
117
+ };
118
+ safeUnlink(walFile);
119
+ safeUnlink(shmFile);
82
120
  // 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
83
121
  // logical import with a stale/incompatible SQLite file.
84
122
  const filesToRestore = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fudrouter/fsrouter",
3
- "version": "0.6.93",
3
+ "version": "0.6.95",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "9Router v2 — Express Backend (API, SSE, DB, Auth, MITM)",
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Post-install: wire automation symlinks so pm2 (cwd=/root) can find the
4
- * Python venv, source tree and browser profiles used by the signup scripts.
3
+ * Post-install: wire automation symlinks so the signup scripts can find the
4
+ * Python venv, source tree and browser profiles regardless of the process cwd.
5
5
  *
6
- * pm2 runs the server with cwd=/root, and the automation routes resolve paths
7
- * relative to process.cwd() (e.g. .venv/bin/python, src/automation/*.py,
8
- * profiles/<provider>). On this VPS the real files live under /root/AMRouter,
9
- * so we symlink /root/{.venv,src,profiles} -> /root/AMRouter/{...}.
6
+ * The automation routes resolve paths relative to process.cwd()
7
+ * (e.g. .venv/bin/python, src/automation/*.py, profiles/<provider>). When the
8
+ * server runs under pm2 the cwd is the installed package directory
9
+ * (/usr/local/lib/node_modules/@fudrouter/fsrouter), NOT /root — so we create
10
+ * the symlinks in BOTH locations to be cwd-independent.
10
11
  *
11
- * Idempotent and non-fatal: never throws (npm install must succeed even if
12
- * the symlinks can't be created).
12
+ * Idempotent and non-fatal: never throws (npm install must succeed even if a
13
+ * symlink can't be created).
13
14
  */
14
15
  const fs = require("fs");
15
16
  const path = require("path");
@@ -19,26 +20,30 @@ function log(msg) {
19
20
  console.log(`[fsrouter:postinstall] ${msg}`);
20
21
  }
21
22
 
23
+ function isSymlink(p) {
24
+ try {
25
+ return fs.lstatSync(p).isSymbolicLink();
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
22
31
  function symlinkIfMissing(linkPath, targetPath) {
23
32
  try {
24
33
  if (!fs.existsSync(targetPath)) {
25
- // target missing — only create the symlink if the link itself is absent
26
- if (fs.existsSync(linkPath) || isSymlink(linkPath)) {
27
- // leave existing link alone; just warn if it points nowhere
28
- if (!fs.existsSync(linkPath)) fs.unlinkSync(linkPath);
29
- }
34
+ // target missing — remove a dangling link if present, then skip
35
+ if (isSymlink(linkPath)) fs.unlinkSync(linkPath);
30
36
  log(`skip ${linkPath} (target ${targetPath} not found)`);
31
37
  return;
32
38
  }
33
39
  if (isSymlink(linkPath)) {
34
- const cur = fs.readlinkSync(linkPath);
35
- if (cur === targetPath) {
40
+ if (fs.readlinkSync(linkPath) === targetPath) {
36
41
  log(`ok ${linkPath} -> ${targetPath}`);
37
42
  return;
38
43
  }
39
44
  fs.unlinkSync(linkPath);
40
45
  } else if (fs.existsSync(linkPath)) {
41
- log(`skip ${linkPath} (exists and is not a symlink)`);
46
+ log(`skip ${linkPath} (exists and is not a symlink — leaving untouched)`);
42
47
  return;
43
48
  }
44
49
  fs.symlinkSync(targetPath, linkPath, "dir");
@@ -48,20 +53,9 @@ function symlinkIfMissing(linkPath, targetPath) {
48
53
  }
49
54
  }
50
55
 
51
- function isSymlink(p) {
52
- try {
53
- return fs.lstatSync(p).isSymbolicLink();
54
- } catch {
55
- return false;
56
- }
57
- }
58
-
59
56
  function main() {
60
- // Prefer the canonical AMRouter checkout; fall back to the installed package.
61
- const candidates = [
62
- "/root/AMRouter",
63
- path.resolve(__dirname, ".."), // package root when installed
64
- ];
57
+ // Real AMRouter checkout (where the venv + browser profiles live)
58
+ const candidates = ["/root/AMRouter", path.resolve(__dirname, "..", "..")];
65
59
  let base = null;
66
60
  for (const c of candidates) {
67
61
  if (fs.existsSync(c)) {
@@ -75,12 +69,20 @@ function main() {
75
69
  }
76
70
  log(`base=${base}`);
77
71
 
78
- // Virtualenv (venv python)
79
- symlinkIfMissing("/root/.venv", path.join(base, ".venv"));
80
- // Source tree (src/automation/*.py)
81
- symlinkIfMissing("/root/src", path.join(base, "backend", "src"));
82
- // Browser profiles used by automation signup scripts
83
- symlinkIfMissing("/root/profiles", path.join(base, "backend", "profiles"));
72
+ const venvTarget = path.join(base, ".venv");
73
+ const srcTarget = path.join(base, "backend", "src");
74
+ const profilesTarget = path.join(base, "backend", "profiles");
75
+
76
+ // Locations to symlink from (cwd-independent coverage)
77
+ const linkBases = ["/root", process.cwd()]; // /root + the dir npm ran install in (global package dir under pm2)
78
+
79
+ for (const lb of linkBases) {
80
+ symlinkIfMissing(path.join(lb, ".venv"), venvTarget);
81
+ // src only if it doesn't already exist as a real dir (package ships src/)
82
+ const srcLink = path.join(lb, "src");
83
+ if (!fs.existsSync(srcLink)) symlinkIfMissing(srcLink, srcTarget);
84
+ symlinkIfMissing(path.join(lb, "profiles"), profilesTarget);
85
+ }
84
86
  }
85
87
 
86
88
  try {