@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
388 lines (380 loc) âĸ 15.1 kB
JavaScript
"use strict";
/**
* NPM Hook Manager
*
* Manages NPM lifecycle hooks to automatically trigger project analysis
* when packages are installed or updated.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NPMHookManager = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const events_1 = require("events");
const logger_1 = __importDefault(require("../../utils/logger"));
/**
* NPM Hook Manager
*
* Provides automatic triggering of project analysis when npm packages
* are installed, updated, or when dependencies change.
*/
class NPMHookManager extends events_1.EventEmitter {
config;
isInitialized = false;
hookScriptContent;
packageJsonWatcher;
constructor(config = {}) {
super();
this.config = {
enablePostInstall: true,
enablePreInstall: false,
enablePostUpdate: true,
projectRoot: process.cwd(),
analysisDelay: 2000, // 2 second delay to allow npm to finish
...config
};
this.hookScriptContent = this.generateHookScript();
}
/**
* Initialize NPM hooks
*/
async initialize() {
if (this.isInitialized) {
return;
}
try {
logger_1.default.info('Initializing NPM Hook Manager', {
projectRoot: this.config.projectRoot,
enablePostInstall: this.config.enablePostInstall
});
// Ensure project root exists
await this.validateProjectRoot();
// Install NPM hooks
if (this.config.enablePostInstall) {
await this.installPostInstallHook();
}
if (this.config.enablePreInstall) {
await this.installPreInstallHook();
}
if (this.config.enablePostUpdate) {
await this.installPostUpdateHook();
}
// Setup package.json watcher for dependency changes
await this.setupPackageJsonWatcher();
this.isInitialized = true;
logger_1.default.info('NPM Hook Manager initialized successfully');
}
catch (error) {
logger_1.default.error('Failed to initialize NPM Hook Manager', { error });
throw error;
}
}
/**
* Manually trigger analysis (for testing or manual execution)
*/
async triggerAnalysis(reason = 'manual') {
const event = {
type: 'postinstall',
timestamp: new Date(),
projectRoot: this.config.projectRoot,
triggeredBy: 'manual'
};
logger_1.default.info('Manually triggering project analysis', { reason, event });
// Add delay to simulate npm completion
setTimeout(() => {
this.emit('analysis_triggered', event);
}, this.config.analysisDelay);
}
/**
* Check if hooks are properly installed
*/
async validateHooks() {
const issues = [];
try {
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Check postinstall script
if (this.config.enablePostInstall) {
const postinstallScript = packageJson.scripts?.postinstall;
if (!postinstallScript || !postinstallScript.includes('aaswe-analysis')) {
issues.push('postinstall hook not found or invalid');
}
}
// Check if hook script exists
const hookScriptPath = this.getHookScriptPath();
try {
await fs.access(hookScriptPath);
}
catch {
issues.push('hook script file not found');
}
return {
isValid: issues.length === 0,
issues
};
}
catch (error) {
issues.push(`validation error: ${error instanceof Error ? error.message : 'unknown'}`);
return { isValid: false, issues };
}
}
/**
* Remove installed hooks
*/
async removeHooks() {
try {
logger_1.default.info('Removing NPM hooks');
// Remove from package.json
await this.removeFromPackageJson();
// Remove hook script file
const hookScriptPath = this.getHookScriptPath();
try {
await fs.unlink(hookScriptPath);
logger_1.default.debug('Hook script file removed', { path: hookScriptPath });
}
catch (error) {
logger_1.default.warn('Failed to remove hook script file', { path: hookScriptPath, error });
}
// Stop package.json watcher
if (this.packageJsonWatcher) {
this.packageJsonWatcher.close();
this.packageJsonWatcher = undefined;
}
this.isInitialized = false;
logger_1.default.info('NPM hooks removed successfully');
}
catch (error) {
logger_1.default.error('Failed to remove NPM hooks', { error });
throw error;
}
}
/**
* Get hook installation status
*/
getStatus() {
return {
isInitialized: this.isInitialized,
config: { ...this.config },
hookScriptPath: this.getHookScriptPath()
};
}
// Private methods
async validateProjectRoot() {
try {
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
await fs.access(packageJsonPath);
}
catch {
throw new Error(`Invalid project root: package.json not found in ${this.config.projectRoot}`);
}
}
async installPostInstallHook() {
logger_1.default.debug('Installing postinstall hook');
// Create hook script
await this.createHookScript();
// Update package.json
await this.updatePackageJson('postinstall', 'node .aaswe/hooks/postinstall.js');
logger_1.default.debug('Postinstall hook installed successfully');
}
async installPreInstallHook() {
logger_1.default.debug('Installing preinstall hook');
await this.updatePackageJson('preinstall', 'node .aaswe/hooks/preinstall.js');
logger_1.default.debug('Preinstall hook installed successfully');
}
async installPostUpdateHook() {
logger_1.default.debug('Installing postupdate hook');
await this.updatePackageJson('postupdate', 'node .aaswe/hooks/postupdate.js');
logger_1.default.debug('Postupdate hook installed successfully');
}
async createHookScript() {
const hookScriptPath = this.getHookScriptPath();
const hookDir = path.dirname(hookScriptPath);
// Ensure directory exists
await fs.mkdir(hookDir, { recursive: true });
// Write hook script
await fs.writeFile(hookScriptPath, this.hookScriptContent, 'utf8');
// Make executable (Unix systems)
if (process.platform !== 'win32') {
await fs.chmod(hookScriptPath, 0o755);
}
logger_1.default.debug('Hook script created', { path: hookScriptPath });
}
async updatePackageJson(scriptName, scriptCommand) {
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
try {
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Initialize scripts object if it doesn't exist
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Check if script already exists
const existingScript = packageJson.scripts[scriptName];
if (existingScript && existingScript.includes('aaswe-analysis')) {
logger_1.default.debug(`${scriptName} script already exists, skipping`);
return;
}
// Add or append to existing script
if (existingScript) {
packageJson.scripts[scriptName] = `${existingScript} && ${scriptCommand}`;
}
else {
packageJson.scripts[scriptName] = scriptCommand;
}
// Write back to package.json
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
logger_1.default.debug(`Updated package.json with ${scriptName} script`);
}
catch (error) {
logger_1.default.error(`Failed to update package.json with ${scriptName} script`, { error });
throw error;
}
}
async removeFromPackageJson() {
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
try {
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
if (!packageJson.scripts) {
return;
}
// Remove or clean scripts
const scriptsToClean = ['postinstall', 'preinstall', 'postupdate'];
for (const scriptName of scriptsToClean) {
const script = packageJson.scripts[scriptName];
if (script && script.includes('aaswe-analysis')) {
// Remove the entire script if it only contains our hook
if (script.trim() === 'node .aaswe/hooks/postinstall.js' ||
script.trim().startsWith('node .aaswe/hooks/')) {
delete packageJson.scripts[scriptName];
}
else {
// Remove our part from the script
packageJson.scripts[scriptName] = script
.replace(/\s*&&\s*node \.aaswe\/hooks\/\w+\.js/, '')
.replace(/node \.aaswe\/hooks\/\w+\.js\s*&&\s*/, '')
.trim();
}
}
}
// Write back to package.json
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
logger_1.default.debug('Cleaned package.json scripts');
}
catch (error) {
logger_1.default.error('Failed to clean package.json scripts', { error });
throw error;
}
}
async setupPackageJsonWatcher() {
try {
const chokidar = await Promise.resolve().then(() => __importStar(require('chokidar')));
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
this.packageJsonWatcher = chokidar.watch(packageJsonPath, {
persistent: false,
ignoreInitial: true
});
this.packageJsonWatcher.on('change', () => {
logger_1.default.debug('package.json changed, checking for dependency updates');
const event = {
type: 'postupdate',
timestamp: new Date(),
projectRoot: this.config.projectRoot,
triggeredBy: 'npm'
};
// Delay to allow file system to settle
setTimeout(() => {
this.emit('analysis_triggered', event);
}, this.config.analysisDelay);
});
logger_1.default.debug('Package.json watcher setup completed');
}
catch (error) {
logger_1.default.warn('Failed to setup package.json watcher', { error });
// Don't throw - watcher is optional
}
}
getHookScriptPath() {
if (this.config.hookScriptPath) {
return this.config.hookScriptPath;
}
return path.join(this.config.projectRoot, '.aaswe', 'hooks', 'postinstall.js');
}
generateHookScript() {
return `#!/usr/bin/env node
/**
* AASWE Automatic Analysis Hook
*
* This script is automatically executed after npm install to trigger
* project analysis and TTL generation.
*/
const { spawn } = require('child_process');
const path = require('path');
async function triggerAnalysis() {
console.log('đ AASWE: Starting automatic project analysis...');
try {
// Check if AASWE is available
const aasweCommand = process.platform === 'win32' ? 'aaswe.cmd' : 'aaswe';
// Trigger analysis with automatic flag
const analysisProcess = spawn(aasweCommand, ['analyze', '--auto', '--quiet'], {
stdio: 'inherit',
cwd: process.cwd()
});
analysisProcess.on('close', (code) => {
if (code === 0) {
console.log('â
AASWE: Project analysis completed successfully');
} else {
console.log('â ī¸ AASWE: Project analysis completed with warnings');
}
});
analysisProcess.on('error', (error) => {
console.log('âšī¸ AASWE: Analysis will be available after installation completes');
console.log(' Run "npx aaswe analyze" manually to generate knowledge files');
});
} catch (error) {
console.log('âšī¸ AASWE: Manual analysis available with "npx aaswe analyze"');
}
}
// Add small delay to ensure npm has finished
setTimeout(triggerAnalysis, 1000);
`;
}
}
exports.NPMHookManager = NPMHookManager;
//# sourceMappingURL=NPMHookManager.js.map