kiro-spec-engine 1.4.4 → 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.
@@ -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;