@oss-autopilot/core 0.43.1 → 0.44.1

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