feat: auto-rebuild native modules on Node.js version mismatch

When Node.js is updated, native modules like better-sqlite3 can become
   incompatible due to NODE_MODULE_VERSION differences. This change adds
   automatic detection and rebuild capability:

   - Add native-module-helper.js utility for detecting version errors
   - Lazy-load better-sqlite3 to catch import errors at runtime
   - Automatically run npm rebuild when version mismatch is detected
   - Clear require cache and retry loading after successful rebuild
   - Provide clear instructions if automatic rebuild fails

   Fixes the issue where users running via npx encounter module errors
   after updating Node.js.
This commit is contained in:
jgor20
2026-01-05 00:43:21 +00:00
parent ea3d3ca4a4
commit e6027ec5a6
3 changed files with 247 additions and 5 deletions

View File

@@ -6,10 +6,102 @@
* - Windows compatibility (no CLI dependency)
* - Native performance
* - Synchronous API (simple error handling)
*
* Includes auto-rebuild capability for handling Node.js version updates
* that cause native module incompatibility.
*/
import Database from 'better-sqlite3';
import { createRequire } from 'module';
import { ANTIGRAVITY_DB_PATH } from '../constants.js';
import { isModuleVersionError, attemptAutoRebuild } from '../utils/native-module-helper.js';
import { logger } from '../utils/logger.js';
const require = createRequire(import.meta.url);
// Lazy-loaded Database constructor
let Database = null;
let moduleLoadError = null;
/**
* Load the better-sqlite3 module with auto-rebuild on version mismatch
* Uses synchronous require to maintain API compatibility
* @returns {Function} The Database constructor
* @throws {Error} If module cannot be loaded even after rebuild
*/
function loadDatabaseModule() {
// Return cached module if already loaded
if (Database) return Database;
// Re-throw cached error if previous load failed permanently
if (moduleLoadError) throw moduleLoadError;
try {
Database = require('better-sqlite3');
return Database;
} catch (error) {
if (isModuleVersionError(error)) {
logger.warn('[Database] Native module version mismatch detected');
if (attemptAutoRebuild(error)) {
// Clear require cache and retry
try {
const resolvedPath = require.resolve('better-sqlite3');
// Clear the module and all its dependencies from cache
clearRequireCache(resolvedPath);
Database = require('better-sqlite3');
logger.success('[Database] Module reloaded successfully after rebuild');
return Database;
} catch (retryError) {
// Rebuild succeeded but reload failed - user needs to restart
moduleLoadError = new Error(
'Native module rebuild completed. Please restart the server to apply the fix.'
);
logger.info('[Database] Rebuild succeeded - server restart required');
throw moduleLoadError;
}
} else {
moduleLoadError = new Error(
'Failed to auto-rebuild native module. Please run manually:\n' +
' npm rebuild better-sqlite3\n' +
'Or if using npx, find the package location in the error and run:\n' +
' cd /path/to/better-sqlite3 && npm rebuild'
);
throw moduleLoadError;
}
}
// Non-version-mismatch error, just throw it
throw error;
}
}
/**
* Clear a module and its dependencies from the require cache
* @param {string} modulePath - Resolved path to the module
*/
function clearRequireCache(modulePath) {
const mod = require.cache[modulePath];
if (!mod) return;
// Recursively clear children first
if (mod.children) {
for (const child of mod.children) {
clearRequireCache(child.id);
}
}
// Remove from parent's children
if (mod.parent && mod.parent.children) {
const idx = mod.parent.children.indexOf(mod);
if (idx !== -1) {
mod.parent.children.splice(idx, 1);
}
}
// Delete from cache
delete require.cache[modulePath];
}
/**
* Query Antigravity database for authentication status
@@ -18,10 +110,11 @@ import { ANTIGRAVITY_DB_PATH } from '../constants.js';
* @throws {Error} If database doesn't exist, query fails, or no auth status found
*/
export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
const Db = loadDatabaseModule();
let db;
try {
// Open database in read-only mode
db = new Database(dbPath, {
db = new Db(dbPath, {
readonly: true,
fileMustExist: true
});
@@ -56,6 +149,10 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
throw error;
}
// Check for version mismatch that might have been thrown by loadDatabaseModule
if (error.message.includes('restart the server') || error.message.includes('auto-rebuild')) {
throw error;
}
throw new Error(`Failed to read Antigravity database: ${error.message}`);
} finally {
// Always close database connection
@@ -73,7 +170,8 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) {
let db;
try {
db = new Database(dbPath, {
const Db = loadDatabaseModule();
db = new Db(dbPath, {
readonly: true,
fileMustExist: true
});