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.
Files changed (25) hide show
  1. package/commands/gsd/gsd-check-profile.md +30 -0
  2. package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
  3. package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
  4. package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
  5. package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
  6. package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
  7. package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
  8. package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
  9. package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
  10. package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
  11. package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
  12. package/get-shit-done/bin/gsd-oc-tools.cjs +130 -0
  13. package/get-shit-done/bin/lib/oc-config.cjs +200 -0
  14. package/get-shit-done/bin/lib/oc-core.cjs +114 -0
  15. package/get-shit-done/bin/lib/oc-models.cjs +133 -0
  16. package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
  17. package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
  18. package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
  19. package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
  20. package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
  21. package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
  22. package/get-shit-done/workflows/oc-check-profile.md +181 -0
  23. package/get-shit-done/workflows/oc-set-profile.md +83 -243
  24. package/get-shit-done/workflows/settings.md +4 -3
  25. package/package.json +2 -2
@@ -0,0 +1,409 @@
1
+ /**
2
+ * oc-profile-config.cjs — Profile configuration operations for oc_config.json
3
+ *
4
+ * Provides functions for loading, validating, and applying profiles from .planning/oc_config.json.
5
+ * Uses separate oc_config.json file (NOT config.json from Phase 15).
6
+ * Follows validate-then-modify pattern with atomic transactions.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, error: outputError, createBackup } = require('./oc-core.cjs');
12
+ const { getModelCatalog } = require('./oc-models.cjs');
13
+ const { applyProfileToOpencode } = require('./oc-config.cjs');
14
+
15
+ /**
16
+ * Error codes for oc_config.json operations
17
+ */
18
+ const ERROR_CODES = {
19
+ CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND',
20
+ INVALID_JSON: 'INVALID_JSON',
21
+ PROFILE_NOT_FOUND: 'PROFILE_NOT_FOUND',
22
+ INVALID_MODELS: 'INVALID_MODELS',
23
+ INCOMPLETE_PROFILE: 'INCOMPLETE_PROFILE',
24
+ WRITE_FAILED: 'WRITE_FAILED',
25
+ APPLY_FAILED: 'APPLY_FAILED',
26
+ ROLLBACK_FAILED: 'ROLLBACK_FAILED'
27
+ };
28
+
29
+ /**
30
+ * Load oc_config.json from .planning directory
31
+ *
32
+ * @param {string} cwd - Current working directory
33
+ * @returns {Object} {success: true, config, configPath} or {success: false, error: {code, message}}
34
+ */
35
+ function loadOcProfileConfig(cwd) {
36
+ try {
37
+ const configPath = path.join(cwd, '.planning', 'oc_config.json');
38
+
39
+ if (!fs.existsSync(configPath)) {
40
+ return {
41
+ success: false,
42
+ error: {
43
+ code: ERROR_CODES.CONFIG_NOT_FOUND,
44
+ message: `.planning/oc_config.json not found at ${configPath}`
45
+ }
46
+ };
47
+ }
48
+
49
+ const content = fs.readFileSync(configPath, 'utf8');
50
+ const config = JSON.parse(content);
51
+
52
+ return {
53
+ success: true,
54
+ config,
55
+ configPath
56
+ };
57
+ } catch (err) {
58
+ if (err instanceof SyntaxError) {
59
+ return {
60
+ success: false,
61
+ error: {
62
+ code: ERROR_CODES.INVALID_JSON,
63
+ message: `Invalid JSON in oc_config.json: ${err.message}`
64
+ }
65
+ };
66
+ }
67
+ return {
68
+ success: false,
69
+ error: {
70
+ code: ERROR_CODES.CONFIG_NOT_FOUND,
71
+ message: `Failed to read oc_config.json: ${err.message}`
72
+ }
73
+ };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Validate a profile definition against model whitelist and completeness requirements
79
+ *
80
+ * @param {Object} config - oc_config.json config object
81
+ * @param {string} profileName - Name of profile to validate
82
+ * @param {string[]} validModels - Array of valid model IDs (from getModelCatalog)
83
+ * @returns {Object} {valid: boolean, errors: [{code, message, field}]}
84
+ */
85
+ function validateProfile(config, profileName, validModels) {
86
+ const errors = [];
87
+
88
+ // Check if profile exists in presets
89
+ const presets = config.profiles?.presets;
90
+ if (!presets || !presets[profileName]) {
91
+ errors.push({
92
+ code: ERROR_CODES.PROFILE_NOT_FOUND,
93
+ message: `Profile "${profileName}" not found in profiles.presets`,
94
+ field: 'profiles.presets'
95
+ });
96
+ return { valid: false, errors };
97
+ }
98
+
99
+ const profile = presets[profileName];
100
+
101
+ // Check for complete profile definition (all three keys required)
102
+ const requiredKeys = ['planning', 'execution', 'verification'];
103
+ const missingKeys = requiredKeys.filter(key => !profile[key]);
104
+
105
+ if (missingKeys.length > 0) {
106
+ errors.push({
107
+ code: ERROR_CODES.INCOMPLETE_PROFILE,
108
+ message: `Profile "${profileName}" is missing required keys: ${missingKeys.join(', ')}`,
109
+ field: 'profiles.presets.' + profileName,
110
+ missingKeys
111
+ });
112
+ // Return early - can't validate models if profile is incomplete
113
+ return { valid: false, errors };
114
+ }
115
+
116
+ // Validate all models against whitelist
117
+ const invalidModels = [];
118
+ for (const key of requiredKeys) {
119
+ const modelId = profile[key];
120
+ if (!validModels.includes(modelId)) {
121
+ invalidModels.push({
122
+ key,
123
+ model: modelId,
124
+ reason: 'Model ID not found in opencode models catalog'
125
+ });
126
+ }
127
+ }
128
+
129
+ if (invalidModels.length > 0) {
130
+ errors.push({
131
+ code: ERROR_CODES.INVALID_MODELS,
132
+ message: `Profile "${profileName}" contains ${invalidModels.length} invalid model ID(s)`,
133
+ field: 'profiles.presets.' + profileName,
134
+ invalidModels
135
+ });
136
+ }
137
+
138
+ return {
139
+ valid: errors.length === 0,
140
+ errors
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Apply profile with full validation, backup, and atomic transaction
146
+ *
147
+ * @param {string} cwd - Current working directory
148
+ * @param {string} profileName - Name of profile to apply
149
+ * @param {Object} options - Options object
150
+ * @param {boolean} options.dryRun - If true, preview changes without modifications
151
+ * @param {boolean} options.verbose - If true, output progress to console.error
152
+ * @param {Object} options.inlineProfile - Optional inline profile definition to create/update
153
+ * @returns {Object} {success: true, data: {profile, models, backup, updated}} or {success: false, error}
154
+ */
155
+ function applyProfileWithValidation(cwd, profileName, options = {}) {
156
+ const { dryRun = false, verbose = false, inlineProfile = null } = options;
157
+ const log = verbose ? (...args) => console.error('[oc-profile-config]', ...args) : () => {};
158
+
159
+ // Step 1: Load oc_config.json
160
+ const loadResult = loadOcProfileConfig(cwd);
161
+ if (!loadResult.success) {
162
+ return { success: false, error: loadResult.error };
163
+ }
164
+
165
+ const { config, configPath } = loadResult;
166
+ let targetProfileName = profileName;
167
+ let profileToUpdate;
168
+
169
+ // Step 2: Handle inline profile definition (Mode 3)
170
+ if (inlineProfile) {
171
+ log('Processing inline profile definition');
172
+
173
+ // Check if profile already exists
174
+ const presets = config.profiles?.presets || {};
175
+ if (presets[profileName] && !dryRun) {
176
+ return {
177
+ success: false,
178
+ error: {
179
+ code: 'PROFILE_EXISTS',
180
+ message: `Profile "${profileName}" already exists. Use a different name or remove --inline flag.`
181
+ }
182
+ };
183
+ }
184
+
185
+ // Validate inline profile has all required keys
186
+ const requiredKeys = ['planning', 'execution', 'verification'];
187
+ const missingKeys = requiredKeys.filter(key => !inlineProfile[key]);
188
+
189
+ if (missingKeys.length > 0) {
190
+ return {
191
+ success: false,
192
+ error: {
193
+ code: ERROR_CODES.INCOMPLETE_PROFILE,
194
+ message: `Inline profile is missing required keys: ${missingKeys.join(', ')}`,
195
+ missingKeys
196
+ }
197
+ };
198
+ }
199
+
200
+ profileToUpdate = inlineProfile;
201
+ } else {
202
+ // Step 2: Use existing profile from config
203
+ const presets = config.profiles?.presets;
204
+ if (!presets || !presets[profileName]) {
205
+ const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
206
+ return {
207
+ success: false,
208
+ error: {
209
+ code: ERROR_CODES.PROFILE_NOT_FOUND,
210
+ message: `Profile "${profileName}" not found in profiles.presets. Available profiles: ${availableProfiles}`
211
+ }
212
+ };
213
+ }
214
+ profileToUpdate = presets[profileName];
215
+ }
216
+
217
+ // Step 3: Get model catalog for validation
218
+ const catalogResult = getModelCatalog();
219
+ if (!catalogResult.success) {
220
+ return { success: false, error: catalogResult.error };
221
+ }
222
+ const validModels = catalogResult.models;
223
+
224
+ // Step 4: Validate profile models
225
+ const validation = validateProfile(
226
+ { profiles: { presets: { [targetProfileName]: profileToUpdate } } },
227
+ targetProfileName,
228
+ validModels
229
+ );
230
+
231
+ if (!validation.valid) {
232
+ return {
233
+ success: false,
234
+ error: {
235
+ code: validation.errors[0].code,
236
+ message: validation.errors[0].message,
237
+ details: validation.errors
238
+ }
239
+ };
240
+ }
241
+
242
+ log('Profile validation passed');
243
+
244
+ // Step 5: Dry-run mode - return preview without modifications
245
+ if (dryRun) {
246
+ const opencodePath = path.join(cwd, 'opencode.json');
247
+ return {
248
+ success: true,
249
+ dryRun: true,
250
+ preview: {
251
+ profile: targetProfileName,
252
+ models: {
253
+ planning: profileToUpdate.planning,
254
+ execution: profileToUpdate.execution,
255
+ verification: profileToUpdate.verification
256
+ },
257
+ changes: {
258
+ oc_config: {
259
+ path: configPath,
260
+ updates: {
261
+ current_oc_profile: targetProfileName,
262
+ ...(inlineProfile ? { 'profiles.presets': { [targetProfileName]: profileToUpdate } } : {})
263
+ }
264
+ },
265
+ opencode: {
266
+ path: opencodePath,
267
+ action: fs.existsSync(opencodePath) ? 'update' : 'create',
268
+ agentsToUpdate: getAgentsForProfile(profileToUpdate)
269
+ }
270
+ }
271
+ }
272
+ };
273
+ }
274
+
275
+ // Step 6: Create backup of oc_config.json
276
+ log('Creating backup of oc_config.json');
277
+ const backupPath = createBackup(configPath, path.join(cwd, '.planning', 'backups'));
278
+ if (!backupPath) {
279
+ return {
280
+ success: false,
281
+ error: {
282
+ code: 'BACKUP_FAILED',
283
+ message: 'Failed to create backup of oc_config.json'
284
+ }
285
+ };
286
+ }
287
+
288
+ // Step 7: Update oc_config.json (atomic transaction start)
289
+ try {
290
+ // Update current_oc_profile
291
+ config.current_oc_profile = targetProfileName;
292
+
293
+ // Add inline profile if provided
294
+ if (inlineProfile) {
295
+ if (!config.profiles) config.profiles = {};
296
+ if (!config.profiles.presets) config.profiles.presets = {};
297
+ config.profiles.presets[targetProfileName] = inlineProfile;
298
+ }
299
+
300
+ // write updated oc_config.json
301
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
302
+ log('Updated oc_config.json');
303
+ } catch (err) {
304
+ return {
305
+ success: false,
306
+ error: {
307
+ code: ERROR_CODES.WRITE_FAILED,
308
+ message: `Failed to write oc_config.json: ${err.message}`
309
+ }
310
+ };
311
+ }
312
+
313
+ // Step 8: Apply to opencode.json
314
+ const opencodePath = path.join(cwd, 'opencode.json');
315
+ const applyResult = applyProfileToOpencode(opencodePath, configPath, targetProfileName);
316
+
317
+ if (!applyResult.success) {
318
+ // Step 9: Rollback oc_config.json on failure
319
+ log('Applying to opencode.json failed, rolling back');
320
+ try {
321
+ const backupContent = fs.readFileSync(backupPath, 'utf8');
322
+ fs.writeFileSync(configPath, backupContent, 'utf8');
323
+ return {
324
+ success: false,
325
+ error: {
326
+ code: ERROR_CODES.APPLY_FAILED,
327
+ message: applyResult.error.message,
328
+ rolledBack: true,
329
+ backupPath
330
+ }
331
+ };
332
+ } catch (rollbackErr) {
333
+ return {
334
+ success: false,
335
+ error: {
336
+ code: ERROR_CODES.ROLLBACK_FAILED,
337
+ message: `Failed to apply profile AND failed to rollback: ${rollbackErr.message}`,
338
+ originalError: applyResult.error,
339
+ backupPath
340
+ }
341
+ };
342
+ }
343
+ }
344
+
345
+ log('Successfully applied profile');
346
+
347
+ // Step 10: Return success with details
348
+ return {
349
+ success: true,
350
+ data: {
351
+ profile: targetProfileName,
352
+ models: {
353
+ planning: profileToUpdate.planning,
354
+ execution: profileToUpdate.execution,
355
+ verification: profileToUpdate.verification
356
+ },
357
+ backup: backupPath,
358
+ updated: applyResult.updated,
359
+ configPath
360
+ }
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Get list of agent names that should be updated for a profile
366
+ * Helper function for dry-run preview
367
+ *
368
+ * @param {Object} profile - Profile object with planning/execution/verification
369
+ * @returns {Array} Array of {agent, model} objects
370
+ */
371
+ function getAgentsForProfile(profile) {
372
+ const PROFILE_AGENT_MAPPING = {
373
+ planning: [
374
+ 'gsd-planner',
375
+ 'gsd-plan-checker',
376
+ 'gsd-phase-researcher',
377
+ 'gsd-roadmapper',
378
+ 'gsd-project-researcher',
379
+ 'gsd-research-synthesizer',
380
+ 'gsd-codebase-mapper'
381
+ ],
382
+ execution: [
383
+ 'gsd-executor',
384
+ 'gsd-debugger'
385
+ ],
386
+ verification: [
387
+ 'gsd-verifier',
388
+ 'gsd-integration-checker'
389
+ ]
390
+ };
391
+
392
+ const agents = [];
393
+ for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
394
+ if (profile[category]) {
395
+ for (const agentName of agentNames) {
396
+ agents.push({ agent: agentName, model: profile[category] });
397
+ }
398
+ }
399
+ }
400
+ return agents;
401
+ }
402
+
403
+ module.exports = {
404
+ loadOcProfileConfig,
405
+ validateProfile,
406
+ applyProfileWithValidation,
407
+ getAgentsForProfile,
408
+ ERROR_CODES
409
+ };
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * gsd-oc-tools.cjs — Main CLI entry point for OpenCode tools
5
+ *
6
+ * Provides command routing for validation utilities and profile management.
7
+ * Follows gsd-tools.cjs architecture pattern.
8
+ *
9
+ * Usage: node gsd-oc-tools.cjs <command> [args] [--raw] [--verbose]
10
+ *
11
+ * Available Commands:
12
+ * check-opencode-json Validate model IDs in opencode.json
13
+ * check-config-json Validate profile configuration in .planning/oc_config.json (migrated from config.json)
14
+ * check-oc-config-json Validate profile configuration in .planning/oc_config.json
15
+ * update-opencode-json Update opencode.json agent models from oc_config profile
16
+ * validate-models Validate model IDs against opencode catalog
17
+ * set-profile Switch profile with interactive model selection
18
+ * get-profile Get current profile or specific profile from oc_config.json
19
+ * help Show this help message
20
+ */
21
+
22
+ const path = require('path');
23
+ const { output, error } = require('./gsd-oc-lib/oc-core.cjs');
24
+
25
+ // Parse command line arguments
26
+ const args = process.argv.slice(2);
27
+ const command = args[0];
28
+ const flags = args.slice(1);
29
+
30
+ const verbose = flags.includes('--verbose');
31
+ const raw = flags.includes('--raw');
32
+
33
+ // Current working directory
34
+ const cwd = process.cwd();
35
+
36
+ /**
37
+ * Show help message
38
+ */
39
+ function showHelp() {
40
+ const helpText = `
41
+ gsd-oc-tools — OpenCode validation utilities
42
+
43
+ Usage: node gsd-oc-tools.cjs <command> [options]
44
+
45
+ Available Commands:
46
+ check-opencode-json Validate model IDs in opencode.json against opencode models catalog
47
+ check-config-json Validate profile configuration in .planning/oc_config.json (migrated from config.json)
48
+ check-oc-config-json Validate profile configuration in .planning/oc_config.json
49
+ update-opencode-json Update opencode.json agent models from oc_config profile (creates backup)
50
+ validate-models Validate one or more model IDs against opencode catalog
51
+ set-profile Switch profile with interactive model selection wizard
52
+ get-profile Get current profile or specific profile from oc_config.json
53
+ help Show this help message
54
+
55
+ Options:
56
+ --verbose Enable verbose output (stderr)
57
+ --raw Output raw values instead of JSON envelope
58
+ --dry-run Preview changes without applying (update-opencode-json)
59
+
60
+ Examples:
61
+ node gsd-oc-tools.cjs check-opencode-json
62
+ node gsd-oc-tools.cjs check-config-json
63
+ node gsd-oc-tools.cjs update-opencode-json --dry-run
64
+ node gsd-oc-tools.cjs validate-models opencode/glm-4.7
65
+ node gsd-oc-tools.cjs set-profile genius
66
+ node gsd-oc-tools.cjs get-profile
67
+ node gsd-oc-tools.cjs get-profile genius
68
+ node gsd-oc-tools.cjs get-profile --raw
69
+ `.trim();
70
+
71
+ console.log(helpText);
72
+ process.exit(0);
73
+ }
74
+
75
+ // Command routing
76
+ if (!command || command === 'help') {
77
+ showHelp();
78
+ }
79
+
80
+ switch (command) {
81
+ case 'check-opencode-json': {
82
+ const checkOpencodeJson = require('./gsd-oc-commands/check-opencode-json.cjs');
83
+ checkOpencodeJson(cwd, flags);
84
+ break;
85
+ }
86
+
87
+ case 'check-config-json': {
88
+ // Updated implementation: validates .planning/oc_config.json (migrated from old config.json format)
89
+ const checkOcConfigJson = require('./gsd-oc-commands/check-oc-config-json.cjs');
90
+ checkOcConfigJson(cwd, flags);
91
+ break;
92
+ }
93
+
94
+ case 'check-oc-config-json': {
95
+ const checkOcConfigJson = require('./gsd-oc-commands/check-oc-config-json.cjs');
96
+ checkOcConfigJson(cwd, flags);
97
+ break;
98
+ }
99
+
100
+ case 'update-opencode-json': {
101
+ const updateOpencodeJson = require('./gsd-oc-commands/update-opencode-json.cjs');
102
+ updateOpencodeJson(cwd, flags);
103
+ break;
104
+ }
105
+
106
+ case 'validate-models': {
107
+ const validateModels = require('./gsd-oc-commands/validate-models.cjs');
108
+ validateModels(cwd, flags);
109
+ break;
110
+ }
111
+
112
+ case 'set-profile': {
113
+ const setProfile = require('./gsd-oc-commands/set-profile.cjs');
114
+ setProfile(cwd, flags);
115
+ break;
116
+ }
117
+
118
+ case 'get-profile': {
119
+ const getProfile = require('./gsd-oc-commands/get-profile.cjs');
120
+ getProfile(cwd, flags);
121
+ break;
122
+ }
123
+
124
+
125
+
126
+
127
+
128
+ default:
129
+ error(`Unknown command: ${command}\nRun 'node gsd-oc-tools.cjs help' for available commands.`);
130
+ }