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
JavaScript
/**
* 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