@vibe-dev-kit/cli
Version:
Advanced Command-line toolkit that analyzes your codebase and deploys project-aware rules, memories, commands and agents to any AI coding assistant - VDK is the world's first Vibe Development Kit
371 lines (327 loc) • 11.3 kB
JavaScript
/**
* IDE Plugin Integration Module
* --------------------------
* Provides hooks and utilities for integrating the Project Scanner with IDE plugins.
* Enables real-time rule generation and synchronization with editors.
* Part of Phase 3 implementation for the Project-Specific Rule Generator.
*/
import chalk from 'chalk';
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import {
detectIDEs as detectIDEConfigs,
ensureRuleDirectory,
getIDEConfigById,
getIDEConfigPaths,
IDE_CONFIGURATIONS,
} from '../../shared/ide-configuration.js';
/**
* IDE Integration Manager class
* Handles communication between the Project Scanner and IDE plugins
*/
class IDEIntegrationManager {
constructor(options = {}) {
this.options = {
supportedIDEs: options.supportedIDEs || IDE_CONFIGURATIONS.map((ide) => ide.id),
watchMode: options.watchMode || false,
pollingInterval: options.pollingInterval || 5000, // 5 seconds default
verbose: options.verbose || false,
};
this.integrations = {};
this.watchers = {};
this.lastScanTimestamp = Date.now();
this.changeBuffer = {};
}
/**
* Initialize IDE integrations based on detected IDE configuration files
* @param {string} projectPath - Path to the project
* @returns {Array} - List of initialized integrations
*/
async initialize(projectPath) {
console.log(chalk.blue('\nInitializing IDE integrations...'));
const detectedIDEs = await this.detectIDEs(projectPath);
if (detectedIDEs.length === 0) {
console.log(
chalk.yellow('No supported IDE configurations detected. Using generic integration.')
);
detectedIDEs.push('generic');
}
// Initialize each detected integration
for (const ideId of detectedIDEs) {
await this.initializeIntegration(ideId, projectPath);
}
console.log(
chalk.green(`IDE integrations initialized: ${Object.keys(this.integrations).join(', ')}`)
);
return Object.keys(this.integrations);
}
/**
* Detect IDE configuration files in the project
* @param {string} projectPath - Path to the project
* @returns {Array} - List of detected IDE IDs
*/
async detectIDEs(projectPath) {
// Use the centralized detection function
const detectedConfigs = detectIDEConfigs(projectPath);
// Filter by supported IDEs and return IDs only
return detectedConfigs
.filter((config) => this.options.supportedIDEs.includes(config.id))
.map((config) => config.id);
}
/**
* Initialize an IDE integration
* @param {string} ideId - IDE identifier
* @param {string} projectPath - Path to the project
*/
async initializeIntegration(ideId, projectPath) {
// Get configuration paths from centralized module
const paths = getIDEConfigPaths(ideId, projectPath);
const ideConfig = getIDEConfigById(ideId);
// Create the integration object
this.integrations[ideId] = {
id: ideId,
name: ideConfig ? ideConfig.name : ideId,
configPath: paths.configPath,
rulePath: paths.rulePath,
handlers: this.getHandlers(ideId),
};
// Create rule directories if they don't exist
const rulePath = this.integrations[ideId].rulePath;
if (!fs.existsSync(rulePath)) {
fs.mkdirSync(rulePath, { recursive: true });
console.log(
chalk.green(`Created rule directory for ${this.integrations[ideId].name}: ${rulePath}`)
);
}
// If in watch mode, start file watchers for IDE config changes
if (this.options.watchMode) {
await this.startWatcher(ideId, projectPath);
}
if (this.options.verbose) {
console.log(
chalk.gray(`Initialized ${this.integrations[ideId].name} integration at ${rulePath}`)
);
}
}
/**
* Get the configuration path for an IDE
* @param {string} ideId - IDE identifier
* @param {string} projectPath - Path to the project
* @returns {string} - Path to the IDE configuration
*/
getConfigPath(ideId, projectPath) {
const paths = getIDEConfigPaths(ideId, projectPath);
return paths.configPath;
}
/**
* Get the rule path for an IDE
* @param {string} ideId - IDE identifier
* @param {string} projectPath - Path to the project
* @returns {string} - Path to the IDE rules
*/
getRulePath(ideId, projectPath) {
const paths = getIDEConfigPaths(ideId, projectPath);
return paths.rulePath;
}
/**
* Get handlers for IDE-specific events
* @param {string} ide - IDE identifier
* @returns {Object} - Handler functions
*/
getHandlers(ide) {
// Define handlers for different IDE events
return {
onConfigChange: async (configPath) => {
console.log(chalk.yellow(`${ide} configuration changed, updating rules...`));
await this.regenerateRules(ide);
},
onRuleUpdate: async (rulePath) => {
console.log(chalk.green(`Rules updated for ${ide}`));
// Notify the IDE about rule changes if supported
switch (ide) {
case 'vscode':
case 'vscode-insiders':
this.notifyVSCode(rulePath, ide);
break;
case 'cursor':
this.notifyCursor(rulePath);
break;
case 'windsurf':
case 'windsurf-next':
this.notifyWindsurf(rulePath, ide);
break;
case 'github-copilot':
this.notifyGitHubCopilot(rulePath);
break;
case 'claude':
case 'claude-desktop':
this.notifyClaude(rulePath, ide);
break;
case 'zed':
this.notifyZed(rulePath);
break;
case 'jetbrains':
this.notifyJetBrains(rulePath);
break;
default:
console.log(chalk.gray(`[${ide} notification] Rules updated at ${rulePath}`));
break;
}
},
};
}
/**
* Start a file watcher for an IDE configuration
* @param {string} ide - IDE identifier
* @param {string} projectPath - Path to the project
*/
async startWatcher(ide, projectPath) {
const configPath = this.getConfigPath(ide, projectPath);
if (!fs.existsSync(configPath)) {
return;
}
console.log(chalk.blue(`Starting watcher for ${ide} configuration...`));
// Simple polling-based file watcher
this.watchers[ide] = setInterval(() => {
const stats = fs.statSync(configPath);
if (stats.mtime.getTime() > this.lastScanTimestamp) {
this.bufferChange(ide, configPath);
}
}, this.options.pollingInterval);
}
/**
* Buffer changes to prevent multiple rapid regenerations
* @param {string} ide - IDE identifier
* @param {string} configPath - Path to the changed configuration
*/
bufferChange(ide, configPath) {
if (!this.changeBuffer[ide]) {
this.changeBuffer[ide] = {
timestamp: Date.now(),
path: configPath,
timer: setTimeout(() => {
// Process the change after a delay
if (this.integrations[ide] && this.integrations[ide].handlers.onConfigChange) {
this.integrations[ide].handlers.onConfigChange(configPath);
}
delete this.changeBuffer[ide];
this.lastScanTimestamp = Date.now();
}, 2000), // 2 second debounce
};
}
}
/**
* Regenerate rules for an IDE
* @param {string} ide - IDE identifier
*/
async regenerateRules(ide) {
if (!this.integrations[ide]) {
return;
}
const rulePath = this.integrations[ide].rulePath;
const projectPath = path.dirname(path.dirname(rulePath));
console.log(chalk.blue(`Regenerating rules for ${ide}...`));
// Launch the scanner process as a background task
const scanner = spawn(
'node',
[
path.join(process.cwd(), 'src', 'index.js'),
'--path',
projectPath,
'--output',
rulePath,
'--verbose',
],
{
detached: true,
stdio: 'ignore',
}
);
// Don't wait for the process to complete, let it run in background
scanner.unref();
console.log(chalk.green(`Rule regeneration initiated for ${ide}`));
}
/**
* Notify VS Code about rule changes
* @param {string} rulePath - Path to the rules
* @param {string} variant - 'vscode' or 'vscode-insiders'
*/
notifyVSCode(rulePath, variant = 'vscode') {
// This would use the VS Code extension API when implemented
console.log(
chalk.gray(
`[${variant === 'vscode-insiders' ? 'VS Code Insiders' : 'VS Code'} notification] Rules updated at ${rulePath}`
)
);
}
/**
* Notify Cursor about rule changes
* @param {string} rulePath - Path to the rules
*/
notifyCursor(rulePath) {
// This would use Cursor-specific mechanisms when implemented
console.log(chalk.gray(`[Cursor notification] Rules updated at ${rulePath}`));
}
/**
* Notify Windsurf about rule changes
* @param {string} rulePath - Path to the rules
* @param {string} variant - 'windsurf' or 'windsurf-next'
*/
notifyWindsurf(rulePath, variant = 'windsurf') {
// This would use Windsurf-specific mechanisms when implemented
const variantName = variant === 'windsurf-next' ? 'Windsurf Next' : 'Windsurf';
console.log(chalk.gray(`[${variantName} notification] Rules updated at ${rulePath}`));
}
/**
* Notify GitHub Copilot about rule changes
* @param {string} rulePath - Path to the rules
*/
notifyGitHubCopilot(rulePath) {
// This would use GitHub Copilot-specific mechanisms when implemented
console.log(chalk.gray(`[GitHub Copilot notification] Rules updated at ${rulePath}`));
}
/**
* Notify Claude about rule changes
* @param {string} rulePath - Path to the rules
* @param {string} variant - 'claude' or 'claude-desktop'
*/
notifyClaude(rulePath, variant = 'claude') {
// This would use Claude-specific mechanisms when implemented
const variantName = variant === 'claude-desktop' ? 'Claude Desktop' : 'Claude Code';
console.log(chalk.gray(`[${variantName} notification] Rules updated at ${rulePath}`));
}
/**
* Notify Zed Editor about rule changes
* @param {string} rulePath - Path to the rules
*/
notifyZed(rulePath) {
// This would use Zed-specific mechanisms when implemented
console.log(chalk.gray(`[Zed Editor notification] Rules updated at ${rulePath}`));
}
/**
* Notify JetBrains IDE about rule changes
* @param {string} rulePath - Path to the rules
*/
notifyJetBrains(rulePath) {
// This would use JetBrains-specific mechanisms when implemented
console.log(chalk.gray(`[JetBrains notification] Rules updated at ${rulePath}`));
}
/**
* Clean up resources when shutting down
*/
shutdown() {
// Clear all watchers
for (const ide in this.watchers) {
clearInterval(this.watchers[ide]);
}
// Clear any pending change timers
for (const ide in this.changeBuffer) {
if (this.changeBuffer[ide].timer) {
clearTimeout(this.changeBuffer[ide].timer);
}
}
console.log(chalk.blue('IDE integrations shut down'));
}
}
export { IDEIntegrationManager };