@oss-scout/core 0.11.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 +78 -61
- package/dist/cli.js +401 -425
- 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.js +63 -70
- 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 +2 -0
- package/dist/core/issue-discovery.js +44 -29
- 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 +15 -10
- package/dist/core/personalization.js +30 -22
- 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 +136 -38
- 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 -20
- 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,157 +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
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).")
|
|
95
|
-
.action(async (count, options) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
142
|
.split(",")
|
|
115
143
|
.map((s) => s.trim())
|
|
116
144
|
.filter(Boolean);
|
|
117
|
-
|
|
145
|
+
strategies = [];
|
|
118
146
|
for (const token of strategyTokens) {
|
|
119
147
|
const parsed = SearchStrategySchema.safeParse(token);
|
|
120
148
|
if (!parsed.success) {
|
|
121
149
|
const valid = [...CONCRETE_STRATEGIES, "all"].join(", ");
|
|
122
|
-
|
|
123
|
-
token +
|
|
124
|
-
'". Valid strategies: ' +
|
|
125
|
-
valid);
|
|
126
|
-
process.exit(1);
|
|
150
|
+
throw new ValidationError(`unknown strategy "${token}". Valid strategies: ${valid}`);
|
|
127
151
|
}
|
|
128
152
|
strategies.push(parsed.data);
|
|
129
153
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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("; ")}]`;
|
|
145
197
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const results = await runSearch({
|
|
149
|
-
maxResults,
|
|
150
|
-
state,
|
|
151
|
-
strategies,
|
|
152
|
-
preferLanguages: splitCsv(options.preferLanguages),
|
|
153
|
-
preferRepos: splitCsv(options.preferRepos),
|
|
154
|
-
diversityRatio,
|
|
155
|
-
});
|
|
156
|
-
if (options.json) {
|
|
157
|
-
console.log(formatJsonSuccess(results));
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
// Human-readable output
|
|
161
|
-
console.log(`\nFound ${results.candidates.length} issue candidates:\n`);
|
|
162
|
-
for (const c of results.candidates) {
|
|
163
|
-
const icon = c.recommendation === "approve"
|
|
164
|
-
? "✅"
|
|
165
|
-
: c.recommendation === "skip"
|
|
166
|
-
? "❌"
|
|
167
|
-
: "⚠️";
|
|
168
|
-
const stalledTag = c.linkedPR?.isStalled
|
|
169
|
-
? " (stalled PR, revive opportunity)"
|
|
170
|
-
: "";
|
|
171
|
-
console.log(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]${stalledTag}`);
|
|
172
|
-
console.log(` ${c.issue.title}`);
|
|
173
|
-
console.log(` ${c.issue.url}`);
|
|
174
|
-
if (c.repoScore) {
|
|
175
|
-
console.log(` Repo: ${c.repoScore.score}/10, ${c.repoScore.mergedPRCount} merged PRs`);
|
|
176
|
-
}
|
|
177
|
-
console.log();
|
|
198
|
+
else if (c.diversitySlot) {
|
|
199
|
+
personalizationTag = " [diversity slot]";
|
|
178
200
|
}
|
|
179
|
-
|
|
180
|
-
|
|
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`);
|
|
181
206
|
}
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
209
|
+
if (results.rateLimitWarning) {
|
|
210
|
+
console.error(`\n⚠️ ${results.rateLimitWarning}`);
|
|
182
211
|
}
|
|
183
212
|
}
|
|
184
|
-
|
|
185
|
-
handleCommandError(err, options);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
213
|
+
}));
|
|
188
214
|
program
|
|
189
215
|
.command("features [count]")
|
|
190
216
|
.description("Surface feature-scoped opportunities in repos where you have 3+ merged PRs")
|
|
@@ -192,85 +218,77 @@ program
|
|
|
192
218
|
.option("--anchor-threshold <n>", "Override featuresAnchorThreshold (1-50)")
|
|
193
219
|
.option("--split-ratio <r>", "Override featuresSplitRatio (0-1, e.g. 0.6)")
|
|
194
220
|
.option("--broad", "Bypass anchor repos; search feature issues across the ecosystem (first-touch mode)")
|
|
195
|
-
.action(async (count, options) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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}`);
|
|
218
276
|
}
|
|
219
|
-
|
|
277
|
+
console.log("");
|
|
220
278
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (options.json) {
|
|
230
|
-
console.log(formatJsonSuccess(result));
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
const total = result.quickWins.length + result.biggerBets.length;
|
|
234
|
-
if (result.message) {
|
|
235
|
-
console.log(`\n${result.message}\n`);
|
|
236
|
-
}
|
|
237
|
-
if (total === 0)
|
|
238
|
-
return;
|
|
239
|
-
const headerScope = options.broad
|
|
240
|
-
? "across the ecosystem"
|
|
241
|
-
: "in your anchor repos";
|
|
242
|
-
console.log(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
|
|
243
|
-
if (!options.broad) {
|
|
244
|
-
console.log(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
|
|
245
|
-
}
|
|
246
|
-
if (result.quickWins.length) {
|
|
247
|
-
console.log("── Quick wins ─────────────────────────────────────────");
|
|
248
|
-
for (const c of result.quickWins) {
|
|
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("");
|
|
256
|
-
}
|
|
257
|
-
if (result.biggerBets.length) {
|
|
258
|
-
console.log("── Bigger bets ────────────────────────────────────────");
|
|
259
|
-
for (const c of result.biggerBets) {
|
|
260
|
-
const stalledTag = c.linkedPR?.isStalled
|
|
261
|
-
? " (stalled PR, revive opportunity)"
|
|
262
|
-
: "";
|
|
263
|
-
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
264
|
-
console.log(` ${c.issue.url}`);
|
|
265
|
-
}
|
|
266
|
-
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}`);
|
|
267
287
|
}
|
|
288
|
+
console.log("");
|
|
268
289
|
}
|
|
269
290
|
}
|
|
270
|
-
|
|
271
|
-
handleCommandError(err, options);
|
|
272
|
-
}
|
|
273
|
-
});
|
|
291
|
+
}));
|
|
274
292
|
// ── results command ────────────────────────────────────────────────
|
|
275
293
|
const resultsCmd = program
|
|
276
294
|
.command("results")
|
|
@@ -279,166 +297,148 @@ resultsCmd
|
|
|
279
297
|
.command("show", { isDefault: true })
|
|
280
298
|
.description("Display saved search results")
|
|
281
299
|
.option("--json", "Output as JSON")
|
|
282
|
-
.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
}
|
|
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
|
+
}));
|
|
312
332
|
resultsCmd
|
|
313
333
|
.command("clear")
|
|
314
334
|
.description("Clear all saved results")
|
|
315
335
|
.option("--json", "Output as JSON")
|
|
316
|
-
.action(async (options) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(formatJsonSuccess({ cleared: true }));
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
console.log("Saved results cleared.");
|
|
325
|
-
}
|
|
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 }));
|
|
326
341
|
}
|
|
327
|
-
|
|
328
|
-
|
|
342
|
+
else {
|
|
343
|
+
console.log("Saved results cleared.");
|
|
329
344
|
}
|
|
330
|
-
});
|
|
345
|
+
}));
|
|
331
346
|
// ── config command ──────────────────────────────────────────────────
|
|
332
347
|
const configCmd = program
|
|
333
348
|
.command("config")
|
|
334
349
|
.description("View and update preferences")
|
|
335
350
|
.option("--json", "Output as JSON")
|
|
336
|
-
.action(async (options) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.log(formatJsonSuccess(getConfigData()));
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
runConfigShow();
|
|
344
|
-
}
|
|
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()));
|
|
345
355
|
}
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
else {
|
|
357
|
+
runConfigShow();
|
|
348
358
|
}
|
|
349
|
-
});
|
|
359
|
+
}));
|
|
350
360
|
configCmd
|
|
351
361
|
.command("set <key> <value>")
|
|
352
|
-
.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"')
|
|
353
363
|
.option("--json", "Output as JSON")
|
|
354
|
-
.action(async (key, value, options) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
console.log(formatJsonSuccess(updated));
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
console.log(`✅ Updated "${key}" successfully.`);
|
|
363
|
-
}
|
|
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));
|
|
364
369
|
}
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
else {
|
|
371
|
+
console.log(`✅ Updated "${key}" successfully.`);
|
|
367
372
|
}
|
|
368
|
-
});
|
|
373
|
+
}));
|
|
369
374
|
configCmd
|
|
370
375
|
.command("reset")
|
|
371
376
|
.description("Reset all preferences to defaults")
|
|
372
377
|
.option("--json", "Output as JSON")
|
|
373
|
-
.action(async (options) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
console.log(formatJsonSuccess(defaults));
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
console.log("✅ Preferences reset to defaults.");
|
|
382
|
-
}
|
|
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));
|
|
383
383
|
}
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
else {
|
|
385
|
+
console.log("✅ Preferences reset to defaults.");
|
|
386
386
|
}
|
|
387
|
-
});
|
|
387
|
+
}));
|
|
388
388
|
program
|
|
389
389
|
.command("vet-list")
|
|
390
390
|
.description("Re-vet all saved search results and classify their current status")
|
|
391
391
|
.option("--prune", "Remove unavailable issues from saved results")
|
|
392
392
|
.option("--concurrency <n>", "Max concurrent API requests (default: 5)", parseInt)
|
|
393
393
|
.option("--json", "Output as JSON")
|
|
394
|
-
.action(async (options) => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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;
|
|
410
413
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
console.log(`
|
|
430
|
-
}
|
|
431
|
-
console.log(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
|
|
432
|
-
if (result.prunedCount != null) {
|
|
433
|
-
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}`);
|
|
434
433
|
}
|
|
435
|
-
console.log();
|
|
436
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();
|
|
437
440
|
}
|
|
438
|
-
|
|
439
|
-
handleCommandError(err, options);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
441
|
+
}));
|
|
442
442
|
// ── skip command ───────────────────────────────────────────────────
|
|
443
443
|
const skipCmd = program
|
|
444
444
|
.command("skip")
|
|
@@ -447,143 +447,119 @@ skipCmd
|
|
|
447
447
|
.command("add <issue-url>")
|
|
448
448
|
.description("Skip an issue by URL")
|
|
449
449
|
.option("--json", "Output as JSON")
|
|
450
|
-
.action(async (issueUrl, options) => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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.");
|
|
457
460
|
}
|
|
458
461
|
else {
|
|
459
|
-
|
|
460
|
-
console.log("Issue already in skip list.");
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
console.log(`Skipped: ${issueUrl}`);
|
|
464
|
-
}
|
|
462
|
+
console.log(`Skipped: ${issueUrl}`);
|
|
465
463
|
}
|
|
466
464
|
}
|
|
467
|
-
|
|
468
|
-
handleCommandError(err, options);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
465
|
+
}));
|
|
471
466
|
skipCmd
|
|
472
467
|
.command("list")
|
|
473
468
|
.description("Show all skipped issues")
|
|
474
469
|
.option("--json", "Output as JSON")
|
|
475
|
-
.action(async (options) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
console.log(formatJsonSuccess(results));
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
if (results.length === 0) {
|
|
484
|
-
console.log("\nNo skipped issues.\n");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
console.log(`\nSkipped issues (${results.length}):\n`);
|
|
488
|
-
console.log(" Repo Issue Skipped Title");
|
|
489
|
-
console.log(" ──────────────────────────────── ────── ────────── ─────");
|
|
490
|
-
for (const s of results) {
|
|
491
|
-
const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
|
|
492
|
-
const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
|
|
493
|
-
const skippedDate = s.skippedAt.split("T")[0] ?? "";
|
|
494
|
-
const title = s.title.length > 50
|
|
495
|
-
? s.title.slice(0, 47) + "..."
|
|
496
|
-
: s.title || s.url;
|
|
497
|
-
console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
|
|
498
|
-
}
|
|
499
|
-
console.log();
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
catch (err) {
|
|
503
|
-
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));
|
|
504
475
|
}
|
|
505
|
-
|
|
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
|
+
}));
|
|
506
496
|
skipCmd
|
|
507
497
|
.command("remove <issue-url>")
|
|
508
498
|
.description("Remove an issue from the skip list (unskip)")
|
|
509
499
|
.option("--json", "Output as JSON")
|
|
510
|
-
.action(async (issueUrl, options) => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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}`);
|
|
516
509
|
}
|
|
517
510
|
else {
|
|
518
|
-
|
|
519
|
-
console.log(`Removed from skip list: ${issueUrl}`);
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
console.log("Issue was not in the skip list.");
|
|
523
|
-
}
|
|
511
|
+
console.log("Issue was not in the skip list.");
|
|
524
512
|
}
|
|
525
513
|
}
|
|
526
|
-
|
|
527
|
-
handleCommandError(err, options);
|
|
528
|
-
}
|
|
529
|
-
});
|
|
514
|
+
}));
|
|
530
515
|
skipCmd
|
|
531
516
|
.command("clear")
|
|
532
517
|
.description("Clear all skipped issues")
|
|
533
518
|
.option("--json", "Output as JSON")
|
|
534
|
-
.action(async (options) => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
console.log(formatJsonSuccess({ cleared: true }));
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
console.log("Skip list cleared.");
|
|
543
|
-
}
|
|
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 }));
|
|
544
524
|
}
|
|
545
|
-
|
|
546
|
-
|
|
525
|
+
else {
|
|
526
|
+
console.log("Skip list cleared.");
|
|
547
527
|
}
|
|
548
|
-
});
|
|
528
|
+
}));
|
|
549
529
|
program
|
|
550
530
|
.command("vet <issue-url>")
|
|
551
531
|
.description("Vet a specific GitHub issue for claimability and project health")
|
|
552
532
|
.option("--json", "Output as JSON")
|
|
553
|
-
.action(async (issueUrl, options) => {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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})`);
|
|
560
557
|
}
|
|
561
558
|
else {
|
|
562
|
-
const icon = result.recommendation === "approve"
|
|
563
|
-
? "✅"
|
|
564
|
-
: result.recommendation === "skip"
|
|
565
|
-
? "❌"
|
|
566
|
-
: "⚠️";
|
|
567
|
-
console.log(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
|
|
568
|
-
console.log(` ${result.issue.title}`);
|
|
569
|
-
console.log(` ${result.issue.url}\n`);
|
|
570
|
-
if (result.reasonsToApprove.length > 0) {
|
|
571
|
-
console.log("Reasons to approve:");
|
|
572
|
-
for (const r of result.reasonsToApprove)
|
|
573
|
-
console.log(` + ${r}`);
|
|
574
|
-
}
|
|
575
|
-
if (result.reasonsToSkip.length > 0) {
|
|
576
|
-
console.log("Reasons to skip:");
|
|
577
|
-
for (const r of result.reasonsToSkip)
|
|
578
|
-
console.log(` - ${r}`);
|
|
579
|
-
}
|
|
580
559
|
console.log(`\nProject health: ${result.projectHealth.isActive ? "Active" : "Inactive"}`);
|
|
581
560
|
console.log(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
|
|
582
561
|
console.log(` CI status: ${result.projectHealth.ciStatus}`);
|
|
583
562
|
}
|
|
584
563
|
}
|
|
585
|
-
|
|
586
|
-
handleCommandError(err, options);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
564
|
+
}));
|
|
589
565
|
program.parse();
|