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