kiro-spec-engine 1.4.3 → 1.5.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/CHANGELOG.md +86 -4
- package/README.md +16 -0
- package/README.zh.md +380 -0
- package/bin/kiro-spec-engine.js +102 -44
- package/docs/adoption-guide.md +53 -0
- package/docs/document-governance.md +864 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +65 -0
- package/docs/troubleshooting.md +339 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/lib/adoption/adoption-strategy.js +22 -6
- package/lib/adoption/conflict-resolver.js +239 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/commands/adopt.js +95 -10
- package/lib/commands/docs.js +717 -0
- package/lib/commands/doctor.js +141 -3
- package/lib/commands/status.js +77 -5
- package/lib/governance/archive-tool.js +231 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/package.json +7 -7
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Logger
|
|
3
|
+
*
|
|
4
|
+
* Tracks governance tool executions for metrics and reporting
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class ExecutionLogger {
|
|
11
|
+
constructor(projectPath) {
|
|
12
|
+
this.projectPath = projectPath;
|
|
13
|
+
this.logDir = path.join(projectPath, '.kiro', 'logs');
|
|
14
|
+
this.logFile = path.join(this.logDir, 'governance-history.json');
|
|
15
|
+
this.maxLogSize = 10 * 1024 * 1024; // 10MB
|
|
16
|
+
this.maxRotatedLogs = 5;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Log a governance tool execution
|
|
21
|
+
*
|
|
22
|
+
* @param {string} tool - Tool name (diagnostic, cleanup, validation, archive)
|
|
23
|
+
* @param {string} operation - Operation performed
|
|
24
|
+
* @param {Object} results - Operation results
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async logExecution(tool, operation, results) {
|
|
28
|
+
try {
|
|
29
|
+
// Ensure log directory exists
|
|
30
|
+
await fs.ensureDir(this.logDir);
|
|
31
|
+
|
|
32
|
+
// Check if log rotation is needed
|
|
33
|
+
await this.rotateLogIfNeeded();
|
|
34
|
+
|
|
35
|
+
// Create log entry
|
|
36
|
+
const entry = {
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
tool,
|
|
39
|
+
operation,
|
|
40
|
+
results: this.sanitizeResults(results)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Read existing log or create new array
|
|
44
|
+
let logEntries = [];
|
|
45
|
+
if (await fs.pathExists(this.logFile)) {
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(this.logFile, 'utf8');
|
|
48
|
+
logEntries = JSON.parse(content);
|
|
49
|
+
|
|
50
|
+
// Ensure it's an array
|
|
51
|
+
if (!Array.isArray(logEntries)) {
|
|
52
|
+
logEntries = [];
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// If log file is corrupted, start fresh
|
|
56
|
+
console.warn('Log file corrupted, starting fresh');
|
|
57
|
+
logEntries = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Append new entry
|
|
62
|
+
logEntries.push(entry);
|
|
63
|
+
|
|
64
|
+
// Write back to file
|
|
65
|
+
await fs.writeFile(this.logFile, JSON.stringify(logEntries, null, 2), 'utf8');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Log errors should not break the main operation
|
|
68
|
+
console.error('Failed to log execution:', error.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get execution history
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} options - Filter options
|
|
76
|
+
* @param {string} options.tool - Filter by tool name
|
|
77
|
+
* @param {Date} options.since - Filter by date
|
|
78
|
+
* @param {number} options.limit - Limit number of entries
|
|
79
|
+
* @returns {Promise<Array>}
|
|
80
|
+
*/
|
|
81
|
+
async getHistory(options = {}) {
|
|
82
|
+
try {
|
|
83
|
+
// Check if log file exists
|
|
84
|
+
if (!await fs.pathExists(this.logFile)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Read log file
|
|
89
|
+
const content = await fs.readFile(this.logFile, 'utf8');
|
|
90
|
+
let entries = JSON.parse(content);
|
|
91
|
+
|
|
92
|
+
// Ensure it's an array
|
|
93
|
+
if (!Array.isArray(entries)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Apply filters
|
|
98
|
+
if (options.tool) {
|
|
99
|
+
entries = entries.filter(entry => entry.tool === options.tool);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.since) {
|
|
103
|
+
const sinceTime = options.since.getTime();
|
|
104
|
+
entries = entries.filter(entry => {
|
|
105
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
106
|
+
return entryTime >= sinceTime;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply limit
|
|
111
|
+
if (options.limit && options.limit > 0) {
|
|
112
|
+
entries = entries.slice(-options.limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return entries;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Failed to read execution history:', error.message);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Rotate log file if it exceeds max size
|
|
124
|
+
*
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
async rotateLogIfNeeded() {
|
|
128
|
+
try {
|
|
129
|
+
// Check if log file exists
|
|
130
|
+
if (!await fs.pathExists(this.logFile)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check file size
|
|
135
|
+
const stats = await fs.stat(this.logFile);
|
|
136
|
+
|
|
137
|
+
if (stats.size < this.maxLogSize) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Rotate logs
|
|
142
|
+
await this.rotateLog();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to check log size:', error.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Rotate log file
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
async rotateLog() {
|
|
154
|
+
try {
|
|
155
|
+
// Remove oldest rotated log if we have max number
|
|
156
|
+
const oldestLog = path.join(this.logDir, `governance-history.${this.maxRotatedLogs}.json`);
|
|
157
|
+
if (await fs.pathExists(oldestLog)) {
|
|
158
|
+
await fs.remove(oldestLog);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Shift existing rotated logs
|
|
162
|
+
for (let i = this.maxRotatedLogs - 1; i >= 1; i--) {
|
|
163
|
+
const currentLog = path.join(this.logDir, `governance-history.${i}.json`);
|
|
164
|
+
const nextLog = path.join(this.logDir, `governance-history.${i + 1}.json`);
|
|
165
|
+
|
|
166
|
+
if (await fs.pathExists(currentLog)) {
|
|
167
|
+
await fs.move(currentLog, nextLog, { overwrite: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Rotate current log to .1
|
|
172
|
+
const rotatedLog = path.join(this.logDir, 'governance-history.1.json');
|
|
173
|
+
await fs.move(this.logFile, rotatedLog, { overwrite: true });
|
|
174
|
+
|
|
175
|
+
// Create new empty log
|
|
176
|
+
await fs.writeFile(this.logFile, '[]', 'utf8');
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('Failed to rotate log:', error.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sanitize results for logging (remove sensitive data, limit size)
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} results - Operation results
|
|
186
|
+
* @returns {Object}
|
|
187
|
+
*/
|
|
188
|
+
sanitizeResults(results) {
|
|
189
|
+
// Create a shallow copy
|
|
190
|
+
const sanitized = { ...results };
|
|
191
|
+
|
|
192
|
+
// Limit array sizes to prevent huge logs
|
|
193
|
+
const maxArraySize = 100;
|
|
194
|
+
|
|
195
|
+
if (Array.isArray(sanitized.violations) && sanitized.violations.length > maxArraySize) {
|
|
196
|
+
sanitized.violations = sanitized.violations.slice(0, maxArraySize);
|
|
197
|
+
sanitized.violationsTruncated = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Array.isArray(sanitized.deletedFiles) && sanitized.deletedFiles.length > maxArraySize) {
|
|
201
|
+
sanitized.deletedFiles = sanitized.deletedFiles.slice(0, maxArraySize);
|
|
202
|
+
sanitized.deletedFilesTruncated = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (Array.isArray(sanitized.movedFiles) && sanitized.movedFiles.length > maxArraySize) {
|
|
206
|
+
sanitized.movedFiles = sanitized.movedFiles.slice(0, maxArraySize);
|
|
207
|
+
sanitized.movedFilesTruncated = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (Array.isArray(sanitized.errors) && sanitized.errors.length > maxArraySize) {
|
|
211
|
+
sanitized.errors = sanitized.errors.slice(0, maxArraySize);
|
|
212
|
+
sanitized.errorsTruncated = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return sanitized;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear all logs (for testing or reset)
|
|
220
|
+
*
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
async clearLogs() {
|
|
224
|
+
try {
|
|
225
|
+
// Remove main log file
|
|
226
|
+
if (await fs.pathExists(this.logFile)) {
|
|
227
|
+
await fs.remove(this.logFile);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Remove rotated logs
|
|
231
|
+
for (let i = 1; i <= this.maxRotatedLogs; i++) {
|
|
232
|
+
const rotatedLog = path.join(this.logDir, `governance-history.${i}.json`);
|
|
233
|
+
if (await fs.pathExists(rotatedLog)) {
|
|
234
|
+
await fs.remove(rotatedLog);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Failed to clear logs:', error.message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = ExecutionLogger;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Scanner
|
|
3
|
+
*
|
|
4
|
+
* Utility for scanning directories and detecting files based on patterns
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { minimatch } = require('minimatch');
|
|
10
|
+
|
|
11
|
+
class FileScanner {
|
|
12
|
+
constructor(projectPath) {
|
|
13
|
+
this.projectPath = projectPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find all markdown files in a directory (non-recursive)
|
|
18
|
+
*
|
|
19
|
+
* @param {string} dirPath - Directory path to scan
|
|
20
|
+
* @returns {Promise<string[]>} - Array of absolute file paths
|
|
21
|
+
*/
|
|
22
|
+
async findMarkdownFiles(dirPath) {
|
|
23
|
+
try {
|
|
24
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
25
|
+
const mdFiles = [];
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
29
|
+
mdFiles.push(path.join(dirPath, entry.name));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return mdFiles;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// If directory doesn't exist or can't be read, return empty array
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Find all markdown files in a directory recursively
|
|
42
|
+
*
|
|
43
|
+
* @param {string} dirPath - Directory path to scan
|
|
44
|
+
* @param {Object} options - Scan options
|
|
45
|
+
* @param {string[]} options.excludeDirs - Directory names to exclude (e.g., ['node_modules', '.git'])
|
|
46
|
+
* @returns {Promise<string[]>} - Array of absolute file paths
|
|
47
|
+
*/
|
|
48
|
+
async findMarkdownFilesRecursive(dirPath, options = {}) {
|
|
49
|
+
const excludeDirs = options.excludeDirs || ['node_modules', '.git'];
|
|
50
|
+
const mdFiles = [];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await this._scanRecursive(dirPath, mdFiles, excludeDirs);
|
|
54
|
+
return mdFiles;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// If directory doesn't exist or can't be read, return empty array
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal recursive scanning helper
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
async _scanRecursive(dirPath, results, excludeDirs) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
72
|
+
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
// Skip excluded directories
|
|
75
|
+
if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
|
76
|
+
await this._scanRecursive(fullPath, results, excludeDirs);
|
|
77
|
+
}
|
|
78
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
79
|
+
results.push(fullPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Skip directories that can't be read
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Match files against glob patterns
|
|
90
|
+
*
|
|
91
|
+
* @param {string[]} filePaths - Array of file paths to check
|
|
92
|
+
* @param {string[]} patterns - Array of glob patterns (e.g., ['*-SUMMARY.md', 'TEMP-*.md'])
|
|
93
|
+
* @returns {string[]} - Array of matching file paths
|
|
94
|
+
*/
|
|
95
|
+
matchPatterns(filePaths, patterns) {
|
|
96
|
+
const matches = [];
|
|
97
|
+
|
|
98
|
+
for (const filePath of filePaths) {
|
|
99
|
+
const basename = path.basename(filePath);
|
|
100
|
+
|
|
101
|
+
for (const pattern of patterns) {
|
|
102
|
+
if (minimatch(basename, pattern, { nocase: false })) {
|
|
103
|
+
matches.push(filePath);
|
|
104
|
+
break; // Don't add the same file multiple times
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return matches;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a file matches any of the given patterns
|
|
114
|
+
*
|
|
115
|
+
* @param {string} filePath - File path to check
|
|
116
|
+
* @param {string[]} patterns - Array of glob patterns
|
|
117
|
+
* @returns {boolean} - True if file matches any pattern
|
|
118
|
+
*/
|
|
119
|
+
matchesPattern(filePath, patterns) {
|
|
120
|
+
const basename = path.basename(filePath);
|
|
121
|
+
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
if (minimatch(basename, pattern, { nocase: false })) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find all Spec directories in the project
|
|
133
|
+
*
|
|
134
|
+
* @returns {Promise<string[]>} - Array of Spec directory paths
|
|
135
|
+
*/
|
|
136
|
+
async findSpecDirectories() {
|
|
137
|
+
const specsPath = path.join(this.projectPath, '.kiro/specs');
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
141
|
+
const specDirs = [];
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
145
|
+
specDirs.push(path.join(specsPath, entry.name));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return specDirs;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// If .kiro/specs doesn't exist, return empty array
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get Spec directory by name
|
|
158
|
+
*
|
|
159
|
+
* @param {string} specName - Spec name
|
|
160
|
+
* @returns {string} - Spec directory path
|
|
161
|
+
*/
|
|
162
|
+
getSpecDirectory(specName) {
|
|
163
|
+
return path.join(this.projectPath, '.kiro/specs', specName);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a path exists
|
|
168
|
+
*
|
|
169
|
+
* @param {string} filePath - Path to check
|
|
170
|
+
* @returns {Promise<boolean>} - True if path exists
|
|
171
|
+
*/
|
|
172
|
+
async exists(filePath) {
|
|
173
|
+
return await fs.pathExists(filePath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if a path is a directory
|
|
178
|
+
*
|
|
179
|
+
* @param {string} dirPath - Path to check
|
|
180
|
+
* @returns {Promise<boolean>} - True if path is a directory
|
|
181
|
+
*/
|
|
182
|
+
async isDirectory(dirPath) {
|
|
183
|
+
try {
|
|
184
|
+
const stat = await fs.stat(dirPath);
|
|
185
|
+
return stat.isDirectory();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if a path is a file
|
|
193
|
+
*
|
|
194
|
+
* @param {string} filePath - Path to check
|
|
195
|
+
* @returns {Promise<boolean>} - True if path is a file
|
|
196
|
+
*/
|
|
197
|
+
async isFile(filePath) {
|
|
198
|
+
try {
|
|
199
|
+
const stat = await fs.stat(filePath);
|
|
200
|
+
return stat.isFile();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get all files in a directory (non-recursive)
|
|
208
|
+
*
|
|
209
|
+
* @param {string} dirPath - Directory path
|
|
210
|
+
* @returns {Promise<string[]>} - Array of file paths
|
|
211
|
+
*/
|
|
212
|
+
async getFiles(dirPath) {
|
|
213
|
+
try {
|
|
214
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
215
|
+
const files = [];
|
|
216
|
+
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (entry.isFile()) {
|
|
219
|
+
files.push(path.join(dirPath, entry.name));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return files;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get all subdirectories in a directory (non-recursive)
|
|
231
|
+
*
|
|
232
|
+
* @param {string} dirPath - Directory path
|
|
233
|
+
* @returns {Promise<string[]>} - Array of subdirectory paths
|
|
234
|
+
*/
|
|
235
|
+
async getSubdirectories(dirPath) {
|
|
236
|
+
try {
|
|
237
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
238
|
+
const dirs = [];
|
|
239
|
+
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
dirs.push(path.join(dirPath, entry.name));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return dirs;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Normalize path for cross-platform compatibility
|
|
254
|
+
*
|
|
255
|
+
* @param {string} filePath - Path to normalize
|
|
256
|
+
* @returns {string} - Normalized path
|
|
257
|
+
*/
|
|
258
|
+
normalizePath(filePath) {
|
|
259
|
+
// First replace all backslashes with forward slashes for consistency
|
|
260
|
+
// Then use path.normalize to get platform-specific separators
|
|
261
|
+
return path.normalize(filePath.replace(/\\/g, '/'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get relative path from project root
|
|
266
|
+
*
|
|
267
|
+
* @param {string} filePath - Absolute file path
|
|
268
|
+
* @returns {string} - Relative path from project root
|
|
269
|
+
*/
|
|
270
|
+
getRelativePath(filePath) {
|
|
271
|
+
return path.relative(this.projectPath, filePath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get absolute path from relative path
|
|
276
|
+
*
|
|
277
|
+
* @param {string} relativePath - Relative path from project root
|
|
278
|
+
* @returns {string} - Absolute path
|
|
279
|
+
*/
|
|
280
|
+
getAbsolutePath(relativePath) {
|
|
281
|
+
return path.join(this.projectPath, relativePath);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = FileScanner;
|