@oss-autopilot/core 1.15.2 → 1.16.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.
@@ -21,6 +21,29 @@ function handleCommandError(err, json) {
21
21
  }
22
22
  process.exit(1);
23
23
  }
24
+ /**
25
+ * Shared action wrapper: runs the command, routes to JSON or display output,
26
+ * and uniformly handles errors (#999).
27
+ *
28
+ * Captures the try/catch + `--json` branching boilerplate that was
29
+ * duplicated in every command action. Commands with non-standard output
30
+ * modes (e.g. `--compact`, `--markdown`, `--badge`) inline their own
31
+ * branching rather than squeezing through this helper.
32
+ */
33
+ async function executeAction(options, run, display) {
34
+ try {
35
+ const data = await run();
36
+ if (options.json) {
37
+ outputJson(data);
38
+ }
39
+ else {
40
+ await display(data);
41
+ }
42
+ }
43
+ catch (err) {
44
+ handleCommandError(err, options.json);
45
+ }
46
+ }
24
47
  /** Print local repos in human-readable format (used by local-repos command). */
25
48
  function printRepos(repos) {
26
49
  const entries = Object.entries(repos).sort(([a], [b]) => a.localeCompare(b));
@@ -75,33 +98,24 @@ export const commands = [
75
98
  .description('Show current status and stats')
76
99
  .option('--json', 'Output as JSON')
77
100
  .option('--offline', 'Use cached data only (no GitHub API calls)')
78
- .action(async (options) => {
79
- try {
80
- const { runStatus } = await import('./commands/status.js');
81
- const data = await runStatus({ offline: options.offline });
82
- if (options.json) {
83
- outputJson(data);
84
- }
85
- else {
86
- console.log('\n\ud83d\udcca OSS Status\n');
87
- console.log(`Merged PRs: ${data.stats.mergedPRs}`);
88
- console.log(`Closed PRs: ${data.stats.closedPRs}`);
89
- console.log(`Merge Rate: ${data.stats.mergeRate}`);
90
- console.log(`Needs Response: ${data.stats.needsResponse}`);
91
- if (data.offline) {
92
- console.log(`\nLast Updated: ${data.lastUpdated || 'Never'}`);
93
- console.log('(Offline mode: showing cached data)');
94
- }
95
- else {
96
- console.log(`\nLast Run: ${data.lastRunAt || 'Never'}`);
97
- }
98
- console.log('\nRun with --json for structured output');
99
- }
100
- }
101
- catch (err) {
102
- handleCommandError(err, options.json);
103
- }
104
- });
101
+ .action((options) => executeAction(options, async () => {
102
+ const { runStatus } = await import('./commands/status.js');
103
+ return runStatus({ offline: options.offline });
104
+ }, (data) => {
105
+ console.log('\n\ud83d\udcca OSS Status\n');
106
+ console.log(`Merged PRs: ${data.stats.mergedPRs}`);
107
+ console.log(`Closed PRs: ${data.stats.closedPRs}`);
108
+ console.log(`Merge Rate: ${data.stats.mergeRate}`);
109
+ console.log(`Needs Response: ${data.stats.needsResponse}`);
110
+ if (data.offline) {
111
+ console.log(`\nLast Updated: ${data.lastUpdated || 'Never'}`);
112
+ console.log('(Offline mode: showing cached data)');
113
+ }
114
+ else {
115
+ console.log(`\nLast Run: ${data.lastRunAt || 'Never'}`);
116
+ }
117
+ console.log('\nRun with --json for structured output');
118
+ }));
105
119
  },
106
120
  },
107
121
  // ── State ──────────────────────────────────────────────────────────────
@@ -116,54 +130,36 @@ export const commands = [
116
130
  .option('--unlink', 'Switch from Gist back to local persistence')
117
131
  .option('--json', 'Output as JSON')
118
132
  .action(async (options) => {
119
- try {
120
- if (options.unlink) {
121
- const { runStateUnlink } = await import('./commands/state-cmd.js');
122
- const data = await runStateUnlink();
123
- if (options.json) {
124
- outputJson(data);
125
- }
126
- else {
127
- console.log(`State written to ${data.localStatePath}`);
128
- console.log('Persistence switched to local mode.');
129
- if (data.previousGistId) {
130
- console.log(`Previous Gist (${data.previousGistId}) was NOT deleted.`);
131
- }
132
- console.log('Restart any running processes (e.g. dashboard server) to pick up the change.');
133
- }
134
- }
135
- else if (options.sync) {
136
- const { runStateSync } = await import('./commands/state-cmd.js');
137
- const data = await runStateSync();
138
- if (options.json) {
139
- outputJson(data);
140
- }
141
- else if (data.pushed) {
133
+ if (options.unlink) {
134
+ await executeAction(options, async () => (await import('./commands/state-cmd.js')).runStateUnlink(), (data) => {
135
+ console.log(`State written to ${data.localStatePath}`);
136
+ console.log('Persistence switched to local mode.');
137
+ if (data.previousGistId) {
138
+ console.log(`Previous Gist (${data.previousGistId}) was NOT deleted.`);
139
+ }
140
+ console.log('Restart any running processes (e.g. dashboard server) to pick up the change.');
141
+ });
142
+ }
143
+ else if (options.sync) {
144
+ await executeAction(options, async () => (await import('./commands/state-cmd.js')).runStateSync(), (data) => {
145
+ if (data.pushed) {
142
146
  console.log(`State pushed to Gist ${data.gistId}`);
143
147
  }
144
148
  else {
145
149
  console.log('Not in Gist mode. Nothing to sync.');
146
150
  }
147
- }
148
- else {
149
- // Default: --show
150
- const { runStateShow } = await import('./commands/state-cmd.js');
151
- const data = await runStateShow();
152
- if (options.json) {
153
- outputJson(data);
154
- }
155
- else {
156
- console.log(`\nPersistence: ${data.persistence}`);
157
- if (data.gistId)
158
- console.log(`Gist ID: ${data.gistId}`);
159
- if (data.gistDegraded)
160
- console.log('Status: DEGRADED (using local cache)');
161
- console.log(`Last run: ${data.lastRunAt ?? 'Never'}\n`);
162
- }
163
- }
151
+ });
164
152
  }
165
- catch (err) {
166
- handleCommandError(err, options.json);
153
+ else {
154
+ // Default: --show
155
+ await executeAction(options, async () => (await import('./commands/state-cmd.js')).runStateShow(), (data) => {
156
+ console.log(`\nPersistence: ${data.persistence}`);
157
+ if (data.gistId)
158
+ console.log(`Gist ID: ${data.gistId}`);
159
+ if (data.gistDegraded)
160
+ console.log('Status: DEGRADED (using local cache)');
161
+ console.log(`Last run: ${data.lastRunAt ?? 'Never'}\n`);
162
+ });
167
163
  }
168
164
  });
169
165
  },
@@ -176,60 +172,50 @@ export const commands = [
176
172
  .command('search [count]')
177
173
  .description('Search for new issues to work on')
178
174
  .option('--json', 'Output as JSON')
179
- .action(async (count, options) => {
180
- try {
181
- const { runSearch } = await import('./commands/search.js');
182
- let maxResults = 5;
183
- if (count !== undefined) {
184
- const parsed = Number(count);
185
- if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
186
- throw new Error(`Invalid count "${count}". Must be a positive integer.`);
187
- }
188
- maxResults = parsed;
189
- }
190
- const MAX_SEARCH_RESULTS = 100;
191
- if (maxResults > MAX_SEARCH_RESULTS) {
192
- console.warn(`Capping search to ${MAX_SEARCH_RESULTS} results (requested: ${maxResults})`);
193
- maxResults = MAX_SEARCH_RESULTS;
194
- }
195
- if (!options.json) {
196
- console.log(`\nSearching for issues (max ${maxResults})...\n`);
175
+ .action((count, options) => executeAction(options, async () => {
176
+ const { runSearch, MAX_SEARCH_RESULTS } = await import('./commands/search.js');
177
+ let maxResults = 5;
178
+ if (count !== undefined) {
179
+ const parsed = Number(count);
180
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
181
+ throw new Error(`Invalid count "${count}". Must be a positive integer.`);
197
182
  }
198
- const data = await runSearch({ maxResults });
199
- if (options.json) {
200
- outputJson(data);
183
+ maxResults = parsed;
184
+ }
185
+ if (maxResults > MAX_SEARCH_RESULTS) {
186
+ console.warn(`Capping search to ${MAX_SEARCH_RESULTS} results (requested: ${maxResults})`);
187
+ maxResults = MAX_SEARCH_RESULTS;
188
+ }
189
+ if (!options.json) {
190
+ console.log(`\nSearching for issues (max ${maxResults})...\n`);
191
+ }
192
+ return runSearch({ maxResults });
193
+ }, (data) => {
194
+ if (data.candidates.length === 0) {
195
+ if (data.rateLimitWarning) {
196
+ console.warn(`\n${data.rateLimitWarning}\n`);
201
197
  }
202
198
  else {
203
- if (data.candidates.length === 0) {
204
- if (data.rateLimitWarning) {
205
- console.warn(`\n${data.rateLimitWarning}\n`);
206
- }
207
- else {
208
- console.log('No matching issues found.');
209
- }
210
- return;
211
- }
212
- if (data.rateLimitWarning) {
213
- console.warn(`\n${data.rateLimitWarning}\n`);
214
- }
215
- console.log(`Found ${data.candidates.length} candidates:\n`);
216
- for (const candidate of data.candidates) {
217
- const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
218
- console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
219
- console.log(` URL: ${issue.url}`);
220
- console.log(` Viability: ${viabilityScore}/100`);
221
- if (reasonsToApprove.length > 0)
222
- console.log(` Approve: ${reasonsToApprove.join(', ')}`);
223
- if (reasonsToSkip.length > 0)
224
- console.log(` Skip: ${reasonsToSkip.join(', ')}`);
225
- console.log('---');
226
- }
199
+ console.log('No matching issues found.');
227
200
  }
201
+ return;
228
202
  }
229
- catch (err) {
230
- handleCommandError(err, options.json);
203
+ if (data.rateLimitWarning) {
204
+ console.warn(`\n${data.rateLimitWarning}\n`);
231
205
  }
232
- });
206
+ console.log(`Found ${data.candidates.length} candidates:\n`);
207
+ for (const candidate of data.candidates) {
208
+ const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
209
+ console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
210
+ console.log(` URL: ${issue.url}`);
211
+ console.log(` Viability: ${viabilityScore}/100`);
212
+ if (reasonsToApprove.length > 0)
213
+ console.log(` Approve: ${reasonsToApprove.join(', ')}`);
214
+ if (reasonsToSkip.length > 0)
215
+ console.log(` Skip: ${reasonsToSkip.join(', ')}`);
216
+ console.log('---');
217
+ }
218
+ }));
233
219
  },
234
220
  },
235
221
  // ── Vet ────────────────────────────────────────────────────────────────
@@ -240,29 +226,17 @@ export const commands = [
240
226
  .command('vet <issue-url>')
241
227
  .description('Vet a specific issue before working on it')
242
228
  .option('--json', 'Output as JSON')
243
- .action(async (issueUrl, options) => {
244
- try {
245
- const { runVet } = await import('./commands/vet.js');
246
- const data = await runVet({ issueUrl });
247
- if (options.json) {
248
- outputJson(data);
249
- }
250
- else {
251
- const { issue, recommendation, reasonsToApprove, reasonsToSkip, grade } = data;
252
- console.log(`\nVetting issue: ${issueUrl}\n`);
253
- console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
254
- console.log(` URL: ${issue.url}`);
255
- console.log(` Success grade: ${grade.letter} (${grade.reason})`);
256
- if (reasonsToApprove.length > 0)
257
- console.log(` Approve: ${reasonsToApprove.join(', ')}`);
258
- if (reasonsToSkip.length > 0)
259
- console.log(` Skip: ${reasonsToSkip.join(', ')}`);
260
- }
261
- }
262
- catch (err) {
263
- handleCommandError(err, options.json);
264
- }
265
- });
229
+ .action((issueUrl, options) => executeAction(options, async () => (await import('./commands/vet.js')).runVet({ issueUrl }), (data) => {
230
+ const { issue, recommendation, reasonsToApprove, reasonsToSkip, grade } = data;
231
+ console.log(`\nVetting issue: ${issueUrl}\n`);
232
+ console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
233
+ console.log(` URL: ${issue.url}`);
234
+ console.log(` Success grade: ${grade.letter} (${grade.reason})`);
235
+ if (reasonsToApprove.length > 0)
236
+ console.log(` Approve: ${reasonsToApprove.join(', ')}`);
237
+ if (reasonsToSkip.length > 0)
238
+ console.log(` Skip: ${reasonsToSkip.join(', ')}`);
239
+ }));
266
240
  },
267
241
  },
268
242
  // ── Vet List ──────────────────────────────────────────────────────────
@@ -276,45 +250,36 @@ export const commands = [
276
250
  .option('--concurrency <n>', 'Max parallel vet operations (default: 5)')
277
251
  .option('--prune', 'After vetting, remove completed/skipped/low-score items from the file')
278
252
  .option('--json', 'Output as JSON')
279
- .action(async (options) => {
280
- try {
281
- const { runVetList } = await import('./commands/vet-list.js');
282
- const concurrency = options.concurrency ? parseInt(options.concurrency, 10) : undefined;
283
- if (concurrency !== undefined && (!Number.isFinite(concurrency) || concurrency < 1)) {
284
- throw new Error(`Invalid concurrency "${options.concurrency}". Must be a positive integer.`);
285
- }
286
- const data = await runVetList({ issueListPath: options.path, concurrency, prune: options.prune });
287
- if (options.json) {
288
- outputJson(data);
289
- }
290
- else {
291
- console.log(`\nRe-vetted ${data.summary.total} issues:\n`);
292
- console.log(` Still available: ${data.summary.stillAvailable}`);
293
- console.log(` Claimed: ${data.summary.claimed}`);
294
- console.log(` Closed: ${data.summary.closed}`);
295
- console.log(` Has PR: ${data.summary.hasPR}`);
296
- console.log(` Errors: ${data.summary.errors}`);
297
- console.log('');
298
- for (const result of data.results) {
299
- const status = result.listStatus === 'still_available'
300
- ? '\u2705'
301
- : result.listStatus === 'error'
302
- ? '\u274c'
303
- : '\u26a0\ufe0f';
304
- console.log(`${status} [${result.listStatus}] ${result.issue.repo}#${result.issue.number}: ${result.issue.title}`);
305
- if (result.errorMessage) {
306
- console.log(` Error: ${result.errorMessage}`);
307
- }
308
- }
309
- if (data.pruneResult) {
310
- console.log(`\nPruned ${data.pruneResult.removedCount} items from issue list.`);
311
- }
312
- }
313
- }
314
- catch (err) {
315
- handleCommandError(err, options.json);
316
- }
317
- });
253
+ .action((options) => executeAction(options, async () => {
254
+ const { runVetList } = await import('./commands/vet-list.js');
255
+ const concurrency = options.concurrency ? parseInt(options.concurrency, 10) : undefined;
256
+ if (concurrency !== undefined && (!Number.isFinite(concurrency) || concurrency < 1)) {
257
+ throw new Error(`Invalid concurrency "${options.concurrency}". Must be a positive integer.`);
258
+ }
259
+ return runVetList({ issueListPath: options.path, concurrency, prune: options.prune });
260
+ }, (data) => {
261
+ console.log(`\nRe-vetted ${data.summary.total} issues:\n`);
262
+ console.log(` Still available: ${data.summary.stillAvailable}`);
263
+ console.log(` Claimed: ${data.summary.claimed}`);
264
+ console.log(` Closed: ${data.summary.closed}`);
265
+ console.log(` Has PR: ${data.summary.hasPR}`);
266
+ console.log(` Errors: ${data.summary.errors}`);
267
+ console.log('');
268
+ for (const result of data.results) {
269
+ const status = result.listStatus === 'still_available'
270
+ ? '\u2705'
271
+ : result.listStatus === 'error'
272
+ ? '\u274c'
273
+ : '\u26a0\ufe0f';
274
+ console.log(`${status} [${result.listStatus}] ${result.issue.repo}#${result.issue.number}: ${result.issue.title}`);
275
+ if (result.errorMessage) {
276
+ console.log(` Error: ${result.errorMessage}`);
277
+ }
278
+ }
279
+ if (data.pruneResult) {
280
+ console.log(`\nPruned ${data.pruneResult.removedCount} items from issue list.`);
281
+ }
282
+ }));
318
283
  },
319
284
  },
320
285
  // ── Skip Add ───────────────────────────────────────────────────────────
@@ -327,26 +292,16 @@ export const commands = [
327
292
  .description('Append an issue URL to the skipped-issues file (idempotent)')
328
293
  .option('--path <file>', 'Skipped-issues file path (falls back to config.skippedIssuesPath)')
329
294
  .option('--json', 'Output as JSON')
330
- .action(async (issueUrl, options) => {
331
- try {
332
- const { runSkipAdd } = await import('./commands/skip-add.js');
333
- const data = runSkipAdd({ issueUrl, skipFilePath: options.path });
334
- if (options.json) {
335
- outputJson(data);
336
- }
337
- else if (data.added) {
338
- console.log(`Added to skip list: ${data.url} (${data.date})`);
339
- console.log(` File: ${data.path}`);
340
- }
341
- else {
342
- console.log(`Already on skip list: ${data.url}`);
343
- console.log(` File: ${data.path}`);
344
- }
295
+ .action((issueUrl, options) => executeAction(options, async () => (await import('./commands/skip-add.js')).runSkipAdd({ issueUrl, skipFilePath: options.path }), (data) => {
296
+ if (data.added) {
297
+ console.log(`Added to skip list: ${data.url} (${data.date})`);
298
+ console.log(` File: ${data.path}`);
345
299
  }
346
- catch (err) {
347
- handleCommandError(err, options.json);
300
+ else {
301
+ console.log(`Already on skip list: ${data.url}`);
302
+ console.log(` File: ${data.path}`);
348
303
  }
349
- });
304
+ }));
350
305
  },
351
306
  },
352
307
  // ── Track ──────────────────────────────────────────────────────────────
@@ -355,24 +310,13 @@ export const commands = [
355
310
  register(program) {
356
311
  program
357
312
  .command('track <pr-url>')
358
- .description('Add a PR to track')
313
+ .description('Fetch metadata for a PR (informational — v2 does not maintain a local tracking list)')
359
314
  .option('--json', 'Output as JSON')
360
- .action(async (prUrl, options) => {
361
- try {
362
- const { runTrack } = await import('./commands/track.js');
363
- const data = await runTrack({ prUrl });
364
- if (options.json) {
365
- outputJson(data);
366
- }
367
- else {
368
- console.log(`\nPR: ${data.pr.repo}#${data.pr.number} - ${data.pr.title}`);
369
- console.log('Note: In v2, PRs are tracked automatically via the daily run.');
370
- }
371
- }
372
- catch (err) {
373
- handleCommandError(err, options.json);
374
- }
375
- });
315
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/track.js')).runTrack({ prUrl }), (data) => {
316
+ console.log(`\nPR: ${data.pr.repo}#${data.pr.number} - ${data.pr.title}`);
317
+ console.log('Note: In v2, PRs are discovered automatically on each daily run — this command only');
318
+ console.log('fetches metadata for inspection. Nothing is persisted locally.');
319
+ }));
376
320
  },
377
321
  },
378
322
  // ── Untrack ────────────────────────────────────────────────────────────
@@ -382,24 +326,13 @@ export const commands = [
382
326
  register(program) {
383
327
  program
384
328
  .command('untrack <pr-url>')
385
- .description('Stop tracking a PR')
329
+ .description('[DEPRECATED] No-op in v2. Use `shelve` to hide a PR from the daily digest.')
386
330
  .option('--json', 'Output as JSON')
387
- .action(async (prUrl, options) => {
388
- try {
389
- const { runUntrack } = await import('./commands/track.js');
390
- const data = await runUntrack({ prUrl });
391
- if (options.json) {
392
- outputJson(data);
393
- }
394
- else {
395
- console.log('Note: In v2, PRs are fetched fresh on each daily run \u2014 there is no local tracking list to remove from.');
396
- console.log('Use `shelve` to temporarily hide a PR from the daily summary.');
397
- }
398
- }
399
- catch (err) {
400
- handleCommandError(err, options.json);
401
- }
402
- });
331
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/track.js')).runUntrack({ prUrl }), () => {
332
+ // Stderr so scripts piping stdout don't see the deprecation notice.
333
+ console.error('[DEPRECATED] `untrack` is a no-op in v2. PRs are fetched fresh on each daily run —');
334
+ console.error('there is no local tracking list to remove from. Use `shelve` to hide a PR instead.');
335
+ }));
403
336
  },
404
337
  },
405
338
  // ── Read ───────────────────────────────────────────────────────────────
@@ -412,21 +345,9 @@ export const commands = [
412
345
  .description('Mark PR comments as read')
413
346
  .option('--all', 'Mark all PRs as read')
414
347
  .option('--json', 'Output as JSON')
415
- .action(async (prUrl, options) => {
416
- try {
417
- const { runRead } = await import('./commands/read.js');
418
- const data = await runRead({ prUrl, all: options.all });
419
- if (options.json) {
420
- outputJson(data);
421
- }
422
- else {
423
- console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
424
- }
425
- }
426
- catch (err) {
427
- handleCommandError(err, options.json);
428
- }
429
- });
348
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/read.js')).runRead({ prUrl, all: options.all }), () => {
349
+ console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
350
+ }));
430
351
  },
431
352
  },
432
353
  // ── Comments ───────────────────────────────────────────────────────────
@@ -438,63 +359,51 @@ export const commands = [
438
359
  .description('Show all comments on a PR')
439
360
  .option('--bots', 'Include bot comments')
440
361
  .option('--json', 'Output as JSON')
441
- .action(async (prUrl, options) => {
442
- try {
443
- const { runComments } = await import('./commands/comments.js');
444
- const data = await runComments({ prUrl, showBots: options.bots });
445
- if (options.json) {
446
- outputJson(data);
362
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/comments.js')).runComments({ prUrl, showBots: options.bots }), async (data) => {
363
+ const { formatRelativeTime } = await import('./core/utils.js');
364
+ console.log(`\nFetching comments for: ${prUrl}\n`);
365
+ console.log(`## ${data.pr.title}\n`);
366
+ console.log(`**Status:** ${data.pr.state} | **Mergeable:** ${data.pr.mergeable ?? 'checking...'}`);
367
+ console.log(`**Branch:** ${data.pr.head} -> ${data.pr.base}`);
368
+ console.log(`**URL:** ${data.pr.url}\n`);
369
+ const REVIEW_STATE_LABELS = {
370
+ APPROVED: '[Approved]',
371
+ CHANGES_REQUESTED: '[Changes]',
372
+ };
373
+ if (data.reviews.length > 0) {
374
+ console.log('### Reviews (newest first)\n');
375
+ for (const review of data.reviews) {
376
+ const state = REVIEW_STATE_LABELS[review.state] ?? '[Comment]';
377
+ const time = review.submittedAt ? formatRelativeTime(review.submittedAt) : '';
378
+ console.log(`${state} **@${review.user}** (${review.state}) - ${time}`);
379
+ if (review.body) {
380
+ console.log(`> ${review.body.split('\n').join('\n> ')}\n`);
381
+ }
382
+ }
383
+ }
384
+ if (data.reviewComments.length > 0) {
385
+ console.log('### Inline Comments (newest first)\n');
386
+ for (const comment of data.reviewComments) {
387
+ const time = formatRelativeTime(comment.createdAt);
388
+ console.log(`**@${comment.user}** on \`${comment.path}\` - ${time}`);
389
+ console.log(`> ${comment.body.split('\n').join('\n> ')}`);
390
+ console.log('');
447
391
  }
448
- else {
449
- const { formatRelativeTime } = await import('./core/utils.js');
450
- console.log(`\nFetching comments for: ${prUrl}\n`);
451
- console.log(`## ${data.pr.title}\n`);
452
- console.log(`**Status:** ${data.pr.state} | **Mergeable:** ${data.pr.mergeable ?? 'checking...'}`);
453
- console.log(`**Branch:** ${data.pr.head} -> ${data.pr.base}`);
454
- console.log(`**URL:** ${data.pr.url}\n`);
455
- const REVIEW_STATE_LABELS = {
456
- APPROVED: '[Approved]',
457
- CHANGES_REQUESTED: '[Changes]',
458
- };
459
- if (data.reviews.length > 0) {
460
- console.log('### Reviews (newest first)\n');
461
- for (const review of data.reviews) {
462
- const state = REVIEW_STATE_LABELS[review.state] ?? '[Comment]';
463
- const time = review.submittedAt ? formatRelativeTime(review.submittedAt) : '';
464
- console.log(`${state} **@${review.user}** (${review.state}) - ${time}`);
465
- if (review.body) {
466
- console.log(`> ${review.body.split('\n').join('\n> ')}\n`);
467
- }
468
- }
469
- }
470
- if (data.reviewComments.length > 0) {
471
- console.log('### Inline Comments (newest first)\n');
472
- for (const comment of data.reviewComments) {
473
- const time = formatRelativeTime(comment.createdAt);
474
- console.log(`**@${comment.user}** on \`${comment.path}\` - ${time}`);
475
- console.log(`> ${comment.body.split('\n').join('\n> ')}`);
476
- console.log('');
477
- }
478
- }
479
- if (data.issueComments.length > 0) {
480
- console.log('### Discussion (newest first)\n');
481
- for (const comment of data.issueComments) {
482
- const time = formatRelativeTime(comment.createdAt);
483
- console.log(`**@${comment.user}** - ${time}`);
484
- console.log(`> ${comment.body?.split('\n').join('\n> ')}\n`);
485
- }
486
- }
487
- if (data.reviewComments.length === 0 && data.issueComments.length === 0 && data.reviews.length === 0) {
488
- console.log('No comments from other users.\n');
489
- }
490
- console.log('---');
491
- console.log(`**Summary:** ${data.summary.reviewCount} reviews, ${data.summary.inlineCommentCount} inline comments, ${data.summary.discussionCommentCount} discussion comments`);
392
+ }
393
+ if (data.issueComments.length > 0) {
394
+ console.log('### Discussion (newest first)\n');
395
+ for (const comment of data.issueComments) {
396
+ const time = formatRelativeTime(comment.createdAt);
397
+ console.log(`**@${comment.user}** - ${time}`);
398
+ console.log(`> ${comment.body?.split('\n').join('\n> ')}\n`);
492
399
  }
493
400
  }
494
- catch (err) {
495
- handleCommandError(err, options.json);
401
+ if (data.reviewComments.length === 0 && data.issueComments.length === 0 && data.reviews.length === 0) {
402
+ console.log('No comments from other users.\n');
496
403
  }
497
- });
404
+ console.log('---');
405
+ console.log(`**Summary:** ${data.summary.reviewCount} reviews, ${data.summary.inlineCommentCount} inline comments, ${data.summary.discussionCommentCount} discussion comments`);
406
+ }));
498
407
  },
499
408
  },
500
409
  // ── Post ───────────────────────────────────────────────────────────────
@@ -506,32 +415,23 @@ export const commands = [
506
415
  .description('Post a comment to a PR or issue')
507
416
  .option('--stdin', 'Read message from stdin')
508
417
  .option('--json', 'Output as JSON')
509
- .action(async (url, messageParts, options) => {
510
- try {
511
- let message;
512
- if (options.stdin) {
513
- const chunks = [];
514
- for await (const chunk of process.stdin) {
515
- chunks.push(chunk);
516
- }
517
- message = Buffer.concat(chunks).toString('utf-8').trim();
518
- }
519
- else {
520
- message = messageParts.join(' ');
521
- }
522
- const { runPost } = await import('./commands/comments.js');
523
- const data = await runPost({ url, message });
524
- if (options.json) {
525
- outputJson(data);
526
- }
527
- else {
528
- console.log(`Comment posted: ${data.commentUrl}`);
418
+ .action((url, messageParts, options) => executeAction(options, async () => {
419
+ let message;
420
+ if (options.stdin) {
421
+ const chunks = [];
422
+ for await (const chunk of process.stdin) {
423
+ chunks.push(chunk);
529
424
  }
425
+ message = Buffer.concat(chunks).toString('utf-8').trim();
530
426
  }
531
- catch (err) {
532
- handleCommandError(err, options.json);
427
+ else {
428
+ message = messageParts.join(' ');
533
429
  }
534
- });
430
+ const { runPost } = await import('./commands/comments.js');
431
+ return runPost({ url, message });
432
+ }, (data) => {
433
+ console.log(`Comment posted: ${data.commentUrl}`);
434
+ }));
535
435
  },
536
436
  },
537
437
  // ── Claim ──────────────────────────────────────────────────────────────
@@ -542,22 +442,13 @@ export const commands = [
542
442
  .command('claim <issue-url> [message...]')
543
443
  .description('Claim an issue by posting a comment')
544
444
  .option('--json', 'Output as JSON')
545
- .action(async (issueUrl, messageParts, options) => {
546
- try {
547
- const { runClaim } = await import('./commands/comments.js');
548
- const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
549
- const data = await runClaim({ issueUrl, message });
550
- if (options.json) {
551
- outputJson(data);
552
- }
553
- else {
554
- console.log(`Issue claimed: ${data.commentUrl}`);
555
- }
556
- }
557
- catch (err) {
558
- handleCommandError(err, options.json);
559
- }
560
- });
445
+ .action((issueUrl, messageParts, options) => executeAction(options, async () => {
446
+ const { runClaim } = await import('./commands/comments.js');
447
+ const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
448
+ return runClaim({ issueUrl, message });
449
+ }, (data) => {
450
+ console.log(`Issue claimed: ${data.commentUrl}`);
451
+ }));
561
452
  },
562
453
  },
563
454
  // ── Config ─────────────────────────────────────────────────────────────
@@ -569,25 +460,15 @@ export const commands = [
569
460
  .command('config [key] [value]')
570
461
  .description('Show or update configuration')
571
462
  .option('--json', 'Output as JSON')
572
- .action(async (key, value, options) => {
573
- try {
574
- const { runConfig } = await import('./commands/config.js');
575
- const data = await runConfig({ key, value });
576
- if (options.json) {
577
- outputJson(data);
578
- }
579
- else if ('config' in data) {
580
- console.log('\n\u2699\ufe0f Current Configuration:\n');
581
- console.log(JSON.stringify(data.config, null, 2));
582
- }
583
- else {
584
- console.log(`Set ${data.key} to: ${data.value}`);
585
- }
463
+ .action((key, value, options) => executeAction(options, async () => (await import('./commands/config.js')).runConfig({ key, value }), (data) => {
464
+ if ('config' in data) {
465
+ console.log('\n\u2699\ufe0f Current Configuration:\n');
466
+ console.log(JSON.stringify(data.config, null, 2));
586
467
  }
587
- catch (err) {
588
- handleCommandError(err, options.json);
468
+ else {
469
+ console.log(`Set ${data.key} to: ${data.value}`);
589
470
  }
590
- });
471
+ }));
591
472
  },
592
473
  },
593
474
  // ── Init ───────────────────────────────────────────────────────────────
@@ -598,22 +479,10 @@ export const commands = [
598
479
  .command('init <username>')
599
480
  .description('Initialize with your GitHub username and import open PRs')
600
481
  .option('--json', 'Output as JSON')
601
- .action(async (username, options) => {
602
- try {
603
- const { runInit } = await import('./commands/init.js');
604
- const data = await runInit({ username });
605
- if (options.json) {
606
- outputJson(data);
607
- }
608
- else {
609
- console.log(`\nUsername set to @${data.username}.`);
610
- console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
611
- }
612
- }
613
- catch (err) {
614
- handleCommandError(err, options.json);
615
- }
616
- });
482
+ .action((username, options) => executeAction(options, async () => (await import('./commands/init.js')).runInit({ username }), (data) => {
483
+ console.log(`\nUsername set to @${data.username}.`);
484
+ console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
485
+ }));
617
486
  },
618
487
  },
619
488
  // ── Setup ──────────────────────────────────────────────────────────────
@@ -627,66 +496,56 @@ export const commands = [
627
496
  .option('--reset', 'Re-run setup even if already complete')
628
497
  .option('--set <settings...>', 'Set specific values (key=value)')
629
498
  .option('--json', 'Output as JSON')
630
- .action(async (options) => {
631
- try {
632
- const { runSetup } = await import('./commands/setup.js');
633
- const data = await runSetup({ reset: options.reset, set: options.set });
634
- if (options.json) {
635
- outputJson(data);
636
- }
637
- else if ('success' in data) {
638
- // --set mode
639
- for (const [key, value] of Object.entries(data.settings)) {
640
- console.log(`\u2713 ${key}: ${value}`);
641
- }
642
- if (data.warnings) {
643
- for (const w of data.warnings) {
644
- console.warn(w);
645
- }
646
- }
647
- }
648
- else if ('setupComplete' in data && data.setupComplete) {
649
- // Already complete
650
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
651
- console.log('\u2713 Setup already complete!\n');
652
- console.log('Current settings:');
653
- console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
654
- console.log(` Max active PRs: ${data.config.maxActivePRs}`);
655
- console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
656
- console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
657
- console.log(` Languages: ${data.config.languages.join(', ')}`);
658
- console.log(` Labels: ${data.config.labels.join(', ')}`);
659
- console.log(`\nRun 'setup --reset' to reconfigure.`);
660
- }
661
- else if ('setupRequired' in data) {
662
- // Needs setup
663
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
664
- console.log('SETUP_REQUIRED');
665
- console.log('---');
666
- console.log('Please configure the following settings:\n');
667
- for (const prompt of data.prompts) {
668
- console.log(`SETTING: ${prompt.setting}`);
669
- console.log(`PROMPT: ${prompt.prompt}`);
670
- const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
671
- console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
672
- if (prompt.required)
673
- console.log('REQUIRED: true');
674
- if (prompt.default !== undefined) {
675
- const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
676
- console.log(`DEFAULT: ${defaultVal}`);
677
- }
678
- if (prompt.type)
679
- console.log(`TYPE: ${prompt.type}`);
680
- console.log('');
681
- }
682
- console.log('---');
683
- console.log('END_SETUP_PROMPTS');
499
+ .action((options) => executeAction(options, async () => (await import('./commands/setup.js')).runSetup({ reset: options.reset, set: options.set }), (data) => {
500
+ if ('success' in data) {
501
+ // --set mode
502
+ for (const [key, value] of Object.entries(data.settings)) {
503
+ console.log(`\u2713 ${key}: ${value}`);
504
+ }
505
+ if (data.warnings) {
506
+ for (const w of data.warnings) {
507
+ console.warn(w);
508
+ }
509
+ }
510
+ }
511
+ else if ('setupComplete' in data && data.setupComplete) {
512
+ // Already complete
513
+ console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
514
+ console.log('\u2713 Setup already complete!\n');
515
+ console.log('Current settings:');
516
+ console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
517
+ console.log(` Max active PRs: ${data.config.maxActivePRs}`);
518
+ console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
519
+ console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
520
+ console.log(` Languages: ${data.config.languages.join(', ')}`);
521
+ console.log(` Labels: ${data.config.labels.join(', ')}`);
522
+ console.log(`\nRun 'setup --reset' to reconfigure.`);
523
+ }
524
+ else if ('setupRequired' in data) {
525
+ // Needs setup
526
+ console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
527
+ console.log('SETUP_REQUIRED');
528
+ console.log('---');
529
+ console.log('Please configure the following settings:\n');
530
+ for (const prompt of data.prompts) {
531
+ console.log(`SETTING: ${prompt.setting}`);
532
+ console.log(`PROMPT: ${prompt.prompt}`);
533
+ const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
534
+ console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
535
+ if (prompt.required)
536
+ console.log('REQUIRED: true');
537
+ if (prompt.default !== undefined) {
538
+ const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
539
+ console.log(`DEFAULT: ${defaultVal}`);
540
+ }
541
+ if (prompt.type)
542
+ console.log(`TYPE: ${prompt.type}`);
543
+ console.log('');
684
544
  }
545
+ console.log('---');
546
+ console.log('END_SETUP_PROMPTS');
685
547
  }
686
- catch (err) {
687
- handleCommandError(err, options.json);
688
- }
689
- });
548
+ }));
690
549
  },
691
550
  },
692
551
  // ── Check Setup ────────────────────────────────────────────────────────
@@ -698,25 +557,15 @@ export const commands = [
698
557
  .command('checkSetup')
699
558
  .description('Check if setup is complete')
700
559
  .option('--json', 'Output as JSON')
701
- .action(async (options) => {
702
- try {
703
- const { runCheckSetup } = await import('./commands/setup.js');
704
- const data = await runCheckSetup();
705
- if (options.json) {
706
- outputJson(data);
707
- }
708
- else if (data.setupComplete) {
709
- console.log('SETUP_COMPLETE');
710
- console.log(`username=${data.username}`);
711
- }
712
- else {
713
- console.log('SETUP_INCOMPLETE');
714
- }
560
+ .action((options) => executeAction(options, async () => (await import('./commands/setup.js')).runCheckSetup(), (data) => {
561
+ if (data.setupComplete) {
562
+ console.log('SETUP_COMPLETE');
563
+ console.log(`username=${data.username}`);
715
564
  }
716
- catch (err) {
717
- handleCommandError(err, options.json);
565
+ else {
566
+ console.log('SETUP_INCOMPLETE');
718
567
  }
719
- });
568
+ }));
720
569
  },
721
570
  },
722
571
  // ── Dashboard Serve ────────────────────────────────────────────────────
@@ -755,36 +604,24 @@ export const commands = [
755
604
  .command('parse-issue-list <path>')
756
605
  .description('Parse a markdown issue list into structured JSON')
757
606
  .option('--json', 'Output as JSON')
758
- .action(async (filePath, options) => {
759
- try {
760
- const { runParseList } = await import('./commands/parse-list.js');
761
- const data = await runParseList({ filePath });
762
- if (options.json) {
763
- outputJson(data);
764
- }
765
- else {
766
- const path = await import('path');
767
- const resolvedPath = path.resolve(filePath);
768
- console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
769
- console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
770
- if (data.available.length > 0) {
771
- console.log('--- Available ---');
772
- for (const item of data.available) {
773
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
774
- }
775
- }
776
- if (data.completed.length > 0) {
777
- console.log('\n--- Completed ---');
778
- for (const item of data.completed) {
779
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
780
- }
781
- }
607
+ .action((filePath, options) => executeAction(options, async () => (await import('./commands/parse-list.js')).runParseList({ filePath }), async (data) => {
608
+ const path = await import('path');
609
+ const resolvedPath = path.resolve(filePath);
610
+ console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
611
+ console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
612
+ if (data.available.length > 0) {
613
+ console.log('--- Available ---');
614
+ for (const item of data.available) {
615
+ console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
782
616
  }
783
617
  }
784
- catch (err) {
785
- handleCommandError(err, options.json);
618
+ if (data.completed.length > 0) {
619
+ console.log('\n--- Completed ---');
620
+ for (const item of data.completed) {
621
+ console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
622
+ }
786
623
  }
787
- });
624
+ }));
788
625
  },
789
626
  },
790
627
  // ── Check Integration ──────────────────────────────────────────────────
@@ -797,38 +634,27 @@ export const commands = [
797
634
  .description('Detect new files not referenced by the codebase')
798
635
  .option('--base <branch>', 'Base branch to compare against', 'main')
799
636
  .option('--json', 'Output as JSON')
800
- .action(async (options) => {
801
- try {
802
- const { runCheckIntegration } = await import('./commands/check-integration.js');
803
- const data = await runCheckIntegration({ base: options.base });
804
- if (options.json) {
805
- outputJson(data);
806
- }
807
- else if (data.newFiles.length === 0) {
808
- console.log('\nNo new code files to check.');
637
+ .action((options) => executeAction(options, async () => (await import('./commands/check-integration.js')).runCheckIntegration({ base: options.base }), (data) => {
638
+ if (data.newFiles.length === 0) {
639
+ console.log('\nNo new code files to check.');
640
+ return;
641
+ }
642
+ console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
643
+ console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
644
+ for (const file of data.newFiles) {
645
+ const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
646
+ console.log(`${status} ${file.path}`);
647
+ if (file.isIntegrated) {
648
+ console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
809
649
  }
810
650
  else {
811
- console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
812
- console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
813
- for (const file of data.newFiles) {
814
- const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
815
- console.log(`${status} ${file.path}`);
816
- if (file.isIntegrated) {
817
- console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
818
- }
819
- else {
820
- console.log(' Not referenced by any file');
821
- if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
822
- console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
823
- }
824
- }
651
+ console.log(' Not referenced by any file');
652
+ if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
653
+ console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
825
654
  }
826
655
  }
827
656
  }
828
- catch (err) {
829
- handleCommandError(err, options.json);
830
- }
831
- });
657
+ }));
832
658
  },
833
659
  },
834
660
  // ── Local Repos ────────────────────────────────────────────────────────
@@ -842,26 +668,19 @@ export const commands = [
842
668
  .option('--scan', 'Force re-scan (ignores cache)')
843
669
  .option('--paths <dirs...>', 'Directories to scan')
844
670
  .option('--json', 'Output as JSON')
845
- .action(async (options) => {
846
- try {
847
- const { runLocalRepos } = await import('./commands/local-repos.js');
848
- const data = await runLocalRepos({ scan: options.scan, paths: options.paths });
849
- if (options.json) {
850
- outputJson(data);
851
- }
852
- else if (data.fromCache) {
853
- console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
854
- printRepos(data.repos);
855
- }
856
- else {
857
- console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
858
- printRepos(data.repos);
859
- }
671
+ .action((options) => executeAction(options, async () => (await import('./commands/local-repos.js')).runLocalRepos({
672
+ scan: options.scan,
673
+ paths: options.paths,
674
+ }), (data) => {
675
+ if (data.fromCache) {
676
+ console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
677
+ printRepos(data.repos);
860
678
  }
861
- catch (err) {
862
- handleCommandError(err, options.json);
679
+ else {
680
+ console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
681
+ printRepos(data.repos);
863
682
  }
864
- });
683
+ }));
865
684
  },
866
685
  },
867
686
  // ── Startup ────────────────────────────────────────────────────────────
@@ -915,21 +734,9 @@ export const commands = [
915
734
  .command('shelve <pr-url>')
916
735
  .description('Shelve a PR (exclude from capacity and actionable issues)')
917
736
  .option('--json', 'Output as JSON')
918
- .action(async (prUrl, options) => {
919
- try {
920
- const { runMove } = await import('./commands/move.js');
921
- const data = await runMove({ prUrl, target: 'shelved' });
922
- if (options.json) {
923
- outputJson(data);
924
- }
925
- else {
926
- console.log(data.description);
927
- }
928
- }
929
- catch (err) {
930
- handleCommandError(err, options.json);
931
- }
932
- });
737
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'shelved' }), (data) => {
738
+ console.log(data.description);
739
+ }));
933
740
  },
934
741
  },
935
742
  // ── Unshelve ───────────────────────────────────────────────────────────
@@ -941,21 +748,9 @@ export const commands = [
941
748
  .command('unshelve <pr-url>')
942
749
  .description('Unshelve a PR (include in capacity and actionable issues again)')
943
750
  .option('--json', 'Output as JSON')
944
- .action(async (prUrl, options) => {
945
- try {
946
- const { runMove } = await import('./commands/move.js');
947
- const data = await runMove({ prUrl, target: 'auto' });
948
- if (options.json) {
949
- outputJson(data);
950
- }
951
- else {
952
- console.log(data.description);
953
- }
954
- }
955
- catch (err) {
956
- handleCommandError(err, options.json);
957
- }
958
- });
751
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
752
+ console.log(data.description);
753
+ }));
959
754
  },
960
755
  },
961
756
  // ── Move ───────────────────────────────────────────────────────────
@@ -967,21 +762,9 @@ export const commands = [
967
762
  .command('move <pr-url> <target>')
968
763
  .description('Move a PR between states: attention, waiting, shelved, or auto (reset to computed)')
969
764
  .option('--json', 'Output as JSON')
970
- .action(async (prUrl, target, options) => {
971
- try {
972
- const { runMove } = await import('./commands/move.js');
973
- const data = await runMove({ prUrl, target });
974
- if (options.json) {
975
- outputJson(data);
976
- }
977
- else {
978
- console.log(data.description);
979
- }
980
- }
981
- catch (err) {
982
- handleCommandError(err, options.json);
983
- }
984
- });
765
+ .action((prUrl, target, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target }), (data) => {
766
+ console.log(data.description);
767
+ }));
985
768
  },
986
769
  },
987
770
  // ── Dismiss ────────────────────────────────────────────────────────────
@@ -993,26 +776,16 @@ export const commands = [
993
776
  .command('dismiss <url>')
994
777
  .description('Dismiss notifications for an issue (resurfaces on new activity)')
995
778
  .option('--json', 'Output as JSON')
996
- .action(async (url, options) => {
997
- try {
998
- const { runDismiss } = await import('./commands/dismiss.js');
999
- const data = await runDismiss({ url });
1000
- if (options.json) {
1001
- outputJson(data);
1002
- }
1003
- else if (data.dismissed) {
1004
- console.log(`Dismissed: ${url}`);
1005
- console.log('Notifications are now muted.');
1006
- console.log('New responses after this point will resurface automatically.');
1007
- }
1008
- else {
1009
- console.log('Already dismissed.');
1010
- }
779
+ .action((url, options) => executeAction(options, async () => (await import('./commands/dismiss.js')).runDismiss({ url }), (data) => {
780
+ if (data.dismissed) {
781
+ console.log(`Dismissed: ${url}`);
782
+ console.log('Notifications are now muted.');
783
+ console.log('New responses after this point will resurface automatically.');
1011
784
  }
1012
- catch (err) {
1013
- handleCommandError(err, options.json);
785
+ else {
786
+ console.log('Already dismissed.');
1014
787
  }
1015
- });
788
+ }));
1016
789
  },
1017
790
  },
1018
791
  // ── Undismiss ──────────────────────────────────────────────────────────
@@ -1024,25 +797,15 @@ export const commands = [
1024
797
  .command('undismiss <url>')
1025
798
  .description('Undismiss an issue (re-enable notifications)')
1026
799
  .option('--json', 'Output as JSON')
1027
- .action(async (url, options) => {
1028
- try {
1029
- const { runUndismiss } = await import('./commands/dismiss.js');
1030
- const data = await runUndismiss({ url });
1031
- if (options.json) {
1032
- outputJson(data);
1033
- }
1034
- else if (data.undismissed) {
1035
- console.log(`Undismissed: ${url}`);
1036
- console.log('Notifications are active again.');
1037
- }
1038
- else {
1039
- console.log('Was not dismissed.');
1040
- }
800
+ .action((url, options) => executeAction(options, async () => (await import('./commands/dismiss.js')).runUndismiss({ url }), (data) => {
801
+ if (data.undismissed) {
802
+ console.log(`Undismissed: ${url}`);
803
+ console.log('Notifications are active again.');
1041
804
  }
1042
- catch (err) {
1043
- handleCommandError(err, options.json);
805
+ else {
806
+ console.log('Was not dismissed.');
1044
807
  }
1045
- });
808
+ }));
1046
809
  },
1047
810
  },
1048
811
  // ── Override Status ────────────────────────────────────────────────────
@@ -1054,26 +817,17 @@ export const commands = [
1054
817
  .command('override <pr-url> <status>')
1055
818
  .description('Manually override PR status (needs_addressing or waiting_on_maintainer)')
1056
819
  .option('--json', 'Output as JSON')
1057
- .action(async (prUrl, status, options) => {
1058
- try {
1059
- const { runMove } = await import('./commands/move.js');
1060
- const validStatuses = ['needs_addressing', 'waiting_on_maintainer'];
1061
- if (!validStatuses.includes(status)) {
1062
- throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
1063
- }
1064
- const target = status === 'needs_addressing' ? 'attention' : 'waiting';
1065
- const data = await runMove({ prUrl, target });
1066
- if (options.json) {
1067
- outputJson(data);
1068
- }
1069
- else {
1070
- console.log(data.description);
1071
- }
1072
- }
1073
- catch (err) {
1074
- handleCommandError(err, options.json);
820
+ .action((prUrl, status, options) => executeAction(options, async () => {
821
+ const validStatuses = ['needs_addressing', 'waiting_on_maintainer'];
822
+ if (!validStatuses.includes(status)) {
823
+ throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
1075
824
  }
1076
- });
825
+ const target = status === 'needs_addressing' ? 'attention' : 'waiting';
826
+ const { runMove } = await import('./commands/move.js');
827
+ return runMove({ prUrl, target });
828
+ }, (data) => {
829
+ console.log(data.description);
830
+ }));
1077
831
  },
1078
832
  },
1079
833
  // ── Clear Override ────────────────────────────────────────────────────
@@ -1085,21 +839,9 @@ export const commands = [
1085
839
  .command('clear-override <pr-url>')
1086
840
  .description('Clear a manual status override for a PR')
1087
841
  .option('--json', 'Output as JSON')
1088
- .action(async (prUrl, options) => {
1089
- try {
1090
- const { runMove } = await import('./commands/move.js');
1091
- const data = await runMove({ prUrl, target: 'auto' });
1092
- if (options.json) {
1093
- outputJson(data);
1094
- }
1095
- else {
1096
- console.log(data.description);
1097
- }
1098
- }
1099
- catch (err) {
1100
- handleCommandError(err, options.json);
1101
- }
1102
- });
842
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
843
+ console.log(data.description);
844
+ }));
1103
845
  },
1104
846
  },
1105
847
  // ── PR Template ──────────────────────────────────────────────────────
@@ -1110,28 +852,18 @@ export const commands = [
1110
852
  .command('pr-template <repo>')
1111
853
  .description("Fetch a repository's PR description template")
1112
854
  .option('--json', 'Output as JSON')
1113
- .action(async (repo, options) => {
1114
- try {
1115
- const { runPRTemplate } = await import('./commands/pr-template.js');
1116
- const data = await runPRTemplate({ repo });
1117
- if (options.json) {
1118
- outputJson(data);
1119
- }
1120
- else if (data.template) {
1121
- console.log(`\nPR template found at: ${data.source}\n`);
1122
- console.log(data.template);
1123
- }
1124
- else if (data.error) {
1125
- console.error(`\nWarning: Could not check for PR template: ${data.error}`);
1126
- }
1127
- else {
1128
- console.log('\nNo PR template found for this repository.');
1129
- }
855
+ .action((repo, options) => executeAction(options, async () => (await import('./commands/pr-template.js')).runPRTemplate({ repo }), (data) => {
856
+ if (data.template) {
857
+ console.log(`\nPR template found at: ${data.source}\n`);
858
+ console.log(data.template);
1130
859
  }
1131
- catch (err) {
1132
- handleCommandError(err, options.json);
860
+ else if (data.error) {
861
+ console.error(`\nWarning: Could not check for PR template: ${data.error}`);
1133
862
  }
1134
- });
863
+ else {
864
+ console.log('\nNo PR template found for this repository.');
865
+ }
866
+ }));
1135
867
  },
1136
868
  },
1137
869
  // ── Detect Formatters ────────────────────────────────────────────────
@@ -1144,48 +876,39 @@ export const commands = [
1144
876
  .description('Detect formatters and linters configured in a repository')
1145
877
  .option('--ci-log <path>', 'Analyze CI log file for formatting failures')
1146
878
  .option('--json', 'Output as JSON')
1147
- .action(async (repoPath, options) => {
1148
- try {
1149
- const { runDetectFormatters } = await import('./commands/detect-formatters.js');
1150
- const data = await runDetectFormatters({ repoPath, ciLog: options.ciLog });
1151
- if (options.json) {
1152
- outputJson(data);
879
+ .action((repoPath, options) => executeAction(options, async () => (await import('./commands/detect-formatters.js')).runDetectFormatters({
880
+ repoPath,
881
+ ciLog: options.ciLog,
882
+ }), (data) => {
883
+ if (data.formatters.length === 0) {
884
+ console.log('\nNo formatters detected.');
885
+ }
886
+ else {
887
+ console.log(`\nDetected ${data.formatters.length} formatter(s):\n`);
888
+ for (const f of data.formatters) {
889
+ console.log(` ${f.name} (${f.configPath})`);
890
+ console.log(` Fix: ${f.fixCommand}`);
891
+ console.log(` Check: ${f.checkCommand}`);
892
+ }
893
+ }
894
+ if (data.packageJsonScripts.length > 0) {
895
+ console.log('\npackage.json scripts:');
896
+ for (const s of data.packageJsonScripts) {
897
+ console.log(` ${s.name}: ${s.command}`);
898
+ }
899
+ }
900
+ if (data.ciDiagnosis) {
901
+ console.log('');
902
+ if (data.ciDiagnosis.isFormattingFailure) {
903
+ console.log(`CI Diagnosis: Formatting failure detected (${data.ciDiagnosis.formatter})`);
904
+ console.log(` Fix: ${data.ciDiagnosis.fixCommand}`);
905
+ console.log(` Evidence: ${data.ciDiagnosis.evidence.join(', ')}`);
1153
906
  }
1154
907
  else {
1155
- if (data.formatters.length === 0) {
1156
- console.log('\nNo formatters detected.');
1157
- }
1158
- else {
1159
- console.log(`\nDetected ${data.formatters.length} formatter(s):\n`);
1160
- for (const f of data.formatters) {
1161
- console.log(` ${f.name} (${f.configPath})`);
1162
- console.log(` Fix: ${f.fixCommand}`);
1163
- console.log(` Check: ${f.checkCommand}`);
1164
- }
1165
- }
1166
- if (data.packageJsonScripts.length > 0) {
1167
- console.log('\npackage.json scripts:');
1168
- for (const s of data.packageJsonScripts) {
1169
- console.log(` ${s.name}: ${s.command}`);
1170
- }
1171
- }
1172
- if (data.ciDiagnosis) {
1173
- console.log('');
1174
- if (data.ciDiagnosis.isFormattingFailure) {
1175
- console.log(`CI Diagnosis: Formatting failure detected (${data.ciDiagnosis.formatter})`);
1176
- console.log(` Fix: ${data.ciDiagnosis.fixCommand}`);
1177
- console.log(` Evidence: ${data.ciDiagnosis.evidence.join(', ')}`);
1178
- }
1179
- else {
1180
- console.log('CI Diagnosis: No formatting failure detected.');
1181
- }
1182
- }
908
+ console.log('CI Diagnosis: No formatting failure detected.');
1183
909
  }
1184
910
  }
1185
- catch (err) {
1186
- handleCommandError(err, options.json);
1187
- }
1188
- });
911
+ }));
1189
912
  },
1190
913
  },
1191
914
  // ── Stats ─────────────────────────────────────────────────────────────