ultra-dex 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,475 @@
1
+ /**
2
+ * ultra-dex github command
3
+ * GitHub App integration for issue tracking, auto-PR creation, and CI/CD
4
+ * This connects Ultra-Dex to the GitHub ecosystem for true automation
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { exec as execCallback } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import inquirer from 'inquirer';
14
+
15
+ const execAsync = promisify(execCallback);
16
+
17
+ // ============================================================================
18
+ // GITHUB CONFIGURATION
19
+ // ============================================================================
20
+
21
+ const GITHUB_CONFIG = {
22
+ // State file for tracking synced issues
23
+ stateFile: '.ultra-dex/github-sync.json',
24
+
25
+ // Label mappings
26
+ labelToAgent: {
27
+ 'backend': '@Backend',
28
+ 'frontend': '@Frontend',
29
+ 'database': '@Database',
30
+ 'auth': '@Auth',
31
+ 'security': '@Security',
32
+ 'testing': '@Testing',
33
+ 'devops': '@DevOps',
34
+ 'bug': '@Debugger',
35
+ 'documentation': '@Documentation',
36
+ 'performance': '@Performance',
37
+ },
38
+
39
+ // PR template
40
+ prTemplate: `## Summary
41
+ {summary}
42
+
43
+ ## Changes
44
+ {changes}
45
+
46
+ ## Testing
47
+ {testing}
48
+
49
+ ---
50
+ šŸ¤– Generated by [Ultra-Dex](https://github.com/Srujan0798/Ultra-Dex) Agent Swarm
51
+ `,
52
+ };
53
+
54
+ // ============================================================================
55
+ // GITHUB CLI UTILITIES
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Check if GitHub CLI is installed and authenticated
60
+ */
61
+ async function checkGitHubCLI() {
62
+ try {
63
+ await execAsync('gh --version');
64
+ const { stdout } = await execAsync('gh auth status 2>&1');
65
+ return { installed: true, authenticated: stdout.includes('Logged in') };
66
+ } catch (err) {
67
+ if (err.message.includes('not found')) {
68
+ return { installed: false, authenticated: false };
69
+ }
70
+ // gh auth status returns non-zero if not authenticated
71
+ return { installed: true, authenticated: false };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get current repository info
77
+ */
78
+ async function getRepoInfo() {
79
+ try {
80
+ const { stdout } = await execAsync('gh repo view --json owner,name,url');
81
+ return JSON.parse(stdout);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * List open issues
89
+ */
90
+ async function listIssues(options = {}) {
91
+ const { limit = 20, labels = [], state = 'open' } = options;
92
+
93
+ let cmd = `gh issue list --state ${state} --limit ${limit} --json number,title,labels,body,assignees,createdAt`;
94
+
95
+ if (labels.length > 0) {
96
+ cmd += ` --label "${labels.join(',')}"`;
97
+ }
98
+
99
+ const { stdout } = await execAsync(cmd);
100
+ return JSON.parse(stdout);
101
+ }
102
+
103
+ /**
104
+ * Create a new issue
105
+ */
106
+ async function createIssue(title, body, options = {}) {
107
+ const { labels = [], assignees = [] } = options;
108
+
109
+ let cmd = `gh issue create --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`;
110
+
111
+ if (labels.length > 0) {
112
+ cmd += ` --label "${labels.join(',')}"`;
113
+ }
114
+
115
+ if (assignees.length > 0) {
116
+ cmd += ` --assignee "${assignees.join(',')}"`;
117
+ }
118
+
119
+ const { stdout } = await execAsync(cmd);
120
+ return stdout.trim();
121
+ }
122
+
123
+ /**
124
+ * Create a pull request
125
+ */
126
+ async function createPullRequest(title, body, options = {}) {
127
+ const { base = 'main', head, draft = false } = options;
128
+
129
+ let cmd = `gh pr create --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" --base ${base}`;
130
+
131
+ if (head) {
132
+ cmd += ` --head ${head}`;
133
+ }
134
+
135
+ if (draft) {
136
+ cmd += ' --draft';
137
+ }
138
+
139
+ const { stdout } = await execAsync(cmd);
140
+ return stdout.trim();
141
+ }
142
+
143
+ /**
144
+ * Get PR status
145
+ */
146
+ async function getPRStatus(prNumber) {
147
+ const { stdout } = await execAsync(`gh pr view ${prNumber} --json state,mergeable,reviews,statusCheckRollup`);
148
+ return JSON.parse(stdout);
149
+ }
150
+
151
+ /**
152
+ * List PRs
153
+ */
154
+ async function listPRs(options = {}) {
155
+ const { state = 'open', limit = 10 } = options;
156
+
157
+ const { stdout } = await execAsync(`gh pr list --state ${state} --limit ${limit} --json number,title,headRefName,state,createdAt`);
158
+ return JSON.parse(stdout);
159
+ }
160
+
161
+ // ============================================================================
162
+ // ISSUE → TASK CONVERSION
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Convert GitHub issue to Ultra-Dex task format
167
+ */
168
+ function issueToTask(issue) {
169
+ // Detect agent from labels
170
+ let agent = '@Planner'; // Default
171
+ for (const label of issue.labels || []) {
172
+ const labelName = label.name?.toLowerCase() || label.toLowerCase();
173
+ if (GITHUB_CONFIG.labelToAgent[labelName]) {
174
+ agent = GITHUB_CONFIG.labelToAgent[labelName];
175
+ break;
176
+ }
177
+ }
178
+
179
+ return {
180
+ id: `gh-${issue.number}`,
181
+ source: 'github',
182
+ issueNumber: issue.number,
183
+ title: issue.title,
184
+ description: issue.body || '',
185
+ agent,
186
+ labels: (issue.labels || []).map(l => l.name || l),
187
+ createdAt: issue.createdAt,
188
+ status: 'pending',
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Sync issues to local task file
194
+ */
195
+ async function syncIssuesToTasks(workdir = process.cwd()) {
196
+ const stateFile = path.join(workdir, GITHUB_CONFIG.stateFile);
197
+
198
+ // Load existing state
199
+ let state = { syncedIssues: {}, lastSync: null };
200
+ try {
201
+ state = JSON.parse(await fs.readFile(stateFile, 'utf8'));
202
+ } catch {
203
+ // No existing state
204
+ }
205
+
206
+ // Fetch open issues
207
+ const issues = await listIssues({ limit: 50 });
208
+
209
+ // Convert to tasks
210
+ const tasks = issues.map(issueToTask);
211
+
212
+ // Track which issues we've synced
213
+ const newTasks = [];
214
+ for (const task of tasks) {
215
+ if (!state.syncedIssues[task.id]) {
216
+ newTasks.push(task);
217
+ state.syncedIssues[task.id] = {
218
+ syncedAt: new Date().toISOString(),
219
+ issueNumber: task.issueNumber,
220
+ };
221
+ }
222
+ }
223
+
224
+ // Save state
225
+ state.lastSync = new Date().toISOString();
226
+ await fs.mkdir(path.dirname(stateFile), { recursive: true });
227
+ await fs.writeFile(stateFile, JSON.stringify(state, null, 2));
228
+
229
+ return { all: tasks, new: newTasks };
230
+ }
231
+
232
+ // ============================================================================
233
+ // AUTO-PR CREATION
234
+ // ============================================================================
235
+
236
+ /**
237
+ * Create PR from swarm output
238
+ */
239
+ async function createPRFromSwarm(swarmResult, options = {}) {
240
+ const { branch = null, title = null, draft = true } = options;
241
+
242
+ // Generate branch name
243
+ const branchName = branch || `ultra-dex/${Date.now()}`;
244
+
245
+ // Create branch
246
+ await execAsync(`git checkout -b ${branchName}`);
247
+
248
+ // Stage all changes
249
+ await execAsync('git add -A');
250
+
251
+ // Commit
252
+ const commitMsg = swarmResult.goal || 'Ultra-Dex agent swarm implementation';
253
+ await execAsync(`git commit -m "${commitMsg}"`);
254
+
255
+ // Push
256
+ await execAsync(`git push -u origin ${branchName}`);
257
+
258
+ // Generate PR body
259
+ const prBody = GITHUB_CONFIG.prTemplate
260
+ .replace('{summary}', swarmResult.goal || 'Automated implementation')
261
+ .replace('{changes}', swarmResult.artifacts?.join('\n- ') || 'See commits for details')
262
+ .replace('{testing}', '- [ ] Manual testing\n- [ ] Automated tests pass');
263
+
264
+ // Create PR
265
+ const prTitle = title || `šŸ¤– ${commitMsg}`;
266
+ const prUrl = await createPullRequest(prTitle, prBody, { head: branchName, draft });
267
+
268
+ return { branch: branchName, prUrl };
269
+ }
270
+
271
+ // ============================================================================
272
+ // WEBHOOK HANDLER (for CI integration)
273
+ // ============================================================================
274
+
275
+ /**
276
+ * Parse GitHub webhook payload
277
+ */
278
+ function parseWebhook(payload) {
279
+ if (payload.action === 'opened' && payload.issue) {
280
+ return {
281
+ type: 'issue_opened',
282
+ issue: issueToTask(payload.issue),
283
+ };
284
+ }
285
+
286
+ if (payload.action === 'completed' && payload.workflow_run) {
287
+ return {
288
+ type: 'workflow_completed',
289
+ workflow: payload.workflow_run,
290
+ success: payload.workflow_run.conclusion === 'success',
291
+ };
292
+ }
293
+
294
+ if (payload.action === 'submitted' && payload.review) {
295
+ return {
296
+ type: 'pr_review',
297
+ pr: payload.pull_request,
298
+ review: payload.review,
299
+ approved: payload.review.state === 'approved',
300
+ };
301
+ }
302
+
303
+ return { type: 'unknown', payload };
304
+ }
305
+
306
+ // ============================================================================
307
+ // CLI COMMAND
308
+ // ============================================================================
309
+
310
+ export function registerGitHubCommand(program) {
311
+ program
312
+ .command('github')
313
+ .description('GitHub integration for issues, PRs, and CI/CD')
314
+ .option('--sync', 'Sync GitHub issues to local tasks')
315
+ .option('--issues', 'List open issues')
316
+ .option('--prs', 'List open pull requests')
317
+ .option('--create-issue <title>', 'Create a new issue')
318
+ .option('--create-pr', 'Create PR from current changes')
319
+ .option('--status', 'Check GitHub CLI status')
320
+ .option('--labels <labels>', 'Filter by labels (comma-separated)')
321
+ .option('--draft', 'Create PR as draft')
322
+ .action(async (options) => {
323
+ console.log(chalk.cyan('\nšŸ™ Ultra-Dex GitHub Integration\n'));
324
+
325
+ // Check GitHub CLI
326
+ const spinner = ora('Checking GitHub CLI...').start();
327
+ const ghStatus = await checkGitHubCLI();
328
+
329
+ if (!ghStatus.installed) {
330
+ spinner.fail('GitHub CLI (gh) not installed');
331
+ console.log(chalk.yellow('\nInstall: https://cli.github.com/'));
332
+ return;
333
+ }
334
+
335
+ if (!ghStatus.authenticated) {
336
+ spinner.fail('Not authenticated with GitHub');
337
+ console.log(chalk.yellow('\nRun: gh auth login'));
338
+ return;
339
+ }
340
+
341
+ spinner.succeed('GitHub CLI ready');
342
+
343
+ // Get repo info
344
+ const repo = await getRepoInfo();
345
+ if (!repo) {
346
+ console.log(chalk.yellow('\nāš ļø Not in a GitHub repository'));
347
+ return;
348
+ }
349
+
350
+ console.log(chalk.gray(`Repository: ${repo.owner.login}/${repo.name}\n`));
351
+
352
+ try {
353
+ if (options.status) {
354
+ // Just show status (already done above)
355
+ console.log(chalk.green('āœ… GitHub integration active'));
356
+ return;
357
+ }
358
+
359
+ if (options.issues) {
360
+ // List issues
361
+ spinner.start('Fetching issues...');
362
+ const labelFilter = options.labels?.split(',') || [];
363
+ const issues = await listIssues({ labels: labelFilter });
364
+ spinner.succeed(`Found ${issues.length} open issues\n`);
365
+
366
+ if (issues.length === 0) {
367
+ console.log(chalk.gray('No open issues found.'));
368
+ return;
369
+ }
370
+
371
+ for (const issue of issues) {
372
+ const labels = (issue.labels || []).map(l => chalk.cyan(`[${l.name}]`)).join(' ');
373
+ console.log(`#${chalk.bold(issue.number)} ${issue.title} ${labels}`);
374
+ }
375
+ return;
376
+ }
377
+
378
+ if (options.prs) {
379
+ // List PRs
380
+ spinner.start('Fetching pull requests...');
381
+ const prs = await listPRs();
382
+ spinner.succeed(`Found ${prs.length} open PRs\n`);
383
+
384
+ for (const pr of prs) {
385
+ console.log(`#${chalk.bold(pr.number)} ${pr.title} ${chalk.gray(`(${pr.headRefName})`)}`);
386
+ }
387
+ return;
388
+ }
389
+
390
+ if (options.sync) {
391
+ // Sync issues to tasks
392
+ spinner.start('Syncing issues to tasks...');
393
+ const result = await syncIssuesToTasks();
394
+ spinner.succeed(`Synced ${result.all.length} issues (${result.new.length} new)\n`);
395
+
396
+ if (result.new.length > 0) {
397
+ console.log(chalk.bold('New tasks:'));
398
+ for (const task of result.new) {
399
+ console.log(` ${task.agent} #${task.issueNumber}: ${task.title}`);
400
+ }
401
+ }
402
+ return;
403
+ }
404
+
405
+ if (options.createIssue) {
406
+ // Create issue
407
+ const { body } = await inquirer.prompt([{
408
+ type: 'editor',
409
+ name: 'body',
410
+ message: 'Issue description:',
411
+ }]);
412
+
413
+ spinner.start('Creating issue...');
414
+ const url = await createIssue(options.createIssue, body);
415
+ spinner.succeed(`Issue created: ${url}`);
416
+ return;
417
+ }
418
+
419
+ if (options.createPr) {
420
+ // Create PR from current changes
421
+ const { title, description } = await inquirer.prompt([
422
+ { type: 'input', name: 'title', message: 'PR title:' },
423
+ { type: 'editor', name: 'description', message: 'PR description:' },
424
+ ]);
425
+
426
+ spinner.start('Creating pull request...');
427
+
428
+ const prBody = GITHUB_CONFIG.prTemplate
429
+ .replace('{summary}', description)
430
+ .replace('{changes}', 'See commits for details')
431
+ .replace('{testing}', '- [ ] Tests pass\n- [ ] Manual testing');
432
+
433
+ const url = await createPullRequest(title, prBody, { draft: options.draft });
434
+ spinner.succeed(`Pull request created: ${url}`);
435
+ return;
436
+ }
437
+
438
+ // Default: show menu
439
+ const { action } = await inquirer.prompt([{
440
+ type: 'list',
441
+ name: 'action',
442
+ message: 'What would you like to do?',
443
+ choices: [
444
+ { name: 'šŸ“‹ List open issues', value: 'issues' },
445
+ { name: 'šŸ”€ List open PRs', value: 'prs' },
446
+ { name: 'šŸ”„ Sync issues to tasks', value: 'sync' },
447
+ { name: 'āž• Create new issue', value: 'create-issue' },
448
+ { name: 'šŸš€ Create PR from changes', value: 'create-pr' },
449
+ { name: 'āŒ Cancel', value: 'cancel' },
450
+ ],
451
+ }]);
452
+
453
+ // Recurse with selected action
454
+ if (action !== 'cancel') {
455
+ const newOptions = { ...options, [action.replace('-', '')]: true };
456
+ await registerGitHubCommand(program).action(newOptions);
457
+ }
458
+
459
+ } catch (err) {
460
+ spinner.fail(`Failed: ${err.message}`);
461
+ }
462
+ });
463
+ }
464
+
465
+ export default {
466
+ registerGitHubCommand,
467
+ checkGitHubCLI,
468
+ listIssues,
469
+ listPRs,
470
+ createIssue,
471
+ createPullRequest,
472
+ createPRFromSwarm,
473
+ syncIssuesToTasks,
474
+ parseWebhook,
475
+ };