d7dc29498c
The bundled hook scripts use `external: ['better-sqlite3']` during esbuild, meaning the dependency must be resolved at runtime. When hooks run from the cache directory (~/.claude/plugins/cache/thedotmack/claude-mem/X.X.X/), they couldn't find better-sqlite3 because: 1. Cache directory had no package.json 2. smart-install.js was hardcoded to install in marketplace directory only This fix: - Adds plugin/package.json declaring runtime dependencies (better-sqlite3) - Updates build-hooks.js to auto-generate plugin/package.json from main package.json - Updates smart-install.js to detect execution context (cache vs marketplace) and install dependencies in the correct location The script now detects if it's running from cache (via path pattern matching) and installs dependencies there, where the hooks actually execute. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Smart Install Script for claude-mem
|
|
*
|
|
* Features:
|
|
* - Detects execution context (cache vs marketplace directory)
|
|
* - Installs dependencies where the hooks actually run (cache directory)
|
|
* - Only runs npm install when necessary (version change or missing deps)
|
|
* - Caches installation state with version marker
|
|
* - Provides helpful Windows-specific error messages
|
|
* - Cross-platform compatible (pure Node.js)
|
|
* - Fast when already installed (just version check)
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
import { execSync } from 'child_process';
|
|
import { join, dirname } from 'path';
|
|
import { homedir } from 'os';
|
|
import { createRequire } from 'module';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
// Determine the directory where THIS script is running from
|
|
// This could be either:
|
|
// 1. Cache: ~/.claude/plugins/cache/thedotmack/claude-mem/X.X.X/scripts/
|
|
// 2. Marketplace: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SCRIPT_ROOT = dirname(__dirname); // Parent of scripts/ directory
|
|
|
|
// Detect if running from cache directory (has version number in path)
|
|
const CACHE_PATTERN = /[/\\]cache[/\\]thedotmack[/\\]claude-mem[/\\]\d+\.\d+\.\d+/;
|
|
const IS_RUNNING_FROM_CACHE = CACHE_PATTERN.test(__dirname);
|
|
|
|
// Set PLUGIN_ROOT based on where we're running
|
|
// If from cache, install dependencies IN the cache directory (where hooks run)
|
|
// If from marketplace, use marketplace directory
|
|
const PLUGIN_ROOT = IS_RUNNING_FROM_CACHE
|
|
? SCRIPT_ROOT // Cache directory (e.g., ~/.claude/plugins/cache/thedotmack/claude-mem/7.0.3/)
|
|
: join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
|
|
|
const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json');
|
|
const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version');
|
|
const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules');
|
|
const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3');
|
|
|
|
// Colors for output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bright: '\x1b[1m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
red: '\x1b[31m',
|
|
cyan: '\x1b[36m',
|
|
dim: '\x1b[2m',
|
|
};
|
|
|
|
function log(message, color = colors.reset) {
|
|
console.error(`${color}${message}${colors.reset}`);
|
|
}
|
|
|
|
function getPackageVersion() {
|
|
try {
|
|
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
|
|
return packageJson.version;
|
|
} catch (error) {
|
|
log(`⚠️ Failed to read package.json: ${error.message}`, colors.yellow);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getNodeVersion() {
|
|
return process.version; // e.g., "v22.21.1"
|
|
}
|
|
|
|
function getInstalledVersion() {
|
|
try {
|
|
if (existsSync(VERSION_MARKER_PATH)) {
|
|
const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
|
|
|
|
// Try parsing as JSON (new format)
|
|
try {
|
|
const marker = JSON.parse(content);
|
|
return {
|
|
packageVersion: marker.packageVersion,
|
|
nodeVersion: marker.nodeVersion,
|
|
installedAt: marker.installedAt
|
|
};
|
|
} catch {
|
|
// Fallback: old format (plain text version string)
|
|
return {
|
|
packageVersion: content,
|
|
nodeVersion: null, // Unknown
|
|
installedAt: null
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Marker doesn't exist or can't be read
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setInstalledVersion(packageVersion, nodeVersion) {
|
|
try {
|
|
const marker = {
|
|
packageVersion,
|
|
nodeVersion,
|
|
installedAt: new Date().toISOString()
|
|
};
|
|
writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf-8');
|
|
} catch (error) {
|
|
log(`⚠️ Failed to write version marker: ${error.message}`, colors.yellow);
|
|
}
|
|
}
|
|
|
|
function needsInstall() {
|
|
// Check if package.json exists (required for npm install)
|
|
if (!existsSync(PACKAGE_JSON_PATH)) {
|
|
log(`⚠️ No package.json found at ${PLUGIN_ROOT}`, colors.yellow);
|
|
return false; // Can't install without package.json
|
|
}
|
|
|
|
// Check if node_modules exists
|
|
if (!existsSync(NODE_MODULES_PATH)) {
|
|
log('📦 Dependencies not found - first time setup', colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// Check if better-sqlite3 is installed
|
|
if (!existsSync(BETTER_SQLITE3_PATH)) {
|
|
log('📦 better-sqlite3 missing - reinstalling', colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// Check version marker
|
|
const currentPackageVersion = getPackageVersion();
|
|
const currentNodeVersion = getNodeVersion();
|
|
const installed = getInstalledVersion();
|
|
|
|
if (!installed) {
|
|
log('📦 No version marker found - installing', colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// Check package version
|
|
if (currentPackageVersion !== installed.packageVersion) {
|
|
log(`📦 Version changed (${installed.packageVersion} → ${currentPackageVersion}) - updating`, colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// Check Node.js version
|
|
if (installed.nodeVersion && currentNodeVersion !== installed.nodeVersion) {
|
|
log(`📦 Node.js version changed (${installed.nodeVersion} → ${currentNodeVersion}) - rebuilding native modules`, colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// If old format (no nodeVersion), assume needs install
|
|
if (!installed.nodeVersion) {
|
|
log('📦 Old version marker format - updating', colors.cyan);
|
|
return true;
|
|
}
|
|
|
|
// All good - no install needed
|
|
log(`✓ Dependencies already installed (v${currentPackageVersion})`, colors.dim);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Verify that better-sqlite3 native module loads correctly
|
|
* This catches ABI mismatches and corrupted builds
|
|
*/
|
|
async function verifyNativeModules() {
|
|
try {
|
|
log('🔍 Verifying native modules...', colors.dim);
|
|
|
|
// Use createRequire() to resolve from PLUGIN_ROOT's node_modules
|
|
const require = createRequire(join(PLUGIN_ROOT, 'package.json'));
|
|
const Database = require('better-sqlite3');
|
|
|
|
// Try to create a test in-memory database
|
|
const db = new Database(':memory:');
|
|
|
|
// Run a simple query to ensure it works
|
|
const result = db.prepare('SELECT 1 + 1 as result').get();
|
|
|
|
// Clean up
|
|
db.close();
|
|
|
|
if (result.result !== 2) {
|
|
throw new Error('SQLite math check failed');
|
|
}
|
|
|
|
log('✓ Native modules verified', colors.dim);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
if (error.code === 'ERR_DLOPEN_FAILED') {
|
|
log('⚠️ Native module ABI mismatch detected', colors.yellow);
|
|
return false;
|
|
}
|
|
|
|
// Other errors are unexpected - log and fail
|
|
log(`❌ Native module verification failed: ${error.message}`, colors.red);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getWindowsErrorHelp(errorOutput) {
|
|
// Detect Python version at runtime
|
|
let pythonStatus = ' Python not detected or version unknown';
|
|
try {
|
|
const pythonVersion = execSync('python --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
const versionMatch = pythonVersion.match(/Python\s+([\d.]+)/);
|
|
if (versionMatch) {
|
|
pythonStatus = ` You have ${versionMatch[0]} installed ✓`;
|
|
}
|
|
} catch (error) {
|
|
// Python not available or failed to detect - use default message
|
|
}
|
|
|
|
const help = [
|
|
'',
|
|
'╔══════════════════════════════════════════════════════════════════════╗',
|
|
'║ Windows Installation Help ║',
|
|
'╚══════════════════════════════════════════════════════════════════════╝',
|
|
'',
|
|
'📋 better-sqlite3 requires build tools to compile native modules.',
|
|
'',
|
|
'🔧 Option 1: Install Visual Studio Build Tools (Recommended)',
|
|
' 1. Download: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
|
' 2. Install "Desktop development with C++"',
|
|
' 3. Restart your terminal',
|
|
' 4. Try again',
|
|
'',
|
|
'🔧 Option 2: Install via npm (automated)',
|
|
' Run as Administrator:',
|
|
' npm install --global windows-build-tools',
|
|
'',
|
|
'🐍 Python Requirement:',
|
|
' Python 3.6+ is required.',
|
|
pythonStatus,
|
|
'',
|
|
];
|
|
|
|
// Check for specific error patterns
|
|
if (errorOutput.includes('MSBuild.exe')) {
|
|
help.push('❌ MSBuild not found - install Visual Studio Build Tools');
|
|
}
|
|
if (errorOutput.includes('MSVS')) {
|
|
help.push('❌ Visual Studio not detected - install Build Tools');
|
|
}
|
|
if (errorOutput.includes('permission') || errorOutput.includes('EPERM')) {
|
|
help.push('❌ Permission denied - try running as Administrator');
|
|
}
|
|
|
|
help.push('');
|
|
help.push('📖 Full documentation: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md');
|
|
help.push('');
|
|
|
|
return help.join('\n');
|
|
}
|
|
|
|
async function runNpmInstall() {
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
log('', colors.cyan);
|
|
log(`🔨 Installing dependencies in ${IS_RUNNING_FROM_CACHE ? 'cache' : 'marketplace'}...`, colors.bright);
|
|
log(` ${PLUGIN_ROOT}`, colors.dim);
|
|
log('', colors.reset);
|
|
|
|
// Try normal install first, then retry with force if it fails
|
|
const strategies = [
|
|
{ command: 'npm install', label: 'normal' },
|
|
{ command: 'npm install --force', label: 'with force flag' },
|
|
];
|
|
|
|
let lastError = null;
|
|
|
|
for (const { command, label } of strategies) {
|
|
try {
|
|
log(`Attempting install ${label}...`, colors.dim);
|
|
|
|
// Run npm install silently
|
|
execSync(command, {
|
|
cwd: PLUGIN_ROOT,
|
|
stdio: 'pipe', // Silent output unless error
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
// Verify better-sqlite3 was installed
|
|
if (!existsSync(BETTER_SQLITE3_PATH)) {
|
|
throw new Error('better-sqlite3 installation verification failed');
|
|
}
|
|
|
|
// Verify native modules actually work
|
|
const nativeModulesWork = await verifyNativeModules();
|
|
if (!nativeModulesWork) {
|
|
throw new Error('Native modules failed to load after install');
|
|
}
|
|
|
|
const packageVersion = getPackageVersion();
|
|
const nodeVersion = getNodeVersion();
|
|
setInstalledVersion(packageVersion, nodeVersion);
|
|
|
|
log('', colors.green);
|
|
log('✅ Dependencies installed successfully!', colors.bright);
|
|
log(` Package version: ${packageVersion}`, colors.dim);
|
|
log(` Node.js version: ${nodeVersion}`, colors.dim);
|
|
log('', colors.reset);
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
lastError = error;
|
|
// Continue to next strategy
|
|
}
|
|
}
|
|
|
|
// All strategies failed - show error
|
|
log('', colors.red);
|
|
log('❌ Installation failed after retrying!', colors.bright);
|
|
log('', colors.reset);
|
|
|
|
// Provide Windows-specific help
|
|
if (isWindows && lastError && lastError.message && lastError.message.includes('better-sqlite3')) {
|
|
log(getWindowsErrorHelp(lastError.message), colors.yellow);
|
|
}
|
|
|
|
// Show generic error info with troubleshooting steps
|
|
if (lastError) {
|
|
if (lastError.stderr) {
|
|
log('Error output:', colors.dim);
|
|
log(lastError.stderr.toString(), colors.red);
|
|
} else if (lastError.message) {
|
|
log(lastError.message, colors.red);
|
|
}
|
|
|
|
log('', colors.yellow);
|
|
log('📋 Troubleshooting Steps:', colors.bright);
|
|
log('', colors.reset);
|
|
log('1. Check your internet connection', colors.yellow);
|
|
log('2. Try running: npm cache clean --force', colors.yellow);
|
|
log('3. Try running: npm install (in plugin directory)', colors.yellow);
|
|
log('4. Check npm version: npm --version (requires npm 7+)', colors.yellow);
|
|
log('5. Try updating npm: npm install -g npm@latest', colors.yellow);
|
|
log('', colors.reset);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
// Log execution context for debugging
|
|
if (IS_RUNNING_FROM_CACHE) {
|
|
log('📍 Running from cache directory', colors.dim);
|
|
} else {
|
|
log('📍 Running from marketplace directory', colors.dim);
|
|
}
|
|
|
|
// Check if we need to install dependencies
|
|
const installNeeded = needsInstall();
|
|
|
|
if (installNeeded) {
|
|
// Run installation (now async)
|
|
const installSuccess = await runNpmInstall();
|
|
|
|
if (!installSuccess) {
|
|
log('', colors.red);
|
|
log('⚠️ Installation failed', colors.yellow);
|
|
log('', colors.reset);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
// Even if install not needed, verify native modules work
|
|
const nativeModulesWork = await verifyNativeModules();
|
|
|
|
if (!nativeModulesWork) {
|
|
log('📦 Native modules need rebuild - reinstalling', colors.cyan);
|
|
const installSuccess = await runNpmInstall();
|
|
|
|
if (!installSuccess) {
|
|
log('', colors.red);
|
|
log('⚠️ Native module rebuild failed', colors.yellow);
|
|
log('', colors.reset);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: Worker auto-start disabled in smart-install.js
|
|
// The context-hook.js calls ensureWorkerRunning() which handles worker startup
|
|
// This avoids potential process management conflicts during plugin initialization
|
|
log('✅ Installation complete', colors.green);
|
|
|
|
// Success - dependencies installed (if needed)
|
|
process.exit(0);
|
|
|
|
} catch (error) {
|
|
log(`❌ Unexpected error: ${error.message}`, colors.red);
|
|
log('', colors.reset);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|