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,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Engine
|
|
3
|
+
*
|
|
4
|
+
* Manages version upgrades by planning and executing migration scripts.
|
|
5
|
+
* Handles incremental upgrades through intermediate versions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { pathExists, readJSON } = require('../utils/fs-utils');
|
|
10
|
+
const VersionManager = require('../version/version-manager');
|
|
11
|
+
|
|
12
|
+
class MigrationEngine {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.versionManager = new VersionManager();
|
|
15
|
+
this.migrationsDir = path.join(__dirname, 'migrations');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Plans upgrade from current to target version
|
|
20
|
+
*
|
|
21
|
+
* @param {string} fromVersion - Current version
|
|
22
|
+
* @param {string} toVersion - Target version
|
|
23
|
+
* @returns {Promise<UpgradePlan>}
|
|
24
|
+
*/
|
|
25
|
+
async planUpgrade(fromVersion, toVersion) {
|
|
26
|
+
try {
|
|
27
|
+
// Calculate upgrade path
|
|
28
|
+
const upgradePath = this.versionManager.calculateUpgradePath(fromVersion, toVersion);
|
|
29
|
+
|
|
30
|
+
// Build list of migrations needed
|
|
31
|
+
const migrations = [];
|
|
32
|
+
for (let i = 0; i < upgradePath.length - 1; i++) {
|
|
33
|
+
const from = upgradePath[i];
|
|
34
|
+
const to = upgradePath[i + 1];
|
|
35
|
+
|
|
36
|
+
// Check if migration script exists
|
|
37
|
+
const migrationScript = await this.findMigrationScript(from, to);
|
|
38
|
+
|
|
39
|
+
// Check compatibility
|
|
40
|
+
const compatibility = this.versionManager.checkCompatibility(from, to);
|
|
41
|
+
|
|
42
|
+
migrations.push({
|
|
43
|
+
from,
|
|
44
|
+
to,
|
|
45
|
+
breaking: compatibility.breaking,
|
|
46
|
+
script: migrationScript,
|
|
47
|
+
required: compatibility.migration === 'required'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Estimate time (rough estimate: 10 seconds per migration)
|
|
52
|
+
const estimatedTime = migrations.length > 0
|
|
53
|
+
? `${migrations.length * 10} seconds`
|
|
54
|
+
: '< 5 seconds';
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
fromVersion,
|
|
58
|
+
toVersion,
|
|
59
|
+
path: upgradePath,
|
|
60
|
+
migrations,
|
|
61
|
+
estimatedTime,
|
|
62
|
+
backupRequired: true
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`Failed to plan upgrade: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Executes upgrade plan
|
|
71
|
+
*
|
|
72
|
+
* @param {string} projectPath - Absolute path to project root
|
|
73
|
+
* @param {UpgradePlan} plan - Upgrade plan from planUpgrade()
|
|
74
|
+
* @param {Object} options - Upgrade options
|
|
75
|
+
* @param {boolean} options.dryRun - If true, don't make changes
|
|
76
|
+
* @param {Function} options.onProgress - Progress callback (step, total, message)
|
|
77
|
+
* @returns {Promise<UpgradeResult>}
|
|
78
|
+
*/
|
|
79
|
+
async executeUpgrade(projectPath, plan, options = {}) {
|
|
80
|
+
const { dryRun = false, onProgress = null } = options;
|
|
81
|
+
|
|
82
|
+
const migrationsExecuted = [];
|
|
83
|
+
const errors = [];
|
|
84
|
+
const warnings = [];
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Read current version info
|
|
88
|
+
const versionInfo = await this.versionManager.readVersion(projectPath);
|
|
89
|
+
if (!versionInfo) {
|
|
90
|
+
throw new Error('version.json not found');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Verify starting version matches plan
|
|
94
|
+
if (versionInfo['kse-version'] !== plan.fromVersion) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Version mismatch: expected ${plan.fromVersion}, found ${versionInfo['kse-version']}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
fromVersion: plan.fromVersion,
|
|
104
|
+
toVersion: plan.toVersion,
|
|
105
|
+
migrationsExecuted: plan.migrations.map(m => ({
|
|
106
|
+
from: m.from,
|
|
107
|
+
to: m.to,
|
|
108
|
+
success: true,
|
|
109
|
+
changes: ['(dry-run) No changes made'],
|
|
110
|
+
error: null
|
|
111
|
+
})),
|
|
112
|
+
backupId: null,
|
|
113
|
+
errors: [],
|
|
114
|
+
warnings: ['Dry run - no changes made']
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Execute migrations sequentially
|
|
119
|
+
for (let i = 0; i < plan.migrations.length; i++) {
|
|
120
|
+
const migration = plan.migrations[i];
|
|
121
|
+
|
|
122
|
+
if (onProgress) {
|
|
123
|
+
onProgress(i + 1, plan.migrations.length, `Migrating ${migration.from} → ${migration.to}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Load migration script if it exists
|
|
128
|
+
let migrationResult = {
|
|
129
|
+
from: migration.from,
|
|
130
|
+
to: migration.to,
|
|
131
|
+
success: true,
|
|
132
|
+
changes: [],
|
|
133
|
+
error: null
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (migration.script) {
|
|
137
|
+
// Execute migration script
|
|
138
|
+
const script = await this.loadMigration(migration.from, migration.to);
|
|
139
|
+
|
|
140
|
+
if (script) {
|
|
141
|
+
const result = await script.migrate(projectPath, {
|
|
142
|
+
fromVersion: migration.from,
|
|
143
|
+
toVersion: migration.to,
|
|
144
|
+
versionInfo
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
migrationResult.changes = result.changes || [];
|
|
148
|
+
} else {
|
|
149
|
+
migrationResult.changes.push('No migration script needed');
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
migrationResult.changes.push('No migration script needed');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Update version info
|
|
156
|
+
this.versionManager.addUpgradeHistory(
|
|
157
|
+
versionInfo,
|
|
158
|
+
migration.from,
|
|
159
|
+
migration.to,
|
|
160
|
+
true
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Write updated version info
|
|
164
|
+
await this.versionManager.writeVersion(projectPath, versionInfo);
|
|
165
|
+
|
|
166
|
+
migrationsExecuted.push(migrationResult);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Migration failed - record error and stop
|
|
169
|
+
const migrationResult = {
|
|
170
|
+
from: migration.from,
|
|
171
|
+
to: migration.to,
|
|
172
|
+
success: false,
|
|
173
|
+
changes: [],
|
|
174
|
+
error: error.message
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
migrationsExecuted.push(migrationResult);
|
|
178
|
+
errors.push(`Migration ${migration.from} → ${migration.to} failed: ${error.message}`);
|
|
179
|
+
|
|
180
|
+
// Add failed upgrade to history
|
|
181
|
+
this.versionManager.addUpgradeHistory(
|
|
182
|
+
versionInfo,
|
|
183
|
+
migration.from,
|
|
184
|
+
migration.to,
|
|
185
|
+
false,
|
|
186
|
+
error.message
|
|
187
|
+
);
|
|
188
|
+
await this.versionManager.writeVersion(projectPath, versionInfo);
|
|
189
|
+
|
|
190
|
+
// Stop execution on first failure
|
|
191
|
+
throw new Error(`Migration failed: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
fromVersion: plan.fromVersion,
|
|
198
|
+
toVersion: plan.toVersion,
|
|
199
|
+
migrationsExecuted,
|
|
200
|
+
backupId: null, // Backup ID should be set by caller
|
|
201
|
+
errors,
|
|
202
|
+
warnings
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
fromVersion: plan.fromVersion,
|
|
208
|
+
toVersion: plan.toVersion,
|
|
209
|
+
migrationsExecuted,
|
|
210
|
+
backupId: null,
|
|
211
|
+
errors: [error.message, ...errors],
|
|
212
|
+
warnings
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Loads migration script for version transition
|
|
219
|
+
*
|
|
220
|
+
* @param {string} fromVersion - Source version
|
|
221
|
+
* @param {string} toVersion - Target version
|
|
222
|
+
* @returns {Promise<MigrationScript|null>}
|
|
223
|
+
*/
|
|
224
|
+
async loadMigration(fromVersion, toVersion) {
|
|
225
|
+
try {
|
|
226
|
+
const scriptPath = await this.findMigrationScript(fromVersion, toVersion);
|
|
227
|
+
|
|
228
|
+
if (!scriptPath) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load the migration script
|
|
233
|
+
const script = require(scriptPath);
|
|
234
|
+
|
|
235
|
+
// Validate script interface
|
|
236
|
+
if (!script.migrate || typeof script.migrate !== 'function') {
|
|
237
|
+
throw new Error(`Invalid migration script: missing migrate() function`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return script;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error(`Failed to load migration script: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Finds migration script file for version transition
|
|
248
|
+
*
|
|
249
|
+
* @param {string} fromVersion - Source version
|
|
250
|
+
* @param {string} toVersion - Target version
|
|
251
|
+
* @returns {Promise<string|null>} - Absolute path to script or null if not found
|
|
252
|
+
*/
|
|
253
|
+
async findMigrationScript(fromVersion, toVersion) {
|
|
254
|
+
// Try different naming conventions
|
|
255
|
+
const possibleNames = [
|
|
256
|
+
`${fromVersion}-to-${toVersion}.js`,
|
|
257
|
+
`${fromVersion}_to_${toVersion}.js`,
|
|
258
|
+
`v${fromVersion}-to-v${toVersion}.js`
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const name of possibleNames) {
|
|
262
|
+
const scriptPath = path.join(this.migrationsDir, name);
|
|
263
|
+
const exists = await pathExists(scriptPath);
|
|
264
|
+
|
|
265
|
+
if (exists) {
|
|
266
|
+
return scriptPath;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validates upgrade result
|
|
275
|
+
*
|
|
276
|
+
* @param {string} projectPath - Absolute path to project root
|
|
277
|
+
* @returns {Promise<ValidationResult>}
|
|
278
|
+
*/
|
|
279
|
+
async validate(projectPath) {
|
|
280
|
+
const errors = [];
|
|
281
|
+
const warnings = [];
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Check if .kiro/ directory exists
|
|
285
|
+
const kiroPath = path.join(projectPath, '.kiro');
|
|
286
|
+
const kiroExists = await pathExists(kiroPath);
|
|
287
|
+
|
|
288
|
+
if (!kiroExists) {
|
|
289
|
+
errors.push('.kiro/ directory not found');
|
|
290
|
+
return { success: false, errors, warnings };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check if version.json exists and is valid
|
|
294
|
+
const versionInfo = await this.versionManager.readVersion(projectPath);
|
|
295
|
+
|
|
296
|
+
if (!versionInfo) {
|
|
297
|
+
errors.push('version.json not found or invalid');
|
|
298
|
+
return { success: false, errors, warnings };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check required directories
|
|
302
|
+
const requiredDirs = ['specs', 'steering', 'tools', 'backups'];
|
|
303
|
+
|
|
304
|
+
for (const dir of requiredDirs) {
|
|
305
|
+
const dirPath = path.join(kiroPath, dir);
|
|
306
|
+
const exists = await pathExists(dirPath);
|
|
307
|
+
|
|
308
|
+
if (!exists) {
|
|
309
|
+
warnings.push(`${dir}/ directory not found`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check required steering files
|
|
314
|
+
const requiredSteeringFiles = [
|
|
315
|
+
'steering/CORE_PRINCIPLES.md',
|
|
316
|
+
'steering/ENVIRONMENT.md',
|
|
317
|
+
'steering/CURRENT_CONTEXT.md',
|
|
318
|
+
'steering/RULES_GUIDE.md'
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const file of requiredSteeringFiles) {
|
|
322
|
+
const filePath = path.join(kiroPath, file);
|
|
323
|
+
const exists = await pathExists(filePath);
|
|
324
|
+
|
|
325
|
+
if (!exists) {
|
|
326
|
+
warnings.push(`${file} not found`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
success: errors.length === 0,
|
|
332
|
+
errors,
|
|
333
|
+
warnings
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
errors.push(`Validation failed: ${error.message}`);
|
|
337
|
+
return { success: false, errors, warnings };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Gets available migrations
|
|
343
|
+
*
|
|
344
|
+
* @returns {Promise<string[]>} - Array of migration script names
|
|
345
|
+
*/
|
|
346
|
+
async getAvailableMigrations() {
|
|
347
|
+
try {
|
|
348
|
+
const exists = await pathExists(this.migrationsDir);
|
|
349
|
+
|
|
350
|
+
if (!exists) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fs = require('fs-extra');
|
|
355
|
+
const files = await fs.readdir(this.migrationsDir);
|
|
356
|
+
|
|
357
|
+
return files.filter(file => file.endsWith('.js'));
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = MigrationEngine;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Migration Scripts Directory
|
|
2
|
+
|
|
3
|
+
This directory contains migration scripts for upgrading between versions.
|
|
4
|
+
|
|
5
|
+
## Migration Script Format
|
|
6
|
+
|
|
7
|
+
Each migration script should follow this format:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
module.exports = {
|
|
11
|
+
version: "1.1.0",
|
|
12
|
+
breaking: false,
|
|
13
|
+
description: "Description of what this migration does",
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes migration
|
|
17
|
+
* @param {string} projectPath - Absolute path to project root
|
|
18
|
+
* @param {MigrationContext} context - Migration context
|
|
19
|
+
* @returns {Promise<MigrationResult>}
|
|
20
|
+
*/
|
|
21
|
+
async migrate(projectPath, context) {
|
|
22
|
+
const changes = [];
|
|
23
|
+
|
|
24
|
+
// Migration logic here
|
|
25
|
+
// Example: Update file structure, modify configs, etc.
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
changes
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rolls back migration (optional)
|
|
35
|
+
* @param {string} projectPath - Absolute path to project root
|
|
36
|
+
* @param {MigrationContext} context - Migration context
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async rollback(projectPath, context) {
|
|
40
|
+
// Rollback logic here
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Naming Convention
|
|
46
|
+
|
|
47
|
+
Migration scripts should be named: `{fromVersion}-to-{toVersion}.js`
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
- `1.0.0-to-1.1.0.js`
|
|
51
|
+
- `1.1.0-to-1.2.0.js`
|
|
52
|
+
- `1.2.0-to-2.0.0.js`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: 1.0.0 → 1.1.0
|
|
3
|
+
*
|
|
4
|
+
* Adds version management foundation:
|
|
5
|
+
* - Ensures version.json exists with correct structure
|
|
6
|
+
* - Adds backups/ directory if missing
|
|
7
|
+
* - No breaking changes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { pathExists, ensureDirectory, writeJSON } = require('../../utils/fs-utils');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
version: "1.1.0",
|
|
15
|
+
breaking: false,
|
|
16
|
+
description: "Add version management foundation (version.json, backups/)",
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Executes migration from 1.0.0 to 1.1.0
|
|
20
|
+
*
|
|
21
|
+
* @param {string} projectPath - Absolute path to project root
|
|
22
|
+
* @param {MigrationContext} context - Migration context
|
|
23
|
+
* @returns {Promise<MigrationResult>}
|
|
24
|
+
*/
|
|
25
|
+
async migrate(projectPath, context) {
|
|
26
|
+
const changes = [];
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const kiroPath = path.join(projectPath, '.kiro');
|
|
30
|
+
|
|
31
|
+
// 1. Ensure backups/ directory exists
|
|
32
|
+
const backupsPath = path.join(kiroPath, 'backups');
|
|
33
|
+
const backupsExists = await pathExists(backupsPath);
|
|
34
|
+
|
|
35
|
+
if (!backupsExists) {
|
|
36
|
+
await ensureDirectory(backupsPath);
|
|
37
|
+
changes.push('Created backups/ directory');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Ensure version.json has correct structure
|
|
41
|
+
// (This is handled by VersionManager, but we verify it here)
|
|
42
|
+
const versionPath = path.join(kiroPath, 'version.json');
|
|
43
|
+
const versionExists = await pathExists(versionPath);
|
|
44
|
+
|
|
45
|
+
if (versionExists) {
|
|
46
|
+
changes.push('Verified version.json structure');
|
|
47
|
+
} else {
|
|
48
|
+
changes.push('version.json will be created by VersionManager');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Add any other 1.1.0-specific changes here
|
|
52
|
+
// (None for this version - it's just the foundation)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
changes
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`Migration 1.0.0 → 1.1.0 failed: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rolls back migration from 1.1.0 to 1.0.0
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectPath - Absolute path to project root
|
|
67
|
+
* @param {MigrationContext} context - Migration context
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
async rollback(projectPath, context) {
|
|
71
|
+
// For 1.0.0 → 1.1.0, rollback is simple:
|
|
72
|
+
// - Remove backups/ directory (but keep backups for safety)
|
|
73
|
+
// - version.json will be handled by VersionManager
|
|
74
|
+
|
|
75
|
+
// Note: We don't actually remove anything to preserve data safety
|
|
76
|
+
// The backup system will handle restoration if needed
|
|
77
|
+
}
|
|
78
|
+
};
|