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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selective Backup System
|
|
3
|
+
*
|
|
4
|
+
* Creates targeted backups of specific files rather than entire directories.
|
|
5
|
+
* Used for conflict resolution during adoption to backup only files being overwritten.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs').promises;
|
|
10
|
+
const {
|
|
11
|
+
pathExists,
|
|
12
|
+
ensureDirectory,
|
|
13
|
+
safeCopy,
|
|
14
|
+
readJSON,
|
|
15
|
+
writeJSON
|
|
16
|
+
} = require('../utils/fs-utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SelectiveBackup class for creating targeted file backups
|
|
20
|
+
*/
|
|
21
|
+
class SelectiveBackup {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.backupDir = '.kiro/backups';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a backup of specific files before overwriting
|
|
28
|
+
*
|
|
29
|
+
* @param {string} projectPath - Project root path
|
|
30
|
+
* @param {string[]} filePaths - Relative paths of files to backup (from .kiro/)
|
|
31
|
+
* @param {Object} options - Backup options
|
|
32
|
+
* @param {string} options.type - Backup type (default: 'conflict')
|
|
33
|
+
* @returns {Promise<SelectiveBackupInfo>}
|
|
34
|
+
*/
|
|
35
|
+
async createSelectiveBackup(projectPath, filePaths, options = {}) {
|
|
36
|
+
const { type = 'conflict' } = options;
|
|
37
|
+
|
|
38
|
+
// Generate backup ID with timestamp
|
|
39
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T');
|
|
40
|
+
const dateStr = timestamp[0];
|
|
41
|
+
const timeStr = timestamp[1].split('-').slice(0, 3).join('');
|
|
42
|
+
const backupId = `${type}-${dateStr}-${timeStr}`;
|
|
43
|
+
|
|
44
|
+
const backupPath = path.join(projectPath, this.backupDir, backupId);
|
|
45
|
+
const filesBackupPath = path.join(backupPath, 'files');
|
|
46
|
+
|
|
47
|
+
// Create backup directory structure
|
|
48
|
+
await ensureDirectory(backupPath);
|
|
49
|
+
await ensureDirectory(filesBackupPath);
|
|
50
|
+
|
|
51
|
+
const backedUpFiles = [];
|
|
52
|
+
let totalSize = 0;
|
|
53
|
+
|
|
54
|
+
// Backup each file
|
|
55
|
+
for (const filePath of filePaths) {
|
|
56
|
+
const sourcePath = path.join(projectPath, this.backupDir.replace('/backups', ''), filePath);
|
|
57
|
+
const destPath = path.join(filesBackupPath, filePath);
|
|
58
|
+
|
|
59
|
+
// Check if source file exists
|
|
60
|
+
const sourceExists = await pathExists(sourcePath);
|
|
61
|
+
if (!sourceExists) {
|
|
62
|
+
continue; // Skip non-existent files
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Ensure destination directory exists
|
|
66
|
+
const destDir = path.dirname(destPath);
|
|
67
|
+
await ensureDirectory(destDir);
|
|
68
|
+
|
|
69
|
+
// Copy file
|
|
70
|
+
await safeCopy(sourcePath, destPath, { overwrite: true });
|
|
71
|
+
|
|
72
|
+
// Get file size
|
|
73
|
+
const stats = await fs.stat(sourcePath);
|
|
74
|
+
totalSize += stats.size;
|
|
75
|
+
|
|
76
|
+
backedUpFiles.push(filePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create metadata
|
|
80
|
+
const metadata = {
|
|
81
|
+
id: backupId,
|
|
82
|
+
type,
|
|
83
|
+
created: new Date().toISOString(),
|
|
84
|
+
files: backedUpFiles,
|
|
85
|
+
fileCount: backedUpFiles.length,
|
|
86
|
+
totalSize
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Write metadata
|
|
90
|
+
await writeJSON(path.join(backupPath, 'metadata.json'), metadata);
|
|
91
|
+
|
|
92
|
+
// Write files list
|
|
93
|
+
await writeJSON(path.join(backupPath, 'files.json'), backedUpFiles);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
id: backupId,
|
|
97
|
+
type,
|
|
98
|
+
created: metadata.created,
|
|
99
|
+
files: backedUpFiles,
|
|
100
|
+
fileCount: backedUpFiles.length,
|
|
101
|
+
totalSize,
|
|
102
|
+
path: backupPath
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Restores specific files from a selective backup
|
|
108
|
+
*
|
|
109
|
+
* @param {string} projectPath - Project root path
|
|
110
|
+
* @param {string} backupId - Backup ID to restore from
|
|
111
|
+
* @param {string[]} filePaths - Optional: specific files to restore (if not provided, restores all)
|
|
112
|
+
* @returns {Promise<RestoreResult>}
|
|
113
|
+
*/
|
|
114
|
+
async restoreSelective(projectPath, backupId, filePaths = null) {
|
|
115
|
+
const backupPath = path.join(projectPath, this.backupDir, backupId);
|
|
116
|
+
|
|
117
|
+
// Check if backup exists
|
|
118
|
+
const backupExists = await pathExists(backupPath);
|
|
119
|
+
if (!backupExists) {
|
|
120
|
+
throw new Error(`Backup not found: ${backupId}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Read metadata
|
|
124
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
125
|
+
const metadata = await readJSON(metadataPath);
|
|
126
|
+
|
|
127
|
+
if (!metadata) {
|
|
128
|
+
throw new Error(`Invalid backup: metadata.json not found in ${backupId}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine which files to restore
|
|
132
|
+
const filesToRestore = filePaths || metadata.files;
|
|
133
|
+
|
|
134
|
+
const restoredFiles = [];
|
|
135
|
+
const errors = [];
|
|
136
|
+
|
|
137
|
+
// Restore each file
|
|
138
|
+
for (const filePath of filesToRestore) {
|
|
139
|
+
try {
|
|
140
|
+
const sourcePath = path.join(backupPath, 'files', filePath);
|
|
141
|
+
const destPath = path.join(projectPath, this.backupDir.replace('/backups', ''), filePath);
|
|
142
|
+
|
|
143
|
+
// Check if backup file exists
|
|
144
|
+
const sourceExists = await pathExists(sourcePath);
|
|
145
|
+
if (!sourceExists) {
|
|
146
|
+
errors.push(`File not found in backup: ${filePath}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Ensure destination directory exists
|
|
151
|
+
const destDir = path.dirname(destPath);
|
|
152
|
+
await ensureDirectory(destDir);
|
|
153
|
+
|
|
154
|
+
// Restore file
|
|
155
|
+
await safeCopy(sourcePath, destPath, { overwrite: true });
|
|
156
|
+
restoredFiles.push(filePath);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
errors.push(`Failed to restore ${filePath}: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: errors.length === 0,
|
|
164
|
+
backupId,
|
|
165
|
+
restoredFiles,
|
|
166
|
+
errors
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Lists files in a selective backup
|
|
172
|
+
*
|
|
173
|
+
* @param {string} projectPath - Project root path
|
|
174
|
+
* @param {string} backupId - Backup ID
|
|
175
|
+
* @returns {Promise<string[]>} - Array of file paths in backup
|
|
176
|
+
*/
|
|
177
|
+
async listBackupFiles(projectPath, backupId) {
|
|
178
|
+
const backupPath = path.join(projectPath, this.backupDir, backupId);
|
|
179
|
+
|
|
180
|
+
// Check if backup exists
|
|
181
|
+
const backupExists = await pathExists(backupPath);
|
|
182
|
+
if (!backupExists) {
|
|
183
|
+
throw new Error(`Backup not found: ${backupId}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Read files list
|
|
187
|
+
const filesPath = path.join(backupPath, 'files.json');
|
|
188
|
+
const filesExists = await pathExists(filesPath);
|
|
189
|
+
|
|
190
|
+
if (filesExists) {
|
|
191
|
+
const files = await readJSON(filesPath);
|
|
192
|
+
return files || [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fallback: read from metadata
|
|
196
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
197
|
+
const metadata = await readJSON(metadataPath);
|
|
198
|
+
|
|
199
|
+
if (metadata && metadata.files) {
|
|
200
|
+
return metadata.files;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = SelectiveBackup;
|
package/lib/commands/adopt.js
CHANGED
|
@@ -15,6 +15,8 @@ const VersionManager = require('../version/version-manager');
|
|
|
15
15
|
const SteeringManager = require('../steering/steering-manager');
|
|
16
16
|
const AdoptionConfig = require('../steering/adoption-config');
|
|
17
17
|
const { detectTool, generateAutoConfig } = require('../utils/tool-detector');
|
|
18
|
+
const ConflictResolver = require('../adoption/conflict-resolver');
|
|
19
|
+
const SelectiveBackup = require('../backup/selective-backup');
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Executes the adopt command
|
|
@@ -23,10 +25,11 @@ const { detectTool, generateAutoConfig } = require('../utils/tool-detector');
|
|
|
23
25
|
* @param {boolean} options.auto - Skip confirmations
|
|
24
26
|
* @param {boolean} options.dryRun - Show what would change without making changes
|
|
25
27
|
* @param {string} options.mode - Force specific adoption mode (fresh/partial/full)
|
|
28
|
+
* @param {boolean} options.force - Force overwrite conflicting files (creates backup first)
|
|
26
29
|
* @returns {Promise<void>}
|
|
27
30
|
*/
|
|
28
31
|
async function adoptCommand(options = {}) {
|
|
29
|
-
const { auto = false, dryRun = false, mode: forcedMode = null } = options;
|
|
32
|
+
const { auto = false, dryRun = false, mode: forcedMode = null, force = false } = options;
|
|
30
33
|
const projectPath = process.cwd();
|
|
31
34
|
|
|
32
35
|
console.log(chalk.red('🔥') + ' Kiro Spec Engine - Project Adoption');
|
|
@@ -71,14 +74,23 @@ async function adoptCommand(options = {}) {
|
|
|
71
74
|
console.log(' - Create backup before changes');
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
// Show conflicts if any
|
|
77
|
+
// Show conflicts if any (brief summary)
|
|
75
78
|
if (detection.conflicts.length > 0) {
|
|
76
79
|
console.log();
|
|
77
80
|
console.log(chalk.yellow('⚠️ Conflicts detected:'));
|
|
78
81
|
detection.conflicts.forEach(conflict => {
|
|
79
82
|
console.log(` - ${conflict.path}`);
|
|
80
83
|
});
|
|
81
|
-
console.log(
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
if (force) {
|
|
87
|
+
console.log(chalk.red(' ⚠️ --force enabled: Conflicting files will be overwritten'));
|
|
88
|
+
console.log(chalk.gray(' A backup will be created before overwriting'));
|
|
89
|
+
} else if (auto) {
|
|
90
|
+
console.log(chalk.gray(' --auto mode: Existing files will be preserved'));
|
|
91
|
+
} else {
|
|
92
|
+
console.log(chalk.gray(' You will be prompted to choose how to handle conflicts'));
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
console.log();
|
|
@@ -94,7 +106,8 @@ async function adoptCommand(options = {}) {
|
|
|
94
106
|
|
|
95
107
|
const result = await adoptionStrategy.execute(projectPath, strategy, {
|
|
96
108
|
kseVersion: packageJson.version,
|
|
97
|
-
dryRun: true
|
|
109
|
+
dryRun: true,
|
|
110
|
+
force
|
|
98
111
|
});
|
|
99
112
|
|
|
100
113
|
if (result.success) {
|
|
@@ -137,7 +150,71 @@ async function adoptCommand(options = {}) {
|
|
|
137
150
|
|
|
138
151
|
console.log();
|
|
139
152
|
|
|
140
|
-
// 7. Handle
|
|
153
|
+
// 7. Handle conflicts interactively
|
|
154
|
+
let resolutionMap = {};
|
|
155
|
+
let conflictBackupId = null;
|
|
156
|
+
|
|
157
|
+
if (detection.conflicts.length > 0) {
|
|
158
|
+
if (!auto && !force) {
|
|
159
|
+
// Interactive mode: prompt user for conflict resolution
|
|
160
|
+
const resolver = new ConflictResolver();
|
|
161
|
+
|
|
162
|
+
// Show detailed conflict summary
|
|
163
|
+
resolver.displayConflictSummary(detection.conflicts);
|
|
164
|
+
|
|
165
|
+
// Get resolution strategy
|
|
166
|
+
const conflictStrategy = await resolver.promptStrategy(detection.conflicts);
|
|
167
|
+
|
|
168
|
+
// Resolve conflicts
|
|
169
|
+
resolutionMap = await resolver.resolveConflicts(detection.conflicts, conflictStrategy, projectPath);
|
|
170
|
+
|
|
171
|
+
// Create selective backup if any files will be overwritten
|
|
172
|
+
const filesToOverwrite = Object.entries(resolutionMap)
|
|
173
|
+
.filter(([_, resolution]) => resolution === 'overwrite')
|
|
174
|
+
.map(([filePath, _]) => filePath);
|
|
175
|
+
|
|
176
|
+
if (filesToOverwrite.length > 0) {
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(chalk.blue('📦 Creating backup of files to be overwritten...'));
|
|
179
|
+
const selectiveBackup = new SelectiveBackup();
|
|
180
|
+
const backup = await selectiveBackup.createSelectiveBackup(
|
|
181
|
+
projectPath,
|
|
182
|
+
filesToOverwrite,
|
|
183
|
+
{ type: 'conflict' }
|
|
184
|
+
);
|
|
185
|
+
conflictBackupId = backup.id;
|
|
186
|
+
console.log(chalk.green(`✅ Backup created: ${conflictBackupId}`));
|
|
187
|
+
}
|
|
188
|
+
} else if (force) {
|
|
189
|
+
// Force mode: overwrite all with backup
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(chalk.blue('📦 Creating backup of conflicting files...'));
|
|
192
|
+
const filesToOverwrite = detection.conflicts.map(c => c.path);
|
|
193
|
+
const selectiveBackup = new SelectiveBackup();
|
|
194
|
+
const backup = await selectiveBackup.createSelectiveBackup(
|
|
195
|
+
projectPath,
|
|
196
|
+
filesToOverwrite,
|
|
197
|
+
{ type: 'conflict' }
|
|
198
|
+
);
|
|
199
|
+
conflictBackupId = backup.id;
|
|
200
|
+
console.log(chalk.green(`✅ Backup created: ${conflictBackupId}`));
|
|
201
|
+
|
|
202
|
+
resolutionMap = detection.conflicts.reduce((map, conflict) => {
|
|
203
|
+
map[conflict.path] = 'overwrite';
|
|
204
|
+
return map;
|
|
205
|
+
}, {});
|
|
206
|
+
} else if (auto) {
|
|
207
|
+
// Auto mode: skip all conflicts
|
|
208
|
+
resolutionMap = detection.conflicts.reduce((map, conflict) => {
|
|
209
|
+
map[conflict.path] = 'keep';
|
|
210
|
+
return map;
|
|
211
|
+
}, {});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
// 8. Handle steering strategy if conflicts detected
|
|
141
218
|
let steeringStrategy = null;
|
|
142
219
|
let steeringBackupId = null;
|
|
143
220
|
|
|
@@ -184,7 +261,7 @@ async function adoptCommand(options = {}) {
|
|
|
184
261
|
console.log();
|
|
185
262
|
}
|
|
186
263
|
|
|
187
|
-
//
|
|
264
|
+
// 9. Create backup if needed (for non-conflict scenarios)
|
|
188
265
|
let backupId = null;
|
|
189
266
|
if (detection.hasKiroDir && (strategy === 'partial' || strategy === 'full')) {
|
|
190
267
|
console.log(chalk.blue('📦 Creating backup...'));
|
|
@@ -203,7 +280,7 @@ async function adoptCommand(options = {}) {
|
|
|
203
280
|
|
|
204
281
|
console.log();
|
|
205
282
|
|
|
206
|
-
//
|
|
283
|
+
// 10. Execute adoption
|
|
207
284
|
console.log(chalk.blue('🚀 Executing adoption...'));
|
|
208
285
|
const adoptionStrategy = getAdoptionStrategy(strategy);
|
|
209
286
|
const packageJson = require('../../package.json');
|
|
@@ -211,12 +288,14 @@ async function adoptCommand(options = {}) {
|
|
|
211
288
|
const result = await adoptionStrategy.execute(projectPath, strategy, {
|
|
212
289
|
kseVersion: packageJson.version,
|
|
213
290
|
dryRun: false,
|
|
214
|
-
backupId
|
|
291
|
+
backupId,
|
|
292
|
+
force,
|
|
293
|
+
resolutionMap // Pass resolution map to adoption strategy
|
|
215
294
|
});
|
|
216
295
|
|
|
217
296
|
console.log();
|
|
218
297
|
|
|
219
|
-
//
|
|
298
|
+
// 11. Report results
|
|
220
299
|
if (result.success) {
|
|
221
300
|
console.log(chalk.green('✅ Adoption completed successfully!'));
|
|
222
301
|
console.log();
|
|
@@ -244,6 +323,12 @@ async function adoptCommand(options = {}) {
|
|
|
244
323
|
result.filesSkipped.forEach(file => console.log(` - ${file}`));
|
|
245
324
|
}
|
|
246
325
|
|
|
326
|
+
if (conflictBackupId) {
|
|
327
|
+
console.log();
|
|
328
|
+
console.log(chalk.blue('📦 Conflict Backup:'), conflictBackupId);
|
|
329
|
+
console.log(chalk.gray(' Run'), chalk.cyan('kse rollback'), chalk.gray('to restore overwritten files'));
|
|
330
|
+
}
|
|
331
|
+
|
|
247
332
|
if (result.warnings.length > 0) {
|
|
248
333
|
console.log();
|
|
249
334
|
console.log(chalk.yellow('⚠️ Warnings:'));
|
|
@@ -258,7 +343,7 @@ async function adoptCommand(options = {}) {
|
|
|
258
343
|
|
|
259
344
|
console.log();
|
|
260
345
|
|
|
261
|
-
//
|
|
346
|
+
// 12. Detect tool and offer automation setup
|
|
262
347
|
console.log(chalk.blue('🔍 Detecting your development environment...'));
|
|
263
348
|
try {
|
|
264
349
|
const toolDetection = await detectTool(projectPath);
|