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 +18 -12
- package/lib/utils/fs-utils.js +274 -0
- package/lib/version/version-manager.js +287 -0
- package/package.json +3 -2
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
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
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
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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.
|
|
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",
|