@syntesseraai/opencode-feature-factory 0.6.6 → 0.6.8
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/README.md +4 -0
- package/agents/pipeline.md +3 -0
- package/bin/ff-deploy.js +81 -7
- package/commands/pipeline/documentation/gate.md +19 -0
- package/commands/pipeline/documentation/run-codex.md +23 -0
- package/commands/pipeline/documentation/run-gemini.md +21 -0
- package/commands/pipeline/documentation/run.md +27 -0
- package/commands/pipeline/start.md +2 -1
- package/dist/agent-config.js +17 -51
- package/dist/mcp-config.js +17 -51
- package/dist/opencode-global-config.d.ts +9 -0
- package/dist/opencode-global-config.js +79 -0
- package/dist/plugin-config.js +17 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ It also updates `~/.config/opencode/opencode.json` non-destructively by merging
|
|
|
46
46
|
- `/pipeline/planning/run`, `/pipeline/planning/run-opus`, `/pipeline/planning/run-gemini`, `/pipeline/planning/run-codex`, `/pipeline/planning/synthesize`, `/pipeline/planning/gate`
|
|
47
47
|
- `/pipeline/building/run`, `/pipeline/building/breakdown`, `/pipeline/building/validate-batch`, `/pipeline/building/implement-batch`
|
|
48
48
|
- `/pipeline/reviewing/run`, `/pipeline/reviewing/triage`, `/pipeline/reviewing/run-opus`, `/pipeline/reviewing/run-gemini`, `/pipeline/reviewing/run-codex`, `/pipeline/reviewing/synthesize`, `/pipeline/reviewing/gate`
|
|
49
|
+
- `/pipeline/documentation/run`, `/pipeline/documentation/run-codex`, `/pipeline/documentation/run-gemini`, `/pipeline/documentation/gate`
|
|
49
50
|
- `/pipeline/complete`
|
|
50
51
|
|
|
51
52
|
## Model Split
|
|
@@ -55,11 +56,14 @@ It also updates `~/.config/opencode/opencode.json` non-destructively by merging
|
|
|
55
56
|
- Implementation: Codex only
|
|
56
57
|
- Review (with architecture validation): Codex, Gemini, and Opus
|
|
57
58
|
- Rework path: `/pipeline/reviewing/run` re-enters implementation via `/pipeline/building/implement-batch` when gate status is `REWORK`
|
|
59
|
+
- Documentation stage: Codex updates documentation, Gemini reviews docs, and ChatGPT 5.4 supervises a bounded loop until approved
|
|
60
|
+
- Documentation stage skill usage: Codex loads `ff-context-tracking`, `ff-todo-management`, `ff-mini-plan`; Gemini loads `ff-report-templates` and `ff-severity-classification`
|
|
58
61
|
|
|
59
62
|
## Quality Gates
|
|
60
63
|
|
|
61
64
|
- Planning approval: `>=75%` consensus.
|
|
62
65
|
- Review approval: `>=95%` confidence and zero unresolved issues.
|
|
66
|
+
- Documentation approval: Gemini verdict `APPROVED` with zero unresolved documentation issues.
|
|
63
67
|
- Planning loop confirmation: after 5 unsuccessful planning iterations, pipeline asks user whether to continue.
|
|
64
68
|
|
|
65
69
|
## Related Docs
|
package/agents/pipeline.md
CHANGED
|
@@ -51,6 +51,7 @@ Do not keep orchestration logic in this file. The workflow lives in:
|
|
|
51
51
|
- `/pipeline/planning/*`
|
|
52
52
|
- `/pipeline/building/*`
|
|
53
53
|
- `/pipeline/reviewing/*`
|
|
54
|
+
- `/pipeline/documentation/*`
|
|
54
55
|
- `/pipeline/complete`
|
|
55
56
|
|
|
56
57
|
## Required Intake Flow
|
|
@@ -76,6 +77,7 @@ When confirmed, execute:
|
|
|
76
77
|
- Enforce quality gates from command files:
|
|
77
78
|
- Planning approval: `>=75%` consensus.
|
|
78
79
|
- Review approval: `>=95%` confidence and zero unresolved issues.
|
|
80
|
+
- Documentation approval: Gemini must return `APPROVED` with zero unresolved documentation issues.
|
|
79
81
|
|
|
80
82
|
## Progress Updates
|
|
81
83
|
|
|
@@ -92,4 +94,5 @@ When `/pipeline/complete` finishes, return a concise execution summary with:
|
|
|
92
94
|
- accepted plan
|
|
93
95
|
- implemented task groups
|
|
94
96
|
- review outcomes
|
|
97
|
+
- documentation outcomes
|
|
95
98
|
- assumptions made
|
package/bin/ff-deploy.js
CHANGED
|
@@ -51,6 +51,40 @@ const DEFAULT_MCP_SERVERS = {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
const DEFAULT_PLUGINS = [
|
|
55
|
+
'@syntesseraai/opencode-feature-factory@latest',
|
|
56
|
+
'@nick-vi/opencode-type-inject@latest',
|
|
57
|
+
'@franlol/opencode-md-table-formatter@latest',
|
|
58
|
+
'@spoons-and-mirrors/subtask2@latest',
|
|
59
|
+
'opencode-pty@latest',
|
|
60
|
+
'@angdrew/opencode-hashline-plugin@latest',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function getPackageBaseName(plugin) {
|
|
64
|
+
if (plugin.startsWith('@')) {
|
|
65
|
+
const slashIndex = plugin.indexOf('/');
|
|
66
|
+
if (slashIndex !== -1) {
|
|
67
|
+
const versionAt = plugin.indexOf('@', slashIndex + 1);
|
|
68
|
+
if (versionAt !== -1) {
|
|
69
|
+
return plugin.substring(0, versionAt);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return plugin;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const atIndex = plugin.indexOf('@');
|
|
76
|
+
if (atIndex !== -1) {
|
|
77
|
+
return plugin.substring(0, atIndex);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return plugin;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasPlugin(existing, plugin) {
|
|
84
|
+
const baseName = getPackageBaseName(plugin);
|
|
85
|
+
return existing.some((entry) => getPackageBaseName(entry) === baseName);
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
async function ensureDir(dir) {
|
|
55
89
|
try {
|
|
56
90
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -127,11 +161,30 @@ async function updateMCPConfig() {
|
|
|
127
161
|
try {
|
|
128
162
|
// Read existing config if it exists
|
|
129
163
|
let existingConfig = {};
|
|
164
|
+
let hasExistingConfigFile = false;
|
|
130
165
|
try {
|
|
131
166
|
const configContent = await fs.readFile(GLOBAL_CONFIG_FILE, 'utf8');
|
|
167
|
+
hasExistingConfigFile = true;
|
|
132
168
|
existingConfig = JSON.parse(configContent);
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
if (!existingConfig || typeof existingConfig !== 'object' || Array.isArray(existingConfig)) {
|
|
170
|
+
if (isInteractive) {
|
|
171
|
+
console.warn('\n⚠️ Existing opencode.json is not a JSON object. Skipping update.');
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error && error.code === 'ENOENT') {
|
|
177
|
+
// No existing config, will create new
|
|
178
|
+
} else if (error instanceof SyntaxError) {
|
|
179
|
+
if (isInteractive) {
|
|
180
|
+
console.warn(
|
|
181
|
+
'\n⚠️ Existing opencode.json is invalid JSON. Skipping update to avoid overwriting user config.'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
} else {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
135
188
|
}
|
|
136
189
|
|
|
137
190
|
// Check which MCP servers need to be added
|
|
@@ -157,15 +210,33 @@ async function updateMCPConfig() {
|
|
|
157
210
|
}
|
|
158
211
|
}
|
|
159
212
|
|
|
160
|
-
|
|
213
|
+
const existingPlugins = Array.isArray(existingConfig.plugin)
|
|
214
|
+
? existingConfig.plugin.filter((entry) => typeof entry === 'string')
|
|
215
|
+
: [];
|
|
216
|
+
const pluginsToAdd = [];
|
|
217
|
+
|
|
218
|
+
for (const plugin of DEFAULT_PLUGINS) {
|
|
219
|
+
if (hasPlugin(existingPlugins, plugin)) {
|
|
220
|
+
if (isInteractive) {
|
|
221
|
+
console.log(` ⏭️ ${plugin}: already exists, skipping`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
pluginsToAdd.push(plugin);
|
|
225
|
+
if (isInteractive) {
|
|
226
|
+
console.log(` ✅ ${plugin}: will be added`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (serversAdded === 0 && pluginsToAdd.length === 0) {
|
|
161
232
|
if (isInteractive) {
|
|
162
|
-
console.log('\n✅
|
|
233
|
+
console.log('\n✅ Required MCP servers and plugins already configured. No changes needed.');
|
|
163
234
|
}
|
|
164
235
|
return;
|
|
165
236
|
}
|
|
166
237
|
|
|
167
238
|
// Backup existing config if it exists
|
|
168
|
-
if (existingConfig && Object.keys(existingConfig).length > 0) {
|
|
239
|
+
if (hasExistingConfigFile && existingConfig && Object.keys(existingConfig).length > 0) {
|
|
169
240
|
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
170
241
|
const backupFile = `${GLOBAL_CONFIG_FILE}.backup.${timestamp}`;
|
|
171
242
|
await fs.writeFile(backupFile, JSON.stringify(existingConfig, null, 2));
|
|
@@ -181,6 +252,7 @@ async function updateMCPConfig() {
|
|
|
181
252
|
...existingMcp,
|
|
182
253
|
...serversToAdd,
|
|
183
254
|
},
|
|
255
|
+
plugin: [...existingPlugins, ...pluginsToAdd],
|
|
184
256
|
};
|
|
185
257
|
|
|
186
258
|
// Write updated config
|
|
@@ -188,11 +260,13 @@ async function updateMCPConfig() {
|
|
|
188
260
|
await fs.writeFile(GLOBAL_CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
|
|
189
261
|
|
|
190
262
|
if (isInteractive) {
|
|
191
|
-
console.log(`\n✅
|
|
263
|
+
console.log(`\n✅ Updated OpenCode config: ${GLOBAL_CONFIG_FILE}`);
|
|
264
|
+
console.log(` Added ${serversAdded} MCP server(s)`);
|
|
265
|
+
console.log(` Added ${pluginsToAdd.length} plugin(s)`);
|
|
192
266
|
if (serversSkipped > 0) {
|
|
193
267
|
console.log(` Skipped ${serversSkipped} existing server(s)`);
|
|
194
268
|
}
|
|
195
|
-
console.log('\n📝 Note: Restart OpenCode to load new
|
|
269
|
+
console.log('\n📝 Note: Restart OpenCode to load new configuration');
|
|
196
270
|
}
|
|
197
271
|
} catch (error) {
|
|
198
272
|
if (isInteractive) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Apply documentation approval gate
|
|
3
|
+
subtask: true
|
|
4
|
+
model: openai/gpt-5.4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Read the latest Gemini documentation review and apply gate exactly:
|
|
8
|
+
|
|
9
|
+
Skill requirements:
|
|
10
|
+
|
|
11
|
+
- ChatGPT gate supervisor must load `ff-context-tracking` before persisting gate decisions.
|
|
12
|
+
|
|
13
|
+
- APPROVED when verdict is `APPROVED` and unresolved documentation issues count is `0`
|
|
14
|
+
- REWORK when verdict is `REWORK_REQUIRED` and iteration < 5
|
|
15
|
+
- ESCALATE when iteration == 5 and still not approved
|
|
16
|
+
|
|
17
|
+
Persist gate decision in pipeline context and output one status line:
|
|
18
|
+
|
|
19
|
+
`DOCUMENTATION_GATE=APPROVED|REWORK|ESCALATE`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Codex documentation implementation pass
|
|
3
|
+
subtask: true
|
|
4
|
+
agent: ff-building-codex
|
|
5
|
+
model: openai/gpt-5.3-codex
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Document the approved code changes and update repository documentation.
|
|
9
|
+
|
|
10
|
+
Before writing docs, load these skills:
|
|
11
|
+
|
|
12
|
+
- `ff-context-tracking`
|
|
13
|
+
- `ff-todo-management`
|
|
14
|
+
- `ff-mini-plan`
|
|
15
|
+
|
|
16
|
+
Requirements:
|
|
17
|
+
|
|
18
|
+
1. Use the latest approved review outputs and implementation artifacts as source of truth.
|
|
19
|
+
2. Update all affected docs so behavior and operational steps match shipped code.
|
|
20
|
+
3. If this is a rework iteration, incorporate Gemini feedback from the previous documentation review.
|
|
21
|
+
4. Summarize what docs were changed and why.
|
|
22
|
+
|
|
23
|
+
Persist to `.feature-factory/agents/ff-documentation-codex-{UUID}-T{N}-iter{I}.md`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Gemini documentation review
|
|
3
|
+
subtask: true
|
|
4
|
+
agent: ff-reviewing-gemini
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Review the latest Codex documentation pass for completeness, correctness, and repository doc consistency.
|
|
8
|
+
|
|
9
|
+
Before reviewing docs, load these skills:
|
|
10
|
+
|
|
11
|
+
- `ff-report-templates`
|
|
12
|
+
- `ff-severity-classification`
|
|
13
|
+
|
|
14
|
+
Required output:
|
|
15
|
+
|
|
16
|
+
1. verdict `APPROVED` or `REWORK_REQUIRED`
|
|
17
|
+
2. unresolved documentation issues (if any)
|
|
18
|
+
3. explicit rework instructions
|
|
19
|
+
4. confidence score (0-100)
|
|
20
|
+
|
|
21
|
+
Persist to `.feature-factory/agents/ff-documentation-gemini-{UUID}-T{N}-iter{I}.md`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run documentation loop after review approval
|
|
3
|
+
subtask: true
|
|
4
|
+
model: openai/gpt-5.4
|
|
5
|
+
loop:
|
|
6
|
+
max: 5
|
|
7
|
+
until: documentation updates are approved by Gemini with zero unresolved documentation issues
|
|
8
|
+
return:
|
|
9
|
+
- /pipeline/documentation/run-codex
|
|
10
|
+
- /pipeline/documentation/run-gemini
|
|
11
|
+
- /pipeline/documentation/gate
|
|
12
|
+
- If `DOCUMENTATION_GATE=REWORK`, continue loop with Gemini feedback for the next Codex pass.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Execute the documentation stage for the current approved implementation.
|
|
16
|
+
|
|
17
|
+
Skill requirements:
|
|
18
|
+
|
|
19
|
+
1. ChatGPT supervisor must load `ff-context-tracking` to track loop state and artifact lineage.
|
|
20
|
+
2. ChatGPT supervisor must load `ff-todo-management` to track documentation actions and rework items per iteration.
|
|
21
|
+
3. Gemini reviewer should use `ff-severity-classification` when reporting documentation issues.
|
|
22
|
+
|
|
23
|
+
Stop criteria:
|
|
24
|
+
|
|
25
|
+
- approve when Gemini confirms documentation is complete, accurate, and repository docs are updated
|
|
26
|
+
- continue loop while unresolved documentation issues exist and iteration < 5
|
|
27
|
+
- escalate to user when iteration reaches 5 without approval
|
|
@@ -6,6 +6,7 @@ return:
|
|
|
6
6
|
- /pipeline/planning/run {loop:5 && until:planning gate is APPROVED or planning gate is BLOCKED and waiting for user confirmation}
|
|
7
7
|
- If planning is BLOCKED or loop max was reached, stop and ask the user whether to continue planning iterations. Otherwise continue.
|
|
8
8
|
- /pipeline/building/run
|
|
9
|
+
- /pipeline/documentation/run
|
|
9
10
|
- /pipeline/complete
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -18,4 +19,4 @@ Execution requirements:
|
|
|
18
19
|
1. Create a pipeline UUID and persist orchestrator state in `.feature-factory/agents/pipeline-{UUID}.md` via `ff-agents-update`.
|
|
19
20
|
2. Keep all phase artifacts in `.feature-factory/plans/` and `.feature-factory/reviews/`.
|
|
20
21
|
3. Use command chaining, parallel fan-out, and loop gates from the `/pipeline/...` command tree.
|
|
21
|
-
4. Do not skip planning and
|
|
22
|
+
4. Do not skip planning, review, and documentation gates.
|
package/dist/agent-config.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
const GLOBAL_OPENCODE_DIR = join(homedir(), '.config', 'opencode');
|
|
5
|
-
const GLOBAL_OPENCODE_CONFIG_PATH = join(GLOBAL_OPENCODE_DIR, 'opencode.json');
|
|
1
|
+
import { isRecord, updateGlobalOpenCodeConfigBlock } from './opencode-global-config.js';
|
|
6
2
|
/**
|
|
7
3
|
* Built-in opencode agents that should be disabled when the Feature Factory
|
|
8
4
|
* plugin is active. These are the standard opencode agents that overlap with
|
|
@@ -42,50 +38,20 @@ export function mergeAgentConfigs(existing, defaults) {
|
|
|
42
38
|
* @param $ - Bun shell instance
|
|
43
39
|
*/
|
|
44
40
|
export async function updateAgentConfig($) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
// Ensure global config directory exists
|
|
63
|
-
try {
|
|
64
|
-
await $ `mkdir -p ${GLOBAL_OPENCODE_DIR}`.quiet();
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Directory might already exist, ignore
|
|
68
|
-
}
|
|
69
|
-
// Backup existing global config if it exists and has content
|
|
70
|
-
if (globalJson && Object.keys(globalJson).length > 0) {
|
|
71
|
-
try {
|
|
72
|
-
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
73
|
-
const backupPath = `${GLOBAL_OPENCODE_CONFIG_PATH}.backup.${timestamp}`;
|
|
74
|
-
const backupContent = JSON.stringify(globalJson, null, 2);
|
|
75
|
-
await $ `echo ${backupContent} > ${backupPath}`.quiet();
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
// Backup failed, but continue anyway
|
|
79
|
-
console.warn('[feature-factory] Could not create backup:', error);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Write updated config to global opencode.json
|
|
83
|
-
const configContent = JSON.stringify(updatedGlobalConfig, null, 2);
|
|
84
|
-
try {
|
|
85
|
-
await $ `echo ${configContent} > ${GLOBAL_OPENCODE_CONFIG_PATH}`.quiet();
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
// Silently fail - don't block if we can't write the config
|
|
89
|
-
console.warn('[feature-factory] Could not update agent config:', error);
|
|
90
|
-
}
|
|
41
|
+
void $;
|
|
42
|
+
await updateGlobalOpenCodeConfigBlock({
|
|
43
|
+
blockName: 'agent',
|
|
44
|
+
warningLabel: 'agent config',
|
|
45
|
+
update: (existingBlock) => {
|
|
46
|
+
const existingAgentConfigs = isRecord(existingBlock)
|
|
47
|
+
? existingBlock
|
|
48
|
+
: undefined;
|
|
49
|
+
const updatedAgentConfigs = mergeAgentConfigs(existingAgentConfigs, DEFAULT_DISABLED_AGENTS);
|
|
50
|
+
const hasChanges = Object.keys(DEFAULT_DISABLED_AGENTS).some((agentName) => !existingAgentConfigs?.[agentName]);
|
|
51
|
+
return {
|
|
52
|
+
nextBlock: updatedAgentConfigs,
|
|
53
|
+
changed: hasChanges,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
91
57
|
}
|
package/dist/mcp-config.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
const GLOBAL_OPENCODE_DIR = join(homedir(), '.config', 'opencode');
|
|
5
|
-
const GLOBAL_OPENCODE_CONFIG_PATH = join(GLOBAL_OPENCODE_DIR, 'opencode.json');
|
|
1
|
+
import { isRecord, updateGlobalOpenCodeConfigBlock } from './opencode-global-config.js';
|
|
6
2
|
/**
|
|
7
3
|
* Default MCP server configuration to be added by the plugin
|
|
8
4
|
* These servers will be merged into the global OpenCode config.
|
|
@@ -54,50 +50,20 @@ export function mergeMCPServers(existing, defaults) {
|
|
|
54
50
|
* @param $ - Bun shell instance
|
|
55
51
|
*/
|
|
56
52
|
export async function updateMCPConfig($) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
// Ensure global config directory exists
|
|
75
|
-
try {
|
|
76
|
-
await $ `mkdir -p ${GLOBAL_OPENCODE_DIR}`.quiet();
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Directory might already exist, ignore
|
|
80
|
-
}
|
|
81
|
-
// Backup existing global config if it exists and has content
|
|
82
|
-
if (globalJson && Object.keys(globalJson).length > 0) {
|
|
83
|
-
try {
|
|
84
|
-
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
85
|
-
const backupPath = `${GLOBAL_OPENCODE_CONFIG_PATH}.backup.${timestamp}`;
|
|
86
|
-
const backupContent = JSON.stringify(globalJson, null, 2);
|
|
87
|
-
await $ `echo ${backupContent} > ${backupPath}`.quiet();
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
// Backup failed, but continue anyway
|
|
91
|
-
console.warn('[feature-factory] Could not create backup:', error);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Write updated config to global opencode.json
|
|
95
|
-
const configContent = JSON.stringify(updatedGlobalConfig, null, 2);
|
|
96
|
-
try {
|
|
97
|
-
await $ `echo ${configContent} > ${GLOBAL_OPENCODE_CONFIG_PATH}`.quiet();
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
// Silently fail - don't block if we can't write the config
|
|
101
|
-
console.warn('[feature-factory] Could not update MCP config:', error);
|
|
102
|
-
}
|
|
53
|
+
void $;
|
|
54
|
+
await updateGlobalOpenCodeConfigBlock({
|
|
55
|
+
blockName: 'mcp',
|
|
56
|
+
warningLabel: 'MCP config',
|
|
57
|
+
update: (existingBlock) => {
|
|
58
|
+
const existingMcpServers = isRecord(existingBlock)
|
|
59
|
+
? existingBlock
|
|
60
|
+
: undefined;
|
|
61
|
+
const updatedMcpServers = mergeMCPServers(existingMcpServers, DEFAULT_MCP_SERVERS);
|
|
62
|
+
const hasChanges = Object.keys(DEFAULT_MCP_SERVERS).some((serverName) => !existingMcpServers?.[serverName]);
|
|
63
|
+
return {
|
|
64
|
+
nextBlock: updatedMcpServers,
|
|
65
|
+
changed: hasChanges,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
103
69
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
export declare function updateGlobalOpenCodeConfigBlock<T>(options: {
|
|
3
|
+
blockName: string;
|
|
4
|
+
warningLabel: string;
|
|
5
|
+
update: (existingBlock: unknown) => {
|
|
6
|
+
nextBlock: T;
|
|
7
|
+
changed: boolean;
|
|
8
|
+
};
|
|
9
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const GLOBAL_OPENCODE_DIR = join(homedir(), '.config', 'opencode');
|
|
5
|
+
const GLOBAL_OPENCODE_CONFIG_PATH = join(GLOBAL_OPENCODE_DIR, 'opencode.json');
|
|
6
|
+
export function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
async function loadGlobalOpenCodeConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const configContent = await readFile(GLOBAL_OPENCODE_CONFIG_PATH, 'utf8');
|
|
12
|
+
const parsed = JSON.parse(configContent);
|
|
13
|
+
if (!isRecord(parsed)) {
|
|
14
|
+
return {
|
|
15
|
+
config: {},
|
|
16
|
+
hasExistingFile: true,
|
|
17
|
+
parseError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
config: parsed,
|
|
22
|
+
hasExistingFile: true,
|
|
23
|
+
parseError: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (typeof error === 'object' &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
'code' in error &&
|
|
30
|
+
error.code === 'ENOENT') {
|
|
31
|
+
return {
|
|
32
|
+
config: {},
|
|
33
|
+
hasExistingFile: false,
|
|
34
|
+
parseError: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof SyntaxError) {
|
|
38
|
+
return {
|
|
39
|
+
config: {},
|
|
40
|
+
hasExistingFile: true,
|
|
41
|
+
parseError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function updateGlobalOpenCodeConfigBlock(options) {
|
|
48
|
+
const { blockName, warningLabel, update } = options;
|
|
49
|
+
const loaded = await loadGlobalOpenCodeConfig();
|
|
50
|
+
if (loaded.parseError) {
|
|
51
|
+
console.warn(`[feature-factory] Could not update ${warningLabel}: existing opencode.json is not valid JSON`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { nextBlock, changed } = update(loaded.config[blockName]);
|
|
55
|
+
if (!changed) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const updatedConfig = {
|
|
59
|
+
...loaded.config,
|
|
60
|
+
[blockName]: nextBlock,
|
|
61
|
+
};
|
|
62
|
+
await mkdir(GLOBAL_OPENCODE_DIR, { recursive: true });
|
|
63
|
+
if (loaded.hasExistingFile && Object.keys(loaded.config).length > 0) {
|
|
64
|
+
try {
|
|
65
|
+
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
66
|
+
const backupPath = `${GLOBAL_OPENCODE_CONFIG_PATH}.backup.${timestamp}`;
|
|
67
|
+
await writeFile(backupPath, JSON.stringify(loaded.config, null, 2));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('[feature-factory] Could not create backup:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await writeFile(GLOBAL_OPENCODE_CONFIG_PATH, JSON.stringify(updatedConfig, null, 2));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.warn(`[feature-factory] Could not update ${warningLabel}:`, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/plugin-config.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
const GLOBAL_OPENCODE_DIR = join(homedir(), '.config', 'opencode');
|
|
5
|
-
const GLOBAL_OPENCODE_CONFIG_PATH = join(GLOBAL_OPENCODE_DIR, 'opencode.json');
|
|
1
|
+
import { updateGlobalOpenCodeConfigBlock } from './opencode-global-config.js';
|
|
6
2
|
/**
|
|
7
3
|
* Default plugins that should be present in the global OpenCode config.
|
|
8
4
|
* These will be merged into the existing plugin array without removing
|
|
@@ -88,53 +84,20 @@ export function mergePlugins(existing, defaults) {
|
|
|
88
84
|
* @param $ - Bun shell instance
|
|
89
85
|
*/
|
|
90
86
|
export async function updatePluginConfig($) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const updatedGlobalConfig = {
|
|
108
|
-
...(globalJson ?? {}),
|
|
109
|
-
plugin: updatedPlugins,
|
|
110
|
-
};
|
|
111
|
-
// Ensure global config directory exists
|
|
112
|
-
try {
|
|
113
|
-
await $ `mkdir -p ${GLOBAL_OPENCODE_DIR}`.quiet();
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
// Directory might already exist, ignore
|
|
117
|
-
}
|
|
118
|
-
// Backup existing global config if it exists and has content
|
|
119
|
-
if (globalJson && Object.keys(globalJson).length > 0) {
|
|
120
|
-
try {
|
|
121
|
-
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
122
|
-
const backupPath = `${GLOBAL_OPENCODE_CONFIG_PATH}.backup.${timestamp}`;
|
|
123
|
-
const backupContent = JSON.stringify(globalJson, null, 2);
|
|
124
|
-
await $ `echo ${backupContent} > ${backupPath}`.quiet();
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
// Backup failed, but continue anyway
|
|
128
|
-
console.warn('[feature-factory] Could not create backup:', error);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// Write updated config to global opencode.json
|
|
132
|
-
const configContent = JSON.stringify(updatedGlobalConfig, null, 2);
|
|
133
|
-
try {
|
|
134
|
-
await $ `echo ${configContent} > ${GLOBAL_OPENCODE_CONFIG_PATH}`.quiet();
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
// Silently fail - don't block if we can't write the config
|
|
138
|
-
console.warn('[feature-factory] Could not update plugin config:', error);
|
|
139
|
-
}
|
|
87
|
+
void $;
|
|
88
|
+
await updateGlobalOpenCodeConfigBlock({
|
|
89
|
+
blockName: 'plugin',
|
|
90
|
+
warningLabel: 'plugin config',
|
|
91
|
+
update: (existingBlock) => {
|
|
92
|
+
const existingPlugins = Array.isArray(existingBlock)
|
|
93
|
+
? existingBlock.filter((entry) => typeof entry === 'string')
|
|
94
|
+
: undefined;
|
|
95
|
+
const existingArray = existingPlugins ?? [];
|
|
96
|
+
const hasChanges = DEFAULT_PLUGINS.some((plugin) => !hasPlugin(existingArray, plugin));
|
|
97
|
+
return {
|
|
98
|
+
nextBlock: mergePlugins(existingPlugins, DEFAULT_PLUGINS),
|
|
99
|
+
changed: hasChanges,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
140
103
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
7
7
|
"license": "MIT",
|