gsd-opencode 1.20.2 → 1.20.4

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 (27) hide show
  1. package/commands/gsd/gsd-check-profile.md +30 -0
  2. package/get-shit-done/bin/gsd-oc-commands/allow-read-config.cjs +235 -0
  3. package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
  4. package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
  5. package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
  6. package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
  7. package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
  8. package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
  9. package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
  10. package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
  11. package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
  12. package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
  13. package/get-shit-done/bin/gsd-oc-tools.cjs +136 -0
  14. package/get-shit-done/bin/lib/oc-config.cjs +200 -0
  15. package/get-shit-done/bin/lib/oc-core.cjs +114 -0
  16. package/get-shit-done/bin/lib/oc-models.cjs +133 -0
  17. package/get-shit-done/bin/test/allow-read-config.test.cjs +262 -0
  18. package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
  19. package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
  20. package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
  21. package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
  22. package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
  23. package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
  24. package/get-shit-done/workflows/oc-check-profile.md +181 -0
  25. package/get-shit-done/workflows/oc-set-profile.md +98 -234
  26. package/get-shit-done/workflows/settings.md +4 -3
  27. package/package.json +2 -2
@@ -0,0 +1,200 @@
1
+ /**
2
+ * oc-config.cjs — Profile configuration operations for gsd-oc-tools CLI
3
+ *
4
+ * Provides functions for loading profile config and applying model assignments to opencode.json.
5
+ * Follows gsd-tools.cjs architecture pattern.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Valid profile types whitelist
13
+ */
14
+ const VALID_PROFILES = ['simple', 'smart', 'genius'];
15
+
16
+ /**
17
+ * Profile to agent mapping
18
+ * Maps profile keys to opencode.json agent names
19
+ */
20
+ const PROFILE_AGENT_MAPPING = {
21
+ // Planning agents
22
+ planning: [
23
+ 'gsd-planner',
24
+ 'gsd-plan-checker',
25
+ 'gsd-phase-researcher',
26
+ 'gsd-roadmapper',
27
+ 'gsd-project-researcher',
28
+ 'gsd-research-synthesizer',
29
+ 'gsd-codebase-mapper'
30
+ ],
31
+ // Execution agents
32
+ execution: [
33
+ 'gsd-executor',
34
+ 'gsd-debugger'
35
+ ],
36
+ // Verification agents
37
+ verification: [
38
+ 'gsd-verifier',
39
+ 'gsd-integration-checker'
40
+ ]
41
+ };
42
+
43
+ /**
44
+ * Load profile configuration from .planning/config.json
45
+ *
46
+ * @param {string} cwd - Current working directory
47
+ * @returns {Object|null} Parsed config object or null on error
48
+ */
49
+ function loadProfileConfig(cwd) {
50
+ try {
51
+ const configPath = path.join(cwd, '.planning', 'config.json');
52
+
53
+ if (!fs.existsSync(configPath)) {
54
+ return null;
55
+ }
56
+
57
+ const content = fs.readFileSync(configPath, 'utf8');
58
+ const config = JSON.parse(content);
59
+
60
+ return config;
61
+ } catch (err) {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Apply profile configuration to opencode.json
68
+ * Updates agent model assignments based on profile
69
+ *
70
+ * @param {string} opencodePath - Path to opencode.json
71
+ * @param {string} configPath - Path to .planning/config.json
72
+ * @returns {Object} {success: true, updated: [agentNames]} or {success: false, error: {code, message}}
73
+ */
74
+ function applyProfileToOpencode(opencodePath, configPath) {
75
+ try {
76
+ // Load profile config
77
+ let config;
78
+ if (fs.existsSync(configPath)) {
79
+ const content = fs.readFileSync(configPath, 'utf8');
80
+ config = JSON.parse(content);
81
+ } else {
82
+ return {
83
+ success: false,
84
+ error: {
85
+ code: 'CONFIG_NOT_FOUND',
86
+ message: `.planning/config.json not found at ${configPath}`
87
+ }
88
+ };
89
+ }
90
+
91
+ // Validate profile_type
92
+ const profileType = config.profile_type || config.profiles?.profile_type;
93
+ if (!profileType) {
94
+ return {
95
+ success: false,
96
+ error: {
97
+ code: 'PROFILE_NOT_FOUND',
98
+ message: 'profile_type not found in config.json'
99
+ }
100
+ };
101
+ }
102
+
103
+ if (!VALID_PROFILES.includes(profileType)) {
104
+ return {
105
+ success: false,
106
+ error: {
107
+ code: 'INVALID_PROFILE',
108
+ message: `Invalid profile_type: "${profileType}". Valid profiles: ${VALID_PROFILES.join(', ')}`
109
+ }
110
+ };
111
+ }
112
+
113
+ // Load opencode.json
114
+ if (!fs.existsSync(opencodePath)) {
115
+ return {
116
+ success: false,
117
+ error: {
118
+ code: 'CONFIG_NOT_FOUND',
119
+ message: `opencode.json not found at ${opencodePath}`
120
+ }
121
+ };
122
+ }
123
+
124
+ const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
125
+ const opencodeData = JSON.parse(opencodeContent);
126
+
127
+ // Get model assignments from profile
128
+ // Support both structures: profiles.planning or profiles.models.planning
129
+ const profiles = config.profiles || {};
130
+ let profileModels;
131
+
132
+ // Try new structure first: profiles.models.{planning|execution|verification}
133
+ if (profiles.models && typeof profiles.models === 'object') {
134
+ profileModels = profiles.models;
135
+ } else {
136
+ // Fallback to old structure: profiles.{planning|execution|verification}
137
+ profileModels = profiles[profileType] || {};
138
+ }
139
+
140
+ if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
141
+ return {
142
+ success: false,
143
+ error: {
144
+ code: 'PROFILE_NOT_FOUND',
145
+ message: `No model assignments found for profile "${profileType}"`
146
+ }
147
+ };
148
+ }
149
+
150
+ // Apply model assignments to agents
151
+ const updatedAgents = [];
152
+
153
+ // Initialize agent object if it doesn't exist
154
+ if (!opencodeData.agent) {
155
+ opencodeData.agent = {};
156
+ }
157
+
158
+ // Apply each profile category
159
+ for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
160
+ const modelId = profileModels[category];
161
+
162
+ if (modelId) {
163
+ for (const agentName of agentNames) {
164
+ // Handle both string and object agent configurations
165
+ if (typeof opencodeData.agent[agentName] === 'string') {
166
+ opencodeData.agent[agentName] = modelId;
167
+ } else if (typeof opencodeData.agent[agentName] === 'object' && opencodeData.agent[agentName] !== null) {
168
+ opencodeData.agent[agentName].model = modelId;
169
+ } else {
170
+ opencodeData.agent[agentName] = modelId;
171
+ }
172
+ updatedAgents.push(agentName);
173
+ }
174
+ }
175
+ }
176
+
177
+ // write updated opencode.json
178
+ fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
179
+
180
+ return {
181
+ success: true,
182
+ updated: updatedAgents
183
+ };
184
+ } catch (err) {
185
+ return {
186
+ success: false,
187
+ error: {
188
+ code: 'UPDATE_FAILED',
189
+ message: `Failed to apply profile: ${err.message}`
190
+ }
191
+ };
192
+ }
193
+ }
194
+
195
+ module.exports = {
196
+ loadProfileConfig,
197
+ applyProfileToOpencode,
198
+ VALID_PROFILES,
199
+ PROFILE_AGENT_MAPPING
200
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * oc-core.cjs — Shared utilities for gsd-oc-tools CLI
3
+ *
4
+ * Provides common functions for output formatting, error handling, file operations.
5
+ * Follows gsd-tools.cjs architecture pattern.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Output result in JSON envelope format
13
+ * Large payloads (>50KB) are written to temp file with @file: prefix
14
+ *
15
+ * @param {Object} result - The result data to output
16
+ * @param {boolean} raw - If true, output raw value instead of envelope
17
+ * @param {*} rawValue - The raw value to output if raw=true
18
+ */
19
+ function output(result, raw = false, rawValue = null) {
20
+ let outputData;
21
+
22
+ if (raw && rawValue !== null) {
23
+ outputData = rawValue;
24
+ } else {
25
+ outputData = result;
26
+ }
27
+
28
+ const outputStr = JSON.stringify(outputData, null, 2);
29
+
30
+ // Large payload handling (>50KB)
31
+ if (outputStr.length > 50 * 1024) {
32
+ const tempFile = path.join(require('os').tmpdir(), `gsd-oc-${Date.now()}.json`);
33
+ fs.writeFileSync(tempFile, outputStr, 'utf8');
34
+ console.log(`@file:${tempFile}`);
35
+ } else {
36
+ console.log(outputStr);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Output error in standardized envelope format to stderr
42
+ *
43
+ * @param {string} message - Error message
44
+ * @param {string} code - Error code (e.g., 'CONFIG_NOT_FOUND', 'INVALID_JSON')
45
+ */
46
+ function error(message, code = 'UNKNOWN_ERROR') {
47
+ const errorEnvelope = {
48
+ success: false,
49
+ error: {
50
+ code,
51
+ message
52
+ }
53
+ };
54
+ console.error(JSON.stringify(errorEnvelope, null, 2));
55
+ process.exit(1);
56
+ }
57
+
58
+ /**
59
+ * Safely read a file, returning null on failure
60
+ *
61
+ * @param {string} filePath - Path to file
62
+ * @returns {string|null} File contents or null
63
+ */
64
+ function safeReadFile(filePath) {
65
+ try {
66
+ return fs.readFileSync(filePath, 'utf8');
67
+ } catch (err) {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Create timestamped backup of a file
74
+ *
75
+ * @param {string} filePath - Path to file to backup
76
+ * @param {string} backupDir - Directory for backups (.opencode-backups/)
77
+ * @returns {string|null} Backup file path or null on failure
78
+ */
79
+ function createBackup(filePath, backupDir = '.opencode-backups') {
80
+ try {
81
+ // Ensure backup directory exists
82
+ if (!fs.existsSync(backupDir)) {
83
+ fs.mkdirSync(backupDir, { recursive: true });
84
+ }
85
+
86
+ // read original file
87
+ const content = fs.readFileSync(filePath, 'utf8');
88
+
89
+ // Create timestamped filename (YYYYMMDD-HHmmss-SSS format)
90
+ const now = new Date();
91
+ const timestamp = now.toISOString()
92
+ .replace(/[-:T]/g, '')
93
+ .replace(/\.\d{3}Z$/, '')
94
+ .replace(/(\d{8})(\d{6})(\d{3})/, '$1-$2-$3');
95
+
96
+ const fileName = path.basename(filePath);
97
+ const backupFileName = `${timestamp}-${fileName}`;
98
+ const backupPath = path.join(backupDir, backupFileName);
99
+
100
+ // write backup
101
+ fs.writeFileSync(backupPath, content, 'utf8');
102
+
103
+ return backupPath;
104
+ } catch (err) {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ output,
111
+ error,
112
+ safeReadFile,
113
+ createBackup
114
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * oc-models.cjs — Model catalog operations for gsd-oc-tools CLI
3
+ *
4
+ * Provides functions for fetching and validating model IDs against opencode models output.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ /**
12
+ * Fetch model catalog from opencode models command
13
+ *
14
+ * @returns {Object} {success: boolean, models: string[]} or {success: false, error: {...}}
15
+ */
16
+ function getModelCatalog() {
17
+ try {
18
+ const output = execSync('opencode models', {
19
+ encoding: 'utf8',
20
+ stdio: ['pipe', 'pipe', 'pipe']
21
+ });
22
+
23
+ // Parse output (one model per line)
24
+ const models = output
25
+ .split('\n')
26
+ .map(line => line.trim())
27
+ .filter(line => line.length > 0);
28
+
29
+ return {
30
+ success: true,
31
+ models
32
+ };
33
+ } catch (err) {
34
+ return {
35
+ success: false,
36
+ error: {
37
+ code: 'FETCH_FAILED',
38
+ message: `Failed to fetch model catalog: ${err.message}`
39
+ }
40
+ };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Validate model IDs in opencode.json against valid models list
46
+ *
47
+ * @param {string} opencodePath - Path to opencode.json file
48
+ * @param {string[]} validModels - Array of valid model IDs
49
+ * @returns {Object} {valid, total, validCount, invalidCount, issues: [{agent, model, reason}]}
50
+ */
51
+ function validateModelIds(opencodePath, validModels) {
52
+ const issues = [];
53
+ let total = 0;
54
+ let validCount = 0;
55
+ let invalidCount = 0;
56
+
57
+ try {
58
+ const content = fs.readFileSync(opencodePath, 'utf8');
59
+ const opencodeData = JSON.parse(content);
60
+
61
+ // Look for agent model assignments
62
+ // Common patterns: agent.model, profiles.*.model, models.*
63
+ const assignments = [];
64
+
65
+ // Check for agents at root level
66
+ if (opencodeData.agent && typeof opencodeData.agent === 'object') {
67
+ Object.entries(opencodeData.agent).forEach(([agentName, config]) => {
68
+ if (typeof config === 'string') {
69
+ assignments.push({ agent: `agent.${agentName}`, model: config });
70
+ } else if (config && typeof config === 'object' && config.model) {
71
+ assignments.push({ agent: `agent.${agentName}`, model: config.model });
72
+ }
73
+ });
74
+ }
75
+
76
+ // Check for profiles
77
+ if (opencodeData.profiles && typeof opencodeData.profiles === 'object') {
78
+ Object.entries(opencodeData.profiles).forEach(([profileName, config]) => {
79
+ if (config && typeof config === 'object') {
80
+ Object.entries(config).forEach(([key, value]) => {
81
+ if (key.includes('model') && typeof value === 'string') {
82
+ assignments.push({ agent: `profiles.${profileName}.${key}`, model: value });
83
+ }
84
+ });
85
+ }
86
+ });
87
+ }
88
+
89
+ // Check for models at root level
90
+ if (opencodeData.models && typeof opencodeData.models === 'object') {
91
+ Object.entries(opencodeData.models).forEach(([modelName, modelId]) => {
92
+ if (typeof modelId === 'string') {
93
+ assignments.push({ agent: `models.${modelName}`, model: modelId });
94
+ }
95
+ });
96
+ }
97
+
98
+ // Validate each assignment
99
+ total = assignments.length;
100
+ for (const { agent, model } of assignments) {
101
+ if (validModels.includes(model)) {
102
+ validCount++;
103
+ } else {
104
+ invalidCount++;
105
+ issues.push({
106
+ agent,
107
+ model,
108
+ reason: 'Model ID not found in opencode models catalog'
109
+ });
110
+ }
111
+ }
112
+
113
+ return {
114
+ valid: invalidCount === 0,
115
+ total,
116
+ validCount,
117
+ invalidCount,
118
+ issues
119
+ };
120
+ } catch (err) {
121
+ if (err.code === 'ENOENT') {
122
+ throw new Error('CONFIG_NOT_FOUND');
123
+ } else if (err instanceof SyntaxError) {
124
+ throw new Error('INVALID_JSON');
125
+ }
126
+ throw err;
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ getModelCatalog,
132
+ validateModelIds
133
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * allow-read-config.test.cjs — Tests for allow-read-config command
3
+ *
4
+ * Tests the allow-read-config command functionality:
5
+ * - Permission creation
6
+ * - Idempotency (detecting existing permission)
7
+ * - Dry-run mode
8
+ * - Backup creation
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { execSync } = require('child_process');
15
+
16
+ const CLI_PATH = path.join(__dirname, '../gsd-oc-commands/allow-read-config.cjs');
17
+ const TOOLS_PATH = path.join(__dirname, '../gsd-oc-tools.cjs');
18
+
19
+ /**
20
+ * Create a temporary test directory
21
+ */
22
+ function createTestDir() {
23
+ const testDir = path.join(os.tmpdir(), `gsd-oc-test-${Date.now()}`);
24
+ fs.mkdirSync(testDir, { recursive: true });
25
+ return testDir;
26
+ }
27
+
28
+ /**
29
+ * Clean up test directory
30
+ */
31
+ function cleanupTestDir(testDir) {
32
+ if (fs.existsSync(testDir)) {
33
+ fs.rmSync(testDir, { recursive: true, force: true });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Run CLI command and parse JSON output
39
+ */
40
+ function runCLI(testDir, args) {
41
+ const cmd = `node ${TOOLS_PATH} allow-read-config ${args.join(' ')}`;
42
+ const output = execSync(cmd, { cwd: testDir, encoding: 'utf8' });
43
+ return JSON.parse(output);
44
+ }
45
+
46
+ /**
47
+ * Test: Create new opencode.json with permission
48
+ */
49
+ function testCreatePermission() {
50
+ console.log('Test: Create new opencode.json with permission...');
51
+
52
+ const testDir = createTestDir();
53
+
54
+ try {
55
+ const result = runCLI(testDir, []);
56
+
57
+ if (!result.success) {
58
+ throw new Error(`Expected success, got: ${JSON.stringify(result)}`);
59
+ }
60
+
61
+ if (result.data.action !== 'add_permission') {
62
+ throw new Error(`Expected action 'add_permission', got: ${result.data.action}`);
63
+ }
64
+
65
+ if (result.data.created !== true) {
66
+ throw new Error(`Expected created=true, got: ${result.data.created}`);
67
+ }
68
+
69
+ // Verify opencode.json was created
70
+ const opencodePath = path.join(testDir, 'opencode.json');
71
+ if (!fs.existsSync(opencodePath)) {
72
+ throw new Error('opencode.json was not created');
73
+ }
74
+
75
+ const content = JSON.parse(fs.readFileSync(opencodePath, 'utf8'));
76
+ if (!content.permission?.external_directory) {
77
+ throw new Error('Permission not added to opencode.json');
78
+ }
79
+
80
+ console.log('✓ PASS: Create permission\n');
81
+ return true;
82
+ } catch (err) {
83
+ console.error('✗ FAIL:', err.message, '\n');
84
+ return false;
85
+ } finally {
86
+ cleanupTestDir(testDir);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Test: Idempotency - detect existing permission
92
+ */
93
+ function testIdempotency() {
94
+ console.log('Test: Idempotency (detect existing permission)...');
95
+
96
+ const testDir = createTestDir();
97
+
98
+ try {
99
+ // First call - create permission
100
+ runCLI(testDir, []);
101
+
102
+ // Second call - should detect existing
103
+ const result = runCLI(testDir, []);
104
+
105
+ if (!result.success) {
106
+ throw new Error(`Expected success, got: ${JSON.stringify(result)}`);
107
+ }
108
+
109
+ if (result.data.action !== 'permission_exists') {
110
+ throw new Error(`Expected action 'permission_exists', got: ${result.data.action}`);
111
+ }
112
+
113
+ console.log('✓ PASS: Idempotency\n');
114
+ return true;
115
+ } catch (err) {
116
+ console.error('✗ FAIL:', err.message, '\n');
117
+ return false;
118
+ } finally {
119
+ cleanupTestDir(testDir);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Test: Dry-run mode
125
+ */
126
+ function testDryRun() {
127
+ console.log('Test: Dry-run mode...');
128
+
129
+ const testDir = createTestDir();
130
+
131
+ try {
132
+ const result = runCLI(testDir, ['--dry-run']);
133
+
134
+ if (!result.success) {
135
+ throw new Error(`Expected success, got: ${JSON.stringify(result)}`);
136
+ }
137
+
138
+ if (result.data.dryRun !== true) {
139
+ throw new Error(`Expected dryRun=true, got: ${result.data.dryRun}`);
140
+ }
141
+
142
+ // Verify opencode.json was NOT created
143
+ const opencodePath = path.join(testDir, 'opencode.json');
144
+ if (fs.existsSync(opencodePath)) {
145
+ throw new Error('opencode.json should not be created in dry-run mode');
146
+ }
147
+
148
+ console.log('✓ PASS: Dry-run mode\n');
149
+ return true;
150
+ } catch (err) {
151
+ console.error('✗ FAIL:', err.message, '\n');
152
+ return false;
153
+ } finally {
154
+ cleanupTestDir(testDir);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Test: Backup creation on update
160
+ */
161
+ function testBackupCreation() {
162
+ console.log('Test: Backup creation on update...');
163
+
164
+ const testDir = createTestDir();
165
+
166
+ try {
167
+ // Create initial opencode.json
168
+ const opencodePath = path.join(testDir, 'opencode.json');
169
+ const initialContent = {
170
+ "$schema": "https://opencode.ai/config.json",
171
+ "model": "test/model"
172
+ };
173
+ fs.writeFileSync(opencodePath, JSON.stringify(initialContent, null, 2) + '\n');
174
+
175
+ // Run allow-read-config
176
+ const result = runCLI(testDir, []);
177
+
178
+ if (!result.success) {
179
+ throw new Error(`Expected success, got: ${JSON.stringify(result)}`);
180
+ }
181
+
182
+ if (!result.data.backup) {
183
+ throw new Error('Expected backup path, got none');
184
+ }
185
+
186
+ if (!fs.existsSync(result.data.backup)) {
187
+ throw new Error(`Backup file does not exist: ${result.data.backup}`);
188
+ }
189
+
190
+ // Verify backup content matches original
191
+ const backupContent = JSON.parse(fs.readFileSync(result.data.backup, 'utf8'));
192
+ if (JSON.stringify(backupContent) !== JSON.stringify(initialContent)) {
193
+ throw new Error('Backup content does not match original');
194
+ }
195
+
196
+ console.log('✓ PASS: Backup creation\n');
197
+ return true;
198
+ } catch (err) {
199
+ console.error('✗ FAIL:', err.message, '\n');
200
+ return false;
201
+ } finally {
202
+ cleanupTestDir(testDir);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Test: Verbose output
208
+ */
209
+ function testVerbose() {
210
+ console.log('Test: Verbose output...');
211
+
212
+ const testDir = createTestDir();
213
+
214
+ try {
215
+ const cmd = `node ${TOOLS_PATH} allow-read-config --verbose`;
216
+ const output = execSync(cmd, { cwd: testDir, encoding: 'utf8', stdio: 'pipe' });
217
+
218
+ // Verbose output should contain log messages to stderr
219
+ // We just verify it doesn't crash
220
+ console.log('✓ PASS: Verbose output\n');
221
+ return true;
222
+ } catch (err) {
223
+ console.error('✗ FAIL:', err.message, '\n');
224
+ return false;
225
+ } finally {
226
+ cleanupTestDir(testDir);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Run all tests
232
+ */
233
+ function runTests() {
234
+ console.log('Running allow-read-config tests...\n');
235
+ console.log('=' .repeat(50));
236
+ console.log();
237
+
238
+ const results = [
239
+ testCreatePermission(),
240
+ testIdempotency(),
241
+ testDryRun(),
242
+ testBackupCreation(),
243
+ testVerbose()
244
+ ];
245
+
246
+ const passed = results.filter(r => r).length;
247
+ const total = results.length;
248
+
249
+ console.log('=' .repeat(50));
250
+ console.log(`Results: ${passed}/${total} tests passed`);
251
+
252
+ if (passed === total) {
253
+ console.log('✓ All tests passed!\n');
254
+ process.exit(0);
255
+ } else {
256
+ console.error(`✗ ${total - passed} test(s) failed\n`);
257
+ process.exit(1);
258
+ }
259
+ }
260
+
261
+ // Run tests
262
+ runTests();