@hyperdrive.bot/bmad-workflow 1.0.16 → 1.0.18

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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * listr2 Helper Functions
3
+ *
4
+ * Utility functions for creating and managing listr2 task structures
5
+ * used by the WorkflowReporter for visualizing workflow progress.
6
+ */
7
+ /**
8
+ * Default terminal width constraint for output formatting
9
+ */
10
+ export declare const DEFAULT_TERMINAL_WIDTH = 80;
11
+ /**
12
+ * Regex pattern for detecting ANSI escape codes
13
+ */
14
+ export declare const ANSI_ESCAPE_PATTERN: RegExp;
15
+ /**
16
+ * Strip ANSI escape codes from text
17
+ *
18
+ * @param text - Text potentially containing ANSI codes
19
+ * @returns Clean text without ANSI escape sequences
20
+ */
21
+ export declare const stripAnsi: (text: string) => string;
22
+ /**
23
+ * Check if the current environment is a TTY
24
+ *
25
+ * @returns true if stdout is a TTY, false otherwise
26
+ */
27
+ export declare const isTTY: () => boolean;
28
+ /**
29
+ * Plain-text status indicators (no ANSI codes)
30
+ */
31
+ export declare const PLAIN_STATUS_INDICATORS: {
32
+ readonly completed: "[+]";
33
+ readonly failed: "[X]";
34
+ readonly queued: "[.]";
35
+ readonly running: "[*]";
36
+ };
37
+ /**
38
+ * Format a plain-text phase header for non-TTY output
39
+ *
40
+ * @param phaseName - Phase name (epic, story, dev, qa)
41
+ * @returns Plain-text header string (e.g., "=== Phase: Epic Generation ===")
42
+ */
43
+ export declare const formatPlainPhaseHeader: (phaseName: string) => string;
44
+ /**
45
+ * Format a plain-text spawn marker for non-TTY output
46
+ *
47
+ * @param itemId - Item identifier
48
+ * @param status - Spawn status (STARTED, COMPLETED, FAILED)
49
+ * @param durationMs - Optional duration in milliseconds
50
+ * @param error - Optional error message for failed spawns
51
+ * @returns Plain-text marker string (e.g., "[SPAWN] story-1.001 STARTED")
52
+ */
53
+ export declare const formatPlainSpawnMarker: (itemId: string, status: "COMPLETED" | "FAILED" | "STARTED", durationMs?: number, error?: string) => string;
54
+ /**
55
+ * Format a plain-text summary for non-TTY output
56
+ *
57
+ * @param totalSpawns - Total number of spawns
58
+ * @param passedCount - Number of passed spawns
59
+ * @param failedCount - Number of failed spawns
60
+ * @param durationMs - Total duration in milliseconds
61
+ * @returns Plain-text summary string
62
+ */
63
+ export declare const formatPlainSummary: (totalSpawns: number, passedCount: number, failedCount: number, durationMs: number) => string;
64
+ /**
65
+ * Format spawn output with visual delimiters for verbose mode
66
+ *
67
+ * @param spawnId - Spawn identifier
68
+ * @param output - Output content from the spawn
69
+ * @param maxWidth - Maximum line width (default: 80)
70
+ * @returns Formatted output with header/footer delimiters
71
+ */
72
+ export declare const formatVerboseSpawnOutput: (spawnId: string, output: string, maxWidth?: number) => string;
73
+ /**
74
+ * Wrap text lines to fit within a maximum width
75
+ *
76
+ * @param text - Text to wrap
77
+ * @param maxWidth - Maximum line width
78
+ * @returns Wrapped text
79
+ */
80
+ export declare const wrapLines: (text: string, maxWidth: number) => string;
81
+ /**
82
+ * Phase emoji mapping for visual consistency
83
+ */
84
+ export declare const PHASE_EMOJI: Record<string, string>;
85
+ /**
86
+ * Phase display names for titles
87
+ */
88
+ export declare const PHASE_TITLES: Record<string, string>;
89
+ /**
90
+ * Action verbs for spawn labels — maps phase to what the spawn is DOING (lowercase)
91
+ */
92
+ export declare const PHASE_ACTION_VERBS: Record<string, string>;
93
+ /**
94
+ * Get the action verb for a phase (e.g., "Creating" for story phase)
95
+ */
96
+ export declare const getPhaseActionVerb: (phaseName: string) => string;
97
+ /**
98
+ * Status indicators for spawn states
99
+ */
100
+ export declare const STATUS_INDICATORS: {
101
+ readonly completed: string;
102
+ readonly failed: string;
103
+ readonly queued: string;
104
+ readonly running: string;
105
+ };
106
+ /**
107
+ * Get emoji for a phase name
108
+ *
109
+ * @param phaseName - Phase name (epic, story, dev, qa)
110
+ * @returns Emoji string for the phase
111
+ */
112
+ export declare const getPhaseEmoji: (phaseName: string) => string;
113
+ /**
114
+ * Create a formatted phase title with emoji
115
+ *
116
+ * @param phaseName - Phase name (epic, story, dev, qa)
117
+ * @returns Formatted title string with emoji (e.g., "📋 Epic Phase")
118
+ */
119
+ export declare const createPhaseTitle: (phaseName: string) => string;
120
+ /**
121
+ * Create a phase task group configuration for listr2
122
+ *
123
+ * @param phaseName - Phase name (epic, story, dev, qa)
124
+ * @returns Object with title and phase metadata
125
+ */
126
+ export declare const createPhaseTaskGroup: (phaseName: string) => {
127
+ emoji: string;
128
+ phaseName: string;
129
+ title: string;
130
+ };
131
+ /**
132
+ * Create a layer task group configuration for listr2
133
+ *
134
+ * @param layerIndex - Zero-based layer index
135
+ * @param spawnCount - Number of spawns in the layer
136
+ * @param totalLayers - Total number of layers in the phase
137
+ * @returns Object with title and layer metadata
138
+ */
139
+ export declare const createLayerTaskGroup: (layerIndex: number, spawnCount: number, totalLayers: number) => {
140
+ layerIndex: number;
141
+ spawnCount: number;
142
+ title: string;
143
+ totalLayers: number;
144
+ };
145
+ /**
146
+ * Spawn status type
147
+ */
148
+ export type SpawnStatus = 'completed' | 'failed' | 'queued' | 'running';
149
+ /**
150
+ * Format spawn status for display
151
+ *
152
+ * @param status - Current spawn status
153
+ * @param durationMs - Duration in milliseconds (optional)
154
+ * @param itemCount - Number of items processed (optional, for completed status)
155
+ * @param errorMessage - Error message (optional, for failed status)
156
+ * @returns Formatted status string
157
+ */
158
+ export declare const formatSpawnStatus: (status: SpawnStatus, durationMs?: number, itemCount?: number, errorMessage?: string) => string;
159
+ /**
160
+ * Layer result summary
161
+ */
162
+ export interface LayerResult {
163
+ durationMs: number;
164
+ failureCount: number;
165
+ layerIndex: number;
166
+ spawnCount: number;
167
+ successCount: number;
168
+ }
169
+ /**
170
+ * Format a layer summary line for collapsed display
171
+ *
172
+ * @param result - Layer execution results
173
+ * @returns Formatted summary string
174
+ */
175
+ export declare const formatLayerSummary: (result: LayerResult) => string;
176
+ /**
177
+ * Phase result summary
178
+ */
179
+ export interface PhaseResult {
180
+ durationMs: number;
181
+ failureCount: number;
182
+ itemCount: number;
183
+ phaseName: string;
184
+ successCount: number;
185
+ }
186
+ /**
187
+ * Format a phase summary for completion display
188
+ *
189
+ * @param result - Phase execution results
190
+ * @returns Formatted summary string
191
+ */
192
+ export declare const formatPhaseSummary: (result: PhaseResult) => string;
193
+ /**
194
+ * Create a spawn task title
195
+ *
196
+ * @param itemId - Item identifier (epic number, story number, etc.)
197
+ * @param itemTitle - Optional item title/description
198
+ * @param agentType - Type of agent being spawned
199
+ * @returns Formatted spawn task title
200
+ */
201
+ export declare const createSpawnTitle: (itemId: string, itemTitle?: string, agentType?: string) => string;
202
+ /**
203
+ * Truncate a string to a maximum length with ellipsis
204
+ *
205
+ * @param text - Text to truncate
206
+ * @param maxLength - Maximum length (default: 60)
207
+ * @returns Truncated string with ellipsis if needed
208
+ */
209
+ export declare const truncateText: (text: string, maxLength?: number) => string;
210
+ /**
211
+ * Format elapsed time for running tasks (updates every second)
212
+ *
213
+ * @param startTime - Start timestamp in milliseconds
214
+ * @returns Formatted elapsed time string
215
+ */
216
+ export declare const formatElapsedTime: (startTime: number) => string;
@@ -0,0 +1,334 @@
1
+ /**
2
+ * listr2 Helper Functions
3
+ *
4
+ * Utility functions for creating and managing listr2 task structures
5
+ * used by the WorkflowReporter for visualizing workflow progress.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { formatDuration } from './formatters.js';
9
+ /**
10
+ * Default terminal width constraint for output formatting
11
+ */
12
+ export const DEFAULT_TERMINAL_WIDTH = 80;
13
+ /**
14
+ * Regex pattern for detecting ANSI escape codes
15
+ */
16
+ export const ANSI_ESCAPE_PATTERN = /\u001B\[[0-9;]*[a-zA-Z]/g;
17
+ /**
18
+ * Strip ANSI escape codes from text
19
+ *
20
+ * @param text - Text potentially containing ANSI codes
21
+ * @returns Clean text without ANSI escape sequences
22
+ */
23
+ export const stripAnsi = (text) => text.replaceAll(ANSI_ESCAPE_PATTERN, '');
24
+ /**
25
+ * Check if the current environment is a TTY
26
+ *
27
+ * @returns true if stdout is a TTY, false otherwise
28
+ */
29
+ export const isTTY = () => process.stdout.isTTY === true;
30
+ /**
31
+ * Plain-text status indicators (no ANSI codes)
32
+ */
33
+ export const PLAIN_STATUS_INDICATORS = {
34
+ completed: '[+]',
35
+ failed: '[X]',
36
+ queued: '[.]',
37
+ running: '[*]',
38
+ };
39
+ /**
40
+ * Format a plain-text phase header for non-TTY output
41
+ *
42
+ * @param phaseName - Phase name (epic, story, dev, qa)
43
+ * @returns Plain-text header string (e.g., "=== Phase: Epic Generation ===")
44
+ */
45
+ export const formatPlainPhaseHeader = (phaseName) => {
46
+ const title = PHASE_TITLES[phaseName.toLowerCase()] || `${phaseName} Phase`;
47
+ return `=== Phase: ${title} ===`;
48
+ };
49
+ /**
50
+ * Format a plain-text spawn marker for non-TTY output
51
+ *
52
+ * @param itemId - Item identifier
53
+ * @param status - Spawn status (STARTED, COMPLETED, FAILED)
54
+ * @param durationMs - Optional duration in milliseconds
55
+ * @param error - Optional error message for failed spawns
56
+ * @returns Plain-text marker string (e.g., "[SPAWN] story-1.001 STARTED")
57
+ */
58
+ export const formatPlainSpawnMarker = (itemId, status, durationMs, error) => {
59
+ const duration = durationMs === undefined ? '' : ` (${formatDuration(durationMs)})`;
60
+ const errorSuffix = error ? `: ${error}` : '';
61
+ return `[SPAWN] ${itemId} ${status}${duration}${errorSuffix}`;
62
+ };
63
+ /**
64
+ * Format a plain-text summary for non-TTY output
65
+ *
66
+ * @param totalSpawns - Total number of spawns
67
+ * @param passedCount - Number of passed spawns
68
+ * @param failedCount - Number of failed spawns
69
+ * @param durationMs - Total duration in milliseconds
70
+ * @returns Plain-text summary string
71
+ */
72
+ export const formatPlainSummary = (totalSpawns, passedCount, failedCount, durationMs) => {
73
+ const lines = [
74
+ '--- Summary ---',
75
+ `Total: ${totalSpawns} spawns`,
76
+ `Passed: ${passedCount}`,
77
+ `Failed: ${failedCount}`,
78
+ `Duration: ${formatDuration(durationMs)}`,
79
+ ];
80
+ return lines.join('\n');
81
+ };
82
+ /**
83
+ * Format spawn output with visual delimiters for verbose mode
84
+ *
85
+ * @param spawnId - Spawn identifier
86
+ * @param output - Output content from the spawn
87
+ * @param maxWidth - Maximum line width (default: 80)
88
+ * @returns Formatted output with header/footer delimiters
89
+ */
90
+ export const formatVerboseSpawnOutput = (spawnId, output, maxWidth = DEFAULT_TERMINAL_WIDTH) => {
91
+ // Create delimiter line that fits within maxWidth
92
+ const headerText = `── ${spawnId} output ──`;
93
+ const footerText = `── end ${spawnId} ──`;
94
+ // Pad the delimiter to maxWidth with dashes
95
+ const headerPadding = Math.max(0, maxWidth - headerText.length);
96
+ const footerPadding = Math.max(0, maxWidth - footerText.length);
97
+ const header = headerText + '─'.repeat(headerPadding);
98
+ const footer = footerText + '─'.repeat(footerPadding);
99
+ // Wrap output lines to respect width constraint
100
+ const wrappedLines = wrapLines(output, maxWidth);
101
+ return `${header}\n${wrappedLines}\n${footer}`;
102
+ };
103
+ /**
104
+ * Wrap text lines to fit within a maximum width
105
+ *
106
+ * @param text - Text to wrap
107
+ * @param maxWidth - Maximum line width
108
+ * @returns Wrapped text
109
+ */
110
+ export const wrapLines = (text, maxWidth) => {
111
+ const lines = text.split('\n');
112
+ const wrappedLines = [];
113
+ for (const line of lines) {
114
+ if (line.length <= maxWidth) {
115
+ wrappedLines.push(line);
116
+ }
117
+ else {
118
+ // Simple word-based wrapping
119
+ let currentLine = '';
120
+ const words = line.split(' ');
121
+ for (const word of words) {
122
+ if (currentLine.length === 0) {
123
+ currentLine = word;
124
+ }
125
+ else if (currentLine.length + 1 + word.length <= maxWidth) {
126
+ currentLine += ' ' + word;
127
+ }
128
+ else {
129
+ wrappedLines.push(currentLine);
130
+ currentLine = word;
131
+ }
132
+ }
133
+ if (currentLine.length > 0) {
134
+ wrappedLines.push(currentLine);
135
+ }
136
+ }
137
+ }
138
+ return wrappedLines.join('\n');
139
+ };
140
+ /**
141
+ * Phase emoji mapping for visual consistency
142
+ */
143
+ export const PHASE_EMOJI = {
144
+ dev: '🛠️',
145
+ epic: '📋',
146
+ qa: '✅',
147
+ story: '📝',
148
+ };
149
+ /**
150
+ * Phase display names for titles
151
+ */
152
+ export const PHASE_TITLES = {
153
+ dev: 'Dev',
154
+ epic: 'Epics',
155
+ qa: 'QA',
156
+ story: 'Stories',
157
+ };
158
+ /**
159
+ * Action verbs for spawn labels — maps phase to what the spawn is DOING (lowercase)
160
+ */
161
+ export const PHASE_ACTION_VERBS = {
162
+ dev: 'developing',
163
+ epic: 'generating',
164
+ qa: 'reviewing',
165
+ story: 'creating',
166
+ };
167
+ /**
168
+ * Get the action verb for a phase (e.g., "Creating" for story phase)
169
+ */
170
+ export const getPhaseActionVerb = (phaseName) => PHASE_ACTION_VERBS[phaseName.toLowerCase()] || 'Processing';
171
+ /**
172
+ * Status indicators for spawn states
173
+ */
174
+ export const STATUS_INDICATORS = {
175
+ completed: chalk.bold('✓'),
176
+ failed: chalk.bold('✗'),
177
+ queued: chalk.dim('◯'),
178
+ running: chalk.bold('◉'),
179
+ };
180
+ /**
181
+ * Get emoji for a phase name
182
+ *
183
+ * @param phaseName - Phase name (epic, story, dev, qa)
184
+ * @returns Emoji string for the phase
185
+ */
186
+ export const getPhaseEmoji = (phaseName) => PHASE_EMOJI[phaseName.toLowerCase()] || '⚙️';
187
+ /**
188
+ * Create a formatted phase title with emoji
189
+ *
190
+ * @param phaseName - Phase name (epic, story, dev, qa)
191
+ * @returns Formatted title string with emoji (e.g., "📋 Epic Phase")
192
+ */
193
+ export const createPhaseTitle = (phaseName) => {
194
+ const emoji = getPhaseEmoji(phaseName);
195
+ const title = PHASE_TITLES[phaseName.toLowerCase()] || `${phaseName} Phase`;
196
+ return `${emoji} ${title}`;
197
+ };
198
+ /**
199
+ * Create a phase task group configuration for listr2
200
+ *
201
+ * @param phaseName - Phase name (epic, story, dev, qa)
202
+ * @returns Object with title and phase metadata
203
+ */
204
+ export const createPhaseTaskGroup = (phaseName) => ({
205
+ emoji: getPhaseEmoji(phaseName),
206
+ phaseName: phaseName.toLowerCase(),
207
+ title: createPhaseTitle(phaseName),
208
+ });
209
+ /**
210
+ * Create a layer task group configuration for listr2
211
+ *
212
+ * @param layerIndex - Zero-based layer index
213
+ * @param spawnCount - Number of spawns in the layer
214
+ * @param totalLayers - Total number of layers in the phase
215
+ * @returns Object with title and layer metadata
216
+ */
217
+ export const createLayerTaskGroup = (layerIndex, spawnCount, totalLayers) => {
218
+ const layerNumber = layerIndex + 1;
219
+ const title = `Layer ${layerNumber}/${totalLayers} (${spawnCount} ${spawnCount === 1 ? 'spawn' : 'spawns'})`;
220
+ return {
221
+ layerIndex,
222
+ spawnCount,
223
+ title,
224
+ totalLayers,
225
+ };
226
+ };
227
+ /**
228
+ * Format spawn status for display
229
+ *
230
+ * @param status - Current spawn status
231
+ * @param durationMs - Duration in milliseconds (optional)
232
+ * @param itemCount - Number of items processed (optional, for completed status)
233
+ * @param errorMessage - Error message (optional, for failed status)
234
+ * @returns Formatted status string
235
+ */
236
+ export const formatSpawnStatus = (status, durationMs, itemCount, errorMessage) => {
237
+ const indicator = STATUS_INDICATORS[status];
238
+ const duration = durationMs === undefined ? '' : formatDuration(durationMs);
239
+ switch (status) {
240
+ case 'completed': {
241
+ if (itemCount !== undefined) {
242
+ return duration
243
+ ? `${indicator} completed (${duration}) → ${itemCount} ${itemCount === 1 ? 'item' : 'items'}`
244
+ : `${indicator} completed → ${itemCount} ${itemCount === 1 ? 'item' : 'items'}`;
245
+ }
246
+ return duration ? `${indicator} completed (${duration})` : `${indicator} completed`;
247
+ }
248
+ case 'failed': {
249
+ return errorMessage
250
+ ? `${indicator} failed: ${errorMessage}`
251
+ : `${indicator} failed`;
252
+ }
253
+ case 'queued': {
254
+ return `${indicator} queued`;
255
+ }
256
+ case 'running': {
257
+ return duration ? `${indicator} running (${duration})` : `${indicator} running`;
258
+ }
259
+ default: {
260
+ return `${indicator} ${status}`;
261
+ }
262
+ }
263
+ };
264
+ /**
265
+ * Format a layer summary line for collapsed display
266
+ *
267
+ * @param result - Layer execution results
268
+ * @returns Formatted summary string
269
+ */
270
+ export const formatLayerSummary = (result) => {
271
+ const layerNumber = result.layerIndex + 1;
272
+ const duration = formatDuration(result.durationMs);
273
+ const passedText = `${result.successCount}/${result.spawnCount} passed`;
274
+ const statusColor = result.failureCount > 0
275
+ ? chalk.bold
276
+ : result.successCount === result.spawnCount
277
+ ? chalk.dim
278
+ : chalk.underline;
279
+ return `Layer ${layerNumber} (${result.spawnCount} ${result.spawnCount === 1 ? 'spawn' : 'spawns'}) — completed in ${duration} [${statusColor(passedText)}]`;
280
+ };
281
+ /**
282
+ * Format a phase summary for completion display
283
+ *
284
+ * @param result - Phase execution results
285
+ * @returns Formatted summary string
286
+ */
287
+ export const formatPhaseSummary = (result) => {
288
+ const emoji = getPhaseEmoji(result.phaseName);
289
+ const title = PHASE_TITLES[result.phaseName.toLowerCase()] || `${result.phaseName} Phase`;
290
+ const duration = formatDuration(result.durationMs);
291
+ const passedText = `${result.successCount}/${result.itemCount}`;
292
+ const statusIcon = result.failureCount > 0
293
+ ? chalk.bold('✗')
294
+ : result.successCount === result.itemCount
295
+ ? chalk.bold('✓')
296
+ : chalk.bold('⚠');
297
+ return `${emoji} ${title} ${statusIcon} — ${passedText} in ${duration}`;
298
+ };
299
+ /**
300
+ * Create a spawn task title
301
+ *
302
+ * @param itemId - Item identifier (epic number, story number, etc.)
303
+ * @param itemTitle - Optional item title/description
304
+ * @param agentType - Type of agent being spawned
305
+ * @returns Formatted spawn task title
306
+ */
307
+ export const createSpawnTitle = (itemId, itemTitle, agentType) => {
308
+ const prefix = agentType ? chalk.dim(`[${agentType}]`) : '';
309
+ const title = itemTitle ? `${itemId}: ${itemTitle}` : itemId;
310
+ return prefix ? `${prefix} ${title}` : title;
311
+ };
312
+ /**
313
+ * Truncate a string to a maximum length with ellipsis
314
+ *
315
+ * @param text - Text to truncate
316
+ * @param maxLength - Maximum length (default: 60)
317
+ * @returns Truncated string with ellipsis if needed
318
+ */
319
+ export const truncateText = (text, maxLength = 60) => {
320
+ if (text.length <= maxLength) {
321
+ return text;
322
+ }
323
+ return `${text.slice(0, maxLength - 3)}...`;
324
+ };
325
+ /**
326
+ * Format elapsed time for running tasks (updates every second)
327
+ *
328
+ * @param startTime - Start timestamp in milliseconds
329
+ * @returns Formatted elapsed time string
330
+ */
331
+ export const formatElapsedTime = (startTime) => {
332
+ const elapsed = Date.now() - startTime;
333
+ return formatDuration(elapsed);
334
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/bmad-workflow",
3
3
  "description": "AI-driven development workflow orchestration CLI for BMAD projects",
4
- "version": "1.0.16",
4
+ "version": "1.0.18",
5
5
  "author": {
6
6
  "name": "DevSquad",
7
7
  "email": "marcelo@devsquad.email",
@@ -23,6 +23,7 @@
23
23
  "fs-extra": "^11.3.2",
24
24
  "js-yaml": "^4.1.0",
25
25
  "jsonwebtoken": "^9.0.2",
26
+ "listr2": "^10.1.0",
26
27
  "nodemailer": "^7.0.9",
27
28
  "ora": "^9.0.0",
28
29
  "pino": "^10.0.0",
@@ -52,10 +53,10 @@
52
53
  "eslint": "^9",
53
54
  "eslint-config-oclif": "^6",
54
55
  "eslint-config-prettier": "^10",
56
+ "esmock": "^2.7.3",
55
57
  "mocha": "^10.8.2",
56
58
  "oclif": "^4",
57
59
  "prettier": "^3.6.2",
58
- "esmock": "^2.7.3",
59
60
  "proxyquire": "^2.1.3",
60
61
  "sinon": "^17.0.1",
61
62
  "ts-node": "^10.9.2",