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

@@ -101,7 +101,8 @@ src/
└── utils/ # Utilities └── utils/ # Utilities
├── helpers.js # formatDuration, sleep ├── helpers.js # formatDuration, sleep
── logger.js # Structured logging ── logger.js # Structured logging
└── native-module-helper.js # Auto-rebuild for native modules
``` ```
**Key Modules:** **Key Modules:**
@@ -109,7 +110,7 @@ src/
- **src/server.js**: Express server exposing Anthropic-compatible endpoints (`/v1/messages`, `/v1/models`, `/health`, `/account-limits`) - **src/server.js**: Express server exposing Anthropic-compatible endpoints (`/v1/messages`, `/v1/models`, `/health`, `/account-limits`)
- **src/cloudcode/**: Cloud Code API client with retry/failover logic, streaming and non-streaming support - **src/cloudcode/**: Cloud Code API client with retry/failover logic, streaming and non-streaming support
- **src/account-manager/**: Multi-account pool with sticky selection, rate limit handling, and automatic cooldown - **src/account-manager/**: Multi-account pool with sticky selection, rate limit handling, and automatic cooldown
- **src/auth/**: Authentication including Google OAuth, token extraction, and database access - **src/auth/**: Authentication including Google OAuth, token extraction, database access, and auto-rebuild of native modules
- **src/format/**: Format conversion between Anthropic and Google Generative AI formats - **src/format/**: Format conversion between Anthropic and Google Generative AI formats
- **src/constants.js**: API endpoints, model mappings, fallback config, OAuth config, and all configuration values - **src/constants.js**: API endpoints, model mappings, fallback config, OAuth config, and all configuration values
- **src/fallback-config.js**: Model fallback mappings (`getFallbackModel()`, `hasFallback()`) - **src/fallback-config.js**: Model fallback mappings (`getFallbackModel()`, `hasFallback()`)
@@ -144,6 +145,13 @@ src/
- For Gemini targets: strict validation - drops unknown or mismatched signatures - For Gemini targets: strict validation - drops unknown or mismatched signatures
- For Claude targets: lenient - lets Claude validate its own signatures - For Claude targets: lenient - lets Claude validate its own signatures
**Native Module Auto-Rebuild:**
- When Node.js is updated, native modules like `better-sqlite3` may become incompatible
- The proxy automatically detects `NODE_MODULE_VERSION` mismatch errors
- On detection, it attempts to rebuild the module using `npm rebuild`
- If rebuild succeeds, the module is reloaded; if reload fails, a server restart is required
- Implementation in `src/utils/native-module-helper.js` and lazy loading in `src/auth/database.js`
## Testing Notes ## Testing Notes
- Tests require the server to be running (`npm start` in separate terminal) - Tests require the server to be running (`npm start` in separate terminal)

View File

@@ -6,10 +6,102 @@
* - Windows compatibility (no CLI dependency) * - Windows compatibility (no CLI dependency)
* - Native performance * - Native performance
* - Synchronous API (simple error handling) * - 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 { 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 * 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 * @throws {Error} If database doesn't exist, query fails, or no auth status found
*/ */
export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) { export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
const Db = loadDatabaseModule();
let db; let db;
try { try {
// Open database in read-only mode // Open database in read-only mode
db = new Database(dbPath, { db = new Db(dbPath, {
readonly: true, readonly: true,
fileMustExist: 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')) { if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
throw error; 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}`); throw new Error(`Failed to read Antigravity database: ${error.message}`);
} finally { } finally {
// Always close database connection // Always close database connection
@@ -73,7 +170,8 @@ export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) { export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) {
let db; let db;
try { try {
db = new Database(dbPath, { const Db = loadDatabaseModule();
db = new Db(dbPath, {
readonly: true, readonly: true,
fileMustExist: true fileMustExist: true
}); });

View File

@@ -0,0 +1,136 @@
/**
* Native Module Helper
* Detects and auto-rebuilds native Node.js modules when they become
* incompatible after a Node.js version update.
*/
import { execSync } from 'child_process';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { createRequire } from 'module';
import { logger } from './logger.js';
/**
* Check if an error is a NODE_MODULE_VERSION mismatch error
* @param {Error} error - The error to check
* @returns {boolean} True if it's a version mismatch error
*/
export function isModuleVersionError(error) {
const message = error?.message || '';
return message.includes('NODE_MODULE_VERSION') &&
message.includes('was compiled against a different Node.js version');
}
/**
* Extract the module path from a NODE_MODULE_VERSION error message
* @param {Error} error - The error containing the module path
* @returns {string|null} The path to the .node file, or null if not found
*/
export function extractModulePath(error) {
const message = error?.message || '';
// Match pattern like: "The module '/path/to/module.node'"
const match = message.match(/The module '([^']+\.node)'/);
return match ? match[1] : null;
}
/**
* Find the package root directory from a .node file path
* @param {string} nodeFilePath - Path to the .node file
* @returns {string|null} Path to the package root, or null if not found
*/
export function findPackageRoot(nodeFilePath) {
// Walk up from the .node file to find package.json
let dir = dirname(nodeFilePath);
while (dir && dir !== '/') {
const packageJsonPath = join(dir, 'package.json');
if (existsSync(packageJsonPath)) {
return dir;
}
dir = dirname(dir);
}
return null;
}
/**
* Attempt to rebuild a native module
* @param {string} packagePath - Path to the package root directory
* @returns {boolean} True if rebuild succeeded, false otherwise
*/
export function rebuildModule(packagePath) {
try {
logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`);
// Run npm rebuild in the package directory
execSync('npm rebuild', {
cwd: packagePath,
stdio: 'pipe', // Capture output instead of printing
timeout: 120000 // 2 minute timeout
});
logger.success('[NativeModule] Rebuild completed successfully');
return true;
} catch (error) {
logger.error(`[NativeModule] Rebuild failed: ${error.message}`);
return false;
}
}
/**
* Attempt to auto-rebuild a native module from an error
* @param {Error} error - The NODE_MODULE_VERSION error
* @returns {boolean} True if rebuild succeeded, false otherwise
*/
export function attemptAutoRebuild(error) {
const nodePath = extractModulePath(error);
if (!nodePath) {
logger.error('[NativeModule] Could not extract module path from error');
return false;
}
const packagePath = findPackageRoot(nodePath);
if (!packagePath) {
logger.error('[NativeModule] Could not find package root');
return false;
}
logger.warn('[NativeModule] Native module version mismatch detected');
logger.info('[NativeModule] Attempting automatic rebuild...');
return rebuildModule(packagePath);
}
/**
* Clear the require cache for a module to force re-import
* This is needed after rebuilding a native module
* @param {string} moduleName - The module name (e.g., 'better-sqlite3')
*/
export function clearModuleCache(moduleName) {
const require = createRequire(import.meta.url);
try {
const resolved = require.resolve(moduleName);
// Clear the main module and its dependencies
const mod = require.cache[resolved];
if (mod) {
// Remove from parent's children array
if (mod.parent) {
const idx = mod.parent.children.indexOf(mod);
if (idx !== -1) {
mod.parent.children.splice(idx, 1);
}
}
// Delete from cache
delete require.cache[resolved];
}
} catch {
// Module might not be in cache, that's okay
}
}
export default {
isModuleVersionError,
extractModulePath,
findPackageRoot,
rebuildModule,
attemptAutoRebuild,
clearModuleCache
};