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.
- 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/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 +33 -17
- package/.claude/rules/mcp-usage.md +62 -2
- package/package.json +1 -1
|
@@ -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
|
+
};
|