@oss-scout/core 0.2.0 → 0.2.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.
Files changed (54) hide show
  1. package/dist/cli.bundle.cjs +42 -42
  2. package/dist/cli.js +110 -86
  3. package/dist/commands/config.d.ts +1 -1
  4. package/dist/commands/config.js +76 -72
  5. package/dist/commands/results.d.ts +1 -1
  6. package/dist/commands/results.js +1 -1
  7. package/dist/commands/search.d.ts +2 -2
  8. package/dist/commands/search.js +16 -6
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.js +27 -21
  11. package/dist/commands/validation.d.ts +1 -1
  12. package/dist/commands/validation.js +1 -1
  13. package/dist/commands/vet-list.d.ts +2 -2
  14. package/dist/commands/vet-list.js +12 -5
  15. package/dist/commands/vet.d.ts +3 -3
  16. package/dist/commands/vet.js +9 -5
  17. package/dist/core/bootstrap.d.ts +1 -1
  18. package/dist/core/bootstrap.js +20 -16
  19. package/dist/core/category-mapping.d.ts +1 -1
  20. package/dist/core/category-mapping.js +104 -13
  21. package/dist/core/errors.d.ts +8 -1
  22. package/dist/core/errors.js +31 -19
  23. package/dist/core/gist-state-store.d.ts +1 -1
  24. package/dist/core/gist-state-store.js +36 -27
  25. package/dist/core/github.d.ts +1 -1
  26. package/dist/core/github.js +5 -5
  27. package/dist/core/http-cache.js +26 -22
  28. package/dist/core/issue-discovery.d.ts +3 -3
  29. package/dist/core/issue-discovery.js +325 -277
  30. package/dist/core/issue-eligibility.d.ts +2 -2
  31. package/dist/core/issue-eligibility.js +26 -21
  32. package/dist/core/issue-filtering.js +23 -15
  33. package/dist/core/issue-scoring.js +1 -1
  34. package/dist/core/issue-vetting.d.ts +2 -2
  35. package/dist/core/issue-vetting.js +66 -53
  36. package/dist/core/local-state.d.ts +1 -1
  37. package/dist/core/local-state.js +16 -14
  38. package/dist/core/repo-health.d.ts +2 -2
  39. package/dist/core/repo-health.js +46 -35
  40. package/dist/core/schemas.d.ts +1 -1
  41. package/dist/core/schemas.js +40 -18
  42. package/dist/core/search-budget.js +3 -3
  43. package/dist/core/search-phases.d.ts +6 -6
  44. package/dist/core/search-phases.js +23 -19
  45. package/dist/core/types.d.ts +9 -9
  46. package/dist/core/types.js +15 -3
  47. package/dist/core/utils.d.ts +10 -1
  48. package/dist/core/utils.js +44 -25
  49. package/dist/formatters/json.d.ts +1 -1
  50. package/dist/index.d.ts +7 -7
  51. package/dist/index.js +5 -5
  52. package/dist/scout.d.ts +4 -5
  53. package/dist/scout.js +72 -31
  54. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,41 +2,41 @@
2
2
  /**
3
3
  * oss-scout CLI — Find open source issues personalized to your contribution history.
4
4
  */
5
- import { Command } from 'commander';
6
- import { enableDebug } from './core/logger.js';
7
- import { getCLIVersion } from './core/utils.js';
8
- import { formatJsonSuccess, formatJsonError } from './formatters/json.js';
9
- import { errorMessage, resolveErrorCode } from './core/errors.js';
10
- import { hasLocalState, loadLocalState, saveLocalState } from './core/local-state.js';
11
- import { CONCRETE_STRATEGIES, SearchStrategySchema } from './core/schemas.js';
5
+ import { Command } from "commander";
6
+ import { enableDebug } from "./core/logger.js";
7
+ import { getCLIVersion } from "./core/utils.js";
8
+ import { formatJsonSuccess, formatJsonError } from "./formatters/json.js";
9
+ import { errorMessage, resolveErrorCode } from "./core/errors.js";
10
+ import { hasLocalState, loadLocalState, saveLocalState, } from "./core/local-state.js";
11
+ import { CONCRETE_STRATEGIES, SearchStrategySchema } from "./core/schemas.js";
12
12
  function handleCommandError(err, options) {
13
13
  if (options.json) {
14
14
  console.log(formatJsonError(errorMessage(err), resolveErrorCode(err)));
15
15
  }
16
16
  else {
17
- console.error('Error:', errorMessage(err));
17
+ console.error("Error:", errorMessage(err));
18
18
  }
19
19
  process.exit(1);
20
20
  }
21
21
  const program = new Command();
22
22
  program
23
- .name('oss-scout')
24
- .description('Find open source issues personalized to your contribution history')
23
+ .name("oss-scout")
24
+ .description("Find open source issues personalized to your contribution history")
25
25
  .version(getCLIVersion())
26
- .option('--debug', 'Enable debug output');
26
+ .option("--debug", "Enable debug output");
27
27
  // Parse --debug early so it's available in preAction hooks
28
- program.hook('preAction', (_thisCommand, _actionCommand) => {
28
+ program.hook("preAction", (_thisCommand, _actionCommand) => {
29
29
  const opts = program.opts();
30
30
  if (opts.debug)
31
31
  enableDebug();
32
32
  });
33
33
  program
34
- .command('setup')
35
- .description('Interactive first-run configuration')
36
- .option('--json', 'Output as JSON')
34
+ .command("setup")
35
+ .description("Interactive first-run configuration")
36
+ .option("--json", "Output as JSON")
37
37
  .action(async (options) => {
38
38
  try {
39
- const { runSetup } = await import('./commands/setup.js');
39
+ const { runSetup } = await import("./commands/setup.js");
40
40
  const prefs = await runSetup();
41
41
  const state = loadLocalState();
42
42
  state.preferences = prefs;
@@ -50,17 +50,21 @@ program
50
50
  }
51
51
  });
52
52
  program
53
- .command('bootstrap')
54
- .description('Import starred repos and PR history from GitHub')
55
- .option('--json', 'Output as JSON')
53
+ .command("bootstrap")
54
+ .description("Import starred repos and PR history from GitHub")
55
+ .option("--json", "Output as JSON")
56
56
  .action(async (options) => {
57
57
  try {
58
- const { bootstrapScout } = await import('./core/bootstrap.js');
59
- const { createScout } = await import('./scout.js');
60
- const { requireGitHubToken } = await import('./core/utils.js');
58
+ const { bootstrapScout } = await import("./core/bootstrap.js");
59
+ const { createScout } = await import("./scout.js");
60
+ const { requireGitHubToken } = await import("./core/utils.js");
61
61
  const token = requireGitHubToken();
62
62
  const state = loadLocalState();
63
- const scout = await createScout({ githubToken: token, persistence: 'provided', initialState: state });
63
+ const scout = await createScout({
64
+ githubToken: token,
65
+ persistence: "provided",
66
+ initialState: state,
67
+ });
64
68
  const result = await bootstrapScout(scout, token);
65
69
  saveLocalState(scout.getState());
66
70
  if (options.json) {
@@ -68,7 +72,7 @@ program
68
72
  }
69
73
  else {
70
74
  if (result.skippedDueToRateLimit) {
71
- console.log('Skipped: GitHub API rate limit too low. Try again later.');
75
+ console.log("Skipped: GitHub API rate limit too low. Try again later.");
72
76
  }
73
77
  else {
74
78
  console.log(`Imported ${result.mergedPRCount} merged PRs, ${result.closedPRCount} closed PRs, ${result.starredRepoCount} starred repos`);
@@ -81,35 +85,41 @@ program
81
85
  }
82
86
  });
83
87
  program
84
- .command('search [count]')
85
- .description('Search for contributable issues using multi-strategy discovery')
86
- .option('--json', 'Output as JSON')
87
- .option('--strategy <strategies>', `Search strategies (${CONCRETE_STRATEGIES.join(',')},all)`, 'all')
88
+ .command("search [count]")
89
+ .description("Search for contributable issues using multi-strategy discovery")
90
+ .option("--json", "Output as JSON")
91
+ .option("--strategy <strategies>", `Search strategies (${CONCRETE_STRATEGIES.join(",")},all)`, "all")
88
92
  .action(async (count, options) => {
89
93
  try {
90
94
  if (!hasLocalState()) {
91
- console.log('💡 Run `oss-scout setup` to configure your preferences for personalized search results.\n');
95
+ console.log("💡 Run `oss-scout setup` to configure your preferences for personalized search results.\n");
92
96
  }
93
- const { runSearch } = await import('./commands/search.js');
97
+ const { runSearch } = await import("./commands/search.js");
94
98
  const maxResults = count ? parseInt(count, 10) : 10;
95
99
  if (isNaN(maxResults) || maxResults < 1) {
96
- console.error('Error: count must be a positive integer');
100
+ console.error("Error: count must be a positive integer");
97
101
  process.exit(1);
98
102
  }
99
103
  const state = loadLocalState();
100
104
  if (state.mergedPRs.length === 0 &&
101
105
  state.starredRepos.length === 0 &&
102
106
  state.preferences.githubUsername) {
103
- console.log('Run `oss-scout bootstrap` to import your starred repos and PR history for better results.\n');
107
+ console.log("Run `oss-scout bootstrap` to import your starred repos and PR history for better results.\n");
104
108
  }
105
109
  // Parse --strategy option
106
- const strategyTokens = (options.strategy ?? 'all').split(',').map(s => s.trim()).filter(Boolean);
110
+ const strategyTokens = (options.strategy ?? "all")
111
+ .split(",")
112
+ .map((s) => s.trim())
113
+ .filter(Boolean);
107
114
  const strategies = [];
108
115
  for (const token of strategyTokens) {
109
116
  const parsed = SearchStrategySchema.safeParse(token);
110
117
  if (!parsed.success) {
111
- const valid = [...CONCRETE_STRATEGIES, 'all'].join(', ');
112
- console.error('Error: unknown strategy "' + token + '". Valid strategies: ' + valid);
118
+ const valid = [...CONCRETE_STRATEGIES, "all"].join(", ");
119
+ console.error('Error: unknown strategy "' +
120
+ token +
121
+ '". Valid strategies: ' +
122
+ valid);
113
123
  process.exit(1);
114
124
  }
115
125
  strategies.push(parsed.data);
@@ -122,7 +132,11 @@ program
122
132
  // Human-readable output
123
133
  console.log(`\nFound ${results.candidates.length} issue candidates:\n`);
124
134
  for (const c of results.candidates) {
125
- const icon = c.recommendation === 'approve' ? '✅' : c.recommendation === 'skip' ? '❌' : '⚠️';
135
+ const icon = c.recommendation === "approve"
136
+ ? "✅"
137
+ : c.recommendation === "skip"
138
+ ? "❌"
139
+ : "⚠️";
126
140
  console.log(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]`);
127
141
  console.log(` ${c.issue.title}`);
128
142
  console.log(` ${c.issue.url}`);
@@ -142,33 +156,33 @@ program
142
156
  });
143
157
  // ── results command ────────────────────────────────────────────────
144
158
  const resultsCmd = program
145
- .command('results')
146
- .description('Show saved search results');
159
+ .command("results")
160
+ .description("Show saved search results");
147
161
  resultsCmd
148
- .command('show', { isDefault: true })
149
- .description('Display saved search results')
150
- .option('--json', 'Output as JSON')
162
+ .command("show", { isDefault: true })
163
+ .description("Display saved search results")
164
+ .option("--json", "Output as JSON")
151
165
  .action(async (options) => {
152
166
  try {
153
- const { runResults } = await import('./commands/results.js');
167
+ const { runResults } = await import("./commands/results.js");
154
168
  const results = await runResults(options);
155
169
  if (options.json) {
156
170
  console.log(formatJsonSuccess(results));
157
171
  }
158
172
  else {
159
173
  if (results.length === 0) {
160
- console.log('\nNo saved results. Run `oss-scout search` to find issues.\n');
174
+ console.log("\nNo saved results. Run `oss-scout search` to find issues.\n");
161
175
  return;
162
176
  }
163
177
  console.log(`\nSaved results (${results.length}):\n`);
164
- console.log(' Score Repo Issue Recommendation Title');
165
- console.log(' ───── ──────────────────────────────── ────── ────────────── ─────');
178
+ console.log(" Score Repo Issue Recommendation Title");
179
+ console.log(" ───── ──────────────────────────────── ────── ────────────── ─────");
166
180
  for (const r of results) {
167
181
  const score = String(r.viabilityScore).padStart(3);
168
182
  const repo = r.repo.padEnd(32).slice(0, 32);
169
183
  const issue = `#${r.number}`.padEnd(6);
170
184
  const rec = r.recommendation.padEnd(14);
171
- const title = r.title.length > 50 ? r.title.slice(0, 47) + '...' : r.title;
185
+ const title = r.title.length > 50 ? r.title.slice(0, 47) + "..." : r.title;
172
186
  console.log(` ${score} ${repo} ${issue} ${rec} ${title}`);
173
187
  }
174
188
  console.log();
@@ -179,18 +193,18 @@ resultsCmd
179
193
  }
180
194
  });
181
195
  resultsCmd
182
- .command('clear')
183
- .description('Clear all saved results')
184
- .option('--json', 'Output as JSON')
196
+ .command("clear")
197
+ .description("Clear all saved results")
198
+ .option("--json", "Output as JSON")
185
199
  .action(async (options) => {
186
200
  try {
187
- const { runResultsClear } = await import('./commands/results.js');
201
+ const { runResultsClear } = await import("./commands/results.js");
188
202
  await runResultsClear();
189
203
  if (options.json) {
190
204
  console.log(formatJsonSuccess({ cleared: true }));
191
205
  }
192
206
  else {
193
- console.log('Saved results cleared.');
207
+ console.log("Saved results cleared.");
194
208
  }
195
209
  }
196
210
  catch (err) {
@@ -199,12 +213,12 @@ resultsCmd
199
213
  });
200
214
  // ── config command ──────────────────────────────────────────────────
201
215
  const configCmd = program
202
- .command('config')
203
- .description('View and update preferences')
204
- .option('--json', 'Output as JSON')
216
+ .command("config")
217
+ .description("View and update preferences")
218
+ .option("--json", "Output as JSON")
205
219
  .action(async (options) => {
206
220
  try {
207
- const { runConfigShow, getConfigData } = await import('./commands/config.js');
221
+ const { runConfigShow, getConfigData } = await import("./commands/config.js");
208
222
  if (options.json) {
209
223
  console.log(formatJsonSuccess(getConfigData()));
210
224
  }
@@ -217,12 +231,12 @@ const configCmd = program
217
231
  }
218
232
  });
219
233
  configCmd
220
- .command('set <key> <value>')
221
- .description('Update a single preference (e.g. config set minStars 100)')
222
- .option('--json', 'Output as JSON')
234
+ .command("set <key> <value>")
235
+ .description("Update a single preference (e.g. config set minStars 100)")
236
+ .option("--json", "Output as JSON")
223
237
  .action(async (key, value, options) => {
224
238
  try {
225
- const { runConfigSet } = await import('./commands/config.js');
239
+ const { runConfigSet } = await import("./commands/config.js");
226
240
  const updated = runConfigSet(key, value);
227
241
  if (options.json) {
228
242
  console.log(formatJsonSuccess(updated));
@@ -236,18 +250,18 @@ configCmd
236
250
  }
237
251
  });
238
252
  configCmd
239
- .command('reset')
240
- .description('Reset all preferences to defaults')
241
- .option('--json', 'Output as JSON')
253
+ .command("reset")
254
+ .description("Reset all preferences to defaults")
255
+ .option("--json", "Output as JSON")
242
256
  .action(async (options) => {
243
257
  try {
244
- const { runConfigReset } = await import('./commands/config.js');
258
+ const { runConfigReset } = await import("./commands/config.js");
245
259
  const defaults = runConfigReset();
246
260
  if (options.json) {
247
261
  console.log(formatJsonSuccess(defaults));
248
262
  }
249
263
  else {
250
- console.log('✅ Preferences reset to defaults.');
264
+ console.log("✅ Preferences reset to defaults.");
251
265
  }
252
266
  }
253
267
  catch (err) {
@@ -255,18 +269,19 @@ configCmd
255
269
  }
256
270
  });
257
271
  program
258
- .command('vet-list')
259
- .description('Re-vet all saved search results and classify their current status')
260
- .option('--prune', 'Remove unavailable issues from saved results')
261
- .option('--concurrency <n>', 'Max concurrent API requests (default: 5)', parseInt)
262
- .option('--json', 'Output as JSON')
272
+ .command("vet-list")
273
+ .description("Re-vet all saved search results and classify their current status")
274
+ .option("--prune", "Remove unavailable issues from saved results")
275
+ .option("--concurrency <n>", "Max concurrent API requests (default: 5)", parseInt)
276
+ .option("--json", "Output as JSON")
263
277
  .action(async (options) => {
264
278
  try {
265
- if (options.concurrency !== undefined && (isNaN(options.concurrency) || options.concurrency < 1)) {
266
- console.error('Error: --concurrency must be a positive integer');
279
+ if (options.concurrency !== undefined &&
280
+ (isNaN(options.concurrency) || options.concurrency < 1)) {
281
+ console.error("Error: --concurrency must be a positive integer");
267
282
  process.exit(1);
268
283
  }
269
- const { runVetList } = await import('./commands/vet-list.js');
284
+ const { runVetList } = await import("./commands/vet-list.js");
270
285
  const state = loadLocalState();
271
286
  const result = await runVetList({
272
287
  state,
@@ -278,16 +293,21 @@ program
278
293
  }
279
294
  else {
280
295
  if (result.results.length === 0) {
281
- console.log('\nNo saved results to vet. Run `oss-scout search` first.\n');
296
+ console.log("\nNo saved results to vet. Run `oss-scout search` first.\n");
282
297
  return;
283
298
  }
284
299
  console.log(`\nVet-list results (${result.summary.total}):\n`);
285
300
  for (const r of result.results) {
286
- const icon = r.status === 'still_available' ? '✅' :
287
- r.status === 'claimed' ? '🔒' :
288
- r.status === 'has_pr' ? '🔀' :
289
- r.status === 'closed' ? '🚫' : '❌';
290
- const score = r.viabilityScore != null ? ` [${r.viabilityScore}/100]` : '';
301
+ const icon = r.status === "still_available"
302
+ ? "✅"
303
+ : r.status === "claimed"
304
+ ? "🔒"
305
+ : r.status === "has_pr"
306
+ ? "🔀"
307
+ : r.status === "closed"
308
+ ? "🚫"
309
+ : "❌";
310
+ const score = r.viabilityScore != null ? ` [${r.viabilityScore}/100]` : "";
291
311
  console.log(` ${icon} ${r.repo}#${r.number} — ${r.status}${score}`);
292
312
  console.log(` ${r.title}`);
293
313
  }
@@ -303,33 +323,37 @@ program
303
323
  }
304
324
  });
305
325
  program
306
- .command('vet <issue-url>')
307
- .description('Vet a specific GitHub issue for claimability and project health')
308
- .option('--json', 'Output as JSON')
326
+ .command("vet <issue-url>")
327
+ .description("Vet a specific GitHub issue for claimability and project health")
328
+ .option("--json", "Output as JSON")
309
329
  .action(async (issueUrl, options) => {
310
330
  try {
311
- const { runVet } = await import('./commands/vet.js');
331
+ const { runVet } = await import("./commands/vet.js");
312
332
  const state = loadLocalState();
313
333
  const result = await runVet({ issueUrl, state });
314
334
  if (options.json) {
315
335
  console.log(formatJsonSuccess(result));
316
336
  }
317
337
  else {
318
- const icon = result.recommendation === 'approve' ? '✅' : result.recommendation === 'skip' ? '❌' : '⚠️';
338
+ const icon = result.recommendation === "approve"
339
+ ? "✅"
340
+ : result.recommendation === "skip"
341
+ ? "❌"
342
+ : "⚠️";
319
343
  console.log(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
320
344
  console.log(` ${result.issue.title}`);
321
345
  console.log(` ${result.issue.url}\n`);
322
346
  if (result.reasonsToApprove.length > 0) {
323
- console.log('Reasons to approve:');
347
+ console.log("Reasons to approve:");
324
348
  for (const r of result.reasonsToApprove)
325
349
  console.log(` + ${r}`);
326
350
  }
327
351
  if (result.reasonsToSkip.length > 0) {
328
- console.log('Reasons to skip:');
352
+ console.log("Reasons to skip:");
329
353
  for (const r of result.reasonsToSkip)
330
354
  console.log(` - ${r}`);
331
355
  }
332
- console.log(`\nProject health: ${result.projectHealth.isActive ? 'Active' : 'Inactive'}`);
356
+ console.log(`\nProject health: ${result.projectHealth.isActive ? "Active" : "Inactive"}`);
333
357
  console.log(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
334
358
  console.log(` CI status: ${result.projectHealth.ciStatus}`);
335
359
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Config command — view and update oss-scout preferences.
3
3
  */
4
- import type { ScoutPreferences } from '../core/schemas.js';
4
+ import type { ScoutPreferences } from "../core/schemas.js";
5
5
  /**
6
6
  * Display current preferences in human-readable format.
7
7
  */
@@ -1,41 +1,37 @@
1
1
  /**
2
2
  * Config command — view and update oss-scout preferences.
3
3
  */
4
- import { loadLocalState, saveLocalState } from '../core/local-state.js';
5
- import { ScoutPreferencesSchema, IssueScopeSchema, ProjectCategorySchema, PersistenceModeSchema } from '../core/schemas.js';
6
- import { ValidationError } from '../core/errors.js';
7
- /** All known preference keys and their types. */
8
- const ARRAY_FIELDS = new Set([
9
- 'languages',
10
- 'labels',
11
- 'preferredOrgs',
12
- 'projectCategories',
13
- 'excludeRepos',
14
- 'excludeOrgs',
15
- 'aiPolicyBlocklist',
16
- ]);
17
- const NUMBER_FIELDS = new Set(['minStars', 'maxIssueAgeDays', 'minRepoScoreThreshold']);
18
- const BOOLEAN_FIELDS = new Set(['includeDocIssues']);
19
- const STRING_FIELDS = new Set(['githubUsername']);
20
- const SCOPE_FIELD = 'scope';
21
- const ENUM_FIELDS = {
22
- persistence: PersistenceModeSchema.options,
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
+ import { ScoutPreferencesSchema, IssueScopeSchema, ProjectCategorySchema, PersistenceModeSchema, SearchStrategySchema, } from "../core/schemas.js";
6
+ import { ValidationError } from "../core/errors.js";
7
+ const FIELD_CONFIGS = {
8
+ languages: { type: "array" },
9
+ labels: { type: "array" },
10
+ excludeRepos: { type: "array" },
11
+ excludeOrgs: { type: "array" },
12
+ aiPolicyBlocklist: { type: "array" },
13
+ preferredOrgs: { type: "array" },
14
+ minStars: { type: "number" },
15
+ maxIssueAgeDays: { type: "number" },
16
+ minRepoScoreThreshold: { type: "number" },
17
+ includeDocIssues: { type: "boolean" },
18
+ scope: { type: "enum-array", validValues: IssueScopeSchema.options },
19
+ projectCategories: {
20
+ type: "enum-array",
21
+ validValues: ProjectCategorySchema.options,
22
+ },
23
+ persistence: { type: "enum", validValues: PersistenceModeSchema.options },
24
+ defaultStrategy: {
25
+ type: "enum-array",
26
+ validValues: SearchStrategySchema.options,
27
+ },
28
+ githubUsername: { type: "string" },
23
29
  };
24
- const ALL_FIELDS = new Set([
25
- ...ARRAY_FIELDS,
26
- ...NUMBER_FIELDS,
27
- ...BOOLEAN_FIELDS,
28
- ...STRING_FIELDS,
29
- ...Object.keys(ENUM_FIELDS),
30
- SCOPE_FIELD,
31
- ]);
32
- const VALID_SCOPES = IssueScopeSchema.options;
33
- const VALID_CATEGORIES = ProjectCategorySchema.options;
34
30
  function parseBoolean(value) {
35
31
  const lower = value.toLowerCase();
36
- if (lower === 'true' || lower === 'yes')
32
+ if (lower === "true" || lower === "yes")
37
33
  return true;
38
- if (lower === 'false' || lower === 'no')
34
+ if (lower === "false" || lower === "no")
39
35
  return false;
40
36
  throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
41
37
  }
@@ -48,7 +44,7 @@ function parseNumber(value, key) {
48
44
  }
49
45
  function parseArrayValue(value) {
50
46
  return value
51
- .split(',')
47
+ .split(",")
52
48
  .map((s) => s.trim())
53
49
  .filter((s) => s.length > 0);
54
50
  }
@@ -56,7 +52,7 @@ function parseArrayValue(value) {
56
52
  * Apply an array update: plain set, +append, or -remove.
57
53
  */
58
54
  function updateArray(current, value) {
59
- if (value.startsWith('+')) {
55
+ if (value.startsWith("+")) {
60
56
  const toAdd = parseArrayValue(value.slice(1));
61
57
  const merged = [...current];
62
58
  for (const item of toAdd) {
@@ -65,14 +61,14 @@ function updateArray(current, value) {
65
61
  }
66
62
  return merged;
67
63
  }
68
- if (value.startsWith('-')) {
64
+ if (value.startsWith("-")) {
69
65
  const toRemove = new Set(parseArrayValue(value.slice(1)));
70
66
  return current.filter((item) => !toRemove.has(item));
71
67
  }
72
68
  return parseArrayValue(value);
73
69
  }
74
70
  function formatArray(arr) {
75
- return arr.length > 0 ? arr.join(', ') : '(none)';
71
+ return arr.length > 0 ? arr.join(", ") : "(none)";
76
72
  }
77
73
  /**
78
74
  * Display current preferences in human-readable format.
@@ -84,11 +80,11 @@ export function runConfigShow(options) {
84
80
  // JSON output handled by caller
85
81
  return;
86
82
  }
87
- console.log('\n⚙️ oss-scout preferences\n');
88
- console.log(` githubUsername: ${prefs.githubUsername || '(not set)'}`);
83
+ console.log("\n⚙️ oss-scout preferences\n");
84
+ console.log(` githubUsername: ${prefs.githubUsername || "(not set)"}`);
89
85
  console.log(` languages: ${formatArray(prefs.languages)}`);
90
86
  console.log(` labels: ${formatArray(prefs.labels)}`);
91
- console.log(` scope: ${prefs.scope ? formatArray(prefs.scope) : '(all)'}`);
87
+ console.log(` scope: ${prefs.scope ? formatArray(prefs.scope) : "(all)"}`);
92
88
  console.log(` minStars: ${prefs.minStars}`);
93
89
  console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
94
90
  console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
@@ -98,6 +94,7 @@ export function runConfigShow(options) {
98
94
  console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
99
95
  console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`);
100
96
  console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`);
97
+ console.log(` defaultStrategy: ${prefs.defaultStrategy ? formatArray(prefs.defaultStrategy) : "(all)"}`);
101
98
  console.log(` persistence: ${prefs.persistence}`);
102
99
  console.log();
103
100
  }
@@ -112,46 +109,53 @@ export function getConfigData() {
112
109
  * Update a single preference by key.
113
110
  */
114
111
  export function runConfigSet(key, value) {
115
- if (!ALL_FIELDS.has(key)) {
116
- throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${[...ALL_FIELDS].sort().join(', ')}`);
112
+ const field = FIELD_CONFIGS[key];
113
+ if (!field) {
114
+ throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${Object.keys(FIELD_CONFIGS).sort().join(", ")}`);
117
115
  }
118
116
  const state = loadLocalState();
119
117
  const prefs = { ...state.preferences };
120
- if (STRING_FIELDS.has(key)) {
121
- prefs[key] = value;
122
- }
123
- else if (BOOLEAN_FIELDS.has(key)) {
124
- prefs[key] = parseBoolean(value);
125
- }
126
- else if (NUMBER_FIELDS.has(key)) {
127
- prefs[key] = parseNumber(value, key);
128
- }
129
- else if (key === SCOPE_FIELD) {
130
- const updated = updateArray(prefs.scope ?? [], value);
131
- const invalid = updated.filter((s) => !VALID_SCOPES.includes(s));
132
- if (invalid.length > 0) {
133
- throw new ValidationError(`Invalid scope value(s): ${invalid.join(', ')}. Valid: ${VALID_SCOPES.join(', ')}`);
118
+ switch (field.type) {
119
+ case "string":
120
+ prefs[key] = value;
121
+ break;
122
+ case "boolean":
123
+ prefs[key] = parseBoolean(value);
124
+ break;
125
+ case "number":
126
+ prefs[key] = parseNumber(value, key);
127
+ break;
128
+ case "array": {
129
+ const current = prefs[key] ?? [];
130
+ prefs[key] = updateArray(current, value);
131
+ break;
134
132
  }
135
- prefs.scope = updated.length > 0 ? updated : undefined;
136
- }
137
- else if (key === 'projectCategories') {
138
- const updated = updateArray(prefs.projectCategories, value);
139
- const invalid = updated.filter((s) => !VALID_CATEGORIES.includes(s));
140
- if (invalid.length > 0) {
141
- throw new ValidationError(`Invalid category value(s): ${invalid.join(', ')}. Valid: ${VALID_CATEGORIES.join(', ')}`);
133
+ case "enum": {
134
+ const validValues = field.validValues;
135
+ if (!validValues.includes(value)) {
136
+ throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(", ")}`);
137
+ }
138
+ prefs[key] = value;
139
+ break;
142
140
  }
143
- prefs.projectCategories = updated;
144
- }
145
- else if (key in ENUM_FIELDS) {
146
- const validValues = ENUM_FIELDS[key];
147
- if (!validValues.includes(value)) {
148
- throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(', ')}`);
141
+ case "enum-array": {
142
+ const current = prefs[key] ?? [];
143
+ const updated = updateArray(current, value);
144
+ const validValues = field.validValues;
145
+ const invalid = updated.filter((s) => !validValues.includes(s));
146
+ if (invalid.length > 0) {
147
+ throw new ValidationError(`Invalid value(s) for "${key}": ${invalid.join(", ")}. Valid: ${validValues.join(", ")}`);
148
+ }
149
+ // For 'scope', empty array means undefined (all scopes)
150
+ if (key === "scope") {
151
+ prefs[key] =
152
+ updated.length > 0 ? updated : undefined;
153
+ }
154
+ else {
155
+ prefs[key] = updated;
156
+ }
157
+ break;
149
158
  }
150
- prefs[key] = value;
151
- }
152
- else if (ARRAY_FIELDS.has(key)) {
153
- const current = prefs[key] ?? [];
154
- prefs[key] = updateArray(current, value);
155
159
  }
156
160
  // Validate the full preferences object
157
161
  const validated = ScoutPreferencesSchema.parse(prefs);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Results command — display and manage saved search results.
3
3
  */
4
- import type { SavedCandidate } from '../core/schemas.js';
4
+ import type { SavedCandidate } from "../core/schemas.js";
5
5
  export declare function runResults(_options: {
6
6
  json?: boolean;
7
7
  }): Promise<SavedCandidate[]>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Results command — display and manage saved search results.
3
3
  */
4
- import { loadLocalState, saveLocalState } from '../core/local-state.js';
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
5
  export async function runResults(_options) {
6
6
  const state = loadLocalState();
7
7
  return state.savedResults ?? [];