@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/bin/dev +5 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/dev.js +5 -0
  6. package/bin/run +5 -0
  7. package/bin/run.cmd +3 -0
  8. package/bin/run.js +5 -0
  9. package/dist/commands/config/show.d.ts +34 -0
  10. package/dist/commands/config/show.js +108 -0
  11. package/dist/commands/config/validate.d.ts +29 -0
  12. package/dist/commands/config/validate.js +131 -0
  13. package/dist/commands/decompose.d.ts +79 -0
  14. package/dist/commands/decompose.js +327 -0
  15. package/dist/commands/demo.d.ts +18 -0
  16. package/dist/commands/demo.js +107 -0
  17. package/dist/commands/epics/create.d.ts +123 -0
  18. package/dist/commands/epics/create.js +459 -0
  19. package/dist/commands/epics/list.d.ts +120 -0
  20. package/dist/commands/epics/list.js +280 -0
  21. package/dist/commands/hello/index.d.ts +12 -0
  22. package/dist/commands/hello/index.js +34 -0
  23. package/dist/commands/hello/world.d.ts +8 -0
  24. package/dist/commands/hello/world.js +24 -0
  25. package/dist/commands/prd/fix.d.ts +39 -0
  26. package/dist/commands/prd/fix.js +140 -0
  27. package/dist/commands/prd/validate.d.ts +112 -0
  28. package/dist/commands/prd/validate.js +302 -0
  29. package/dist/commands/stories/create.d.ts +95 -0
  30. package/dist/commands/stories/create.js +431 -0
  31. package/dist/commands/stories/develop.d.ts +91 -0
  32. package/dist/commands/stories/develop.js +460 -0
  33. package/dist/commands/stories/list.d.ts +84 -0
  34. package/dist/commands/stories/list.js +291 -0
  35. package/dist/commands/stories/move.d.ts +66 -0
  36. package/dist/commands/stories/move.js +273 -0
  37. package/dist/commands/stories/qa.d.ts +99 -0
  38. package/dist/commands/stories/qa.js +530 -0
  39. package/dist/commands/workflow.d.ts +97 -0
  40. package/dist/commands/workflow.js +390 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/models/agent-options.d.ts +50 -0
  44. package/dist/models/agent-options.js +1 -0
  45. package/dist/models/agent-result.d.ts +29 -0
  46. package/dist/models/agent-result.js +1 -0
  47. package/dist/models/index.d.ts +10 -0
  48. package/dist/models/index.js +10 -0
  49. package/dist/models/phase-result.d.ts +65 -0
  50. package/dist/models/phase-result.js +7 -0
  51. package/dist/models/provider.d.ts +28 -0
  52. package/dist/models/provider.js +18 -0
  53. package/dist/models/story.d.ts +154 -0
  54. package/dist/models/story.js +18 -0
  55. package/dist/models/workflow-config.d.ts +148 -0
  56. package/dist/models/workflow-config.js +1 -0
  57. package/dist/models/workflow-result.d.ts +164 -0
  58. package/dist/models/workflow-result.js +7 -0
  59. package/dist/services/agents/agent-runner-factory.d.ts +31 -0
  60. package/dist/services/agents/agent-runner-factory.js +44 -0
  61. package/dist/services/agents/agent-runner.d.ts +46 -0
  62. package/dist/services/agents/agent-runner.js +29 -0
  63. package/dist/services/agents/claude-agent-runner.d.ts +81 -0
  64. package/dist/services/agents/claude-agent-runner.js +332 -0
  65. package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
  66. package/dist/services/agents/gemini-agent-runner.js +350 -0
  67. package/dist/services/agents/index.d.ts +7 -0
  68. package/dist/services/agents/index.js +7 -0
  69. package/dist/services/file-system/file-manager.d.ts +110 -0
  70. package/dist/services/file-system/file-manager.js +223 -0
  71. package/dist/services/file-system/glob-matcher.d.ts +75 -0
  72. package/dist/services/file-system/glob-matcher.js +126 -0
  73. package/dist/services/file-system/path-resolver.d.ts +183 -0
  74. package/dist/services/file-system/path-resolver.js +400 -0
  75. package/dist/services/logging/workflow-logger.d.ts +232 -0
  76. package/dist/services/logging/workflow-logger.js +552 -0
  77. package/dist/services/orchestration/batch-processor.d.ts +113 -0
  78. package/dist/services/orchestration/batch-processor.js +187 -0
  79. package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
  80. package/dist/services/orchestration/dependency-graph-executor.js +447 -0
  81. package/dist/services/orchestration/index.d.ts +10 -0
  82. package/dist/services/orchestration/index.js +8 -0
  83. package/dist/services/orchestration/input-detector.d.ts +125 -0
  84. package/dist/services/orchestration/input-detector.js +381 -0
  85. package/dist/services/orchestration/story-queue.d.ts +94 -0
  86. package/dist/services/orchestration/story-queue.js +170 -0
  87. package/dist/services/orchestration/story-type-detector.d.ts +80 -0
  88. package/dist/services/orchestration/story-type-detector.js +258 -0
  89. package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
  90. package/dist/services/orchestration/task-decomposition-service.js +607 -0
  91. package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
  92. package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
  93. package/dist/services/parsers/epic-parser.d.ts +117 -0
  94. package/dist/services/parsers/epic-parser.js +264 -0
  95. package/dist/services/parsers/prd-fixer.d.ts +86 -0
  96. package/dist/services/parsers/prd-fixer.js +194 -0
  97. package/dist/services/parsers/prd-parser.d.ts +123 -0
  98. package/dist/services/parsers/prd-parser.js +286 -0
  99. package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
  100. package/dist/services/parsers/standalone-story-parser.js +255 -0
  101. package/dist/services/parsers/story-parser-factory.d.ts +81 -0
  102. package/dist/services/parsers/story-parser-factory.js +108 -0
  103. package/dist/services/parsers/story-parser.d.ts +122 -0
  104. package/dist/services/parsers/story-parser.js +262 -0
  105. package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
  106. package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
  107. package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
  108. package/dist/services/scaffolding/file-scaffolder.js +314 -0
  109. package/dist/services/validation/config-validator.d.ts +88 -0
  110. package/dist/services/validation/config-validator.js +167 -0
  111. package/dist/types/task-graph.d.ts +142 -0
  112. package/dist/types/task-graph.js +5 -0
  113. package/dist/utils/colors.d.ts +49 -0
  114. package/dist/utils/colors.js +50 -0
  115. package/dist/utils/error-formatter.d.ts +64 -0
  116. package/dist/utils/error-formatter.js +279 -0
  117. package/dist/utils/errors.d.ts +170 -0
  118. package/dist/utils/errors.js +233 -0
  119. package/dist/utils/formatters.d.ts +84 -0
  120. package/dist/utils/formatters.js +162 -0
  121. package/dist/utils/logger.d.ts +63 -0
  122. package/dist/utils/logger.js +78 -0
  123. package/dist/utils/progress.d.ts +104 -0
  124. package/dist/utils/progress.js +161 -0
  125. package/dist/utils/retry.d.ts +114 -0
  126. package/dist/utils/retry.js +160 -0
  127. package/dist/utils/shared-flags.d.ts +28 -0
  128. package/dist/utils/shared-flags.js +43 -0
  129. package/package.json +119 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Stories List Command
3
+ *
4
+ * Lists all stories with filtering by status and epic number.
5
+ * Displays results in table format or JSON format with counts summary.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * bmad-workflow stories list
10
+ * bmad-workflow stories list --status=draft
11
+ * bmad-workflow stories list --epic=4
12
+ * bmad-workflow stories list --status=ready --epic=4 --json
13
+ * ```
14
+ */
15
+ import { Command, Flags } from '@oclif/core';
16
+ import chalk from 'chalk';
17
+ import { isEpicStory } from '../../models/story.js';
18
+ import { FileManager } from '../../services/file-system/file-manager.js';
19
+ import { PathResolver } from '../../services/file-system/path-resolver.js';
20
+ import { StoryParserFactory } from '../../services/parsers/story-parser-factory.js';
21
+ import { FileSystemError } from '../../utils/errors.js';
22
+ import { createLogger } from '../../utils/logger.js';
23
+ /**
24
+ * Stories List Command
25
+ *
26
+ * Lists all stories with optional filtering by status and epic number.
27
+ */
28
+ export default class StoriesListCommand extends Command {
29
+ static description = 'List all stories with optional filtering by status and epic';
30
+ static examples = [
31
+ {
32
+ command: '<%= config.bin %> <%= command.id %>',
33
+ description: 'List all stories',
34
+ },
35
+ {
36
+ command: '<%= config.bin %> <%= command.id %> --status=draft',
37
+ description: 'List only draft stories',
38
+ },
39
+ {
40
+ command: '<%= config.bin %> <%= command.id %> --epic=4',
41
+ description: 'List stories from epic 4',
42
+ },
43
+ {
44
+ command: '<%= config.bin %> <%= command.id %> --status=ready --epic=4 --json',
45
+ description: 'List ready stories from epic 4 as JSON',
46
+ },
47
+ ];
48
+ static flags = {
49
+ epic: Flags.integer({
50
+ description: 'Filter by epic number',
51
+ }),
52
+ json: Flags.boolean({
53
+ description: 'Output results as JSON',
54
+ }),
55
+ status: Flags.string({
56
+ description: 'Filter by story status (draft, ready, done)',
57
+ options: ['draft', 'ready', 'done', 'inprogress', 'failed'],
58
+ }),
59
+ };
60
+ // Service instances
61
+ fileManager;
62
+ logger;
63
+ pathResolver;
64
+ storyParserFactory;
65
+ /**
66
+ * Run command
67
+ */
68
+ async run() {
69
+ const { flags } = await this.parse(StoriesListCommand);
70
+ // Initialize services
71
+ this.initServices();
72
+ this.logger.info('Listing stories with filters: status=%s, epic=%s', flags.status, flags.epic);
73
+ try {
74
+ // Get story directory from PathResolver
75
+ const storyDir = this.pathResolver.getStoryDir();
76
+ this.logger.debug('Story directory: %s', storyDir);
77
+ // Read directory contents
78
+ const files = await this.fileManager.listFiles(storyDir, '*.md');
79
+ this.logger.info('Found %d markdown files in story directory', files.length);
80
+ // Parse story metadata from each file
81
+ const stories = await this.parseStories(files);
82
+ this.logger.info('Successfully parsed %d stories', stories.length);
83
+ // Apply filters
84
+ let filtered = this.applyFilters(stories, flags);
85
+ // Sort by story number
86
+ filtered = this.sortStories(filtered);
87
+ this.logger.info('After filtering and sorting: %d stories', filtered.length);
88
+ // Output results
89
+ if (flags.json) {
90
+ this.outputJson(filtered);
91
+ }
92
+ else {
93
+ this.outputTable(filtered);
94
+ }
95
+ }
96
+ catch (error) {
97
+ this.handleError(error);
98
+ }
99
+ }
100
+ /**
101
+ * Apply filters to stories
102
+ */
103
+ applyFilters(stories, flags) {
104
+ let filtered = stories;
105
+ // Filter by status
106
+ if (flags.status) {
107
+ filtered = filtered.filter((s) => s.status.toLowerCase() === flags.status.toLowerCase());
108
+ this.logger.debug('After status filter (%s): %d stories', flags.status, filtered.length);
109
+ }
110
+ // Filter by epic number (only applies to epic-based stories)
111
+ if (flags.epic !== undefined) {
112
+ filtered = filtered.filter((s) => isEpicStory(s) && s.epicNumber === flags.epic);
113
+ this.logger.debug('After epic filter (%d): %d stories', flags.epic, filtered.length);
114
+ }
115
+ return filtered;
116
+ }
117
+ /**
118
+ * Display count summary
119
+ */
120
+ displaySummary(stories) {
121
+ // Count by status
122
+ const statusCounts = {};
123
+ for (const story of stories) {
124
+ const status = story.status.toLowerCase();
125
+ statusCounts[status] = (statusCounts[status] || 0) + 1;
126
+ }
127
+ // Build summary message
128
+ const summaryParts = Object.entries(statusCounts)
129
+ .map(([status, count]) => `${count} ${status}`)
130
+ .join(', ');
131
+ this.log('');
132
+ this.log(chalk.white(`Found ${stories.length} stories: ${summaryParts}`));
133
+ this.log('');
134
+ }
135
+ /**
136
+ * Get color for status
137
+ */
138
+ getStatusColor(status) {
139
+ switch (status.toLowerCase()) {
140
+ case 'done': {
141
+ return 'green';
142
+ }
143
+ case 'draft': {
144
+ return 'yellow';
145
+ }
146
+ case 'failed': {
147
+ return 'red';
148
+ }
149
+ case 'ready': {
150
+ return 'blue';
151
+ }
152
+ default: {
153
+ return 'gray';
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Handle errors with user-friendly messages
159
+ */
160
+ handleError(error) {
161
+ this.logger.error('Error listing stories: %O', error);
162
+ if (error instanceof FileSystemError) {
163
+ const fsError = error;
164
+ if (error.message.includes('does not exist')) {
165
+ this.error(`Story directory not found: ${fsError.context?.dirPath || 'unknown'}\n` +
166
+ 'Check your .bmad-core/core-config.yaml configuration.', { exit: 1 });
167
+ }
168
+ if (error.message.includes('permission denied')) {
169
+ this.error(`Permission denied reading story directory: ${fsError.context?.dirPath || 'unknown'}\n` +
170
+ 'Check file permissions and try again.', { exit: 1 });
171
+ }
172
+ }
173
+ this.error(`Failed to list stories: ${error.message}`, { exit: 1 });
174
+ }
175
+ /**
176
+ * Initialize services
177
+ */
178
+ initServices() {
179
+ this.logger = createLogger({ namespace: 'commands:stories:list' });
180
+ this.fileManager = new FileManager(this.logger);
181
+ this.pathResolver = new PathResolver(this.fileManager, this.logger);
182
+ this.storyParserFactory = new StoryParserFactory(this.fileManager, this.logger);
183
+ }
184
+ /**
185
+ * Output stories as JSON
186
+ */
187
+ outputJson(stories) {
188
+ const output = stories.map((story) => {
189
+ const base = {
190
+ filePath: story.filePath,
191
+ id: story.id,
192
+ status: story.status,
193
+ title: story.title,
194
+ type: story.type,
195
+ };
196
+ if (isEpicStory(story)) {
197
+ return {
198
+ ...base,
199
+ epic: story.epicNumber,
200
+ number: story.number,
201
+ storyNumber: story.storyNumber,
202
+ };
203
+ }
204
+ return {
205
+ ...base,
206
+ category: story.category,
207
+ };
208
+ });
209
+ this.log(JSON.stringify(output, null, 2));
210
+ }
211
+ /**
212
+ * Output stories as table
213
+ */
214
+ outputTable(stories) {
215
+ if (stories.length === 0) {
216
+ this.log(chalk.yellow('No stories found matching criteria'));
217
+ return;
218
+ }
219
+ // Create simple table
220
+ this.log('');
221
+ this.log(chalk.cyan.bold('Story ID'.padEnd(30)) +
222
+ chalk.cyan.bold('Title'.padEnd(45)) +
223
+ chalk.cyan.bold('Status'.padEnd(15)) +
224
+ chalk.cyan.bold('Type'));
225
+ this.log(chalk.gray('─'.repeat(100)));
226
+ // Add rows
227
+ for (const story of stories) {
228
+ const statusColor = this.getStatusColor(story.status);
229
+ const truncatedTitle = this.truncate(story.title, 43);
230
+ const storyId = isEpicStory(story) ? story.number : story.id;
231
+ const typeLabel = isEpicStory(story) ? `Epic ${story.epicNumber}` : story.category || 'Standalone';
232
+ this.log(storyId.padEnd(30) +
233
+ truncatedTitle.padEnd(45) +
234
+ chalk[statusColor](story.status.padEnd(15)) +
235
+ chalk.gray(typeLabel));
236
+ }
237
+ // Display summary
238
+ this.displaySummary(stories);
239
+ }
240
+ /**
241
+ * Parse stories from file paths
242
+ */
243
+ async parseStories(files) {
244
+ const stories = [];
245
+ for (const file of files) {
246
+ try {
247
+ // Sequential parsing to maintain order and reduce memory usage
248
+ // eslint-disable-next-line no-await-in-loop
249
+ const metadata = await this.storyParserFactory.parseStory(file);
250
+ stories.push(metadata);
251
+ }
252
+ catch (error) {
253
+ this.logger.warn({ err: error, file }, 'Failed to parse story');
254
+ // Continue processing other files
255
+ }
256
+ }
257
+ return stories;
258
+ }
259
+ /**
260
+ * Sort stories by type and ID
261
+ *
262
+ * Epic stories are sorted by epic.story number ascending.
263
+ * Standalone stories are sorted alphabetically by ID.
264
+ * Epic stories appear before standalone stories.
265
+ */
266
+ sortStories(stories) {
267
+ return stories.sort((a, b) => {
268
+ // Epic stories come before standalone stories
269
+ if (isEpicStory(a) && !isEpicStory(b))
270
+ return -1;
271
+ if (!isEpicStory(a) && isEpicStory(b))
272
+ return 1;
273
+ // Both epic stories: sort by epic.story number
274
+ if (isEpicStory(a) && isEpicStory(b)) {
275
+ // Compare epic numbers first
276
+ if (a.epicNumber !== b.epicNumber)
277
+ return a.epicNumber - b.epicNumber;
278
+ // Then compare story numbers
279
+ return a.storyNumber - b.storyNumber;
280
+ }
281
+ // Both standalone: sort alphabetically by ID
282
+ return a.id.localeCompare(b.id);
283
+ });
284
+ }
285
+ /**
286
+ * Truncate string to max length
287
+ */
288
+ truncate(str, maxLength) {
289
+ return str.length > maxLength ? str.slice(0, maxLength - 3) + '...' : str;
290
+ }
291
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Stories Move Command
3
+ *
4
+ * Moves story files from active stories directory to QA directory.
5
+ * Supports glob patterns for selecting multiple files and requires confirmation
6
+ * unless --force flag is used.
7
+ *
8
+ * @example
9
+ * ```bash
10
+ * bmad-workflow stories move "4.7-*.md"
11
+ * bmad-workflow stories move "4.*-*.md" --force
12
+ * bmad-workflow stories move "*.md"
13
+ * ```
14
+ */
15
+ import { Command } from '@oclif/core';
16
+ /**
17
+ * Stories Move Command
18
+ *
19
+ * Moves story files from active directory (docs/stories) to QA directory (docs/qa/stories).
20
+ * Provides confirmation before moving (unless --force flag is used) and displays progress.
21
+ */
22
+ export default class StoriesMoveCommand extends Command {
23
+ static args: {
24
+ pattern: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
25
+ };
26
+ static description: string;
27
+ static examples: {
28
+ command: string;
29
+ description: string;
30
+ }[];
31
+ static flags: {
32
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
33
+ };
34
+ private fileManager;
35
+ private globMatcher;
36
+ private logger;
37
+ private pathResolver;
38
+ /**
39
+ * Run command
40
+ */
41
+ run(): Promise<void>;
42
+ /**
43
+ * Confirm move operation with user
44
+ */
45
+ private confirmMove;
46
+ /**
47
+ * Display list of files to be moved
48
+ */
49
+ private displayFileList;
50
+ /**
51
+ * Display summary of move operation
52
+ */
53
+ private displaySummary;
54
+ /**
55
+ * Handle errors with user-friendly messages
56
+ */
57
+ private handleError;
58
+ /**
59
+ * Initialize services
60
+ */
61
+ private initServices;
62
+ /**
63
+ * Move files from source to QA directory
64
+ */
65
+ private moveFiles;
66
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Stories Move Command
3
+ *
4
+ * Moves story files from active stories directory to QA directory.
5
+ * Supports glob patterns for selecting multiple files and requires confirmation
6
+ * unless --force flag is used.
7
+ *
8
+ * @example
9
+ * ```bash
10
+ * bmad-workflow stories move "4.7-*.md"
11
+ * bmad-workflow stories move "4.*-*.md" --force
12
+ * bmad-workflow stories move "*.md"
13
+ * ```
14
+ */
15
+ import { Args, Command, Flags } from '@oclif/core';
16
+ import chalk from 'chalk';
17
+ import path from 'node:path';
18
+ import * as readline from 'node:readline';
19
+ import ora from 'ora';
20
+ import { FileManager } from '../../services/file-system/file-manager.js';
21
+ import { GlobMatcher } from '../../services/file-system/glob-matcher.js';
22
+ import { PathResolver } from '../../services/file-system/path-resolver.js';
23
+ import { FileSystemError } from '../../utils/errors.js';
24
+ import { createLogger } from '../../utils/logger.js';
25
+ /**
26
+ * Stories Move Command
27
+ *
28
+ * Moves story files from active directory (docs/stories) to QA directory (docs/qa/stories).
29
+ * Provides confirmation before moving (unless --force flag is used) and displays progress.
30
+ */
31
+ export default class StoriesMoveCommand extends Command {
32
+ static args = {
33
+ pattern: Args.string({
34
+ description: 'Glob pattern to match story files (e.g., "4.7-*.md", "*.md")',
35
+ required: true,
36
+ }),
37
+ };
38
+ static description = 'Move story files from active to QA directory';
39
+ static examples = [
40
+ {
41
+ command: '<%= config.bin %> <%= command.id %> "4.7-*.md"',
42
+ description: 'Move all stories matching pattern 4.7-*.md',
43
+ },
44
+ {
45
+ command: '<%= config.bin %> <%= command.id %> "4.*-*.md" --force',
46
+ description: 'Move all epic 4 stories without confirmation',
47
+ },
48
+ {
49
+ command: '<%= config.bin %> <%= command.id %> "STORY-123-*.md"',
50
+ description: 'Move specific story file',
51
+ },
52
+ ];
53
+ static flags = {
54
+ force: Flags.boolean({
55
+ char: 'f',
56
+ default: false,
57
+ description: 'Skip confirmation prompt',
58
+ }),
59
+ };
60
+ // Service instances
61
+ fileManager;
62
+ globMatcher;
63
+ logger;
64
+ pathResolver;
65
+ /**
66
+ * Run command
67
+ */
68
+ async run() {
69
+ const { args, flags } = await this.parse(StoriesMoveCommand);
70
+ // Initialize services
71
+ this.initServices();
72
+ this.logger.info('Moving stories with pattern: %s, force: %s', args.pattern, flags.force);
73
+ try {
74
+ // Get directories from PathResolver
75
+ const storyDir = this.pathResolver.getStoryDir();
76
+ const qaStoryDir = this.pathResolver.getQaStoryDir();
77
+ this.logger.debug('Source directory: %s', storyDir);
78
+ this.logger.debug('Destination directory: %s', qaStoryDir);
79
+ // Expand pattern to find matching files
80
+ const fullPattern = path.join(storyDir, args.pattern);
81
+ const matchedFiles = await this.globMatcher.expandPattern(fullPattern);
82
+ // Filter to only include files in the stories directory
83
+ const filteredFiles = matchedFiles.filter((file) => file.startsWith(storyDir));
84
+ this.logger.info('Found %d files matching pattern', filteredFiles.length);
85
+ if (filteredFiles.length === 0) {
86
+ this.log(chalk.yellow(`No stories found matching pattern: ${args.pattern}`));
87
+ return;
88
+ }
89
+ // Display list of files to be moved
90
+ this.displayFileList(filteredFiles, storyDir);
91
+ // Get confirmation unless --force flag is set
92
+ if (flags.force) {
93
+ this.logger.info('Skipping confirmation due to --force flag');
94
+ }
95
+ else {
96
+ const confirmed = await this.confirmMove(filteredFiles.length);
97
+ if (!confirmed) {
98
+ this.log(chalk.yellow('Operation cancelled'));
99
+ this.logger.info('User cancelled operation');
100
+ return;
101
+ }
102
+ }
103
+ // Move files
104
+ const results = await this.moveFiles(filteredFiles, storyDir, qaStoryDir);
105
+ // Display summary
106
+ this.displaySummary(results);
107
+ // Exit with appropriate code
108
+ const failed = results.filter((r) => r.status === 'failed').length;
109
+ if (failed > 0) {
110
+ this.logger.error('Move operation completed with %d failures', failed);
111
+ this.exit(1);
112
+ }
113
+ this.logger.info('Move operation completed successfully');
114
+ }
115
+ catch (error) {
116
+ this.handleError(error);
117
+ }
118
+ }
119
+ /**
120
+ * Confirm move operation with user
121
+ */
122
+ async confirmMove(fileCount) {
123
+ this.log(''); // Blank line for spacing
124
+ return new Promise((resolve) => {
125
+ const rl = readline.createInterface({
126
+ input: process.stdin,
127
+ output: process.stdout,
128
+ });
129
+ rl.question(chalk.yellow(`Move ${fileCount} file${fileCount === 1 ? '' : 's'} to QA directory? (Y/N) `), (answer) => {
130
+ rl.close();
131
+ const normalized = answer.trim().toLowerCase();
132
+ resolve(normalized === 'y' || normalized === 'yes');
133
+ });
134
+ });
135
+ }
136
+ /**
137
+ * Display list of files to be moved
138
+ */
139
+ displayFileList(files, baseDir) {
140
+ this.log('');
141
+ this.log(chalk.cyan.bold('Files to be moved:'));
142
+ for (const file of files) {
143
+ const relativePath = path.relative(baseDir, file);
144
+ this.log(chalk.white(` - ${relativePath}`));
145
+ }
146
+ }
147
+ /**
148
+ * Display summary of move operation
149
+ */
150
+ displaySummary(results) {
151
+ const moved = results.filter((r) => r.status === 'moved').length;
152
+ const skipped = results.filter((r) => r.status === 'skipped').length;
153
+ const failed = results.filter((r) => r.status === 'failed').length;
154
+ this.log('');
155
+ this.log(chalk.white('─'.repeat(60)));
156
+ this.log(chalk.bold('Move Summary:'));
157
+ if (moved > 0) {
158
+ this.log(chalk.green(` ✓ Moved: ${moved} file${moved === 1 ? '' : 's'}`));
159
+ }
160
+ if (skipped > 0) {
161
+ this.log(chalk.yellow(` ⚠ Skipped: ${skipped} file${skipped === 1 ? '' : 's'}`));
162
+ }
163
+ if (failed > 0) {
164
+ this.log(chalk.red(` ✗ Failed: ${failed} file${failed === 1 ? '' : 's'}`));
165
+ }
166
+ this.log(chalk.white('─'.repeat(60)));
167
+ this.log('');
168
+ // Log skipped files with reasons
169
+ const skippedResults = results.filter((r) => r.status === 'skipped');
170
+ if (skippedResults.length > 0) {
171
+ this.log(chalk.yellow.bold('Skipped files:'));
172
+ for (const result of skippedResults) {
173
+ this.log(chalk.yellow(` ${path.basename(result.source)}: ${result.reason || 'Unknown reason'}`));
174
+ }
175
+ this.log('');
176
+ }
177
+ // Log failed files with errors
178
+ const failedResults = results.filter((r) => r.status === 'failed');
179
+ if (failedResults.length > 0) {
180
+ this.log(chalk.red.bold('Failed files:'));
181
+ for (const result of failedResults) {
182
+ this.log(chalk.red(` ${path.basename(result.source)}: ${result.error || 'Unknown error'}`));
183
+ }
184
+ this.log('');
185
+ }
186
+ }
187
+ /**
188
+ * Handle errors with user-friendly messages
189
+ */
190
+ handleError(error) {
191
+ this.logger.error('Error moving stories: %O', error);
192
+ if (error instanceof FileSystemError) {
193
+ const fsError = error;
194
+ if (error.message.includes('does not exist')) {
195
+ this.error(`Directory not found: ${fsError.context?.dirPath || 'unknown'}\n` +
196
+ 'Check your .bmad-core/core-config.yaml configuration.', { exit: 1 });
197
+ }
198
+ if (error.message.includes('permission denied')) {
199
+ this.error(`Permission denied: ${fsError.context?.dirPath || 'unknown'}\nCheck file permissions and try again.`, { exit: 1 });
200
+ }
201
+ if (error.message.includes('Glob pattern cannot be empty')) {
202
+ this.error('Pattern cannot be empty. Please provide a valid glob pattern.', { exit: 1 });
203
+ }
204
+ }
205
+ this.error(`Failed to move stories: ${error.message}`, { exit: 1 });
206
+ }
207
+ /**
208
+ * Initialize services
209
+ */
210
+ initServices() {
211
+ this.logger = createLogger({ namespace: 'commands:stories:move' });
212
+ this.fileManager = new FileManager(this.logger);
213
+ this.pathResolver = new PathResolver(this.fileManager, this.logger);
214
+ this.globMatcher = new GlobMatcher(this.fileManager, this.logger);
215
+ }
216
+ /**
217
+ * Move files from source to QA directory
218
+ */
219
+ async moveFiles(files, sourceDir, destDir) {
220
+ const results = [];
221
+ const spinner = ora('Moving files...').start();
222
+ // Process files sequentially to show progress for each file
223
+ /* eslint-disable no-await-in-loop */
224
+ for (const sourceFile of files) {
225
+ const fileName = path.basename(sourceFile);
226
+ const destFile = path.join(destDir, fileName);
227
+ spinner.text = `Moving ${fileName}...`;
228
+ this.logger.info('Moving file: %s -> %s', sourceFile, destFile);
229
+ try {
230
+ // Check if destination file exists
231
+ const destExists = await this.fileManager.fileExists(destFile);
232
+ if (destExists) {
233
+ spinner.warn(chalk.yellow(`Skipped ${fileName} (file already exists in destination)`));
234
+ this.logger.warn('Destination file already exists: %s', destFile);
235
+ results.push({
236
+ destination: destFile,
237
+ reason: 'File already exists in destination',
238
+ source: sourceFile,
239
+ status: 'skipped',
240
+ });
241
+ continue;
242
+ }
243
+ // Move file
244
+ await this.fileManager.moveFile(sourceFile, destFile);
245
+ spinner.succeed(chalk.green(`✓ Moved ${fileName}`));
246
+ this.logger.info('Successfully moved file: %s -> %s', sourceFile, destFile);
247
+ results.push({
248
+ destination: destFile,
249
+ source: sourceFile,
250
+ status: 'moved',
251
+ });
252
+ }
253
+ catch (error) {
254
+ const err = error;
255
+ spinner.fail(chalk.red(`✗ Failed to move ${fileName}: ${err.message}`));
256
+ this.logger.error('Error moving file %s: %O', sourceFile, err);
257
+ results.push({
258
+ destination: destFile,
259
+ error: err.message,
260
+ source: sourceFile,
261
+ status: 'failed',
262
+ });
263
+ }
264
+ // Restart spinner for next iteration
265
+ if (files.indexOf(sourceFile) < files.length - 1) {
266
+ spinner.start();
267
+ }
268
+ }
269
+ /* eslint-enable no-await-in-loop */
270
+ spinner.stop();
271
+ return results;
272
+ }
273
+ }