@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epics List Command
|
|
3
|
+
*
|
|
4
|
+
* Lists all epic files with their metadata in a formatted table.
|
|
5
|
+
* Scans docs/epics/ directory for epic files and displays:
|
|
6
|
+
* - Epic number
|
|
7
|
+
* - Epic title
|
|
8
|
+
* - Story count
|
|
9
|
+
* - File path
|
|
10
|
+
*
|
|
11
|
+
* Supports JSON output mode for machine-readable consumption.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* # Display epics in table format
|
|
16
|
+
* bmad-workflow epics list
|
|
17
|
+
*
|
|
18
|
+
* # Output as JSON
|
|
19
|
+
* bmad-workflow epics list --json
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { Command, Flags } from '@oclif/core';
|
|
23
|
+
import Table from 'cli-table3';
|
|
24
|
+
import { relative } from 'node:path';
|
|
25
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
26
|
+
import { PathResolver } from '../../services/file-system/path-resolver.js';
|
|
27
|
+
import { EpicParser } from '../../services/parsers/epic-parser.js';
|
|
28
|
+
import * as Colors from '../../utils/colors.js';
|
|
29
|
+
import { createLogger } from '../../utils/logger.js';
|
|
30
|
+
/**
|
|
31
|
+
* Epics List Command
|
|
32
|
+
*
|
|
33
|
+
* Lists all epic files with metadata in table or JSON format.
|
|
34
|
+
*/
|
|
35
|
+
export default class EpicsList extends Command {
|
|
36
|
+
/**
|
|
37
|
+
* Command description
|
|
38
|
+
*/
|
|
39
|
+
static description = 'List all epics with metadata from docs/epics/ directory';
|
|
40
|
+
/**
|
|
41
|
+
* Command examples
|
|
42
|
+
*/
|
|
43
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
|
|
44
|
+
/**
|
|
45
|
+
* Command flags
|
|
46
|
+
*/
|
|
47
|
+
static flags = {
|
|
48
|
+
json: Flags.boolean({
|
|
49
|
+
char: 'j',
|
|
50
|
+
default: false,
|
|
51
|
+
description: 'Output as JSON array',
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Logger instance
|
|
56
|
+
*/
|
|
57
|
+
logger = createLogger({ namespace: 'bmad-workflow:commands:epics:list' });
|
|
58
|
+
/**
|
|
59
|
+
* EpicParser instance
|
|
60
|
+
*/
|
|
61
|
+
epicParser = new EpicParser(this.logger);
|
|
62
|
+
/**
|
|
63
|
+
* FileManager instance
|
|
64
|
+
*/
|
|
65
|
+
fileManager = new FileManager(this.logger);
|
|
66
|
+
/**
|
|
67
|
+
* PathResolver instance
|
|
68
|
+
*/
|
|
69
|
+
pathResolver = new PathResolver(this.fileManager, this.logger);
|
|
70
|
+
/**
|
|
71
|
+
* Run the command
|
|
72
|
+
*/
|
|
73
|
+
async run() {
|
|
74
|
+
const { flags } = await this.parse(EpicsList);
|
|
75
|
+
try {
|
|
76
|
+
// Discover epic files
|
|
77
|
+
const epicMetadata = await this.discoverEpics();
|
|
78
|
+
// Sort by epic number
|
|
79
|
+
epicMetadata.sort((a, b) => a.number - b.number);
|
|
80
|
+
// Output in requested format
|
|
81
|
+
if (flags.json) {
|
|
82
|
+
this.outputJson(epicMetadata);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.outputTable(epicMetadata);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const err = error;
|
|
90
|
+
this.logger.error({ error: err }, 'Failed to list epics');
|
|
91
|
+
this.error(Colors.error(`Failed to list epics: ${err.message}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Discover epic files and extract metadata
|
|
96
|
+
*
|
|
97
|
+
* @returns Array of epic metadata objects
|
|
98
|
+
*/
|
|
99
|
+
async discoverEpics() {
|
|
100
|
+
this.logger.info('Discovering epic files');
|
|
101
|
+
// Get epic directory path
|
|
102
|
+
const epicDir = this.pathResolver.getEpicDir();
|
|
103
|
+
this.logger.debug({ epicDir }, 'Epic directory resolved');
|
|
104
|
+
// Read directory contents
|
|
105
|
+
const files = await this.fileManager.listFiles(epicDir, '*.md');
|
|
106
|
+
this.logger.debug({ fileCount: files.length }, 'Epic files discovered');
|
|
107
|
+
// Filter files matching epic pattern
|
|
108
|
+
const epicFiles = files.filter((file) => this.isEpicFile(file));
|
|
109
|
+
this.logger.debug({ epicFileCount: epicFiles.length }, 'Epic files filtered');
|
|
110
|
+
if (epicFiles.length === 0) {
|
|
111
|
+
this.logger.warn('No epic files found');
|
|
112
|
+
this.warn(Colors.warning('No epic files found in epic directory'));
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
// Parse each epic file to extract metadata
|
|
116
|
+
const epicMetadata = await Promise.all(epicFiles.map(async (epicFile) => {
|
|
117
|
+
try {
|
|
118
|
+
return await this.parseEpicMetadata(epicFile);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// Handle parsing errors gracefully
|
|
122
|
+
const err = error;
|
|
123
|
+
this.logger.error({ epicFile, error: err }, 'Failed to parse epic file');
|
|
124
|
+
// Extract epic number from filename
|
|
125
|
+
const epicNumber = this.extractEpicNumber(epicFile);
|
|
126
|
+
const relativePath = relative(process.cwd(), epicFile);
|
|
127
|
+
return {
|
|
128
|
+
filePath: relativePath,
|
|
129
|
+
number: epicNumber,
|
|
130
|
+
storyCount: 0,
|
|
131
|
+
title: 'Parse Error',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
this.logger.info({ epicCount: epicMetadata.length }, 'Epic metadata extracted');
|
|
136
|
+
return epicMetadata;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Extract epic number from filename
|
|
140
|
+
*
|
|
141
|
+
* @param filePath - Epic file path
|
|
142
|
+
* @returns Epic number
|
|
143
|
+
*/
|
|
144
|
+
extractEpicNumber(filePath) {
|
|
145
|
+
const filename = filePath.split('/').pop() || filePath;
|
|
146
|
+
const match = filename.match(/epic-(\d+)-/i);
|
|
147
|
+
if (match && match[1]) {
|
|
148
|
+
return Number.parseInt(match[1], 10);
|
|
149
|
+
}
|
|
150
|
+
// Fallback to 0 if pattern doesn't match
|
|
151
|
+
this.logger.warn({ filename }, 'Could not extract epic number from filename');
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract epic title from content
|
|
156
|
+
*
|
|
157
|
+
* Looks for first # heading in content, or derives from filename.
|
|
158
|
+
*
|
|
159
|
+
* @param content - Epic file content
|
|
160
|
+
* @param filePath - Epic file path (for fallback title)
|
|
161
|
+
* @returns Epic title
|
|
162
|
+
*/
|
|
163
|
+
extractEpicTitle(content, filePath) {
|
|
164
|
+
// Look for first # heading
|
|
165
|
+
const headingPattern = /^#\s+(.+?)$/m;
|
|
166
|
+
const match = content.match(headingPattern);
|
|
167
|
+
if (match && match[1]) {
|
|
168
|
+
return match[1].trim();
|
|
169
|
+
}
|
|
170
|
+
// Fallback: derive from filename
|
|
171
|
+
const filename = filePath.split('/').pop() || filePath;
|
|
172
|
+
const titlePart = filename
|
|
173
|
+
.replace(/^epic-\d+-/i, '') // Remove epic-N- prefix
|
|
174
|
+
.replace(/\.md$/, '') // Remove .md extension
|
|
175
|
+
// eslint-disable-next-line unicorn/prefer-string-replace-all
|
|
176
|
+
.replace(/-/g, ' ') // Replace all dashes with spaces
|
|
177
|
+
.split(' ')
|
|
178
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Title case
|
|
179
|
+
.join(' ');
|
|
180
|
+
this.logger.debug({ filename, title: titlePart }, 'Title derived from filename');
|
|
181
|
+
return titlePart;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a file is an epic file
|
|
185
|
+
*
|
|
186
|
+
* @param filePath - File path to check
|
|
187
|
+
* @returns True if file matches epic pattern
|
|
188
|
+
*/
|
|
189
|
+
isEpicFile(filePath) {
|
|
190
|
+
const filename = filePath.split('/').pop() || filePath;
|
|
191
|
+
// Pattern: epic-{n}-*.md or EPIC-{n}-*.md
|
|
192
|
+
const epicPattern = /^epic-\d+-.+\.md$/i;
|
|
193
|
+
return epicPattern.test(filename);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Output epic metadata as JSON
|
|
197
|
+
*
|
|
198
|
+
* @param epicMetadata - Array of epic metadata
|
|
199
|
+
*/
|
|
200
|
+
outputJson(epicMetadata) {
|
|
201
|
+
const output = JSON.stringify(epicMetadata, null, 2);
|
|
202
|
+
this.log(output);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Output epic metadata as formatted table
|
|
206
|
+
*
|
|
207
|
+
* @param epicMetadata - Array of epic metadata
|
|
208
|
+
*/
|
|
209
|
+
outputTable(epicMetadata) {
|
|
210
|
+
if (epicMetadata.length === 0) {
|
|
211
|
+
this.log(Colors.warning('No epics found'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Create table
|
|
215
|
+
const table = new Table({
|
|
216
|
+
colWidths: [10, 50, 10, 50],
|
|
217
|
+
head: [
|
|
218
|
+
Colors.bold(Colors.highlight('Epic #')),
|
|
219
|
+
Colors.bold(Colors.highlight('Title')),
|
|
220
|
+
Colors.bold(Colors.highlight('Stories')),
|
|
221
|
+
Colors.bold(Colors.highlight('File Path')),
|
|
222
|
+
],
|
|
223
|
+
wordWrap: true,
|
|
224
|
+
});
|
|
225
|
+
// Add rows
|
|
226
|
+
for (const epic of epicMetadata) {
|
|
227
|
+
table.push([
|
|
228
|
+
epic.number.toString(),
|
|
229
|
+
this.truncateTitle(epic.title, 48),
|
|
230
|
+
epic.storyCount.toString(),
|
|
231
|
+
epic.filePath,
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
// Display table
|
|
235
|
+
this.log(table.toString());
|
|
236
|
+
// Display summary
|
|
237
|
+
const totalStories = epicMetadata.reduce((sum, epic) => sum + epic.storyCount, 0);
|
|
238
|
+
const summary = `Found ${epicMetadata.length} epics with ${totalStories} total stories`;
|
|
239
|
+
this.log('');
|
|
240
|
+
this.log(Colors.success(summary));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parse epic file to extract metadata
|
|
244
|
+
*
|
|
245
|
+
* @param epicFile - Path to epic file
|
|
246
|
+
* @returns Epic metadata
|
|
247
|
+
*/
|
|
248
|
+
async parseEpicMetadata(epicFile) {
|
|
249
|
+
this.logger.debug({ epicFile }, 'Parsing epic metadata');
|
|
250
|
+
// Read file content
|
|
251
|
+
const content = await this.fileManager.readFile(epicFile);
|
|
252
|
+
// Extract epic number from filename
|
|
253
|
+
const epicNumber = this.extractEpicNumber(epicFile);
|
|
254
|
+
// Parse stories from content
|
|
255
|
+
const stories = this.epicParser.parseStories(content, epicFile);
|
|
256
|
+
// Extract title from first heading
|
|
257
|
+
const title = this.extractEpicTitle(content, epicFile);
|
|
258
|
+
// Build relative path
|
|
259
|
+
const relativePath = relative(process.cwd(), epicFile);
|
|
260
|
+
return {
|
|
261
|
+
filePath: relativePath,
|
|
262
|
+
number: epicNumber,
|
|
263
|
+
storyCount: stories.length,
|
|
264
|
+
title,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Truncate title if too long
|
|
269
|
+
*
|
|
270
|
+
* @param title - Title to truncate
|
|
271
|
+
* @param maxLength - Maximum length
|
|
272
|
+
* @returns Truncated title with ellipsis if needed
|
|
273
|
+
*/
|
|
274
|
+
truncateTitle(title, maxLength) {
|
|
275
|
+
if (title.length <= maxLength) {
|
|
276
|
+
return title;
|
|
277
|
+
}
|
|
278
|
+
return title.slice(0, maxLength - 3) + '...';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Hello extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
person: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
from: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
3
|
+
export default class Hello extends Command {
|
|
4
|
+
static args = {
|
|
5
|
+
person: Args.string({ description: 'Person to say hello to', required: true }),
|
|
6
|
+
};
|
|
7
|
+
static description = 'Say hello';
|
|
8
|
+
static examples = [
|
|
9
|
+
`<%= config.bin %> <%= command.id %> friend --from oclif
|
|
10
|
+
hello friend from oclif! (./src/commands/hello/index.ts)
|
|
11
|
+
`,
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
from: Flags.string({ char: 'f', description: 'Who is saying hello', required: true }),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args, flags } = await this.parse(Hello);
|
|
18
|
+
// Create logger with command context
|
|
19
|
+
const logger = createLogger({ namespace: 'commands:hello' });
|
|
20
|
+
const correlationId = generateCorrelationId();
|
|
21
|
+
// Log command execution with context
|
|
22
|
+
logger.info({
|
|
23
|
+
command: 'hello',
|
|
24
|
+
correlationId,
|
|
25
|
+
from: flags.from,
|
|
26
|
+
person: args.person,
|
|
27
|
+
}, 'Executing hello command');
|
|
28
|
+
// Demonstrate different log levels
|
|
29
|
+
logger.debug({ correlationId }, 'Debug: Command arguments parsed successfully');
|
|
30
|
+
logger.info({ correlationId }, `Greeting ${args.person} from ${flags.from}`);
|
|
31
|
+
this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`);
|
|
32
|
+
logger.info({ correlationId }, 'Hello command completed successfully');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
3
|
+
export default class World extends Command {
|
|
4
|
+
static args = {};
|
|
5
|
+
static description = 'Say hello world';
|
|
6
|
+
static examples = [
|
|
7
|
+
`<%= config.bin %> <%= command.id %>
|
|
8
|
+
hello world! (./src/commands/hello/world.ts)
|
|
9
|
+
`,
|
|
10
|
+
];
|
|
11
|
+
static flags = {};
|
|
12
|
+
async run() {
|
|
13
|
+
// Create logger with command context
|
|
14
|
+
const logger = createLogger({ namespace: 'commands:hello:world' });
|
|
15
|
+
const correlationId = generateCorrelationId();
|
|
16
|
+
// Log command execution
|
|
17
|
+
logger.info({
|
|
18
|
+
command: 'hello:world',
|
|
19
|
+
correlationId,
|
|
20
|
+
}, 'Executing hello world command');
|
|
21
|
+
this.log('hello world! (./src/commands/hello/world.ts)');
|
|
22
|
+
logger.info({ correlationId }, 'Hello world command completed');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Fix Command
|
|
3
|
+
*
|
|
4
|
+
* Uses AI to automatically reformat a PRD file to match the expected
|
|
5
|
+
* epic/story structure for workflow processing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* # Fix PRD format
|
|
10
|
+
* bmad-workflow prd fix docs/PRD-feature.md
|
|
11
|
+
*
|
|
12
|
+
* # Fix with architecture reference for context
|
|
13
|
+
* bmad-workflow prd fix docs/PRD-feature.md --reference docs/architecture.md
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from '@oclif/core';
|
|
17
|
+
/**
|
|
18
|
+
* PRD Fix Command
|
|
19
|
+
*
|
|
20
|
+
* Reformats a PRD file to match expected epic/story patterns using AI.
|
|
21
|
+
*/
|
|
22
|
+
export default class PrdFix extends Command {
|
|
23
|
+
static args: {
|
|
24
|
+
prdFile: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
25
|
+
};
|
|
26
|
+
static description: string;
|
|
27
|
+
static examples: string[];
|
|
28
|
+
static flags: {
|
|
29
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
|
+
reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
31
|
+
};
|
|
32
|
+
private fileManager;
|
|
33
|
+
private logger;
|
|
34
|
+
private prdFixer;
|
|
35
|
+
private prdParser;
|
|
36
|
+
run(): Promise<void>;
|
|
37
|
+
private initializeServices;
|
|
38
|
+
private validateFixedContent;
|
|
39
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Fix Command
|
|
3
|
+
*
|
|
4
|
+
* Uses AI to automatically reformat a PRD file to match the expected
|
|
5
|
+
* epic/story structure for workflow processing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* # Fix PRD format
|
|
10
|
+
* bmad-workflow prd fix docs/PRD-feature.md
|
|
11
|
+
*
|
|
12
|
+
* # Fix with architecture reference for context
|
|
13
|
+
* bmad-workflow prd fix docs/PRD-feature.md --reference docs/architecture.md
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
17
|
+
import { relative } from 'node:path';
|
|
18
|
+
import { createAgentRunner, isProviderSupported } from '../../services/agents/agent-runner-factory.js';
|
|
19
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
20
|
+
import { PrdFixer } from '../../services/parsers/prd-fixer.js';
|
|
21
|
+
import { PrdParser } from '../../services/parsers/prd-parser.js';
|
|
22
|
+
import * as Colors from '../../utils/colors.js';
|
|
23
|
+
import { createLogger } from '../../utils/logger.js';
|
|
24
|
+
/**
|
|
25
|
+
* PRD Fix Command
|
|
26
|
+
*
|
|
27
|
+
* Reformats a PRD file to match expected epic/story patterns using AI.
|
|
28
|
+
*/
|
|
29
|
+
export default class PrdFix extends Command {
|
|
30
|
+
static args = {
|
|
31
|
+
prdFile: Args.string({
|
|
32
|
+
description: 'Path to PRD markdown file to fix',
|
|
33
|
+
required: true,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
static description = 'Fix PRD format using AI to match expected epic/story structure';
|
|
37
|
+
static examples = [
|
|
38
|
+
'<%= config.bin %> <%= command.id %> docs/PRD-feature.md',
|
|
39
|
+
'<%= config.bin %> <%= command.id %> docs/PRD-feature.md --reference docs/architecture.md',
|
|
40
|
+
'<%= config.bin %> <%= command.id %> docs/PRD-feature.md --provider gemini',
|
|
41
|
+
];
|
|
42
|
+
static flags = {
|
|
43
|
+
provider: Flags.string({
|
|
44
|
+
default: 'claude',
|
|
45
|
+
description: 'AI provider to use (claude or gemini)',
|
|
46
|
+
options: ['claude', 'gemini'],
|
|
47
|
+
}),
|
|
48
|
+
reference: Flags.string({
|
|
49
|
+
description: 'Reference files for context (can be used multiple times)',
|
|
50
|
+
multiple: true,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
fileManager;
|
|
54
|
+
logger = createLogger({ namespace: 'bmad-workflow:commands:prd:fix' });
|
|
55
|
+
prdFixer;
|
|
56
|
+
prdParser;
|
|
57
|
+
async run() {
|
|
58
|
+
const { args, flags } = await this.parse(PrdFix);
|
|
59
|
+
// Validate provider
|
|
60
|
+
if (!isProviderSupported(flags.provider)) {
|
|
61
|
+
this.error(`Unsupported provider: ${flags.provider}. Use 'claude' or 'gemini'.`, { exit: 1 });
|
|
62
|
+
}
|
|
63
|
+
// Initialize services
|
|
64
|
+
this.initializeServices(flags.provider);
|
|
65
|
+
const prdPath = args.prdFile;
|
|
66
|
+
const relativePath = relative(process.cwd(), prdPath) || prdPath;
|
|
67
|
+
try {
|
|
68
|
+
// Check if file exists
|
|
69
|
+
const exists = await this.fileManager.fileExists(prdPath);
|
|
70
|
+
if (!exists) {
|
|
71
|
+
this.error(Colors.error(`PRD file not found: ${prdPath}`), { exit: 1 });
|
|
72
|
+
}
|
|
73
|
+
// Read the PRD content
|
|
74
|
+
const originalContent = await this.fileManager.readFile(prdPath);
|
|
75
|
+
// First, check if the PRD already parses correctly
|
|
76
|
+
this.log(Colors.info(`\nAnalyzing PRD: ${relativePath}`));
|
|
77
|
+
let needsFix = false;
|
|
78
|
+
try {
|
|
79
|
+
const epics = this.prdParser.parseEpics(originalContent, prdPath);
|
|
80
|
+
this.log(Colors.success(`\n✓ PRD already valid - found ${epics.length} epic(s)`));
|
|
81
|
+
this.log(Colors.dim(' No changes needed.\n'));
|
|
82
|
+
// Show the epics found
|
|
83
|
+
for (const epic of epics) {
|
|
84
|
+
this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title}`));
|
|
85
|
+
}
|
|
86
|
+
this.log('');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
needsFix = true;
|
|
91
|
+
this.log(Colors.warning('\n⚠ PRD format issues detected - attempting auto-fix...'));
|
|
92
|
+
}
|
|
93
|
+
if (needsFix) {
|
|
94
|
+
// Attempt to fix the PRD
|
|
95
|
+
const references = flags.reference || [];
|
|
96
|
+
if (references.length > 0) {
|
|
97
|
+
this.log(Colors.dim(` Using ${references.length} reference file(s) for context`));
|
|
98
|
+
}
|
|
99
|
+
this.log(Colors.info(' Running AI fixer...'));
|
|
100
|
+
const result = await this.prdFixer.fixPrd(prdPath, originalContent, references);
|
|
101
|
+
if (result.fixed) {
|
|
102
|
+
this.log(Colors.success('\n✓ PRD fixed successfully!'));
|
|
103
|
+
this.log(Colors.dim(` Backup saved to: ${relativePath}.bak`));
|
|
104
|
+
// Validate the fixed content
|
|
105
|
+
this.validateFixedContent(result.content, prdPath);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.log(Colors.error('\n✗ Failed to fix PRD'));
|
|
109
|
+
this.log(Colors.error(` Error: ${result.error}\n`));
|
|
110
|
+
this.error('PRD fix failed', { exit: 1 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const err = error;
|
|
116
|
+
this.logger.error({ error: err.message, prdPath }, 'PRD fix command failed');
|
|
117
|
+
this.error(Colors.error(`Fix failed: ${err.message}`), { exit: 1 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
initializeServices(provider) {
|
|
121
|
+
this.fileManager = new FileManager(this.logger);
|
|
122
|
+
this.prdParser = new PrdParser(this.logger);
|
|
123
|
+
const agentRunner = createAgentRunner(provider, this.logger);
|
|
124
|
+
this.prdFixer = new PrdFixer(agentRunner, this.fileManager, this.logger);
|
|
125
|
+
}
|
|
126
|
+
validateFixedContent(content, prdPath) {
|
|
127
|
+
try {
|
|
128
|
+
const epics = this.prdParser.parseEpics(content, prdPath);
|
|
129
|
+
this.log(Colors.success(` Found ${epics.length} epic(s) after fix:\n`));
|
|
130
|
+
for (const epic of epics) {
|
|
131
|
+
this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title}`));
|
|
132
|
+
}
|
|
133
|
+
this.log('');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
this.log(Colors.warning('\n⚠ Fixed file still has parsing issues.'));
|
|
137
|
+
this.log(Colors.dim(' You may need to manually review the PRD.\n'));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Validate Command
|
|
3
|
+
*
|
|
4
|
+
* Validates a PRD file and previews the epic/story extraction that would
|
|
5
|
+
* occur during a workflow run. This is a dry-run inspection tool that:
|
|
6
|
+
* - Parses the PRD to extract epic definitions
|
|
7
|
+
* - Checks for existing epic files
|
|
8
|
+
* - Counts stories in existing epic files
|
|
9
|
+
* - Reports what would be generated by the workflow command
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```bash
|
|
13
|
+
* # Validate PRD and show extraction preview
|
|
14
|
+
* bmad-workflow prd validate docs/PRD-feature.md
|
|
15
|
+
*
|
|
16
|
+
* # Output as JSON for scripting
|
|
17
|
+
* bmad-workflow prd validate docs/PRD-feature.md --json
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from '@oclif/core';
|
|
21
|
+
/**
|
|
22
|
+
* PRD Validate Command
|
|
23
|
+
*
|
|
24
|
+
* Parses a PRD file and reports what epics and stories would be
|
|
25
|
+
* extracted by the workflow command.
|
|
26
|
+
*/
|
|
27
|
+
export default class PrdValidate extends Command {
|
|
28
|
+
static args: {
|
|
29
|
+
prdFile: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
30
|
+
};
|
|
31
|
+
static description: string;
|
|
32
|
+
static examples: string[];
|
|
33
|
+
static flags: {
|
|
34
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* EpicParser instance for parsing story content
|
|
38
|
+
*/
|
|
39
|
+
private epicParser;
|
|
40
|
+
/**
|
|
41
|
+
* FileManager instance for file operations
|
|
42
|
+
*/
|
|
43
|
+
private fileManager;
|
|
44
|
+
/**
|
|
45
|
+
* Logger instance
|
|
46
|
+
*/
|
|
47
|
+
private logger;
|
|
48
|
+
/**
|
|
49
|
+
* PathResolver instance for resolving paths
|
|
50
|
+
*/
|
|
51
|
+
private pathResolver;
|
|
52
|
+
/**
|
|
53
|
+
* PrdParser instance for parsing PRD content
|
|
54
|
+
*/
|
|
55
|
+
private prdParser;
|
|
56
|
+
/**
|
|
57
|
+
* Run the validation command
|
|
58
|
+
*/
|
|
59
|
+
run(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Calculate summary statistics from validation results
|
|
62
|
+
*
|
|
63
|
+
* @param results - Array of epic validation results
|
|
64
|
+
* @returns Summary statistics
|
|
65
|
+
*/
|
|
66
|
+
private calculateSummary;
|
|
67
|
+
/**
|
|
68
|
+
* Find epic file matching the epic number
|
|
69
|
+
*
|
|
70
|
+
* Searches the epics directory for files matching pattern:
|
|
71
|
+
* - epic-{N}-*.md
|
|
72
|
+
* - *-epic-{N}.md
|
|
73
|
+
* - *-epic-{N}-*.md
|
|
74
|
+
*
|
|
75
|
+
* @param epicNumber - Epic number to search for
|
|
76
|
+
* @returns Path to epic file if found, null otherwise
|
|
77
|
+
*/
|
|
78
|
+
private findEpicFile;
|
|
79
|
+
/**
|
|
80
|
+
* Initialize service instances
|
|
81
|
+
*/
|
|
82
|
+
private initializeServices;
|
|
83
|
+
/**
|
|
84
|
+
* Output validation results as JSON
|
|
85
|
+
*
|
|
86
|
+
* @param output - Complete validation output
|
|
87
|
+
*/
|
|
88
|
+
private outputJson;
|
|
89
|
+
/**
|
|
90
|
+
* Output validation results as formatted table
|
|
91
|
+
*
|
|
92
|
+
* @param output - Complete validation output
|
|
93
|
+
*/
|
|
94
|
+
private outputTable;
|
|
95
|
+
/**
|
|
96
|
+
* Truncate text with ellipsis if too long
|
|
97
|
+
*
|
|
98
|
+
* @param text - Text to truncate
|
|
99
|
+
* @param maxLength - Maximum length
|
|
100
|
+
* @returns Truncated text
|
|
101
|
+
*/
|
|
102
|
+
private truncateText;
|
|
103
|
+
/**
|
|
104
|
+
* Validate a single epic
|
|
105
|
+
*
|
|
106
|
+
* Checks if the epic file exists and counts stories if it does.
|
|
107
|
+
*
|
|
108
|
+
* @param epic - Epic information from PRD
|
|
109
|
+
* @returns Validation result for the epic
|
|
110
|
+
*/
|
|
111
|
+
private validateEpic;
|
|
112
|
+
}
|