git-contextor
Version:
A code context tool with vector search and real-time monitoring, with optional Git integration.
252 lines (216 loc) • 8.94 kB
JavaScript
const path = require('path');
const fs = require('fs').promises;
const logger = require('../cli/utils/logger');
const apiServer = require('../api/server');
const simpleGit = require('simple-git');
const SharingService = require('./SharingService');
class ServiceManager {
constructor(repoPath, config, services) {
this.repoPath = repoPath;
this.config = config;
this.services = services; // { fileWatcher, indexer, vectorStore, contextOptimizer }
this.sharingService = new SharingService(this.repoPath, this.config);
this.pidFile = path.join(this.repoPath, '.gitcontextor', 'daemon.pid');
this.isGitRepo = false;
this.summaryUpdateInterval = null;
this.isUpdatingSummary = false;
this.lastSummaryUpdateTime = null;
}
async validateEnvironment() {
// Check if we're in a git repository, but don't make it mandatory.
try {
const git = simpleGit(this.repoPath);
await git.status();
this.isGitRepo = true;
logger.info('Git repository detected. Git-related features are enabled.');
} catch (error) {
this.isGitRepo = false;
logger.warn('No Git repository detected. Operating in non-Git mode. File discovery will scan the directory.');
}
// Check write permissions
try {
await fs.access(this.repoPath, fs.constants.W_OK);
} catch (error) {
throw new Error('No write permission in current directory.');
}
// Validate embedding config
const { provider, apiKey } = this.config.embedding;
if (provider !== 'local' && !apiKey) {
throw new Error(`${provider} embedding provider requires an API key. Set it with: git-contextor config --api-key YOUR_KEY`);
}
}
async start(options = {}) {
logger.info('Starting Git Contextor services...');
if (options.watch === false) {
this.config.monitoring.watchEnabled = false;
logger.info('File watching disabled for this session via the --no-watch flag.');
}
await this.validateEnvironment();
try {
await fs.writeFile(this.pidFile, process.pid.toString());
} catch (err) {
logger.error('Failed to write PID file.', err);
throw new Error(`Could not write PID file to ${this.pidFile}. Check permissions.`);
}
if (this.services.indexer) {
this.services.indexer.isGitRepo = this.isGitRepo;
}
if (this.services.fileWatcher) {
this.services.fileWatcher.isGitRepo = this.isGitRepo;
}
// Start API server first to respond to health checks quickly
const servicesForApi = {
...this.services,
sharingService: this.sharingService
};
await apiServer.start(this.config, servicesForApi, this);
logger.success('Git Contextor services started successfully. API is up.');
// Now start other services
if (this.config.monitoring.watchEnabled) {
this.services.fileWatcher.start();
} else {
logger.info('File watcher is disabled by configuration.');
}
// Start initial indexing in the background after a short delay
// to ensure the API server and other services are fully responsive.
setTimeout(() => {
logger.info('Starting initial repository index in the background...');
this.services.indexer.reindexAll()
.then(() => {
logger.info('Initial index complete.');
})
.catch(error => {
logger.error('Initial repository index failed. The index may be incomplete.', error);
});
}, 100);
// Start the idle summary updater
this.summaryUpdateInterval = setInterval(() => this.checkForIdleAndUpdateSummary(), 5000); // Check every 5 seconds
}
async stop({ silent = false } = {}) {
if (!silent) {
logger.info('Stopping Git Contextor services...');
}
if (!await this.isRunning() && !silent) {
logger.warn('Git Contextor is not running.');
// Clean up stale pid file if it exists but process is dead
try {
await fs.unlink(this.pidFile);
logger.info('Removed stale PID file.');
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn(`Could not remove stale PID file: ${this.pidFile}`, error);
}
}
return;
}
// Stop file watcher
if (this.services.fileWatcher) {
this.services.fileWatcher.stop();
}
if (this.summaryUpdateInterval) {
clearInterval(this.summaryUpdateInterval);
}
// Stop API and UI servers
await apiServer.stop();
// Delete collection if not configured to keep
if (!this.config.services.keepCollectionOnExit && this.services.vectorStore) {
if (!silent) {
logger.info('Removing Qdrant collection as per configuration...');
}
await this.services.vectorStore.deleteCollection();
}
// Remove PID file
try {
await fs.unlink(this.pidFile);
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn(`Could not remove PID file: ${this.pidFile}`, error);
}
}
if (!silent) {
logger.info('Git Contextor services stopped.');
}
}
async getStatus() {
if (!await this.isRunning()) {
return { status: 'stopped', message: 'Git Contextor is not running.' };
}
const pid = await this.readPidFile();
const indexerStatus = await this.services.indexer.getStatus();
return {
status: 'running',
pid: pid,
repoPath: this.repoPath,
api: {
port: this.config.services.port,
status: 'running' // Simplified for now
},
ui: {
port: this.config.services.port,
status: 'running' // Simplified for now
},
watcher: {
status: this.config.monitoring.watchEnabled ? 'enabled' : 'disabled'
},
indexing: indexerStatus
};
}
async isRunning() {
try {
const pid = await this.readPidFile();
if (!pid) return false;
// The `process.kill` with signal 0 is a test for process existence.
process.kill(pid, 0);
return true;
} catch (error) {
// ESRCH means process doesn't exist, which is what we expect if it's not running.
// ENOENT means pidfile doesn't exist.
if (error.code === 'ESRCH' || error.code === 'ENOENT') {
return false;
}
// Other errors should be thrown
throw error;
}
}
async readPidFile() {
try {
const pidString = await fs.readFile(this.pidFile, 'utf8');
return parseInt(pidString, 10);
} catch (error) {
return null;
}
}
async checkForIdleAndUpdateSummary() {
if (this.isUpdatingSummary) {
return;
}
try {
const indexerStatus = await this.services.indexer.getStatus();
if (indexerStatus.status !== 'idle' || !indexerStatus.lastActivity) {
return;
}
const lastActivityTime = new Date(indexerStatus.lastActivity).getTime();
// If we've already updated the summary since the last activity, do nothing.
if (this.lastSummaryUpdateTime && lastActivityTime < this.lastSummaryUpdateTime) {
return;
}
const idleTime = Date.now() - lastActivityTime;
if (idleTime > 20000) { // More than 20 seconds idle
this.isUpdatingSummary = true;
logger.info('Indexer has been idle for over 20 seconds. Automatically updating collection summary...');
try {
await this.services.contextOptimizer.summarizeCollection();
this.lastSummaryUpdateTime = Date.now();
logger.info('Automatic collection summary update completed successfully.');
} catch (error) {
logger.error('Automatic collection summary update failed:', error);
} finally {
this.isUpdatingSummary = false;
}
}
} catch (error) {
logger.error('Error during idle summary check:', error);
}
}
}
module.exports = ServiceManager;