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,447 @@
1
+ /**
2
+ * Unit tests for get-profile.cjs command
3
+ *
4
+ * Tests for both operation modes:
5
+ * - Mode 1: No parameters (get current profile)
6
+ * - Mode 2: Profile name parameter (get specific profile)
7
+ *
8
+ * Also tests --raw and --verbose flags, and error handling
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+
16
+ // Mock console.log and console.error to capture output
17
+ const originalLog = console.log;
18
+ const originalError = console.error;
19
+ const originalExit = process.exit;
20
+
21
+ // Test fixtures
22
+ const VALID_CONFIG_WITH_CURRENT = {
23
+ current_oc_profile: 'smart',
24
+ profiles: {
25
+ presets: {
26
+ simple: {
27
+ planning: 'bailian-coding-plan/qwen3.5-plus',
28
+ execution: 'bailian-coding-plan/qwen3.5-plus',
29
+ verification: 'bailian-coding-plan/qwen3.5-plus'
30
+ },
31
+ smart: {
32
+ planning: 'bailian-coding-plan/qwen3.5-plus',
33
+ execution: 'bailian-coding-plan/qwen3.5-plus',
34
+ verification: 'bailian-coding-plan/qwen3.5-plus'
35
+ },
36
+ genius: {
37
+ planning: 'bailian-coding-plan/qwen3.5-plus',
38
+ execution: 'bailian-coding-plan/qwen3.5-plus',
39
+ verification: 'bailian-coding-plan/qwen3.5-plus'
40
+ }
41
+ }
42
+ }
43
+ };
44
+
45
+ const VALID_CONFIG_WITHOUT_CURRENT = {
46
+ profiles: {
47
+ presets: {
48
+ simple: {
49
+ planning: 'bailian-coding-plan/qwen3.5-plus',
50
+ execution: 'bailian-coding-plan/qwen3.5-plus',
51
+ verification: 'bailian-coding-plan/qwen3.5-plus'
52
+ }
53
+ }
54
+ }
55
+ };
56
+
57
+ const VALID_CONFIG_INCOMPLETE_PROFILE = {
58
+ current_oc_profile: 'broken',
59
+ profiles: {
60
+ presets: {
61
+ broken: {
62
+ planning: 'bailian-coding-plan/qwen3.5-plus'
63
+ // missing execution and verification
64
+ }
65
+ }
66
+ }
67
+ };
68
+
69
+ describe('get-profile.cjs', () => {
70
+ let testDir;
71
+ let planningDir;
72
+ let configPath;
73
+ let capturedLog;
74
+ let capturedError;
75
+ let exitCode;
76
+ let allLogs;
77
+ let allErrors;
78
+
79
+ beforeEach(() => {
80
+ // Create isolated test directory
81
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-profile-test-'));
82
+ planningDir = path.join(testDir, '.planning');
83
+ configPath = path.join(planningDir, 'oc_config.json');
84
+ fs.mkdirSync(planningDir, { recursive: true });
85
+
86
+ // Reset captured output
87
+ capturedLog = null;
88
+ capturedError = null;
89
+ exitCode = null;
90
+ allLogs = [];
91
+ allErrors = [];
92
+
93
+ // Mock console.log to capture all output
94
+ console.log = (msg) => {
95
+ allLogs.push(msg);
96
+ capturedLog = msg;
97
+ };
98
+ console.error = (msg) => {
99
+ allErrors.push(msg);
100
+ capturedError = msg;
101
+ };
102
+ process.exit = (code) => {
103
+ exitCode = code;
104
+ throw new Error(`process.exit(${code})`);
105
+ };
106
+ });
107
+
108
+ afterEach(() => {
109
+ // Restore original functions
110
+ console.log = originalLog;
111
+ console.error = originalError;
112
+ process.exit = originalExit;
113
+
114
+ // Cleanup test directory
115
+ try {
116
+ fs.rmSync(testDir, { recursive: true, force: true });
117
+ } catch (err) {
118
+ // Ignore cleanup errors
119
+ }
120
+ });
121
+
122
+ // Import getProfile inside tests to use mocked functions
123
+ const importGetProfile = () => {
124
+ // Need to clear cache to get fresh import with mocked dependencies
125
+ const modulePath = '../gsd-oc-commands/get-profile.cjs';
126
+ delete require.cache[require.resolve(modulePath)];
127
+ return require(modulePath);
128
+ };
129
+
130
+ describe('Mode 1: No parameters (get current profile)', () => {
131
+ it('returns current profile when current_oc_profile is set', () => {
132
+ // Write config with current_oc_profile
133
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
134
+
135
+ const getProfile = importGetProfile();
136
+
137
+ try {
138
+ getProfile(testDir, []);
139
+ } catch (err) {
140
+ // Expected to throw due to process.exit mock
141
+ }
142
+
143
+ expect(exitCode).toBe(0);
144
+ const output = JSON.parse(capturedLog);
145
+ expect(output.success).toBe(true);
146
+ expect(output.data).toHaveProperty('smart');
147
+ expect(output.data.smart).toEqual(VALID_CONFIG_WITH_CURRENT.profiles.presets.smart);
148
+ });
149
+
150
+ it('returns MISSING_CURRENT_PROFILE error when current_oc_profile not set', () => {
151
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITHOUT_CURRENT, null, 2));
152
+
153
+ const getProfile = importGetProfile();
154
+
155
+ try {
156
+ getProfile(testDir, []);
157
+ } catch (err) {
158
+ // Expected
159
+ }
160
+
161
+ expect(exitCode).toBe(1);
162
+ const output = JSON.parse(capturedError);
163
+ expect(output.success).toBe(false);
164
+ expect(output.error.code).toBe('MISSING_CURRENT_PROFILE');
165
+ expect(output.error.message).toContain('current_oc_profile not set');
166
+ });
167
+
168
+ it('returns PROFILE_NOT_FOUND when current profile does not exist', () => {
169
+ const configWithMissingProfile = {
170
+ current_oc_profile: 'nonexistent',
171
+ profiles: {
172
+ presets: {
173
+ smart: {
174
+ planning: 'bailian-coding-plan/qwen3.5-plus',
175
+ execution: 'bailian-coding-plan/qwen3.5-plus',
176
+ verification: 'bailian-coding-plan/qwen3.5-plus'
177
+ }
178
+ }
179
+ }
180
+ };
181
+ fs.writeFileSync(configPath, JSON.stringify(configWithMissingProfile, null, 2));
182
+
183
+ const getProfile = importGetProfile();
184
+
185
+ try {
186
+ getProfile(testDir, []);
187
+ } catch (err) {
188
+ // Expected
189
+ }
190
+
191
+ expect(exitCode).toBe(1);
192
+ const output = JSON.parse(capturedError);
193
+ expect(output.success).toBe(false);
194
+ expect(output.error.code).toBe('PROFILE_NOT_FOUND');
195
+ expect(output.error.message).toContain('nonexistent');
196
+ expect(output.error.message).toContain('smart');
197
+ });
198
+ });
199
+
200
+ describe('Mode 2: Profile name parameter (get specific profile)', () => {
201
+ it('returns specified profile when it exists', () => {
202
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
203
+
204
+ const getProfile = importGetProfile();
205
+
206
+ try {
207
+ getProfile(testDir, ['genius']);
208
+ } catch (err) {
209
+ // Expected
210
+ }
211
+
212
+ expect(exitCode).toBe(0);
213
+ const output = JSON.parse(capturedLog);
214
+ expect(output.success).toBe(true);
215
+ expect(output.data).toHaveProperty('genius');
216
+ expect(output.data.genius).toEqual(VALID_CONFIG_WITH_CURRENT.profiles.presets.genius);
217
+ });
218
+
219
+ it('works even when current_oc_profile is not set', () => {
220
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITHOUT_CURRENT, null, 2));
221
+
222
+ const getProfile = importGetProfile();
223
+
224
+ try {
225
+ getProfile(testDir, ['simple']);
226
+ } catch (err) {
227
+ // Expected
228
+ }
229
+
230
+ expect(exitCode).toBe(0);
231
+ const output = JSON.parse(capturedLog);
232
+ expect(output.success).toBe(true);
233
+ expect(output.data).toHaveProperty('simple');
234
+ expect(output.data.simple).toEqual(VALID_CONFIG_WITHOUT_CURRENT.profiles.presets.simple);
235
+ });
236
+
237
+ it('returns PROFILE_NOT_FOUND with available profiles when profile does not exist', () => {
238
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
239
+
240
+ const getProfile = importGetProfile();
241
+
242
+ try {
243
+ getProfile(testDir, ['nonexistent']);
244
+ } catch (err) {
245
+ // Expected
246
+ }
247
+
248
+ expect(exitCode).toBe(1);
249
+ const output = JSON.parse(capturedError);
250
+ expect(output.success).toBe(false);
251
+ expect(output.error.code).toBe('PROFILE_NOT_FOUND');
252
+ expect(output.error.message).toContain('nonexistent');
253
+ expect(output.error.message).toContain('simple');
254
+ expect(output.error.message).toContain('smart');
255
+ expect(output.error.message).toContain('genius');
256
+ });
257
+ });
258
+
259
+ describe('--raw flag', () => {
260
+ it('outputs raw JSON without envelope in Mode 1', () => {
261
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
262
+
263
+ const getProfile = importGetProfile();
264
+
265
+ try {
266
+ getProfile(testDir, ['--raw']);
267
+ } catch (err) {
268
+ // Expected
269
+ }
270
+
271
+ expect(exitCode).toBe(0);
272
+ // capturedLog is already a string from JSON.stringify
273
+ const output = JSON.parse(capturedLog);
274
+ // Raw output should NOT have success/data envelope
275
+ expect(output).not.toHaveProperty('success');
276
+ expect(output).toHaveProperty('smart');
277
+ });
278
+
279
+ it('outputs raw JSON without envelope in Mode 2', () => {
280
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
281
+
282
+ const getProfile = importGetProfile();
283
+
284
+ try {
285
+ getProfile(testDir, ['genius', '--raw']);
286
+ } catch (err) {
287
+ // Expected
288
+ }
289
+
290
+ expect(exitCode).toBe(0);
291
+ const output = JSON.parse(capturedLog);
292
+ expect(output).not.toHaveProperty('success');
293
+ expect(output).toHaveProperty('genius');
294
+ });
295
+ });
296
+
297
+ describe('--verbose flag', () => {
298
+ it('outputs diagnostic info to stderr in Mode 1', () => {
299
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
300
+
301
+ const getProfile = importGetProfile();
302
+
303
+ try {
304
+ getProfile(testDir, ['--verbose']);
305
+ } catch (err) {
306
+ // Expected
307
+ }
308
+
309
+ expect(exitCode).toBe(0);
310
+ expect(allErrors.length).toBeGreaterThan(0);
311
+ // Check if any error message contains the expected content
312
+ const hasVerboseOutput = allErrors.some(msg => msg.includes('[get-profile]'));
313
+ expect(hasVerboseOutput).toBe(true);
314
+ });
315
+
316
+ it('outputs diagnostic info to stderr in Mode 2', () => {
317
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
318
+
319
+ const getProfile = importGetProfile();
320
+
321
+ try {
322
+ getProfile(testDir, ['genius', '--verbose']);
323
+ } catch (err) {
324
+ // Expected
325
+ }
326
+
327
+ expect(exitCode).toBe(0);
328
+ // Verbose output is sent to console.error, check if any errors were logged
329
+ expect(allErrors.length).toBeGreaterThan(0);
330
+ });
331
+ });
332
+
333
+ describe('Error format', () => {
334
+ it('uses structured JSON error format for CONFIG_NOT_FOUND', () => {
335
+ // Don't create config file
336
+ const getProfile = importGetProfile();
337
+
338
+ try {
339
+ getProfile(testDir, []);
340
+ } catch (err) {
341
+ // Expected
342
+ }
343
+
344
+ expect(exitCode).toBe(1);
345
+ const output = JSON.parse(capturedError);
346
+ expect(output.success).toBe(false);
347
+ expect(output.error).toHaveProperty('code');
348
+ expect(output.error).toHaveProperty('message');
349
+ expect(output.error.code).toBe('CONFIG_NOT_FOUND');
350
+ });
351
+
352
+ it('uses structured JSON error format for INVALID_JSON', () => {
353
+ fs.writeFileSync(configPath, 'invalid json {');
354
+
355
+ const getProfile = importGetProfile();
356
+
357
+ try {
358
+ getProfile(testDir, []);
359
+ } catch (err) {
360
+ // Expected
361
+ }
362
+
363
+ expect(exitCode).toBe(1);
364
+ const output = JSON.parse(capturedError);
365
+ expect(output.success).toBe(false);
366
+ expect(output.error.code).toBe('INVALID_JSON');
367
+ });
368
+
369
+ it('provides descriptive error messages', () => {
370
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITHOUT_CURRENT, null, 2));
371
+
372
+ const getProfile = importGetProfile();
373
+
374
+ try {
375
+ getProfile(testDir, []);
376
+ } catch (err) {
377
+ // Expected
378
+ }
379
+
380
+ expect(exitCode).toBe(1);
381
+ const output = JSON.parse(capturedError);
382
+ expect(output.error.message).toContain('current_oc_profile');
383
+ expect(output.error.message).toContain('Run set-profile first');
384
+ });
385
+ });
386
+
387
+ describe('Output format', () => {
388
+ it('returns profile definition as {profileName: {planning, execution, verification}}', () => {
389
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
390
+
391
+ const getProfile = importGetProfile();
392
+
393
+ try {
394
+ getProfile(testDir, ['smart']);
395
+ } catch (err) {
396
+ // Expected
397
+ }
398
+
399
+ expect(exitCode).toBe(0);
400
+ const output = JSON.parse(capturedLog);
401
+ expect(output.data).toHaveProperty('smart');
402
+ expect(output.data.smart).toHaveProperty('planning');
403
+ expect(output.data.smart).toHaveProperty('execution');
404
+ expect(output.data.smart).toHaveProperty('verification');
405
+ });
406
+ });
407
+
408
+ describe('Error handling', () => {
409
+ it('handles missing .planning directory', () => {
410
+ const badTestDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-profile-test-'));
411
+ // Don't create .planning directory
412
+
413
+ const getProfile = importGetProfile();
414
+
415
+ try {
416
+ getProfile(badTestDir, []);
417
+ } catch (err) {
418
+ // Expected
419
+ }
420
+
421
+ expect(exitCode).toBe(1);
422
+ const output = JSON.parse(capturedError);
423
+ expect(output.success).toBe(false);
424
+ expect(output.error.code).toBe('CONFIG_NOT_FOUND');
425
+
426
+ fs.rmSync(badTestDir, { recursive: true, force: true });
427
+ });
428
+
429
+ it('handles too many arguments', () => {
430
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
431
+
432
+ const getProfile = importGetProfile();
433
+
434
+ try {
435
+ getProfile(testDir, ['smart', 'genius']);
436
+ } catch (err) {
437
+ // Expected
438
+ }
439
+
440
+ expect(exitCode).toBe(1);
441
+ const output = JSON.parse(capturedError);
442
+ expect(output.success).toBe(false);
443
+ expect(output.error.code).toBe('INVALID_ARGS');
444
+ expect(output.error.message).toContain('Too many arguments');
445
+ });
446
+ });
447
+ });