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.
- package/commands/gsd/gsd-check-profile.md +30 -0
- package/get-shit-done/bin/gsd-oc-commands/allow-read-config.cjs +235 -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 +136 -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/allow-read-config.test.cjs +262 -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 +98 -234
- package/get-shit-done/workflows/settings.md +4 -3
- 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();
|