kiro-spec-engine 1.4.4 → 1.5.5

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.
@@ -41,14 +41,12 @@ class AdoptionStrategy {
41
41
  /**
42
42
  * Gets the path to template directory
43
43
  * This would be embedded in the kse package
44
- * For now, we'll use a placeholder
45
44
  *
46
45
  * @returns {string}
47
46
  */
48
47
  getTemplatePath() {
49
- // In production, this would be: path.join(__dirname, '../../templates/kiro')
50
- // For now, return a placeholder that can be configured
51
- return path.join(__dirname, '../../templates/kiro');
48
+ // Template is at template/.kiro/ in the package
49
+ return path.join(__dirname, '../../template/.kiro');
52
50
  }
53
51
 
54
52
  /**
@@ -85,10 +83,11 @@ class AdoptionStrategy {
85
83
  * @param {Object} options - Copy options
86
84
  * @param {boolean} options.overwrite - Whether to overwrite existing files
87
85
  * @param {string[]} options.skip - Files to skip
86
+ * @param {Object} options.resolutionMap - Map of file paths to resolutions ('keep' | 'overwrite')
88
87
  * @returns {Promise<{created: string[], updated: string[], skipped: string[]}>}
89
88
  */
90
89
  async copyTemplateFiles(projectPath, options = {}) {
91
- const { overwrite = false, skip = [] } = options;
90
+ const { overwrite = false, skip = [], resolutionMap = {} } = options;
92
91
  const kiroPath = this.getKiroPath(projectPath);
93
92
  const templatePath = this.getTemplatePath();
94
93
 
@@ -106,15 +105,12 @@ class AdoptionStrategy {
106
105
 
107
106
  // Define template structure
108
107
  const templateFiles = [
108
+ 'README.md',
109
109
  'steering/CORE_PRINCIPLES.md',
110
110
  'steering/ENVIRONMENT.md',
111
111
  'steering/CURRENT_CONTEXT.md',
112
112
  'steering/RULES_GUIDE.md',
113
- 'tools/ultrawork_enhancer.py',
114
- 'README.md',
115
- 'ultrawork-application-guide.md',
116
- 'ultrawork-integration-summary.md',
117
- 'sisyphus-deep-dive.md'
113
+ 'specs/SPEC_WORKFLOW_GUIDE.md'
118
114
  ];
119
115
 
120
116
  for (const file of templateFiles) {
@@ -124,6 +120,15 @@ class AdoptionStrategy {
124
120
  continue;
125
121
  }
126
122
 
123
+ // Check resolution map for this file
124
+ if (resolutionMap[file]) {
125
+ if (resolutionMap[file] === 'keep') {
126
+ skipped.push(file);
127
+ continue;
128
+ }
129
+ // If 'overwrite', proceed with copying
130
+ }
131
+
127
132
  const sourcePath = path.join(templatePath, file);
128
133
  const destPath = path.join(kiroPath, file);
129
134
 
@@ -137,13 +142,19 @@ class AdoptionStrategy {
137
142
  // Check if destination exists
138
143
  const destExists = await pathExists(destPath);
139
144
 
140
- if (destExists && !overwrite) {
145
+ // Determine if we should overwrite
146
+ let shouldOverwrite = overwrite;
147
+ if (resolutionMap[file] === 'overwrite') {
148
+ shouldOverwrite = true;
149
+ }
150
+
151
+ if (destExists && !shouldOverwrite) {
141
152
  skipped.push(file);
142
153
  continue;
143
154
  }
144
155
 
145
156
  try {
146
- await safeCopy(sourcePath, destPath, { overwrite });
157
+ await safeCopy(sourcePath, destPath, { overwrite: shouldOverwrite });
147
158
 
148
159
  if (destExists) {
149
160
  updated.push(file);
@@ -263,7 +274,7 @@ class PartialAdoption extends AdoptionStrategy {
263
274
  * @returns {Promise<AdoptionResult>}
264
275
  */
265
276
  async execute(projectPath, mode, options = {}) {
266
- const { kseVersion = '1.0.0', dryRun = false, backupId = null } = options;
277
+ const { kseVersion = '1.0.0', dryRun = false, backupId = null, force = false, resolutionMap = {} } = options;
267
278
 
268
279
  const filesCreated = [];
269
280
  const filesUpdated = [];
@@ -326,8 +337,8 @@ class PartialAdoption extends AdoptionStrategy {
326
337
  filesCreated.push('backups/');
327
338
  }
328
339
 
329
- // Copy template files (don't overwrite existing)
330
- const copyResult = await this.copyTemplateFiles(projectPath, { overwrite: false });
340
+ // Copy template files (overwrite if force is enabled)
341
+ const copyResult = await this.copyTemplateFiles(projectPath, { overwrite: force, resolutionMap });
331
342
  filesCreated.push(...copyResult.created);
332
343
  filesUpdated.push(...copyResult.updated);
333
344
  filesSkipped.push(...copyResult.skipped);
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Conflict Resolver
3
+ *
4
+ * Manages interactive conflict resolution prompts and user decisions.
5
+ * Provides three-tier resolution: skip-all, overwrite-all, or review-each.
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const inquirer = require('inquirer');
10
+ const path = require('path');
11
+ const DiffViewer = require('./diff-viewer');
12
+
13
+ /**
14
+ * ConflictResolver class for interactive conflict resolution
15
+ */
16
+ class ConflictResolver {
17
+ constructor() {
18
+ this.diffViewer = new DiffViewer();
19
+ }
20
+
21
+ /**
22
+ * Displays conflict summary grouped by category
23
+ *
24
+ * @param {FileConflict[]} conflicts - Array of conflicts
25
+ * @returns {void}
26
+ */
27
+ displayConflictSummary(conflicts) {
28
+ console.log();
29
+ console.log(chalk.yellow('⚠️ Conflicts Detected'));
30
+ console.log(chalk.yellow('═══════════════════════════════════════════════════════'));
31
+ console.log();
32
+
33
+ // Categorize conflicts
34
+ const categorized = this.categorizeConflicts(conflicts);
35
+
36
+ // Display by category
37
+ if (categorized.steering.length > 0) {
38
+ console.log(chalk.blue('Steering Files:'));
39
+ categorized.steering.forEach(c => console.log(` - ${c.path}`));
40
+ console.log();
41
+ }
42
+
43
+ if (categorized.documentation.length > 0) {
44
+ console.log(chalk.blue('Documentation:'));
45
+ categorized.documentation.forEach(c => console.log(` - ${c.path}`));
46
+ console.log();
47
+ }
48
+
49
+ if (categorized.tools.length > 0) {
50
+ console.log(chalk.blue('Tools:'));
51
+ categorized.tools.forEach(c => console.log(` - ${c.path}`));
52
+ console.log();
53
+ }
54
+
55
+ if (categorized.other.length > 0) {
56
+ console.log(chalk.blue('Other:'));
57
+ categorized.other.forEach(c => console.log(` - ${c.path}`));
58
+ console.log();
59
+ }
60
+
61
+ console.log(chalk.yellow(`Total: ${conflicts.length} conflict(s)`));
62
+ console.log(chalk.yellow('═══════════════════════════════════════════════════════'));
63
+ console.log();
64
+ }
65
+
66
+ /**
67
+ * Categorizes conflicts by type
68
+ *
69
+ * @param {FileConflict[]} conflicts - Array of conflicts
70
+ * @returns {CategorizedConflicts}
71
+ */
72
+ categorizeConflicts(conflicts) {
73
+ return {
74
+ steering: conflicts.filter(c => c.path.startsWith('steering/')),
75
+ documentation: conflicts.filter(c =>
76
+ c.path.endsWith('.md') && !c.path.startsWith('steering/')
77
+ ),
78
+ tools: conflicts.filter(c => c.path.startsWith('tools/')),
79
+ other: conflicts.filter(c =>
80
+ !c.path.startsWith('steering/') &&
81
+ !c.path.startsWith('tools/') &&
82
+ !c.path.endsWith('.md')
83
+ )
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Prompts user for overall conflict resolution strategy
89
+ *
90
+ * @param {FileConflict[]} conflicts - Array of detected conflicts
91
+ * @returns {Promise<ConflictStrategy>} - 'skip-all' | 'overwrite-all' | 'review-each'
92
+ */
93
+ async promptStrategy(conflicts) {
94
+ const { strategy } = await inquirer.prompt([
95
+ {
96
+ type: 'list',
97
+ name: 'strategy',
98
+ message: 'How would you like to handle these conflicts?',
99
+ choices: [
100
+ {
101
+ name: 'Skip conflicting files (keep existing files)',
102
+ value: 'skip-all'
103
+ },
104
+ {
105
+ name: 'Overwrite conflicting files (backup will be created)',
106
+ value: 'overwrite-all'
107
+ },
108
+ {
109
+ name: 'Review conflicts one by one',
110
+ value: 'review-each'
111
+ }
112
+ ],
113
+ default: 'skip-all'
114
+ }
115
+ ]);
116
+
117
+ return strategy;
118
+ }
119
+
120
+ /**
121
+ * Prompts user for resolution of a single file conflict
122
+ *
123
+ * @param {FileConflict} conflict - The conflict to resolve
124
+ * @param {number} currentIndex - Current conflict number (for display)
125
+ * @param {number} totalConflicts - Total number of conflicts
126
+ * @param {string} projectPath - Project root path
127
+ * @returns {Promise<FileResolution>} - 'keep' | 'overwrite'
128
+ */
129
+ async promptFileResolution(conflict, currentIndex, totalConflicts, projectPath) {
130
+ console.log();
131
+ console.log(chalk.blue('─────────────────────────────────────────────────────'));
132
+ console.log(chalk.blue(`Conflict ${currentIndex} of ${totalConflicts}`));
133
+ console.log(chalk.blue('─────────────────────────────────────────────────────'));
134
+ console.log();
135
+ console.log(chalk.cyan('File:'), conflict.path);
136
+ console.log();
137
+
138
+ let resolution = null;
139
+
140
+ while (resolution === null) {
141
+ const { action } = await inquirer.prompt([
142
+ {
143
+ type: 'list',
144
+ name: 'action',
145
+ message: 'What would you like to do?',
146
+ choices: [
147
+ {
148
+ name: 'Keep existing file',
149
+ value: 'keep'
150
+ },
151
+ {
152
+ name: 'Use template file (backup will be created)',
153
+ value: 'overwrite'
154
+ },
155
+ {
156
+ name: 'View diff',
157
+ value: 'view-diff'
158
+ }
159
+ ],
160
+ default: 'keep'
161
+ }
162
+ ]);
163
+
164
+ if (action === 'view-diff') {
165
+ // Show diff
166
+ const existingPath = path.join(projectPath, '.kiro', conflict.path);
167
+ const templatePath = conflict.templatePath || path.join(projectPath, 'template', '.kiro', conflict.path);
168
+
169
+ await this.diffViewer.showDiff(existingPath, templatePath);
170
+
171
+ // Re-prompt with only keep/overwrite options
172
+ const { finalAction } = await inquirer.prompt([
173
+ {
174
+ type: 'list',
175
+ name: 'finalAction',
176
+ message: 'After viewing the diff, what would you like to do?',
177
+ choices: [
178
+ {
179
+ name: 'Keep existing file',
180
+ value: 'keep'
181
+ },
182
+ {
183
+ name: 'Use template file (backup will be created)',
184
+ value: 'overwrite'
185
+ }
186
+ ],
187
+ default: 'keep'
188
+ }
189
+ ]);
190
+
191
+ resolution = finalAction;
192
+ } else {
193
+ resolution = action;
194
+ }
195
+ }
196
+
197
+ return resolution;
198
+ }
199
+
200
+ /**
201
+ * Processes all conflicts based on strategy and returns resolution map
202
+ *
203
+ * @param {FileConflict[]} conflicts - Array of conflicts
204
+ * @param {ConflictStrategy} strategy - Overall strategy
205
+ * @param {string} projectPath - Project root path
206
+ * @returns {Promise<ResolutionMap>} - Map of file paths to resolutions
207
+ */
208
+ async resolveConflicts(conflicts, strategy, projectPath) {
209
+ const resolutionMap = {};
210
+
211
+ if (strategy === 'skip-all') {
212
+ // Mark all as 'keep'
213
+ conflicts.forEach(conflict => {
214
+ resolutionMap[conflict.path] = 'keep';
215
+ });
216
+ } else if (strategy === 'overwrite-all') {
217
+ // Mark all as 'overwrite'
218
+ conflicts.forEach(conflict => {
219
+ resolutionMap[conflict.path] = 'overwrite';
220
+ });
221
+ } else if (strategy === 'review-each') {
222
+ // Prompt for each conflict
223
+ for (let i = 0; i < conflicts.length; i++) {
224
+ const conflict = conflicts[i];
225
+ const resolution = await this.promptFileResolution(
226
+ conflict,
227
+ i + 1,
228
+ conflicts.length,
229
+ projectPath
230
+ );
231
+ resolutionMap[conflict.path] = resolution;
232
+ }
233
+ }
234
+
235
+ return resolutionMap;
236
+ }
237
+ }
238
+
239
+ module.exports = ConflictResolver;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Diff Viewer
3
+ *
4
+ * Displays file differences in a user-friendly format for conflict review.
5
+ * Shows metadata comparison and text diffs for files during adoption.
6
+ */
7
+
8
+ const fs = require('fs').promises;
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+ const { pathExists } = require('../utils/fs-utils');
12
+
13
+ /**
14
+ * DiffViewer class for displaying file comparisons
15
+ */
16
+ class DiffViewer {
17
+ /**
18
+ * Gets file metadata for comparison
19
+ *
20
+ * @param {string} filePath - Path to file
21
+ * @returns {Promise<FileMetadata>}
22
+ */
23
+ async getFileMetadata(filePath) {
24
+ const exists = await pathExists(filePath);
25
+
26
+ if (!exists) {
27
+ return {
28
+ path: filePath,
29
+ size: 0,
30
+ sizeFormatted: '0 B',
31
+ modified: null,
32
+ modifiedFormatted: 'N/A',
33
+ isText: false,
34
+ isBinary: false,
35
+ exists: false
36
+ };
37
+ }
38
+
39
+ const stats = await fs.stat(filePath);
40
+ const size = stats.size;
41
+ const modified = stats.mtime.toISOString();
42
+
43
+ // Format size
44
+ const sizeFormatted = this.formatSize(size);
45
+
46
+ // Format date
47
+ const modifiedFormatted = this.formatDate(stats.mtime);
48
+
49
+ // Detect if file is text or binary
50
+ const isText = await this.isTextFile(filePath);
51
+ const isBinary = !isText;
52
+
53
+ return {
54
+ path: filePath,
55
+ size,
56
+ sizeFormatted,
57
+ modified,
58
+ modifiedFormatted,
59
+ isText,
60
+ isBinary,
61
+ exists: true
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Formats file size in human-readable format
67
+ *
68
+ * @param {number} bytes - File size in bytes
69
+ * @returns {string}
70
+ */
71
+ formatSize(bytes) {
72
+ if (bytes === 0) return '0 B';
73
+
74
+ const units = ['B', 'KB', 'MB', 'GB'];
75
+ const k = 1024;
76
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
77
+
78
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`;
79
+ }
80
+
81
+ /**
82
+ * Formats date in human-readable format
83
+ *
84
+ * @param {Date} date - Date object
85
+ * @returns {string}
86
+ */
87
+ formatDate(date) {
88
+ const year = date.getFullYear();
89
+ const month = String(date.getMonth() + 1).padStart(2, '0');
90
+ const day = String(date.getDate()).padStart(2, '0');
91
+ const hours = String(date.getHours()).padStart(2, '0');
92
+ const minutes = String(date.getMinutes()).padStart(2, '0');
93
+ const seconds = String(date.getSeconds()).padStart(2, '0');
94
+
95
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
96
+ }
97
+
98
+ /**
99
+ * Detects if a file is text or binary
100
+ *
101
+ * @param {string} filePath - Path to file
102
+ * @returns {Promise<boolean>}
103
+ */
104
+ async isTextFile(filePath) {
105
+ try {
106
+ // Read first 8KB to detect binary content
107
+ const buffer = Buffer.alloc(8192);
108
+ const fd = await fs.open(filePath, 'r');
109
+ const { bytesRead } = await fd.read(buffer, 0, 8192, 0);
110
+ await fd.close();
111
+
112
+ // Check for null bytes (common in binary files)
113
+ for (let i = 0; i < bytesRead; i++) {
114
+ if (buffer[i] === 0) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ return true;
120
+ } catch (error) {
121
+ // If we can't read it, assume binary
122
+ return false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Displays a summary diff between existing and template files
128
+ *
129
+ * @param {string} existingPath - Path to existing file
130
+ * @param {string} templatePath - Path to template file
131
+ * @returns {Promise<void>}
132
+ */
133
+ async showDiff(existingPath, templatePath) {
134
+ console.log();
135
+ console.log(chalk.blue('═══════════════════════════════════════════════════════'));
136
+ console.log(chalk.blue('Comparing:'), chalk.cyan(path.basename(existingPath)));
137
+ console.log(chalk.blue('═══════════════════════════════════════════════════════'));
138
+ console.log();
139
+
140
+ // Get metadata for both files
141
+ const existingMeta = await this.getFileMetadata(existingPath);
142
+ const templateMeta = await this.getFileMetadata(templatePath);
143
+
144
+ // Display metadata comparison
145
+ console.log(chalk.yellow('Existing File:'));
146
+ console.log(` Size: ${existingMeta.sizeFormatted}`);
147
+ console.log(` Modified: ${existingMeta.modifiedFormatted}`);
148
+ console.log();
149
+
150
+ console.log(chalk.green('Template File:'));
151
+ console.log(` Size: ${templateMeta.sizeFormatted}`);
152
+ console.log(` Modified: ${templateMeta.modifiedFormatted}`);
153
+ console.log();
154
+
155
+ // Show diff content if both are text files
156
+ if (existingMeta.isText && templateMeta.isText) {
157
+ await this.showLineDiff(existingPath, templatePath, 10);
158
+ } else if (existingMeta.isBinary || templateMeta.isBinary) {
159
+ console.log(chalk.gray('⚠️ Binary file - detailed diff not available'));
160
+ console.log(chalk.gray(' Open files in an editor to compare'));
161
+ }
162
+
163
+ console.log();
164
+ console.log(chalk.blue('═══════════════════════════════════════════════════════'));
165
+ console.log();
166
+ }
167
+
168
+ /**
169
+ * Displays first N lines of differences
170
+ *
171
+ * @param {string} existingPath - Path to existing file
172
+ * @param {string} templatePath - Path to template file
173
+ * @param {number} maxLines - Maximum lines to show (default: 10)
174
+ * @returns {Promise<void>}
175
+ */
176
+ async showLineDiff(existingPath, templatePath, maxLines = 10) {
177
+ try {
178
+ const existingContent = await fs.readFile(existingPath, 'utf-8');
179
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
180
+
181
+ const existingLines = existingContent.split('\n');
182
+ const templateLines = templateContent.split('\n');
183
+
184
+ console.log(chalk.blue('First differences:'));
185
+ console.log();
186
+
187
+ let diffsShown = 0;
188
+ const maxLineNum = Math.max(existingLines.length, templateLines.length);
189
+
190
+ for (let i = 0; i < maxLineNum && diffsShown < maxLines; i++) {
191
+ const existingLine = existingLines[i] || '';
192
+ const templateLine = templateLines[i] || '';
193
+
194
+ if (existingLine !== templateLine) {
195
+ console.log(chalk.gray(` Line ${i + 1}:`));
196
+
197
+ if (existingLine) {
198
+ console.log(chalk.red(` - ${existingLine.substring(0, 80)}`));
199
+ }
200
+
201
+ if (templateLine) {
202
+ console.log(chalk.green(` + ${templateLine.substring(0, 80)}`));
203
+ }
204
+
205
+ console.log();
206
+ diffsShown++;
207
+ }
208
+ }
209
+
210
+ if (diffsShown === 0) {
211
+ console.log(chalk.gray(' No differences found in first 10 lines'));
212
+ console.log();
213
+ } else if (diffsShown >= maxLines) {
214
+ console.log(chalk.gray(` ... (showing first ${maxLines} differences)`));
215
+ console.log();
216
+ }
217
+
218
+ console.log(chalk.gray('[Note: Full diff available by opening files in editor]'));
219
+ } catch (error) {
220
+ console.log(chalk.red('⚠️ Unable to generate diff'));
221
+ console.log(chalk.gray(` ${error.message}`));
222
+ }
223
+ }
224
+ }
225
+
226
+ module.exports = DiffViewer;