kiro-spec-engine 1.0.0 → 1.2.0
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 +61 -12
- package/README.md +88 -0
- package/README.zh.md +88 -0
- package/bin/kiro-spec-engine.js +35 -0
- package/lib/adoption/adoption-strategy.js +516 -0
- package/lib/adoption/detection-engine.js +242 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/commands/adopt.js +231 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/upgrade/migration-engine.js +364 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/version/version-manager.js +287 -0
- package/package.json +3 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Engine
|
|
3
|
+
*
|
|
4
|
+
* Analyzes project structure and determines the appropriate adoption strategy.
|
|
5
|
+
* Detects project type, existing .kiro/ components, and potential conflicts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const {
|
|
10
|
+
pathExists,
|
|
11
|
+
listFiles,
|
|
12
|
+
readJSON
|
|
13
|
+
} = require('../utils/fs-utils');
|
|
14
|
+
|
|
15
|
+
class DetectionEngine {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.kiroDir = '.kiro';
|
|
18
|
+
this.versionFile = 'version.json';
|
|
19
|
+
this.specsDir = 'specs';
|
|
20
|
+
this.steeringDir = 'steering';
|
|
21
|
+
this.toolsDir = 'tools';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyzes project directory and returns detection result
|
|
26
|
+
*
|
|
27
|
+
* @param {string} projectPath - Absolute path to project root
|
|
28
|
+
* @returns {Promise<DetectionResult>}
|
|
29
|
+
*/
|
|
30
|
+
async analyze(projectPath) {
|
|
31
|
+
try {
|
|
32
|
+
const kiroPath = path.join(projectPath, this.kiroDir);
|
|
33
|
+
const hasKiroDir = await pathExists(kiroPath);
|
|
34
|
+
|
|
35
|
+
let hasVersionFile = false;
|
|
36
|
+
let hasSpecs = false;
|
|
37
|
+
let hasSteering = false;
|
|
38
|
+
let hasTools = false;
|
|
39
|
+
let existingVersion = null;
|
|
40
|
+
|
|
41
|
+
if (hasKiroDir) {
|
|
42
|
+
// Check for version.json
|
|
43
|
+
const versionPath = path.join(kiroPath, this.versionFile);
|
|
44
|
+
hasVersionFile = await pathExists(versionPath);
|
|
45
|
+
|
|
46
|
+
if (hasVersionFile) {
|
|
47
|
+
try {
|
|
48
|
+
const versionInfo = await readJSON(versionPath);
|
|
49
|
+
existingVersion = versionInfo['kse-version'] || null;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// Invalid version file
|
|
52
|
+
hasVersionFile = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for specs/
|
|
57
|
+
const specsPath = path.join(kiroPath, this.specsDir);
|
|
58
|
+
hasSpecs = await pathExists(specsPath);
|
|
59
|
+
|
|
60
|
+
// Check for steering/
|
|
61
|
+
const steeringPath = path.join(kiroPath, this.steeringDir);
|
|
62
|
+
hasSteering = await pathExists(steeringPath);
|
|
63
|
+
|
|
64
|
+
// Check for tools/
|
|
65
|
+
const toolsPath = path.join(kiroPath, this.toolsDir);
|
|
66
|
+
hasTools = await pathExists(toolsPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Detect project type
|
|
70
|
+
const projectType = await this.detectProjectType(projectPath);
|
|
71
|
+
|
|
72
|
+
// Detect conflicts (only if we're going to add template files)
|
|
73
|
+
const conflicts = hasKiroDir ? await this.detectConflicts(projectPath) : [];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
hasKiroDir,
|
|
77
|
+
hasVersionFile,
|
|
78
|
+
hasSpecs,
|
|
79
|
+
hasSteering,
|
|
80
|
+
hasTools,
|
|
81
|
+
projectType,
|
|
82
|
+
existingVersion,
|
|
83
|
+
conflicts
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to analyze project: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determines which adoption strategy to use
|
|
92
|
+
*
|
|
93
|
+
* @param {DetectionResult} result - Detection result from analyze()
|
|
94
|
+
* @returns {AdoptionMode} - 'fresh', 'partial', or 'full'
|
|
95
|
+
*/
|
|
96
|
+
determineStrategy(result) {
|
|
97
|
+
// Fresh adoption: no .kiro/ directory
|
|
98
|
+
if (!result.hasKiroDir) {
|
|
99
|
+
return 'fresh';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Partial adoption: .kiro/ exists but no version.json
|
|
103
|
+
if (!result.hasVersionFile) {
|
|
104
|
+
return 'partial';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Full adoption: complete .kiro/ with version.json
|
|
108
|
+
return 'full';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detects project type (Node.js, Python, mixed, unknown)
|
|
113
|
+
*
|
|
114
|
+
* @param {string} projectPath - Absolute path to project root
|
|
115
|
+
* @returns {Promise<ProjectType>}
|
|
116
|
+
*/
|
|
117
|
+
async detectProjectType(projectPath) {
|
|
118
|
+
try {
|
|
119
|
+
const hasPackageJson = await pathExists(path.join(projectPath, 'package.json'));
|
|
120
|
+
const hasRequirementsTxt = await pathExists(path.join(projectPath, 'requirements.txt'));
|
|
121
|
+
const hasPyprojectToml = await pathExists(path.join(projectPath, 'pyproject.toml'));
|
|
122
|
+
const hasSetupPy = await pathExists(path.join(projectPath, 'setup.py'));
|
|
123
|
+
|
|
124
|
+
const isNodeJs = hasPackageJson;
|
|
125
|
+
const isPython = hasRequirementsTxt || hasPyprojectToml || hasSetupPy;
|
|
126
|
+
|
|
127
|
+
if (isNodeJs && isPython) {
|
|
128
|
+
return 'mixed';
|
|
129
|
+
} else if (isNodeJs) {
|
|
130
|
+
return 'nodejs';
|
|
131
|
+
} else if (isPython) {
|
|
132
|
+
return 'python';
|
|
133
|
+
} else {
|
|
134
|
+
return 'unknown';
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`Failed to detect project type: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Detects conflicts between existing files and template files
|
|
143
|
+
*
|
|
144
|
+
* @param {string} projectPath - Absolute path to project root
|
|
145
|
+
* @returns {Promise<FileConflict[]>}
|
|
146
|
+
*/
|
|
147
|
+
async detectConflicts(projectPath) {
|
|
148
|
+
const conflicts = [];
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const kiroPath = path.join(projectPath, this.kiroDir);
|
|
152
|
+
|
|
153
|
+
// Define template files that might conflict
|
|
154
|
+
const templateFiles = [
|
|
155
|
+
'steering/CORE_PRINCIPLES.md',
|
|
156
|
+
'steering/ENVIRONMENT.md',
|
|
157
|
+
'steering/CURRENT_CONTEXT.md',
|
|
158
|
+
'steering/RULES_GUIDE.md',
|
|
159
|
+
'tools/ultrawork_enhancer.py',
|
|
160
|
+
'README.md',
|
|
161
|
+
'ultrawork-application-guide.md',
|
|
162
|
+
'ultrawork-integration-summary.md',
|
|
163
|
+
'sisyphus-deep-dive.md'
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const templateFile of templateFiles) {
|
|
167
|
+
const filePath = path.join(kiroPath, templateFile);
|
|
168
|
+
const exists = await pathExists(filePath);
|
|
169
|
+
|
|
170
|
+
if (exists) {
|
|
171
|
+
conflicts.push({
|
|
172
|
+
path: templateFile,
|
|
173
|
+
type: 'file',
|
|
174
|
+
existingContent: filePath,
|
|
175
|
+
templateContent: `template:${templateFile}`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return conflicts;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Failed to detect conflicts: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validates that a project path is valid
|
|
188
|
+
*
|
|
189
|
+
* @param {string} projectPath - Path to validate
|
|
190
|
+
* @returns {Promise<boolean>}
|
|
191
|
+
*/
|
|
192
|
+
async validateProjectPath(projectPath) {
|
|
193
|
+
try {
|
|
194
|
+
const exists = await pathExists(projectPath);
|
|
195
|
+
if (!exists) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if it's a directory
|
|
200
|
+
const fs = require('fs-extra');
|
|
201
|
+
const stats = await fs.stat(projectPath);
|
|
202
|
+
return stats.isDirectory();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gets a summary of the detection result for display
|
|
210
|
+
*
|
|
211
|
+
* @param {DetectionResult} result - Detection result
|
|
212
|
+
* @returns {string} - Human-readable summary
|
|
213
|
+
*/
|
|
214
|
+
getSummary(result) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
|
|
217
|
+
lines.push('Project Analysis:');
|
|
218
|
+
lines.push(` Project Type: ${result.projectType}`);
|
|
219
|
+
lines.push(` .kiro/ Directory: ${result.hasKiroDir ? 'Yes' : 'No'}`);
|
|
220
|
+
|
|
221
|
+
if (result.hasKiroDir) {
|
|
222
|
+
lines.push(` version.json: ${result.hasVersionFile ? 'Yes' : 'No'}`);
|
|
223
|
+
if (result.existingVersion) {
|
|
224
|
+
lines.push(` Current Version: ${result.existingVersion}`);
|
|
225
|
+
}
|
|
226
|
+
lines.push(` specs/: ${result.hasSpecs ? 'Yes' : 'No'}`);
|
|
227
|
+
lines.push(` steering/: ${result.hasSteering ? 'Yes' : 'No'}`);
|
|
228
|
+
lines.push(` tools/: ${result.hasTools ? 'Yes' : 'No'}`);
|
|
229
|
+
|
|
230
|
+
if (result.conflicts.length > 0) {
|
|
231
|
+
lines.push(` Conflicts: ${result.conflicts.length} file(s)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const strategy = this.determineStrategy(result);
|
|
236
|
+
lines.push(` Recommended Strategy: ${strategy}`);
|
|
237
|
+
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = DetectionEngine;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup System
|
|
3
|
+
*
|
|
4
|
+
* Creates, manages, and restores backups of the .kiro/ directory.
|
|
5
|
+
* Provides rollback capability for safe adoption and upgrade operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const {
|
|
11
|
+
pathExists,
|
|
12
|
+
copyDirectory,
|
|
13
|
+
ensureDirectory,
|
|
14
|
+
listFiles,
|
|
15
|
+
listFilesRecursive,
|
|
16
|
+
getDirectorySize,
|
|
17
|
+
readJSON,
|
|
18
|
+
writeJSON,
|
|
19
|
+
remove
|
|
20
|
+
} = require('../utils/fs-utils');
|
|
21
|
+
|
|
22
|
+
class BackupSystem {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.backupDirName = 'backups';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the path to the backups directory
|
|
29
|
+
*
|
|
30
|
+
* @param {string} projectPath - Absolute path to project root
|
|
31
|
+
* @returns {string} - Absolute path to backups directory
|
|
32
|
+
*/
|
|
33
|
+
getBackupDir(projectPath) {
|
|
34
|
+
return path.join(projectPath, '.kiro', this.backupDirName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gets the path to the .kiro directory
|
|
39
|
+
*
|
|
40
|
+
* @param {string} projectPath - Absolute path to project root
|
|
41
|
+
* @returns {string} - Absolute path to .kiro directory
|
|
42
|
+
*/
|
|
43
|
+
getKiroDir(projectPath) {
|
|
44
|
+
return path.join(projectPath, '.kiro');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates a backup ID based on type and timestamp
|
|
49
|
+
*
|
|
50
|
+
* @param {string} type - Backup type (adopt, upgrade, pre-rollback)
|
|
51
|
+
* @returns {string} - Backup ID (e.g., "adopt-2026-01-23-100000")
|
|
52
|
+
*/
|
|
53
|
+
generateBackupId(type) {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const year = now.getFullYear();
|
|
56
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
57
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
58
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
59
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
60
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
61
|
+
|
|
62
|
+
return `${type}-${year}-${month}-${day}-${hours}${minutes}${seconds}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates backup of .kiro/ directory
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectPath - Absolute path to project root
|
|
69
|
+
* @param {Object} options - Backup options
|
|
70
|
+
* @param {string} options.type - Backup type (adopt, upgrade, pre-rollback)
|
|
71
|
+
* @returns {Promise<BackupInfo>}
|
|
72
|
+
*/
|
|
73
|
+
async createBackup(projectPath, options = {}) {
|
|
74
|
+
const { type = 'manual' } = options;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const kiroDir = this.getKiroDir(projectPath);
|
|
78
|
+
|
|
79
|
+
// Check if .kiro/ exists
|
|
80
|
+
const kiroExists = await pathExists(kiroDir);
|
|
81
|
+
if (!kiroExists) {
|
|
82
|
+
throw new Error('.kiro/ directory does not exist');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create backups directory if it doesn't exist
|
|
86
|
+
const backupDir = this.getBackupDir(projectPath);
|
|
87
|
+
await ensureDirectory(backupDir);
|
|
88
|
+
|
|
89
|
+
// Generate backup ID
|
|
90
|
+
const backupId = this.generateBackupId(type);
|
|
91
|
+
const backupPath = path.join(backupDir, backupId);
|
|
92
|
+
|
|
93
|
+
// Check if backup already exists (shouldn't happen with timestamp)
|
|
94
|
+
const backupExists = await pathExists(backupPath);
|
|
95
|
+
if (backupExists) {
|
|
96
|
+
throw new Error(`Backup already exists: ${backupId}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create backup directory
|
|
100
|
+
await ensureDirectory(backupPath);
|
|
101
|
+
|
|
102
|
+
// Copy .kiro/ contents to backup (excluding backups/ itself)
|
|
103
|
+
const items = await listFiles(kiroDir);
|
|
104
|
+
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
// Skip the backups directory itself
|
|
107
|
+
if (item === this.backupDirName) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sourcePath = path.join(kiroDir, item);
|
|
112
|
+
const destPath = path.join(backupPath, item);
|
|
113
|
+
|
|
114
|
+
await copyDirectory(sourcePath, destPath, { overwrite: false });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get backup metadata
|
|
118
|
+
const files = await listFilesRecursive(backupPath);
|
|
119
|
+
const size = await getDirectorySize(backupPath);
|
|
120
|
+
|
|
121
|
+
// Read version from backup if it exists
|
|
122
|
+
const versionPath = path.join(backupPath, 'version.json');
|
|
123
|
+
let version = 'unknown';
|
|
124
|
+
try {
|
|
125
|
+
const versionExists = await pathExists(versionPath);
|
|
126
|
+
if (versionExists) {
|
|
127
|
+
const versionInfo = await readJSON(versionPath);
|
|
128
|
+
version = versionInfo['kse-version'] || 'unknown';
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Ignore version read errors
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create metadata file
|
|
135
|
+
const metadata = {
|
|
136
|
+
id: backupId,
|
|
137
|
+
type,
|
|
138
|
+
created: new Date().toISOString(),
|
|
139
|
+
version,
|
|
140
|
+
size,
|
|
141
|
+
files: files.length
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
145
|
+
await writeJSON(metadataPath, metadata, { spaces: 2 });
|
|
146
|
+
|
|
147
|
+
// Return backup info
|
|
148
|
+
return {
|
|
149
|
+
id: backupId,
|
|
150
|
+
type,
|
|
151
|
+
created: metadata.created,
|
|
152
|
+
version,
|
|
153
|
+
size,
|
|
154
|
+
files: files.length,
|
|
155
|
+
path: backupPath
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new Error(`Failed to create backup: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Lists available backups
|
|
164
|
+
*
|
|
165
|
+
* @param {string} projectPath - Absolute path to project root
|
|
166
|
+
* @returns {Promise<BackupInfo[]>} - Array of backup info, sorted by date (newest first)
|
|
167
|
+
*/
|
|
168
|
+
async listBackups(projectPath) {
|
|
169
|
+
try {
|
|
170
|
+
const backupDir = this.getBackupDir(projectPath);
|
|
171
|
+
|
|
172
|
+
// Check if backups directory exists
|
|
173
|
+
const backupDirExists = await pathExists(backupDir);
|
|
174
|
+
if (!backupDirExists) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// List backup directories
|
|
179
|
+
const items = await listFiles(backupDir);
|
|
180
|
+
const backups = [];
|
|
181
|
+
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const backupPath = path.join(backupDir, item);
|
|
184
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Check if metadata exists
|
|
188
|
+
const metadataExists = await pathExists(metadataPath);
|
|
189
|
+
if (!metadataExists) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Read metadata
|
|
194
|
+
const metadata = await readJSON(metadataPath);
|
|
195
|
+
|
|
196
|
+
backups.push({
|
|
197
|
+
id: metadata.id,
|
|
198
|
+
type: metadata.type,
|
|
199
|
+
created: metadata.created,
|
|
200
|
+
version: metadata.version,
|
|
201
|
+
size: metadata.size,
|
|
202
|
+
files: metadata.files,
|
|
203
|
+
path: backupPath
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Skip backups with invalid metadata
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Sort by created date (newest first)
|
|
212
|
+
backups.sort((a, b) => {
|
|
213
|
+
return new Date(b.created) - new Date(a.created);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return backups;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Failed to list backups: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Restores from backup
|
|
224
|
+
*
|
|
225
|
+
* @param {string} projectPath - Absolute path to project root
|
|
226
|
+
* @param {string} backupId - Backup ID to restore from
|
|
227
|
+
* @returns {Promise<RestoreResult>}
|
|
228
|
+
*/
|
|
229
|
+
async restore(projectPath, backupId) {
|
|
230
|
+
try {
|
|
231
|
+
const backupDir = this.getBackupDir(projectPath);
|
|
232
|
+
const backupPath = path.join(backupDir, backupId);
|
|
233
|
+
|
|
234
|
+
// Check if backup exists
|
|
235
|
+
const backupExists = await pathExists(backupPath);
|
|
236
|
+
if (!backupExists) {
|
|
237
|
+
throw new Error(`Backup not found: ${backupId}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Validate backup before restoring
|
|
241
|
+
const isValid = await this.validateBackup(backupPath);
|
|
242
|
+
if (!isValid) {
|
|
243
|
+
throw new Error(`Backup validation failed: ${backupId}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const kiroDir = this.getKiroDir(projectPath);
|
|
247
|
+
|
|
248
|
+
// Get list of items to restore (excluding metadata.json)
|
|
249
|
+
const items = await listFiles(backupPath);
|
|
250
|
+
const itemsToRestore = items.filter(item => item !== 'metadata.json');
|
|
251
|
+
|
|
252
|
+
// Remove existing .kiro/ contents (except backups/)
|
|
253
|
+
const existingItems = await listFiles(kiroDir);
|
|
254
|
+
for (const item of existingItems) {
|
|
255
|
+
if (item === this.backupDirName) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const itemPath = path.join(kiroDir, item);
|
|
260
|
+
await remove(itemPath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Restore items from backup
|
|
264
|
+
const restoredFiles = [];
|
|
265
|
+
for (const item of itemsToRestore) {
|
|
266
|
+
const sourcePath = path.join(backupPath, item);
|
|
267
|
+
const destPath = path.join(kiroDir, item);
|
|
268
|
+
|
|
269
|
+
await copyDirectory(sourcePath, destPath, { overwrite: true });
|
|
270
|
+
restoredFiles.push(item);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
backupId,
|
|
276
|
+
filesRestored: restoredFiles.length,
|
|
277
|
+
files: restoredFiles
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new Error(`Failed to restore backup: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates backup integrity
|
|
286
|
+
*
|
|
287
|
+
* @param {string} backupPath - Absolute path to backup directory
|
|
288
|
+
* @returns {Promise<boolean>}
|
|
289
|
+
*/
|
|
290
|
+
async validateBackup(backupPath) {
|
|
291
|
+
try {
|
|
292
|
+
// Check if backup directory exists
|
|
293
|
+
const backupExists = await pathExists(backupPath);
|
|
294
|
+
if (!backupExists) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if metadata exists
|
|
299
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
300
|
+
const metadataExists = await pathExists(metadataPath);
|
|
301
|
+
if (!metadataExists) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Read and validate metadata
|
|
306
|
+
const metadata = await readJSON(metadataPath);
|
|
307
|
+
if (!metadata.id || !metadata.type || !metadata.created) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Count files in backup
|
|
312
|
+
const files = await listFilesRecursive(backupPath);
|
|
313
|
+
// Subtract 1 for metadata.json itself
|
|
314
|
+
const fileCount = files.length - 1;
|
|
315
|
+
|
|
316
|
+
// Verify file count matches metadata (allow some tolerance)
|
|
317
|
+
// Files might be slightly different due to metadata.json
|
|
318
|
+
if (Math.abs(fileCount - metadata.files) > 1) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Verify version.json exists and is valid (if present)
|
|
323
|
+
const versionPath = path.join(backupPath, 'version.json');
|
|
324
|
+
const versionExists = await pathExists(versionPath);
|
|
325
|
+
if (versionExists) {
|
|
326
|
+
try {
|
|
327
|
+
const versionInfo = await readJSON(versionPath);
|
|
328
|
+
// Basic validation
|
|
329
|
+
if (!versionInfo['kse-version']) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return true;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Cleans old backups (keeps last N backups)
|
|
345
|
+
*
|
|
346
|
+
* @param {string} projectPath - Absolute path to project root
|
|
347
|
+
* @param {number} keepCount - Number of backups to keep (default: 5)
|
|
348
|
+
* @returns {Promise<void>}
|
|
349
|
+
*/
|
|
350
|
+
async cleanOldBackups(projectPath, keepCount = 5) {
|
|
351
|
+
try {
|
|
352
|
+
// Get all backups sorted by date (newest first)
|
|
353
|
+
const backups = await this.listBackups(projectPath);
|
|
354
|
+
|
|
355
|
+
// If we have fewer backups than keepCount, nothing to do
|
|
356
|
+
if (backups.length <= keepCount) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Remove old backups
|
|
361
|
+
const backupsToRemove = backups.slice(keepCount);
|
|
362
|
+
|
|
363
|
+
for (const backup of backupsToRemove) {
|
|
364
|
+
await remove(backup.path);
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw new Error(`Failed to clean old backups: ${error.message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = BackupSystem;
|