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.
@@ -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;
@@ -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(' Existing files will be preserved, template files will be skipped');
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 steering strategy if conflicts detected
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
- // 8. Create backup if needed
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
- // 9. Execute adoption
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
- // 10. Report results
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
- // 11. Detect tool and offer automation setup
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);