quilltap 4.10.0-dev.32 → 4.10.0-dev.40

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/bin/quilltap.js CHANGED
@@ -486,6 +486,18 @@ function verifyPidIsNode(pid, expectedArgv0) {
486
486
 
487
487
  const HEARTBEAT_FRESH_MS = 5 * 60 * 1000;
488
488
 
489
+ /**
490
+ * The freshness window in words, derived from the constant the check actually
491
+ * uses — so the sentence `--lock-clean` prints cannot drift away from the
492
+ * behaviour it describes.
493
+ */
494
+ function describeFreshWindow() {
495
+ const seconds = Math.round(HEARTBEAT_FRESH_MS / 1000);
496
+ if (seconds % 60 !== 0) return `${seconds} seconds`;
497
+ const minutes = seconds / 60;
498
+ return minutes === 1 ? '1 minute' : `${minutes} minutes`;
499
+ }
500
+
489
501
  /**
490
502
  * Decide whether a lock record belongs to a live process.
491
503
  *
@@ -628,9 +640,17 @@ function handleLockCommand(dataDir, opts) {
628
640
  console.log('Stop the running instance first, or use --lock-override to force.');
629
641
  process.exit(1);
630
642
  } else if (heartbeatFresh) {
643
+ // Reached only when `alive` is false — the arm above has already claimed
644
+ // every confirmed-live case — so this must not assert that anything is
645
+ // running. Freshness alone is the deliberate fallback (bug 126) for
646
+ // environments where PID checks are unreliable; the refusal is right,
647
+ // and what it says is simply what was tested.
631
648
  const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
632
- console.log(`Lock is still being refreshed (heartbeat ${ageStr} ago) — its holder is alive. Cannot clean.`);
633
- console.log('Stop the running instance first, or use --lock-override to force.');
649
+ console.log(`Lock heartbeat is still fresh (${ageStr} ago). Cannot clean.`);
650
+ console.log(
651
+ `A lock counts as held until its heartbeat is ${describeFreshWindow()} stale, even if its process has gone. ` +
652
+ 'Wait it out, or use --lock-override to force.'
653
+ );
634
654
  process.exit(1);
635
655
  } else if (alive && !isNode) {
636
656
  console.log(`Lock references PID ${lock.pid} which is alive but does NOT look like a Quilltap process.`);
@@ -62,6 +62,84 @@ function betterSqlite3NeedsRebuild() {
62
62
  }
63
63
  }
64
64
 
65
+ // Locate an executable installed by a dependency, given the package directory
66
+ // that needs it. Checks the package's own `.bin` first, then the `.bin` beside
67
+ // it (the hoisted case, which is the usual one). Returns null when absent.
68
+ function findBinFor(pkgDir, tool) {
69
+ const fs = require('fs');
70
+ const names = process.platform === 'win32' ? [`${tool}.cmd`, `${tool}.exe`, tool] : [tool];
71
+ const dirs = [
72
+ path.join(pkgDir, 'node_modules', '.bin'), // nested install
73
+ path.join(pkgDir, '..', '.bin'), // hoisted alongside the package
74
+ ];
75
+ for (const dir of dirs) {
76
+ for (const name of names) {
77
+ const candidate = path.join(dir, name);
78
+ if (fs.existsSync(candidate)) return candidate;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // Rebuild ONE native package in place, addressed by its directory.
85
+ //
86
+ // Deliberately does NOT go through `npm rebuild <name>`, which fails this job
87
+ // two different ways:
88
+ //
89
+ // 1. npm refuses install scripts for any package not listed in the root
90
+ // package.json `allowScripts` map (keyed `name@version`). The root
91
+ // SQLCipher copy is installed under the ALIAS `better-sqlite3`, which is
92
+ // not a key there, so the rebuild dies with EALLOWSCRIPTS.
93
+ // 2. Asking for the real name (`better-sqlite3-multiple-ciphers`) at the root
94
+ // instead resolves a phantom directory: npm reports "rebuilt dependencies
95
+ // successfully" and the binding on disk is untouched. A success message is
96
+ // worse than an error.
97
+ //
98
+ // Running the package's own build chain (`prebuild-install || node-gyp rebuild`,
99
+ // exactly what its `install` script does) in its own directory sidesteps both:
100
+ // no name resolution, no npm script policy. `prebuild-install` downloads the
101
+ // prebuilt binary for the running ABI and needs network; node-gyp compiles and
102
+ // needs a toolchain.
103
+ //
104
+ // Returns { ok, reason }. Never throws. Verifies the result rather than trusting
105
+ // the exit code — see (2): the whole failure mode here is a rebuild that claims
106
+ // to have worked.
107
+ function rebuildNativePackage(pkgDir, bindingPath) {
108
+ const fs = require('fs');
109
+ if (!fs.existsSync(pkgDir)) return { ok: false, reason: `no package at ${pkgDir}` };
110
+
111
+ const before = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
112
+ const attempts = [];
113
+
114
+ const prebuild = findBinFor(pkgDir, 'prebuild-install');
115
+ if (prebuild) attempts.push({ label: 'prebuild-install', cmd: `"${prebuild}"` });
116
+
117
+ const nodeGyp = findBinFor(pkgDir, 'node-gyp');
118
+ if (nodeGyp) attempts.push({ label: 'node-gyp rebuild', cmd: `"${nodeGyp}" rebuild --release` });
119
+
120
+ if (attempts.length === 0) {
121
+ return { ok: false, reason: 'neither prebuild-install nor node-gyp is installed' };
122
+ }
123
+
124
+ const failures = [];
125
+ for (const attempt of attempts) {
126
+ try {
127
+ execSync(attempt.cmd, { cwd: pkgDir, stdio: 'inherit' });
128
+ } catch (err) {
129
+ failures.push(`${attempt.label}: ${err.message.split('\n')[0]}`);
130
+ continue;
131
+ }
132
+ // Trust nothing but the binary. A tool can exit 0 having done nothing.
133
+ const after = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
134
+ if (after === process.versions.modules) return { ok: true, reason: attempt.label };
135
+ failures.push(
136
+ `${attempt.label}: exited 0 but the binding is still ${after ?? 'missing'} ` +
137
+ `(wanted ${process.versions.modules}${before ? `, was ${before}` : ''})`,
138
+ );
139
+ }
140
+ return { ok: false, reason: failures.join('; ') };
141
+ }
142
+
65
143
  // Rebuild the named native modules against the current Node ABI. Prints a
66
144
  // friendly notice rather than throwing; returns true on success, false on
67
145
  // failure. Backfills node-pty's spawn-helper afterward.
@@ -96,7 +174,27 @@ function ensureDatabaseNativeModule() {
96
174
  } catch {
97
175
  return true; // detection hiccup — let the real load be the source of truth
98
176
  }
99
- return rebuildModules(['better-sqlite3-multiple-ciphers']);
177
+
178
+ const bindingPath = betterSqlite3BindingPath();
179
+ if (!bindingPath) {
180
+ console.error(' Warning: could not locate the SQLCipher binding to rebuild.');
181
+ return false;
182
+ }
183
+ // build/Release/better_sqlite3.node → the package directory above it.
184
+ const pkgDir = path.resolve(path.dirname(bindingPath), '..', '..');
185
+
186
+ console.log(` Rebuilding the SQLCipher binding for Node.js ${process.version}...`);
187
+ const result = rebuildNativePackage(pkgDir, bindingPath);
188
+ if (result.ok) {
189
+ console.log(` Done (${result.reason}).`);
190
+ console.log('');
191
+ return true;
192
+ }
193
+ console.error('');
194
+ console.error(` Warning: failed to rebuild the SQLCipher binding — ${result.reason}`);
195
+ console.error(` Try running: (cd ${pkgDir} && ./node_modules/.bin/prebuild-install)`);
196
+ console.error('');
197
+ return false;
100
198
  }
101
199
 
102
200
  // node-pty needs a `spawn-helper` executable beside the pty.node it loads, or
@@ -194,7 +292,10 @@ function ensureNativeModules() {
194
292
  module.exports = {
195
293
  resolveModuleDir,
196
294
  readCompiledAbi,
295
+ betterSqlite3BindingPath,
197
296
  betterSqlite3NeedsRebuild,
297
+ findBinFor,
298
+ rebuildNativePackage,
198
299
  ensureDatabaseNativeModule,
199
300
  ensureNativeModules,
200
301
  reconcileNodePtySpawnHelper,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.10.0-dev.32",
3
+ "version": "4.10.0-dev.40",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",