aios-core 3.4.0 → 3.6.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/development/agents/architect.md +4 -0
- package/.aios-core/development/agents/squad-creator.md +11 -0
- package/.aios-core/development/scripts/squad/index.js +48 -0
- package/.aios-core/development/scripts/squad/squad-downloader.js +510 -0
- package/.aios-core/development/scripts/squad/squad-migrator.js +634 -0
- package/.aios-core/development/scripts/squad/squad-publisher.js +629 -0
- package/.aios-core/development/tasks/add-mcp.md +124 -13
- package/.aios-core/development/tasks/analyze-project-structure.md +621 -0
- package/.aios-core/development/tasks/setup-mcp-docker.md +46 -6
- package/.aios-core/development/tasks/squad-creator-download.md +135 -33
- package/.aios-core/development/tasks/squad-creator-migrate.md +243 -0
- package/.aios-core/development/tasks/squad-creator-publish.md +190 -47
- package/.aios-core/development/tasks/squad-creator-sync-synkra.md +280 -48
- package/.aios-core/install-manifest.yaml +39 -19
- package/.claude/rules/mcp-usage.md +62 -2
- package/package.json +1 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squad Migrator Utility
|
|
3
|
+
*
|
|
4
|
+
* Migrates legacy squad formats to AIOS 2.1 standard.
|
|
5
|
+
* Handles manifest, structure, and task format migrations.
|
|
6
|
+
*
|
|
7
|
+
* Used by: squad-creator agent (*migrate-squad task)
|
|
8
|
+
*
|
|
9
|
+
* @module squad-migrator
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
* @see Story SQS-7: Squad Migration Tool
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error codes for SquadMigratorError
|
|
20
|
+
* @enum {string}
|
|
21
|
+
*/
|
|
22
|
+
const MigratorErrorCodes = {
|
|
23
|
+
SQUAD_NOT_FOUND: 'SQUAD_NOT_FOUND',
|
|
24
|
+
NO_MANIFEST: 'NO_MANIFEST',
|
|
25
|
+
BACKUP_FAILED: 'BACKUP_FAILED',
|
|
26
|
+
MIGRATION_FAILED: 'MIGRATION_FAILED',
|
|
27
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
28
|
+
INVALID_PATH: 'INVALID_PATH',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Custom error class for migration errors
|
|
33
|
+
*/
|
|
34
|
+
class SquadMigratorError extends Error {
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} code - Error code from MigratorErrorCodes
|
|
37
|
+
* @param {string} message - Human-readable error message
|
|
38
|
+
* @param {Object} [details={}] - Additional error details
|
|
39
|
+
*/
|
|
40
|
+
constructor(code, message, details = {}) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'SquadMigratorError';
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.details = details;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Migration analysis result structure
|
|
50
|
+
* @typedef {Object} MigrationAnalysis
|
|
51
|
+
* @property {boolean} needsMigration - Whether migration is required
|
|
52
|
+
* @property {Array<MigrationIssue>} issues - Issues found during analysis
|
|
53
|
+
* @property {Array<MigrationAction>} actions - Actions to perform
|
|
54
|
+
* @property {string} squadPath - Analyzed squad path
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Migration issue structure
|
|
59
|
+
* @typedef {Object} MigrationIssue
|
|
60
|
+
* @property {string} type - Issue type
|
|
61
|
+
* @property {string} message - Human-readable message
|
|
62
|
+
* @property {string} severity - 'error' | 'warning' | 'info'
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Migration action structure
|
|
67
|
+
* @typedef {Object} MigrationAction
|
|
68
|
+
* @property {string} type - Action type
|
|
69
|
+
* @property {Object} [params] - Action parameters
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Migration result structure
|
|
74
|
+
* @typedef {Object} MigrationResult
|
|
75
|
+
* @property {boolean} success - Whether migration succeeded
|
|
76
|
+
* @property {string} message - Result message
|
|
77
|
+
* @property {Array<ExecutedAction>} actions - Actions executed
|
|
78
|
+
* @property {Object|null} validation - Post-migration validation result
|
|
79
|
+
* @property {string|null} backupPath - Path to backup directory
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Squad Migrator class for migrating legacy squad formats
|
|
84
|
+
*/
|
|
85
|
+
class SquadMigrator {
|
|
86
|
+
/**
|
|
87
|
+
* Create a SquadMigrator instance
|
|
88
|
+
* @param {Object} [options={}] - Configuration options
|
|
89
|
+
* @param {boolean} [options.dryRun=false] - Simulate without modifying files
|
|
90
|
+
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
91
|
+
* @param {Object} [options.validator=null] - Custom SquadValidator instance
|
|
92
|
+
*/
|
|
93
|
+
constructor(options = {}) {
|
|
94
|
+
this.dryRun = options.dryRun || false;
|
|
95
|
+
this.verbose = options.verbose || false;
|
|
96
|
+
this.validator = options.validator || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Log message if verbose mode is enabled
|
|
101
|
+
* @param {string} message - Message to log
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
_log(message) {
|
|
105
|
+
if (this.verbose) {
|
|
106
|
+
console.log(`[SquadMigrator] ${message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a path exists
|
|
112
|
+
* @param {string} filePath - Path to check
|
|
113
|
+
* @returns {Promise<boolean>}
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
async _pathExists(filePath) {
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(filePath);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recursively copy a file or directory
|
|
127
|
+
* @param {string} src - Source path
|
|
128
|
+
* @param {string} dest - Destination path
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
async _copyRecursive(src, dest) {
|
|
132
|
+
const stats = await fs.stat(src);
|
|
133
|
+
if (stats.isDirectory()) {
|
|
134
|
+
await fs.mkdir(dest, { recursive: true });
|
|
135
|
+
const entries = await fs.readdir(src);
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
await this._copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
await fs.copyFile(src, dest);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Analyze squad for migration needs
|
|
146
|
+
* @param {string} squadPath - Path to squad directory
|
|
147
|
+
* @returns {Promise<MigrationAnalysis>}
|
|
148
|
+
*/
|
|
149
|
+
async analyze(squadPath) {
|
|
150
|
+
this._log(`Analyzing squad at: ${squadPath}`);
|
|
151
|
+
|
|
152
|
+
// Verify squad directory exists
|
|
153
|
+
if (!(await this._pathExists(squadPath))) {
|
|
154
|
+
throw new SquadMigratorError(
|
|
155
|
+
MigratorErrorCodes.SQUAD_NOT_FOUND,
|
|
156
|
+
`Squad directory not found: ${squadPath}`,
|
|
157
|
+
{ squadPath }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const analysis = {
|
|
162
|
+
needsMigration: false,
|
|
163
|
+
issues: [],
|
|
164
|
+
actions: [],
|
|
165
|
+
squadPath,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Check for legacy manifest (config.yaml)
|
|
169
|
+
const hasConfigYaml = await this._pathExists(path.join(squadPath, 'config.yaml'));
|
|
170
|
+
const hasSquadYaml = await this._pathExists(path.join(squadPath, 'squad.yaml'));
|
|
171
|
+
|
|
172
|
+
if (!hasConfigYaml && !hasSquadYaml) {
|
|
173
|
+
throw new SquadMigratorError(
|
|
174
|
+
MigratorErrorCodes.NO_MANIFEST,
|
|
175
|
+
'No manifest found (config.yaml or squad.yaml)',
|
|
176
|
+
{ squadPath }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check for legacy manifest name
|
|
181
|
+
if (hasConfigYaml && !hasSquadYaml) {
|
|
182
|
+
analysis.needsMigration = true;
|
|
183
|
+
analysis.issues.push({
|
|
184
|
+
type: 'LEGACY_MANIFEST',
|
|
185
|
+
message: 'Uses deprecated config.yaml manifest',
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
});
|
|
188
|
+
analysis.actions.push({
|
|
189
|
+
type: 'RENAME_MANIFEST',
|
|
190
|
+
from: 'config.yaml',
|
|
191
|
+
to: 'squad.yaml',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for flat structure (missing directories)
|
|
196
|
+
const hasTasksDir = await this._pathExists(path.join(squadPath, 'tasks'));
|
|
197
|
+
const hasAgentsDir = await this._pathExists(path.join(squadPath, 'agents'));
|
|
198
|
+
const hasConfigDir = await this._pathExists(path.join(squadPath, 'config'));
|
|
199
|
+
|
|
200
|
+
const missingDirs = [];
|
|
201
|
+
if (!hasTasksDir) {
|
|
202
|
+
missingDirs.push('tasks');
|
|
203
|
+
}
|
|
204
|
+
if (!hasAgentsDir) {
|
|
205
|
+
missingDirs.push('agents');
|
|
206
|
+
}
|
|
207
|
+
if (!hasConfigDir) {
|
|
208
|
+
missingDirs.push('config');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (missingDirs.length > 0) {
|
|
212
|
+
analysis.needsMigration = true;
|
|
213
|
+
analysis.issues.push({
|
|
214
|
+
type: 'FLAT_STRUCTURE',
|
|
215
|
+
message: `Missing task-first directories: ${missingDirs.join(', ')}`,
|
|
216
|
+
severity: 'warning',
|
|
217
|
+
});
|
|
218
|
+
analysis.actions.push({
|
|
219
|
+
type: 'CREATE_DIRECTORIES',
|
|
220
|
+
dirs: missingDirs,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check manifest schema compliance
|
|
225
|
+
const manifestPath = hasSquadYaml
|
|
226
|
+
? path.join(squadPath, 'squad.yaml')
|
|
227
|
+
: path.join(squadPath, 'config.yaml');
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
231
|
+
const manifest = yaml.load(content);
|
|
232
|
+
|
|
233
|
+
// Check for missing aios.type
|
|
234
|
+
if (!manifest.aios?.type) {
|
|
235
|
+
analysis.needsMigration = true;
|
|
236
|
+
analysis.issues.push({
|
|
237
|
+
type: 'MISSING_AIOS_TYPE',
|
|
238
|
+
message: 'Missing required field: aios.type',
|
|
239
|
+
severity: 'error',
|
|
240
|
+
});
|
|
241
|
+
analysis.actions.push({
|
|
242
|
+
type: 'ADD_FIELD',
|
|
243
|
+
path: 'aios.type',
|
|
244
|
+
value: 'squad',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for missing aios.minVersion
|
|
249
|
+
if (!manifest.aios?.minVersion) {
|
|
250
|
+
analysis.needsMigration = true;
|
|
251
|
+
analysis.issues.push({
|
|
252
|
+
type: 'MISSING_MIN_VERSION',
|
|
253
|
+
message: 'Missing required field: aios.minVersion',
|
|
254
|
+
severity: 'error',
|
|
255
|
+
});
|
|
256
|
+
analysis.actions.push({
|
|
257
|
+
type: 'ADD_FIELD',
|
|
258
|
+
path: 'aios.minVersion',
|
|
259
|
+
value: '2.1.0',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for missing name field
|
|
264
|
+
if (!manifest.name) {
|
|
265
|
+
analysis.needsMigration = true;
|
|
266
|
+
analysis.issues.push({
|
|
267
|
+
type: 'MISSING_NAME',
|
|
268
|
+
message: 'Missing required field: name',
|
|
269
|
+
severity: 'error',
|
|
270
|
+
});
|
|
271
|
+
// Try to infer name from directory
|
|
272
|
+
const inferredName = path.basename(squadPath);
|
|
273
|
+
analysis.actions.push({
|
|
274
|
+
type: 'ADD_FIELD',
|
|
275
|
+
path: 'name',
|
|
276
|
+
value: inferredName,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for missing version field
|
|
281
|
+
if (!manifest.version) {
|
|
282
|
+
analysis.needsMigration = true;
|
|
283
|
+
analysis.issues.push({
|
|
284
|
+
type: 'MISSING_VERSION',
|
|
285
|
+
message: 'Missing required field: version',
|
|
286
|
+
severity: 'error',
|
|
287
|
+
});
|
|
288
|
+
analysis.actions.push({
|
|
289
|
+
type: 'ADD_FIELD',
|
|
290
|
+
path: 'version',
|
|
291
|
+
value: '1.0.0',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.name === 'YAMLException') {
|
|
296
|
+
throw new SquadMigratorError(
|
|
297
|
+
MigratorErrorCodes.MIGRATION_FAILED,
|
|
298
|
+
`Invalid YAML in manifest: ${error.message}`,
|
|
299
|
+
{ squadPath, error: error.message }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this._log(`Analysis complete. Needs migration: ${analysis.needsMigration}`);
|
|
306
|
+
this._log(`Issues found: ${analysis.issues.length}`);
|
|
307
|
+
this._log(`Actions planned: ${analysis.actions.length}`);
|
|
308
|
+
|
|
309
|
+
return analysis;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Create backup of squad before migration
|
|
314
|
+
* @param {string} squadPath - Path to squad directory
|
|
315
|
+
* @returns {Promise<string>} Path to backup directory
|
|
316
|
+
*/
|
|
317
|
+
async createBackup(squadPath) {
|
|
318
|
+
const timestamp = Date.now();
|
|
319
|
+
const backupDir = path.join(squadPath, '.backup');
|
|
320
|
+
const backupPath = path.join(backupDir, `pre-migration-${timestamp}`);
|
|
321
|
+
|
|
322
|
+
this._log(`Creating backup at: ${backupPath}`);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await fs.mkdir(backupPath, { recursive: true });
|
|
326
|
+
|
|
327
|
+
// Copy all files except .backup directory
|
|
328
|
+
const files = await fs.readdir(squadPath);
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
if (file === '.backup') {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const src = path.join(squadPath, file);
|
|
334
|
+
const dest = path.join(backupPath, file);
|
|
335
|
+
await this._copyRecursive(src, dest);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this._log(`Backup created successfully`);
|
|
339
|
+
return backupPath;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new SquadMigratorError(
|
|
342
|
+
MigratorErrorCodes.BACKUP_FAILED,
|
|
343
|
+
`Failed to create backup: ${error.message}`,
|
|
344
|
+
{ squadPath, error: error.message }
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Execute a single migration action
|
|
351
|
+
* @param {string} squadPath - Path to squad directory
|
|
352
|
+
* @param {MigrationAction} action - Action to execute
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
async _executeAction(squadPath, action) {
|
|
356
|
+
this._log(`Executing action: ${action.type}`);
|
|
357
|
+
|
|
358
|
+
switch (action.type) {
|
|
359
|
+
case 'RENAME_MANIFEST':
|
|
360
|
+
await fs.rename(
|
|
361
|
+
path.join(squadPath, action.from),
|
|
362
|
+
path.join(squadPath, action.to)
|
|
363
|
+
);
|
|
364
|
+
break;
|
|
365
|
+
|
|
366
|
+
case 'CREATE_DIRECTORIES':
|
|
367
|
+
for (const dir of action.dirs) {
|
|
368
|
+
await fs.mkdir(path.join(squadPath, dir), { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case 'ADD_FIELD':
|
|
373
|
+
await this._addManifestField(squadPath, action.path, action.value);
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case 'MOVE_FILE':
|
|
377
|
+
await fs.rename(
|
|
378
|
+
path.join(squadPath, action.from),
|
|
379
|
+
path.join(squadPath, action.to)
|
|
380
|
+
);
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
throw new SquadMigratorError(
|
|
385
|
+
MigratorErrorCodes.MIGRATION_FAILED,
|
|
386
|
+
`Unknown action type: ${action.type}`,
|
|
387
|
+
{ action }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Add or update a field in the manifest YAML
|
|
394
|
+
* @param {string} squadPath - Path to squad directory
|
|
395
|
+
* @param {string} fieldPath - Dot-separated path to field
|
|
396
|
+
* @param {any} value - Value to set
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
async _addManifestField(squadPath, fieldPath, value) {
|
|
400
|
+
const manifestPath = path.join(squadPath, 'squad.yaml');
|
|
401
|
+
|
|
402
|
+
let content;
|
|
403
|
+
let manifest;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
content = await fs.readFile(manifestPath, 'utf-8');
|
|
407
|
+
manifest = yaml.load(content) || {};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error.code === 'ENOENT') {
|
|
410
|
+
// If squad.yaml doesn't exist yet, try config.yaml
|
|
411
|
+
const configPath = path.join(squadPath, 'config.yaml');
|
|
412
|
+
content = await fs.readFile(configPath, 'utf-8');
|
|
413
|
+
manifest = yaml.load(content) || {};
|
|
414
|
+
} else {
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Navigate to nested path and set value
|
|
420
|
+
const parts = fieldPath.split('.');
|
|
421
|
+
let current = manifest;
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
424
|
+
if (!current[parts[i]]) {
|
|
425
|
+
current[parts[i]] = {};
|
|
426
|
+
}
|
|
427
|
+
current = current[parts[i]];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
current[parts[parts.length - 1]] = value;
|
|
431
|
+
|
|
432
|
+
// Write back to manifest
|
|
433
|
+
await fs.writeFile(manifestPath, yaml.dump(manifest, { lineWidth: -1 }), 'utf-8');
|
|
434
|
+
|
|
435
|
+
this._log(`Added field ${fieldPath} = ${value}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Migrate squad to current format
|
|
440
|
+
* @param {string} squadPath - Path to squad directory
|
|
441
|
+
* @returns {Promise<MigrationResult>}
|
|
442
|
+
*/
|
|
443
|
+
async migrate(squadPath) {
|
|
444
|
+
this._log(`Starting migration for: ${squadPath}`);
|
|
445
|
+
|
|
446
|
+
// Analyze squad first
|
|
447
|
+
const analysis = await this.analyze(squadPath);
|
|
448
|
+
|
|
449
|
+
// If no migration needed, return early
|
|
450
|
+
if (!analysis.needsMigration) {
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
message: 'Squad is already up to date',
|
|
454
|
+
actions: [],
|
|
455
|
+
validation: null,
|
|
456
|
+
backupPath: null,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Create backup (unless dry-run)
|
|
461
|
+
let backupPath = null;
|
|
462
|
+
if (!this.dryRun) {
|
|
463
|
+
backupPath = await this.createBackup(squadPath);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Execute actions
|
|
467
|
+
const executedActions = [];
|
|
468
|
+
|
|
469
|
+
for (const action of analysis.actions) {
|
|
470
|
+
if (this.dryRun) {
|
|
471
|
+
executedActions.push({
|
|
472
|
+
...action,
|
|
473
|
+
status: 'dry-run',
|
|
474
|
+
});
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
await this._executeAction(squadPath, action);
|
|
480
|
+
executedActions.push({
|
|
481
|
+
...action,
|
|
482
|
+
status: 'success',
|
|
483
|
+
});
|
|
484
|
+
} catch (error) {
|
|
485
|
+
executedActions.push({
|
|
486
|
+
...action,
|
|
487
|
+
status: 'failed',
|
|
488
|
+
error: error.message,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check if any actions failed
|
|
494
|
+
const hasFailures = executedActions.some((a) => a.status === 'failed');
|
|
495
|
+
|
|
496
|
+
// Validate after migration (unless dry-run)
|
|
497
|
+
let validation = null;
|
|
498
|
+
if (!this.dryRun && this.validator) {
|
|
499
|
+
try {
|
|
500
|
+
validation = await this.validator.validate(squadPath);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this._log(`Validation error: ${error.message}`);
|
|
503
|
+
validation = { valid: false, error: error.message };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const result = {
|
|
508
|
+
success: !hasFailures,
|
|
509
|
+
message: hasFailures
|
|
510
|
+
? 'Migration completed with errors'
|
|
511
|
+
: this.dryRun
|
|
512
|
+
? 'Dry-run completed successfully'
|
|
513
|
+
: 'Migration completed successfully',
|
|
514
|
+
actions: executedActions,
|
|
515
|
+
validation,
|
|
516
|
+
backupPath,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
this._log(`Migration complete. Success: ${result.success}`);
|
|
520
|
+
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Generate migration report
|
|
526
|
+
* @param {MigrationAnalysis} analysis - Analysis result
|
|
527
|
+
* @param {MigrationResult} [result] - Migration result (if executed)
|
|
528
|
+
* @returns {string} Formatted report
|
|
529
|
+
*/
|
|
530
|
+
generateReport(analysis, result = null) {
|
|
531
|
+
const lines = [];
|
|
532
|
+
|
|
533
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
534
|
+
lines.push(' SQUAD MIGRATION REPORT');
|
|
535
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
536
|
+
lines.push('');
|
|
537
|
+
lines.push(`Squad Path: ${analysis.squadPath}`);
|
|
538
|
+
lines.push(`Needs Migration: ${analysis.needsMigration ? 'Yes' : 'No'}`);
|
|
539
|
+
lines.push('');
|
|
540
|
+
|
|
541
|
+
// Issues section
|
|
542
|
+
if (analysis.issues.length > 0) {
|
|
543
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
544
|
+
lines.push('ISSUES FOUND:');
|
|
545
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
546
|
+
for (const issue of analysis.issues) {
|
|
547
|
+
const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
548
|
+
lines.push(` ${icon} [${issue.severity.toUpperCase()}] ${issue.message}`);
|
|
549
|
+
}
|
|
550
|
+
lines.push('');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Actions section
|
|
554
|
+
if (analysis.actions.length > 0) {
|
|
555
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
556
|
+
lines.push('PLANNED ACTIONS:');
|
|
557
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
558
|
+
for (let i = 0; i < analysis.actions.length; i++) {
|
|
559
|
+
const action = analysis.actions[i];
|
|
560
|
+
lines.push(` ${i + 1}. ${this._formatAction(action)}`);
|
|
561
|
+
}
|
|
562
|
+
lines.push('');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Result section (if migration was executed)
|
|
566
|
+
if (result) {
|
|
567
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
568
|
+
lines.push('MIGRATION RESULT:');
|
|
569
|
+
lines.push('───────────────────────────────────────────────────────────');
|
|
570
|
+
lines.push(` Status: ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
|
|
571
|
+
lines.push(` Message: ${result.message}`);
|
|
572
|
+
|
|
573
|
+
if (result.backupPath) {
|
|
574
|
+
lines.push(` Backup: ${result.backupPath}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (result.actions.length > 0) {
|
|
578
|
+
lines.push('');
|
|
579
|
+
lines.push(' Executed Actions:');
|
|
580
|
+
for (const action of result.actions) {
|
|
581
|
+
const icon = action.status === 'success' ? '✅' : action.status === 'dry-run' ? '🔍' : '❌';
|
|
582
|
+
lines.push(` ${icon} ${this._formatAction(action)} [${action.status}]`);
|
|
583
|
+
if (action.error) {
|
|
584
|
+
lines.push(` Error: ${action.error}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (result.validation) {
|
|
590
|
+
lines.push('');
|
|
591
|
+
lines.push(' Post-Migration Validation:');
|
|
592
|
+
lines.push(` Valid: ${result.validation.valid ? 'Yes' : 'No'}`);
|
|
593
|
+
if (result.validation.errors?.length > 0) {
|
|
594
|
+
lines.push(` Errors: ${result.validation.errors.length}`);
|
|
595
|
+
}
|
|
596
|
+
if (result.validation.warnings?.length > 0) {
|
|
597
|
+
lines.push(` Warnings: ${result.validation.warnings.length}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
lines.push('');
|
|
603
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
604
|
+
|
|
605
|
+
return lines.join('\n');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Format an action for display
|
|
610
|
+
* @param {MigrationAction} action - Action to format
|
|
611
|
+
* @returns {string}
|
|
612
|
+
* @private
|
|
613
|
+
*/
|
|
614
|
+
_formatAction(action) {
|
|
615
|
+
switch (action.type) {
|
|
616
|
+
case 'RENAME_MANIFEST':
|
|
617
|
+
return `Rename ${action.from} → ${action.to}`;
|
|
618
|
+
case 'CREATE_DIRECTORIES':
|
|
619
|
+
return `Create directories: ${action.dirs.join(', ')}`;
|
|
620
|
+
case 'ADD_FIELD':
|
|
621
|
+
return `Add field: ${action.path} = "${action.value}"`;
|
|
622
|
+
case 'MOVE_FILE':
|
|
623
|
+
return `Move ${action.from} → ${action.to}`;
|
|
624
|
+
default:
|
|
625
|
+
return `${action.type}`;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
module.exports = {
|
|
631
|
+
SquadMigrator,
|
|
632
|
+
SquadMigratorError,
|
|
633
|
+
MigratorErrorCodes,
|
|
634
|
+
};
|