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
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squad Publisher Utility
|
|
3
|
+
*
|
|
4
|
+
* Publishes squads to the aios-squads GitHub repository via Pull Request.
|
|
5
|
+
* Requires GitHub CLI (gh) authentication.
|
|
6
|
+
*
|
|
7
|
+
* @module squad-publisher
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @see Story SQS-6: Download & Publish Tasks
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execSync, spawnSync } = require('child_process');
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Regex pattern for safe squad/branch names
|
|
18
|
+
* Only allows alphanumerics, hyphens, underscores, and dots
|
|
19
|
+
* @constant {RegExp}
|
|
20
|
+
*/
|
|
21
|
+
const SAFE_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Repository for aios-squads
|
|
25
|
+
* @constant {string}
|
|
26
|
+
*/
|
|
27
|
+
const AIOS_SQUADS_REPO = 'SynkraAI/aios-squads';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error codes for SquadPublisherError
|
|
31
|
+
* @enum {string}
|
|
32
|
+
*/
|
|
33
|
+
const PublisherErrorCodes = {
|
|
34
|
+
AUTH_REQUIRED: 'AUTH_REQUIRED',
|
|
35
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
36
|
+
SQUAD_NOT_FOUND: 'SQUAD_NOT_FOUND',
|
|
37
|
+
MANIFEST_ERROR: 'MANIFEST_ERROR',
|
|
38
|
+
GH_CLI_ERROR: 'GH_CLI_ERROR',
|
|
39
|
+
PR_ERROR: 'PR_ERROR',
|
|
40
|
+
FORK_ERROR: 'FORK_ERROR',
|
|
41
|
+
SQUAD_EXISTS_IN_REGISTRY: 'SQUAD_EXISTS_IN_REGISTRY',
|
|
42
|
+
INVALID_SQUAD_NAME: 'INVALID_SQUAD_NAME',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize a string for safe use in shell commands and file paths
|
|
47
|
+
* @param {string} value - Value to sanitize
|
|
48
|
+
* @returns {string} Sanitized value
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeForShell(value) {
|
|
51
|
+
if (!value || typeof value !== 'string') {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
// Replace unsafe characters with hyphens, then collapse multiple hyphens
|
|
55
|
+
return value
|
|
56
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
57
|
+
.replace(/-+/g, '-')
|
|
58
|
+
.replace(/^-|-$/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate that a name is safe for use in shell commands
|
|
63
|
+
* @param {string} name - Name to validate
|
|
64
|
+
* @returns {boolean} True if safe
|
|
65
|
+
*/
|
|
66
|
+
function isValidName(name) {
|
|
67
|
+
if (!name || typeof name !== 'string') {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return SAFE_NAME_PATTERN.test(name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Custom error class for Squad Publisher operations
|
|
75
|
+
* @extends Error
|
|
76
|
+
*/
|
|
77
|
+
class SquadPublisherError extends Error {
|
|
78
|
+
/**
|
|
79
|
+
* Create a SquadPublisherError
|
|
80
|
+
* @param {string} code - Error code from PublisherErrorCodes
|
|
81
|
+
* @param {string} message - Human-readable error message
|
|
82
|
+
* @param {string} [suggestion] - Suggested fix for the error
|
|
83
|
+
*/
|
|
84
|
+
constructor(code, message, suggestion) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = 'SquadPublisherError';
|
|
87
|
+
this.code = code;
|
|
88
|
+
this.suggestion = suggestion || '';
|
|
89
|
+
|
|
90
|
+
if (Error.captureStackTrace) {
|
|
91
|
+
Error.captureStackTrace(this, SquadPublisherError);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns formatted error string
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
toString() {
|
|
100
|
+
let str = `[${this.code}] ${this.message}`;
|
|
101
|
+
if (this.suggestion) {
|
|
102
|
+
str += `\n Suggestion: ${this.suggestion}`;
|
|
103
|
+
}
|
|
104
|
+
return str;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Squad Publisher class for publishing squads to aios-squads repository
|
|
110
|
+
*/
|
|
111
|
+
class SquadPublisher {
|
|
112
|
+
/**
|
|
113
|
+
* Create a SquadPublisher instance
|
|
114
|
+
* @param {Object} [options={}] - Configuration options
|
|
115
|
+
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
116
|
+
* @param {boolean} [options.dryRun=false] - Simulate without creating PR
|
|
117
|
+
* @param {string} [options.repo] - Target repository (default: SynkraAI/aios-squads)
|
|
118
|
+
*/
|
|
119
|
+
constructor(options = {}) {
|
|
120
|
+
this.verbose = options.verbose || false;
|
|
121
|
+
this.dryRun = options.dryRun || false;
|
|
122
|
+
this.repo = options.repo || AIOS_SQUADS_REPO;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Log message if verbose mode is enabled
|
|
127
|
+
* @private
|
|
128
|
+
* @param {string} message - Message to log
|
|
129
|
+
*/
|
|
130
|
+
_log(message) {
|
|
131
|
+
if (this.verbose) {
|
|
132
|
+
console.log(`[SquadPublisher] ${message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check GitHub CLI authentication
|
|
138
|
+
*
|
|
139
|
+
* @returns {Promise<{authenticated: boolean, username: string|null}>}
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const publisher = new SquadPublisher();
|
|
143
|
+
* const auth = await publisher.checkAuth();
|
|
144
|
+
* if (!auth.authenticated) {
|
|
145
|
+
* console.log('Please run: gh auth login');
|
|
146
|
+
* }
|
|
147
|
+
*/
|
|
148
|
+
async checkAuth() {
|
|
149
|
+
this._log('Checking GitHub CLI authentication');
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = execSync('gh auth status', {
|
|
153
|
+
encoding: 'utf-8',
|
|
154
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Extract username from output (supports hyphenated GitHub usernames)
|
|
158
|
+
const usernameMatch = result.match(/Logged in to .* as ([\w-]+)/);
|
|
159
|
+
const username = usernameMatch ? usernameMatch[1] : null;
|
|
160
|
+
|
|
161
|
+
this._log(`Authenticated as: ${username || 'unknown'}`);
|
|
162
|
+
return { authenticated: true, username };
|
|
163
|
+
} catch {
|
|
164
|
+
this._log('Not authenticated with GitHub CLI');
|
|
165
|
+
return { authenticated: false, username: null };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Publish squad to aios-squads repository
|
|
171
|
+
*
|
|
172
|
+
* @param {string} squadPath - Path to squad directory
|
|
173
|
+
* @param {Object} [options={}] - Publish options
|
|
174
|
+
* @param {string} [options.category='community'] - Category: 'official' or 'community'
|
|
175
|
+
* @returns {Promise<{prUrl: string, branch: string, manifest: object}>}
|
|
176
|
+
* @throws {SquadPublisherError} AUTH_REQUIRED if not authenticated
|
|
177
|
+
* @throws {SquadPublisherError} VALIDATION_FAILED if squad validation fails
|
|
178
|
+
* @throws {SquadPublisherError} SQUAD_NOT_FOUND if squad path doesn't exist
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const publisher = new SquadPublisher();
|
|
182
|
+
* const result = await publisher.publish('./squads/my-squad');
|
|
183
|
+
* console.log(`PR created: ${result.prUrl}`);
|
|
184
|
+
*
|
|
185
|
+
* // Dry run
|
|
186
|
+
* const dryPublisher = new SquadPublisher({ dryRun: true });
|
|
187
|
+
* const preview = await dryPublisher.publish('./squads/my-squad');
|
|
188
|
+
*/
|
|
189
|
+
async publish(squadPath, options = {}) {
|
|
190
|
+
const category = options.category || 'community';
|
|
191
|
+
|
|
192
|
+
this._log(`Publishing squad from: ${squadPath}`);
|
|
193
|
+
this._log(`Category: ${category}`);
|
|
194
|
+
this._log(`Dry run: ${this.dryRun}`);
|
|
195
|
+
|
|
196
|
+
// 1. Check if squad path exists
|
|
197
|
+
if (!(await this._pathExists(squadPath))) {
|
|
198
|
+
throw new SquadPublisherError(
|
|
199
|
+
PublisherErrorCodes.SQUAD_NOT_FOUND,
|
|
200
|
+
`Squad not found at: ${squadPath}`,
|
|
201
|
+
'Check the path and ensure squad exists',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 2. Validate squad
|
|
206
|
+
const validation = await this._validateSquad(squadPath);
|
|
207
|
+
if (!validation.valid) {
|
|
208
|
+
throw new SquadPublisherError(
|
|
209
|
+
PublisherErrorCodes.VALIDATION_FAILED,
|
|
210
|
+
`Squad validation failed:\n${validation.errors.map((e) => e.message).join('\n')}`,
|
|
211
|
+
'Run *validate-squad to see all issues',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 3. Load manifest
|
|
216
|
+
const manifest = await this._loadManifest(squadPath);
|
|
217
|
+
if (!manifest || !manifest.name) {
|
|
218
|
+
throw new SquadPublisherError(
|
|
219
|
+
PublisherErrorCodes.MANIFEST_ERROR,
|
|
220
|
+
'Failed to load squad manifest or missing name',
|
|
221
|
+
'Ensure squad.yaml has required fields: name, version',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const squadName = manifest.name;
|
|
226
|
+
|
|
227
|
+
// 3.1 Validate squad name is safe for shell commands
|
|
228
|
+
if (!isValidName(squadName)) {
|
|
229
|
+
throw new SquadPublisherError(
|
|
230
|
+
PublisherErrorCodes.INVALID_SQUAD_NAME,
|
|
231
|
+
`Invalid squad name: "${squadName}". Only alphanumerics, hyphens, underscores, and dots allowed.`,
|
|
232
|
+
'Update squad.yaml name field to use only safe characters',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const branchName = `squad/${squadName}`;
|
|
237
|
+
|
|
238
|
+
this._log(`Squad name: ${squadName}`);
|
|
239
|
+
this._log(`Branch: ${branchName}`);
|
|
240
|
+
|
|
241
|
+
// 4. Check GitHub auth
|
|
242
|
+
const auth = await this.checkAuth();
|
|
243
|
+
if (!auth.authenticated) {
|
|
244
|
+
throw new SquadPublisherError(
|
|
245
|
+
PublisherErrorCodes.AUTH_REQUIRED,
|
|
246
|
+
'GitHub CLI not authenticated',
|
|
247
|
+
'Run: gh auth login',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 5. Generate PR body
|
|
252
|
+
const prTitle = `Add squad: ${squadName}`;
|
|
253
|
+
const prBody = this.generatePRBody(manifest, category);
|
|
254
|
+
|
|
255
|
+
// 6. Dry run - return preview
|
|
256
|
+
if (this.dryRun) {
|
|
257
|
+
this._log('Dry run mode - not creating actual PR');
|
|
258
|
+
return {
|
|
259
|
+
prUrl: '[dry-run] PR would be created',
|
|
260
|
+
branch: branchName,
|
|
261
|
+
manifest,
|
|
262
|
+
preview: {
|
|
263
|
+
title: prTitle,
|
|
264
|
+
body: prBody,
|
|
265
|
+
repo: this.repo,
|
|
266
|
+
category,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 7. Create actual PR
|
|
272
|
+
const prUrl = await this._createPR(squadPath, manifest, branchName, prTitle, prBody);
|
|
273
|
+
|
|
274
|
+
this._log(`PR created: ${prUrl}`);
|
|
275
|
+
return { prUrl, branch: branchName, manifest };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Generate PR body with squad metadata
|
|
280
|
+
*
|
|
281
|
+
* @param {Object} manifest - Squad manifest
|
|
282
|
+
* @param {string} [category='community'] - Squad category
|
|
283
|
+
* @returns {string} Markdown-formatted PR body
|
|
284
|
+
*/
|
|
285
|
+
generatePRBody(manifest, category = 'community') {
|
|
286
|
+
const components = manifest.components || {};
|
|
287
|
+
const tasksCount = components.tasks?.length || 0;
|
|
288
|
+
const agentsCount = components.agents?.length || 0;
|
|
289
|
+
const workflowsCount = components.workflows?.length || 0;
|
|
290
|
+
const checklistsCount = components.checklists?.length || 0;
|
|
291
|
+
const templatesCount = components.templates?.length || 0;
|
|
292
|
+
|
|
293
|
+
return `## New Squad: ${manifest.name}
|
|
294
|
+
|
|
295
|
+
**Version:** ${manifest.version || '1.0.0'}
|
|
296
|
+
**Author:** ${manifest.author || 'Unknown'}
|
|
297
|
+
**Category:** ${category}
|
|
298
|
+
**Description:** ${manifest.description || 'No description provided'}
|
|
299
|
+
|
|
300
|
+
### Components
|
|
301
|
+
|
|
302
|
+
| Type | Count |
|
|
303
|
+
|------|-------|
|
|
304
|
+
| Tasks | ${tasksCount} |
|
|
305
|
+
| Agents | ${agentsCount} |
|
|
306
|
+
| Workflows | ${workflowsCount} |
|
|
307
|
+
| Checklists | ${checklistsCount} |
|
|
308
|
+
| Templates | ${templatesCount} |
|
|
309
|
+
|
|
310
|
+
### Dependencies
|
|
311
|
+
|
|
312
|
+
${manifest.dependencies?.length > 0 ? manifest.dependencies.map((d) => `- ${d}`).join('\n') : 'None specified'}
|
|
313
|
+
|
|
314
|
+
### Pre-submission Checklist
|
|
315
|
+
|
|
316
|
+
- [x] Squad follows AIOS task-first architecture
|
|
317
|
+
- [x] Documentation is complete (squad.yaml has all required fields)
|
|
318
|
+
- [x] Squad validated locally with \`*validate-squad\`
|
|
319
|
+
- [ ] No sensitive data included (API keys, credentials, etc.)
|
|
320
|
+
- [ ] All files use kebab-case naming convention
|
|
321
|
+
|
|
322
|
+
### Testing
|
|
323
|
+
|
|
324
|
+
Tested locally with:
|
|
325
|
+
\`\`\`bash
|
|
326
|
+
@squad-creator
|
|
327
|
+
*validate-squad ${manifest.name}
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
*Submitted via \`*publish-squad\` from AIOS-FullStack*`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create PR using GitHub CLI
|
|
336
|
+
* @private
|
|
337
|
+
* @param {string} squadPath - Path to squad
|
|
338
|
+
* @param {Object} manifest - Squad manifest
|
|
339
|
+
* @param {string} branchName - Branch name
|
|
340
|
+
* @param {string} title - PR title
|
|
341
|
+
* @param {string} body - PR body
|
|
342
|
+
* @returns {Promise<string>} PR URL
|
|
343
|
+
*/
|
|
344
|
+
async _createPR(squadPath, manifest, branchName, title, body) {
|
|
345
|
+
const squadName = manifest.name;
|
|
346
|
+
// Sanitize values for commit message (safety layer even though name is validated)
|
|
347
|
+
const safeVersion = sanitizeForShell(manifest.version) || '1.0.0';
|
|
348
|
+
const safeAuthor = sanitizeForShell(manifest.author) || 'Unknown';
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
// Step 1: Fork the repository (if not already forked)
|
|
352
|
+
this._log('Checking/creating fork...');
|
|
353
|
+
try {
|
|
354
|
+
// Use spawnSync with array args to prevent shell injection
|
|
355
|
+
const forkResult = spawnSync('gh', ['repo', 'fork', this.repo, '--clone=false'], {
|
|
356
|
+
encoding: 'utf-8',
|
|
357
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
358
|
+
});
|
|
359
|
+
if (forkResult.error) {
|
|
360
|
+
throw forkResult.error;
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// Fork may already exist, that's OK
|
|
364
|
+
this._log('Fork already exists or created');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Step 2: Clone the fork to a temp directory
|
|
368
|
+
const tempDir = path.join(process.cwd(), '.tmp-squad-publish');
|
|
369
|
+
await this._cleanupTemp(tempDir);
|
|
370
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
371
|
+
|
|
372
|
+
this._log(`Cloning fork to: ${tempDir}`);
|
|
373
|
+
// Use spawnSync with array args
|
|
374
|
+
const cloneResult = spawnSync('gh', ['repo', 'clone', this.repo, tempDir, '--', '--depth', '1'], {
|
|
375
|
+
encoding: 'utf-8',
|
|
376
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
377
|
+
});
|
|
378
|
+
if (cloneResult.status !== 0) {
|
|
379
|
+
throw new Error(cloneResult.stderr || 'Clone failed');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Step 3: Create branch
|
|
383
|
+
this._log(`Creating branch: ${branchName}`);
|
|
384
|
+
// Use spawnSync with array args - branchName is validated
|
|
385
|
+
const checkoutResult = spawnSync('git', ['checkout', '-b', branchName], {
|
|
386
|
+
cwd: tempDir,
|
|
387
|
+
encoding: 'utf-8',
|
|
388
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
389
|
+
});
|
|
390
|
+
if (checkoutResult.status !== 0) {
|
|
391
|
+
throw new Error(checkoutResult.stderr || 'Checkout failed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Step 4: Copy squad files
|
|
395
|
+
const targetSquadDir = path.join(tempDir, 'packages', squadName);
|
|
396
|
+
await fs.mkdir(targetSquadDir, { recursive: true });
|
|
397
|
+
await this._copyDir(squadPath, targetSquadDir);
|
|
398
|
+
|
|
399
|
+
this._log(`Copied squad to: ${targetSquadDir}`);
|
|
400
|
+
|
|
401
|
+
// Step 5: Update registry.json (add to community section)
|
|
402
|
+
const registryPath = path.join(tempDir, 'registry.json');
|
|
403
|
+
await this._updateRegistry(registryPath, manifest);
|
|
404
|
+
|
|
405
|
+
// Step 6: Commit changes
|
|
406
|
+
this._log('Committing changes...');
|
|
407
|
+
spawnSync('git', ['add', '.'], {
|
|
408
|
+
cwd: tempDir,
|
|
409
|
+
encoding: 'utf-8',
|
|
410
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Build commit message with sanitized values
|
|
414
|
+
const commitMessage = `Add squad: ${squadName}\n\nVersion: ${safeVersion}\nAuthor: ${safeAuthor}`;
|
|
415
|
+
// Use spawnSync with -m flag and message as separate arg
|
|
416
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
417
|
+
cwd: tempDir,
|
|
418
|
+
encoding: 'utf-8',
|
|
419
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
420
|
+
});
|
|
421
|
+
if (commitResult.status !== 0) {
|
|
422
|
+
throw new Error(commitResult.stderr || 'Commit failed');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Step 7: Push branch
|
|
426
|
+
this._log('Pushing to fork...');
|
|
427
|
+
// Use spawnSync with array args - branchName is validated
|
|
428
|
+
const pushResult = spawnSync('git', ['push', '-u', 'origin', branchName], {
|
|
429
|
+
cwd: tempDir,
|
|
430
|
+
encoding: 'utf-8',
|
|
431
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
432
|
+
});
|
|
433
|
+
if (pushResult.status !== 0) {
|
|
434
|
+
throw new Error(pushResult.stderr || 'Push failed');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Step 8: Create PR
|
|
438
|
+
this._log('Creating PR...');
|
|
439
|
+
const prBodyFile = path.join(tempDir, 'pr-body.md');
|
|
440
|
+
await fs.writeFile(prBodyFile, body);
|
|
441
|
+
|
|
442
|
+
// Use spawnSync with array args - title contains validated squadName
|
|
443
|
+
const prResult = spawnSync(
|
|
444
|
+
'gh',
|
|
445
|
+
['pr', 'create', '--repo', this.repo, '--title', title, '--body-file', prBodyFile, '--base', 'main'],
|
|
446
|
+
{
|
|
447
|
+
cwd: tempDir,
|
|
448
|
+
encoding: 'utf-8',
|
|
449
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
if (prResult.status !== 0) {
|
|
453
|
+
throw new Error(prResult.stderr || 'PR creation failed');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const prUrl = (prResult.stdout || '').trim();
|
|
457
|
+
|
|
458
|
+
// Step 9: Cleanup
|
|
459
|
+
await this._cleanupTemp(tempDir);
|
|
460
|
+
|
|
461
|
+
return prUrl;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw new SquadPublisherError(
|
|
464
|
+
PublisherErrorCodes.PR_ERROR,
|
|
465
|
+
`Failed to create PR: ${error.message}`,
|
|
466
|
+
'Check GitHub CLI is working: gh auth status',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Update registry.json with new squad
|
|
473
|
+
* @private
|
|
474
|
+
* @param {string} registryPath - Path to registry.json
|
|
475
|
+
* @param {Object} manifest - Squad manifest
|
|
476
|
+
*/
|
|
477
|
+
async _updateRegistry(registryPath, manifest) {
|
|
478
|
+
let registry;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const content = await fs.readFile(registryPath, 'utf-8');
|
|
482
|
+
registry = JSON.parse(content);
|
|
483
|
+
} catch {
|
|
484
|
+
// Create new registry if doesn't exist
|
|
485
|
+
registry = {
|
|
486
|
+
version: '1.0.0',
|
|
487
|
+
squads: {
|
|
488
|
+
official: [],
|
|
489
|
+
community: [],
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Ensure structure
|
|
495
|
+
if (!registry.squads) {
|
|
496
|
+
registry.squads = {};
|
|
497
|
+
}
|
|
498
|
+
if (!registry.squads.community) {
|
|
499
|
+
registry.squads.community = [];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Check if squad already exists
|
|
503
|
+
const exists = registry.squads.community.some((s) => s.name === manifest.name);
|
|
504
|
+
if (exists) {
|
|
505
|
+
// Update existing entry
|
|
506
|
+
registry.squads.community = registry.squads.community.map((s) =>
|
|
507
|
+
s.name === manifest.name
|
|
508
|
+
? {
|
|
509
|
+
name: manifest.name,
|
|
510
|
+
version: manifest.version || '1.0.0',
|
|
511
|
+
description: manifest.description || '',
|
|
512
|
+
author: manifest.author || 'Unknown',
|
|
513
|
+
}
|
|
514
|
+
: s,
|
|
515
|
+
);
|
|
516
|
+
} else {
|
|
517
|
+
// Add new entry
|
|
518
|
+
registry.squads.community.push({
|
|
519
|
+
name: manifest.name,
|
|
520
|
+
version: manifest.version || '1.0.0',
|
|
521
|
+
description: manifest.description || '',
|
|
522
|
+
author: manifest.author || 'Unknown',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Sort alphabetically
|
|
527
|
+
registry.squads.community.sort((a, b) => a.name.localeCompare(b.name));
|
|
528
|
+
|
|
529
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
530
|
+
this._log('Updated registry.json');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Validate squad
|
|
535
|
+
* @private
|
|
536
|
+
* @param {string} squadPath - Path to squad
|
|
537
|
+
* @returns {Promise<{valid: boolean, errors: Array}>}
|
|
538
|
+
*/
|
|
539
|
+
async _validateSquad(squadPath) {
|
|
540
|
+
try {
|
|
541
|
+
const { SquadValidator } = require('./squad-validator');
|
|
542
|
+
const validator = new SquadValidator({ verbose: this.verbose });
|
|
543
|
+
return await validator.validate(squadPath);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
this._log(`Validation error: ${error.message}`);
|
|
546
|
+
return {
|
|
547
|
+
valid: false,
|
|
548
|
+
errors: [{ message: `Validation failed: ${error.message}` }],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Load squad manifest
|
|
555
|
+
* @private
|
|
556
|
+
* @param {string} squadPath - Path to squad
|
|
557
|
+
* @returns {Promise<Object|null>}
|
|
558
|
+
*/
|
|
559
|
+
async _loadManifest(squadPath) {
|
|
560
|
+
try {
|
|
561
|
+
const { SquadLoader } = require('./squad-loader');
|
|
562
|
+
const loader = new SquadLoader();
|
|
563
|
+
return await loader.loadManifest(squadPath);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
this._log(`Failed to load manifest: ${error.message}`);
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Copy directory recursively
|
|
572
|
+
* @private
|
|
573
|
+
* @param {string} src - Source path
|
|
574
|
+
* @param {string} dest - Destination path
|
|
575
|
+
*/
|
|
576
|
+
async _copyDir(src, dest) {
|
|
577
|
+
await fs.mkdir(dest, { recursive: true });
|
|
578
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
579
|
+
|
|
580
|
+
for (const entry of entries) {
|
|
581
|
+
const srcPath = path.join(src, entry.name);
|
|
582
|
+
const destPath = path.join(dest, entry.name);
|
|
583
|
+
|
|
584
|
+
if (entry.isDirectory()) {
|
|
585
|
+
await this._copyDir(srcPath, destPath);
|
|
586
|
+
} else {
|
|
587
|
+
await fs.copyFile(srcPath, destPath);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Clean up temp directory
|
|
594
|
+
* @private
|
|
595
|
+
* @param {string} tempDir - Temp directory path
|
|
596
|
+
*/
|
|
597
|
+
async _cleanupTemp(tempDir) {
|
|
598
|
+
try {
|
|
599
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
600
|
+
} catch {
|
|
601
|
+
// Ignore cleanup errors
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Check if path exists
|
|
607
|
+
* @private
|
|
608
|
+
* @param {string} filePath - Path to check
|
|
609
|
+
* @returns {Promise<boolean>}
|
|
610
|
+
*/
|
|
611
|
+
async _pathExists(filePath) {
|
|
612
|
+
try {
|
|
613
|
+
await fs.access(filePath);
|
|
614
|
+
return true;
|
|
615
|
+
} catch {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
module.exports = {
|
|
622
|
+
SquadPublisher,
|
|
623
|
+
SquadPublisherError,
|
|
624
|
+
PublisherErrorCodes,
|
|
625
|
+
AIOS_SQUADS_REPO,
|
|
626
|
+
SAFE_NAME_PATTERN,
|
|
627
|
+
sanitizeForShell,
|
|
628
|
+
isValidName,
|
|
629
|
+
};
|