@oss-autopilot/core 0.44.0 → 0.44.2

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/dist/cli.js CHANGED
@@ -1,824 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * OSS Autopilot CLI
4
- * Entry point with commander for argument parsing
4
+ * Entry point with commander for argument parsing.
5
5
  *
6
- * Supports --json flag for structured output (used by Claude Code plugin)
7
- *
8
- * Performance: Command modules are lazy-loaded via dynamic import() so that
9
- * only the invoked command's code is evaluated. The preAction hook uses an
10
- * async token fetch to avoid blocking the event loop on `gh auth token`.
6
+ * Command definitions live in cli-registry.ts each declares its name,
7
+ * localOnly flag (skip GitHub token check), and a register function.
8
+ * Heavy command modules are lazy-loaded via dynamic import() in action
9
+ * handlers so only the invoked command's dependencies are evaluated.
11
10
  */
12
11
  import { Command } from 'commander';
13
- import { getGitHubTokenAsync, enableDebug, debug, formatRelativeTime, getCLIVersion } from './core/index.js';
14
- import { errorMessage } from './core/errors.js';
15
- import { outputJson, outputJsonError } from './formatters/json.js';
16
- /** Print local repos in human-readable format */
17
- function printRepos(repos) {
18
- const entries = Object.entries(repos).sort(([a], [b]) => a.localeCompare(b));
19
- for (const [remote, info] of entries) {
20
- const branch = info.currentBranch ? ` (${info.currentBranch})` : '';
21
- console.log(` ${remote}${branch}`);
22
- console.log(` ${info.path}`);
23
- }
24
- }
25
- /** Shared error handler for CLI action catch blocks. */
26
- function handleCommandError(err, json) {
27
- const msg = errorMessage(err);
28
- if (json) {
29
- outputJsonError(msg);
30
- }
31
- else {
32
- console.error(`Error: ${msg}`);
33
- }
34
- process.exit(1);
35
- }
12
+ import { getGitHubTokenAsync, enableDebug, debug, getCLIVersion } from './core/index.js';
13
+ import { commands } from './cli-registry.js';
36
14
  const VERSION = getCLIVersion();
37
- // Commands that skip the preAction GitHub token check.
38
- // startup handles auth internally (returns authError in JSON instead of process.exit).
39
- const LOCAL_ONLY_COMMANDS = [
40
- 'help',
41
- 'status',
42
- 'config',
43
- 'read',
44
- 'untrack',
45
- 'version',
46
- 'setup',
47
- 'checkSetup',
48
- 'serve',
49
- 'parse-issue-list',
50
- 'check-integration',
51
- 'local-repos',
52
- 'startup',
53
- 'shelve',
54
- 'unshelve',
55
- 'dismiss',
56
- 'undismiss',
57
- 'snooze',
58
- 'unsnooze',
59
- ];
60
15
  const program = new Command();
61
16
  program
62
17
  .name('oss-autopilot')
63
18
  .description('AI-powered autopilot for managing open source contributions')
64
19
  .version(VERSION)
65
20
  .option('--debug', 'Enable debug logging');
66
- // Daily check command
67
- program
68
- .command('daily')
69
- .description('Run daily check on all tracked PRs')
70
- .option('--json', 'Output as JSON')
71
- .action(async (options) => {
72
- try {
73
- if (options.json) {
74
- const { runDaily } = await import('./commands/daily.js');
75
- const data = await runDaily();
76
- outputJson(data);
77
- }
78
- else {
79
- const { runDailyForDisplay, printDigest } = await import('./commands/daily.js');
80
- const result = await runDailyForDisplay();
81
- printDigest(result.digest, result.capacity, result.commentedIssues);
82
- }
83
- }
84
- catch (err) {
85
- handleCommandError(err, options.json);
86
- }
87
- });
88
- // Status command
89
- program
90
- .command('status')
91
- .description('Show current status and stats')
92
- .option('--json', 'Output as JSON')
93
- .option('--offline', 'Use cached data only (no GitHub API calls)')
94
- .action(async (options) => {
95
- try {
96
- const { runStatus } = await import('./commands/status.js');
97
- const data = await runStatus({ offline: options.offline });
98
- if (options.json) {
99
- outputJson(data);
100
- }
101
- else {
102
- console.log('\n\ud83d\udcca OSS Status\n');
103
- console.log(`Merged PRs: ${data.stats.mergedPRs}`);
104
- console.log(`Closed PRs: ${data.stats.closedPRs}`);
105
- console.log(`Merge Rate: ${data.stats.mergeRate}`);
106
- console.log(`Needs Response: ${data.stats.needsResponse}`);
107
- if (data.offline) {
108
- console.log(`\nLast Updated: ${data.lastUpdated || 'Never'}`);
109
- console.log('(Offline mode: showing cached data)');
110
- }
111
- else {
112
- console.log(`\nLast Run: ${data.lastRunAt || 'Never'}`);
113
- }
114
- console.log('\nRun with --json for structured output');
115
- }
116
- }
117
- catch (err) {
118
- handleCommandError(err, options.json);
119
- }
120
- });
121
- // Search command
122
- program
123
- .command('search [count]')
124
- .description('Search for new issues to work on')
125
- .option('--json', 'Output as JSON')
126
- .action(async (count, options) => {
127
- try {
128
- const { runSearch } = await import('./commands/search.js');
129
- let maxResults = 5;
130
- if (count !== undefined) {
131
- const parsed = Number(count);
132
- if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
133
- throw new Error(`Invalid count "${count}". Must be a positive integer.`);
134
- }
135
- maxResults = parsed;
136
- }
137
- if (!options.json) {
138
- console.log(`\nSearching for issues (max ${maxResults})...\n`);
139
- }
140
- const data = await runSearch({ maxResults });
141
- if (options.json) {
142
- outputJson(data);
143
- }
144
- else {
145
- if (data.candidates.length === 0) {
146
- if (data.rateLimitWarning) {
147
- console.warn(`\n${data.rateLimitWarning}\n`);
148
- }
149
- else {
150
- console.log('No matching issues found.');
151
- }
152
- return;
153
- }
154
- if (data.rateLimitWarning) {
155
- console.warn(`\n${data.rateLimitWarning}\n`);
156
- }
157
- console.log(`Found ${data.candidates.length} candidates:\n`);
158
- for (const candidate of data.candidates) {
159
- // Simple text format for candidates
160
- const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
161
- console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
162
- console.log(` URL: ${issue.url}`);
163
- console.log(` Viability: ${viabilityScore}/100`);
164
- if (reasonsToApprove.length > 0)
165
- console.log(` Approve: ${reasonsToApprove.join(', ')}`);
166
- if (reasonsToSkip.length > 0)
167
- console.log(` Skip: ${reasonsToSkip.join(', ')}`);
168
- console.log('---');
169
- }
170
- }
171
- }
172
- catch (err) {
173
- handleCommandError(err, options.json);
174
- }
175
- });
176
- // Vet command
177
- program
178
- .command('vet <issue-url>')
179
- .description('Vet a specific issue before working on it')
180
- .option('--json', 'Output as JSON')
181
- .action(async (issueUrl, options) => {
182
- try {
183
- const { runVet } = await import('./commands/vet.js');
184
- const data = await runVet({ issueUrl });
185
- if (options.json) {
186
- outputJson(data);
187
- }
188
- else {
189
- const { issue, recommendation, reasonsToApprove, reasonsToSkip } = data;
190
- console.log(`\nVetting issue: ${issueUrl}\n`);
191
- console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
192
- console.log(` URL: ${issue.url}`);
193
- if (reasonsToApprove.length > 0)
194
- console.log(` Approve: ${reasonsToApprove.join(', ')}`);
195
- if (reasonsToSkip.length > 0)
196
- console.log(` Skip: ${reasonsToSkip.join(', ')}`);
197
- }
198
- }
199
- catch (err) {
200
- handleCommandError(err, options.json);
201
- }
202
- });
203
- // Track command
204
- program
205
- .command('track <pr-url>')
206
- .description('Add a PR to track')
207
- .option('--json', 'Output as JSON')
208
- .action(async (prUrl, options) => {
209
- try {
210
- const { runTrack } = await import('./commands/track.js');
211
- const data = await runTrack({ prUrl });
212
- if (options.json) {
213
- outputJson(data);
214
- }
215
- else {
216
- console.log(`\nPR: ${data.pr.repo}#${data.pr.number} - ${data.pr.title}`);
217
- console.log('Note: In v2, PRs are tracked automatically via the daily run.');
218
- }
219
- }
220
- catch (err) {
221
- handleCommandError(err, options.json);
222
- }
223
- });
224
- // Untrack command
225
- program
226
- .command('untrack <pr-url>')
227
- .description('Stop tracking a PR')
228
- .option('--json', 'Output as JSON')
229
- .action(async (prUrl, options) => {
230
- try {
231
- const { runUntrack } = await import('./commands/track.js');
232
- const data = await runUntrack({ prUrl });
233
- if (options.json) {
234
- outputJson(data);
235
- }
236
- else {
237
- console.log('Note: In v2, PRs are fetched fresh on each daily run \u2014 there is no local tracking list to remove from.');
238
- console.log('Use `shelve` to temporarily hide a PR from the daily summary.');
239
- }
240
- }
241
- catch (err) {
242
- handleCommandError(err, options.json);
243
- }
244
- });
245
- // Read command (mark as read)
246
- program
247
- .command('read [pr-url]')
248
- .description('Mark PR comments as read')
249
- .option('--all', 'Mark all PRs as read')
250
- .option('--json', 'Output as JSON')
251
- .action(async (prUrl, options) => {
252
- try {
253
- const { runRead } = await import('./commands/read.js');
254
- const data = await runRead({ prUrl, all: options.all });
255
- if (options.json) {
256
- outputJson(data);
257
- }
258
- else {
259
- console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
260
- }
261
- }
262
- catch (err) {
263
- handleCommandError(err, options.json);
264
- }
265
- });
266
- // Comments command
267
- program
268
- .command('comments <pr-url>')
269
- .description('Show all comments on a PR')
270
- .option('--bots', 'Include bot comments')
271
- .option('--json', 'Output as JSON')
272
- .action(async (prUrl, options) => {
273
- try {
274
- const { runComments } = await import('./commands/comments.js');
275
- const data = await runComments({ prUrl, showBots: options.bots });
276
- if (options.json) {
277
- outputJson(data);
278
- }
279
- else {
280
- // Text output
281
- console.log(`\nFetching comments for: ${prUrl}\n`);
282
- console.log(`## ${data.pr.title}\n`);
283
- console.log(`**Status:** ${data.pr.state} | **Mergeable:** ${data.pr.mergeable ?? 'checking...'}`);
284
- console.log(`**Branch:** ${data.pr.head} -> ${data.pr.base}`);
285
- console.log(`**URL:** ${data.pr.url}\n`);
286
- const REVIEW_STATE_LABELS = {
287
- APPROVED: '[Approved]',
288
- CHANGES_REQUESTED: '[Changes]',
289
- };
290
- if (data.reviews.length > 0) {
291
- console.log('### Reviews (newest first)\n');
292
- for (const review of data.reviews) {
293
- const state = REVIEW_STATE_LABELS[review.state] ?? '[Comment]';
294
- const time = review.submittedAt ? formatRelativeTime(review.submittedAt) : '';
295
- console.log(`${state} **@${review.user}** (${review.state}) - ${time}`);
296
- if (review.body) {
297
- console.log(`> ${review.body.split('\n').join('\n> ')}\n`);
298
- }
299
- }
300
- }
301
- if (data.reviewComments.length > 0) {
302
- console.log('### Inline Comments (newest first)\n');
303
- for (const comment of data.reviewComments) {
304
- const time = formatRelativeTime(comment.createdAt);
305
- console.log(`**@${comment.user}** on \`${comment.path}\` - ${time}`);
306
- console.log(`> ${comment.body.split('\n').join('\n> ')}`);
307
- console.log('');
308
- }
309
- }
310
- if (data.issueComments.length > 0) {
311
- console.log('### Discussion (newest first)\n');
312
- for (const comment of data.issueComments) {
313
- const time = formatRelativeTime(comment.createdAt);
314
- console.log(`**@${comment.user}** - ${time}`);
315
- console.log(`> ${comment.body?.split('\n').join('\n> ')}\n`);
316
- }
317
- }
318
- if (data.reviewComments.length === 0 && data.issueComments.length === 0 && data.reviews.length === 0) {
319
- console.log('No comments from other users.\n');
320
- }
321
- console.log('---');
322
- console.log(`**Summary:** ${data.summary.reviewCount} reviews, ${data.summary.inlineCommentCount} inline comments, ${data.summary.discussionCommentCount} discussion comments`);
323
- }
324
- }
325
- catch (err) {
326
- handleCommandError(err, options.json);
327
- }
328
- });
329
- // Post command
330
- program
331
- .command('post <url> [message...]')
332
- .description('Post a comment to a PR or issue')
333
- .option('--stdin', 'Read message from stdin')
334
- .option('--json', 'Output as JSON')
335
- .action(async (url, messageParts, options) => {
336
- try {
337
- let message;
338
- if (options.stdin) {
339
- const chunks = [];
340
- for await (const chunk of process.stdin) {
341
- chunks.push(chunk);
342
- }
343
- message = Buffer.concat(chunks).toString('utf-8').trim();
344
- }
345
- else {
346
- message = messageParts.join(' ');
347
- }
348
- const { runPost } = await import('./commands/comments.js');
349
- const data = await runPost({ url, message });
350
- if (options.json) {
351
- outputJson(data);
352
- }
353
- else {
354
- console.log(`Comment posted: ${data.commentUrl}`);
355
- }
356
- }
357
- catch (err) {
358
- handleCommandError(err, options.json);
359
- }
360
- });
361
- // Claim command
362
- program
363
- .command('claim <issue-url> [message...]')
364
- .description('Claim an issue by posting a comment')
365
- .option('--json', 'Output as JSON')
366
- .action(async (issueUrl, messageParts, options) => {
367
- try {
368
- const { runClaim } = await import('./commands/comments.js');
369
- const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
370
- const data = await runClaim({ issueUrl, message });
371
- if (options.json) {
372
- outputJson(data);
373
- }
374
- else {
375
- console.log(`Issue claimed: ${data.commentUrl}`);
376
- }
377
- }
378
- catch (err) {
379
- handleCommandError(err, options.json);
380
- }
381
- });
382
- // Config command
383
- program
384
- .command('config [key] [value]')
385
- .description('Show or update configuration')
386
- .option('--json', 'Output as JSON')
387
- .action(async (key, value, options) => {
388
- try {
389
- const { runConfig } = await import('./commands/config.js');
390
- const data = await runConfig({ key, value });
391
- if (options.json) {
392
- outputJson(data);
393
- }
394
- else if ('config' in data) {
395
- console.log('\n\u2699\ufe0f Current Configuration:\n');
396
- console.log(JSON.stringify(data.config, null, 2));
397
- }
398
- else {
399
- console.log(`Set ${data.key} to: ${data.value}`);
400
- }
401
- }
402
- catch (err) {
403
- handleCommandError(err, options.json);
404
- }
405
- });
406
- // Init command
407
- program
408
- .command('init <username>')
409
- .description('Initialize with your GitHub username and import open PRs')
410
- .option('--json', 'Output as JSON')
411
- .action(async (username, options) => {
412
- try {
413
- const { runInit } = await import('./commands/init.js');
414
- const data = await runInit({ username });
415
- if (options.json) {
416
- outputJson(data);
417
- }
418
- else {
419
- console.log(`\nUsername set to @${data.username}.`);
420
- console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
421
- }
422
- }
423
- catch (err) {
424
- handleCommandError(err, options.json);
425
- }
426
- });
427
- // Setup command
428
- program
429
- .command('setup')
430
- .description('Interactive setup / configuration')
431
- .option('--reset', 'Re-run setup even if already complete')
432
- .option('--set <settings...>', 'Set specific values (key=value)')
433
- .option('--json', 'Output as JSON')
434
- .action(async (options) => {
435
- try {
436
- const { runSetup } = await import('./commands/setup.js');
437
- const data = await runSetup({ reset: options.reset, set: options.set });
438
- if (options.json) {
439
- outputJson(data);
440
- }
441
- else if ('success' in data) {
442
- // --set mode
443
- for (const [key, value] of Object.entries(data.settings)) {
444
- console.log(`\u2713 ${key}: ${value}`);
445
- }
446
- if (data.warnings) {
447
- for (const w of data.warnings) {
448
- console.warn(w);
449
- }
450
- }
451
- }
452
- else if ('setupComplete' in data && data.setupComplete) {
453
- // Already complete
454
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
455
- console.log('\u2713 Setup already complete!\n');
456
- console.log('Current settings:');
457
- console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
458
- console.log(` Max active PRs: ${data.config.maxActivePRs}`);
459
- console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
460
- console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
461
- console.log(` Languages: ${data.config.languages.join(', ')}`);
462
- console.log(` Labels: ${data.config.labels.join(', ')}`);
463
- console.log(`\nRun 'setup --reset' to reconfigure.`);
464
- }
465
- else if ('setupRequired' in data) {
466
- // Needs setup
467
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
468
- console.log('SETUP_REQUIRED');
469
- console.log('---');
470
- console.log('Please configure the following settings:\n');
471
- for (const prompt of data.prompts) {
472
- console.log(`SETTING: ${prompt.setting}`);
473
- console.log(`PROMPT: ${prompt.prompt}`);
474
- const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
475
- console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
476
- if (prompt.required)
477
- console.log('REQUIRED: true');
478
- if (prompt.default !== undefined) {
479
- const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
480
- console.log(`DEFAULT: ${defaultVal}`);
481
- }
482
- if (prompt.type)
483
- console.log(`TYPE: ${prompt.type}`);
484
- console.log('');
485
- }
486
- console.log('---');
487
- console.log('END_SETUP_PROMPTS');
488
- }
489
- }
490
- catch (err) {
491
- handleCommandError(err, options.json);
492
- }
493
- });
494
- // Check setup command
495
- program
496
- .command('checkSetup')
497
- .description('Check if setup is complete')
498
- .option('--json', 'Output as JSON')
499
- .action(async (options) => {
500
- try {
501
- const { runCheckSetup } = await import('./commands/setup.js');
502
- const data = await runCheckSetup();
503
- if (options.json) {
504
- outputJson(data);
505
- }
506
- else if (data.setupComplete) {
507
- console.log('SETUP_COMPLETE');
508
- console.log(`username=${data.username}`);
509
- }
510
- else {
511
- console.log('SETUP_INCOMPLETE');
512
- }
513
- }
514
- catch (err) {
515
- handleCommandError(err, options.json);
516
- }
517
- });
518
- // Dashboard commands
519
- const dashboardCmd = program.command('dashboard').description('Dashboard commands');
520
- dashboardCmd
521
- .command('serve')
522
- .description('Start interactive dashboard server')
523
- .option('--port <port>', 'Port to listen on', '3000')
524
- .option('--no-open', 'Do not open browser automatically')
525
- .action(async (options) => {
526
- try {
527
- const port = parseInt(options.port, 10);
528
- if (isNaN(port) || port < 1 || port > 65535) {
529
- console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
530
- process.exit(1);
531
- }
532
- const { serveDashboard } = await import('./commands/dashboard.js');
533
- await serveDashboard({ port, open: options.open });
534
- }
535
- catch (err) {
536
- handleCommandError(err);
537
- }
538
- });
539
- // Parse issue list command (#82)
540
- program
541
- .command('parse-issue-list <path>')
542
- .description('Parse a markdown issue list into structured JSON')
543
- .option('--json', 'Output as JSON')
544
- .action(async (filePath, options) => {
545
- try {
546
- const { runParseList } = await import('./commands/parse-list.js');
547
- const data = await runParseList({ filePath });
548
- if (options.json) {
549
- outputJson(data);
550
- }
551
- else {
552
- const path = await import('path');
553
- const resolvedPath = path.resolve(filePath);
554
- console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
555
- console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
556
- if (data.available.length > 0) {
557
- console.log('--- Available ---');
558
- for (const item of data.available) {
559
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
560
- }
561
- }
562
- if (data.completed.length > 0) {
563
- console.log('\n--- Completed ---');
564
- for (const item of data.completed) {
565
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
566
- }
567
- }
568
- }
569
- }
570
- catch (err) {
571
- handleCommandError(err, options.json);
572
- }
573
- });
574
- // Check integration command (#83)
575
- program
576
- .command('check-integration')
577
- .description('Detect new files not referenced by the codebase')
578
- .option('--base <branch>', 'Base branch to compare against', 'main')
579
- .option('--json', 'Output as JSON')
580
- .action(async (options) => {
581
- try {
582
- const { runCheckIntegration } = await import('./commands/check-integration.js');
583
- const data = await runCheckIntegration({ base: options.base });
584
- if (options.json) {
585
- outputJson(data);
586
- }
587
- else if (data.newFiles.length === 0) {
588
- console.log('\nNo new code files to check.');
589
- }
590
- else {
591
- console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
592
- console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
593
- for (const file of data.newFiles) {
594
- const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
595
- console.log(`${status} ${file.path}`);
596
- if (file.isIntegrated) {
597
- console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
598
- }
599
- else {
600
- console.log(' Not referenced by any file');
601
- if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
602
- console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
603
- }
604
- }
605
- }
606
- }
607
- }
608
- catch (err) {
609
- handleCommandError(err, options.json);
610
- }
611
- });
612
- // Local repos command (#84)
613
- program
614
- .command('local-repos')
615
- .description('Scan filesystem for local git clones')
616
- .option('--scan', 'Force re-scan (ignores cache)')
617
- .option('--paths <dirs...>', 'Directories to scan')
618
- .option('--json', 'Output as JSON')
619
- .action(async (options) => {
620
- try {
621
- const { runLocalRepos } = await import('./commands/local-repos.js');
622
- const data = await runLocalRepos({ scan: options.scan, paths: options.paths });
623
- if (options.json) {
624
- outputJson(data);
625
- }
626
- else if (data.fromCache) {
627
- console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
628
- printRepos(data.repos);
629
- }
630
- else {
631
- console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
632
- printRepos(data.repos);
633
- }
634
- }
635
- catch (err) {
636
- handleCommandError(err, options.json);
637
- }
638
- });
639
- // Startup command (combines auth, setup, daily, dashboard, issue list)
640
- program
641
- .command('startup')
642
- .description('Run all pre-flight checks and daily fetch in one call')
643
- .option('--json', 'Output as JSON')
644
- .action(async (options) => {
645
- try {
646
- const { runStartup } = await import('./commands/startup.js');
647
- const data = await runStartup();
648
- if (options.json) {
649
- outputJson(data);
650
- }
651
- else {
652
- if (!data.setupComplete) {
653
- console.log('Setup incomplete. Run /setup-oss first.');
654
- }
655
- else if (data.authError) {
656
- console.error(`Error: ${data.authError}`);
657
- }
658
- else {
659
- console.log(`OSS Autopilot v${data.version}`);
660
- console.log(data.daily?.briefSummary ?? '');
661
- }
662
- }
663
- }
664
- catch (err) {
665
- handleCommandError(err, options.json);
666
- }
667
- });
668
- // Shelve command
669
- program
670
- .command('shelve <pr-url>')
671
- .description('Shelve a PR (exclude from capacity and actionable issues)')
672
- .option('--json', 'Output as JSON')
673
- .action(async (prUrl, options) => {
674
- try {
675
- const { runShelve } = await import('./commands/shelve.js');
676
- const data = await runShelve({ prUrl });
677
- if (options.json) {
678
- outputJson(data);
679
- }
680
- else if (data.shelved) {
681
- console.log(`Shelved: ${prUrl}`);
682
- console.log('This PR is now excluded from capacity and actionable issues.');
683
- console.log('It will auto-unshelve if a maintainer engages.');
684
- }
685
- else {
686
- console.log('PR is already shelved.');
687
- }
688
- }
689
- catch (err) {
690
- handleCommandError(err, options.json);
691
- }
692
- });
693
- // Unshelve command
694
- program
695
- .command('unshelve <pr-url>')
696
- .description('Unshelve a PR (include in capacity and actionable issues again)')
697
- .option('--json', 'Output as JSON')
698
- .action(async (prUrl, options) => {
699
- try {
700
- const { runUnshelve } = await import('./commands/shelve.js');
701
- const data = await runUnshelve({ prUrl });
702
- if (options.json) {
703
- outputJson(data);
704
- }
705
- else if (data.unshelved) {
706
- console.log(`Unshelved: ${prUrl}`);
707
- console.log('This PR is now active again.');
708
- }
709
- else {
710
- console.log('PR was not shelved.');
711
- }
712
- }
713
- catch (err) {
714
- handleCommandError(err, options.json);
715
- }
716
- });
717
- // Dismiss command
718
- program
719
- .command('dismiss <url>')
720
- .description('Dismiss notifications for an issue or PR (resurfaces on new activity)')
721
- .option('--json', 'Output as JSON')
722
- .action(async (url, options) => {
723
- try {
724
- const { runDismiss } = await import('./commands/dismiss.js');
725
- const data = await runDismiss({ url });
726
- if (options.json) {
727
- outputJson(data);
728
- }
729
- else if (data.dismissed) {
730
- console.log(`Dismissed: ${url}`);
731
- console.log('Notifications are now muted.');
732
- console.log('New responses after this point will resurface automatically.');
733
- }
734
- else {
735
- console.log('Already dismissed.');
736
- }
737
- }
738
- catch (err) {
739
- handleCommandError(err, options.json);
740
- }
741
- });
742
- // Undismiss command
743
- program
744
- .command('undismiss <url>')
745
- .description('Undismiss an issue or PR (re-enable notifications)')
746
- .option('--json', 'Output as JSON')
747
- .action(async (url, options) => {
748
- try {
749
- const { runUndismiss } = await import('./commands/dismiss.js');
750
- const data = await runUndismiss({ url });
751
- if (options.json) {
752
- outputJson(data);
753
- }
754
- else if (data.undismissed) {
755
- console.log(`Undismissed: ${url}`);
756
- console.log('Notifications are active again.');
757
- }
758
- else {
759
- console.log('Was not dismissed.');
760
- }
761
- }
762
- catch (err) {
763
- handleCommandError(err, options.json);
764
- }
765
- });
766
- // Snooze command
767
- program
768
- .command('snooze <pr-url>')
769
- .description('Snooze CI failure notifications for a PR')
770
- .requiredOption('--reason <reason>', 'Reason for snoozing (e.g., "upstream infrastructure issue")')
771
- .option('--days <days>', 'Number of days to snooze (default: 7)', '7')
772
- .option('--json', 'Output as JSON')
773
- .action(async (prUrl, options) => {
774
- try {
775
- const { runSnooze } = await import('./commands/snooze.js');
776
- const data = await runSnooze({ prUrl, reason: options.reason, days: parseInt(options.days, 10) });
777
- if (options.json) {
778
- outputJson(data);
779
- }
780
- else if (data.snoozed) {
781
- console.log(`Snoozed: ${prUrl}`);
782
- console.log(`Reason: ${data.reason}`);
783
- console.log(`Duration: ${data.days} day${data.days === 1 ? '' : 's'}`);
784
- console.log(`Expires: ${data.expiresAt ? new Date(data.expiresAt).toLocaleString() : 'unknown'}`);
785
- console.log('CI failure notifications are now muted for this PR.');
786
- }
787
- else {
788
- console.log('PR is already snoozed.');
789
- if (data.expiresAt) {
790
- console.log(`Expires: ${new Date(data.expiresAt).toLocaleString()}`);
791
- }
792
- }
793
- }
794
- catch (err) {
795
- handleCommandError(err, options.json);
796
- }
797
- });
798
- // Unsnooze command
799
- program
800
- .command('unsnooze <pr-url>')
801
- .description('Unsnooze a PR (re-enable CI failure notifications)')
802
- .option('--json', 'Output as JSON')
803
- .action(async (prUrl, options) => {
804
- try {
805
- const { runUnsnooze } = await import('./commands/snooze.js');
806
- const data = await runUnsnooze({ prUrl });
807
- if (options.json) {
808
- outputJson(data);
809
- }
810
- else if (data.unsnoozed) {
811
- console.log(`Unsnoozed: ${prUrl}`);
812
- console.log('CI failure notifications are active again for this PR.');
813
- }
814
- else {
815
- console.log('PR was not snoozed.');
816
- }
817
- }
818
- catch (err) {
819
- handleCommandError(err, options.json);
820
- }
821
- });
21
+ // Build the local-only set from registry metadata (replaces hardcoded LOCAL_ONLY_COMMANDS).
22
+ const localOnlySet = new Set(commands.filter((c) => c.localOnly).map((c) => c.name));
23
+ // Register all commands from the registry.
24
+ for (const cmd of commands) {
25
+ cmd.register(program);
26
+ }
822
27
  // Validate GitHub token before running commands that need it
823
28
  program.hook('preAction', async (thisCommand, actionCommand) => {
824
29
  // Enable debug logging if --debug flag is set
@@ -829,7 +34,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
829
34
  }
830
35
  // actionCommand is the command being executed (e.g., 'status', 'daily')
831
36
  const commandName = actionCommand.name();
832
- if (!LOCAL_ONLY_COMMANDS.includes(commandName)) {
37
+ if (!localOnlySet.has(commandName)) {
833
38
  const token = await getGitHubTokenAsync();
834
39
  if (!token) {
835
40
  console.error('Error: GitHub authentication required.');