206 lines
5.8 KiB
JavaScript
206 lines
5.8 KiB
JavaScript
/**
|
|
* oc-config.cjs — Profile configuration operations for gsd-oc-tools CLI
|
|
*
|
|
* Provides functions for loading profile config and applying model assignments to opencode.json.
|
|
* Follows gsd-tools.cjs architecture pattern.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Valid profile types whitelist
|
|
*/
|
|
const VALID_PROFILES = ['simple', 'smart', 'genius'];
|
|
|
|
/**
|
|
* Profile to agent mapping
|
|
* Maps profile keys to opencode.json agent names
|
|
*/
|
|
const PROFILE_AGENT_MAPPING = {
|
|
// Planning agents
|
|
planning: [
|
|
'gsd-planner',
|
|
'gsd-plan-checker',
|
|
'gsd-phase-researcher',
|
|
'gsd-roadmapper',
|
|
'gsd-project-researcher',
|
|
'gsd-research-synthesizer',
|
|
'gsd-codebase-mapper'
|
|
],
|
|
// Execution agents
|
|
execution: [
|
|
'gsd-executor',
|
|
'gsd-debugger'
|
|
],
|
|
// Verification agents
|
|
verification: [
|
|
'gsd-verifier',
|
|
'gsd-integration-checker'
|
|
]
|
|
};
|
|
|
|
/**
|
|
* Load profile configuration from .planning/config.json
|
|
*
|
|
* @param {string} cwd - Current working directory
|
|
* @returns {Object|null} Parsed config object or null on error
|
|
*/
|
|
function loadProfileConfig(cwd) {
|
|
try {
|
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
|
|
if (!fs.existsSync(configPath)) {
|
|
return null;
|
|
}
|
|
|
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
let config = JSON.parse(content);
|
|
|
|
// Auto-migrate old key name: current_os_profile → current_oc_profile
|
|
if (config.current_os_profile && !config.current_oc_profile) {
|
|
config.current_oc_profile = config.current_os_profile;
|
|
delete config.current_os_profile;
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
}
|
|
|
|
return config;
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply profile configuration to opencode.json
|
|
* Updates agent model assignments based on profile
|
|
*
|
|
* @param {string} opencodePath - Path to opencode.json
|
|
* @param {string} configPath - Path to .planning/config.json
|
|
* @param {string} [profileName] - Optional profile name to use (overrides current_oc_profile)
|
|
* @returns {Object} {success: true, updated: [agentNames]} or {success: false, error: {code, message}}
|
|
*/
|
|
function applyProfileToOpencode(opencodePath, configPath, profileName = null) {
|
|
try {
|
|
// Load profile config
|
|
let config;
|
|
if (fs.existsSync(configPath)) {
|
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
config = JSON.parse(content);
|
|
} else {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: 'CONFIG_NOT_FOUND',
|
|
message: `.planning/config.json not found at ${configPath}`
|
|
}
|
|
};
|
|
}
|
|
|
|
// Determine which profile to use
|
|
const targetProfile = profileName || config.current_oc_profile;
|
|
|
|
if (!targetProfile) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: 'PROFILE_NOT_FOUND',
|
|
message: 'current_oc_profile not found in config.json. Run set-profile with a profile name first.'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Validate profile exists in profiles.presets
|
|
const presets = config.profiles?.presets;
|
|
if (!presets || !presets[targetProfile]) {
|
|
const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: 'PROFILE_NOT_FOUND',
|
|
message: `Profile "${targetProfile}" not found in profiles.presets. Available profiles: ${availableProfiles}`
|
|
}
|
|
};
|
|
}
|
|
|
|
// Load or create opencode.json
|
|
let opencodeData;
|
|
if (!fs.existsSync(opencodePath)) {
|
|
// Create initial opencode.json structure
|
|
opencodeData = {
|
|
"$schema": "https://opencode.ai/config.json",
|
|
"agent": {}
|
|
};
|
|
} else {
|
|
// Load existing opencode.json
|
|
const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
|
|
opencodeData = JSON.parse(opencodeContent);
|
|
|
|
// Ensure agent object exists
|
|
if (!opencodeData.agent) {
|
|
opencodeData.agent = {};
|
|
}
|
|
}
|
|
|
|
// Get model assignments from profiles.presets.{profile_name}.models
|
|
const profileModels = presets[targetProfile];
|
|
|
|
if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: 'PROFILE_NOT_FOUND',
|
|
message: `No model assignments found for profile "${targetProfile}"`
|
|
}
|
|
};
|
|
}
|
|
|
|
// Apply model assignments to agents (MERGE - preserve non-gsd agents)
|
|
const updatedAgents = [];
|
|
|
|
// Initialize agent object if it doesn't exist
|
|
if (!opencodeData.agent) {
|
|
opencodeData.agent = {};
|
|
}
|
|
|
|
// Apply each profile category - ONLY update gsd-* agents
|
|
for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
|
|
const modelId = profileModels[category];
|
|
|
|
if (modelId) {
|
|
for (const agentName of agentNames) {
|
|
// Only update gsd-* agents, preserve all others
|
|
if (typeof opencodeData.agent[agentName] === 'object' && opencodeData.agent[agentName] !== null) {
|
|
opencodeData.agent[agentName].model = modelId;
|
|
} else {
|
|
opencodeData.agent[agentName] = { model: modelId };
|
|
}
|
|
updatedAgents.push({ agent: agentName, model: modelId });
|
|
}
|
|
}
|
|
}
|
|
|
|
// write updated opencode.json
|
|
fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
updated: updatedAgents
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: 'UPDATE_FAILED',
|
|
message: `Failed to apply profile: ${err.message}`
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
loadProfileConfig,
|
|
applyProfileToOpencode,
|
|
VALID_PROFILES,
|
|
PROFILE_AGENT_MAPPING
|
|
};
|