@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 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
@@ -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
- } catch {
134
- // No existing config, will create new
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
- if (serversAdded === 0) {
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✅ All MCP servers already configured. No changes needed.');
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✅ Added ${serversAdded} MCP server(s) to: ${GLOBAL_CONFIG_FILE}`);
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 MCP configuration');
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 review gates.
22
+ 4. Do not skip planning, review, and documentation gates.
@@ -1,8 +1,4 @@
1
- import { readJsonFile } from './quality-gate-config.js';
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
- // Read existing global config
46
- const globalJson = await readJsonFile($, GLOBAL_OPENCODE_CONFIG_PATH);
47
- // Get existing agent configs from global config
48
- const existingAgentConfigs = (globalJson?.agent ?? {});
49
- // Merge with default agent configs
50
- const updatedAgentConfigs = mergeAgentConfigs(existingAgentConfigs, DEFAULT_DISABLED_AGENTS);
51
- // Check if any changes are needed
52
- const hasChanges = Object.keys(DEFAULT_DISABLED_AGENTS).some((agentName) => !existingAgentConfigs[agentName]);
53
- if (!hasChanges) {
54
- // All default agent configs already exist, no need to update
55
- return;
56
- }
57
- // Prepare updated global config
58
- const updatedGlobalConfig = {
59
- ...(globalJson ?? {}),
60
- agent: updatedAgentConfigs,
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
  }
@@ -1,8 +1,4 @@
1
- import { readJsonFile } from './quality-gate-config.js';
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
- // Read existing global config
58
- const globalJson = await readJsonFile($, GLOBAL_OPENCODE_CONFIG_PATH);
59
- // Get existing MCP servers from global config
60
- const existingMcpServers = (globalJson?.mcp ?? {});
61
- // Merge with default MCP servers
62
- const updatedMcpServers = mergeMCPServers(existingMcpServers, DEFAULT_MCP_SERVERS);
63
- // Check if any changes are needed
64
- const hasChanges = Object.keys(DEFAULT_MCP_SERVERS).some((serverName) => !existingMcpServers[serverName]);
65
- if (!hasChanges) {
66
- // All default servers already exist, no need to update
67
- return;
68
- }
69
- // Prepare updated global config
70
- const updatedGlobalConfig = {
71
- ...(globalJson ?? {}),
72
- mcp: updatedMcpServers,
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
+ }
@@ -1,8 +1,4 @@
1
- import { readJsonFile } from './quality-gate-config.js';
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
- // Read existing global config
92
- const globalJson = await readJsonFile($, GLOBAL_OPENCODE_CONFIG_PATH);
93
- // Get existing plugins from global config
94
- const existingPlugins = Array.isArray(globalJson?.plugin)
95
- ? globalJson.plugin
96
- : undefined;
97
- // Check if any changes are needed (by base name)
98
- const existingArray = existingPlugins ?? [];
99
- const hasChanges = DEFAULT_PLUGINS.some((plugin) => !hasPlugin(existingArray, plugin));
100
- if (!hasChanges) {
101
- // All default plugins already present (possibly with pinned versions)
102
- return;
103
- }
104
- // Merge with default plugins
105
- const updatedPlugins = mergePlugins(existingPlugins, DEFAULT_PLUGINS);
106
- // Prepare updated global config
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.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",