@oss-autopilot/core 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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`);
197
- }
198
- const data = await runSearch({ maxResults });
199
- if (options.json) {
200
- outputJson(data);
201
- }
202
- 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
- }
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.`);
227
182
  }
183
+ maxResults = parsed;
228
184
  }
229
- catch (err) {
230
- handleCommandError(err, options.json);
185
+ if (maxResults > MAX_SEARCH_RESULTS) {
186
+ console.warn(`Capping search to ${MAX_SEARCH_RESULTS} results (requested: ${maxResults})`);
187
+ maxResults = MAX_SEARCH_RESULTS;
231
188
  }
232
- });
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`);
197
+ }
198
+ else {
199
+ console.log('No matching issues found.');
200
+ }
201
+ return;
202
+ }
203
+ if (data.rateLimitWarning) {
204
+ console.warn(`\n${data.rateLimitWarning}\n`);
205
+ }
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,58 @@ 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
- }
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
+ }));
283
+ },
284
+ },
285
+ // ── Skip Add ───────────────────────────────────────────────────────────
286
+ {
287
+ name: 'skip-add',
288
+ localOnly: true,
289
+ register(program) {
290
+ program
291
+ .command('skip-add <issue-url>')
292
+ .description('Append an issue URL to the skipped-issues file (idempotent)')
293
+ .option('--path <file>', 'Skipped-issues file path (falls back to config.skippedIssuesPath)')
294
+ .option('--json', 'Output as JSON')
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}`);
313
299
  }
314
- catch (err) {
315
- handleCommandError(err, options.json);
300
+ else {
301
+ console.log(`Already on skip list: ${data.url}`);
302
+ console.log(` File: ${data.path}`);
316
303
  }
317
- });
304
+ }));
318
305
  },
319
306
  },
320
307
  // ── Track ──────────────────────────────────────────────────────────────
@@ -323,24 +310,13 @@ export const commands = [
323
310
  register(program) {
324
311
  program
325
312
  .command('track <pr-url>')
326
- .description('Add a PR to track')
313
+ .description('Fetch metadata for a PR (informational — v2 does not maintain a local tracking list)')
327
314
  .option('--json', 'Output as JSON')
328
- .action(async (prUrl, options) => {
329
- try {
330
- const { runTrack } = await import('./commands/track.js');
331
- const data = await runTrack({ prUrl });
332
- if (options.json) {
333
- outputJson(data);
334
- }
335
- else {
336
- console.log(`\nPR: ${data.pr.repo}#${data.pr.number} - ${data.pr.title}`);
337
- console.log('Note: In v2, PRs are tracked automatically via the daily run.');
338
- }
339
- }
340
- catch (err) {
341
- handleCommandError(err, options.json);
342
- }
343
- });
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
+ }));
344
320
  },
345
321
  },
346
322
  // ── Untrack ────────────────────────────────────────────────────────────
@@ -350,24 +326,13 @@ export const commands = [
350
326
  register(program) {
351
327
  program
352
328
  .command('untrack <pr-url>')
353
- .description('Stop tracking a PR')
329
+ .description('[DEPRECATED] No-op in v2. Use `shelve` to hide a PR from the daily digest.')
354
330
  .option('--json', 'Output as JSON')
355
- .action(async (prUrl, options) => {
356
- try {
357
- const { runUntrack } = await import('./commands/track.js');
358
- const data = await runUntrack({ prUrl });
359
- if (options.json) {
360
- outputJson(data);
361
- }
362
- else {
363
- console.log('Note: In v2, PRs are fetched fresh on each daily run \u2014 there is no local tracking list to remove from.');
364
- console.log('Use `shelve` to temporarily hide a PR from the daily summary.');
365
- }
366
- }
367
- catch (err) {
368
- handleCommandError(err, options.json);
369
- }
370
- });
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
+ }));
371
336
  },
372
337
  },
373
338
  // ── Read ───────────────────────────────────────────────────────────────
@@ -380,21 +345,9 @@ export const commands = [
380
345
  .description('Mark PR comments as read')
381
346
  .option('--all', 'Mark all PRs as read')
382
347
  .option('--json', 'Output as JSON')
383
- .action(async (prUrl, options) => {
384
- try {
385
- const { runRead } = await import('./commands/read.js');
386
- const data = await runRead({ prUrl, all: options.all });
387
- if (options.json) {
388
- outputJson(data);
389
- }
390
- else {
391
- console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
392
- }
393
- }
394
- catch (err) {
395
- handleCommandError(err, options.json);
396
- }
397
- });
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
+ }));
398
351
  },
399
352
  },
400
353
  // ── Comments ───────────────────────────────────────────────────────────
@@ -406,63 +359,51 @@ export const commands = [
406
359
  .description('Show all comments on a PR')
407
360
  .option('--bots', 'Include bot comments')
408
361
  .option('--json', 'Output as JSON')
409
- .action(async (prUrl, options) => {
410
- try {
411
- const { runComments } = await import('./commands/comments.js');
412
- const data = await runComments({ prUrl, showBots: options.bots });
413
- if (options.json) {
414
- 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('');
415
391
  }
416
- else {
417
- const { formatRelativeTime } = await import('./core/utils.js');
418
- console.log(`\nFetching comments for: ${prUrl}\n`);
419
- console.log(`## ${data.pr.title}\n`);
420
- console.log(`**Status:** ${data.pr.state} | **Mergeable:** ${data.pr.mergeable ?? 'checking...'}`);
421
- console.log(`**Branch:** ${data.pr.head} -> ${data.pr.base}`);
422
- console.log(`**URL:** ${data.pr.url}\n`);
423
- const REVIEW_STATE_LABELS = {
424
- APPROVED: '[Approved]',
425
- CHANGES_REQUESTED: '[Changes]',
426
- };
427
- if (data.reviews.length > 0) {
428
- console.log('### Reviews (newest first)\n');
429
- for (const review of data.reviews) {
430
- const state = REVIEW_STATE_LABELS[review.state] ?? '[Comment]';
431
- const time = review.submittedAt ? formatRelativeTime(review.submittedAt) : '';
432
- console.log(`${state} **@${review.user}** (${review.state}) - ${time}`);
433
- if (review.body) {
434
- console.log(`> ${review.body.split('\n').join('\n> ')}\n`);
435
- }
436
- }
437
- }
438
- if (data.reviewComments.length > 0) {
439
- console.log('### Inline Comments (newest first)\n');
440
- for (const comment of data.reviewComments) {
441
- const time = formatRelativeTime(comment.createdAt);
442
- console.log(`**@${comment.user}** on \`${comment.path}\` - ${time}`);
443
- console.log(`> ${comment.body.split('\n').join('\n> ')}`);
444
- console.log('');
445
- }
446
- }
447
- if (data.issueComments.length > 0) {
448
- console.log('### Discussion (newest first)\n');
449
- for (const comment of data.issueComments) {
450
- const time = formatRelativeTime(comment.createdAt);
451
- console.log(`**@${comment.user}** - ${time}`);
452
- console.log(`> ${comment.body?.split('\n').join('\n> ')}\n`);
453
- }
454
- }
455
- if (data.reviewComments.length === 0 && data.issueComments.length === 0 && data.reviews.length === 0) {
456
- console.log('No comments from other users.\n');
457
- }
458
- console.log('---');
459
- 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`);
460
399
  }
461
400
  }
462
- catch (err) {
463
- 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');
464
403
  }
465
- });
404
+ console.log('---');
405
+ console.log(`**Summary:** ${data.summary.reviewCount} reviews, ${data.summary.inlineCommentCount} inline comments, ${data.summary.discussionCommentCount} discussion comments`);
406
+ }));
466
407
  },
467
408
  },
468
409
  // ── Post ───────────────────────────────────────────────────────────────
@@ -474,32 +415,23 @@ export const commands = [
474
415
  .description('Post a comment to a PR or issue')
475
416
  .option('--stdin', 'Read message from stdin')
476
417
  .option('--json', 'Output as JSON')
477
- .action(async (url, messageParts, options) => {
478
- try {
479
- let message;
480
- if (options.stdin) {
481
- const chunks = [];
482
- for await (const chunk of process.stdin) {
483
- chunks.push(chunk);
484
- }
485
- message = Buffer.concat(chunks).toString('utf-8').trim();
486
- }
487
- else {
488
- message = messageParts.join(' ');
489
- }
490
- const { runPost } = await import('./commands/comments.js');
491
- const data = await runPost({ url, message });
492
- if (options.json) {
493
- outputJson(data);
494
- }
495
- else {
496
- console.log(`Comment posted: ${data.commentUrl}`);
497
- }
498
- }
499
- catch (err) {
500
- handleCommandError(err, options.json);
501
- }
502
- });
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);
424
+ }
425
+ message = Buffer.concat(chunks).toString('utf-8').trim();
426
+ }
427
+ else {
428
+ message = messageParts.join(' ');
429
+ }
430
+ const { runPost } = await import('./commands/comments.js');
431
+ return runPost({ url, message });
432
+ }, (data) => {
433
+ console.log(`Comment posted: ${data.commentUrl}`);
434
+ }));
503
435
  },
504
436
  },
505
437
  // ── Claim ──────────────────────────────────────────────────────────────
@@ -510,22 +442,13 @@ export const commands = [
510
442
  .command('claim <issue-url> [message...]')
511
443
  .description('Claim an issue by posting a comment')
512
444
  .option('--json', 'Output as JSON')
513
- .action(async (issueUrl, messageParts, options) => {
514
- try {
515
- const { runClaim } = await import('./commands/comments.js');
516
- const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
517
- const data = await runClaim({ issueUrl, message });
518
- if (options.json) {
519
- outputJson(data);
520
- }
521
- else {
522
- console.log(`Issue claimed: ${data.commentUrl}`);
523
- }
524
- }
525
- catch (err) {
526
- handleCommandError(err, options.json);
527
- }
528
- });
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
+ }));
529
452
  },
530
453
  },
531
454
  // ── Config ─────────────────────────────────────────────────────────────
@@ -537,25 +460,15 @@ export const commands = [
537
460
  .command('config [key] [value]')
538
461
  .description('Show or update configuration')
539
462
  .option('--json', 'Output as JSON')
540
- .action(async (key, value, options) => {
541
- try {
542
- const { runConfig } = await import('./commands/config.js');
543
- const data = await runConfig({ key, value });
544
- if (options.json) {
545
- outputJson(data);
546
- }
547
- else if ('config' in data) {
548
- console.log('\n\u2699\ufe0f Current Configuration:\n');
549
- console.log(JSON.stringify(data.config, null, 2));
550
- }
551
- else {
552
- console.log(`Set ${data.key} to: ${data.value}`);
553
- }
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));
554
467
  }
555
- catch (err) {
556
- handleCommandError(err, options.json);
468
+ else {
469
+ console.log(`Set ${data.key} to: ${data.value}`);
557
470
  }
558
- });
471
+ }));
559
472
  },
560
473
  },
561
474
  // ── Init ───────────────────────────────────────────────────────────────
@@ -566,22 +479,10 @@ export const commands = [
566
479
  .command('init <username>')
567
480
  .description('Initialize with your GitHub username and import open PRs')
568
481
  .option('--json', 'Output as JSON')
569
- .action(async (username, options) => {
570
- try {
571
- const { runInit } = await import('./commands/init.js');
572
- const data = await runInit({ username });
573
- if (options.json) {
574
- outputJson(data);
575
- }
576
- else {
577
- console.log(`\nUsername set to @${data.username}.`);
578
- console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
579
- }
580
- }
581
- catch (err) {
582
- handleCommandError(err, options.json);
583
- }
584
- });
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
+ }));
585
486
  },
586
487
  },
587
488
  // ── Setup ──────────────────────────────────────────────────────────────
@@ -595,66 +496,56 @@ export const commands = [
595
496
  .option('--reset', 'Re-run setup even if already complete')
596
497
  .option('--set <settings...>', 'Set specific values (key=value)')
597
498
  .option('--json', 'Output as JSON')
598
- .action(async (options) => {
599
- try {
600
- const { runSetup } = await import('./commands/setup.js');
601
- const data = await runSetup({ reset: options.reset, set: options.set });
602
- if (options.json) {
603
- outputJson(data);
604
- }
605
- else if ('success' in data) {
606
- // --set mode
607
- for (const [key, value] of Object.entries(data.settings)) {
608
- console.log(`\u2713 ${key}: ${value}`);
609
- }
610
- if (data.warnings) {
611
- for (const w of data.warnings) {
612
- console.warn(w);
613
- }
614
- }
615
- }
616
- else if ('setupComplete' in data && data.setupComplete) {
617
- // Already complete
618
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
619
- console.log('\u2713 Setup already complete!\n');
620
- console.log('Current settings:');
621
- console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
622
- console.log(` Max active PRs: ${data.config.maxActivePRs}`);
623
- console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
624
- console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
625
- console.log(` Languages: ${data.config.languages.join(', ')}`);
626
- console.log(` Labels: ${data.config.labels.join(', ')}`);
627
- console.log(`\nRun 'setup --reset' to reconfigure.`);
628
- }
629
- else if ('setupRequired' in data) {
630
- // Needs setup
631
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
632
- console.log('SETUP_REQUIRED');
633
- console.log('---');
634
- console.log('Please configure the following settings:\n');
635
- for (const prompt of data.prompts) {
636
- console.log(`SETTING: ${prompt.setting}`);
637
- console.log(`PROMPT: ${prompt.prompt}`);
638
- const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
639
- console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
640
- if (prompt.required)
641
- console.log('REQUIRED: true');
642
- if (prompt.default !== undefined) {
643
- const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
644
- console.log(`DEFAULT: ${defaultVal}`);
645
- }
646
- if (prompt.type)
647
- console.log(`TYPE: ${prompt.type}`);
648
- console.log('');
649
- }
650
- console.log('---');
651
- 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('');
652
544
  }
545
+ console.log('---');
546
+ console.log('END_SETUP_PROMPTS');
653
547
  }
654
- catch (err) {
655
- handleCommandError(err, options.json);
656
- }
657
- });
548
+ }));
658
549
  },
659
550
  },
660
551
  // ── Check Setup ────────────────────────────────────────────────────────
@@ -666,25 +557,15 @@ export const commands = [
666
557
  .command('checkSetup')
667
558
  .description('Check if setup is complete')
668
559
  .option('--json', 'Output as JSON')
669
- .action(async (options) => {
670
- try {
671
- const { runCheckSetup } = await import('./commands/setup.js');
672
- const data = await runCheckSetup();
673
- if (options.json) {
674
- outputJson(data);
675
- }
676
- else if (data.setupComplete) {
677
- console.log('SETUP_COMPLETE');
678
- console.log(`username=${data.username}`);
679
- }
680
- else {
681
- console.log('SETUP_INCOMPLETE');
682
- }
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}`);
683
564
  }
684
- catch (err) {
685
- handleCommandError(err, options.json);
565
+ else {
566
+ console.log('SETUP_INCOMPLETE');
686
567
  }
687
- });
568
+ }));
688
569
  },
689
570
  },
690
571
  // ── Dashboard Serve ────────────────────────────────────────────────────
@@ -723,36 +604,24 @@ export const commands = [
723
604
  .command('parse-issue-list <path>')
724
605
  .description('Parse a markdown issue list into structured JSON')
725
606
  .option('--json', 'Output as JSON')
726
- .action(async (filePath, options) => {
727
- try {
728
- const { runParseList } = await import('./commands/parse-list.js');
729
- const data = await runParseList({ filePath });
730
- if (options.json) {
731
- outputJson(data);
732
- }
733
- else {
734
- const path = await import('path');
735
- const resolvedPath = path.resolve(filePath);
736
- console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
737
- console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
738
- if (data.available.length > 0) {
739
- console.log('--- Available ---');
740
- for (const item of data.available) {
741
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
742
- }
743
- }
744
- if (data.completed.length > 0) {
745
- console.log('\n--- Completed ---');
746
- for (const item of data.completed) {
747
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
748
- }
749
- }
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}`);
750
616
  }
751
617
  }
752
- catch (err) {
753
- 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
+ }
754
623
  }
755
- });
624
+ }));
756
625
  },
757
626
  },
758
627
  // ── Check Integration ──────────────────────────────────────────────────
@@ -765,38 +634,27 @@ export const commands = [
765
634
  .description('Detect new files not referenced by the codebase')
766
635
  .option('--base <branch>', 'Base branch to compare against', 'main')
767
636
  .option('--json', 'Output as JSON')
768
- .action(async (options) => {
769
- try {
770
- const { runCheckIntegration } = await import('./commands/check-integration.js');
771
- const data = await runCheckIntegration({ base: options.base });
772
- if (options.json) {
773
- outputJson(data);
774
- }
775
- else if (data.newFiles.length === 0) {
776
- 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(', ')}`);
777
649
  }
778
650
  else {
779
- console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
780
- console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
781
- for (const file of data.newFiles) {
782
- const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
783
- console.log(`${status} ${file.path}`);
784
- if (file.isIntegrated) {
785
- console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
786
- }
787
- else {
788
- console.log(' Not referenced by any file');
789
- if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
790
- console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
791
- }
792
- }
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(', ')}`);
793
654
  }
794
655
  }
795
656
  }
796
- catch (err) {
797
- handleCommandError(err, options.json);
798
- }
799
- });
657
+ }));
800
658
  },
801
659
  },
802
660
  // ── Local Repos ────────────────────────────────────────────────────────
@@ -810,26 +668,19 @@ export const commands = [
810
668
  .option('--scan', 'Force re-scan (ignores cache)')
811
669
  .option('--paths <dirs...>', 'Directories to scan')
812
670
  .option('--json', 'Output as JSON')
813
- .action(async (options) => {
814
- try {
815
- const { runLocalRepos } = await import('./commands/local-repos.js');
816
- const data = await runLocalRepos({ scan: options.scan, paths: options.paths });
817
- if (options.json) {
818
- outputJson(data);
819
- }
820
- else if (data.fromCache) {
821
- console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
822
- printRepos(data.repos);
823
- }
824
- else {
825
- console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
826
- printRepos(data.repos);
827
- }
828
- }
829
- catch (err) {
830
- handleCommandError(err, options.json);
831
- }
832
- });
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);
678
+ }
679
+ else {
680
+ console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
681
+ printRepos(data.repos);
682
+ }
683
+ }));
833
684
  },
834
685
  },
835
686
  // ── Startup ────────────────────────────────────────────────────────────
@@ -883,21 +734,9 @@ export const commands = [
883
734
  .command('shelve <pr-url>')
884
735
  .description('Shelve a PR (exclude from capacity and actionable issues)')
885
736
  .option('--json', 'Output as JSON')
886
- .action(async (prUrl, options) => {
887
- try {
888
- const { runMove } = await import('./commands/move.js');
889
- const data = await runMove({ prUrl, target: 'shelved' });
890
- if (options.json) {
891
- outputJson(data);
892
- }
893
- else {
894
- console.log(data.description);
895
- }
896
- }
897
- catch (err) {
898
- handleCommandError(err, options.json);
899
- }
900
- });
737
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'shelved' }), (data) => {
738
+ console.log(data.description);
739
+ }));
901
740
  },
902
741
  },
903
742
  // ── Unshelve ───────────────────────────────────────────────────────────
@@ -909,21 +748,9 @@ export const commands = [
909
748
  .command('unshelve <pr-url>')
910
749
  .description('Unshelve a PR (include in capacity and actionable issues again)')
911
750
  .option('--json', 'Output as JSON')
912
- .action(async (prUrl, options) => {
913
- try {
914
- const { runMove } = await import('./commands/move.js');
915
- const data = await runMove({ prUrl, target: 'auto' });
916
- if (options.json) {
917
- outputJson(data);
918
- }
919
- else {
920
- console.log(data.description);
921
- }
922
- }
923
- catch (err) {
924
- handleCommandError(err, options.json);
925
- }
926
- });
751
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
752
+ console.log(data.description);
753
+ }));
927
754
  },
928
755
  },
929
756
  // ── Move ───────────────────────────────────────────────────────────
@@ -935,21 +762,9 @@ export const commands = [
935
762
  .command('move <pr-url> <target>')
936
763
  .description('Move a PR between states: attention, waiting, shelved, or auto (reset to computed)')
937
764
  .option('--json', 'Output as JSON')
938
- .action(async (prUrl, target, options) => {
939
- try {
940
- const { runMove } = await import('./commands/move.js');
941
- const data = await runMove({ prUrl, target });
942
- if (options.json) {
943
- outputJson(data);
944
- }
945
- else {
946
- console.log(data.description);
947
- }
948
- }
949
- catch (err) {
950
- handleCommandError(err, options.json);
951
- }
952
- });
765
+ .action((prUrl, target, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target }), (data) => {
766
+ console.log(data.description);
767
+ }));
953
768
  },
954
769
  },
955
770
  // ── Dismiss ────────────────────────────────────────────────────────────
@@ -961,26 +776,16 @@ export const commands = [
961
776
  .command('dismiss <url>')
962
777
  .description('Dismiss notifications for an issue (resurfaces on new activity)')
963
778
  .option('--json', 'Output as JSON')
964
- .action(async (url, options) => {
965
- try {
966
- const { runDismiss } = await import('./commands/dismiss.js');
967
- const data = await runDismiss({ url });
968
- if (options.json) {
969
- outputJson(data);
970
- }
971
- else if (data.dismissed) {
972
- console.log(`Dismissed: ${url}`);
973
- console.log('Notifications are now muted.');
974
- console.log('New responses after this point will resurface automatically.');
975
- }
976
- else {
977
- console.log('Already dismissed.');
978
- }
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.');
979
784
  }
980
- catch (err) {
981
- handleCommandError(err, options.json);
785
+ else {
786
+ console.log('Already dismissed.');
982
787
  }
983
- });
788
+ }));
984
789
  },
985
790
  },
986
791
  // ── Undismiss ──────────────────────────────────────────────────────────
@@ -992,25 +797,15 @@ export const commands = [
992
797
  .command('undismiss <url>')
993
798
  .description('Undismiss an issue (re-enable notifications)')
994
799
  .option('--json', 'Output as JSON')
995
- .action(async (url, options) => {
996
- try {
997
- const { runUndismiss } = await import('./commands/dismiss.js');
998
- const data = await runUndismiss({ url });
999
- if (options.json) {
1000
- outputJson(data);
1001
- }
1002
- else if (data.undismissed) {
1003
- console.log(`Undismissed: ${url}`);
1004
- console.log('Notifications are active again.');
1005
- }
1006
- else {
1007
- console.log('Was not dismissed.');
1008
- }
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.');
1009
804
  }
1010
- catch (err) {
1011
- handleCommandError(err, options.json);
805
+ else {
806
+ console.log('Was not dismissed.');
1012
807
  }
1013
- });
808
+ }));
1014
809
  },
1015
810
  },
1016
811
  // ── Override Status ────────────────────────────────────────────────────
@@ -1022,26 +817,17 @@ export const commands = [
1022
817
  .command('override <pr-url> <status>')
1023
818
  .description('Manually override PR status (needs_addressing or waiting_on_maintainer)')
1024
819
  .option('--json', 'Output as JSON')
1025
- .action(async (prUrl, status, options) => {
1026
- try {
1027
- const { runMove } = await import('./commands/move.js');
1028
- const validStatuses = ['needs_addressing', 'waiting_on_maintainer'];
1029
- if (!validStatuses.includes(status)) {
1030
- throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
1031
- }
1032
- const target = status === 'needs_addressing' ? 'attention' : 'waiting';
1033
- const data = await runMove({ prUrl, target });
1034
- if (options.json) {
1035
- outputJson(data);
1036
- }
1037
- else {
1038
- console.log(data.description);
1039
- }
1040
- }
1041
- catch (err) {
1042
- handleCommandError(err, options.json);
1043
- }
1044
- });
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(', ')}`);
824
+ }
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
+ }));
1045
831
  },
1046
832
  },
1047
833
  // ── Clear Override ────────────────────────────────────────────────────
@@ -1053,21 +839,9 @@ export const commands = [
1053
839
  .command('clear-override <pr-url>')
1054
840
  .description('Clear a manual status override for a PR')
1055
841
  .option('--json', 'Output as JSON')
1056
- .action(async (prUrl, options) => {
1057
- try {
1058
- const { runMove } = await import('./commands/move.js');
1059
- const data = await runMove({ prUrl, target: 'auto' });
1060
- if (options.json) {
1061
- outputJson(data);
1062
- }
1063
- else {
1064
- console.log(data.description);
1065
- }
1066
- }
1067
- catch (err) {
1068
- handleCommandError(err, options.json);
1069
- }
1070
- });
842
+ .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
843
+ console.log(data.description);
844
+ }));
1071
845
  },
1072
846
  },
1073
847
  // ── PR Template ──────────────────────────────────────────────────────
@@ -1078,28 +852,18 @@ export const commands = [
1078
852
  .command('pr-template <repo>')
1079
853
  .description("Fetch a repository's PR description template")
1080
854
  .option('--json', 'Output as JSON')
1081
- .action(async (repo, options) => {
1082
- try {
1083
- const { runPRTemplate } = await import('./commands/pr-template.js');
1084
- const data = await runPRTemplate({ repo });
1085
- if (options.json) {
1086
- outputJson(data);
1087
- }
1088
- else if (data.template) {
1089
- console.log(`\nPR template found at: ${data.source}\n`);
1090
- console.log(data.template);
1091
- }
1092
- else if (data.error) {
1093
- console.error(`\nWarning: Could not check for PR template: ${data.error}`);
1094
- }
1095
- else {
1096
- console.log('\nNo PR template found for this repository.');
1097
- }
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);
1098
859
  }
1099
- catch (err) {
1100
- handleCommandError(err, options.json);
860
+ else if (data.error) {
861
+ console.error(`\nWarning: Could not check for PR template: ${data.error}`);
1101
862
  }
1102
- });
863
+ else {
864
+ console.log('\nNo PR template found for this repository.');
865
+ }
866
+ }));
1103
867
  },
1104
868
  },
1105
869
  // ── Detect Formatters ────────────────────────────────────────────────
@@ -1112,48 +876,39 @@ export const commands = [
1112
876
  .description('Detect formatters and linters configured in a repository')
1113
877
  .option('--ci-log <path>', 'Analyze CI log file for formatting failures')
1114
878
  .option('--json', 'Output as JSON')
1115
- .action(async (repoPath, options) => {
1116
- try {
1117
- const { runDetectFormatters } = await import('./commands/detect-formatters.js');
1118
- const data = await runDetectFormatters({ repoPath, ciLog: options.ciLog });
1119
- if (options.json) {
1120
- 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(', ')}`);
1121
906
  }
1122
907
  else {
1123
- if (data.formatters.length === 0) {
1124
- console.log('\nNo formatters detected.');
1125
- }
1126
- else {
1127
- console.log(`\nDetected ${data.formatters.length} formatter(s):\n`);
1128
- for (const f of data.formatters) {
1129
- console.log(` ${f.name} (${f.configPath})`);
1130
- console.log(` Fix: ${f.fixCommand}`);
1131
- console.log(` Check: ${f.checkCommand}`);
1132
- }
1133
- }
1134
- if (data.packageJsonScripts.length > 0) {
1135
- console.log('\npackage.json scripts:');
1136
- for (const s of data.packageJsonScripts) {
1137
- console.log(` ${s.name}: ${s.command}`);
1138
- }
1139
- }
1140
- if (data.ciDiagnosis) {
1141
- console.log('');
1142
- if (data.ciDiagnosis.isFormattingFailure) {
1143
- console.log(`CI Diagnosis: Formatting failure detected (${data.ciDiagnosis.formatter})`);
1144
- console.log(` Fix: ${data.ciDiagnosis.fixCommand}`);
1145
- console.log(` Evidence: ${data.ciDiagnosis.evidence.join(', ')}`);
1146
- }
1147
- else {
1148
- console.log('CI Diagnosis: No formatting failure detected.');
1149
- }
1150
- }
908
+ console.log('CI Diagnosis: No formatting failure detected.');
1151
909
  }
1152
910
  }
1153
- catch (err) {
1154
- handleCommandError(err, options.json);
1155
- }
1156
- });
911
+ }));
1157
912
  },
1158
913
  },
1159
914
  // ── Stats ─────────────────────────────────────────────────────────────