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.
- package/CHANGELOG.md +90 -1
- 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 +26 -15
- 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 +1 -1
- package/template/.kiro/README.md +237 -197
|
@@ -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
|
-
//
|
|
50
|
-
|
|
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
|
-
'
|
|
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
|
|
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 (
|
|
330
|
-
const copyResult = await this.copyTemplateFiles(projectPath, { overwrite:
|
|
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;
|