quilltap 3.1.0-dev.29 → 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 +96 -9
  2. package/package.json +1 -1
package/bin/quilltap.js CHANGED
@@ -114,6 +114,17 @@ 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.
@@ -123,10 +134,11 @@ function ensureNativeModules() {
123
134
  // Check better-sqlite3: it lazy-loads the native .node binary only when you
124
135
  // create a Database, so a bare require('better-sqlite3') always succeeds.
125
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.
126
138
  try {
127
- const bindingsPath = path.join(
128
- PACKAGE_DIR, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'
129
- );
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');
130
142
  require(bindingsPath);
131
143
  } catch (err) {
132
144
  if (err.message && err.message.includes('NODE_MODULE_VERSION')) {
@@ -167,6 +179,77 @@ function ensureNativeModules() {
167
179
  }
168
180
  }
169
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
+
170
253
  // Main
171
254
  async function main() {
172
255
  const opts = parseArgs(process.argv);
@@ -206,6 +289,9 @@ async function main() {
206
289
  // Ensure native modules are compiled for the current Node.js version
207
290
  ensureNativeModules();
208
291
 
292
+ // Symlink native modules into standalone dir so standard resolution finds them
293
+ linkNativeModules(standaloneDir);
294
+
209
295
  // Set up environment
210
296
  const env = {
211
297
  ...process.env,
@@ -218,13 +304,14 @@ async function main() {
218
304
  env.QUILLTAP_DATA_DIR = path.resolve(opts.dataDir);
219
305
  }
220
306
 
221
- // Set NODE_PATH so native modules resolve from the npm package's own node_modules.
222
- // The standalone output has native modules stripped better-sqlite3 and sharp
223
- // 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).
224
310
  const packageNodeModules = path.join(PACKAGE_DIR, 'node_modules');
225
- env.NODE_PATH = env.NODE_PATH
226
- ? `${packageNodeModules}${path.delimiter}${env.NODE_PATH}`
227
- : packageNodeModules;
311
+ const parentNodeModules = path.resolve(PACKAGE_DIR, '..');
312
+ env.NODE_PATH = [packageNodeModules, parentNodeModules, env.NODE_PATH]
313
+ .filter(Boolean)
314
+ .join(path.delimiter);
228
315
 
229
316
  const url = `http://localhost:${opts.port}`;
230
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilltap",
3
- "version": "3.1.0-dev.29",
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",