quilltap 3.1.0-dev.27 → 3.1.0-dev.30

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/bin/quilltap.js +118 -17
  2. package/package.json +1 -1
package/bin/quilltap.js CHANGED
@@ -114,23 +114,49 @@ function openBrowser(url) {
114
114
  });
115
115
  }
116
116
 
117
+ // Resolve a native module's directory, handling npm hoisting.
118
+ // Returns the directory containing package.json, or null if not found.
119
+ function resolveModuleDir(moduleName) {
120
+ try {
121
+ const pkgJson = require.resolve(moduleName + '/package.json');
122
+ return path.dirname(pkgJson);
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
117
128
  // Check if native modules are compiled for the current Node.js version.
118
129
  // This handles the case where npx caches the package but the user upgrades
119
130
  // Node.js — the cached native modules will have a stale NODE_MODULE_VERSION.
120
131
  function ensureNativeModules() {
121
- const nativeModules = ['better-sqlite3', 'sharp'];
122
132
  const needsRebuild = [];
123
133
 
124
- for (const mod of nativeModules) {
125
- try {
126
- require(mod);
127
- } catch (err) {
128
- if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
129
- needsRebuild.push(mod);
130
- } else if (err.code === 'MODULE_NOT_FOUND') {
131
- needsRebuild.push(mod);
132
- }
133
- // Other errors (e.g., missing system libs) — let the server handle them
134
+ // Check better-sqlite3: it lazy-loads the native .node binary only when you
135
+ // create a Database, so a bare require('better-sqlite3') always succeeds.
136
+ // We must load the native binding directly to detect NODE_MODULE_VERSION mismatches.
137
+ // Use require.resolve to find it regardless of npm hoisting.
138
+ try {
139
+ const modDir = resolveModuleDir('better-sqlite3');
140
+ if (!modDir) throw Object.assign(new Error('not found'), { code: 'MODULE_NOT_FOUND' });
141
+ const bindingsPath = path.join(modDir, 'build', 'Release', 'better_sqlite3.node');
142
+ require(bindingsPath);
143
+ } catch (err) {
144
+ if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
145
+ needsRebuild.push('better-sqlite3');
146
+ } else if (err.code === 'MODULE_NOT_FOUND') {
147
+ needsRebuild.push('better-sqlite3');
148
+ }
149
+ }
150
+
151
+ // Check sharp: loads its native binding eagerly on require, but we use
152
+ // the same explicit-path approach for consistency and reliability.
153
+ try {
154
+ require('sharp');
155
+ } catch (err) {
156
+ if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
157
+ needsRebuild.push('sharp');
158
+ } else if (err.code === 'MODULE_NOT_FOUND') {
159
+ needsRebuild.push('sharp');
134
160
  }
135
161
  }
136
162
 
@@ -153,6 +179,77 @@ function ensureNativeModules() {
153
179
  }
154
180
  }
155
181
 
182
+ // Symlink native modules into the standalone directory's node_modules
183
+ // so that standard Node.js resolution finds them without relying on NODE_PATH.
184
+ function linkNativeModules(standaloneDir) {
185
+ const standaloneNodeModules = path.join(standaloneDir, 'node_modules');
186
+
187
+ // Ensure top-level node_modules exists in standalone dir
188
+ if (!fs.existsSync(standaloneNodeModules)) {
189
+ fs.mkdirSync(standaloneNodeModules, { recursive: true });
190
+ }
191
+
192
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
193
+
194
+ // Link a single module directory into standaloneDir/node_modules/<name>
195
+ function linkModule(name, sourceDir) {
196
+ if (!sourceDir) return;
197
+ const targetPath = path.join(standaloneNodeModules, name);
198
+
199
+ // If already exists and points to the right place, skip
200
+ if (fs.existsSync(targetPath)) {
201
+ try {
202
+ const existing = fs.realpathSync(targetPath);
203
+ const source = fs.realpathSync(sourceDir);
204
+ if (existing === source) return; // already linked correctly
205
+ } catch {
206
+ // If we can't resolve, remove and re-link
207
+ }
208
+ // Remove stale link/dir
209
+ fs.rmSync(targetPath, { recursive: true, force: true });
210
+ }
211
+
212
+ // Ensure parent directory exists (for scoped packages like @img/sharp-*)
213
+ const parentDir = path.dirname(targetPath);
214
+ if (!fs.existsSync(parentDir)) {
215
+ fs.mkdirSync(parentDir, { recursive: true });
216
+ }
217
+
218
+ try {
219
+ fs.symlinkSync(sourceDir, targetPath, symlinkType);
220
+ } catch (err) {
221
+ // If symlink fails (e.g. permissions), try copying as fallback
222
+ console.error(` Warning: Could not symlink ${name}: ${err.message}`);
223
+ }
224
+ }
225
+
226
+ // Link better-sqlite3
227
+ const betterSqlite3Dir = resolveModuleDir('better-sqlite3');
228
+ linkModule('better-sqlite3', betterSqlite3Dir);
229
+
230
+ // Link sharp
231
+ const sharpDir = resolveModuleDir('sharp');
232
+ linkModule('sharp', sharpDir);
233
+
234
+ // Link sharp's @img platform packages — they live near sharp's location
235
+ if (sharpDir) {
236
+ const sharpParent = path.dirname(sharpDir);
237
+
238
+ // If sharp is in a scoped dir or regular node_modules, look for @img there
239
+ const imgDir = path.join(sharpParent, '@img');
240
+ if (fs.existsSync(imgDir)) {
241
+ try {
242
+ const imgPackages = fs.readdirSync(imgDir).filter(name => name.startsWith('sharp-'));
243
+ for (const pkg of imgPackages) {
244
+ linkModule(`@img/${pkg}`, path.join(imgDir, pkg));
245
+ }
246
+ } catch {
247
+ // Non-fatal — sharp may work without explicit @img links
248
+ }
249
+ }
250
+ }
251
+ }
252
+
156
253
  // Main
157
254
  async function main() {
158
255
  const opts = parseArgs(process.argv);
@@ -192,6 +289,9 @@ async function main() {
192
289
  // Ensure native modules are compiled for the current Node.js version
193
290
  ensureNativeModules();
194
291
 
292
+ // Symlink native modules into standalone dir so standard resolution finds them
293
+ linkNativeModules(standaloneDir);
294
+
195
295
  // Set up environment
196
296
  const env = {
197
297
  ...process.env,
@@ -204,13 +304,14 @@ async function main() {
204
304
  env.QUILLTAP_DATA_DIR = path.resolve(opts.dataDir);
205
305
  }
206
306
 
207
- // Set NODE_PATH so native modules resolve from the npm package's own node_modules.
208
- // The standalone output has native modules stripped better-sqlite3 and sharp
209
- // are installed as real npm dependencies so they compile for the user's platform.
307
+ // Set NODE_PATH as a fallback native modules are symlinked into standaloneDir
308
+ // but NODE_PATH covers any other dependencies. Include the parent node_modules
309
+ // to handle npm hoisting (e.g. npx installs where deps are hoisted up a level).
210
310
  const packageNodeModules = path.join(PACKAGE_DIR, 'node_modules');
211
- env.NODE_PATH = env.NODE_PATH
212
- ? `${packageNodeModules}${path.delimiter}${env.NODE_PATH}`
213
- : packageNodeModules;
311
+ const parentNodeModules = path.resolve(PACKAGE_DIR, '..');
312
+ env.NODE_PATH = [packageNodeModules, parentNodeModules, env.NODE_PATH]
313
+ .filter(Boolean)
314
+ .join(path.delimiter);
214
315
 
215
316
  const url = `http://localhost:${opts.port}`;
216
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "3.1.0-dev.27",
3
+ "version": "3.1.0-dev.30",
4
4
  "description": "Self-hosted AI workspace for writers, worldbuilders, and roleplayers. Run with npx quilltap.",
5
5
  "author": {
6
6
  "name": "Charles Sebold",