aios-core 3.1.0 → 3.3.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/.aios-core/core-config.yaml +44 -0
- package/.aios-core/infrastructure/scripts/ide-sync/agent-parser.js +251 -0
- package/.aios-core/infrastructure/scripts/ide-sync/index.js +480 -0
- package/.aios-core/infrastructure/scripts/ide-sync/redirect-generator.js +200 -0
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/antigravity.js +105 -0
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/claude-code.js +84 -0
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/cursor.js +93 -0
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/trae.js +125 -0
- package/.aios-core/infrastructure/scripts/ide-sync/transformers/windsurf.js +106 -0
- package/.aios-core/infrastructure/scripts/ide-sync/validator.js +273 -0
- package/.aios-core/install-manifest.yaml +2269 -349
- package/bin/aios-init.js +126 -0
- package/package.json +13 -1
- package/scripts/generate-install-manifest.js +337 -0
- package/scripts/validate-manifest.js +265 -0
- package/src/installer/brownfield-upgrader.js +438 -0
- package/src/installer/file-hasher.js +137 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate Install Manifest
|
|
4
|
+
* Ensures install-manifest.yaml is up-to-date with actual files
|
|
5
|
+
*
|
|
6
|
+
* @script scripts/validate-manifest.js
|
|
7
|
+
* @story 6.18 - Dynamic Manifest & Brownfield Upgrade System
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/validate-manifest.js
|
|
11
|
+
* npm run validate:manifest
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - Manifest is valid and up-to-date
|
|
15
|
+
* 1 - Manifest is outdated or has issues
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs-extra');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const yaml = require('js-yaml');
|
|
21
|
+
const { hashFile } = require('../src/installer/file-hasher');
|
|
22
|
+
const {
|
|
23
|
+
scanDirectory,
|
|
24
|
+
FOLDERS_TO_COPY,
|
|
25
|
+
ROOT_FILES_TO_COPY,
|
|
26
|
+
getFileType,
|
|
27
|
+
} = require('./generate-install-manifest');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validation result object
|
|
31
|
+
* @typedef {Object} ValidationResult
|
|
32
|
+
* @property {boolean} valid - Whether manifest is valid
|
|
33
|
+
* @property {string[]} newFiles - Files in filesystem but not in manifest
|
|
34
|
+
* @property {string[]} removedFiles - Files in manifest but not in filesystem
|
|
35
|
+
* @property {string[]} modifiedFiles - Files with different hashes
|
|
36
|
+
* @property {string[]} errors - Error messages
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load and parse the install manifest
|
|
41
|
+
* @returns {Object|null} - Parsed manifest or null if not found
|
|
42
|
+
*/
|
|
43
|
+
function loadManifest() {
|
|
44
|
+
const manifestPath = path.join(__dirname, '..', '.aios-core', 'install-manifest.yaml');
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(manifestPath)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
51
|
+
return yaml.load(content);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get current files from filesystem
|
|
56
|
+
* @returns {Map<string, Object>} - Map of relativePath -> file metadata
|
|
57
|
+
*/
|
|
58
|
+
function getCurrentFiles() {
|
|
59
|
+
const aiosCoreDir = path.join(__dirname, '..', '.aios-core');
|
|
60
|
+
const filesMap = new Map();
|
|
61
|
+
|
|
62
|
+
// Scan folders
|
|
63
|
+
const allFiles = [];
|
|
64
|
+
for (const folder of FOLDERS_TO_COPY) {
|
|
65
|
+
const folderPath = path.join(aiosCoreDir, folder);
|
|
66
|
+
if (fs.existsSync(folderPath)) {
|
|
67
|
+
scanDirectory(folderPath, aiosCoreDir, allFiles);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add root files
|
|
72
|
+
for (const file of ROOT_FILES_TO_COPY) {
|
|
73
|
+
const filePath = path.join(aiosCoreDir, file);
|
|
74
|
+
if (fs.existsSync(filePath)) {
|
|
75
|
+
allFiles.push(filePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build map
|
|
80
|
+
for (const fullPath of allFiles) {
|
|
81
|
+
const relativePath = path.relative(aiosCoreDir, fullPath).replace(/\\/g, '/');
|
|
82
|
+
try {
|
|
83
|
+
const hash = hashFile(fullPath);
|
|
84
|
+
filesMap.set(relativePath, {
|
|
85
|
+
path: relativePath,
|
|
86
|
+
hash: `sha256:${hash}`,
|
|
87
|
+
type: getFileType(relativePath),
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(`Warning: Could not hash ${relativePath}: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return filesMap;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate manifest against current filesystem
|
|
99
|
+
* @returns {ValidationResult} - Validation results
|
|
100
|
+
*/
|
|
101
|
+
function validateManifest() {
|
|
102
|
+
const result = {
|
|
103
|
+
valid: true,
|
|
104
|
+
newFiles: [],
|
|
105
|
+
removedFiles: [],
|
|
106
|
+
modifiedFiles: [],
|
|
107
|
+
errors: [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Load manifest
|
|
111
|
+
const manifest = loadManifest();
|
|
112
|
+
if (!manifest) {
|
|
113
|
+
result.valid = false;
|
|
114
|
+
result.errors.push('install-manifest.yaml not found');
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!manifest.files || !Array.isArray(manifest.files)) {
|
|
119
|
+
result.valid = false;
|
|
120
|
+
result.errors.push('Manifest has no files array');
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get current files
|
|
125
|
+
const currentFiles = getCurrentFiles();
|
|
126
|
+
|
|
127
|
+
// Build set of manifest paths
|
|
128
|
+
const manifestPaths = new Set();
|
|
129
|
+
const manifestMap = new Map();
|
|
130
|
+
|
|
131
|
+
for (const entry of manifest.files) {
|
|
132
|
+
const normalizedPath = entry.path.replace(/\\/g, '/');
|
|
133
|
+
manifestPaths.add(normalizedPath);
|
|
134
|
+
manifestMap.set(normalizedPath, entry);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for new files (in filesystem but not in manifest)
|
|
138
|
+
for (const [filePath, _fileData] of currentFiles) {
|
|
139
|
+
if (!manifestPaths.has(filePath)) {
|
|
140
|
+
result.newFiles.push(filePath);
|
|
141
|
+
result.valid = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for removed files and hash mismatches
|
|
146
|
+
for (const [manifestPath, manifestEntry] of manifestMap) {
|
|
147
|
+
const currentFile = currentFiles.get(manifestPath);
|
|
148
|
+
|
|
149
|
+
if (!currentFile) {
|
|
150
|
+
// File in manifest but not in filesystem
|
|
151
|
+
result.removedFiles.push(manifestPath);
|
|
152
|
+
result.valid = false;
|
|
153
|
+
} else if (currentFile.hash !== manifestEntry.hash) {
|
|
154
|
+
// Hash mismatch
|
|
155
|
+
result.modifiedFiles.push({
|
|
156
|
+
path: manifestPath,
|
|
157
|
+
manifestHash: manifestEntry.hash,
|
|
158
|
+
currentHash: currentFile.hash,
|
|
159
|
+
});
|
|
160
|
+
result.valid = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Print validation report
|
|
169
|
+
* @param {ValidationResult} result - Validation results
|
|
170
|
+
*/
|
|
171
|
+
function printReport(result) {
|
|
172
|
+
console.log('='.repeat(60));
|
|
173
|
+
console.log('AIOS-Core Manifest Validation Report');
|
|
174
|
+
console.log('='.repeat(60));
|
|
175
|
+
console.log('');
|
|
176
|
+
|
|
177
|
+
if (result.errors.length > 0) {
|
|
178
|
+
console.log('❌ ERRORS:');
|
|
179
|
+
for (const error of result.errors) {
|
|
180
|
+
console.log(` - ${error}`);
|
|
181
|
+
}
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (result.newFiles.length > 0) {
|
|
186
|
+
console.log(`📁 NEW FILES (${result.newFiles.length}) - not in manifest:`);
|
|
187
|
+
for (const file of result.newFiles.slice(0, 20)) {
|
|
188
|
+
console.log(` + ${file}`);
|
|
189
|
+
}
|
|
190
|
+
if (result.newFiles.length > 20) {
|
|
191
|
+
console.log(` ... and ${result.newFiles.length - 20} more`);
|
|
192
|
+
}
|
|
193
|
+
console.log('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.removedFiles.length > 0) {
|
|
197
|
+
console.log(`🗑️ REMOVED FILES (${result.removedFiles.length}) - in manifest but missing:`);
|
|
198
|
+
for (const file of result.removedFiles.slice(0, 20)) {
|
|
199
|
+
console.log(` - ${file}`);
|
|
200
|
+
}
|
|
201
|
+
if (result.removedFiles.length > 20) {
|
|
202
|
+
console.log(` ... and ${result.removedFiles.length - 20} more`);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (result.modifiedFiles.length > 0) {
|
|
208
|
+
console.log(`📝 MODIFIED FILES (${result.modifiedFiles.length}) - hash mismatch:`);
|
|
209
|
+
for (const file of result.modifiedFiles.slice(0, 20)) {
|
|
210
|
+
console.log(` ~ ${file.path}`);
|
|
211
|
+
if (process.env.VERBOSE) {
|
|
212
|
+
console.log(` manifest: ${file.manifestHash}`);
|
|
213
|
+
console.log(` current: ${file.currentHash}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (result.modifiedFiles.length > 20) {
|
|
217
|
+
console.log(` ... and ${result.modifiedFiles.length - 20} more`);
|
|
218
|
+
}
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Summary
|
|
223
|
+
console.log('-'.repeat(60));
|
|
224
|
+
if (result.valid) {
|
|
225
|
+
console.log('✅ Manifest is VALID and up-to-date');
|
|
226
|
+
} else {
|
|
227
|
+
console.log('❌ Manifest is OUTDATED');
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log('To fix, run: npm run generate:manifest');
|
|
230
|
+
}
|
|
231
|
+
console.log('-'.repeat(60));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Main execution
|
|
236
|
+
*/
|
|
237
|
+
async function main() {
|
|
238
|
+
try {
|
|
239
|
+
const result = validateManifest();
|
|
240
|
+
printReport(result);
|
|
241
|
+
|
|
242
|
+
if (!result.valid) {
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
process.exit(0);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('\n❌ Error validating manifest:', error.message);
|
|
248
|
+
if (process.env.DEBUG) {
|
|
249
|
+
console.error(error.stack);
|
|
250
|
+
}
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Run if called directly
|
|
256
|
+
if (require.main === module) {
|
|
257
|
+
main();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
validateManifest,
|
|
262
|
+
loadManifest,
|
|
263
|
+
getCurrentFiles,
|
|
264
|
+
printReport,
|
|
265
|
+
};
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brownfield Upgrader
|
|
3
|
+
* Handles incremental upgrades for existing AIOS-Core installations
|
|
4
|
+
*
|
|
5
|
+
* @module src/installer/brownfield-upgrader
|
|
6
|
+
* @story 6.18 - Dynamic Manifest & Brownfield Upgrade System
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
const semver = require('semver');
|
|
13
|
+
const { hashFile, hashesMatch } = require('./file-hasher');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Upgrade report structure
|
|
17
|
+
* @typedef {Object} UpgradeReport
|
|
18
|
+
* @property {string} sourceVersion - Version being upgraded to
|
|
19
|
+
* @property {string} installedVersion - Currently installed version
|
|
20
|
+
* @property {Object[]} newFiles - Files to be added
|
|
21
|
+
* @property {Object[]} modifiedFiles - Files that changed (framework side)
|
|
22
|
+
* @property {Object[]} userModifiedFiles - Files modified by user (won't overwrite)
|
|
23
|
+
* @property {Object[]} deletedFiles - Files removed from framework
|
|
24
|
+
* @property {boolean} upgradeAvailable - Whether an upgrade is available
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load manifest from a given base path
|
|
29
|
+
* @param {string} basePath - Base path to look for manifest
|
|
30
|
+
* @param {string} manifestName - Name of manifest file
|
|
31
|
+
* @returns {Object|null} - Parsed manifest or null
|
|
32
|
+
*/
|
|
33
|
+
function loadManifest(basePath, manifestName = 'install-manifest.yaml') {
|
|
34
|
+
const manifestPath = path.join(basePath, manifestName);
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(manifestPath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
42
|
+
return yaml.load(content);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Error loading manifest from ${manifestPath}:`, error.message);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load installed manifest from target project
|
|
51
|
+
* @param {string} targetDir - Target project directory
|
|
52
|
+
* @returns {Object|null} - Installed manifest or null if not found
|
|
53
|
+
*/
|
|
54
|
+
function loadInstalledManifest(targetDir) {
|
|
55
|
+
return loadManifest(
|
|
56
|
+
path.join(targetDir, '.aios-core'),
|
|
57
|
+
'.installed-manifest.yaml',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load source manifest from package
|
|
63
|
+
* @param {string} sourceDir - Source package directory (.aios-core from npm)
|
|
64
|
+
* @returns {Object|null} - Source manifest
|
|
65
|
+
*/
|
|
66
|
+
function loadSourceManifest(sourceDir) {
|
|
67
|
+
return loadManifest(sourceDir, 'install-manifest.yaml');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build file map from manifest for quick lookup
|
|
72
|
+
* @param {Object} manifest - Manifest object
|
|
73
|
+
* @returns {Map<string, Object>} - Map of path -> entry
|
|
74
|
+
*/
|
|
75
|
+
function buildFileMap(manifest) {
|
|
76
|
+
const map = new Map();
|
|
77
|
+
if (manifest && manifest.files) {
|
|
78
|
+
for (const entry of manifest.files) {
|
|
79
|
+
const normalizedPath = entry.path.replace(/\\/g, '/');
|
|
80
|
+
map.set(normalizedPath, entry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return map;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a file has been modified by the user
|
|
88
|
+
* Compares current file hash against installed manifest hash
|
|
89
|
+
*
|
|
90
|
+
* @param {string} filePath - Absolute path to file
|
|
91
|
+
* @param {string} expectedHash - Hash from installed manifest (with sha256: prefix)
|
|
92
|
+
* @returns {boolean} - True if file was modified by user
|
|
93
|
+
*/
|
|
94
|
+
function isUserModified(filePath, expectedHash) {
|
|
95
|
+
if (!fs.existsSync(filePath)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const currentHash = `sha256:${hashFile(filePath)}`;
|
|
101
|
+
return !hashesMatch(currentHash, expectedHash);
|
|
102
|
+
} catch (_error) {
|
|
103
|
+
// If we can't hash, assume it's modified
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate upgrade report comparing source and installed manifests
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} sourceManifest - Manifest from source (npm package)
|
|
112
|
+
* @param {Object} installedManifest - Manifest from target installation
|
|
113
|
+
* @param {string} targetDir - Target project directory
|
|
114
|
+
* @returns {UpgradeReport} - Detailed upgrade report
|
|
115
|
+
*/
|
|
116
|
+
function generateUpgradeReport(sourceManifest, installedManifest, targetDir) {
|
|
117
|
+
const report = {
|
|
118
|
+
sourceVersion: sourceManifest?.version || 'unknown',
|
|
119
|
+
installedVersion: installedManifest?.installed_version || installedManifest?.version || 'unknown',
|
|
120
|
+
newFiles: [],
|
|
121
|
+
modifiedFiles: [],
|
|
122
|
+
userModifiedFiles: [],
|
|
123
|
+
deletedFiles: [],
|
|
124
|
+
unchangedFiles: 0,
|
|
125
|
+
upgradeAvailable: false,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Check if upgrade is available via semver
|
|
129
|
+
if (sourceManifest?.version && installedManifest?.installed_version) {
|
|
130
|
+
const sourceVer = semver.coerce(sourceManifest.version);
|
|
131
|
+
const installedVer = semver.coerce(installedManifest.installed_version);
|
|
132
|
+
|
|
133
|
+
if (sourceVer && installedVer) {
|
|
134
|
+
report.upgradeAvailable = semver.gt(sourceVer, installedVer);
|
|
135
|
+
}
|
|
136
|
+
} else if (sourceManifest?.version && !installedManifest) {
|
|
137
|
+
// No installed manifest means this is a fresh install scenario
|
|
138
|
+
report.upgradeAvailable = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sourceMap = buildFileMap(sourceManifest);
|
|
142
|
+
const installedMap = buildFileMap(installedManifest);
|
|
143
|
+
|
|
144
|
+
const aiosCoreDir = path.join(targetDir, '.aios-core');
|
|
145
|
+
|
|
146
|
+
// Check source files against installed
|
|
147
|
+
for (const [filePath, sourceEntry] of sourceMap) {
|
|
148
|
+
const installedEntry = installedMap.get(filePath);
|
|
149
|
+
const absolutePath = path.join(aiosCoreDir, filePath);
|
|
150
|
+
|
|
151
|
+
if (!installedEntry) {
|
|
152
|
+
// New file in source
|
|
153
|
+
report.newFiles.push({
|
|
154
|
+
path: filePath,
|
|
155
|
+
type: sourceEntry.type,
|
|
156
|
+
hash: sourceEntry.hash,
|
|
157
|
+
size: sourceEntry.size,
|
|
158
|
+
});
|
|
159
|
+
} else if (!hashesMatch(sourceEntry.hash, installedEntry.hash)) {
|
|
160
|
+
// File changed in source
|
|
161
|
+
// Check if user modified the local copy
|
|
162
|
+
if (isUserModified(absolutePath, installedEntry.hash)) {
|
|
163
|
+
report.userModifiedFiles.push({
|
|
164
|
+
path: filePath,
|
|
165
|
+
type: sourceEntry.type,
|
|
166
|
+
sourceHash: sourceEntry.hash,
|
|
167
|
+
installedHash: installedEntry.hash,
|
|
168
|
+
reason: 'User modified local file',
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
report.modifiedFiles.push({
|
|
172
|
+
path: filePath,
|
|
173
|
+
type: sourceEntry.type,
|
|
174
|
+
sourceHash: sourceEntry.hash,
|
|
175
|
+
installedHash: installedEntry.hash,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
report.unchangedFiles++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check for deleted files (in installed but not in source)
|
|
184
|
+
for (const [filePath, installedEntry] of installedMap) {
|
|
185
|
+
if (!sourceMap.has(filePath)) {
|
|
186
|
+
report.deletedFiles.push({
|
|
187
|
+
path: filePath,
|
|
188
|
+
type: installedEntry.type,
|
|
189
|
+
hash: installedEntry.hash,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return report;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Apply upgrade to target directory
|
|
199
|
+
*
|
|
200
|
+
* @param {UpgradeReport} report - Upgrade report
|
|
201
|
+
* @param {string} sourceDir - Source directory (npm package .aios-core)
|
|
202
|
+
* @param {string} targetDir - Target project directory
|
|
203
|
+
* @param {Object} options - Upgrade options
|
|
204
|
+
* @param {boolean} options.dryRun - If true, don't actually copy files
|
|
205
|
+
* @param {boolean} options.includeModified - If true, also update modified files
|
|
206
|
+
* @returns {Object} - Result of upgrade operation
|
|
207
|
+
*/
|
|
208
|
+
async function applyUpgrade(report, sourceDir, targetDir, options = {}) {
|
|
209
|
+
const { dryRun = false, includeModified = true } = options;
|
|
210
|
+
const result = {
|
|
211
|
+
success: true,
|
|
212
|
+
filesInstalled: [],
|
|
213
|
+
filesSkipped: [],
|
|
214
|
+
errors: [],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const aiosCoreDir = path.join(targetDir, '.aios-core');
|
|
218
|
+
|
|
219
|
+
// Ensure .aios-core directory exists
|
|
220
|
+
if (!dryRun) {
|
|
221
|
+
fs.ensureDirSync(aiosCoreDir);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Install new files
|
|
225
|
+
for (const file of report.newFiles) {
|
|
226
|
+
const sourcePath = path.join(sourceDir, file.path);
|
|
227
|
+
const targetPath = path.join(aiosCoreDir, file.path);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
if (!dryRun) {
|
|
231
|
+
fs.ensureDirSync(path.dirname(targetPath));
|
|
232
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
233
|
+
}
|
|
234
|
+
result.filesInstalled.push({ path: file.path, action: 'new' });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
result.errors.push({ path: file.path, error: error.message });
|
|
237
|
+
result.success = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update modified files (if option enabled)
|
|
242
|
+
if (includeModified) {
|
|
243
|
+
for (const file of report.modifiedFiles) {
|
|
244
|
+
const sourcePath = path.join(sourceDir, file.path);
|
|
245
|
+
const targetPath = path.join(aiosCoreDir, file.path);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
if (!dryRun) {
|
|
249
|
+
fs.ensureDirSync(path.dirname(targetPath));
|
|
250
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
251
|
+
}
|
|
252
|
+
result.filesInstalled.push({ path: file.path, action: 'updated' });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
result.errors.push({ path: file.path, error: error.message });
|
|
255
|
+
result.success = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Skip user-modified files
|
|
261
|
+
for (const file of report.userModifiedFiles) {
|
|
262
|
+
result.filesSkipped.push({
|
|
263
|
+
path: file.path,
|
|
264
|
+
reason: 'User modified - preserving local changes',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Note: We don't delete files that were removed from source
|
|
269
|
+
// This is intentional to preserve user additions
|
|
270
|
+
for (const file of report.deletedFiles) {
|
|
271
|
+
result.filesSkipped.push({
|
|
272
|
+
path: file.path,
|
|
273
|
+
reason: 'Removed from source - keeping local copy',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create or update the installed manifest after upgrade
|
|
282
|
+
*
|
|
283
|
+
* @param {string} targetDir - Target project directory
|
|
284
|
+
* @param {Object} sourceManifest - Source manifest that was installed
|
|
285
|
+
* @param {string} sourcePackage - Name and version of source package
|
|
286
|
+
*/
|
|
287
|
+
function updateInstalledManifest(targetDir, sourceManifest, sourcePackage) {
|
|
288
|
+
const installedManifestPath = path.join(targetDir, '.aios-core', '.installed-manifest.yaml');
|
|
289
|
+
|
|
290
|
+
const installedManifest = {
|
|
291
|
+
installed_at: new Date().toISOString(),
|
|
292
|
+
installed_from: sourcePackage,
|
|
293
|
+
installed_version: sourceManifest.version,
|
|
294
|
+
source_manifest_hash: `sha256:${require('./file-hasher').hashString(
|
|
295
|
+
JSON.stringify(sourceManifest.files),
|
|
296
|
+
)}`,
|
|
297
|
+
file_count: sourceManifest.files.length,
|
|
298
|
+
files: sourceManifest.files.map(f => ({
|
|
299
|
+
path: f.path,
|
|
300
|
+
hash: f.hash,
|
|
301
|
+
modified_by_user: false,
|
|
302
|
+
})),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const yamlContent = yaml.dump(installedManifest, {
|
|
306
|
+
indent: 2,
|
|
307
|
+
lineWidth: 120,
|
|
308
|
+
noRefs: true,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const header = `# AIOS-Core Installed Manifest
|
|
312
|
+
# This file tracks what was installed from the npm package
|
|
313
|
+
# Used for brownfield upgrades to detect changes
|
|
314
|
+
# DO NOT EDIT MANUALLY
|
|
315
|
+
#
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
fs.ensureDirSync(path.dirname(installedManifestPath));
|
|
319
|
+
fs.writeFileSync(installedManifestPath, header + yamlContent, 'utf8');
|
|
320
|
+
|
|
321
|
+
return installedManifestPath;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if an upgrade is available
|
|
326
|
+
*
|
|
327
|
+
* @param {string} sourceDir - Source package directory
|
|
328
|
+
* @param {string} targetDir - Target project directory
|
|
329
|
+
* @returns {Object} - { available: boolean, from: string, to: string }
|
|
330
|
+
*/
|
|
331
|
+
function checkUpgradeAvailable(sourceDir, targetDir) {
|
|
332
|
+
const sourceManifest = loadSourceManifest(sourceDir);
|
|
333
|
+
const installedManifest = loadInstalledManifest(targetDir);
|
|
334
|
+
|
|
335
|
+
if (!sourceManifest) {
|
|
336
|
+
return { available: false, error: 'Source manifest not found' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!installedManifest) {
|
|
340
|
+
return { available: false, reason: 'No installed manifest (fresh install)' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const sourceVer = semver.coerce(sourceManifest.version);
|
|
344
|
+
const installedVer = semver.coerce(installedManifest.installed_version);
|
|
345
|
+
|
|
346
|
+
if (!sourceVer || !installedVer) {
|
|
347
|
+
return { available: false, error: 'Invalid version numbers' };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
available: semver.gt(sourceVer, installedVer),
|
|
352
|
+
from: installedManifest.installed_version,
|
|
353
|
+
to: sourceManifest.version,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Format upgrade report for display
|
|
359
|
+
* @param {UpgradeReport} report - Upgrade report
|
|
360
|
+
* @returns {string} - Formatted report string
|
|
361
|
+
*/
|
|
362
|
+
function formatUpgradeReport(report) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
|
|
365
|
+
lines.push('═'.repeat(60));
|
|
366
|
+
lines.push('AIOS-Core Upgrade Report');
|
|
367
|
+
lines.push('═'.repeat(60));
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push(`Current Version: ${report.installedVersion}`);
|
|
370
|
+
lines.push(`Available Version: ${report.sourceVersion}`);
|
|
371
|
+
lines.push(`Upgrade Available: ${report.upgradeAvailable ? 'Yes ✅' : 'No'}`);
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
if (report.newFiles.length > 0) {
|
|
375
|
+
lines.push(`📁 New Files (${report.newFiles.length}):`);
|
|
376
|
+
for (const file of report.newFiles.slice(0, 10)) {
|
|
377
|
+
lines.push(` + ${file.path} [${file.type}]`);
|
|
378
|
+
}
|
|
379
|
+
if (report.newFiles.length > 10) {
|
|
380
|
+
lines.push(` ... and ${report.newFiles.length - 10} more`);
|
|
381
|
+
}
|
|
382
|
+
lines.push('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (report.modifiedFiles.length > 0) {
|
|
386
|
+
lines.push(`📝 Modified Files (${report.modifiedFiles.length}):`);
|
|
387
|
+
for (const file of report.modifiedFiles.slice(0, 10)) {
|
|
388
|
+
lines.push(` ~ ${file.path} [${file.type}]`);
|
|
389
|
+
}
|
|
390
|
+
if (report.modifiedFiles.length > 10) {
|
|
391
|
+
lines.push(` ... and ${report.modifiedFiles.length - 10} more`);
|
|
392
|
+
}
|
|
393
|
+
lines.push('');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (report.userModifiedFiles.length > 0) {
|
|
397
|
+
lines.push(`⚠️ User Modified (${report.userModifiedFiles.length}) - will be preserved:`);
|
|
398
|
+
for (const file of report.userModifiedFiles.slice(0, 10)) {
|
|
399
|
+
lines.push(` ⊘ ${file.path}`);
|
|
400
|
+
}
|
|
401
|
+
if (report.userModifiedFiles.length > 10) {
|
|
402
|
+
lines.push(` ... and ${report.userModifiedFiles.length - 10} more`);
|
|
403
|
+
}
|
|
404
|
+
lines.push('');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (report.deletedFiles.length > 0) {
|
|
408
|
+
lines.push(`🗑️ Removed from Source (${report.deletedFiles.length}):`);
|
|
409
|
+
for (const file of report.deletedFiles.slice(0, 5)) {
|
|
410
|
+
lines.push(` - ${file.path}`);
|
|
411
|
+
}
|
|
412
|
+
if (report.deletedFiles.length > 5) {
|
|
413
|
+
lines.push(` ... and ${report.deletedFiles.length - 5} more`);
|
|
414
|
+
}
|
|
415
|
+
lines.push('');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
lines.push('─'.repeat(60));
|
|
419
|
+
const totalChanges = report.newFiles.length + report.modifiedFiles.length;
|
|
420
|
+
const totalSkipped = report.userModifiedFiles.length + report.deletedFiles.length;
|
|
421
|
+
lines.push(`Summary: ${totalChanges} files to update, ${totalSkipped} files preserved`);
|
|
422
|
+
lines.push('─'.repeat(60));
|
|
423
|
+
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
loadManifest,
|
|
429
|
+
loadInstalledManifest,
|
|
430
|
+
loadSourceManifest,
|
|
431
|
+
generateUpgradeReport,
|
|
432
|
+
applyUpgrade,
|
|
433
|
+
updateInstalledManifest,
|
|
434
|
+
checkUpgradeAvailable,
|
|
435
|
+
formatUpgradeReport,
|
|
436
|
+
buildFileMap,
|
|
437
|
+
isUserModified,
|
|
438
|
+
};
|