@syntesseraai/opencode-feature-factory 0.2.14 → 0.2.15

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.
@@ -1,4 +1,4 @@
1
- import { tool } from '@opencode-ai/plugin';
1
+ import { tool } from '@opencode-ai/plugin/tool';
2
2
  import { listActiveAgents, findAgentFiles, findAgentFilesById, findAllAgentFiles, readAgentContextById, } from './agent-context.js';
3
3
  /**
4
4
  * Create agent management tools for the plugin
@@ -18,6 +18,34 @@ async function log(client, level, message, extra) {
18
18
  // Logging failure should not affect plugin operation
19
19
  }
20
20
  }
21
+ /**
22
+ * Execute a shell command with a timeout to prevent hangs
23
+ */
24
+ async function executeWithTimeout(operation, timeoutMs = 5000, operationName = 'shell command') {
25
+ const timeoutPromise = new Promise((_, reject) => {
26
+ setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs);
27
+ });
28
+ try {
29
+ return await Promise.race([operation(), timeoutPromise]);
30
+ }
31
+ catch (error) {
32
+ // Return null on timeout or error
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Validate that a directory path is safe to use
38
+ */
39
+ function isValidDirectory(directory) {
40
+ if (!directory || typeof directory !== 'string')
41
+ return false;
42
+ if (directory === '' || directory === '/')
43
+ return false;
44
+ // Check for common invalid patterns
45
+ if (directory.includes('\0') || directory.includes('..'))
46
+ return false;
47
+ return true;
48
+ }
21
49
  /**
22
50
  * Initialize the Feature Factory directory structure.
23
51
  * Creates .feature-factory/ and .feature-factory/agents/ directories.
@@ -25,11 +53,24 @@ async function log(client, level, message, extra) {
25
53
  */
26
54
  export async function initializeFeatureFactory(input, $) {
27
55
  const { directory, client } = input;
56
+ // Validate directory before attempting any operations
57
+ if (!isValidDirectory(directory)) {
58
+ await log(client, 'debug', 'feature-factory-init-skipped-invalid-directory', {
59
+ directory: String(directory),
60
+ });
61
+ return;
62
+ }
28
63
  const featureFactoryDir = `${directory}/.feature-factory`;
29
64
  const agentsDir = `${featureFactoryDir}/agents`;
30
- // Create directories if they don't exist
65
+ // Create directories if they don't exist (with timeout protection)
31
66
  try {
32
- await $ `mkdir -p ${agentsDir}`.quiet();
67
+ const result = await executeWithTimeout(async () => await $ `mkdir -p ${agentsDir}`.quiet(), 3000, 'mkdir -p');
68
+ if (result === null) {
69
+ await log(client, 'warn', 'feature-factory-directory-creation-timeout', {
70
+ agentsDir,
71
+ });
72
+ return; // Exit early if we can't create directories
73
+ }
33
74
  await log(client, 'debug', 'feature-factory-directories-created', {
34
75
  featureFactoryDir,
35
76
  agentsDir,
@@ -41,44 +82,70 @@ export async function initializeFeatureFactory(input, $) {
41
82
  });
42
83
  // Continue even if directory creation fails - it might already exist
43
84
  }
44
- // Check for CI script migration
85
+ // Check for CI script migration (with timeout protection)
45
86
  const oldCiPath = `${directory}/management/ci.sh`;
46
87
  const newCiPath = `${featureFactoryDir}/ci.sh`;
47
88
  try {
48
- // Check if old CI exists
49
- await $ `test -f ${oldCiPath}`.quiet();
50
- // Old exists, check if new already exists
51
- try {
89
+ // Check if old CI exists (with timeout)
90
+ const oldExists = await executeWithTimeout(async () => {
91
+ await $ `test -f ${oldCiPath}`.quiet();
92
+ return true;
93
+ }, 2000, 'test -f old-ci');
94
+ if (oldExists !== true) {
95
+ // Old doesn't exist, nothing to migrate
96
+ await log(client, 'debug', 'ci-sh-no-migration-needed', {
97
+ oldPath: oldCiPath,
98
+ });
99
+ return;
100
+ }
101
+ // Old exists, check if new already exists (with timeout)
102
+ const newExists = await executeWithTimeout(async () => {
52
103
  await $ `test -f ${newCiPath}`.quiet();
104
+ return true;
105
+ }, 2000, 'test -f new-ci');
106
+ if (newExists === true) {
53
107
  // Both exist - migration already done or user has both
54
108
  await log(client, 'debug', 'ci-sh-both-locations-exist', {
55
109
  oldPath: oldCiPath,
56
110
  newPath: newCiPath,
57
111
  });
112
+ return;
58
113
  }
59
- catch {
60
- // Old exists, new doesn't - migrate
61
- try {
62
- await $ `cp ${oldCiPath} ${newCiPath}`.quiet();
114
+ // Old exists, new doesn't - migrate (with timeout)
115
+ const copied = await executeWithTimeout(async () => {
116
+ await $ `cp ${oldCiPath} ${newCiPath}`.quiet();
117
+ return true;
118
+ }, 3000, 'cp ci.sh');
119
+ if (copied === true) {
120
+ const removed = await executeWithTimeout(async () => {
63
121
  await $ `rm ${oldCiPath}`.quiet();
122
+ return true;
123
+ }, 2000, 'rm old-ci');
124
+ if (removed === true) {
64
125
  await log(client, 'info', 'ci-sh-migrated', {
65
126
  from: oldCiPath,
66
127
  to: newCiPath,
67
128
  });
68
129
  }
69
- catch (migrateError) {
70
- await log(client, 'error', 'ci-sh-migration-failed', {
130
+ else {
131
+ await log(client, 'warn', 'ci-sh-migration-partial', {
71
132
  from: oldCiPath,
72
133
  to: newCiPath,
73
- error: String(migrateError),
134
+ reason: 'copied but failed to remove old file',
74
135
  });
75
136
  }
76
137
  }
138
+ else {
139
+ await log(client, 'error', 'ci-sh-migration-failed', {
140
+ from: oldCiPath,
141
+ to: newCiPath,
142
+ reason: 'copy operation timed out or failed',
143
+ });
144
+ }
77
145
  }
78
- catch {
79
- // Old doesn't exist, nothing to migrate
80
- await log(client, 'debug', 'ci-sh-no-migration-needed', {
81
- oldPath: oldCiPath,
146
+ catch (error) {
147
+ await log(client, 'error', 'ci-sh-migration-error', {
148
+ error: String(error),
82
149
  });
83
150
  }
84
151
  }
package/dist/index.js CHANGED
@@ -2,6 +2,21 @@ import { createQualityGateHooks } from './stop-quality-gate.js';
2
2
  import { initializeFeatureFactory } from './feature-factory-setup.js';
3
3
  import { createAgentManagementTools } from './agent-management-tools.js';
4
4
  const SERVICE_NAME = 'feature-factory';
5
+ const PLUGIN_INIT_TIMEOUT_MS = 10000; // 10 seconds max for plugin initialization
6
+ /**
7
+ * Wrap an async operation with a timeout to prevent hangs
8
+ */
9
+ async function withTimeout(operation, timeoutMs, _context) {
10
+ const timeoutPromise = new Promise((_, reject) => {
11
+ setTimeout(() => reject(new Error(`timeout`)), timeoutMs);
12
+ });
13
+ try {
14
+ return await Promise.race([operation(), timeoutPromise]);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
5
20
  /**
6
21
  * Log a message using the OpenCode client's structured logging.
7
22
  * Silently fails if logging is unavailable.
@@ -56,26 +71,39 @@ export const StopQualityGatePlugin = async (input) => {
56
71
  if (!rootDir || rootDir === '' || rootDir === '/') {
57
72
  return {};
58
73
  }
59
- // Initialize Feature Factory directory structure
74
+ // Initialize Feature Factory directory structure (with timeout protection)
60
75
  try {
61
- await initializeFeatureFactory(input, $);
76
+ const initResult = await withTimeout(async () => await initializeFeatureFactory(input, $), PLUGIN_INIT_TIMEOUT_MS, 'feature-factory-init');
77
+ if (initResult === null) {
78
+ await log(client, 'warn', 'feature-factory-init-timeout', {
79
+ timeoutMs: PLUGIN_INIT_TIMEOUT_MS,
80
+ });
81
+ }
62
82
  }
63
83
  catch (error) {
64
84
  await log(client, 'error', 'feature-factory-init-error', {
65
85
  error: String(error),
66
86
  });
67
87
  }
68
- // Create quality gate hooks
88
+ // Create quality gate hooks (with timeout protection)
69
89
  let qualityGateHooks = {};
70
90
  try {
71
- qualityGateHooks = await createQualityGateHooks(input);
91
+ const hooksResult = await withTimeout(async () => await createQualityGateHooks(input), PLUGIN_INIT_TIMEOUT_MS, 'quality-gate-init');
92
+ if (hooksResult !== null) {
93
+ qualityGateHooks = hooksResult;
94
+ }
95
+ else {
96
+ await log(client, 'warn', 'quality-gate-init-timeout', {
97
+ timeoutMs: PLUGIN_INIT_TIMEOUT_MS,
98
+ });
99
+ }
72
100
  }
73
101
  catch (error) {
74
102
  await log(client, 'error', 'quality-gate.init-error', {
75
103
  error: String(error),
76
104
  });
77
105
  }
78
- // Create agent management tools
106
+ // Create agent management tools (synchronous, minimal timeout for safety)
79
107
  let agentManagementHooks = {};
80
108
  try {
81
109
  agentManagementHooks = createAgentManagementTools(input);
@@ -154,12 +154,30 @@ async function log(client, level, message, extra) {
154
154
  return undefined;
155
155
  }
156
156
  }
157
+ /**
158
+ * Execute a shell command with a timeout to prevent hangs
159
+ */
160
+ async function executeWithTimeout(operation, timeoutMs = 5000, _operationName = 'shell command') {
161
+ const timeoutPromise = new Promise((_, reject) => {
162
+ setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
163
+ });
164
+ try {
165
+ return await Promise.race([operation(), timeoutPromise]);
166
+ }
167
+ catch {
168
+ // Return null on timeout or error
169
+ return null;
170
+ }
171
+ }
157
172
  export async function createQualityGateHooks(input) {
158
173
  const { client, $, directory } = input;
159
174
  async function ciShExists() {
160
175
  try {
161
- await $ `test -f ${directory}/.feature-factory/ci.sh`.quiet();
162
- return true;
176
+ const result = await executeWithTimeout(async () => {
177
+ await $ `test -f ${directory}/.feature-factory/ci.sh`.quiet();
178
+ return true;
179
+ }, 2000, 'test -f ci.sh');
180
+ return result === true;
163
181
  }
164
182
  catch {
165
183
  return false;
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.2.14",
4
+ "version": "0.2.15",
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",