@oss-scout/core 0.10.0 → 1.0.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.
- package/dist/cli.bundle.cjs +77 -60
- package/dist/cli.js +403 -416
- package/dist/commands/command-scout.d.ts +21 -0
- package/dist/commands/command-scout.js +21 -0
- package/dist/commands/config.js +10 -128
- package/dist/commands/features.js +15 -28
- package/dist/commands/results.d.ts +13 -2
- package/dist/commands/results.js +29 -2
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +63 -68
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +35 -6
- package/dist/commands/skip.d.ts +4 -0
- package/dist/commands/skip.js +45 -55
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.js +10 -0
- package/dist/commands/vet-list.js +3 -19
- package/dist/commands/vet.js +18 -25
- package/dist/commands/with-scout.d.ts +32 -0
- package/dist/commands/with-scout.js +41 -0
- package/dist/core/anti-llm-policy.js +4 -5
- package/dist/core/bootstrap.d.ts +2 -2
- package/dist/core/bootstrap.js +5 -9
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.js +20 -5
- package/dist/core/feature-discovery.d.ts +13 -1
- package/dist/core/feature-discovery.js +104 -81
- package/dist/core/gist-state-store.d.ts +13 -12
- package/dist/core/gist-state-store.js +128 -53
- package/dist/core/http-cache.d.ts +32 -2
- package/dist/core/http-cache.js +74 -19
- package/dist/core/issue-discovery.d.ts +3 -0
- package/dist/core/issue-discovery.js +51 -31
- package/dist/core/issue-eligibility.d.ts +10 -4
- package/dist/core/issue-eligibility.js +119 -67
- package/dist/core/issue-graphql.d.ts +58 -0
- package/dist/core/issue-graphql.js +108 -0
- package/dist/core/issue-vetting.d.ts +105 -8
- package/dist/core/issue-vetting.js +234 -107
- package/dist/core/local-state.d.ts +6 -2
- package/dist/core/local-state.js +23 -5
- package/dist/core/logger.d.ts +12 -4
- package/dist/core/logger.js +33 -7
- package/dist/core/personalization.d.ts +51 -18
- package/dist/core/personalization.js +101 -27
- package/dist/core/preference-fields.d.ts +47 -0
- package/dist/core/preference-fields.js +178 -0
- package/dist/core/repo-health.js +31 -15
- package/dist/core/roadmap.js +17 -3
- package/dist/core/schemas.d.ts +144 -26
- package/dist/core/schemas.js +74 -17
- package/dist/core/search-budget.d.ts +9 -0
- package/dist/core/search-budget.js +36 -3
- package/dist/core/search-phases.d.ts +0 -18
- package/dist/core/search-phases.js +27 -82
- package/dist/core/types.d.ts +146 -30
- package/dist/core/utils.js +60 -26
- package/dist/formatters/markdown.d.ts +10 -0
- package/dist/formatters/markdown.js +31 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +8 -0
- package/dist/scout.d.ts +59 -10
- package/dist/scout.js +244 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
import { enableDebug } from "./core/logger.js";
|
|
7
7
|
import { getCLIVersion } from "./core/utils.js";
|
|
8
8
|
import { formatJsonSuccess, formatJsonError } from "./formatters/json.js";
|
|
9
|
-
import { errorMessage, resolveErrorCode } from "./core/errors.js";
|
|
9
|
+
import { ValidationError, errorMessage, resolveErrorCode, } from "./core/errors.js";
|
|
10
10
|
import { hasLocalState, loadLocalState, saveLocalState, } from "./core/local-state.js";
|
|
11
11
|
import { CONCRETE_STRATEGIES, SearchStrategySchema } from "./core/schemas.js";
|
|
12
12
|
function handleCommandError(err, options) {
|
|
@@ -18,6 +18,26 @@ function handleCommandError(err, options) {
|
|
|
18
18
|
}
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Run a command body, routing any thrown error through handleCommandError so
|
|
23
|
+
* every command shares one try/catch epilogue instead of repeating it (#154).
|
|
24
|
+
*/
|
|
25
|
+
async function runAction(options, body) {
|
|
26
|
+
try {
|
|
27
|
+
await body();
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
handleCommandError(err, options);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Emoji for a vetting recommendation, shared by the search and vet renderers. */
|
|
34
|
+
function recommendationIcon(recommendation) {
|
|
35
|
+
if (recommendation === "approve")
|
|
36
|
+
return "✅";
|
|
37
|
+
if (recommendation === "skip")
|
|
38
|
+
return "❌";
|
|
39
|
+
return "⚠️";
|
|
40
|
+
}
|
|
21
41
|
const program = new Command();
|
|
22
42
|
program
|
|
23
43
|
.name("oss-scout")
|
|
@@ -34,146 +54,163 @@ program
|
|
|
34
54
|
.command("setup")
|
|
35
55
|
.description("Interactive first-run configuration")
|
|
36
56
|
.option("--json", "Output as JSON")
|
|
37
|
-
.action(async (options) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
handleCommandError(err, options);
|
|
57
|
+
.action(async (options) => runAction(options, async () => {
|
|
58
|
+
const { runSetup } = await import("./commands/setup.js");
|
|
59
|
+
const prefs = await runSetup();
|
|
60
|
+
const state = loadLocalState();
|
|
61
|
+
state.preferences = prefs;
|
|
62
|
+
state.preferencesUpdatedAt = new Date().toISOString(); // #117 merge recency
|
|
63
|
+
saveLocalState(state);
|
|
64
|
+
if (options.json) {
|
|
65
|
+
console.log(formatJsonSuccess(prefs));
|
|
50
66
|
}
|
|
51
|
-
});
|
|
67
|
+
}));
|
|
52
68
|
program
|
|
53
69
|
.command("bootstrap")
|
|
54
70
|
.description("Import starred repos and PR history from GitHub")
|
|
55
71
|
.option("--json", "Output as JSON")
|
|
56
|
-
.action(async (options) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
.action(async (options) => runAction(options, async () => {
|
|
73
|
+
const { bootstrapScout } = await import("./core/bootstrap.js");
|
|
74
|
+
const { createScout } = await import("./scout.js");
|
|
75
|
+
const { requireGitHubToken } = await import("./core/utils.js");
|
|
76
|
+
const token = requireGitHubToken();
|
|
77
|
+
const state = loadLocalState();
|
|
78
|
+
const scout = await createScout({
|
|
79
|
+
githubToken: token,
|
|
80
|
+
persistence: "provided",
|
|
81
|
+
initialState: state,
|
|
82
|
+
});
|
|
83
|
+
const result = await bootstrapScout(scout, token);
|
|
84
|
+
saveLocalState(scout.getState());
|
|
85
|
+
if (options.json) {
|
|
86
|
+
console.log(formatJsonSuccess(result));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (result.skippedDueToRateLimit) {
|
|
90
|
+
console.log("Skipped: GitHub API rate limit too low. Try again later.");
|
|
72
91
|
}
|
|
73
92
|
else {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
console.log(`Imported ${result.mergedPRCount} merged PRs, ${result.closedPRCount} closed PRs, ${result.starredRepoCount} starred repos`);
|
|
79
|
-
console.log(`Scored ${result.reposScoredCount} repositories`);
|
|
80
|
-
}
|
|
93
|
+
console.log(`Imported ${result.mergedPRCount} merged PRs, ${result.closedPRCount} closed PRs, ${result.starredRepoCount} starred repos`);
|
|
94
|
+
console.log(`Scored ${result.reposScoredCount} repositories`);
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
}));
|
|
98
|
+
program
|
|
99
|
+
.command("sync")
|
|
100
|
+
.description("Reconcile tracked open PRs (mark merged/closed) without a full bootstrap")
|
|
101
|
+
.option("--json", "Output as JSON")
|
|
102
|
+
.action(async (options) => runAction(options, async () => {
|
|
103
|
+
const { runSync } = await import("./commands/sync.js");
|
|
104
|
+
const result = await runSync();
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(formatJsonSuccess(result));
|
|
85
107
|
}
|
|
86
|
-
|
|
108
|
+
else {
|
|
109
|
+
console.log(`Synced ${result.checked} open PRs: ${result.merged} merged, ${result.closed} closed, ${result.stillOpen} still open${result.errors > 0 ? `, ${result.errors} unchecked` : ""}.`);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
87
112
|
program
|
|
88
113
|
.command("search [count]")
|
|
89
114
|
.description("Search for contributable issues using multi-strategy discovery")
|
|
90
115
|
.option("--json", "Output as JSON")
|
|
91
|
-
.option("--strategy <strategies>", `Search strategies (${CONCRETE_STRATEGIES.join(",")},all)
|
|
116
|
+
.option("--strategy <strategies>", `Search strategies (${CONCRETE_STRATEGIES.join(",")},all). Defaults to the defaultStrategy preference, or all.`)
|
|
92
117
|
.option("--prefer-languages <list>", "Comma-separated languages to soft-boost in ranking (#1244). Candidates whose repo language matches sort above equally-recommended non-matches. Does not filter results.")
|
|
93
118
|
.option("--prefer-repos <list>", "Comma-separated `owner/repo` slugs to soft-boost in ranking (#1244). Stronger weight than language match. Does not filter results.")
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
.option("--diversity-ratio <n>", "Fraction of result slots (0-1) reserved for candidates that matched NEITHER preference list (#1244). Counterweights echo-chamber bias as boosts accumulate. Default 0 (disabled).")
|
|
120
|
+
.action(async (count, options) => runAction(options, async () => {
|
|
121
|
+
if (!hasLocalState() && !options.json) {
|
|
122
|
+
// Human hint only: stdout must stay pure JSON under --json (#131)
|
|
123
|
+
console.log("💡 Run `oss-scout setup` to configure your preferences for personalized search results.\n");
|
|
124
|
+
}
|
|
125
|
+
const { runSearch } = await import("./commands/search.js");
|
|
126
|
+
const maxResults = count ? parseInt(count, 10) : 10;
|
|
127
|
+
if (isNaN(maxResults) || maxResults < 1) {
|
|
128
|
+
throw new ValidationError("count must be a positive integer");
|
|
129
|
+
}
|
|
130
|
+
const state = loadLocalState();
|
|
131
|
+
if (state.mergedPRs.length === 0 &&
|
|
132
|
+
state.starredRepos.length === 0 &&
|
|
133
|
+
state.preferences.githubUsername &&
|
|
134
|
+
!options.json) {
|
|
135
|
+
console.log("Run `oss-scout bootstrap` to import your starred repos and PR history for better results.\n");
|
|
136
|
+
}
|
|
137
|
+
// Parse --strategy. Absent means undefined so the stored
|
|
138
|
+
// defaultStrategy preference applies (discovery falls back to "all").
|
|
139
|
+
let strategies;
|
|
140
|
+
if (options.strategy !== undefined) {
|
|
141
|
+
const strategyTokens = options.strategy
|
|
113
142
|
.split(",")
|
|
114
143
|
.map((s) => s.trim())
|
|
115
144
|
.filter(Boolean);
|
|
116
|
-
|
|
145
|
+
strategies = [];
|
|
117
146
|
for (const token of strategyTokens) {
|
|
118
147
|
const parsed = SearchStrategySchema.safeParse(token);
|
|
119
148
|
if (!parsed.success) {
|
|
120
149
|
const valid = [...CONCRETE_STRATEGIES, "all"].join(", ");
|
|
121
|
-
|
|
122
|
-
token +
|
|
123
|
-
'". Valid strategies: ' +
|
|
124
|
-
valid);
|
|
125
|
-
process.exit(1);
|
|
150
|
+
throw new ValidationError(`unknown strategy "${token}". Valid strategies: ${valid}`);
|
|
126
151
|
}
|
|
127
152
|
strategies.push(parsed.data);
|
|
128
153
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
154
|
+
}
|
|
155
|
+
const splitCsv = (raw) => {
|
|
156
|
+
if (!raw)
|
|
157
|
+
return undefined;
|
|
158
|
+
const parts = raw
|
|
159
|
+
.split(",")
|
|
160
|
+
.map((s) => s.trim())
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
return parts.length > 0 ? parts : undefined;
|
|
163
|
+
};
|
|
164
|
+
let diversityRatio;
|
|
165
|
+
if (options.diversityRatio !== undefined) {
|
|
166
|
+
const parsed = Number(options.diversityRatio);
|
|
167
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
168
|
+
throw new ValidationError(`--diversity-ratio must be a number in [0, 1] (got "${options.diversityRatio}")`);
|
|
169
|
+
}
|
|
170
|
+
diversityRatio = parsed;
|
|
171
|
+
}
|
|
172
|
+
const results = await runSearch({
|
|
173
|
+
maxResults,
|
|
174
|
+
state,
|
|
175
|
+
strategies,
|
|
176
|
+
preferLanguages: splitCsv(options.preferLanguages),
|
|
177
|
+
preferRepos: splitCsv(options.preferRepos),
|
|
178
|
+
diversityRatio,
|
|
179
|
+
});
|
|
180
|
+
if (options.json) {
|
|
181
|
+
console.log(formatJsonSuccess(results));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Human-readable output
|
|
185
|
+
console.log(`\nFound ${results.candidates.length} issue candidates:\n`);
|
|
186
|
+
for (const c of results.candidates) {
|
|
187
|
+
const icon = recommendationIcon(c.recommendation);
|
|
188
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
189
|
+
? " (stalled PR, revive opportunity)"
|
|
190
|
+
: "";
|
|
191
|
+
// Personalization tag (#1244). A candidate is either boosted
|
|
192
|
+
// (matched a preference) or a diversity slot (matched none and
|
|
193
|
+
// filled a reserved slot); never both.
|
|
194
|
+
let personalizationTag = "";
|
|
195
|
+
if (c.boostScore && c.boostReasons && c.boostReasons.length > 0) {
|
|
196
|
+
personalizationTag = ` [boosted: ${c.boostReasons.join("; ")}]`;
|
|
197
|
+
}
|
|
198
|
+
else if (c.diversitySlot) {
|
|
199
|
+
personalizationTag = " [diversity slot]";
|
|
167
200
|
}
|
|
168
|
-
|
|
169
|
-
|
|
201
|
+
console.log(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]${personalizationTag}${stalledTag}`);
|
|
202
|
+
console.log(` ${c.issue.title}`);
|
|
203
|
+
console.log(` ${c.issue.url}`);
|
|
204
|
+
if (c.repoScore) {
|
|
205
|
+
console.log(` Repo: ${c.repoScore.score}/10, ${c.repoScore.mergedPRCount} merged PRs`);
|
|
170
206
|
}
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
209
|
+
if (results.rateLimitWarning) {
|
|
210
|
+
console.error(`\n⚠️ ${results.rateLimitWarning}`);
|
|
171
211
|
}
|
|
172
212
|
}
|
|
173
|
-
|
|
174
|
-
handleCommandError(err, options);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
213
|
+
}));
|
|
177
214
|
program
|
|
178
215
|
.command("features [count]")
|
|
179
216
|
.description("Surface feature-scoped opportunities in repos where you have 3+ merged PRs")
|
|
@@ -181,85 +218,77 @@ program
|
|
|
181
218
|
.option("--anchor-threshold <n>", "Override featuresAnchorThreshold (1-50)")
|
|
182
219
|
.option("--split-ratio <r>", "Override featuresSplitRatio (0-1, e.g. 0.6)")
|
|
183
220
|
.option("--broad", "Bypass anchor repos; search feature issues across the ecosystem (first-touch mode)")
|
|
184
|
-
.action(async (count, options) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
.action(async (count, options) => runAction(options, async () => {
|
|
222
|
+
const { runFeatures } = await import("./commands/features.js");
|
|
223
|
+
const maxResults = count ? parseInt(count, 10) : 10;
|
|
224
|
+
if (isNaN(maxResults) || maxResults < 1 || maxResults > 50) {
|
|
225
|
+
throw new ValidationError("count must be an integer between 1 and 50");
|
|
226
|
+
}
|
|
227
|
+
let anchorThreshold;
|
|
228
|
+
if (options.anchorThreshold !== undefined) {
|
|
229
|
+
const parsed = parseInt(options.anchorThreshold, 10);
|
|
230
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 50) {
|
|
231
|
+
throw new ValidationError("--anchor-threshold must be an integer between 1 and 50");
|
|
232
|
+
}
|
|
233
|
+
anchorThreshold = parsed;
|
|
234
|
+
}
|
|
235
|
+
let splitRatio;
|
|
236
|
+
if (options.splitRatio !== undefined) {
|
|
237
|
+
const parsed = Number.parseFloat(options.splitRatio);
|
|
238
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 1) {
|
|
239
|
+
throw new ValidationError("--split-ratio must be a number between 0 and 1");
|
|
240
|
+
}
|
|
241
|
+
splitRatio = parsed;
|
|
242
|
+
}
|
|
243
|
+
const state = loadLocalState();
|
|
244
|
+
const result = await runFeatures({
|
|
245
|
+
maxResults,
|
|
246
|
+
state,
|
|
247
|
+
anchorThreshold,
|
|
248
|
+
splitRatio,
|
|
249
|
+
broad: options.broad,
|
|
250
|
+
});
|
|
251
|
+
if (options.json) {
|
|
252
|
+
console.log(formatJsonSuccess(result));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const total = result.quickWins.length + result.biggerBets.length;
|
|
256
|
+
if (result.message) {
|
|
257
|
+
console.log(`\n${result.message}\n`);
|
|
258
|
+
}
|
|
259
|
+
if (total === 0)
|
|
260
|
+
return;
|
|
261
|
+
const headerScope = options.broad
|
|
262
|
+
? "across the ecosystem"
|
|
263
|
+
: "in your anchor repos";
|
|
264
|
+
console.log(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
|
|
265
|
+
if (!options.broad) {
|
|
266
|
+
console.log(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
|
|
267
|
+
}
|
|
268
|
+
if (result.quickWins.length) {
|
|
269
|
+
console.log("── Quick wins ─────────────────────────────────────────");
|
|
270
|
+
for (const c of result.quickWins) {
|
|
271
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
272
|
+
? " (stalled PR, revive opportunity)"
|
|
273
|
+
: "";
|
|
274
|
+
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
275
|
+
console.log(` ${c.issue.url}`);
|
|
207
276
|
}
|
|
208
|
-
|
|
277
|
+
console.log("");
|
|
209
278
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (options.json) {
|
|
219
|
-
console.log(formatJsonSuccess(result));
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
const total = result.quickWins.length + result.biggerBets.length;
|
|
223
|
-
if (result.message) {
|
|
224
|
-
console.log(`\n${result.message}\n`);
|
|
225
|
-
}
|
|
226
|
-
if (total === 0)
|
|
227
|
-
return;
|
|
228
|
-
const headerScope = options.broad
|
|
229
|
-
? "across the ecosystem"
|
|
230
|
-
: "in your anchor repos";
|
|
231
|
-
console.log(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
|
|
232
|
-
if (!options.broad) {
|
|
233
|
-
console.log(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
|
|
234
|
-
}
|
|
235
|
-
if (result.quickWins.length) {
|
|
236
|
-
console.log("── Quick wins ─────────────────────────────────────────");
|
|
237
|
-
for (const c of result.quickWins) {
|
|
238
|
-
const stalledTag = c.linkedPR?.isStalled
|
|
239
|
-
? " (stalled PR, revive opportunity)"
|
|
240
|
-
: "";
|
|
241
|
-
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
242
|
-
console.log(` ${c.issue.url}`);
|
|
243
|
-
}
|
|
244
|
-
console.log("");
|
|
245
|
-
}
|
|
246
|
-
if (result.biggerBets.length) {
|
|
247
|
-
console.log("── Bigger bets ────────────────────────────────────────");
|
|
248
|
-
for (const c of result.biggerBets) {
|
|
249
|
-
const stalledTag = c.linkedPR?.isStalled
|
|
250
|
-
? " (stalled PR, revive opportunity)"
|
|
251
|
-
: "";
|
|
252
|
-
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
253
|
-
console.log(` ${c.issue.url}`);
|
|
254
|
-
}
|
|
255
|
-
console.log("");
|
|
279
|
+
if (result.biggerBets.length) {
|
|
280
|
+
console.log("── Bigger bets ────────────────────────────────────────");
|
|
281
|
+
for (const c of result.biggerBets) {
|
|
282
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
283
|
+
? " (stalled PR, revive opportunity)"
|
|
284
|
+
: "";
|
|
285
|
+
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
286
|
+
console.log(` ${c.issue.url}`);
|
|
256
287
|
}
|
|
288
|
+
console.log("");
|
|
257
289
|
}
|
|
258
290
|
}
|
|
259
|
-
|
|
260
|
-
handleCommandError(err, options);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
291
|
+
}));
|
|
263
292
|
// ── results command ────────────────────────────────────────────────
|
|
264
293
|
const resultsCmd = program
|
|
265
294
|
.command("results")
|
|
@@ -268,166 +297,148 @@ resultsCmd
|
|
|
268
297
|
.command("show", { isDefault: true })
|
|
269
298
|
.description("Display saved search results")
|
|
270
299
|
.option("--json", "Output as JSON")
|
|
271
|
-
.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
}
|
|
300
|
+
.option("--markdown", "Output as a markdown table (for digests)")
|
|
301
|
+
.option("--new-only", "Only results first seen during/after the last search")
|
|
302
|
+
.option("--since <date>", "Only results first seen at/after this date")
|
|
303
|
+
.action(async (options) => runAction(options, async () => {
|
|
304
|
+
const { runResults } = await import("./commands/results.js");
|
|
305
|
+
const results = await runResults(options);
|
|
306
|
+
if (options.json) {
|
|
307
|
+
console.log(formatJsonSuccess(results));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (options.markdown) {
|
|
311
|
+
const { formatResultsMarkdown } = await import("./formatters/markdown.js");
|
|
312
|
+
console.log(formatResultsMarkdown(results));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (results.length === 0) {
|
|
316
|
+
console.log("\nNo saved results. Run `oss-scout search` to find issues.\n");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
console.log(`\nSaved results (${results.length}):\n`);
|
|
320
|
+
console.log(" Score Repo Issue Recommendation Title");
|
|
321
|
+
console.log(" ───── ──────────────────────────────── ────── ────────────── ─────");
|
|
322
|
+
for (const r of results) {
|
|
323
|
+
const score = String(r.viabilityScore).padStart(3);
|
|
324
|
+
const repo = r.repo.padEnd(32).slice(0, 32);
|
|
325
|
+
const issue = `#${r.number}`.padEnd(6);
|
|
326
|
+
const rec = r.recommendation.padEnd(14);
|
|
327
|
+
const title = r.title.length > 50 ? r.title.slice(0, 47) + "..." : r.title;
|
|
328
|
+
console.log(` ${score} ${repo} ${issue} ${rec} ${title}`);
|
|
329
|
+
}
|
|
330
|
+
console.log();
|
|
331
|
+
}));
|
|
301
332
|
resultsCmd
|
|
302
333
|
.command("clear")
|
|
303
334
|
.description("Clear all saved results")
|
|
304
335
|
.option("--json", "Output as JSON")
|
|
305
|
-
.action(async (options) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
console.log(formatJsonSuccess({ cleared: true }));
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
console.log("Saved results cleared.");
|
|
314
|
-
}
|
|
336
|
+
.action(async (options) => runAction(options, async () => {
|
|
337
|
+
const { runResultsClear } = await import("./commands/results.js");
|
|
338
|
+
await runResultsClear();
|
|
339
|
+
if (options.json) {
|
|
340
|
+
console.log(formatJsonSuccess({ cleared: true }));
|
|
315
341
|
}
|
|
316
|
-
|
|
317
|
-
|
|
342
|
+
else {
|
|
343
|
+
console.log("Saved results cleared.");
|
|
318
344
|
}
|
|
319
|
-
});
|
|
345
|
+
}));
|
|
320
346
|
// ── config command ──────────────────────────────────────────────────
|
|
321
347
|
const configCmd = program
|
|
322
348
|
.command("config")
|
|
323
349
|
.description("View and update preferences")
|
|
324
350
|
.option("--json", "Output as JSON")
|
|
325
|
-
.action(async (options) => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
console.log(formatJsonSuccess(getConfigData()));
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
runConfigShow();
|
|
333
|
-
}
|
|
351
|
+
.action(async (options) => runAction(options, async () => {
|
|
352
|
+
const { runConfigShow, getConfigData } = await import("./commands/config.js");
|
|
353
|
+
if (options.json) {
|
|
354
|
+
console.log(formatJsonSuccess(getConfigData()));
|
|
334
355
|
}
|
|
335
|
-
|
|
336
|
-
|
|
356
|
+
else {
|
|
357
|
+
runConfigShow();
|
|
337
358
|
}
|
|
338
|
-
});
|
|
359
|
+
}));
|
|
339
360
|
configCmd
|
|
340
361
|
.command("set <key> <value>")
|
|
341
|
-
.description(
|
|
362
|
+
.description('Update a single preference (e.g. config set minStars 100). For dash-prefixed values like the array-remove form, escape with --: config set excludeRepos -- "-spam/repo"')
|
|
342
363
|
.option("--json", "Output as JSON")
|
|
343
|
-
.action(async (key, value, options) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
console.log(formatJsonSuccess(updated));
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
console.log(`✅ Updated "${key}" successfully.`);
|
|
352
|
-
}
|
|
364
|
+
.action(async (key, value, options) => runAction(options, async () => {
|
|
365
|
+
const { runConfigSet } = await import("./commands/config.js");
|
|
366
|
+
const updated = runConfigSet(key, value);
|
|
367
|
+
if (options.json) {
|
|
368
|
+
console.log(formatJsonSuccess(updated));
|
|
353
369
|
}
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
else {
|
|
371
|
+
console.log(`✅ Updated "${key}" successfully.`);
|
|
356
372
|
}
|
|
357
|
-
});
|
|
373
|
+
}));
|
|
358
374
|
configCmd
|
|
359
375
|
.command("reset")
|
|
360
376
|
.description("Reset all preferences to defaults")
|
|
361
377
|
.option("--json", "Output as JSON")
|
|
362
|
-
.action(async (options) => {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.log(formatJsonSuccess(defaults));
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
console.log("✅ Preferences reset to defaults.");
|
|
371
|
-
}
|
|
378
|
+
.action(async (options) => runAction(options, async () => {
|
|
379
|
+
const { runConfigReset } = await import("./commands/config.js");
|
|
380
|
+
const defaults = runConfigReset();
|
|
381
|
+
if (options.json) {
|
|
382
|
+
console.log(formatJsonSuccess(defaults));
|
|
372
383
|
}
|
|
373
|
-
|
|
374
|
-
|
|
384
|
+
else {
|
|
385
|
+
console.log("✅ Preferences reset to defaults.");
|
|
375
386
|
}
|
|
376
|
-
});
|
|
387
|
+
}));
|
|
377
388
|
program
|
|
378
389
|
.command("vet-list")
|
|
379
390
|
.description("Re-vet all saved search results and classify their current status")
|
|
380
391
|
.option("--prune", "Remove unavailable issues from saved results")
|
|
381
392
|
.option("--concurrency <n>", "Max concurrent API requests (default: 5)", parseInt)
|
|
382
393
|
.option("--json", "Output as JSON")
|
|
383
|
-
.action(async (options) => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
.action(async (options) => runAction(options, async () => {
|
|
395
|
+
if (options.concurrency !== undefined &&
|
|
396
|
+
(isNaN(options.concurrency) || options.concurrency < 1)) {
|
|
397
|
+
throw new ValidationError("--concurrency must be a positive integer");
|
|
398
|
+
}
|
|
399
|
+
const { runVetList } = await import("./commands/vet-list.js");
|
|
400
|
+
const state = loadLocalState();
|
|
401
|
+
const result = await runVetList({
|
|
402
|
+
state,
|
|
403
|
+
prune: options.prune,
|
|
404
|
+
concurrency: options.concurrency,
|
|
405
|
+
});
|
|
406
|
+
if (options.json) {
|
|
407
|
+
console.log(formatJsonSuccess(result));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
if (result.results.length === 0) {
|
|
411
|
+
console.log("\nNo saved results to vet. Run `oss-scout search` first.\n");
|
|
412
|
+
return;
|
|
399
413
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
console.log(`
|
|
419
|
-
}
|
|
420
|
-
console.log(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
|
|
421
|
-
if (result.prunedCount != null) {
|
|
422
|
-
console.log(`Pruned ${result.prunedCount} unavailable issues from saved results.`);
|
|
414
|
+
console.log(`\nVet-list results (${result.summary.total}):\n`);
|
|
415
|
+
for (const r of result.results) {
|
|
416
|
+
const icon = r.status === "still_available"
|
|
417
|
+
? "✅"
|
|
418
|
+
: r.status === "claimed"
|
|
419
|
+
? "🔒"
|
|
420
|
+
: r.status === "has_pr"
|
|
421
|
+
? "🔀"
|
|
422
|
+
: r.status === "closed"
|
|
423
|
+
? "🚫"
|
|
424
|
+
: "❌";
|
|
425
|
+
const score = r.ok ? ` [${r.viabilityScore}/100]` : "";
|
|
426
|
+
console.log(` ${icon} ${r.repo}#${r.number} — ${r.status}${score}`);
|
|
427
|
+
console.log(` ${r.title}`);
|
|
428
|
+
}
|
|
429
|
+
if (result.transitions.length > 0) {
|
|
430
|
+
console.log(`\n🔔 Changes since last check (${result.transitions.length}):`);
|
|
431
|
+
for (const t of result.transitions) {
|
|
432
|
+
console.log(` ${t.repo}#${t.number}: ${t.from} → ${t.to}`);
|
|
423
433
|
}
|
|
424
|
-
console.log();
|
|
425
434
|
}
|
|
435
|
+
console.log(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
|
|
436
|
+
if (result.prunedCount != null) {
|
|
437
|
+
console.log(`Pruned ${result.prunedCount} unavailable issues from saved results.`);
|
|
438
|
+
}
|
|
439
|
+
console.log();
|
|
426
440
|
}
|
|
427
|
-
|
|
428
|
-
handleCommandError(err, options);
|
|
429
|
-
}
|
|
430
|
-
});
|
|
441
|
+
}));
|
|
431
442
|
// ── skip command ───────────────────────────────────────────────────
|
|
432
443
|
const skipCmd = program
|
|
433
444
|
.command("skip")
|
|
@@ -436,143 +447,119 @@ skipCmd
|
|
|
436
447
|
.command("add <issue-url>")
|
|
437
448
|
.description("Skip an issue by URL")
|
|
438
449
|
.option("--json", "Output as JSON")
|
|
439
|
-
.action(async (issueUrl, options) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
.action(async (issueUrl, options) => runAction(options, async () => {
|
|
451
|
+
const { runSkip } = await import("./commands/skip.js");
|
|
452
|
+
const state = loadLocalState();
|
|
453
|
+
const result = await runSkip({ issueUrl, state });
|
|
454
|
+
if (options.json) {
|
|
455
|
+
console.log(formatJsonSuccess(result));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
if (result.alreadySkipped) {
|
|
459
|
+
console.log("Issue already in skip list.");
|
|
446
460
|
}
|
|
447
461
|
else {
|
|
448
|
-
|
|
449
|
-
console.log("Issue already in skip list.");
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
console.log(`Skipped: ${issueUrl}`);
|
|
453
|
-
}
|
|
462
|
+
console.log(`Skipped: ${issueUrl}`);
|
|
454
463
|
}
|
|
455
464
|
}
|
|
456
|
-
|
|
457
|
-
handleCommandError(err, options);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
465
|
+
}));
|
|
460
466
|
skipCmd
|
|
461
467
|
.command("list")
|
|
462
468
|
.description("Show all skipped issues")
|
|
463
469
|
.option("--json", "Output as JSON")
|
|
464
|
-
.action(async (options) => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
console.log(formatJsonSuccess(results));
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
if (results.length === 0) {
|
|
473
|
-
console.log("\nNo skipped issues.\n");
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
console.log(`\nSkipped issues (${results.length}):\n`);
|
|
477
|
-
console.log(" Repo Issue Skipped Title");
|
|
478
|
-
console.log(" ──────────────────────────────── ────── ────────── ─────");
|
|
479
|
-
for (const s of results) {
|
|
480
|
-
const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
|
|
481
|
-
const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
|
|
482
|
-
const skippedDate = s.skippedAt.split("T")[0] ?? "";
|
|
483
|
-
const title = s.title.length > 50
|
|
484
|
-
? s.title.slice(0, 47) + "..."
|
|
485
|
-
: s.title || s.url;
|
|
486
|
-
console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
|
|
487
|
-
}
|
|
488
|
-
console.log();
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
handleCommandError(err, options);
|
|
470
|
+
.action(async (options) => runAction(options, async () => {
|
|
471
|
+
const { runSkipList } = await import("./commands/skip.js");
|
|
472
|
+
const results = runSkipList();
|
|
473
|
+
if (options.json) {
|
|
474
|
+
console.log(formatJsonSuccess(results));
|
|
493
475
|
}
|
|
494
|
-
|
|
476
|
+
else {
|
|
477
|
+
if (results.length === 0) {
|
|
478
|
+
console.log("\nNo skipped issues.\n");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
console.log(`\nSkipped issues (${results.length}):\n`);
|
|
482
|
+
console.log(" Repo Issue Skipped Title");
|
|
483
|
+
console.log(" ──────────────────────────────── ────── ────────── ─────");
|
|
484
|
+
for (const s of results) {
|
|
485
|
+
const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
|
|
486
|
+
const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
|
|
487
|
+
const skippedDate = s.skippedAt.split("T")[0] ?? "";
|
|
488
|
+
const title = s.title.length > 50
|
|
489
|
+
? s.title.slice(0, 47) + "..."
|
|
490
|
+
: s.title || s.url;
|
|
491
|
+
console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
|
|
492
|
+
}
|
|
493
|
+
console.log();
|
|
494
|
+
}
|
|
495
|
+
}));
|
|
495
496
|
skipCmd
|
|
496
497
|
.command("remove <issue-url>")
|
|
497
498
|
.description("Remove an issue from the skip list (unskip)")
|
|
498
499
|
.option("--json", "Output as JSON")
|
|
499
|
-
.action(async (issueUrl, options) => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
500
|
+
.action(async (issueUrl, options) => runAction(options, async () => {
|
|
501
|
+
const { runSkipRemove } = await import("./commands/skip.js");
|
|
502
|
+
const result = await runSkipRemove({ issueUrl });
|
|
503
|
+
if (options.json) {
|
|
504
|
+
console.log(formatJsonSuccess(result));
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
if (result.removed) {
|
|
508
|
+
console.log(`Removed from skip list: ${issueUrl}`);
|
|
505
509
|
}
|
|
506
510
|
else {
|
|
507
|
-
|
|
508
|
-
console.log(`Removed from skip list: ${issueUrl}`);
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
console.log("Issue was not in the skip list.");
|
|
512
|
-
}
|
|
511
|
+
console.log("Issue was not in the skip list.");
|
|
513
512
|
}
|
|
514
513
|
}
|
|
515
|
-
|
|
516
|
-
handleCommandError(err, options);
|
|
517
|
-
}
|
|
518
|
-
});
|
|
514
|
+
}));
|
|
519
515
|
skipCmd
|
|
520
516
|
.command("clear")
|
|
521
517
|
.description("Clear all skipped issues")
|
|
522
518
|
.option("--json", "Output as JSON")
|
|
523
|
-
.action(async (options) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
console.log(formatJsonSuccess({ cleared: true }));
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
console.log("Skip list cleared.");
|
|
532
|
-
}
|
|
519
|
+
.action(async (options) => runAction(options, async () => {
|
|
520
|
+
const { runSkipClear } = await import("./commands/skip.js");
|
|
521
|
+
await runSkipClear();
|
|
522
|
+
if (options.json) {
|
|
523
|
+
console.log(formatJsonSuccess({ cleared: true }));
|
|
533
524
|
}
|
|
534
|
-
|
|
535
|
-
|
|
525
|
+
else {
|
|
526
|
+
console.log("Skip list cleared.");
|
|
536
527
|
}
|
|
537
|
-
});
|
|
528
|
+
}));
|
|
538
529
|
program
|
|
539
530
|
.command("vet <issue-url>")
|
|
540
531
|
.description("Vet a specific GitHub issue for claimability and project health")
|
|
541
532
|
.option("--json", "Output as JSON")
|
|
542
|
-
.action(async (issueUrl, options) => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
533
|
+
.action(async (issueUrl, options) => runAction(options, async () => {
|
|
534
|
+
const { runVet } = await import("./commands/vet.js");
|
|
535
|
+
const state = loadLocalState();
|
|
536
|
+
const result = await runVet({ issueUrl, state });
|
|
537
|
+
if (options.json) {
|
|
538
|
+
console.log(formatJsonSuccess(result));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const icon = recommendationIcon(result.recommendation);
|
|
542
|
+
console.log(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
|
|
543
|
+
console.log(` ${result.issue.title}`);
|
|
544
|
+
console.log(` ${result.issue.url}\n`);
|
|
545
|
+
if (result.reasonsToApprove.length > 0) {
|
|
546
|
+
console.log("Reasons to approve:");
|
|
547
|
+
for (const r of result.reasonsToApprove)
|
|
548
|
+
console.log(` + ${r}`);
|
|
549
|
+
}
|
|
550
|
+
if (result.reasonsToSkip.length > 0) {
|
|
551
|
+
console.log("Reasons to skip:");
|
|
552
|
+
for (const r of result.reasonsToSkip)
|
|
553
|
+
console.log(` - ${r}`);
|
|
554
|
+
}
|
|
555
|
+
if (result.projectHealth.checkFailed) {
|
|
556
|
+
console.log(`\nProject health: unknown (check failed: ${result.projectHealth.failureReason})`);
|
|
549
557
|
}
|
|
550
558
|
else {
|
|
551
|
-
const icon = result.recommendation === "approve"
|
|
552
|
-
? "✅"
|
|
553
|
-
: result.recommendation === "skip"
|
|
554
|
-
? "❌"
|
|
555
|
-
: "⚠️";
|
|
556
|
-
console.log(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
|
|
557
|
-
console.log(` ${result.issue.title}`);
|
|
558
|
-
console.log(` ${result.issue.url}\n`);
|
|
559
|
-
if (result.reasonsToApprove.length > 0) {
|
|
560
|
-
console.log("Reasons to approve:");
|
|
561
|
-
for (const r of result.reasonsToApprove)
|
|
562
|
-
console.log(` + ${r}`);
|
|
563
|
-
}
|
|
564
|
-
if (result.reasonsToSkip.length > 0) {
|
|
565
|
-
console.log("Reasons to skip:");
|
|
566
|
-
for (const r of result.reasonsToSkip)
|
|
567
|
-
console.log(` - ${r}`);
|
|
568
|
-
}
|
|
569
559
|
console.log(`\nProject health: ${result.projectHealth.isActive ? "Active" : "Inactive"}`);
|
|
570
560
|
console.log(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
|
|
571
561
|
console.log(` CI status: ${result.projectHealth.ciStatus}`);
|
|
572
562
|
}
|
|
573
563
|
}
|
|
574
|
-
|
|
575
|
-
handleCommandError(err, options);
|
|
576
|
-
}
|
|
577
|
-
});
|
|
564
|
+
}));
|
|
578
565
|
program.parse();
|