aios-core 3.4.0 → 3.5.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.
@@ -101,6 +101,10 @@ commands:
101
101
  - name: list-squads
102
102
  visibility: [full, quick]
103
103
  description: "List all local squads in the project"
104
+ - name: migrate-squad
105
+ visibility: [full, quick]
106
+ description: "Migrate legacy squad to AIOS 2.1 format"
107
+ task: squad-creator-migrate.md
104
108
 
105
109
  # Distribution (Sprint 8 - Placeholders)
106
110
  - name: download-squad
@@ -130,6 +134,7 @@ dependencies:
130
134
  - squad-creator-create.md
131
135
  - squad-creator-validate.md
132
136
  - squad-creator-list.md
137
+ - squad-creator-migrate.md
133
138
  - squad-creator-download.md
134
139
  - squad-creator-publish.md
135
140
  - squad-creator-sync-synkra.md
@@ -138,6 +143,7 @@ dependencies:
138
143
  - squad/squad-validator.js
139
144
  - squad/squad-generator.js
140
145
  - squad/squad-designer.js
146
+ - squad/squad-migrator.js
141
147
  schemas:
142
148
  - squad-schema.json
143
149
  - squad-design-schema.json
@@ -173,6 +179,11 @@ squad_distribution:
173
179
  - `*validate-squad {name}` - Validate existing squad
174
180
  - `*list-squads` - List local squads
175
181
 
182
+ **Migration:**
183
+ - `*migrate-squad {path}` - Migrate legacy squad to AIOS 2.1 format
184
+ - `*migrate-squad {path} --dry-run` - Preview migration changes
185
+ - `*migrate-squad {path} --verbose` - Migrate with detailed output
186
+
176
187
  **Distribution (Sprint 8):**
177
188
  - `*download-squad {name}` - Download from aios-squads
178
189
  - `*publish-squad {name}` - Publish to aios-squads
@@ -8,6 +8,9 @@
8
8
  * @see {@link ./squad-validator.js} - Validate squad structure (SQS-3)
9
9
  * @see {@link ./squad-generator.js} - Generate new squads (SQS-4)
10
10
  * @see {@link ./squad-designer.js} - Design squads from documentation (SQS-9)
11
+ * @see {@link ./squad-migrator.js} - Migrate legacy squads to AIOS 2.1 (SQS-7)
12
+ * @see {@link ./squad-downloader.js} - Download squads from registry (SQS-6)
13
+ * @see {@link ./squad-publisher.js} - Publish squads to registry (SQS-6)
11
14
  */
12
15
 
13
16
  const {
@@ -43,6 +46,30 @@ const {
43
46
  DesignerErrorCodes,
44
47
  } = require('./squad-designer');
45
48
 
49
+ const {
50
+ SquadMigrator,
51
+ SquadMigratorError,
52
+ MigratorErrorCodes,
53
+ } = require('./squad-migrator');
54
+
55
+ const {
56
+ SquadDownloader,
57
+ SquadDownloaderError,
58
+ DownloaderErrorCodes,
59
+ REGISTRY_URL,
60
+ GITHUB_API_BASE,
61
+ } = require('./squad-downloader');
62
+
63
+ const {
64
+ SquadPublisher,
65
+ SquadPublisherError,
66
+ PublisherErrorCodes,
67
+ AIOS_SQUADS_REPO,
68
+ SAFE_NAME_PATTERN,
69
+ sanitizeForShell,
70
+ isValidName,
71
+ } = require('./squad-publisher');
72
+
46
73
  module.exports = {
47
74
  // Squad Loader (SQS-2)
48
75
  SquadLoader,
@@ -72,4 +99,25 @@ module.exports = {
72
99
  SquadDesigner,
73
100
  SquadDesignerError,
74
101
  DesignerErrorCodes,
102
+
103
+ // Squad Migrator (SQS-7)
104
+ SquadMigrator,
105
+ SquadMigratorError,
106
+ MigratorErrorCodes,
107
+
108
+ // Squad Downloader (SQS-6)
109
+ SquadDownloader,
110
+ SquadDownloaderError,
111
+ DownloaderErrorCodes,
112
+ REGISTRY_URL,
113
+ GITHUB_API_BASE,
114
+
115
+ // Squad Publisher (SQS-6)
116
+ SquadPublisher,
117
+ SquadPublisherError,
118
+ PublisherErrorCodes,
119
+ AIOS_SQUADS_REPO,
120
+ SAFE_NAME_PATTERN,
121
+ sanitizeForShell,
122
+ isValidName,
75
123
  };
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Squad Downloader Utility
3
+ *
4
+ * Downloads squads from the aios-squads GitHub repository.
5
+ * Uses GitHub API for registry.json and raw file downloads.
6
+ *
7
+ * @module squad-downloader
8
+ * @version 1.0.0
9
+ * @see Story SQS-6: Download & Publish Tasks
10
+ */
11
+
12
+ const https = require('https');
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+
16
+ /**
17
+ * Default registry URL for aios-squads
18
+ * @constant {string}
19
+ */
20
+ const REGISTRY_URL =
21
+ 'https://raw.githubusercontent.com/SynkraAI/aios-squads/main/registry.json';
22
+
23
+ /**
24
+ * GitHub API base URL for aios-squads contents
25
+ * @constant {string}
26
+ */
27
+ const GITHUB_API_BASE =
28
+ 'https://api.github.com/repos/SynkraAI/aios-squads/contents/packages';
29
+
30
+ /**
31
+ * Default path for downloaded squads
32
+ * @constant {string}
33
+ */
34
+ const DEFAULT_SQUADS_PATH = './squads';
35
+
36
+ /**
37
+ * Error codes for SquadDownloaderError
38
+ * @enum {string}
39
+ */
40
+ const DownloaderErrorCodes = {
41
+ REGISTRY_FETCH_ERROR: 'REGISTRY_FETCH_ERROR',
42
+ SQUAD_NOT_FOUND: 'SQUAD_NOT_FOUND',
43
+ VERSION_NOT_FOUND: 'VERSION_NOT_FOUND',
44
+ DOWNLOAD_ERROR: 'DOWNLOAD_ERROR',
45
+ NETWORK_ERROR: 'NETWORK_ERROR',
46
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
47
+ SQUAD_EXISTS: 'SQUAD_EXISTS',
48
+ RATE_LIMIT: 'RATE_LIMIT',
49
+ };
50
+
51
+ /**
52
+ * Custom error class for Squad Downloader operations
53
+ * @extends Error
54
+ */
55
+ class SquadDownloaderError extends Error {
56
+ /**
57
+ * Create a SquadDownloaderError
58
+ * @param {string} code - Error code from DownloaderErrorCodes
59
+ * @param {string} message - Human-readable error message
60
+ * @param {string} [suggestion] - Suggested fix for the error
61
+ */
62
+ constructor(code, message, suggestion) {
63
+ super(message);
64
+ this.name = 'SquadDownloaderError';
65
+ this.code = code;
66
+ this.suggestion = suggestion || '';
67
+
68
+ if (Error.captureStackTrace) {
69
+ Error.captureStackTrace(this, SquadDownloaderError);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Returns formatted error string
75
+ * @returns {string}
76
+ */
77
+ toString() {
78
+ let str = `[${this.code}] ${this.message}`;
79
+ if (this.suggestion) {
80
+ str += `\n Suggestion: ${this.suggestion}`;
81
+ }
82
+ return str;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Squad Downloader class for downloading squads from aios-squads repository
88
+ */
89
+ class SquadDownloader {
90
+ /**
91
+ * Create a SquadDownloader instance
92
+ * @param {Object} [options={}] - Configuration options
93
+ * @param {string} [options.squadsPath='./squads'] - Path to download squads to
94
+ * @param {boolean} [options.verbose=false] - Enable verbose logging
95
+ * @param {boolean} [options.overwrite=false] - Overwrite existing squads
96
+ * @param {string} [options.registryUrl] - Custom registry URL
97
+ * @param {string} [options.githubToken] - GitHub token for API rate limits
98
+ */
99
+ constructor(options = {}) {
100
+ this.squadsPath = options.squadsPath || DEFAULT_SQUADS_PATH;
101
+ this.verbose = options.verbose || false;
102
+ this.overwrite = options.overwrite || false;
103
+ this.registryUrl = options.registryUrl || REGISTRY_URL;
104
+ this.githubToken = options.githubToken || process.env.GITHUB_TOKEN || null;
105
+
106
+ // Cache for registry data
107
+ this._registryCache = null;
108
+ this._registryCacheTime = null;
109
+ this._cacheMaxAge = 5 * 60 * 1000; // 5 minutes
110
+ }
111
+
112
+ /**
113
+ * Log message if verbose mode is enabled
114
+ * @private
115
+ * @param {string} message - Message to log
116
+ */
117
+ _log(message) {
118
+ if (this.verbose) {
119
+ console.log(`[SquadDownloader] ${message}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * List available squads from registry
125
+ *
126
+ * @returns {Promise<Array<{name: string, version: string, description: string, type: string}>>}
127
+ * @throws {SquadDownloaderError} REGISTRY_FETCH_ERROR if registry cannot be fetched
128
+ *
129
+ * @example
130
+ * const downloader = new SquadDownloader();
131
+ * const squads = await downloader.listAvailable();
132
+ * // [{ name: 'etl-squad', version: '1.0.0', description: '...', type: 'official' }]
133
+ */
134
+ async listAvailable() {
135
+ this._log('Listing available squads from registry');
136
+ const registry = await this.fetchRegistry();
137
+
138
+ const squads = [];
139
+
140
+ // Add official squads
141
+ if (registry.squads && registry.squads.official) {
142
+ for (const squad of registry.squads.official) {
143
+ squads.push({
144
+ name: squad.name,
145
+ version: squad.version || 'latest',
146
+ description: squad.description || '',
147
+ type: 'official',
148
+ author: squad.author || 'SynkraAI',
149
+ });
150
+ }
151
+ }
152
+
153
+ // Add community squads
154
+ if (registry.squads && registry.squads.community) {
155
+ for (const squad of registry.squads.community) {
156
+ squads.push({
157
+ name: squad.name,
158
+ version: squad.version || 'latest',
159
+ description: squad.description || '',
160
+ type: 'community',
161
+ author: squad.author || 'Community',
162
+ });
163
+ }
164
+ }
165
+
166
+ this._log(`Found ${squads.length} available squad(s)`);
167
+ return squads;
168
+ }
169
+
170
+ /**
171
+ * Download squad by name
172
+ *
173
+ * @param {string} squadName - Name of squad to download (can include @version)
174
+ * @param {Object} [options={}] - Download options
175
+ * @param {string} [options.version='latest'] - Specific version to download
176
+ * @param {boolean} [options.validate=true] - Validate after download
177
+ * @returns {Promise<{path: string, manifest: object, validation: object}>}
178
+ * @throws {SquadDownloaderError} SQUAD_NOT_FOUND if squad doesn't exist in registry
179
+ * @throws {SquadDownloaderError} SQUAD_EXISTS if squad exists and overwrite is false
180
+ * @throws {SquadDownloaderError} DOWNLOAD_ERROR if download fails
181
+ *
182
+ * @example
183
+ * const downloader = new SquadDownloader();
184
+ * const result = await downloader.download('etl-squad');
185
+ * // { path: './squads/etl-squad', manifest: {...}, validation: {...} }
186
+ *
187
+ * // With version
188
+ * await downloader.download('etl-squad@2.0.0');
189
+ */
190
+ async download(squadName, options = {}) {
191
+ // Parse name@version syntax
192
+ let name = squadName;
193
+ let version = options.version || 'latest';
194
+
195
+ if (squadName.includes('@')) {
196
+ const parts = squadName.split('@');
197
+ name = parts[0];
198
+ version = parts[1] || 'latest';
199
+ }
200
+
201
+ this._log(`Downloading squad: ${name}@${version}`);
202
+
203
+ // 1. Check if squad already exists locally
204
+ const targetPath = path.join(this.squadsPath, name);
205
+ if (!this.overwrite && (await this._pathExists(targetPath))) {
206
+ throw new SquadDownloaderError(
207
+ DownloaderErrorCodes.SQUAD_EXISTS,
208
+ `Squad "${name}" already exists at ${targetPath}`,
209
+ 'Use --overwrite flag or delete existing squad first',
210
+ );
211
+ }
212
+
213
+ // 2. Check registry for squad
214
+ const registry = await this.fetchRegistry();
215
+ const squadInfo = this._findSquad(registry, name);
216
+
217
+ if (!squadInfo) {
218
+ throw new SquadDownloaderError(
219
+ DownloaderErrorCodes.SQUAD_NOT_FOUND,
220
+ `Squad "${name}" not found in registry`,
221
+ 'Use *download-squad --list to see available squads',
222
+ );
223
+ }
224
+
225
+ // 3. Verify version if specified
226
+ if (version !== 'latest' && squadInfo.version !== version) {
227
+ this._log(
228
+ `Warning: Requested version ${version}, but only ${squadInfo.version} is available`,
229
+ );
230
+ }
231
+
232
+ // 4. Download squad files
233
+ await this._downloadSquadFiles(squadInfo, targetPath);
234
+
235
+ // 5. Validate downloaded squad (optional)
236
+ let validation = { valid: true, errors: [], warnings: [] };
237
+ if (options.validate !== false) {
238
+ try {
239
+ const { SquadValidator } = require('./squad-validator');
240
+ const validator = new SquadValidator({ verbose: this.verbose });
241
+ validation = await validator.validate(targetPath);
242
+ } catch (error) {
243
+ this._log(`Validation skipped: ${error.message}`);
244
+ }
245
+ }
246
+
247
+ // 6. Load manifest
248
+ let manifest = null;
249
+ try {
250
+ const { SquadLoader } = require('./squad-loader');
251
+ const loader = new SquadLoader({ squadsPath: this.squadsPath });
252
+ manifest = await loader.loadManifest(targetPath);
253
+ } catch (error) {
254
+ this._log(`Failed to load manifest: ${error.message}`);
255
+ }
256
+
257
+ this._log(`Squad "${name}" downloaded successfully to ${targetPath}`);
258
+ return { path: targetPath, manifest, validation };
259
+ }
260
+
261
+ /**
262
+ * Fetch registry from aios-squads repository
263
+ *
264
+ * @returns {Promise<Object>} Registry data
265
+ * @throws {SquadDownloaderError} REGISTRY_FETCH_ERROR if fetch fails
266
+ */
267
+ async fetchRegistry() {
268
+ // Check cache
269
+ if (
270
+ this._registryCache &&
271
+ this._registryCacheTime &&
272
+ Date.now() - this._registryCacheTime < this._cacheMaxAge
273
+ ) {
274
+ this._log('Using cached registry');
275
+ return this._registryCache;
276
+ }
277
+
278
+ this._log(`Fetching registry from: ${this.registryUrl}`);
279
+
280
+ try {
281
+ const data = await this._fetch(this.registryUrl);
282
+ const registry = JSON.parse(data.toString('utf-8'));
283
+
284
+ // Update cache
285
+ this._registryCache = registry;
286
+ this._registryCacheTime = Date.now();
287
+
288
+ return registry;
289
+ } catch (error) {
290
+ if (error.code === 'RATE_LIMIT') {
291
+ throw error;
292
+ }
293
+ throw new SquadDownloaderError(
294
+ DownloaderErrorCodes.REGISTRY_FETCH_ERROR,
295
+ `Failed to fetch registry: ${error.message}`,
296
+ 'Check network connection or try again later',
297
+ );
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Find squad in registry
303
+ * @private
304
+ * @param {Object} registry - Registry data
305
+ * @param {string} name - Squad name
306
+ * @returns {Object|null} Squad info or null
307
+ */
308
+ _findSquad(registry, name) {
309
+ if (!registry || !registry.squads) {
310
+ return null;
311
+ }
312
+
313
+ // Check official squads
314
+ if (registry.squads.official) {
315
+ const found = registry.squads.official.find((s) => s.name === name);
316
+ if (found) {
317
+ return { ...found, type: 'official' };
318
+ }
319
+ }
320
+
321
+ // Check community squads
322
+ if (registry.squads.community) {
323
+ const found = registry.squads.community.find((s) => s.name === name);
324
+ if (found) {
325
+ return { ...found, type: 'community' };
326
+ }
327
+ }
328
+
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Download squad files from GitHub
334
+ * @private
335
+ * @param {Object} squadInfo - Squad info from registry
336
+ * @param {string} targetPath - Local path to download to
337
+ */
338
+ async _downloadSquadFiles(squadInfo, targetPath) {
339
+ this._log(`Downloading files to: ${targetPath}`);
340
+
341
+ // Ensure target directory exists
342
+ await fs.mkdir(targetPath, { recursive: true });
343
+
344
+ // Get squad files from GitHub API
345
+ const apiUrl = `${GITHUB_API_BASE}/${squadInfo.name}`;
346
+ let contents;
347
+
348
+ try {
349
+ const data = await this._fetch(apiUrl, true);
350
+ contents = JSON.parse(data.toString('utf-8'));
351
+ } catch (error) {
352
+ throw new SquadDownloaderError(
353
+ DownloaderErrorCodes.DOWNLOAD_ERROR,
354
+ `Failed to fetch squad contents: ${error.message}`,
355
+ 'Squad may not exist in repository yet',
356
+ );
357
+ }
358
+
359
+ if (!Array.isArray(contents)) {
360
+ throw new SquadDownloaderError(
361
+ DownloaderErrorCodes.DOWNLOAD_ERROR,
362
+ 'Invalid response from GitHub API',
363
+ 'Check if squad exists in aios-squads repository',
364
+ );
365
+ }
366
+
367
+ // Download each file/directory recursively
368
+ await this._downloadContents(contents, targetPath);
369
+
370
+ this._log(`Downloaded ${contents.length} items to ${targetPath}`);
371
+ }
372
+
373
+ /**
374
+ * Download contents recursively
375
+ * @private
376
+ * @param {Array} contents - GitHub API contents array
377
+ * @param {string} targetPath - Local target path
378
+ */
379
+ async _downloadContents(contents, targetPath) {
380
+ for (const item of contents) {
381
+ const itemPath = path.join(targetPath, item.name);
382
+
383
+ if (item.type === 'file') {
384
+ // Download file - Buffer is written directly (supports binary files)
385
+ this._log(`Downloading: ${item.name}`);
386
+ const fileContent = await this._fetch(item.download_url);
387
+ await fs.writeFile(itemPath, fileContent);
388
+ } else if (item.type === 'dir') {
389
+ // Create directory and download contents
390
+ await fs.mkdir(itemPath, { recursive: true });
391
+ const dirContents = await this._fetch(item.url, true);
392
+ const parsed = JSON.parse(dirContents.toString('utf-8'));
393
+ await this._downloadContents(parsed, itemPath);
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Make HTTPS request
400
+ * @private
401
+ * @param {string} url - URL to fetch
402
+ * @param {boolean} [useApi=false] - Whether to use GitHub API headers
403
+ * @returns {Promise<Buffer>} Response body as Buffer (supports binary files)
404
+ */
405
+ _fetch(url, useApi = false) {
406
+ return new Promise((resolve, reject) => {
407
+ const options = {
408
+ headers: {
409
+ 'User-Agent': 'AIOS-SquadDownloader/1.0',
410
+ },
411
+ };
412
+
413
+ if (useApi) {
414
+ options.headers['Accept'] = 'application/vnd.github.v3+json';
415
+ if (this.githubToken) {
416
+ options.headers['Authorization'] = `token ${this.githubToken}`;
417
+ }
418
+ }
419
+
420
+ https
421
+ .get(url, options, (res) => {
422
+ // Check for rate limiting
423
+ if (res.statusCode === 403) {
424
+ const rateLimitRemaining = res.headers['x-ratelimit-remaining'];
425
+ if (rateLimitRemaining === '0') {
426
+ const resetTime = res.headers['x-ratelimit-reset'];
427
+ const resetDate = new Date(parseInt(resetTime) * 1000);
428
+ reject(
429
+ new SquadDownloaderError(
430
+ DownloaderErrorCodes.RATE_LIMIT,
431
+ `GitHub API rate limit exceeded. Resets at ${resetDate.toISOString()}`,
432
+ 'Set GITHUB_TOKEN environment variable to increase rate limit',
433
+ ),
434
+ );
435
+ return;
436
+ }
437
+ }
438
+
439
+ // Check for redirect
440
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
441
+ this._fetch(res.headers.location, useApi).then(resolve).catch(reject);
442
+ return;
443
+ }
444
+
445
+ // Check for errors
446
+ if (res.statusCode !== 200) {
447
+ reject(
448
+ new SquadDownloaderError(
449
+ DownloaderErrorCodes.NETWORK_ERROR,
450
+ `HTTP ${res.statusCode}: ${res.statusMessage}`,
451
+ ),
452
+ );
453
+ return;
454
+ }
455
+
456
+ // Collect chunks as Buffer objects to support binary files
457
+ const chunks = [];
458
+ res.on('data', (chunk) => {
459
+ chunks.push(chunk);
460
+ });
461
+ res.on('end', () => {
462
+ // Concatenate all chunks into a single Buffer
463
+ resolve(Buffer.concat(chunks));
464
+ });
465
+ })
466
+ .on('error', (error) => {
467
+ reject(
468
+ new SquadDownloaderError(
469
+ DownloaderErrorCodes.NETWORK_ERROR,
470
+ `Network error: ${error.message}`,
471
+ 'Check internet connection',
472
+ ),
473
+ );
474
+ });
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Check if path exists
480
+ * @private
481
+ * @param {string} filePath - Path to check
482
+ * @returns {Promise<boolean>}
483
+ */
484
+ async _pathExists(filePath) {
485
+ try {
486
+ await fs.access(filePath);
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Clear registry cache
495
+ */
496
+ clearCache() {
497
+ this._registryCache = null;
498
+ this._registryCacheTime = null;
499
+ this._log('Registry cache cleared');
500
+ }
501
+ }
502
+
503
+ module.exports = {
504
+ SquadDownloader,
505
+ SquadDownloaderError,
506
+ DownloaderErrorCodes,
507
+ REGISTRY_URL,
508
+ GITHUB_API_BASE,
509
+ DEFAULT_SQUADS_PATH,
510
+ };