@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,223 @@
|
|
|
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 fs from 'fs-extra';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { FileSystemError } from '../../utils/errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* FileManager service for all file system operations
|
|
12
|
+
*
|
|
13
|
+
* Abstracts file system operations behind a consistent interface with
|
|
14
|
+
* comprehensive error handling and logging. All methods use async/await
|
|
15
|
+
* for consistency.
|
|
16
|
+
*/
|
|
17
|
+
export class FileManager {
|
|
18
|
+
/**
|
|
19
|
+
* Logger instance for file operations
|
|
20
|
+
*/
|
|
21
|
+
logger;
|
|
22
|
+
/**
|
|
23
|
+
* Create a new FileManager instance
|
|
24
|
+
*
|
|
25
|
+
* @param logger - Pino logger instance for logging file operations
|
|
26
|
+
* @example
|
|
27
|
+
* const logger = createLogger({ namespace: 'services:file-system' })
|
|
28
|
+
* const fileManager = new FileManager(logger)
|
|
29
|
+
*/
|
|
30
|
+
constructor(logger) {
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a directory (and parent directories if needed)
|
|
35
|
+
*
|
|
36
|
+
* Uses ensureDir which is idempotent - safe to call if directory already exists.
|
|
37
|
+
*
|
|
38
|
+
* @param path - Path to the directory to create
|
|
39
|
+
* @throws {FileSystemError} If directory cannot be created (permission denied, invalid path, etc.)
|
|
40
|
+
* @example
|
|
41
|
+
* await fileManager.createDirectory('docs/epics')
|
|
42
|
+
*/
|
|
43
|
+
async createDirectory(path) {
|
|
44
|
+
this.logger.info('Creating directory: %s', path);
|
|
45
|
+
try {
|
|
46
|
+
await fs.ensureDir(path);
|
|
47
|
+
this.logger.info('Directory created successfully: %s', path);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const err = error;
|
|
51
|
+
this.logger.error('Error creating directory %s: %O', path, err);
|
|
52
|
+
throw new FileSystemError(`Failed to create directory: ${path}: ${err.message}`, {
|
|
53
|
+
operation: 'createDirectory',
|
|
54
|
+
originalError: err.message,
|
|
55
|
+
path,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a file or directory exists
|
|
61
|
+
*
|
|
62
|
+
* @param path - Path to check for existence
|
|
63
|
+
* @returns True if path exists, false otherwise
|
|
64
|
+
* @example
|
|
65
|
+
* const exists = await fileManager.fileExists('docs/prd.md')
|
|
66
|
+
* if (!exists) {
|
|
67
|
+
* throw new Error('PRD not found')
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
async fileExists(path) {
|
|
71
|
+
this.logger.debug('Checking file existence: %s', path);
|
|
72
|
+
try {
|
|
73
|
+
const exists = await fs.pathExists(path);
|
|
74
|
+
this.logger.debug('File existence check result for %s: %s', path, exists);
|
|
75
|
+
return exists;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// pathExists should not throw, but handle defensively
|
|
79
|
+
const err = error;
|
|
80
|
+
this.logger.error('Error checking file existence %s: %O', path, err);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* List files in a directory matching an optional pattern
|
|
86
|
+
*
|
|
87
|
+
* @param directory - Directory to list files from
|
|
88
|
+
* @param pattern - Optional glob pattern to filter files (e.g., '*.md', 'STORY-*.md')
|
|
89
|
+
* @returns Array of file paths matching the pattern
|
|
90
|
+
* @throws {FileSystemError} If directory cannot be read
|
|
91
|
+
* @example
|
|
92
|
+
* const storyFiles = await fileManager.listFiles('docs/stories', 'STORY-*.md')
|
|
93
|
+
*/
|
|
94
|
+
async listFiles(directory, pattern) {
|
|
95
|
+
this.logger.info('Listing files in directory: %s with pattern: %s', directory, pattern ?? 'none');
|
|
96
|
+
try {
|
|
97
|
+
const files = await fs.readdir(directory);
|
|
98
|
+
// Filter files by pattern if provided
|
|
99
|
+
const filteredFiles = pattern ? files.filter((file) => this.matchesPattern(file, pattern)) : files;
|
|
100
|
+
const filePaths = filteredFiles.map((file) => `${directory}/${file}`);
|
|
101
|
+
this.logger.info('Found %d files in directory: %s', filePaths.length, directory);
|
|
102
|
+
return filePaths;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const err = error;
|
|
106
|
+
this.logger.error('Error listing files in directory %s: %O', directory, err);
|
|
107
|
+
throw new FileSystemError(`Failed to list files in directory: ${directory}: ${err.message}`, {
|
|
108
|
+
directory,
|
|
109
|
+
operation: 'listFiles',
|
|
110
|
+
originalError: err.message,
|
|
111
|
+
pattern,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Move a file from source to destination
|
|
117
|
+
*
|
|
118
|
+
* Creates destination directory if it doesn't exist.
|
|
119
|
+
* Overwrites destination file if it exists.
|
|
120
|
+
*
|
|
121
|
+
* @param source - Source file path
|
|
122
|
+
* @param dest - Destination file path
|
|
123
|
+
* @throws {FileSystemError} If file cannot be moved (source not found, permission denied, etc.)
|
|
124
|
+
* @example
|
|
125
|
+
* await fileManager.moveFile('docs/stories/1.1-story.md', 'docs/qa/stories/1.1-story.md')
|
|
126
|
+
*/
|
|
127
|
+
async moveFile(source, dest) {
|
|
128
|
+
this.logger.info('Moving file from %s to %s', source, dest);
|
|
129
|
+
try {
|
|
130
|
+
// Ensure destination directory exists
|
|
131
|
+
await fs.ensureDir(dirname(dest));
|
|
132
|
+
// Move file with overwrite
|
|
133
|
+
await fs.move(source, dest, { overwrite: true });
|
|
134
|
+
this.logger.info('File moved successfully from %s to %s', source, dest);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const err = error;
|
|
138
|
+
this.logger.error('Error moving file from %s to %s: %O', source, dest, err);
|
|
139
|
+
throw new FileSystemError(`Failed to move file from ${source} to ${dest}: ${err.message}`, {
|
|
140
|
+
dest,
|
|
141
|
+
operation: 'moveFile',
|
|
142
|
+
originalError: err.message,
|
|
143
|
+
source,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read file contents as UTF-8 string
|
|
149
|
+
*
|
|
150
|
+
* @param path - Path to the file to read
|
|
151
|
+
* @returns File content as string
|
|
152
|
+
* @throws {FileSystemError} If file cannot be read (not found, permission denied, etc.)
|
|
153
|
+
* @example
|
|
154
|
+
* const content = await fileManager.readFile('docs/prd.md')
|
|
155
|
+
*/
|
|
156
|
+
async readFile(path) {
|
|
157
|
+
this.logger.info('Reading file: %s', path);
|
|
158
|
+
try {
|
|
159
|
+
const content = await fs.readFile(path, 'utf8');
|
|
160
|
+
this.logger.info('File read successfully: %s', path);
|
|
161
|
+
return content;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const err = error;
|
|
165
|
+
this.logger.error('Error reading file %s: %O', path, err);
|
|
166
|
+
throw new FileSystemError(`Failed to read file: ${path}: ${err.message}`, {
|
|
167
|
+
operation: 'readFile',
|
|
168
|
+
originalError: err.message,
|
|
169
|
+
path,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Write content to file as UTF-8
|
|
175
|
+
*
|
|
176
|
+
* Creates parent directories if they don't exist.
|
|
177
|
+
* Overwrites existing file if present.
|
|
178
|
+
*
|
|
179
|
+
* @param path - Path to the file to write
|
|
180
|
+
* @param content - Content to write to file
|
|
181
|
+
* @throws {FileSystemError} If file cannot be written (permission denied, invalid path, etc.)
|
|
182
|
+
* @example
|
|
183
|
+
* await fileManager.writeFile('docs/epics/epic-1.md', epicContent)
|
|
184
|
+
*/
|
|
185
|
+
async writeFile(path, content) {
|
|
186
|
+
this.logger.info('Writing file: %s', path);
|
|
187
|
+
try {
|
|
188
|
+
// Ensure parent directory exists
|
|
189
|
+
await fs.ensureDir(dirname(path));
|
|
190
|
+
// Write file with UTF-8 encoding
|
|
191
|
+
await fs.writeFile(path, content, 'utf8');
|
|
192
|
+
this.logger.info('File written successfully: %s', path);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const err = error;
|
|
196
|
+
this.logger.error('Error writing file %s: %O', path, err);
|
|
197
|
+
throw new FileSystemError(`Failed to write file: ${path}: ${err.message}`, {
|
|
198
|
+
operation: 'writeFile',
|
|
199
|
+
originalError: err.message,
|
|
200
|
+
path,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Match a file name against a simple glob pattern
|
|
206
|
+
*
|
|
207
|
+
* Supports * wildcard only (e.g., 'STORY-*.md', '*.txt')
|
|
208
|
+
*
|
|
209
|
+
* @param fileName - File name to check
|
|
210
|
+
* @param pattern - Glob pattern with * wildcards
|
|
211
|
+
* @returns True if file matches pattern
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
matchesPattern(fileName, pattern) {
|
|
215
|
+
// Convert glob pattern to regex
|
|
216
|
+
// Escape special regex characters except *
|
|
217
|
+
const regexPattern = pattern
|
|
218
|
+
.replaceAll(/[.+?^${}()|[\]\\]/g, String.raw `\$&`) // Escape special chars
|
|
219
|
+
.replaceAll('*', '.*'); // Convert * to .*
|
|
220
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
221
|
+
return regex.test(fileName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobMatcher Service
|
|
3
|
+
*
|
|
4
|
+
* Provides glob pattern matching functionality with wildcard support.
|
|
5
|
+
* Enables flexible file pattern matching for story and epic file selection.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import { FileManager } from './file-manager.js';
|
|
9
|
+
/**
|
|
10
|
+
* GlobMatcher service for file pattern matching
|
|
11
|
+
*
|
|
12
|
+
* Expands glob patterns (with * and ? wildcards) to lists of matching files.
|
|
13
|
+
* All results are filtered to existing files only and sorted alphabetically.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const globMatcher = new GlobMatcher(fileManager, logger)
|
|
17
|
+
* const files = await globMatcher.expandPattern('docs/stories/AUTH-*.md')
|
|
18
|
+
* // Returns: ['docs/stories/AUTH-1.md', 'docs/stories/AUTH-2.md']
|
|
19
|
+
*/
|
|
20
|
+
export declare class GlobMatcher {
|
|
21
|
+
/**
|
|
22
|
+
* FileManager instance for file existence checks
|
|
23
|
+
*/
|
|
24
|
+
private readonly fileManager;
|
|
25
|
+
/**
|
|
26
|
+
* Logger instance for glob operations
|
|
27
|
+
*/
|
|
28
|
+
private readonly logger;
|
|
29
|
+
/**
|
|
30
|
+
* Create a new GlobMatcher instance
|
|
31
|
+
*
|
|
32
|
+
* @param fileManager - FileManager instance for file operations
|
|
33
|
+
* @param logger - Pino logger instance for logging glob operations
|
|
34
|
+
* @example
|
|
35
|
+
* const logger = createLogger({ namespace: 'services:file-system:glob-matcher' })
|
|
36
|
+
* const fileManager = new FileManager(logger)
|
|
37
|
+
* const globMatcher = new GlobMatcher(fileManager, logger)
|
|
38
|
+
*/
|
|
39
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
40
|
+
/**
|
|
41
|
+
* Expand glob pattern to list of matching file paths
|
|
42
|
+
*
|
|
43
|
+
* Supports wildcards:
|
|
44
|
+
* - * matches zero or more characters (e.g., AUTH-* matches AUTH-1.md, AUTH-feature.md)
|
|
45
|
+
* - ? matches exactly one character (e.g., file?.txt matches file1.txt, fileA.txt)
|
|
46
|
+
* - ** matches directories recursively (e.g., docs/** /AUTH-* matches files in any subdirectory)
|
|
47
|
+
*
|
|
48
|
+
* Results are filtered to only include existing files and sorted alphabetically (case-insensitive).
|
|
49
|
+
*
|
|
50
|
+
* @param pattern - Glob pattern with wildcards (* and ?)
|
|
51
|
+
* @returns Array of matching file paths, sorted alphabetically (empty array if no matches)
|
|
52
|
+
* @throws {FileSystemError} If pattern expansion fails
|
|
53
|
+
* @example
|
|
54
|
+
* // Simple wildcard
|
|
55
|
+
* const files = await globMatcher.expandPattern('docs/stories/*.md')
|
|
56
|
+
*
|
|
57
|
+
* // Multiple wildcards
|
|
58
|
+
* const files = await globMatcher.expandPattern('docs/* /AUTH-*.md')
|
|
59
|
+
*
|
|
60
|
+
* // Question mark wildcard
|
|
61
|
+
* const files = await globMatcher.expandPattern('file?.txt')
|
|
62
|
+
*/
|
|
63
|
+
expandPattern(pattern: string): Promise<string[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Filter paths to only include existing files
|
|
66
|
+
*
|
|
67
|
+
* Uses FileManager to check file existence for each path.
|
|
68
|
+
* This provides a double-check beyond fast-glob's results.
|
|
69
|
+
*
|
|
70
|
+
* @param paths - Array of file paths to filter
|
|
71
|
+
* @returns Array of paths that exist
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
private filterExisting;
|
|
75
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobMatcher Service
|
|
3
|
+
*
|
|
4
|
+
* Provides glob pattern matching functionality with wildcard support.
|
|
5
|
+
* Enables flexible file pattern matching for story and epic file selection.
|
|
6
|
+
*/
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
import { FileSystemError } from '../../utils/errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* GlobMatcher service for file pattern matching
|
|
11
|
+
*
|
|
12
|
+
* Expands glob patterns (with * and ? wildcards) to lists of matching files.
|
|
13
|
+
* All results are filtered to existing files only and sorted alphabetically.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const globMatcher = new GlobMatcher(fileManager, logger)
|
|
17
|
+
* const files = await globMatcher.expandPattern('docs/stories/AUTH-*.md')
|
|
18
|
+
* // Returns: ['docs/stories/AUTH-1.md', 'docs/stories/AUTH-2.md']
|
|
19
|
+
*/
|
|
20
|
+
export class GlobMatcher {
|
|
21
|
+
/**
|
|
22
|
+
* FileManager instance for file existence checks
|
|
23
|
+
*/
|
|
24
|
+
fileManager;
|
|
25
|
+
/**
|
|
26
|
+
* Logger instance for glob operations
|
|
27
|
+
*/
|
|
28
|
+
logger;
|
|
29
|
+
/**
|
|
30
|
+
* Create a new GlobMatcher instance
|
|
31
|
+
*
|
|
32
|
+
* @param fileManager - FileManager instance for file operations
|
|
33
|
+
* @param logger - Pino logger instance for logging glob operations
|
|
34
|
+
* @example
|
|
35
|
+
* const logger = createLogger({ namespace: 'services:file-system:glob-matcher' })
|
|
36
|
+
* const fileManager = new FileManager(logger)
|
|
37
|
+
* const globMatcher = new GlobMatcher(fileManager, logger)
|
|
38
|
+
*/
|
|
39
|
+
constructor(fileManager, logger) {
|
|
40
|
+
this.fileManager = fileManager;
|
|
41
|
+
this.logger = logger;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Expand glob pattern to list of matching file paths
|
|
45
|
+
*
|
|
46
|
+
* Supports wildcards:
|
|
47
|
+
* - * matches zero or more characters (e.g., AUTH-* matches AUTH-1.md, AUTH-feature.md)
|
|
48
|
+
* - ? matches exactly one character (e.g., file?.txt matches file1.txt, fileA.txt)
|
|
49
|
+
* - ** matches directories recursively (e.g., docs/** /AUTH-* matches files in any subdirectory)
|
|
50
|
+
*
|
|
51
|
+
* Results are filtered to only include existing files and sorted alphabetically (case-insensitive).
|
|
52
|
+
*
|
|
53
|
+
* @param pattern - Glob pattern with wildcards (* and ?)
|
|
54
|
+
* @returns Array of matching file paths, sorted alphabetically (empty array if no matches)
|
|
55
|
+
* @throws {FileSystemError} If pattern expansion fails
|
|
56
|
+
* @example
|
|
57
|
+
* // Simple wildcard
|
|
58
|
+
* const files = await globMatcher.expandPattern('docs/stories/*.md')
|
|
59
|
+
*
|
|
60
|
+
* // Multiple wildcards
|
|
61
|
+
* const files = await globMatcher.expandPattern('docs/* /AUTH-*.md')
|
|
62
|
+
*
|
|
63
|
+
* // Question mark wildcard
|
|
64
|
+
* const files = await globMatcher.expandPattern('file?.txt')
|
|
65
|
+
*/
|
|
66
|
+
async expandPattern(pattern) {
|
|
67
|
+
this.logger.debug({ operation: 'expandPattern', pattern }, 'Expanding glob pattern');
|
|
68
|
+
// Validate pattern is not empty
|
|
69
|
+
if (!pattern || pattern.trim().length === 0) {
|
|
70
|
+
this.logger.error({ operation: 'expandPattern', pattern }, 'Empty glob pattern provided');
|
|
71
|
+
throw new FileSystemError('Glob pattern cannot be empty', {
|
|
72
|
+
operation: 'expandPattern',
|
|
73
|
+
pattern,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
// Expand glob pattern using fast-glob
|
|
78
|
+
const matches = await fg(pattern, {
|
|
79
|
+
absolute: false, // Return relative paths
|
|
80
|
+
dot: false, // Don't match dotfiles
|
|
81
|
+
ignore: ['node_modules/**', '.git/**', 'dist/**', 'coverage/**'], // Ignore common directories
|
|
82
|
+
onlyFiles: true, // Only return files, not directories
|
|
83
|
+
});
|
|
84
|
+
this.logger.debug({ matchCount: matches.length, operation: 'expandPattern', pattern }, 'Fast-glob expansion complete');
|
|
85
|
+
// Filter to only existing files (double-check with FileManager)
|
|
86
|
+
const existing = await this.filterExisting(matches);
|
|
87
|
+
// Sort alphabetically (case-insensitive)
|
|
88
|
+
const sorted = existing.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
89
|
+
this.logger.info({ existingCount: sorted.length, matchCount: matches.length, operation: 'expandPattern', pattern }, 'Glob pattern expansion complete');
|
|
90
|
+
return sorted;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const err = error;
|
|
94
|
+
// Check if it's already a FileSystemError
|
|
95
|
+
if (err instanceof FileSystemError) {
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
this.logger.error({ error: err.message, operation: 'expandPattern', pattern, stack: err.stack }, 'Error expanding glob pattern');
|
|
99
|
+
throw new FileSystemError(`Failed to expand glob pattern: ${pattern}: ${err.message}`, {
|
|
100
|
+
operation: 'expandPattern',
|
|
101
|
+
originalError: err.message,
|
|
102
|
+
pattern,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Filter paths to only include existing files
|
|
108
|
+
*
|
|
109
|
+
* Uses FileManager to check file existence for each path.
|
|
110
|
+
* This provides a double-check beyond fast-glob's results.
|
|
111
|
+
*
|
|
112
|
+
* @param paths - Array of file paths to filter
|
|
113
|
+
* @returns Array of paths that exist
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
async filterExisting(paths) {
|
|
117
|
+
this.logger.debug({ operation: 'filterExisting', pathCount: paths.length }, 'Filtering to existing files');
|
|
118
|
+
const checks = await Promise.all(paths.map(async (path) => ({
|
|
119
|
+
exists: await this.fileManager.fileExists(path),
|
|
120
|
+
path,
|
|
121
|
+
})));
|
|
122
|
+
const existing = checks.filter((check) => check.exists).map((check) => check.path);
|
|
123
|
+
this.logger.debug({ existingCount: existing.length, operation: 'filterExisting', pathCount: paths.length }, 'File existence filtering complete');
|
|
124
|
+
return existing;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PathResolver Service
|
|
3
|
+
*
|
|
4
|
+
* Resolves and validates file paths from core-config.yaml configuration.
|
|
5
|
+
* All commands use this service to get correct directories for epics, stories, and PRD files.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import { FileManager } from './file-manager.js';
|
|
9
|
+
/**
|
|
10
|
+
* PathResolver service for resolving and validating file paths
|
|
11
|
+
*
|
|
12
|
+
* Loads configuration from .bmad-core/core-config.yaml and provides methods
|
|
13
|
+
* to get correct directories for epics, stories, QA, and PRD files. All paths
|
|
14
|
+
* are resolved relative to the project root and validated for existence.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PathResolver {
|
|
17
|
+
/**
|
|
18
|
+
* Cached configuration to avoid repeated file reads
|
|
19
|
+
*/
|
|
20
|
+
private cachedConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Cached resolved paths
|
|
23
|
+
*/
|
|
24
|
+
private cachedPaths;
|
|
25
|
+
/**
|
|
26
|
+
* FileManager instance for file operations
|
|
27
|
+
*/
|
|
28
|
+
private readonly fileManager;
|
|
29
|
+
/**
|
|
30
|
+
* Logger instance for path resolution operations
|
|
31
|
+
*/
|
|
32
|
+
private readonly logger;
|
|
33
|
+
/**
|
|
34
|
+
* Project root directory (current working directory)
|
|
35
|
+
*/
|
|
36
|
+
private readonly projectRoot;
|
|
37
|
+
/**
|
|
38
|
+
* Create a new PathResolver instance
|
|
39
|
+
*
|
|
40
|
+
* @param fileManager - FileManager instance for file operations
|
|
41
|
+
* @param logger - Pino logger instance for logging path operations
|
|
42
|
+
* @example
|
|
43
|
+
* const logger = createLogger({ namespace: 'services:path-resolver' })
|
|
44
|
+
* const fileManager = new FileManager(logger)
|
|
45
|
+
* const pathResolver = new PathResolver(fileManager, logger)
|
|
46
|
+
*/
|
|
47
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
48
|
+
/**
|
|
49
|
+
* Get all story directories for existence checks
|
|
50
|
+
*
|
|
51
|
+
* Returns an array of all story directories to check when determining
|
|
52
|
+
* if a story already exists. This includes:
|
|
53
|
+
* - docs/stories (dev stories)
|
|
54
|
+
* - docs/qa/stories (QA stories)
|
|
55
|
+
* - docs/done/stories (completed stories)
|
|
56
|
+
*
|
|
57
|
+
* @returns Array of absolute paths to story directories
|
|
58
|
+
* @example
|
|
59
|
+
* const allDirs = pathResolver.getAllStoryDirs()
|
|
60
|
+
* // Returns: ['/path/to/project/docs/stories', '/path/to/project/docs/qa/stories', '/path/to/project/docs/done/stories']
|
|
61
|
+
*/
|
|
62
|
+
getAllStoryDirs(): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Get the configuration file path
|
|
65
|
+
*
|
|
66
|
+
* Returns the absolute path to .bmad-core/core-config.yaml
|
|
67
|
+
*
|
|
68
|
+
* @returns Absolute path to configuration file
|
|
69
|
+
* @example
|
|
70
|
+
* const configPath = pathResolver.getConfigPath()
|
|
71
|
+
* // Returns: '/path/to/project/.bmad-core/core-config.yaml'
|
|
72
|
+
*/
|
|
73
|
+
getConfigPath(): string;
|
|
74
|
+
/**
|
|
75
|
+
* Get the Done story directory path
|
|
76
|
+
*
|
|
77
|
+
* Returns the docs/done/stories directory for completed stories.
|
|
78
|
+
*
|
|
79
|
+
* @returns Absolute path to Done story directory
|
|
80
|
+
* @example
|
|
81
|
+
* const doneStoryDir = pathResolver.getDoneStoryDir()
|
|
82
|
+
* // Returns: '/path/to/project/docs/done/stories'
|
|
83
|
+
*/
|
|
84
|
+
getDoneStoryDir(): string;
|
|
85
|
+
/**
|
|
86
|
+
* Get the epic directory path
|
|
87
|
+
*
|
|
88
|
+
* Derives epic directory from story location or explicit configuration.
|
|
89
|
+
* By convention, epics are in docs/epics if stories are in docs/stories.
|
|
90
|
+
*
|
|
91
|
+
* @returns Absolute path to epic directory
|
|
92
|
+
* @throws {FileSystemError} If epic directory does not exist
|
|
93
|
+
* @example
|
|
94
|
+
* const epicDir = pathResolver.getEpicDir()
|
|
95
|
+
* // Returns: '/path/to/project/docs/epics'
|
|
96
|
+
*/
|
|
97
|
+
getEpicDir(): string;
|
|
98
|
+
/**
|
|
99
|
+
* Get the PRD file path
|
|
100
|
+
*
|
|
101
|
+
* Returns the configured PRD file path from core-config.yaml.
|
|
102
|
+
*
|
|
103
|
+
* @returns Absolute path to PRD file
|
|
104
|
+
* @example
|
|
105
|
+
* const prdFile = pathResolver.getPrdFile()
|
|
106
|
+
* // Returns: '/path/to/project/docs/prd.md'
|
|
107
|
+
*/
|
|
108
|
+
getPrdFile(): string;
|
|
109
|
+
/**
|
|
110
|
+
* Get the QA story directory path
|
|
111
|
+
*
|
|
112
|
+
* Returns the configured QA location plus /stories subdirectory.
|
|
113
|
+
*
|
|
114
|
+
* @returns Absolute path to QA story directory
|
|
115
|
+
* @throws {FileSystemError} If QA story directory does not exist
|
|
116
|
+
* @example
|
|
117
|
+
* const qaStoryDir = pathResolver.getQaStoryDir()
|
|
118
|
+
* // Returns: '/path/to/project/docs/qa/stories'
|
|
119
|
+
*/
|
|
120
|
+
getQaStoryDir(): string;
|
|
121
|
+
/**
|
|
122
|
+
* Get the story directory path
|
|
123
|
+
*
|
|
124
|
+
* Returns the configured story location from core-config.yaml.
|
|
125
|
+
*
|
|
126
|
+
* @returns Absolute path to story directory
|
|
127
|
+
* @throws {FileSystemError} If story directory does not exist
|
|
128
|
+
* @example
|
|
129
|
+
* const storyDir = pathResolver.getStoryDir()
|
|
130
|
+
* // Returns: '/path/to/project/docs/stories'
|
|
131
|
+
*/
|
|
132
|
+
getStoryDir(): string;
|
|
133
|
+
/**
|
|
134
|
+
* Find configuration file by searching up the directory tree
|
|
135
|
+
*
|
|
136
|
+
* Searches for .bmad-core/core-config.yaml starting from project root
|
|
137
|
+
* and walking up to 5 parent directories.
|
|
138
|
+
*
|
|
139
|
+
* @returns Path to found config file, or null if not found
|
|
140
|
+
*/
|
|
141
|
+
private findConfigFile;
|
|
142
|
+
/**
|
|
143
|
+
* Get resolved paths (with caching)
|
|
144
|
+
*
|
|
145
|
+
* Loads configuration and resolves all paths on first access,
|
|
146
|
+
* then returns cached paths on subsequent calls.
|
|
147
|
+
*
|
|
148
|
+
* @returns Resolved paths structure
|
|
149
|
+
*/
|
|
150
|
+
private getResolvedPaths;
|
|
151
|
+
/**
|
|
152
|
+
* Load configuration from .bmad-core/core-config.yaml
|
|
153
|
+
*
|
|
154
|
+
* Reads and parses YAML configuration file, validates structure,
|
|
155
|
+
* and caches the result for subsequent calls. Searches up to 5 parent
|
|
156
|
+
* directories to find the config file.
|
|
157
|
+
*
|
|
158
|
+
* @returns Parsed and validated configuration
|
|
159
|
+
* @throws {FileSystemError} If configuration file is missing
|
|
160
|
+
* @throws {ConfigurationError} If configuration structure is invalid
|
|
161
|
+
*/
|
|
162
|
+
private loadConfig;
|
|
163
|
+
/**
|
|
164
|
+
* Validate configuration structure
|
|
165
|
+
*
|
|
166
|
+
* Checks that all required fields are present and have correct types.
|
|
167
|
+
*
|
|
168
|
+
* @param config - Configuration object to validate
|
|
169
|
+
* @throws {ConfigurationError} If configuration is invalid
|
|
170
|
+
*/
|
|
171
|
+
private validateConfig;
|
|
172
|
+
/**
|
|
173
|
+
* Validate that a directory exists (synchronous)
|
|
174
|
+
*
|
|
175
|
+
* Checks directory existence and throws error if not found.
|
|
176
|
+
* Uses synchronous fs check for initialization validation.
|
|
177
|
+
*
|
|
178
|
+
* @param dirPath - Absolute path to directory
|
|
179
|
+
* @param name - Human-readable directory name for error messages
|
|
180
|
+
* @throws {FileSystemError} If directory does not exist
|
|
181
|
+
*/
|
|
182
|
+
private validateDirectorySync;
|
|
183
|
+
}
|