@hyperdrive.bot/bmad-workflow 1.0.2
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/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- package/package.json +119 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeminiAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Gemini CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from Claude: Gemini CLI doesn't support @file references natively,
|
|
8
|
+
* so this runner preprocesses prompts to inline file content before execution.
|
|
9
|
+
*/
|
|
10
|
+
import { exec } from 'node:child_process';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
import { PROVIDER_CONFIGS } from '../../models/provider.js';
|
|
16
|
+
/**
|
|
17
|
+
* Track active child processes for cleanup on SIGINT
|
|
18
|
+
*/
|
|
19
|
+
const activeProcesses = new Set();
|
|
20
|
+
/**
|
|
21
|
+
* Track if SIGINT handler has been registered
|
|
22
|
+
*/
|
|
23
|
+
let sigintHandlerRegistered = false;
|
|
24
|
+
/**
|
|
25
|
+
* Register SIGINT handler for graceful process cleanup
|
|
26
|
+
*/
|
|
27
|
+
const registerSigintHandler = (logger) => {
|
|
28
|
+
if (sigintHandlerRegistered)
|
|
29
|
+
return;
|
|
30
|
+
process.on('SIGINT', () => {
|
|
31
|
+
logger.info({
|
|
32
|
+
activeProcessCount: activeProcesses.size,
|
|
33
|
+
}, 'Received SIGINT, cleaning up active Gemini processes');
|
|
34
|
+
// Kill all active child processes
|
|
35
|
+
for (const childProcess of activeProcesses) {
|
|
36
|
+
try {
|
|
37
|
+
childProcess.kill('SIGTERM');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
logger.error({
|
|
41
|
+
error: error.message,
|
|
42
|
+
}, 'Error killing child process');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Clear the set
|
|
46
|
+
activeProcesses.clear();
|
|
47
|
+
// Exit the main process
|
|
48
|
+
process.exit(130); // 130 = 128 + SIGINT signal number (2)
|
|
49
|
+
});
|
|
50
|
+
sigintHandlerRegistered = true;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Regex to match @file references in prompts
|
|
54
|
+
* Matches: @path/to/file.md, @./relative/path.ts, @../parent/file.yaml
|
|
55
|
+
*/
|
|
56
|
+
const FILE_REFERENCE_REGEX = /@(\.{0,2}[\w./-]+\.\w+)/g;
|
|
57
|
+
/**
|
|
58
|
+
* GeminiAgentRunner service for executing Gemini AI agents
|
|
59
|
+
*
|
|
60
|
+
* Spawns Gemini CLI processes to execute AI agents with specified prompts.
|
|
61
|
+
* Preprocesses @file references to inline file content since Gemini CLI
|
|
62
|
+
* doesn't support native file references like Claude.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
67
|
+
* const runner = new GeminiAgentRunner(logger)
|
|
68
|
+
* const result = await runner.runAgent({
|
|
69
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
70
|
+
* agentType: 'architect',
|
|
71
|
+
* timeout: 300000
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class GeminiAgentRunner {
|
|
76
|
+
provider = 'gemini';
|
|
77
|
+
config = PROVIDER_CONFIGS.gemini;
|
|
78
|
+
logger;
|
|
79
|
+
/**
|
|
80
|
+
* Create a new GeminiAgentRunner instance
|
|
81
|
+
*
|
|
82
|
+
* @param logger - Logger instance for structured logging
|
|
83
|
+
*/
|
|
84
|
+
constructor(logger) {
|
|
85
|
+
this.logger = logger;
|
|
86
|
+
registerSigintHandler(logger);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the count of active processes
|
|
90
|
+
* (Useful for testing and monitoring purposes)
|
|
91
|
+
*
|
|
92
|
+
* @returns Number of currently active child processes
|
|
93
|
+
*/
|
|
94
|
+
getActiveProcessCount() {
|
|
95
|
+
return activeProcesses.size;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Execute a Gemini AI agent with the specified prompt and options
|
|
99
|
+
*
|
|
100
|
+
* This method spawns a Gemini CLI process, captures output, handles errors,
|
|
101
|
+
* and enforces timeouts. It preprocesses @file references to inline content
|
|
102
|
+
* since Gemini CLI doesn't support native file references.
|
|
103
|
+
*
|
|
104
|
+
* @param prompt - The prompt to execute with the agent
|
|
105
|
+
* @param options - Agent execution options (without prompt)
|
|
106
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
107
|
+
*/
|
|
108
|
+
async runAgent(prompt, options) {
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
const timeout = options.timeout ?? 1_800_000; // Default 30 minutes
|
|
111
|
+
// Input validation
|
|
112
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
113
|
+
return this.returnWithCallback({
|
|
114
|
+
agentType: options.agentType,
|
|
115
|
+
duration: Date.now() - startTime,
|
|
116
|
+
errors: 'Prompt is required and cannot be empty',
|
|
117
|
+
exitCode: -1,
|
|
118
|
+
output: '',
|
|
119
|
+
success: false,
|
|
120
|
+
}, options.onResponse);
|
|
121
|
+
}
|
|
122
|
+
// Preprocess prompt to inline @file references
|
|
123
|
+
const processedPrompt = this.preprocessPrompt(prompt);
|
|
124
|
+
// Log execution start with metadata
|
|
125
|
+
this.logger.info({
|
|
126
|
+
agentType: options.agentType,
|
|
127
|
+
originalPromptLength: prompt.length,
|
|
128
|
+
processedPromptLength: processedPrompt.length,
|
|
129
|
+
references: options.references?.length ?? 0,
|
|
130
|
+
timeout,
|
|
131
|
+
}, 'Executing Gemini agent');
|
|
132
|
+
// Invoke onPrompt callback if provided
|
|
133
|
+
if (options.onPrompt) {
|
|
134
|
+
try {
|
|
135
|
+
// Create options object without callbacks for the callback
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
137
|
+
const { onPrompt, onResponse, ...callbackOptions } = options;
|
|
138
|
+
await onPrompt(processedPrompt, callbackOptions);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.logger.warn({ error: error.message }, 'Error in onPrompt callback');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Build full command string
|
|
145
|
+
// Escape double quotes and backticks in the prompt
|
|
146
|
+
const escapedPrompt = processedPrompt
|
|
147
|
+
.replaceAll('\\', String.raw `\\`)
|
|
148
|
+
.replaceAll('"', String.raw `\"`)
|
|
149
|
+
.replaceAll('`', String.raw `\``)
|
|
150
|
+
.replaceAll('$', String.raw `\$`);
|
|
151
|
+
// Build command with provider config flags
|
|
152
|
+
// Gemini uses -p flag which expects the prompt as the next argument
|
|
153
|
+
const command = `${this.config.command} -p "${escapedPrompt}" --yolo < /dev/null`;
|
|
154
|
+
// Log the command being executed (truncated for readability)
|
|
155
|
+
this.logger.info({
|
|
156
|
+
commandLength: command.length,
|
|
157
|
+
promptLength: processedPrompt.length,
|
|
158
|
+
}, 'Executing Gemini command');
|
|
159
|
+
let stdoutData = '';
|
|
160
|
+
let stderrData = '';
|
|
161
|
+
try {
|
|
162
|
+
// Use exec instead of spawn for better shell compatibility
|
|
163
|
+
const { stderr, stdout } = await execAsync(command, {
|
|
164
|
+
env: process.env,
|
|
165
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
166
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
167
|
+
timeout,
|
|
168
|
+
});
|
|
169
|
+
stdoutData = stdout;
|
|
170
|
+
stderrData = stderr;
|
|
171
|
+
const duration = Date.now() - startTime;
|
|
172
|
+
this.logger.info({
|
|
173
|
+
duration,
|
|
174
|
+
stderrLength: stderrData.length,
|
|
175
|
+
stdoutLength: stdoutData.length,
|
|
176
|
+
}, 'Gemini CLI process completed successfully');
|
|
177
|
+
const result = {
|
|
178
|
+
agentType: options.agentType,
|
|
179
|
+
duration,
|
|
180
|
+
errors: stderrData,
|
|
181
|
+
exitCode: 0,
|
|
182
|
+
output: stdoutData,
|
|
183
|
+
success: true,
|
|
184
|
+
};
|
|
185
|
+
return this.returnWithCallback(result, options.onResponse);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// Handle exec errors (includes timeout, non-zero exit, etc.)
|
|
189
|
+
const duration = Date.now() - startTime;
|
|
190
|
+
// Extract stdout/stderr from exec error if available
|
|
191
|
+
const execError = error;
|
|
192
|
+
if (execError.stdout)
|
|
193
|
+
stdoutData = execError.stdout;
|
|
194
|
+
if (execError.stderr)
|
|
195
|
+
stderrData = execError.stderr;
|
|
196
|
+
const exitCode = execError.code ?? -1;
|
|
197
|
+
const isTimeout = execError.killed === true && execError.signal === 'SIGTERM';
|
|
198
|
+
const wasKilled = execError.killed === true;
|
|
199
|
+
const signal = execError.signal ?? null;
|
|
200
|
+
// Build detailed error context for debugging
|
|
201
|
+
const errorContext = this.buildErrorContext({
|
|
202
|
+
cmd: execError.cmd,
|
|
203
|
+
duration,
|
|
204
|
+
exitCode,
|
|
205
|
+
isTimeout,
|
|
206
|
+
signal,
|
|
207
|
+
stderrData,
|
|
208
|
+
stdoutData,
|
|
209
|
+
wasKilled,
|
|
210
|
+
});
|
|
211
|
+
this.logger.error({
|
|
212
|
+
cmd: execError.cmd,
|
|
213
|
+
duration,
|
|
214
|
+
error: execError.message,
|
|
215
|
+
exitCode,
|
|
216
|
+
isTimeout,
|
|
217
|
+
signal,
|
|
218
|
+
stderrLength: stderrData.length,
|
|
219
|
+
stdoutLength: stdoutData.length,
|
|
220
|
+
wasKilled,
|
|
221
|
+
}, isTimeout ? 'Gemini CLI process timeout' : 'Gemini CLI process error');
|
|
222
|
+
return this.returnWithCallback({
|
|
223
|
+
agentType: options.agentType,
|
|
224
|
+
duration,
|
|
225
|
+
errors: errorContext,
|
|
226
|
+
exitCode: isTimeout ? 124 : exitCode,
|
|
227
|
+
output: stdoutData,
|
|
228
|
+
success: false,
|
|
229
|
+
}, options.onResponse);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Build detailed error context for debugging process failures
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
buildErrorContext(info) {
|
|
237
|
+
const lines = [];
|
|
238
|
+
// Determine the type of failure
|
|
239
|
+
if (info.isTimeout) {
|
|
240
|
+
lines.push(`Process timeout after ${info.duration}ms`);
|
|
241
|
+
}
|
|
242
|
+
else if (info.wasKilled) {
|
|
243
|
+
lines.push('Process was killed externally');
|
|
244
|
+
// Explain common exit codes
|
|
245
|
+
switch (info.exitCode) {
|
|
246
|
+
case 130: {
|
|
247
|
+
lines.push('Exit code 130 = SIGINT (user interrupt, Ctrl+C)');
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 137: {
|
|
251
|
+
lines.push('Exit code 137 = SIGKILL (process forcefully killed)', 'Possible causes:', ' - System OOM killer', ' - Exceeded memory limits', ' - Docker/container killed the process');
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 143: {
|
|
255
|
+
lines.push('Exit code 143 = SIGTERM (process terminated)', 'Possible causes:', ' - System OOM killer terminated the process', ' - Another process sent SIGTERM', ' - Parent process cleanup', ' - Docker/container resource limits');
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
// No default
|
|
259
|
+
}
|
|
260
|
+
if (info.signal) {
|
|
261
|
+
lines.push(`Signal received: ${info.signal}`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lines.push('No signal information available (signal: null)');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
lines.push(`Process exited with code ${info.exitCode}`);
|
|
269
|
+
}
|
|
270
|
+
// Add duration context
|
|
271
|
+
lines.push(`Duration: ${(info.duration / 1000).toFixed(2)}s`);
|
|
272
|
+
// Add output context
|
|
273
|
+
if (info.stdoutData.length > 0) {
|
|
274
|
+
lines.push(`Stdout (${info.stdoutData.length} chars):`);
|
|
275
|
+
// Truncate if too long, show last 500 chars which are often most relevant
|
|
276
|
+
const truncatedStdout = info.stdoutData.length > 500 ? `...[truncated]...\n${info.stdoutData.slice(-500)}` : info.stdoutData;
|
|
277
|
+
lines.push(truncatedStdout);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
lines.push('Stdout: (empty - no output captured)');
|
|
281
|
+
}
|
|
282
|
+
if (info.stderrData.length > 0) {
|
|
283
|
+
lines.push(`Stderr (${info.stderrData.length} chars):`);
|
|
284
|
+
const truncatedStderr = info.stderrData.length > 500 ? `...[truncated]...\n${info.stderrData.slice(-500)}` : info.stderrData;
|
|
285
|
+
lines.push(truncatedStderr);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
lines.push('Stderr: (empty - no error output)');
|
|
289
|
+
}
|
|
290
|
+
// Add command for reference (truncated if too long)
|
|
291
|
+
if (info.cmd) {
|
|
292
|
+
const truncatedCmd = info.cmd.length > 200 ? `${info.cmd.slice(0, 200)}...[truncated]` : info.cmd;
|
|
293
|
+
lines.push(`Command: ${truncatedCmd}`);
|
|
294
|
+
}
|
|
295
|
+
// Add troubleshooting suggestions for empty output
|
|
296
|
+
if (info.stdoutData.length === 0 && info.stderrData.length === 0) {
|
|
297
|
+
lines.push('', 'Troubleshooting (no output captured):', ' 1. Check system resources (memory, CPU)', ' 2. Verify Gemini CLI is properly installed: `gemini --version`', ' 3. Check for Gemini API issues', ' 4. Review system logs: `dmesg | tail -50` (for OOM killer)', ' 5. Try running the command manually to see output');
|
|
298
|
+
}
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Preprocess prompt to inline @file references
|
|
303
|
+
*
|
|
304
|
+
* Since Gemini CLI doesn't support @file syntax natively, this method
|
|
305
|
+
* finds all @file references and replaces them with the actual file content.
|
|
306
|
+
*
|
|
307
|
+
* @param prompt - The original prompt with @file references
|
|
308
|
+
* @param basePath - Base path for resolving relative file paths
|
|
309
|
+
* @returns Preprocessed prompt with inlined file content
|
|
310
|
+
*/
|
|
311
|
+
preprocessPrompt(prompt, basePath = process.cwd()) {
|
|
312
|
+
const inlinedFiles = [];
|
|
313
|
+
const processedPrompt = prompt.replaceAll(FILE_REFERENCE_REGEX, (match, filePath) => {
|
|
314
|
+
const absolutePath = resolve(basePath, filePath);
|
|
315
|
+
if (!existsSync(absolutePath)) {
|
|
316
|
+
this.logger.warn({ absolutePath, filePath }, 'File reference not found, keeping original reference');
|
|
317
|
+
return match; // Keep original @file reference if file doesn't exist
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const content = readFileSync(absolutePath, 'utf8');
|
|
321
|
+
inlinedFiles.push(filePath);
|
|
322
|
+
// Return the file content wrapped in a clear delimiter
|
|
323
|
+
return `\n--- BEGIN FILE: ${filePath} ---\n${content}\n--- END FILE: ${filePath} ---\n`;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
this.logger.error({ absolutePath, error: error.message, filePath }, 'Error reading file for inlining');
|
|
327
|
+
return match; // Keep original reference on error
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
if (inlinedFiles.length > 0) {
|
|
331
|
+
this.logger.info({ fileCount: inlinedFiles.length, files: inlinedFiles }, 'Inlined file references for Gemini CLI');
|
|
332
|
+
}
|
|
333
|
+
return processedPrompt;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Invoke onResponse callback and return result
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
async returnWithCallback(result, onResponse) {
|
|
340
|
+
if (onResponse) {
|
|
341
|
+
try {
|
|
342
|
+
await onResponse(result);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
this.logger.warn({ error: error.message }, 'Error in onResponse callback');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileManager Service
|
|
3
|
+
*
|
|
4
|
+
* Provides safe file system operations with error handling and logging.
|
|
5
|
+
* All file operations throughout the CLI should use this service for consistency and testability.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
/**
|
|
9
|
+
* FileManager service for all file system operations
|
|
10
|
+
*
|
|
11
|
+
* Abstracts file system operations behind a consistent interface with
|
|
12
|
+
* comprehensive error handling and logging. All methods use async/await
|
|
13
|
+
* for consistency.
|
|
14
|
+
*/
|
|
15
|
+
export declare class FileManager {
|
|
16
|
+
/**
|
|
17
|
+
* Logger instance for file operations
|
|
18
|
+
*/
|
|
19
|
+
private readonly logger;
|
|
20
|
+
/**
|
|
21
|
+
* Create a new FileManager instance
|
|
22
|
+
*
|
|
23
|
+
* @param logger - Pino logger instance for logging file operations
|
|
24
|
+
* @example
|
|
25
|
+
* const logger = createLogger({ namespace: 'services:file-system' })
|
|
26
|
+
* const fileManager = new FileManager(logger)
|
|
27
|
+
*/
|
|
28
|
+
constructor(logger: pino.Logger);
|
|
29
|
+
/**
|
|
30
|
+
* Create a directory (and parent directories if needed)
|
|
31
|
+
*
|
|
32
|
+
* Uses ensureDir which is idempotent - safe to call if directory already exists.
|
|
33
|
+
*
|
|
34
|
+
* @param path - Path to the directory to create
|
|
35
|
+
* @throws {FileSystemError} If directory cannot be created (permission denied, invalid path, etc.)
|
|
36
|
+
* @example
|
|
37
|
+
* await fileManager.createDirectory('docs/epics')
|
|
38
|
+
*/
|
|
39
|
+
createDirectory(path: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if a file or directory exists
|
|
42
|
+
*
|
|
43
|
+
* @param path - Path to check for existence
|
|
44
|
+
* @returns True if path exists, false otherwise
|
|
45
|
+
* @example
|
|
46
|
+
* const exists = await fileManager.fileExists('docs/prd.md')
|
|
47
|
+
* if (!exists) {
|
|
48
|
+
* throw new Error('PRD not found')
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
fileExists(path: string): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* List files in a directory matching an optional pattern
|
|
54
|
+
*
|
|
55
|
+
* @param directory - Directory to list files from
|
|
56
|
+
* @param pattern - Optional glob pattern to filter files (e.g., '*.md', 'STORY-*.md')
|
|
57
|
+
* @returns Array of file paths matching the pattern
|
|
58
|
+
* @throws {FileSystemError} If directory cannot be read
|
|
59
|
+
* @example
|
|
60
|
+
* const storyFiles = await fileManager.listFiles('docs/stories', 'STORY-*.md')
|
|
61
|
+
*/
|
|
62
|
+
listFiles(directory: string, pattern?: string): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Move a file from source to destination
|
|
65
|
+
*
|
|
66
|
+
* Creates destination directory if it doesn't exist.
|
|
67
|
+
* Overwrites destination file if it exists.
|
|
68
|
+
*
|
|
69
|
+
* @param source - Source file path
|
|
70
|
+
* @param dest - Destination file path
|
|
71
|
+
* @throws {FileSystemError} If file cannot be moved (source not found, permission denied, etc.)
|
|
72
|
+
* @example
|
|
73
|
+
* await fileManager.moveFile('docs/stories/1.1-story.md', 'docs/qa/stories/1.1-story.md')
|
|
74
|
+
*/
|
|
75
|
+
moveFile(source: string, dest: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Read file contents as UTF-8 string
|
|
78
|
+
*
|
|
79
|
+
* @param path - Path to the file to read
|
|
80
|
+
* @returns File content as string
|
|
81
|
+
* @throws {FileSystemError} If file cannot be read (not found, permission denied, etc.)
|
|
82
|
+
* @example
|
|
83
|
+
* const content = await fileManager.readFile('docs/prd.md')
|
|
84
|
+
*/
|
|
85
|
+
readFile(path: string): Promise<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Write content to file as UTF-8
|
|
88
|
+
*
|
|
89
|
+
* Creates parent directories if they don't exist.
|
|
90
|
+
* Overwrites existing file if present.
|
|
91
|
+
*
|
|
92
|
+
* @param path - Path to the file to write
|
|
93
|
+
* @param content - Content to write to file
|
|
94
|
+
* @throws {FileSystemError} If file cannot be written (permission denied, invalid path, etc.)
|
|
95
|
+
* @example
|
|
96
|
+
* await fileManager.writeFile('docs/epics/epic-1.md', epicContent)
|
|
97
|
+
*/
|
|
98
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Match a file name against a simple glob pattern
|
|
101
|
+
*
|
|
102
|
+
* Supports * wildcard only (e.g., 'STORY-*.md', '*.txt')
|
|
103
|
+
*
|
|
104
|
+
* @param fileName - File name to check
|
|
105
|
+
* @param pattern - Glob pattern with * wildcards
|
|
106
|
+
* @returns True if file matches pattern
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
private matchesPattern;
|
|
110
|
+
}
|