200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
/**
|
|
* update-opencode-json.cjs — Update opencode.json agent models from profile config
|
|
*
|
|
* Command module that updates opencode.json model assignments based on oc_config.json structure.
|
|
* Creates timestamped backup before modifications.
|
|
* Outputs JSON envelope format with update results.
|
|
*
|
|
* Usage: node update-opencode-json.cjs [cwd] [--dry-run] [--verbose]
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { output, error, createBackup } = require('../gsd-oc-lib/oc-core.cjs');
|
|
const { applyProfileToOpencode } = require('../gsd-oc-lib/oc-config.cjs');
|
|
|
|
/**
|
|
* Main command function
|
|
*
|
|
* @param {string} cwd - Current working directory
|
|
* @param {string[]} args - Command line arguments
|
|
*/
|
|
function updateOpencodeJson(cwd, args) {
|
|
const verbose = args.includes('--verbose');
|
|
const dryRun = args.includes('--dry-run');
|
|
|
|
const opencodePath = path.join(cwd, 'opencode.json');
|
|
const configPath = path.join(cwd, '.planning', 'oc_config.json');
|
|
|
|
// Check if opencode.json exists
|
|
if (!fs.existsSync(opencodePath)) {
|
|
error('opencode.json not found in current directory', 'CONFIG_NOT_FOUND');
|
|
}
|
|
|
|
// Check if .planning/oc_config.json exists
|
|
if (!fs.existsSync(configPath)) {
|
|
error('.planning/oc_config.json not found', 'CONFIG_NOT_FOUND');
|
|
}
|
|
|
|
if (verbose) {
|
|
console.error(`[verbose] opencode.json: ${opencodePath}`);
|
|
console.error(`[verbose] oc_config.json: ${configPath}`);
|
|
console.error(`[verbose] dry-run: ${dryRun}`);
|
|
}
|
|
|
|
// Load and validate profile config
|
|
let config;
|
|
try {
|
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
config = JSON.parse(content);
|
|
} catch (err) {
|
|
error('Failed to parse .planning/oc_config.json', 'INVALID_JSON');
|
|
}
|
|
|
|
// Validate current_oc_profile
|
|
const profileName = config.current_oc_profile;
|
|
if (!profileName) {
|
|
error('current_oc_profile not found in oc_config.json', 'PROFILE_NOT_FOUND');
|
|
}
|
|
|
|
// Validate profile exists in profiles.presets
|
|
const presets = config.profiles?.presets;
|
|
if (!presets || !presets[profileName]) {
|
|
const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
|
|
error(`Profile "${profileName}" not found in profiles.presets. Available profiles: ${availableProfiles}`, 'PROFILE_NOT_FOUND');
|
|
}
|
|
|
|
if (verbose) {
|
|
console.error(`[verbose] Profile name: ${profileName}`);
|
|
}
|
|
|
|
// Dry-run mode: preview changes without modifying
|
|
if (dryRun) {
|
|
if (verbose) {
|
|
console.error('[verbose] Dry-run mode - no changes will be made');
|
|
}
|
|
|
|
// Simulate what would be updated
|
|
try {
|
|
const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
|
|
const opencodeData = JSON.parse(opencodeContent);
|
|
|
|
const profileModels = presets[profileName];
|
|
|
|
if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
|
|
error(`No model assignments found for profile "${profileName}"`, 'PROFILE_NOT_FOUND');
|
|
}
|
|
|
|
// Determine which agents would be updated
|
|
const wouldUpdate = [];
|
|
|
|
const PROFILE_AGENT_MAPPING = {
|
|
planning: [
|
|
'gsd-planner', 'gsd-plan-checker', 'gsd-phase-researcher',
|
|
'gsd-roadmapper', 'gsd-project-researcher', 'gsd-research-synthesizer',
|
|
'gsd-codebase-mapper'
|
|
],
|
|
execution: ['gsd-executor', 'gsd-debugger'],
|
|
verification: ['gsd-verifier', 'gsd-integration-checker']
|
|
};
|
|
|
|
for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
|
|
const modelId = profileModels[category];
|
|
if (modelId) {
|
|
for (const agentName of agentNames) {
|
|
const currentModel = typeof opencodeData.agent[agentName] === 'string'
|
|
? opencodeData.agent[agentName]
|
|
: opencodeData.agent[agentName]?.model;
|
|
|
|
if (currentModel !== modelId) {
|
|
wouldUpdate.push({
|
|
agent: agentName,
|
|
from: currentModel || '(not set)',
|
|
to: modelId,
|
|
modelId: modelId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
success: true,
|
|
data: {
|
|
backup: null,
|
|
updated: wouldUpdate.map(u => u.agent),
|
|
dryRun: true,
|
|
changes: wouldUpdate
|
|
}
|
|
};
|
|
|
|
if (verbose) {
|
|
console.error(`[verbose] Would update ${wouldUpdate.length} agent(s)`);
|
|
}
|
|
|
|
output(result);
|
|
process.exit(0);
|
|
} catch (err) {
|
|
error(`Failed to preview changes: ${err.message}`, 'PREVIEW_FAILED');
|
|
}
|
|
}
|
|
|
|
// Actual update mode
|
|
if (verbose) {
|
|
console.error('[verbose] Creating backup...');
|
|
}
|
|
|
|
// Create timestamped backup
|
|
const backupPath = createBackup(opencodePath);
|
|
if (!backupPath) {
|
|
error('Failed to create backup of opencode.json', 'BACKUP_FAILED');
|
|
}
|
|
|
|
if (verbose) {
|
|
console.error(`[verbose] Backup created: ${backupPath}`);
|
|
}
|
|
|
|
// Apply profile to opencode.json using the existing function which already supports oc_config.json
|
|
if (verbose) {
|
|
console.error('[verbose] Applying profile to opencode.json...');
|
|
}
|
|
|
|
const result = applyProfileToOpencode(opencodePath, configPath, profileName);
|
|
|
|
if (!result.success) {
|
|
// Restore backup on failure
|
|
if (verbose) {
|
|
console.error('[verbose] Update failed, restoring backup...');
|
|
}
|
|
try {
|
|
fs.copyFileSync(backupPath, opencodePath);
|
|
} catch (err) {
|
|
// Best effort restore
|
|
}
|
|
error(result.error.message, result.error.code);
|
|
}
|
|
|
|
if (verbose) {
|
|
console.error(`[verbose] Updated ${result.updated.length} agent(s)`);
|
|
for (const { agent } of result.updated) {
|
|
console.error(`[verbose] - ${agent}`);
|
|
}
|
|
}
|
|
|
|
const outputResult = {
|
|
success: true,
|
|
data: {
|
|
backup: backupPath,
|
|
updated: result.updated.map(u => u.agent),
|
|
dryRun: false,
|
|
details: result.updated
|
|
}
|
|
};
|
|
|
|
output(outputResult);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Export for use by main router
|
|
module.exports = updateOpencodeJson;
|