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.
@@ -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
+ };