@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
375 lines • 13.9 kB
JavaScript
"use strict";
/**
* Installation Detector
*
* Detects package installations, updates, and dependency changes
* to trigger appropriate analysis workflows.
*/
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.InstallationDetector = 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"));
/**
* Installation Detector
*
* Monitors package installations and dependency changes to trigger
* automatic project analysis when significant changes occur.
*/
class InstallationDetector extends events_1.EventEmitter {
config;
isActive = false;
watchers = [];
lastPackageSnapshot;
debounceTimer;
constructor(config = {}) {
super();
this.config = {
projectRoot: process.cwd(),
watchLockFiles: true,
watchPackageJson: true,
debounceDelay: 3000, // 3 seconds
ignoreDevDependencies: false,
...config
};
}
/**
* Start monitoring for installation events
*/
async startMonitoring() {
if (this.isActive) {
return;
}
try {
logger_1.default.info('Starting installation monitoring', {
projectRoot: this.config.projectRoot,
watchLockFiles: this.config.watchLockFiles,
watchPackageJson: this.config.watchPackageJson
});
// Take initial snapshot
this.lastPackageSnapshot = await this.createPackageSnapshot();
// Setup file watchers
if (this.config.watchPackageJson) {
await this.setupPackageJsonWatcher();
}
if (this.config.watchLockFiles) {
await this.setupLockFileWatchers();
}
this.isActive = true;
logger_1.default.info('Installation monitoring started successfully');
}
catch (error) {
logger_1.default.error('Failed to start installation monitoring', { error });
throw error;
}
}
/**
* Stop monitoring
*/
async stopMonitoring() {
if (!this.isActive) {
return;
}
logger_1.default.info('Stopping installation monitoring');
// Clear debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = undefined;
}
// Close all watchers
for (const watcher of this.watchers) {
try {
watcher.close();
}
catch (error) {
logger_1.default.warn('Failed to close watcher', { error });
}
}
this.watchers = [];
this.isActive = false;
logger_1.default.info('Installation monitoring stopped');
}
/**
* Manually detect changes (for testing or one-time checks)
*/
async detectChanges() {
try {
const currentSnapshot = await this.createPackageSnapshot();
if (!this.lastPackageSnapshot) {
this.lastPackageSnapshot = currentSnapshot;
return null;
}
const changes = this.compareSnapshots(this.lastPackageSnapshot, currentSnapshot);
if (changes.length === 0) {
return null;
}
const event = {
type: this.determineEventType(changes),
timestamp: new Date(),
projectRoot: this.config.projectRoot,
changes,
packageManager: await this.detectPackageManager(),
triggerAnalysis: this.shouldTriggerAnalysis(changes)
};
this.lastPackageSnapshot = currentSnapshot;
logger_1.default.info('Package changes detected', {
changeCount: changes.length,
triggerAnalysis: event.triggerAnalysis,
changes: changes.map(c => `${c.name}: ${c.changeType}`)
});
return event;
}
catch (error) {
logger_1.default.error('Failed to detect package changes', { error });
return null;
}
}
/**
* Get current monitoring status
*/
getStatus() {
return {
isActive: this.isActive,
config: { ...this.config },
watcherCount: this.watchers.length,
lastSnapshot: this.lastPackageSnapshot ? new Date() : undefined
};
}
// Private methods
async setupPackageJsonWatcher() {
try {
const chokidar = await Promise.resolve().then(() => __importStar(require('chokidar')));
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
const watcher = chokidar.watch(packageJsonPath, {
persistent: false,
ignoreInitial: true
});
watcher.on('change', () => {
logger_1.default.debug('package.json changed');
this.scheduleChangeDetection();
});
watcher.on('error', (error) => {
logger_1.default.warn('Package.json watcher error', { error });
});
this.watchers.push(watcher);
logger_1.default.debug('Package.json watcher setup completed');
}
catch (error) {
logger_1.default.warn('Failed to setup package.json watcher', { error });
}
}
async setupLockFileWatchers() {
try {
const chokidar = await Promise.resolve().then(() => __importStar(require('chokidar')));
const lockFiles = [
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml'
];
for (const lockFile of lockFiles) {
const lockFilePath = path.join(this.config.projectRoot, lockFile);
try {
await fs.access(lockFilePath);
const watcher = chokidar.watch(lockFilePath, {
persistent: false,
ignoreInitial: true
});
watcher.on('change', () => {
logger_1.default.debug(`${lockFile} changed`);
this.scheduleChangeDetection();
});
watcher.on('add', () => {
logger_1.default.debug(`${lockFile} created`);
this.scheduleChangeDetection();
});
watcher.on('error', (error) => {
logger_1.default.warn(`${lockFile} watcher error`, { error });
});
this.watchers.push(watcher);
logger_1.default.debug(`${lockFile} watcher setup completed`);
}
catch {
// Lock file doesn't exist, skip
}
}
}
catch (error) {
logger_1.default.warn('Failed to setup lock file watchers', { error });
}
}
scheduleChangeDetection() {
// Clear existing timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// Schedule detection with debounce
this.debounceTimer = setTimeout(async () => {
try {
const event = await this.detectChanges();
if (event) {
this.emit('installation_detected', event);
}
}
catch (error) {
logger_1.default.error('Failed to detect changes in scheduled detection', { error });
}
}, this.config.debounceDelay);
}
async createPackageSnapshot() {
const snapshot = new Map();
try {
const packageJsonPath = path.join(this.config.projectRoot, 'package.json');
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// Add production dependencies
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.dependencies)) {
snapshot.set(name, `prod:${version}`);
}
}
// Add dev dependencies if not ignored
if (!this.config.ignoreDevDependencies && packageJson.devDependencies) {
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
snapshot.set(name, `dev:${version}`);
}
}
}
catch (error) {
logger_1.default.warn('Failed to create package snapshot', { error });
}
return snapshot;
}
compareSnapshots(oldSnapshot, newSnapshot) {
const changes = [];
// Check for added and updated packages
for (const [name, newVersionInfo] of newSnapshot) {
const [newType, newVersion] = newVersionInfo.split(':');
const isDev = newType === 'dev';
if (!oldSnapshot.has(name)) {
// Package added
changes.push({
name,
newVersion,
changeType: 'added',
isDev
});
}
else {
const oldVersionInfo = oldSnapshot.get(name);
const [, oldVersion] = oldVersionInfo.split(':');
if (oldVersion !== newVersion) {
// Package updated
changes.push({
name,
oldVersion,
newVersion,
changeType: 'updated',
isDev
});
}
}
}
// Check for removed packages
for (const [name, oldVersionInfo] of oldSnapshot) {
if (!newSnapshot.has(name)) {
const [oldType, oldVersion] = oldVersionInfo.split(':');
const isDev = oldType === 'dev';
changes.push({
name,
oldVersion,
newVersion: '',
changeType: 'removed',
isDev
});
}
}
return changes;
}
determineEventType(changes) {
const hasAdded = changes.some(c => c.changeType === 'added');
const hasUpdated = changes.some(c => c.changeType === 'updated');
const hasRemoved = changes.some(c => c.changeType === 'removed');
if (hasAdded && !hasUpdated && !hasRemoved) {
return 'package_installed';
}
else if (hasUpdated && !hasAdded && !hasRemoved) {
return 'package_updated';
}
else if (hasRemoved && !hasAdded && !hasUpdated) {
return 'package_removed';
}
else {
return 'lockfile_changed';
}
}
shouldTriggerAnalysis(changes) {
// Don't trigger for dev dependencies if ignored
if (this.config.ignoreDevDependencies) {
const nonDevChanges = changes.filter(c => !c.isDev);
if (nonDevChanges.length === 0) {
return false;
}
}
// Trigger analysis for any significant changes
return changes.some(c => c.changeType === 'added' ||
c.changeType === 'updated' ||
(c.changeType === 'removed' && !c.isDev));
}
async detectPackageManager() {
const lockFiles = [
{ file: 'package-lock.json', manager: 'npm' },
{ file: 'yarn.lock', manager: 'yarn' },
{ file: 'pnpm-lock.yaml', manager: 'pnpm' }
];
for (const { file, manager } of lockFiles) {
try {
const lockFilePath = path.join(this.config.projectRoot, file);
await fs.access(lockFilePath);
return manager;
}
catch {
// File doesn't exist, continue
}
}
return 'unknown';
}
}
exports.InstallationDetector = InstallationDetector;
//# sourceMappingURL=InstallationDetector.js.map