kiro-spec-engine 1.0.0 → 1.1.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 CHANGED
@@ -7,21 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-01-23
11
+
10
12
  ### Added
11
- - Initial project structure
12
- - CLI implementation with Node.js
13
- - Ultrawork quality enhancement tool (Python)
14
- - Multi-language support (English and Chinese)
15
- - Project initialization command (`kse init`)
16
- - Spec creation command (`kse create-spec`)
17
- - Status checking command (`kse status`)
18
- - Document enhancement command (`kse enhance`)
13
+ - Version management system for project adoption and upgrades
14
+ - VersionManager class for tracking project versions
15
+ - Compatibility matrix for version compatibility checking
16
+ - Upgrade path calculation for incremental upgrades
17
+ - Safe file system utilities with atomic operations
18
+ - Path validation to prevent path traversal attacks
19
+ - Project structure for future adoption/upgrade features
20
+
21
+ ### Infrastructure
22
+ - Added semver dependency for version comparison
23
+ - Created lib/version/ directory for version management
24
+ - Created lib/utils/ directory for shared utilities
25
+ - Prepared foundation for kse adopt and kse upgrade commands
19
26
 
20
27
  ### Documentation
21
- - README.md (English)
22
- - README.zh.md (Chinese)
23
- - CONTRIBUTING.md
24
- - LICENSE (MIT)
28
+ - Created spec 02-00-project-adoption-and-upgrade
29
+ - Comprehensive design for project adoption system
30
+ - Detailed requirements for smooth upgrade experience
25
31
 
26
32
  ## [1.0.0] - 2026-01-23
27
33
 
@@ -0,0 +1,274 @@
1
+ /**
2
+ * File System Utilities
3
+ *
4
+ * Provides safe, atomic file operations for the adoption/upgrade system.
5
+ * Implements path validation, atomic writes, and error handling.
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ /**
13
+ * Validates that a file path is within the project directory
14
+ * Prevents path traversal attacks
15
+ *
16
+ * @param {string} projectPath - Absolute path to project root
17
+ * @param {string} filePath - Relative or absolute file path to validate
18
+ * @returns {string} - Validated absolute path
19
+ * @throws {Error} - If path traversal is detected
20
+ */
21
+ function validatePath(projectPath, filePath) {
22
+ const resolvedProject = path.resolve(projectPath);
23
+ const resolvedFile = path.resolve(projectPath, filePath);
24
+
25
+ if (!resolvedFile.startsWith(resolvedProject)) {
26
+ throw new Error(`Path traversal detected: ${filePath} is outside project directory`);
27
+ }
28
+
29
+ return resolvedFile;
30
+ }
31
+
32
+ /**
33
+ * Atomically writes content to a file
34
+ * Uses temp file + rename for atomicity
35
+ *
36
+ * @param {string} filePath - Absolute path to target file
37
+ * @param {string} content - Content to write
38
+ * @returns {Promise<void>}
39
+ */
40
+ async function atomicWrite(filePath, content) {
41
+ const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substr(2, 9)}`;
42
+
43
+ try {
44
+ // Write to temp file
45
+ await fs.writeFile(tempPath, content, 'utf8');
46
+
47
+ // Atomic rename (on most systems)
48
+ await fs.rename(tempPath, filePath);
49
+ } catch (error) {
50
+ // Clean up temp file if it exists
51
+ try {
52
+ await fs.unlink(tempPath);
53
+ } catch (cleanupError) {
54
+ // Ignore cleanup errors
55
+ }
56
+
57
+ throw new Error(`Failed to write file atomically: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Safely copies a file with error handling
63
+ * Creates parent directories if needed
64
+ *
65
+ * @param {string} sourcePath - Absolute path to source file
66
+ * @param {string} destPath - Absolute path to destination file
67
+ * @param {Object} options - Copy options
68
+ * @param {boolean} options.overwrite - Whether to overwrite existing file (default: false)
69
+ * @returns {Promise<void>}
70
+ */
71
+ async function safeCopy(sourcePath, destPath, options = {}) {
72
+ const { overwrite = false } = options;
73
+
74
+ try {
75
+ // Check if source exists
76
+ const sourceExists = await fs.pathExists(sourcePath);
77
+ if (!sourceExists) {
78
+ throw new Error(`Source file does not exist: ${sourcePath}`);
79
+ }
80
+
81
+ // Check if destination exists
82
+ const destExists = await fs.pathExists(destPath);
83
+ if (destExists && !overwrite) {
84
+ throw new Error(`Destination file already exists: ${destPath}`);
85
+ }
86
+
87
+ // Ensure parent directory exists
88
+ const parentDir = path.dirname(destPath);
89
+ await fs.ensureDir(parentDir);
90
+
91
+ // Copy file
92
+ await fs.copy(sourcePath, destPath, { overwrite });
93
+ } catch (error) {
94
+ throw new Error(`Failed to copy file: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Recursively creates a directory
100
+ * Safe to call even if directory already exists
101
+ *
102
+ * @param {string} dirPath - Absolute path to directory
103
+ * @returns {Promise<void>}
104
+ */
105
+ async function ensureDirectory(dirPath) {
106
+ try {
107
+ await fs.ensureDir(dirPath);
108
+ } catch (error) {
109
+ throw new Error(`Failed to create directory: ${error.message}`);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Recursively copies a directory
115
+ *
116
+ * @param {string} sourceDir - Absolute path to source directory
117
+ * @param {string} destDir - Absolute path to destination directory
118
+ * @param {Object} options - Copy options
119
+ * @param {boolean} options.overwrite - Whether to overwrite existing files (default: false)
120
+ * @param {Function} options.filter - Filter function (path) => boolean
121
+ * @returns {Promise<void>}
122
+ */
123
+ async function copyDirectory(sourceDir, destDir, options = {}) {
124
+ const { overwrite = false, filter = null } = options;
125
+
126
+ try {
127
+ await fs.copy(sourceDir, destDir, {
128
+ overwrite,
129
+ filter: filter || (() => true)
130
+ });
131
+ } catch (error) {
132
+ throw new Error(`Failed to copy directory: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Checks if a path exists
138
+ *
139
+ * @param {string} filePath - Path to check
140
+ * @returns {Promise<boolean>}
141
+ */
142
+ async function pathExists(filePath) {
143
+ return fs.pathExists(filePath);
144
+ }
145
+
146
+ /**
147
+ * Reads a JSON file safely
148
+ *
149
+ * @param {string} filePath - Absolute path to JSON file
150
+ * @returns {Promise<Object>} - Parsed JSON object
151
+ * @throws {Error} - If file doesn't exist or JSON is invalid
152
+ */
153
+ async function readJSON(filePath) {
154
+ try {
155
+ return await fs.readJSON(filePath);
156
+ } catch (error) {
157
+ throw new Error(`Failed to read JSON file: ${error.message}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Writes a JSON file atomically
163
+ *
164
+ * @param {string} filePath - Absolute path to JSON file
165
+ * @param {Object} data - Data to write
166
+ * @param {Object} options - Write options
167
+ * @param {number} options.spaces - Number of spaces for indentation (default: 2)
168
+ * @returns {Promise<void>}
169
+ */
170
+ async function writeJSON(filePath, data, options = {}) {
171
+ const { spaces = 2 } = options;
172
+ const content = JSON.stringify(data, null, spaces);
173
+ await atomicWrite(filePath, content);
174
+ }
175
+
176
+ /**
177
+ * Removes a file or directory
178
+ *
179
+ * @param {string} targetPath - Path to remove
180
+ * @returns {Promise<void>}
181
+ */
182
+ async function remove(targetPath) {
183
+ try {
184
+ await fs.remove(targetPath);
185
+ } catch (error) {
186
+ throw new Error(`Failed to remove path: ${error.message}`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Gets file stats
192
+ *
193
+ * @param {string} filePath - Path to file
194
+ * @returns {Promise<fs.Stats>}
195
+ */
196
+ async function getStats(filePath) {
197
+ try {
198
+ return await fs.stat(filePath);
199
+ } catch (error) {
200
+ throw new Error(`Failed to get file stats: ${error.message}`);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Lists files in a directory
206
+ *
207
+ * @param {string} dirPath - Path to directory
208
+ * @returns {Promise<string[]>} - Array of file names
209
+ */
210
+ async function listFiles(dirPath) {
211
+ try {
212
+ return await fs.readdir(dirPath);
213
+ } catch (error) {
214
+ throw new Error(`Failed to list directory: ${error.message}`);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Recursively lists all files in a directory
220
+ *
221
+ * @param {string} dirPath - Path to directory
222
+ * @param {string[]} fileList - Accumulator for recursive calls
223
+ * @returns {Promise<string[]>} - Array of absolute file paths
224
+ */
225
+ async function listFilesRecursive(dirPath, fileList = []) {
226
+ const files = await fs.readdir(dirPath);
227
+
228
+ for (const file of files) {
229
+ const filePath = path.join(dirPath, file);
230
+ const stat = await fs.stat(filePath);
231
+
232
+ if (stat.isDirectory()) {
233
+ await listFilesRecursive(filePath, fileList);
234
+ } else {
235
+ fileList.push(filePath);
236
+ }
237
+ }
238
+
239
+ return fileList;
240
+ }
241
+
242
+ /**
243
+ * Calculates total size of a directory
244
+ *
245
+ * @param {string} dirPath - Path to directory
246
+ * @returns {Promise<number>} - Total size in bytes
247
+ */
248
+ async function getDirectorySize(dirPath) {
249
+ const files = await listFilesRecursive(dirPath);
250
+ let totalSize = 0;
251
+
252
+ for (const file of files) {
253
+ const stats = await fs.stat(file);
254
+ totalSize += stats.size;
255
+ }
256
+
257
+ return totalSize;
258
+ }
259
+
260
+ module.exports = {
261
+ validatePath,
262
+ atomicWrite,
263
+ safeCopy,
264
+ ensureDirectory,
265
+ copyDirectory,
266
+ pathExists,
267
+ readJSON,
268
+ writeJSON,
269
+ remove,
270
+ getStats,
271
+ listFiles,
272
+ listFilesRecursive,
273
+ getDirectorySize
274
+ };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Version Manager
3
+ *
4
+ * Manages version tracking, compatibility checking, and upgrade path calculation
5
+ * for the kiro-spec-engine project adoption and upgrade system.
6
+ */
7
+
8
+ const path = require('path');
9
+ const semver = require('semver');
10
+ const { readJSON, writeJSON, pathExists } = require('../utils/fs-utils');
11
+
12
+ /**
13
+ * Compatibility matrix defining which versions can work together
14
+ * Format: { version: { compatible: [versions], breaking: boolean } }
15
+ */
16
+ const COMPATIBILITY_MATRIX = {
17
+ '1.0.0': { compatible: ['1.0.0', '1.1.0', '1.2.0'], breaking: false },
18
+ '1.1.0': { compatible: ['1.0.0', '1.1.0', '1.2.0'], breaking: false },
19
+ '1.2.0': { compatible: ['1.0.0', '1.1.0', '1.2.0'], breaking: false },
20
+ '2.0.0': { compatible: ['2.0.0'], breaking: true, migration: 'required' }
21
+ };
22
+
23
+ class VersionManager {
24
+ constructor() {
25
+ this.versionFileName = 'version.json';
26
+ }
27
+
28
+ /**
29
+ * Gets the path to version.json in a project
30
+ *
31
+ * @param {string} projectPath - Absolute path to project root
32
+ * @returns {string} - Absolute path to version.json
33
+ */
34
+ getVersionFilePath(projectPath) {
35
+ return path.join(projectPath, '.kiro', this.versionFileName);
36
+ }
37
+
38
+ /**
39
+ * Reads version information from project
40
+ *
41
+ * @param {string} projectPath - Absolute path to project root
42
+ * @returns {Promise<VersionInfo|null>} - Version info or null if not found
43
+ */
44
+ async readVersion(projectPath) {
45
+ const versionPath = this.getVersionFilePath(projectPath);
46
+
47
+ try {
48
+ const exists = await pathExists(versionPath);
49
+ if (!exists) {
50
+ return null;
51
+ }
52
+
53
+ const versionInfo = await readJSON(versionPath);
54
+
55
+ // Validate structure
56
+ if (!this.isValidVersionInfo(versionInfo)) {
57
+ throw new Error('Invalid version.json structure');
58
+ }
59
+
60
+ return versionInfo;
61
+ } catch (error) {
62
+ throw new Error(`Failed to read version file: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Writes version information to project
68
+ *
69
+ * @param {string} projectPath - Absolute path to project root
70
+ * @param {VersionInfo} versionInfo - Version information to write
71
+ * @returns {Promise<void>}
72
+ */
73
+ async writeVersion(projectPath, versionInfo) {
74
+ const versionPath = this.getVersionFilePath(projectPath);
75
+
76
+ try {
77
+ // Validate structure before writing
78
+ if (!this.isValidVersionInfo(versionInfo)) {
79
+ throw new Error('Invalid version info structure');
80
+ }
81
+
82
+ await writeJSON(versionPath, versionInfo, { spaces: 2 });
83
+ } catch (error) {
84
+ throw new Error(`Failed to write version file: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Creates initial version info for a new project
90
+ *
91
+ * @param {string} kseVersion - Current kse version
92
+ * @param {string} templateVersion - Template version (default: same as kse)
93
+ * @returns {VersionInfo}
94
+ */
95
+ createVersionInfo(kseVersion, templateVersion = null) {
96
+ const now = new Date().toISOString();
97
+
98
+ return {
99
+ 'kse-version': kseVersion,
100
+ 'template-version': templateVersion || kseVersion,
101
+ 'created': now,
102
+ 'last-upgraded': now,
103
+ 'upgrade-history': []
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Validates version info structure
109
+ *
110
+ * @param {Object} versionInfo - Version info to validate
111
+ * @returns {boolean}
112
+ */
113
+ isValidVersionInfo(versionInfo) {
114
+ if (!versionInfo || typeof versionInfo !== 'object') {
115
+ return false;
116
+ }
117
+
118
+ const requiredFields = [
119
+ 'kse-version',
120
+ 'template-version',
121
+ 'created',
122
+ 'last-upgraded',
123
+ 'upgrade-history'
124
+ ];
125
+
126
+ for (const field of requiredFields) {
127
+ if (!(field in versionInfo)) {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // Validate upgrade-history is an array
133
+ if (!Array.isArray(versionInfo['upgrade-history'])) {
134
+ return false;
135
+ }
136
+
137
+ return true;
138
+ }
139
+
140
+ /**
141
+ * Checks if upgrade is needed
142
+ *
143
+ * @param {string} projectVersion - Current project version
144
+ * @param {string} kseVersion - Installed kse version
145
+ * @returns {boolean}
146
+ */
147
+ needsUpgrade(projectVersion, kseVersion) {
148
+ if (!projectVersion || !kseVersion) {
149
+ return false;
150
+ }
151
+
152
+ try {
153
+ // Use semver for comparison
154
+ return semver.lt(projectVersion, kseVersion);
155
+ } catch (error) {
156
+ // If semver comparison fails, do string comparison
157
+ return projectVersion !== kseVersion;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Checks compatibility between versions
163
+ *
164
+ * @param {string} fromVersion - Source version
165
+ * @param {string} toVersion - Target version
166
+ * @returns {CompatibilityResult}
167
+ */
168
+ checkCompatibility(fromVersion, toVersion) {
169
+ // If versions are the same, always compatible
170
+ if (fromVersion === toVersion) {
171
+ return {
172
+ compatible: true,
173
+ breaking: false,
174
+ migration: 'none'
175
+ };
176
+ }
177
+
178
+ // Check compatibility matrix
179
+ const fromInfo = COMPATIBILITY_MATRIX[fromVersion];
180
+
181
+ if (!fromInfo) {
182
+ // Unknown version - assume incompatible
183
+ return {
184
+ compatible: false,
185
+ breaking: true,
186
+ migration: 'unknown',
187
+ message: `Unknown source version: ${fromVersion}`
188
+ };
189
+ }
190
+
191
+ const isCompatible = fromInfo.compatible.includes(toVersion);
192
+
193
+ return {
194
+ compatible: isCompatible,
195
+ breaking: !isCompatible || fromInfo.breaking,
196
+ migration: !isCompatible ? 'required' : 'none'
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Calculates upgrade path for version gap
202
+ * Returns array of intermediate versions to upgrade through
203
+ *
204
+ * @param {string} fromVersion - Current version
205
+ * @param {string} toVersion - Target version
206
+ * @returns {string[]} - Array of versions in upgrade order (including from and to)
207
+ */
208
+ calculateUpgradePath(fromVersion, toVersion) {
209
+ // If same version, no upgrade needed
210
+ if (fromVersion === toVersion) {
211
+ return [fromVersion];
212
+ }
213
+
214
+ // Get all versions from compatibility matrix
215
+ const allVersions = Object.keys(COMPATIBILITY_MATRIX).sort((a, b) => {
216
+ try {
217
+ return semver.compare(a, b);
218
+ } catch (error) {
219
+ return a.localeCompare(b);
220
+ }
221
+ });
222
+
223
+ // Find indices
224
+ const fromIndex = allVersions.indexOf(fromVersion);
225
+ const toIndex = allVersions.indexOf(toVersion);
226
+
227
+ if (fromIndex === -1) {
228
+ throw new Error(`Unknown source version: ${fromVersion}`);
229
+ }
230
+
231
+ if (toIndex === -1) {
232
+ throw new Error(`Unknown target version: ${toVersion}`);
233
+ }
234
+
235
+ if (fromIndex > toIndex) {
236
+ throw new Error('Cannot downgrade versions');
237
+ }
238
+
239
+ // Return all versions from source to target (inclusive)
240
+ return allVersions.slice(fromIndex, toIndex + 1);
241
+ }
242
+
243
+ /**
244
+ * Adds an upgrade entry to version history
245
+ *
246
+ * @param {VersionInfo} versionInfo - Current version info
247
+ * @param {string} fromVersion - Version upgraded from
248
+ * @param {string} toVersion - Version upgraded to
249
+ * @param {boolean} success - Whether upgrade succeeded
250
+ * @param {string} error - Error message if failed
251
+ * @returns {VersionInfo} - Updated version info
252
+ */
253
+ addUpgradeHistory(versionInfo, fromVersion, toVersion, success, error = null) {
254
+ const entry = {
255
+ from: fromVersion,
256
+ to: toVersion,
257
+ date: new Date().toISOString(),
258
+ success
259
+ };
260
+
261
+ if (error) {
262
+ entry.error = error;
263
+ }
264
+
265
+ versionInfo['upgrade-history'].push(entry);
266
+
267
+ // Update version and last-upgraded if successful
268
+ if (success) {
269
+ versionInfo['kse-version'] = toVersion;
270
+ versionInfo['template-version'] = toVersion;
271
+ versionInfo['last-upgraded'] = entry.date;
272
+ }
273
+
274
+ return versionInfo;
275
+ }
276
+
277
+ /**
278
+ * Gets the compatibility matrix
279
+ *
280
+ * @returns {Object} - Compatibility matrix
281
+ */
282
+ getCompatibilityMatrix() {
283
+ return { ...COMPATIBILITY_MATRIX };
284
+ }
285
+ }
286
+
287
+ module.exports = VersionManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Kiro Spec Engine - A spec-driven development engine with steering rules and quality enhancement powered by Ultrawork spirit",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -69,7 +69,8 @@
69
69
  "commander": "^9.0.0",
70
70
  "fs-extra": "^10.0.0",
71
71
  "inquirer": "^8.2.0",
72
- "path": "^0.12.7"
72
+ "path": "^0.12.7",
73
+ "semver": "^7.5.4"
73
74
  },
74
75
  "devDependencies": {
75
76
  "fast-check": "^4.5.3",