@syntesseraai/opencode-feature-factory 0.6.4 → 0.6.5

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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
2
2
  import { updateMCPConfig } from './mcp-config.js';
3
3
  import { updateAgentConfig } from './agent-config.js';
4
+ import { updatePluginConfig } from './plugin-config.js';
4
5
  import { $ } from 'bun';
5
6
  // Import tool creator functions
6
7
  import { createFFAgentsCurrentTool } from './plugins/ff-agents-current-plugin.js';
@@ -53,6 +54,14 @@ export const FeatureFactoryPlugin = async (input) => {
53
54
  catch {
54
55
  console.error('Failed to update agent config in OpenCode plugin');
55
56
  }
57
+ // Update plugin list in global OpenCode config
58
+ // This ensures required companion plugins are present
59
+ try {
60
+ await updatePluginConfig($);
61
+ }
62
+ catch {
63
+ console.error('Failed to update plugin config in OpenCode plugin');
64
+ }
56
65
  // Load hooks from the quality gate plugin
57
66
  const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
58
67
  // Create all tools
@@ -0,0 +1,49 @@
1
+ type BunShell = any;
2
+ /**
3
+ * Default plugins that should be present in the global OpenCode config.
4
+ * These will be merged into the existing plugin array without removing
5
+ * any user-added entries.
6
+ */
7
+ export declare const DEFAULT_PLUGINS: readonly string[];
8
+ /**
9
+ * Extract the base package name from a plugin specifier, stripping the
10
+ * version suffix. Handles both scoped (`@scope/name@version`) and
11
+ * unscoped (`name@version`) packages.
12
+ *
13
+ * Examples:
14
+ * "@scope/pkg@latest" → "@scope/pkg"
15
+ * "@scope/pkg@1.2.3" → "@scope/pkg"
16
+ * "pkg@latest" → "pkg"
17
+ * "pkg" → "pkg"
18
+ */
19
+ export declare function getPackageBaseName(plugin: string): string;
20
+ /**
21
+ * Check whether a plugin (by base name) is already present in the array,
22
+ * regardless of the version suffix. If a user has pinned a specific version
23
+ * (e.g. `@scope/pkg@1.2.3`), we treat the package as already present and
24
+ * do NOT add the `@latest` variant.
25
+ */
26
+ export declare function hasPlugin(existing: string[], plugin: string): boolean;
27
+ /**
28
+ * Merge plugin arrays, preserving existing entries and appending new ones.
29
+ * Existing entries are never removed or reordered.
30
+ *
31
+ * Matching is done by base package name (without version suffix), so a
32
+ * user-pinned `@scope/pkg@1.0.0` will prevent `@scope/pkg@latest` from
33
+ * being added.
34
+ */
35
+ export declare function mergePlugins(existing: string[] | undefined, defaults: readonly string[]): string[];
36
+ /**
37
+ * Update the plugin list in global opencode.json.
38
+ *
39
+ * This function:
40
+ * 1. Reads existing config from ~/.config/opencode/opencode.json
41
+ * 2. Preserves all existing plugins in the array
42
+ * 3. Appends default Feature Factory plugins that aren't already present
43
+ * (matched by base package name, so pinned versions are respected)
44
+ * 4. Writes updated config back to ~/.config/opencode/opencode.json
45
+ *
46
+ * @param $ - Bun shell instance
47
+ */
48
+ export declare function updatePluginConfig($: BunShell): Promise<void>;
49
+ export {};
@@ -0,0 +1,140 @@
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');
6
+ /**
7
+ * Default plugins that should be present in the global OpenCode config.
8
+ * These will be merged into the existing plugin array without removing
9
+ * any user-added entries.
10
+ */
11
+ export const DEFAULT_PLUGINS = [
12
+ '@syntesseraai/opencode-feature-factory@latest',
13
+ '@nick-vi/opencode-type-inject@latest',
14
+ '@franlol/opencode-md-table-formatter@latest',
15
+ '@spoons-and-mirrors/subtask2@latest',
16
+ 'opencode-pty@latest',
17
+ '@angdrew/opencode-hashline-plugin@latest',
18
+ ];
19
+ /**
20
+ * Extract the base package name from a plugin specifier, stripping the
21
+ * version suffix. Handles both scoped (`@scope/name@version`) and
22
+ * unscoped (`name@version`) packages.
23
+ *
24
+ * Examples:
25
+ * "@scope/pkg@latest" → "@scope/pkg"
26
+ * "@scope/pkg@1.2.3" → "@scope/pkg"
27
+ * "pkg@latest" → "pkg"
28
+ * "pkg" → "pkg"
29
+ */
30
+ export function getPackageBaseName(plugin) {
31
+ if (plugin.startsWith('@')) {
32
+ // Scoped package: the version delimiter is the '@' after the '/'
33
+ const slashIndex = plugin.indexOf('/');
34
+ if (slashIndex !== -1) {
35
+ const versionAt = plugin.indexOf('@', slashIndex + 1);
36
+ if (versionAt !== -1) {
37
+ return plugin.substring(0, versionAt);
38
+ }
39
+ }
40
+ // No slash or no version suffix — return as-is
41
+ return plugin;
42
+ }
43
+ // Unscoped package: version delimiter is the first '@'
44
+ const atIndex = plugin.indexOf('@');
45
+ if (atIndex !== -1) {
46
+ return plugin.substring(0, atIndex);
47
+ }
48
+ return plugin;
49
+ }
50
+ /**
51
+ * Check whether a plugin (by base name) is already present in the array,
52
+ * regardless of the version suffix. If a user has pinned a specific version
53
+ * (e.g. `@scope/pkg@1.2.3`), we treat the package as already present and
54
+ * do NOT add the `@latest` variant.
55
+ */
56
+ export function hasPlugin(existing, plugin) {
57
+ const baseName = getPackageBaseName(plugin);
58
+ return existing.some((entry) => getPackageBaseName(entry) === baseName);
59
+ }
60
+ /**
61
+ * Merge plugin arrays, preserving existing entries and appending new ones.
62
+ * Existing entries are never removed or reordered.
63
+ *
64
+ * Matching is done by base package name (without version suffix), so a
65
+ * user-pinned `@scope/pkg@1.0.0` will prevent `@scope/pkg@latest` from
66
+ * being added.
67
+ */
68
+ export function mergePlugins(existing, defaults) {
69
+ const existingPlugins = existing ?? [];
70
+ const result = [...existingPlugins];
71
+ for (const plugin of defaults) {
72
+ if (!hasPlugin(result, plugin)) {
73
+ result.push(plugin);
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Update the plugin list in global opencode.json.
80
+ *
81
+ * This function:
82
+ * 1. Reads existing config from ~/.config/opencode/opencode.json
83
+ * 2. Preserves all existing plugins in the array
84
+ * 3. Appends default Feature Factory plugins that aren't already present
85
+ * (matched by base package name, so pinned versions are respected)
86
+ * 4. Writes updated config back to ~/.config/opencode/opencode.json
87
+ *
88
+ * @param $ - Bun shell instance
89
+ */
90
+ 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
+ }
140
+ }
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",
4
+ "version": "0.6.5",
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",