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
235 lines • 8.31 kB
JavaScript
/**
* FrameworkConfigLoader - Load and merge framework configurations
*
* Handles loading framework-specific configs, merging with shared configs,
* and managing configuration precedence. Supports JSON and YAML formats.
*
* FID-007 Framework-Scoped Workspaces configuration management.
*
* @module src/plugin/framework-config-loader
* @version 1.0.0
* @since 2025-10-23
*/
import * as fs from 'fs/promises';
import * as path from 'path';
// ===========================
// FrameworkConfigLoader Class
// ===========================
export class FrameworkConfigLoader {
projectRoot;
configCache = new Map();
constructor(projectRoot) {
this.projectRoot = path.resolve(projectRoot);
}
/**
* Load framework configuration
*
* @param framework - Framework name
* @param options - Load options
* @returns Merged configuration
*/
async loadConfig(framework, options = {}) {
const cacheKey = `${framework}-${options.environment || 'default'}`;
// Check cache
if (this.configCache.has(cacheKey)) {
return this.configCache.get(cacheKey);
}
// Load shared config
const sharedConfig = await this.loadSharedConfig();
// Load framework-specific config
const frameworkConfig = await this.loadFrameworkConfig(framework);
// Load environment-specific config
let envConfig = {};
if (options.environment) {
envConfig = await this.loadEnvironmentConfig(framework, options.environment);
}
// Load local overrides
let localConfig = {};
if (options.includeLocal) {
localConfig = await this.loadLocalConfig(framework);
}
// Merge configs (later configs override earlier)
let merged = this.deepMerge(sharedConfig, frameworkConfig, envConfig, localConfig);
// Apply merge strategy if requested
if (options.respectMergeStrategy && sharedConfig.mergeStrategy) {
merged = this.applyMergeStrategy(merged, sharedConfig.mergeStrategy);
}
// Cache result
this.configCache.set(cacheKey, merged);
return merged;
}
/**
* Get specific config value
*
* @param framework - Framework name
* @param key - Config key (supports dot notation)
* @param defaultValue - Default value if key not found
* @returns Config value
*/
getConfigValue(framework, key, defaultValue) {
const config = this.configCache.get(`${framework}-default`);
if (!config) {
return defaultValue;
}
const keys = key.split('.');
let value = config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
}
else {
return defaultValue;
}
}
return value !== undefined ? value : defaultValue;
}
/**
* Save framework configuration
*
* @param framework - Framework name
* @param config - Configuration to save
*/
async saveConfig(framework, config) {
const configPath = path.join(this.projectRoot, '.aiwg', framework, 'settings.json');
// Ensure directory exists
await fs.mkdir(path.dirname(configPath), { recursive: true });
// Write config
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
// Invalidate cache
this.configCache.delete(`${framework}-default`);
}
// ===========================
// Private Methods
// ===========================
async loadSharedConfig() {
const workspacePath = path.join(this.projectRoot, '.aiwg', 'workspace.json');
try {
const content = await fs.readFile(workspacePath, 'utf-8');
const workspace = JSON.parse(content);
return workspace.shared || {};
}
catch {
return {};
}
}
async loadFrameworkConfig(framework) {
// Try JSON first
const jsonPath = path.join(this.projectRoot, '.aiwg', framework, 'settings.json');
try {
const content = await fs.readFile(jsonPath, 'utf-8');
return JSON.parse(content);
}
catch {
// Try YAML
const yamlPath = path.join(this.projectRoot, '.aiwg', framework, 'config.yaml');
try {
const content = await fs.readFile(yamlPath, 'utf-8');
return this.parseSimpleYaml(content);
}
catch {
// Return default config
return { framework };
}
}
}
async loadEnvironmentConfig(framework, environment) {
const envPath = path.join(this.projectRoot, '.aiwg', framework, `settings.${environment}.json`);
try {
const content = await fs.readFile(envPath, 'utf-8');
return JSON.parse(content);
}
catch {
return {};
}
}
async loadLocalConfig(framework) {
const localPath = path.join(this.projectRoot, '.aiwg', framework, 'settings.local.json');
try {
const content = await fs.readFile(localPath, 'utf-8');
return JSON.parse(content);
}
catch {
return {};
}
}
deepMerge(...objects) {
const result = {};
for (const obj of objects) {
if (!obj)
continue;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Merge objects recursively
result[key] = this.deepMerge(result[key] || {}, value);
}
else {
// Override with new value
result[key] = value;
}
}
}
}
return result;
}
applyMergeStrategy(config, strategy) {
// Apply merge strategies (e.g., append arrays instead of override)
const result = { ...config };
for (const key in strategy) {
if (strategy[key] === 'append' && Array.isArray(config[key])) {
// Append strategy already handled in initial merge
// This is a placeholder for more complex strategies
}
}
return result;
}
parseSimpleYaml(content) {
const result = {};
const lines = content.split('\n');
let currentKey = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#'))
continue;
// Check for array items
if (trimmed.startsWith('- ')) {
const value = trimmed.slice(2).trim();
if (currentKey) {
if (!Array.isArray(result[currentKey])) {
result[currentKey] = [];
}
result[currentKey].push(value);
}
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) {
// Simple key-value - handle booleans and numbers
if (value === 'true') {
result[key] = true;
}
else if (value === 'false') {
result[key] = false;
}
else if (!isNaN(Number(value))) {
result[key] = Number(value);
}
else {
result[key] = value;
}
currentKey = null;
}
else {
// Key with no value - next lines will be array items or nested object
currentKey = key;
}
}
return result;
}
}
//# sourceMappingURL=framework-config-loader.js.map