UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

341 lines 11.8 kB
/** * FrameworkIsolator - Enforce framework-scoped isolation * * Ensures framework-specific resources (agents, commands, memory) are isolated * from each other while allowing shared resources (requirements, architecture) * to be accessible across all frameworks. * * FID-007 Framework-Scoped Workspaces isolation logic. * * @module src/plugin/framework-isolator * @version 1.0.0 * @since 2025-10-23 */ import * as fs from 'fs/promises'; import * as path from 'path'; // =========================== // FrameworkIsolator Class // =========================== export class FrameworkIsolator { projectRoot; FRAMEWORK_SPECIFIC = ['agents', 'commands', 'memory', 'context']; SHARED_RESOURCES = [ 'intake', 'requirements', 'architecture', 'testing', 'deployment', 'security', 'quality', 'risks', 'planning', 'reports', 'working', 'handoffs', 'gates', 'decisions', 'team', 'management' ]; constructor(projectRoot) { this.projectRoot = path.resolve(projectRoot); } /** * Get framework-specific path * * @param framework - Framework name * @param resource - Optional resource path * @returns Full path to framework resource */ getFrameworkPath(framework, resource) { if (framework === 'shared') { const basePath = path.join(this.projectRoot, '.aiwg', 'shared'); return resource ? path.join(basePath, resource) : basePath; } if (!['claude', 'codex', 'cursor'].includes(framework)) { throw new Error(`Unknown framework: ${framework}`); } const basePath = path.join(this.projectRoot, '.aiwg', framework); if (resource) { // Normalize resource path const normalizedResource = resource.startsWith('/') ? resource.slice(1) : resource; return path.join(basePath, normalizedResource); } return basePath; } /** * Check if resource is shared across frameworks * * @param resourcePath - Resource path * @returns True if resource is shared */ isSharedResource(resourcePath) { const firstDir = resourcePath.split('/')[0]; return this.SHARED_RESOURCES.includes(firstDir); } /** * Get framework-specific resources * * @param framework - Framework name * @param resourceType - Resource type (agents, commands, etc.) * @returns Array of resource paths */ async getFrameworkResources(framework, resourceType) { const frameworkPath = this.getFrameworkPath(framework, resourceType); try { const entries = await fs.readdir(frameworkPath); return entries; } catch { return []; } } /** * Get shared resources * * @param resourceType - Resource type * @param framework - Optional framework name (for access checking) * @returns Array of resource paths */ async getSharedResources(resourceType, _framework) { const sharedPath = path.join(this.projectRoot, '.aiwg', 'shared', resourceType); try { const entries = await fs.readdir(sharedPath); return entries; } catch { return []; } } /** * Get framework-specific config * * @param framework - Framework name * @returns Framework configuration */ async getFrameworkConfig(framework) { const configPath = this.getFrameworkPath(framework, 'settings.json'); try { const content = await fs.readFile(configPath, 'utf-8'); return JSON.parse(content); } catch { // Fallback for YAML config const yamlPath = this.getFrameworkPath(framework, 'config.yaml'); try { const content = await fs.readFile(yamlPath, 'utf-8'); // Simple YAML parsing (for basic cases) return this.parseSimpleYaml(content); } catch { return { framework }; } } } /** * Validate workspace isolation * * @returns Validation result */ async validateIsolation() { const errors = []; // Check for framework-specific resources in shared const sharedPath = path.join(this.projectRoot, '.aiwg', 'shared'); try { const entries = await fs.readdir(sharedPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && this.FRAMEWORK_SPECIFIC.includes(entry.name)) { errors.push({ type: 'contamination', path: path.join('shared', entry.name), message: `${entry.name} should not be in shared` }); } } // Check for settings files in shared const files = entries.filter(e => e.isFile()); for (const file of files) { if (file.name === 'settings.json' || file.name === 'config.yaml') { errors.push({ type: 'contamination', path: path.join('shared', file.name), message: `${file.name} should not be in shared` }); } } } catch { // Shared directory doesn't exist } return { valid: errors.length === 0, errors }; } /** * Check if framework can access resource * * @param framework - Framework name * @param resourcePath - Resource path * @returns True if access allowed */ async canAccess(framework, resourcePath) { // Shared resources are accessible to all if (resourcePath.startsWith('shared/')) { return true; } // Framework can access its own resources if (resourcePath.startsWith(`${framework}/`)) { return true; } return false; } /** * Check if framework can read resource * * @param framework - Framework name * @param resourcePath - Resource path * @returns True if read allowed */ async canRead(framework, resourcePath) { return this.canAccess(framework, resourcePath); } /** * Check if framework can write to resource * * @param framework - Framework name * @param resourcePath - Resource path * @returns True if write allowed */ async canWrite(framework, resourcePath) { // Cannot write to other framework's directories if (resourcePath.startsWith('claude/') || resourcePath.startsWith('codex/') || resourcePath.startsWith('cursor/')) { return resourcePath.startsWith(`${framework}/`); } // Can write to shared if authorized if (resourcePath.startsWith('shared/')) { return true; } return false; } /** * Identify framework-specific resources in legacy workspace * * @param workspacePath - Workspace path * @returns Array of framework-specific resources */ async identifyFrameworkSpecificResources(workspacePath) { const resources = []; for (const dir of this.FRAMEWORK_SPECIFIC) { const dirPath = path.join(workspacePath, dir); try { const files = await this.listFilesRecursive(dirPath); for (const file of files) { resources.push({ source: path.join(dir, file), type: 'framework-specific' }); } } catch { // Directory doesn't exist } } return resources; } /** * Identify shared resources in legacy workspace * * @param workspacePath - Workspace path * @returns Array of shared resources */ async identifySharedResources(workspacePath) { const resources = []; for (const dir of this.SHARED_RESOURCES) { const dirPath = path.join(workspacePath, dir); try { const files = await this.listFilesRecursive(dirPath); for (const file of files) { resources.push({ source: path.join(dir, file), target: path.join('shared', dir, file) }); } } catch { // Directory doesn't exist } } return resources; } /** * Categorize all resources * * @param workspacePath - Workspace path * @returns Categorized resources */ async categorizeResources(workspacePath) { const frameworkSpecificResources = await this.identifyFrameworkSpecificResources(workspacePath); const sharedResources = await this.identifySharedResources(workspacePath); return { frameworkSpecific: frameworkSpecificResources.map(r => r.source), shared: sharedResources.map(r => r.source) }; } /** * Suggest target frameworks for resources * * @param workspacePath - Workspace path * @returns Array of framework suggestions */ async suggestFrameworkTargets(workspacePath) { const suggestions = []; // Detect installed frameworks const { FrameworkDetector } = await import('./framework-detector.js'); const detector = new FrameworkDetector(this.projectRoot); const frameworks = await detector.detectFrameworks(); if (frameworks.length === 0) { return suggestions; } // Suggest first detected framework for all framework-specific resources const frameworkSpecific = await this.identifyFrameworkSpecificResources(workspacePath); const targetFramework = frameworks[0]; for (const resource of frameworkSpecific) { suggestions.push({ resource: resource.source, suggestedFramework: targetFramework, confidence: frameworks.length === 1 ? 1.0 : 0.7 }); } return suggestions; } // =========================== // Helper Methods // =========================== async listFilesRecursive(dirPath) { const files = []; const walk = async (currentPath, relativePath = '') => { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); const relPath = path.join(relativePath, entry.name); if (entry.isDirectory()) { await walk(fullPath, relPath); } else { files.push(relPath); } } }; await walk(dirPath); return files; } parseSimpleYaml(content) { const result = {}; const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const colonIndex = trimmed.indexOf(':'); if (colonIndex === -1) continue; const key = trimmed.slice(0, colonIndex).trim(); const value = trimmed.slice(colonIndex + 1).trim(); if (value) { result[key] = value; } } return result; } } //# sourceMappingURL=framework-isolator.js.map