quilltap 4.7.0-dev.101 → 4.7.0-dev.117

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
@@ -13,7 +13,7 @@ const {
13
13
  loadDbKey,
14
14
  } = require('../lib/db-helpers');
15
15
  const { resolveInstance } = require('../lib/instances');
16
- const { resolveModuleDir, ensureNativeModules } = require('../lib/native-modules');
16
+ const { resolveModuleDir, ensureNativeModules, ensureDatabaseNativeModule } = require('../lib/native-modules');
17
17
 
18
18
  const PACKAGE_DIR = path.resolve(__dirname, '..');
19
19
 
@@ -1124,6 +1124,14 @@ const subName = subIdx >= 0 ? cliArgs[subIdx] : '';
1124
1124
  // Everything except the subcommand token itself (leading global flags kept).
1125
1125
  const subArgs = subIdx >= 0 ? [...cliArgs.slice(0, subIdx), ...cliArgs.slice(subIdx + 1)] : [];
1126
1126
 
1127
+ // Subcommands load the SQLCipher binding directly and never reach main()'s
1128
+ // native-module heal, so self-heal the database ABI here first. Cheap no-op when
1129
+ // healthy; rebuilds (with a friendly notice, not an error) only on a real
1130
+ // Node-ABI mismatch — e.g. after the user upgrades Node under a cached install.
1131
+ if (subName) {
1132
+ ensureDatabaseNativeModule();
1133
+ }
1134
+
1127
1135
  if (subName === 'db') {
1128
1136
  dbCommand(subArgs);
1129
1137
  } else if (subName === 'themes') {
@@ -21,6 +21,84 @@ function resolveModuleDir(moduleName) {
21
21
  }
22
22
  }
23
23
 
24
+ // Locate the SQLCipher binding (better-sqlite3-multiple-ciphers, aliased as
25
+ // better-sqlite3). Returns the absolute path to better_sqlite3.node, or null.
26
+ function betterSqlite3BindingPath() {
27
+ const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
28
+ || resolveModuleDir('better-sqlite3');
29
+ if (!modDir) return null;
30
+ return path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
31
+ }
32
+
33
+ // Read the Node ABI (NODE_MODULE_VERSION) a node-gyp/NAN addon was compiled
34
+ // against, by scanning for its `node_register_module_v<ABI>` export — a few-KB
35
+ // byte read, no dlopen, no rebuild. Returns the ABI as a string, or null when
36
+ // the symbol is absent (e.g. an N-API build, which is ABI-stable) or unreadable.
37
+ function readCompiledAbi(bindingPath) {
38
+ try {
39
+ const buf = require('fs').readFileSync(bindingPath);
40
+ const m = /node_register_module_v(\d+)/.exec(buf.toString('latin1'));
41
+ return m ? m[1] : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // True when the SQLCipher binding is missing or was built for a different Node
48
+ // ABI than the one we're running. Reads the compiled-for ABI straight from the
49
+ // binary; only falls back to an actual load probe if the symbol can't be read.
50
+ function betterSqlite3NeedsRebuild() {
51
+ const bindingPath = betterSqlite3BindingPath();
52
+ if (!bindingPath) return true; // unresolvable → needs (re)build
53
+ if (!require('fs').existsSync(bindingPath)) return true;
54
+ const compiledAbi = readCompiledAbi(bindingPath);
55
+ if (compiledAbi) return compiledAbi !== process.versions.modules;
56
+ // Symbol unreadable — fall back to the authoritative dlopen probe.
57
+ try {
58
+ require(bindingPath);
59
+ return false;
60
+ } catch (err) {
61
+ return !!(err.message && err.message.includes('NODE_MODULE_VERSION'));
62
+ }
63
+ }
64
+
65
+ // Rebuild the named native modules against the current Node ABI. Prints a
66
+ // friendly notice rather than throwing; returns true on success, false on
67
+ // failure. Backfills node-pty's spawn-helper afterward.
68
+ function rebuildModules(moduleNames) {
69
+ console.log(` Rebuilding native modules for Node.js ${process.version}...`);
70
+ try {
71
+ execSync(`npm rebuild ${moduleNames.join(' ')}`, {
72
+ cwd: PACKAGE_DIR,
73
+ stdio: 'inherit',
74
+ });
75
+ console.log(' Done.');
76
+ console.log('');
77
+ reconcileNodePtySpawnHelper();
78
+ return true;
79
+ } catch (err) {
80
+ console.error('');
81
+ console.error(` Warning: Failed to rebuild native modules: ${err.message}`);
82
+ console.error(' Try running: npm rebuild --prefix ' + PACKAGE_DIR);
83
+ console.error('');
84
+ return false;
85
+ }
86
+ }
87
+
88
+ // Fast pre-flight for the ONE ABI-fragile native module every DB path needs:
89
+ // better-sqlite3-multiple-ciphers (SQLCipher). sharp and node-pty are N-API and
90
+ // ABI-stable, so they can't hit this failure. Detects an ABI mismatch from the
91
+ // binary itself and rebuilds before anything tries to load it, so a Node upgrade
92
+ // self-heals instead of throwing. Cheap no-op when already healthy. Never throws.
93
+ function ensureDatabaseNativeModule() {
94
+ try {
95
+ if (!betterSqlite3NeedsRebuild()) return true;
96
+ } catch {
97
+ return true; // detection hiccup — let the real load be the source of truth
98
+ }
99
+ return rebuildModules(['better-sqlite3-multiple-ciphers']);
100
+ }
101
+
24
102
  // node-pty needs a `spawn-helper` executable beside the pty.node it loads, or
25
103
  // pty.spawn() fails with `posix_spawnp failed`. An ABI rebuild lands a fresh
26
104
  // build/Release/pty.node (which node-pty's loader prefers over prebuilds/) but
@@ -74,19 +152,9 @@ function ensureNativeModules() {
74
152
  // Check better-sqlite3-multiple-ciphers (provides SQLCipher encryption support).
75
153
  // The main app depends on this via an npm alias as 'better-sqlite3', so we must
76
154
  // ensure the SQLCipher-capable version is available and link it as 'better-sqlite3'.
77
- // We must load the native binding directly to detect NODE_MODULE_VERSION mismatches.
78
- try {
79
- const modDir = resolveModuleDir('better-sqlite3-multiple-ciphers')
80
- || resolveModuleDir('better-sqlite3');
81
- if (!modDir) throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
82
- const bindingsPath = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
83
- require(bindingsPath);
84
- } catch (err) {
85
- if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
86
- needsRebuild.push('better-sqlite3-multiple-ciphers');
87
- } else if (err.code === 'MODULE_NOT_FOUND') {
88
- needsRebuild.push('better-sqlite3-multiple-ciphers');
89
- }
155
+ // This is the only ABI-fragile binding detected straight from the binary.
156
+ if (betterSqlite3NeedsRebuild()) {
157
+ needsRebuild.push('better-sqlite3-multiple-ciphers');
90
158
  }
91
159
 
92
160
  // Check sharp: loads its native binding eagerly on require.
@@ -120,27 +188,18 @@ function ensureNativeModules() {
120
188
  return true;
121
189
  }
122
190
 
123
- console.log(` Rebuilding native modules for Node.js ${process.version}...`);
124
-
125
- try {
126
- execSync(`npm rebuild ${needsRebuild.join(' ')}`, {
127
- cwd: PACKAGE_DIR,
128
- stdio: 'inherit',
129
- });
130
- console.log(' Done.');
131
- console.log('');
132
- reconcileNodePtySpawnHelper();
133
- return true;
134
- } catch (err) {
135
- console.error('');
136
- console.error(` Warning: Failed to rebuild native modules: ${err.message}`);
137
- console.error(' Try running: npm rebuild --prefix ' + PACKAGE_DIR);
138
- console.error('');
139
- return false;
140
- }
191
+ return rebuildModules(needsRebuild);
141
192
  }
142
193
 
143
- module.exports = { resolveModuleDir, ensureNativeModules, reconcileNodePtySpawnHelper, PACKAGE_DIR };
194
+ module.exports = {
195
+ resolveModuleDir,
196
+ readCompiledAbi,
197
+ betterSqlite3NeedsRebuild,
198
+ ensureDatabaseNativeModule,
199
+ ensureNativeModules,
200
+ reconcileNodePtySpawnHelper,
201
+ PACKAGE_DIR,
202
+ };
144
203
 
145
204
  // Allow this file to be invoked directly as a postinstall script:
146
205
  // node lib/native-modules.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "4.7.0-dev.101",
3
+ "version": "4.7.0-dev.117",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",
@@ -37,11 +37,11 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@napi-rs/canvas": "^0.1.100",
40
- "better-sqlite3-multiple-ciphers": "^12.10.0",
40
+ "better-sqlite3-multiple-ciphers": "^12.11.1",
41
41
  "node-pty": "^1.1.0",
42
42
  "sharp": "^0.34.5",
43
43
  "tar": "^7.5.16",
44
- "yauzl": "^3.3.2"
44
+ "yauzl": "^3.4.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"