gsd-opencode 1.20.2 → 1.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/gsd/gsd-check-profile.md +30 -0
- package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
- package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
- package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
- package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
- package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
- package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
- package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
- package/get-shit-done/bin/gsd-oc-tools.cjs +130 -0
- package/get-shit-done/bin/lib/oc-config.cjs +200 -0
- package/get-shit-done/bin/lib/oc-core.cjs +114 -0
- package/get-shit-done/bin/lib/oc-models.cjs +133 -0
- package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
- package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
- package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
- package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
- package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
- package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
- package/get-shit-done/workflows/oc-check-profile.md +181 -0
- package/get-shit-done/workflows/oc-set-profile.md +83 -243
- package/get-shit-done/workflows/settings.md +4 -3
- package/package.json +2 -2
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* set-profile.cjs — Switch profile in oc_config.json with three operation modes
|
|
3
|
+
*
|
|
4
|
+
* Command module for managing OpenCode profiles using .planning/oc_config.json:
|
|
5
|
+
* 1. Mode 1 (no profile name): Validate and apply current profile
|
|
6
|
+
* 2. Mode 2 (profile name): Switch to specified profile
|
|
7
|
+
* 3. Mode 3 (inline JSON): Create new profile from definition
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Pre-flight validation BEFORE any file modifications
|
|
11
|
+
* - Atomic transaction with rollback on failure
|
|
12
|
+
* - Dry-run mode for previewing changes
|
|
13
|
+
* - Structured JSON output
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node set-profile.cjs # Mode 1: validate current
|
|
17
|
+
* node set-profile.cjs genius # Mode 2: switch to profile
|
|
18
|
+
* node set-profile.cjs 'custom:{...}' # Mode 3: create profile
|
|
19
|
+
* node set-profile.cjs --dry-run genius # Preview changes
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { output, error, createBackup } = require('../gsd-oc-lib/oc-core.cjs');
|
|
25
|
+
const { applyProfileWithValidation } = require('../gsd-oc-lib/oc-profile-config.cjs');
|
|
26
|
+
const { getModelCatalog } = require('../gsd-oc-lib/oc-models.cjs');
|
|
27
|
+
const { applyProfileToOpencode } = require('../gsd-oc-lib/oc-config.cjs');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error codes for set-profile operations
|
|
31
|
+
*/
|
|
32
|
+
const ERROR_CODES = {
|
|
33
|
+
CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND',
|
|
34
|
+
INVALID_JSON: 'INVALID_JSON',
|
|
35
|
+
INVALID_SYNTAX: 'INVALID_SYNTAX',
|
|
36
|
+
PROFILE_NOT_FOUND: 'PROFILE_NOT_FOUND',
|
|
37
|
+
PROFILE_EXISTS: 'PROFILE_EXISTS',
|
|
38
|
+
INVALID_MODELS: 'INVALID_MODELS',
|
|
39
|
+
INCOMPLETE_PROFILE: 'INCOMPLETE_PROFILE',
|
|
40
|
+
WRITE_FAILED: 'WRITE_FAILED',
|
|
41
|
+
APPLY_FAILED: 'APPLY_FAILED',
|
|
42
|
+
ROLLBACK_FAILED: 'ROLLBACK_FAILED',
|
|
43
|
+
MISSING_CURRENT_PROFILE: 'MISSING_CURRENT_PROFILE',
|
|
44
|
+
INVALID_ARGS: 'INVALID_ARGS'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse inline profile definition from argument
|
|
49
|
+
* Expected format: profileName:{"planning":"...", "execution":"...", "verification":"..."}
|
|
50
|
+
*
|
|
51
|
+
* @param {string} arg - Argument string
|
|
52
|
+
* @returns {Object|null} {name, profile} or null if invalid
|
|
53
|
+
*/
|
|
54
|
+
function parseInlineProfile(arg) {
|
|
55
|
+
const match = arg.match(/^([^:]+):(.+)$/);
|
|
56
|
+
if (!match) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const [, profileName, profileJson] = match;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const profile = JSON.parse(profileJson);
|
|
64
|
+
return { name: profileName, profile };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate inline profile definition has all required keys
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} profile - Profile object to validate
|
|
74
|
+
* @returns {Object} {valid: boolean, missingKeys: string[]}
|
|
75
|
+
*/
|
|
76
|
+
function validateInlineProfile(profile) {
|
|
77
|
+
const requiredKeys = ['planning', 'execution', 'verification'];
|
|
78
|
+
const missingKeys = requiredKeys.filter(key => !profile[key]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
valid: missingKeys.length === 0,
|
|
82
|
+
missingKeys
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate models against whitelist
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} profile - Profile with planning/execution/verification
|
|
90
|
+
* @param {string[]} validModels - Array of valid model IDs
|
|
91
|
+
* @returns {Object} {valid: boolean, invalidModels: string[]}
|
|
92
|
+
*/
|
|
93
|
+
function validateProfileModels(profile, validModels) {
|
|
94
|
+
const modelsToCheck = [profile.planning, profile.execution, profile.verification].filter(Boolean);
|
|
95
|
+
const invalidModels = modelsToCheck.filter(model => !validModels.includes(model));
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
valid: invalidModels.length === 0,
|
|
99
|
+
invalidModels
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Main command function
|
|
105
|
+
*
|
|
106
|
+
* @param {string} cwd - Current working directory
|
|
107
|
+
* @param {string[]} args - Command line arguments
|
|
108
|
+
*/
|
|
109
|
+
function setProfilePhase16(cwd, args) {
|
|
110
|
+
const verbose = args.includes('--verbose');
|
|
111
|
+
const dryRun = args.includes('--dry-run');
|
|
112
|
+
const raw = args.includes('--raw');
|
|
113
|
+
|
|
114
|
+
const log = verbose ? (...args) => console.error('[set-profile]', ...args) : () => {};
|
|
115
|
+
const configPath = path.join(cwd, '.planning', 'oc_config.json');
|
|
116
|
+
const opencodePath = path.join(cwd, 'opencode.json');
|
|
117
|
+
const backupsDir = path.join(cwd, '.planning', 'backups');
|
|
118
|
+
|
|
119
|
+
log('Starting set-profile command');
|
|
120
|
+
|
|
121
|
+
// Filter flags to get profile argument
|
|
122
|
+
const profileArgs = args.filter(arg => !arg.startsWith('--'));
|
|
123
|
+
|
|
124
|
+
// Check for too many arguments
|
|
125
|
+
if (profileArgs.length > 1) {
|
|
126
|
+
error('Too many arguments. Usage: set-profile [profile-name | profileName:JSON] [--dry-run]', 'INVALID_ARGS');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const profileArg = profileArgs.length > 0 ? profileArgs[0] : null;
|
|
130
|
+
|
|
131
|
+
// ========== MODE 3: Inline profile definition ==========
|
|
132
|
+
if (profileArg && profileArg.includes(':')) {
|
|
133
|
+
const parsed = parseInlineProfile(profileArg);
|
|
134
|
+
|
|
135
|
+
if (!parsed) {
|
|
136
|
+
error(
|
|
137
|
+
'Invalid profile syntax. Use: profileName:{"planning":"...", "execution":"...", "verification":"..."}',
|
|
138
|
+
'INVALID_SYNTAX'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { name: profileName, profile } = parsed;
|
|
143
|
+
log(`Mode 3: Creating inline profile "${profileName}"`);
|
|
144
|
+
|
|
145
|
+
// Validate complete profile definition
|
|
146
|
+
const validation = validateInlineProfile(profile);
|
|
147
|
+
if (!validation.valid) {
|
|
148
|
+
error(
|
|
149
|
+
`Profile definition missing required keys: ${validation.missingKeys.join(', ')}`,
|
|
150
|
+
'INCOMPLETE_PROFILE'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get model catalog for validation
|
|
155
|
+
const catalogResult = getModelCatalog();
|
|
156
|
+
if (!catalogResult.success) {
|
|
157
|
+
error(catalogResult.error.message, catalogResult.error.code);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate models against whitelist
|
|
161
|
+
const modelValidation = validateProfileModels(profile, catalogResult.models);
|
|
162
|
+
if (!modelValidation.valid) {
|
|
163
|
+
error(
|
|
164
|
+
`Invalid models: ${modelValidation.invalidModels.join(', ')}`,
|
|
165
|
+
'INVALID_MODELS'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
log('Inline profile validation passed');
|
|
170
|
+
|
|
171
|
+
// Dry-run mode
|
|
172
|
+
if (dryRun) {
|
|
173
|
+
output({
|
|
174
|
+
success: true,
|
|
175
|
+
data: {
|
|
176
|
+
dryRun: true,
|
|
177
|
+
action: 'create_profile',
|
|
178
|
+
profile: profileName,
|
|
179
|
+
models: profile
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Load or create oc_config.json
|
|
186
|
+
let config = {};
|
|
187
|
+
if (fs.existsSync(configPath)) {
|
|
188
|
+
try {
|
|
189
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error(`Failed to parse oc_config.json: ${err.message}`, 'INVALID_JSON');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Create backup
|
|
196
|
+
if (!fs.existsSync(backupsDir)) {
|
|
197
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const backupPath = createBackup(configPath, backupsDir);
|
|
201
|
+
|
|
202
|
+
// Initialize structure if needed
|
|
203
|
+
if (!config.profiles) config.profiles = {};
|
|
204
|
+
if (!config.profiles.presets) config.profiles.presets = {};
|
|
205
|
+
|
|
206
|
+
// Add profile and set as current
|
|
207
|
+
config.profiles.presets[profileName] = profile;
|
|
208
|
+
config.current_oc_profile = profileName;
|
|
209
|
+
|
|
210
|
+
// write oc_config.json
|
|
211
|
+
try {
|
|
212
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
213
|
+
log('Updated oc_config.json');
|
|
214
|
+
} catch (err) {
|
|
215
|
+
error(`Failed to write oc_config.json: ${err.message}`, 'WRITE_FAILED');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Apply to opencode.json
|
|
219
|
+
const applyResult = applyProfileToOpencode(opencodePath, configPath, profileName);
|
|
220
|
+
if (!applyResult.success) {
|
|
221
|
+
// Rollback
|
|
222
|
+
try {
|
|
223
|
+
if (backupPath) {
|
|
224
|
+
fs.copyFileSync(backupPath, configPath);
|
|
225
|
+
}
|
|
226
|
+
} catch (rollbackErr) {
|
|
227
|
+
error(
|
|
228
|
+
`Failed to apply profile AND failed to rollback: ${rollbackErr.message}`,
|
|
229
|
+
'ROLLBACK_FAILED'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
error(`Failed to apply profile to opencode.json: ${applyResult.error.message}`, 'APPLY_FAILED');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
output({
|
|
236
|
+
success: true,
|
|
237
|
+
data: {
|
|
238
|
+
profile: profileName,
|
|
239
|
+
models: profile,
|
|
240
|
+
backup: backupPath,
|
|
241
|
+
configPath
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ========== MODE 1 & 2: Use applyProfileWithValidation ==========
|
|
248
|
+
// Load oc_config.json first to determine mode
|
|
249
|
+
let config;
|
|
250
|
+
if (fs.existsSync(configPath)) {
|
|
251
|
+
try {
|
|
252
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
253
|
+
} catch (err) {
|
|
254
|
+
error(`Failed to parse oc_config.json: ${err.message}`, 'INVALID_JSON');
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
error('.planning/oc_config.json not found. Create it with an inline profile definition first.', 'CONFIG_NOT_FOUND');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const presets = config.profiles?.presets || {};
|
|
261
|
+
const currentProfile = config.current_oc_profile;
|
|
262
|
+
|
|
263
|
+
// ========== MODE 2: Profile name provided ==========
|
|
264
|
+
if (profileArg) {
|
|
265
|
+
log(`Mode 2: Switching to profile "${profileArg}"`);
|
|
266
|
+
|
|
267
|
+
// Check profile exists
|
|
268
|
+
if (!presets[profileArg]) {
|
|
269
|
+
const available = Object.keys(presets).join(', ') || 'none';
|
|
270
|
+
error(`Profile "${profileArg}" not found. Available profiles: ${available}`, 'PROFILE_NOT_FOUND');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Use applyProfileWithValidation for Mode 2
|
|
274
|
+
const result = applyProfileWithValidation(cwd, profileArg, { dryRun, verbose });
|
|
275
|
+
|
|
276
|
+
if (!result.success) {
|
|
277
|
+
error(result.error.message, result.error.code || 'UNKNOWN_ERROR');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (result.dryRun) {
|
|
281
|
+
output({
|
|
282
|
+
success: true,
|
|
283
|
+
data: {
|
|
284
|
+
dryRun: true,
|
|
285
|
+
action: 'switch_profile',
|
|
286
|
+
profile: profileArg,
|
|
287
|
+
models: result.preview.models,
|
|
288
|
+
changes: result.preview.changes
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
output({
|
|
293
|
+
success: true,
|
|
294
|
+
data: {
|
|
295
|
+
profile: profileArg,
|
|
296
|
+
models: result.data.models,
|
|
297
|
+
backup: result.data.backup,
|
|
298
|
+
updated: result.data.updated,
|
|
299
|
+
configPath: result.data.configPath
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ========== MODE 1: No profile name - validate current profile ==========
|
|
307
|
+
log('Mode 1: Validating current profile');
|
|
308
|
+
|
|
309
|
+
if (!currentProfile) {
|
|
310
|
+
const available = Object.keys(presets).join(', ') || 'none';
|
|
311
|
+
error(
|
|
312
|
+
`current_oc_profile not set. Available profiles: ${available}`,
|
|
313
|
+
'MISSING_CURRENT_PROFILE'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!presets[currentProfile]) {
|
|
318
|
+
error(
|
|
319
|
+
`Current profile "${currentProfile}" not found in profiles.presets`,
|
|
320
|
+
'PROFILE_NOT_FOUND'
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Use applyProfileWithValidation for Mode 1
|
|
325
|
+
const result = applyProfileWithValidation(cwd, currentProfile, { dryRun, verbose });
|
|
326
|
+
|
|
327
|
+
if (!result.success) {
|
|
328
|
+
error(result.error.message, result.error.code || 'UNKNOWN_ERROR');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (result.dryRun) {
|
|
332
|
+
output({
|
|
333
|
+
success: true,
|
|
334
|
+
data: {
|
|
335
|
+
dryRun: true,
|
|
336
|
+
action: 'validate_current',
|
|
337
|
+
profile: currentProfile,
|
|
338
|
+
models: result.preview.models,
|
|
339
|
+
changes: result.preview.changes
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
output({
|
|
344
|
+
success: true,
|
|
345
|
+
data: {
|
|
346
|
+
profile: currentProfile,
|
|
347
|
+
models: result.data.models,
|
|
348
|
+
backup: result.data.backup,
|
|
349
|
+
updated: result.data.updated,
|
|
350
|
+
configPath: result.data.configPath
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = setProfilePhase16;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-opencode-json.cjs — Update opencode.json agent models from profile config
|
|
3
|
+
*
|
|
4
|
+
* Command module that updates opencode.json model assignments based on oc_config.json structure.
|
|
5
|
+
* Creates timestamped backup before modifications.
|
|
6
|
+
* Outputs JSON envelope format with update results.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node update-opencode-json.cjs [cwd] [--dry-run] [--verbose]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { output, error, createBackup } = require('../gsd-oc-lib/oc-core.cjs');
|
|
14
|
+
const { applyProfileToOpencode } = require('../gsd-oc-lib/oc-config.cjs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main command function
|
|
18
|
+
*
|
|
19
|
+
* @param {string} cwd - Current working directory
|
|
20
|
+
* @param {string[]} args - Command line arguments
|
|
21
|
+
*/
|
|
22
|
+
function updateOpencodeJson(cwd, args) {
|
|
23
|
+
const verbose = args.includes('--verbose');
|
|
24
|
+
const dryRun = args.includes('--dry-run');
|
|
25
|
+
|
|
26
|
+
const opencodePath = path.join(cwd, 'opencode.json');
|
|
27
|
+
const configPath = path.join(cwd, '.planning', 'oc_config.json');
|
|
28
|
+
|
|
29
|
+
// Check if opencode.json exists
|
|
30
|
+
if (!fs.existsSync(opencodePath)) {
|
|
31
|
+
error('opencode.json not found in current directory', 'CONFIG_NOT_FOUND');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if .planning/oc_config.json exists
|
|
35
|
+
if (!fs.existsSync(configPath)) {
|
|
36
|
+
error('.planning/oc_config.json not found', 'CONFIG_NOT_FOUND');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.error(`[verbose] opencode.json: ${opencodePath}`);
|
|
41
|
+
console.error(`[verbose] oc_config.json: ${configPath}`);
|
|
42
|
+
console.error(`[verbose] dry-run: ${dryRun}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load and validate profile config
|
|
46
|
+
let config;
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
49
|
+
config = JSON.parse(content);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error('Failed to parse .planning/oc_config.json', 'INVALID_JSON');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate current_oc_profile
|
|
55
|
+
const profileName = config.current_oc_profile;
|
|
56
|
+
if (!profileName) {
|
|
57
|
+
error('current_oc_profile not found in oc_config.json', 'PROFILE_NOT_FOUND');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate profile exists in profiles.presets
|
|
61
|
+
const presets = config.profiles?.presets;
|
|
62
|
+
if (!presets || !presets[profileName]) {
|
|
63
|
+
const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
|
|
64
|
+
error(`Profile "${profileName}" not found in profiles.presets. Available profiles: ${availableProfiles}`, 'PROFILE_NOT_FOUND');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (verbose) {
|
|
68
|
+
console.error(`[verbose] Profile name: ${profileName}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Dry-run mode: preview changes without modifying
|
|
72
|
+
if (dryRun) {
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.error('[verbose] Dry-run mode - no changes will be made');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Simulate what would be updated
|
|
78
|
+
try {
|
|
79
|
+
const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
|
|
80
|
+
const opencodeData = JSON.parse(opencodeContent);
|
|
81
|
+
|
|
82
|
+
const profileModels = presets[profileName];
|
|
83
|
+
|
|
84
|
+
if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
|
|
85
|
+
error(`No model assignments found for profile "${profileName}"`, 'PROFILE_NOT_FOUND');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Determine which agents would be updated
|
|
89
|
+
const wouldUpdate = [];
|
|
90
|
+
|
|
91
|
+
const PROFILE_AGENT_MAPPING = {
|
|
92
|
+
planning: [
|
|
93
|
+
'gsd-planner', 'gsd-plan-checker', 'gsd-phase-researcher',
|
|
94
|
+
'gsd-roadmapper', 'gsd-project-researcher', 'gsd-research-synthesizer',
|
|
95
|
+
'gsd-codebase-mapper'
|
|
96
|
+
],
|
|
97
|
+
execution: ['gsd-executor', 'gsd-debugger'],
|
|
98
|
+
verification: ['gsd-verifier', 'gsd-integration-checker']
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
|
|
102
|
+
const modelId = profileModels[category];
|
|
103
|
+
if (modelId) {
|
|
104
|
+
for (const agentName of agentNames) {
|
|
105
|
+
const currentModel = typeof opencodeData.agent[agentName] === 'string'
|
|
106
|
+
? opencodeData.agent[agentName]
|
|
107
|
+
: opencodeData.agent[agentName]?.model;
|
|
108
|
+
|
|
109
|
+
if (currentModel !== modelId) {
|
|
110
|
+
wouldUpdate.push({
|
|
111
|
+
agent: agentName,
|
|
112
|
+
from: currentModel || '(not set)',
|
|
113
|
+
to: modelId,
|
|
114
|
+
modelId: modelId
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = {
|
|
122
|
+
success: true,
|
|
123
|
+
data: {
|
|
124
|
+
backup: null,
|
|
125
|
+
updated: wouldUpdate.map(u => u.agent),
|
|
126
|
+
dryRun: true,
|
|
127
|
+
changes: wouldUpdate
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (verbose) {
|
|
132
|
+
console.error(`[verbose] Would update ${wouldUpdate.length} agent(s)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
output(result);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
error(`Failed to preview changes: ${err.message}`, 'PREVIEW_FAILED');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Actual update mode
|
|
143
|
+
if (verbose) {
|
|
144
|
+
console.error('[verbose] Creating backup...');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create timestamped backup
|
|
148
|
+
const backupPath = createBackup(opencodePath);
|
|
149
|
+
if (!backupPath) {
|
|
150
|
+
error('Failed to create backup of opencode.json', 'BACKUP_FAILED');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (verbose) {
|
|
154
|
+
console.error(`[verbose] Backup created: ${backupPath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply profile to opencode.json using the existing function which already supports oc_config.json
|
|
158
|
+
if (verbose) {
|
|
159
|
+
console.error('[verbose] Applying profile to opencode.json...');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const result = applyProfileToOpencode(opencodePath, configPath, profileName);
|
|
163
|
+
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
// Restore backup on failure
|
|
166
|
+
if (verbose) {
|
|
167
|
+
console.error('[verbose] Update failed, restoring backup...');
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
fs.copyFileSync(backupPath, opencodePath);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// Best effort restore
|
|
173
|
+
}
|
|
174
|
+
error(result.error.message, result.error.code);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (verbose) {
|
|
178
|
+
console.error(`[verbose] Updated ${result.updated.length} agent(s)`);
|
|
179
|
+
for (const { agent } of result.updated) {
|
|
180
|
+
console.error(`[verbose] - ${agent}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const outputResult = {
|
|
185
|
+
success: true,
|
|
186
|
+
data: {
|
|
187
|
+
backup: backupPath,
|
|
188
|
+
updated: result.updated.map(u => u.agent),
|
|
189
|
+
dryRun: false,
|
|
190
|
+
details: result.updated
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
output(outputResult);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Export for use by main router
|
|
199
|
+
module.exports = updateOpencodeJson;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-models.cjs — Validate model IDs against opencode models catalog
|
|
3
|
+
*
|
|
4
|
+
* Command module that validates one or more model IDs exist in the opencode catalog.
|
|
5
|
+
* Outputs JSON envelope format with validation results.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node validate-models.cjs <model1> [model2...] [--raw]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { output, error } = require('../gsd-oc-lib/oc-core.cjs');
|
|
11
|
+
const { getModelCatalog } = require('../gsd-oc-lib/oc-models.cjs');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main command function
|
|
15
|
+
*
|
|
16
|
+
* @param {string} cwd - Current working directory
|
|
17
|
+
* @param {string[]} args - Command line arguments (model IDs)
|
|
18
|
+
*/
|
|
19
|
+
function validateModels(cwd, args) {
|
|
20
|
+
const raw = args.includes('--raw');
|
|
21
|
+
const modelIds = args.filter(arg => !arg.startsWith('--'));
|
|
22
|
+
|
|
23
|
+
if (modelIds.length === 0) {
|
|
24
|
+
error('No model IDs provided. Usage: validate-models <model1> [model2...]', 'INVALID_USAGE');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fetch model catalog
|
|
28
|
+
const catalogResult = getModelCatalog();
|
|
29
|
+
if (!catalogResult.success) {
|
|
30
|
+
error(catalogResult.error.message, catalogResult.error.code);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const validModels = catalogResult.models;
|
|
34
|
+
const results = [];
|
|
35
|
+
|
|
36
|
+
for (const modelId of modelIds) {
|
|
37
|
+
const isValid = validModels.includes(modelId);
|
|
38
|
+
results.push({
|
|
39
|
+
model: modelId,
|
|
40
|
+
valid: isValid,
|
|
41
|
+
reason: isValid ? 'Model found in catalog' : 'Model not found in catalog'
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const allValid = results.every(r => r.valid);
|
|
46
|
+
const validCount = results.filter(r => r.valid).length;
|
|
47
|
+
const invalidCount = results.filter(r => !r.valid).length;
|
|
48
|
+
|
|
49
|
+
const result = {
|
|
50
|
+
success: allValid,
|
|
51
|
+
data: {
|
|
52
|
+
total: modelIds.length,
|
|
53
|
+
valid: validCount,
|
|
54
|
+
invalid: invalidCount,
|
|
55
|
+
models: results
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (!allValid) {
|
|
60
|
+
result.error = {
|
|
61
|
+
code: 'INVALID_MODELS',
|
|
62
|
+
message: `${invalidCount} model(s) not found in catalog`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (raw) {
|
|
67
|
+
output(result, true, allValid ? 'valid' : 'invalid');
|
|
68
|
+
} else {
|
|
69
|
+
output(result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.exit(allValid ? 0 : 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = validateModels;
|