kiro-spec-engine 1.1.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,516 @@
1
+ /**
2
+ * Adoption Strategy
3
+ *
4
+ * Implements different adoption strategies based on project state:
5
+ * - Fresh: Create complete .kiro/ structure from scratch
6
+ * - Partial: Add missing components to existing .kiro/
7
+ * - Full: Upgrade existing complete .kiro/ to current version
8
+ */
9
+
10
+ const path = require('path');
11
+ const {
12
+ pathExists,
13
+ ensureDirectory,
14
+ copyDirectory,
15
+ safeCopy,
16
+ listFiles,
17
+ readJSON,
18
+ writeJSON
19
+ } = require('../utils/fs-utils');
20
+ const VersionManager = require('../version/version-manager');
21
+
22
+ /**
23
+ * Base class for adoption strategies
24
+ */
25
+ class AdoptionStrategy {
26
+ constructor() {
27
+ this.versionManager = new VersionManager();
28
+ this.kiroDir = '.kiro';
29
+ }
30
+
31
+ /**
32
+ * Gets the path to .kiro/ directory
33
+ *
34
+ * @param {string} projectPath - Absolute path to project root
35
+ * @returns {string}
36
+ */
37
+ getKiroPath(projectPath) {
38
+ return path.join(projectPath, this.kiroDir);
39
+ }
40
+
41
+ /**
42
+ * Gets the path to template directory
43
+ * This would be embedded in the kse package
44
+ * For now, we'll use a placeholder
45
+ *
46
+ * @returns {string}
47
+ */
48
+ 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');
52
+ }
53
+
54
+ /**
55
+ * Executes adoption strategy
56
+ * Must be implemented by subclasses
57
+ *
58
+ * @param {string} projectPath - Absolute path to project root
59
+ * @param {AdoptionMode} mode - Adoption mode
60
+ * @param {AdoptionOptions} options - Adoption options
61
+ * @returns {Promise<AdoptionResult>}
62
+ */
63
+ async execute(projectPath, mode, options) {
64
+ throw new Error('execute() must be implemented by subclass');
65
+ }
66
+
67
+ /**
68
+ * Creates initial directory structure
69
+ *
70
+ * @param {string} kiroPath - Path to .kiro/ directory
71
+ * @returns {Promise<void>}
72
+ */
73
+ async createDirectoryStructure(kiroPath) {
74
+ await ensureDirectory(kiroPath);
75
+ await ensureDirectory(path.join(kiroPath, 'specs'));
76
+ await ensureDirectory(path.join(kiroPath, 'steering'));
77
+ await ensureDirectory(path.join(kiroPath, 'tools'));
78
+ await ensureDirectory(path.join(kiroPath, 'backups'));
79
+ }
80
+
81
+ /**
82
+ * Copies template files to project
83
+ *
84
+ * @param {string} projectPath - Absolute path to project root
85
+ * @param {Object} options - Copy options
86
+ * @param {boolean} options.overwrite - Whether to overwrite existing files
87
+ * @param {string[]} options.skip - Files to skip
88
+ * @returns {Promise<{created: string[], updated: string[], skipped: string[]}>}
89
+ */
90
+ async copyTemplateFiles(projectPath, options = {}) {
91
+ const { overwrite = false, skip = [] } = options;
92
+ const kiroPath = this.getKiroPath(projectPath);
93
+ const templatePath = this.getTemplatePath();
94
+
95
+ const created = [];
96
+ const updated = [];
97
+ const skipped = [];
98
+
99
+ // Check if template directory exists
100
+ const templateExists = await pathExists(templatePath);
101
+ if (!templateExists) {
102
+ // Template directory doesn't exist yet - this is expected during development
103
+ // In production, templates would be bundled with the package
104
+ return { created, updated, skipped };
105
+ }
106
+
107
+ // Define template structure
108
+ const templateFiles = [
109
+ 'steering/CORE_PRINCIPLES.md',
110
+ 'steering/ENVIRONMENT.md',
111
+ 'steering/CURRENT_CONTEXT.md',
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'
118
+ ];
119
+
120
+ for (const file of templateFiles) {
121
+ // Check if file should be skipped
122
+ if (skip.includes(file)) {
123
+ skipped.push(file);
124
+ continue;
125
+ }
126
+
127
+ const sourcePath = path.join(templatePath, file);
128
+ const destPath = path.join(kiroPath, file);
129
+
130
+ // Check if source exists
131
+ const sourceExists = await pathExists(sourcePath);
132
+ if (!sourceExists) {
133
+ skipped.push(file);
134
+ continue;
135
+ }
136
+
137
+ // Check if destination exists
138
+ const destExists = await pathExists(destPath);
139
+
140
+ if (destExists && !overwrite) {
141
+ skipped.push(file);
142
+ continue;
143
+ }
144
+
145
+ try {
146
+ await safeCopy(sourcePath, destPath, { overwrite });
147
+
148
+ if (destExists) {
149
+ updated.push(file);
150
+ } else {
151
+ created.push(file);
152
+ }
153
+ } catch (error) {
154
+ // If copy fails, add to skipped
155
+ skipped.push(file);
156
+ }
157
+ }
158
+
159
+ return { created, updated, skipped };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Fresh Adoption Strategy
165
+ * Creates complete .kiro/ structure from scratch
166
+ */
167
+ class FreshAdoption extends AdoptionStrategy {
168
+ /**
169
+ * Executes fresh adoption
170
+ *
171
+ * @param {string} projectPath - Absolute path to project root
172
+ * @param {AdoptionMode} mode - Should be 'fresh'
173
+ * @param {AdoptionOptions} options - Adoption options
174
+ * @returns {Promise<AdoptionResult>}
175
+ */
176
+ async execute(projectPath, mode, options = {}) {
177
+ const { kseVersion = '1.0.0', dryRun = false } = options;
178
+
179
+ const filesCreated = [];
180
+ const filesUpdated = [];
181
+ const filesSkipped = [];
182
+ const errors = [];
183
+ const warnings = [];
184
+
185
+ try {
186
+ const kiroPath = this.getKiroPath(projectPath);
187
+
188
+ // Check if .kiro/ already exists
189
+ const kiroExists = await pathExists(kiroPath);
190
+ if (kiroExists) {
191
+ throw new Error('.kiro/ directory already exists - use partial or full adoption');
192
+ }
193
+
194
+ if (dryRun) {
195
+ return {
196
+ success: true,
197
+ mode: 'fresh',
198
+ filesCreated: ['(dry-run) .kiro/ structure would be created'],
199
+ filesUpdated: [],
200
+ filesSkipped: [],
201
+ backupId: null,
202
+ errors: [],
203
+ warnings: []
204
+ };
205
+ }
206
+
207
+ // Create directory structure
208
+ await this.createDirectoryStructure(kiroPath);
209
+ filesCreated.push('.kiro/');
210
+ filesCreated.push('.kiro/specs/');
211
+ filesCreated.push('.kiro/steering/');
212
+ filesCreated.push('.kiro/tools/');
213
+ filesCreated.push('.kiro/backups/');
214
+
215
+ // Copy template files
216
+ const copyResult = await this.copyTemplateFiles(projectPath, { overwrite: false });
217
+ filesCreated.push(...copyResult.created);
218
+ filesUpdated.push(...copyResult.updated);
219
+ filesSkipped.push(...copyResult.skipped);
220
+
221
+ // Create version.json
222
+ const versionInfo = this.versionManager.createVersionInfo(kseVersion);
223
+ await this.versionManager.writeVersion(projectPath, versionInfo);
224
+ filesCreated.push('version.json');
225
+
226
+ return {
227
+ success: true,
228
+ mode: 'fresh',
229
+ filesCreated,
230
+ filesUpdated,
231
+ filesSkipped,
232
+ backupId: null,
233
+ errors,
234
+ warnings
235
+ };
236
+ } catch (error) {
237
+ errors.push(error.message);
238
+ return {
239
+ success: false,
240
+ mode: 'fresh',
241
+ filesCreated,
242
+ filesUpdated,
243
+ filesSkipped,
244
+ backupId: null,
245
+ errors,
246
+ warnings
247
+ };
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Partial Adoption Strategy
254
+ * Adds missing components to existing .kiro/
255
+ */
256
+ class PartialAdoption extends AdoptionStrategy {
257
+ /**
258
+ * Executes partial adoption
259
+ *
260
+ * @param {string} projectPath - Absolute path to project root
261
+ * @param {AdoptionMode} mode - Should be 'partial'
262
+ * @param {AdoptionOptions} options - Adoption options
263
+ * @returns {Promise<AdoptionResult>}
264
+ */
265
+ async execute(projectPath, mode, options = {}) {
266
+ const { kseVersion = '1.0.0', dryRun = false, backupId = null } = options;
267
+
268
+ const filesCreated = [];
269
+ const filesUpdated = [];
270
+ const filesSkipped = [];
271
+ const errors = [];
272
+ const warnings = [];
273
+
274
+ try {
275
+ const kiroPath = this.getKiroPath(projectPath);
276
+
277
+ // Check if .kiro/ exists
278
+ const kiroExists = await pathExists(kiroPath);
279
+ if (!kiroExists) {
280
+ throw new Error('.kiro/ directory does not exist - use fresh adoption');
281
+ }
282
+
283
+ // Check if version.json exists
284
+ const versionPath = path.join(kiroPath, 'version.json');
285
+ const versionExists = await pathExists(versionPath);
286
+ if (versionExists) {
287
+ warnings.push('version.json already exists - use full adoption for upgrades');
288
+ }
289
+
290
+ if (dryRun) {
291
+ return {
292
+ success: true,
293
+ mode: 'partial',
294
+ filesCreated: ['(dry-run) Missing components would be added'],
295
+ filesUpdated: [],
296
+ filesSkipped: [],
297
+ backupId,
298
+ errors: [],
299
+ warnings
300
+ };
301
+ }
302
+
303
+ // Ensure all required directories exist
304
+ const specsPath = path.join(kiroPath, 'specs');
305
+ const steeringPath = path.join(kiroPath, 'steering');
306
+ const toolsPath = path.join(kiroPath, 'tools');
307
+ const backupsPath = path.join(kiroPath, 'backups');
308
+
309
+ if (!await pathExists(specsPath)) {
310
+ await ensureDirectory(specsPath);
311
+ filesCreated.push('specs/');
312
+ }
313
+
314
+ if (!await pathExists(steeringPath)) {
315
+ await ensureDirectory(steeringPath);
316
+ filesCreated.push('steering/');
317
+ }
318
+
319
+ if (!await pathExists(toolsPath)) {
320
+ await ensureDirectory(toolsPath);
321
+ filesCreated.push('tools/');
322
+ }
323
+
324
+ if (!await pathExists(backupsPath)) {
325
+ await ensureDirectory(backupsPath);
326
+ filesCreated.push('backups/');
327
+ }
328
+
329
+ // Copy template files (don't overwrite existing)
330
+ const copyResult = await this.copyTemplateFiles(projectPath, { overwrite: false });
331
+ filesCreated.push(...copyResult.created);
332
+ filesUpdated.push(...copyResult.updated);
333
+ filesSkipped.push(...copyResult.skipped);
334
+
335
+ // Create or update version.json
336
+ if (!versionExists) {
337
+ const versionInfo = this.versionManager.createVersionInfo(kseVersion);
338
+ await this.versionManager.writeVersion(projectPath, versionInfo);
339
+ filesCreated.push('version.json');
340
+ } else {
341
+ // Update existing version.json
342
+ const versionInfo = await this.versionManager.readVersion(projectPath);
343
+ if (versionInfo) {
344
+ versionInfo['kse-version'] = kseVersion;
345
+ versionInfo['template-version'] = kseVersion;
346
+ versionInfo['last-upgraded'] = new Date().toISOString();
347
+ await this.versionManager.writeVersion(projectPath, versionInfo);
348
+ filesUpdated.push('version.json');
349
+ }
350
+ }
351
+
352
+ return {
353
+ success: true,
354
+ mode: 'partial',
355
+ filesCreated,
356
+ filesUpdated,
357
+ filesSkipped,
358
+ backupId,
359
+ errors,
360
+ warnings
361
+ };
362
+ } catch (error) {
363
+ errors.push(error.message);
364
+ return {
365
+ success: false,
366
+ mode: 'partial',
367
+ filesCreated,
368
+ filesUpdated,
369
+ filesSkipped,
370
+ backupId,
371
+ errors,
372
+ warnings
373
+ };
374
+ }
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Full Adoption Strategy
380
+ * Upgrades existing complete .kiro/ to current version
381
+ */
382
+ class FullAdoption extends AdoptionStrategy {
383
+ /**
384
+ * Executes full adoption (upgrade)
385
+ *
386
+ * @param {string} projectPath - Absolute path to project root
387
+ * @param {AdoptionMode} mode - Should be 'full'
388
+ * @param {AdoptionOptions} options - Adoption options
389
+ * @returns {Promise<AdoptionResult>}
390
+ */
391
+ async execute(projectPath, mode, options = {}) {
392
+ const { kseVersion = '1.0.0', dryRun = false, backupId = null } = options;
393
+
394
+ const filesCreated = [];
395
+ const filesUpdated = [];
396
+ const filesSkipped = [];
397
+ const errors = [];
398
+ const warnings = [];
399
+
400
+ try {
401
+ const kiroPath = this.getKiroPath(projectPath);
402
+
403
+ // Check if .kiro/ exists
404
+ const kiroExists = await pathExists(kiroPath);
405
+ if (!kiroExists) {
406
+ throw new Error('.kiro/ directory does not exist - use fresh adoption');
407
+ }
408
+
409
+ // Read existing version
410
+ const existingVersion = await this.versionManager.readVersion(projectPath);
411
+ if (!existingVersion) {
412
+ throw new Error('version.json not found - use partial adoption');
413
+ }
414
+
415
+ const currentVersion = existingVersion['kse-version'];
416
+
417
+ // Check if upgrade is needed
418
+ if (!this.versionManager.needsUpgrade(currentVersion, kseVersion)) {
419
+ warnings.push(`Already at version ${kseVersion} - no upgrade needed`);
420
+ return {
421
+ success: true,
422
+ mode: 'full',
423
+ filesCreated: [],
424
+ filesUpdated: [],
425
+ filesSkipped: [],
426
+ backupId,
427
+ errors: [],
428
+ warnings
429
+ };
430
+ }
431
+
432
+ if (dryRun) {
433
+ return {
434
+ success: true,
435
+ mode: 'full',
436
+ filesCreated: [],
437
+ filesUpdated: [`(dry-run) Would upgrade from ${currentVersion} to ${kseVersion}`],
438
+ filesSkipped: [],
439
+ backupId,
440
+ errors: [],
441
+ warnings
442
+ };
443
+ }
444
+
445
+ // Copy template files (overwrite template files, preserve user content)
446
+ // User content is in specs/ and any custom files
447
+ const copyResult = await this.copyTemplateFiles(projectPath, {
448
+ overwrite: true,
449
+ skip: [] // Don't skip anything - we want to update templates
450
+ });
451
+ filesCreated.push(...copyResult.created);
452
+ filesUpdated.push(...copyResult.updated);
453
+ filesSkipped.push(...copyResult.skipped);
454
+
455
+ // Update version.json with upgrade history
456
+ const updatedVersion = this.versionManager.addUpgradeHistory(
457
+ existingVersion,
458
+ currentVersion,
459
+ kseVersion,
460
+ true
461
+ );
462
+ await this.versionManager.writeVersion(projectPath, updatedVersion);
463
+ filesUpdated.push('version.json');
464
+
465
+ return {
466
+ success: true,
467
+ mode: 'full',
468
+ filesCreated,
469
+ filesUpdated,
470
+ filesSkipped,
471
+ backupId,
472
+ errors,
473
+ warnings
474
+ };
475
+ } catch (error) {
476
+ errors.push(error.message);
477
+ return {
478
+ success: false,
479
+ mode: 'full',
480
+ filesCreated,
481
+ filesUpdated,
482
+ filesSkipped,
483
+ backupId,
484
+ errors,
485
+ warnings
486
+ };
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Factory function to get the appropriate strategy
493
+ *
494
+ * @param {AdoptionMode} mode - Adoption mode ('fresh', 'partial', 'full')
495
+ * @returns {AdoptionStrategy}
496
+ */
497
+ function getAdoptionStrategy(mode) {
498
+ switch (mode) {
499
+ case 'fresh':
500
+ return new FreshAdoption();
501
+ case 'partial':
502
+ return new PartialAdoption();
503
+ case 'full':
504
+ return new FullAdoption();
505
+ default:
506
+ throw new Error(`Unknown adoption mode: ${mode}`);
507
+ }
508
+ }
509
+
510
+ module.exports = {
511
+ AdoptionStrategy,
512
+ FreshAdoption,
513
+ PartialAdoption,
514
+ FullAdoption,
515
+ getAdoptionStrategy
516
+ };