import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from '../../utils/logger.js'; import { eventBus } from '../../mcp/EventBus.js'; const execAsync = promisify(exec); export interface DevToolsStatus { lastScan: string | null; repoCount: number; brainPath: string; isScanning: boolean; lastError: string | null; } export class DevToolsService { private static instance: DevToolsService; private isScanning = false; private devToolsPath: string; private brainPath: string | null = null; private constructor() { // Assuming DevTools-Monorepo is parallel to WidgeTDC root // WidgeTDC/apps/backend -> ../../../DevTools-Monorepo this.devToolsPath = path.resolve(process.cwd(), '../../../DevTools-Monorepo'); this.findBrainPath(); } public static getInstance(): DevToolsService { if (!DevToolsService.instance) { DevToolsService.instance = new DevToolsService(); } return DevToolsService.instance; } private async findBrainPath() { // Logic mirrors PowerShell script to find active brain const potentialPaths = [ 'G:\\My Drive\\WidgeTDC-Config', path.join(process.env.USERPROFILE || '', 'Google Drive', 'WidgeTDC-Config'), path.join(process.env.OneDrive || '', 'WidgeTDC-Config'), path.join(process.env.HOME || process.env.USERPROFILE || '', '.widget-tdc') ]; for (const p of potentialPaths) { try { await fs.access(p); this.brainPath = path.join(p, 'brain.json'); break; } catch { continue; } } } public async getStatus(): Promise { let repoCount = 0; let lastScan = null; if (this.brainPath) { try { const data = await fs.readFile(this.brainPath, 'utf-8'); const brain = JSON.parse(data); if (brain.templates) { repoCount = Object.keys(brain.templates).length; // Find most recent scan time const dates = Object.values(brain.templates) .map((t: any) => t.last_scanned ? new Date(t.last_scanned).getTime() : 0); if (dates.length > 0) { lastScan = new Date(Math.max(...dates)).toISOString(); } } } catch (error) { logger.error('Failed to read brain.json', error); } } return { lastScan, repoCount, brainPath: this.brainPath || 'Not Found', isScanning: this.isScanning, lastError: null }; } public async runScan(): Promise { if (this.isScanning) throw new Error('Scan already in progress'); this.isScanning = true; eventBus.emit('devtools:scan:started', {}); try { const script = path.join(this.devToolsPath, 'widget-agent.ps1'); const command = `pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}" scan-github`; logger.info(`Running DevTools scan: ${command}`); const { stdout, stderr } = await execAsync(command); logger.info('DevTools scan completed', { stdout }); if (stderr) logger.warn('DevTools scan stderr', { stderr }); eventBus.emit('devtools:scan:completed', { output: stdout }); } catch (error) { logger.error('DevTools scan failed', error); eventBus.emit('devtools:scan:failed', { error: String(error) }); throw error; } finally { this.isScanning = false; } } public async validateRepo(repoPath: string): Promise { const script = path.join(this.devToolsPath, 'widget-agent.ps1'); // We need to run validate in the context of the target repo const command = `pwsh -NoProfile -ExecutionPolicy Bypass -File "${script}" validate`; try { const { stdout } = await execAsync(command, { cwd: repoPath }); return stdout; } catch (error: any) { // PowerShell script returns exit code 1 on validation failure, which throws here if (error.stdout) return error.stdout; // Return the validation output even if it failed throw error; } } } export const devToolsService = DevToolsService.getInstance();