@oss-scout/core 0.1.1 → 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 +48 -48
  2. package/dist/cli.js +110 -86
  3. package/dist/commands/config.d.ts +1 -1
  4. package/dist/commands/config.js +77 -71
  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 -270
  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 +3 -1
  41. package/dist/core/schemas.js +41 -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
@@ -1,39 +1,58 @@
1
1
  /**
2
2
  * Shared utility functions for oss-scout.
3
3
  */
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import * as os from 'os';
7
- import { execFileSync } from 'child_process';
8
- import { ConfigurationError, errorMessage } from './errors.js';
9
- import { debug } from './logger.js';
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as os from "os";
7
+ import { execFileSync } from "child_process";
8
+ import { ConfigurationError, errorMessage } from "./errors.js";
9
+ import { debug } from "./logger.js";
10
10
  export function sleep(ms) {
11
11
  return new Promise((resolve) => setTimeout(resolve, ms));
12
12
  }
13
- const MODULE = 'utils';
13
+ const MODULE = "utils";
14
14
  let cachedGitHubToken = null;
15
15
  let tokenFetchAttempted = false;
16
16
  export function getDataDir() {
17
- const dir = path.join(os.homedir(), '.oss-scout');
17
+ const dir = path.join(os.homedir(), ".oss-scout");
18
18
  if (!fs.existsSync(dir)) {
19
19
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
20
20
  }
21
21
  return dir;
22
22
  }
23
23
  export function getCacheDir() {
24
- const dir = path.join(getDataDir(), 'cache');
24
+ const dir = path.join(getDataDir(), "cache");
25
25
  if (!fs.existsSync(dir)) {
26
26
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
27
  }
28
28
  return dir;
29
29
  }
30
+ /**
31
+ * Extract "owner/repo" from any GitHub URL format:
32
+ * - https://github.com/owner/repo
33
+ * - https://github.com/owner/repo/pull/123
34
+ * - https://github.com/owner/repo/issues/123
35
+ * - https://api.github.com/repos/owner/repo
36
+ * - https://api.github.com/repos/owner/repo/...
37
+ */
38
+ export function extractRepoFromUrl(url) {
39
+ // API URLs: https://api.github.com/repos/owner/repo[/...]
40
+ const apiMatch = url.match(/api\.github\.com\/repos\/([^/]+\/[^/]+)/);
41
+ if (apiMatch)
42
+ return apiMatch[1];
43
+ // Web URLs: https://github.com/owner/repo[/...]
44
+ const webMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
45
+ if (webMatch)
46
+ return webMatch[1];
47
+ return null;
48
+ }
30
49
  const OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
31
50
  const REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
32
51
  function isValidOwnerRepo(owner, repo) {
33
52
  return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
34
53
  }
35
54
  export function parseGitHubUrl(url) {
36
- if (!url.startsWith('https://github.com/'))
55
+ if (!url.startsWith("https://github.com/"))
37
56
  return null;
38
57
  const prMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
39
58
  if (prMatch) {
@@ -41,7 +60,7 @@ export function parseGitHubUrl(url) {
41
60
  const repo = prMatch[2];
42
61
  if (!isValidOwnerRepo(owner, repo))
43
62
  return null;
44
- return { owner, repo, number: parseInt(prMatch[3], 10), type: 'pull' };
63
+ return { owner, repo, number: parseInt(prMatch[3], 10), type: "pull" };
45
64
  }
46
65
  const issueMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
47
66
  if (issueMatch) {
@@ -49,7 +68,7 @@ export function parseGitHubUrl(url) {
49
68
  const repo = issueMatch[2];
50
69
  if (!isValidOwnerRepo(owner, repo))
51
70
  return null;
52
- return { owner, repo, number: parseInt(issueMatch[3], 10), type: 'issues' };
71
+ return { owner, repo, number: parseInt(issueMatch[3], 10), type: "issues" };
53
72
  }
54
73
  return null;
55
74
  }
@@ -58,12 +77,12 @@ export function daysBetween(from, to = new Date()) {
58
77
  }
59
78
  export function getCLIVersion() {
60
79
  try {
61
- const pkgPath = path.join(path.dirname(process.argv[1]), '..', 'package.json');
62
- return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
80
+ const pkgPath = path.join(path.dirname(process.argv[1]), "..", "package.json");
81
+ return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
63
82
  }
64
83
  catch (err) {
65
84
  debug(MODULE, `Could not read CLI version: ${errorMessage(err)}`);
66
- return 'unknown';
85
+ return "unknown";
67
86
  }
68
87
  }
69
88
  export function getGitHubToken() {
@@ -77,30 +96,30 @@ export function getGitHubToken() {
77
96
  return cachedGitHubToken;
78
97
  }
79
98
  try {
80
- const token = execFileSync('gh', ['auth', 'token'], {
81
- encoding: 'utf-8',
82
- stdio: ['pipe', 'pipe', 'pipe'],
99
+ const token = execFileSync("gh", ["auth", "token"], {
100
+ encoding: "utf-8",
101
+ stdio: ["pipe", "pipe", "pipe"],
83
102
  timeout: 2000,
84
103
  }).trim();
85
104
  if (token && token.length > 0) {
86
105
  cachedGitHubToken = token;
87
- debug(MODULE, 'Using GitHub token from gh CLI');
106
+ debug(MODULE, "Using GitHub token from gh CLI");
88
107
  return cachedGitHubToken;
89
108
  }
90
109
  }
91
110
  catch (err) {
92
- debug(MODULE, 'gh auth token failed', err);
111
+ debug(MODULE, "gh auth token failed", err);
93
112
  }
94
113
  return null;
95
114
  }
96
115
  export function requireGitHubToken() {
97
116
  const token = getGitHubToken();
98
117
  if (!token) {
99
- throw new ConfigurationError('GitHub authentication required.\n\n' +
100
- 'Options:\n' +
101
- ' 1. Use gh CLI: gh auth login\n' +
102
- ' 2. Set GITHUB_TOKEN environment variable\n\n' +
103
- 'The gh CLI is recommended - install from https://cli.github.com');
118
+ throw new ConfigurationError("GitHub authentication required.\n\n" +
119
+ "Options:\n" +
120
+ " 1. Use gh CLI: gh auth login\n" +
121
+ " 2. Set GITHUB_TOKEN environment variable\n\n" +
122
+ "The gh CLI is recommended - install from https://cli.github.com");
104
123
  }
105
124
  return token;
106
125
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * JSON output formatter for oss-scout CLI.
3
3
  */
4
- import type { ErrorCode } from '../core/errors.js';
4
+ import type { ErrorCode } from "../core/errors.js";
5
5
  export declare function formatJsonSuccess<T>(data: T): string;
6
6
  export declare function formatJsonError(error: string, errorCode?: ErrorCode): string;
package/dist/index.d.ts CHANGED
@@ -14,10 +14,10 @@
14
14
  *
15
15
  * @packageDocumentation
16
16
  */
17
- export { createScout, OssScout } from './scout.js';
18
- export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from './core/types.js';
19
- export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, SearchStrategy, } from './core/schemas.js';
20
- export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from './core/schemas.js';
21
- export { requireGitHubToken, getGitHubToken } from './core/utils.js';
22
- export { IssueDiscovery } from './core/issue-discovery.js';
23
- export { IssueVetter, type ScoutStateReader } from './core/issue-vetting.js';
17
+ export { createScout, OssScout } from "./scout.js";
18
+ export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
19
+ export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, SearchStrategy, } from "./core/schemas.js";
20
+ export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from "./core/schemas.js";
21
+ export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
22
+ export { IssueDiscovery } from "./core/issue-discovery.js";
23
+ export { IssueVetter, type ScoutStateReader } from "./core/issue-vetting.js";
package/dist/index.js CHANGED
@@ -15,11 +15,11 @@
15
15
  * @packageDocumentation
16
16
  */
17
17
  // Main API
18
- export { createScout, OssScout } from './scout.js';
18
+ export { createScout, OssScout } from "./scout.js";
19
19
  // Schemas (for consumers who need runtime validation)
20
- export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from './core/schemas.js';
20
+ export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from "./core/schemas.js";
21
21
  // Utilities
22
- export { requireGitHubToken, getGitHubToken } from './core/utils.js';
22
+ export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
23
23
  // Internal classes (for advanced use)
24
- export { IssueDiscovery } from './core/issue-discovery.js';
25
- export { IssueVetter } from './core/issue-vetting.js';
24
+ export { IssueDiscovery } from "./core/issue-discovery.js";
25
+ export { IssueVetter } from "./core/issue-vetting.js";
package/dist/scout.d.ts CHANGED
@@ -4,10 +4,10 @@
4
4
  * Provides personalized issue discovery, vetting, and scoring.
5
5
  * Implements ScoutStateReader to bridge state with the search engine.
6
6
  */
7
- import type { ScoutStateReader } from './core/issue-vetting.js';
8
- import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate } from './core/schemas.js';
9
- import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from './core/types.js';
10
- import { GistStateStore } from './core/gist-state-store.js';
7
+ import type { ScoutStateReader } from "./core/issue-vetting.js";
8
+ import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate } from "./core/schemas.js";
9
+ import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
10
+ import { GistStateStore } from "./core/gist-state-store.js";
11
11
  /**
12
12
  * Create an OssScout instance.
13
13
  *
@@ -114,7 +114,6 @@ export declare class OssScout implements ScoutStateReader {
114
114
  * Get the full state snapshot for serialization or external consumption.
115
115
  */
116
116
  getState(): Readonly<ScoutState>;
117
- private extractRepoFromUrl;
118
117
  private updateRepoScoreFromPRs;
119
118
  /**
120
119
  * Calculate repo score (1-10) from observed data.
package/dist/scout.js CHANGED
@@ -4,12 +4,55 @@
4
4
  * Provides personalized issue discovery, vetting, and scoring.
5
5
  * Implements ScoutStateReader to bridge state with the search engine.
6
6
  */
7
- import { IssueDiscovery } from './core/issue-discovery.js';
8
- import { ScoutStateSchema } from './core/schemas.js';
9
- import { GistStateStore, mergeStates } from './core/gist-state-store.js';
10
- import { getOctokit } from './core/github.js';
11
- import { loadLocalState } from './core/local-state.js';
12
- import { warn } from './core/logger.js';
7
+ import { IssueDiscovery } from "./core/issue-discovery.js";
8
+ import { ScoutStateSchema } from "./core/schemas.js";
9
+ import { GistStateStore, mergeStates } from "./core/gist-state-store.js";
10
+ import { getOctokit } from "./core/github.js";
11
+ import { loadLocalState } from "./core/local-state.js";
12
+ import { warn } from "./core/logger.js";
13
+ import { extractRepoFromUrl } from "./core/utils.js";
14
+ /** Wrap a real Octokit instance as GistOctokitLike without unsafe double casts. */
15
+ function toGistOctokit(octokit) {
16
+ return {
17
+ gists: {
18
+ async get(params) {
19
+ const { data } = await octokit.gists.get(params);
20
+ if (!data.id)
21
+ throw new Error("Gist get returned no id");
22
+ const files = data.files
23
+ ? Object.fromEntries(Object.entries(data.files).map(([k, v]) => [
24
+ k,
25
+ v ? { content: v.content } : undefined,
26
+ ]))
27
+ : null;
28
+ return { data: { id: data.id, files } };
29
+ },
30
+ async create(params) {
31
+ const { data } = await octokit.gists.create(params);
32
+ if (!data.id)
33
+ throw new Error("Gist create returned no id");
34
+ return { data: { id: data.id } };
35
+ },
36
+ async update(params) {
37
+ const { data } = await octokit.gists.update(params);
38
+ if (!data.id)
39
+ throw new Error("Gist update returned no id");
40
+ return { data: { id: data.id } };
41
+ },
42
+ async list(params) {
43
+ const { data } = await octokit.gists.list(params);
44
+ return {
45
+ data: data
46
+ .filter((g) => g.id)
47
+ .map((g) => ({
48
+ id: g.id,
49
+ description: g.description ?? null,
50
+ })),
51
+ };
52
+ },
53
+ },
54
+ };
55
+ }
13
56
  /**
14
57
  * Create an OssScout instance.
15
58
  *
@@ -34,14 +77,14 @@ import { warn } from './core/logger.js';
34
77
  export async function createScout(config) {
35
78
  let state;
36
79
  let gistStore = null;
37
- if (config.persistence === 'provided') {
80
+ if (config.persistence === "provided") {
38
81
  state = config.initialState;
39
82
  }
40
- else if (config.persistence === 'gist') {
41
- gistStore = new GistStateStore(getOctokit(config.githubToken));
83
+ else if (config.persistence === "gist") {
84
+ gistStore = new GistStateStore(toGistOctokit(getOctokit(config.githubToken)));
42
85
  const result = await gistStore.bootstrap();
43
86
  if (result.degraded) {
44
- warn('scout', 'Gist sync unavailable — running in offline mode. Changes will only be saved locally.');
87
+ warn("scout", "Gist sync unavailable — running in offline mode. Changes will only be saved locally.");
45
88
  }
46
89
  const localState = loadLocalState();
47
90
  state = mergeStates(localState, result.state);
@@ -126,13 +169,13 @@ export class OssScout {
126
169
  })
127
170
  .catch((error) => {
128
171
  const msg = error instanceof Error ? error.message : String(error);
129
- const isGone = msg.includes('Not Found') || msg.includes('410');
172
+ const isGone = msg.includes("Not Found") || msg.includes("410");
130
173
  results.push({
131
174
  issueUrl: item.issueUrl,
132
175
  repo: item.repo,
133
176
  number: item.number,
134
177
  title: item.title,
135
- status: isGone ? 'closed' : 'error',
178
+ status: isGone ? "closed" : "error",
136
179
  errorMessage: msg,
137
180
  });
138
181
  })
@@ -147,15 +190,18 @@ export class OssScout {
147
190
  await Promise.allSettled(pending.values());
148
191
  const summary = {
149
192
  total: results.length,
150
- stillAvailable: results.filter((r) => r.status === 'still_available').length,
151
- claimed: results.filter((r) => r.status === 'claimed').length,
152
- closed: results.filter((r) => r.status === 'closed').length,
153
- hasPR: results.filter((r) => r.status === 'has_pr').length,
154
- errors: results.filter((r) => r.status === 'error').length,
193
+ stillAvailable: results.filter((r) => r.status === "still_available")
194
+ .length,
195
+ claimed: results.filter((r) => r.status === "claimed").length,
196
+ closed: results.filter((r) => r.status === "closed").length,
197
+ hasPR: results.filter((r) => r.status === "has_pr").length,
198
+ errors: results.filter((r) => r.status === "error").length,
155
199
  };
156
200
  let prunedCount;
157
201
  if (options?.prune) {
158
- const unavailableUrls = new Set(results.filter((r) => r.status !== 'still_available').map((r) => r.issueUrl));
202
+ const unavailableUrls = new Set(results
203
+ .filter((r) => r.status !== "still_available")
204
+ .map((r) => r.issueUrl));
159
205
  const before = (this.state.savedResults ?? []).length;
160
206
  this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
161
207
  prunedCount = before - (this.state.savedResults?.length ?? 0);
@@ -166,16 +212,16 @@ export class OssScout {
166
212
  classifyVetResult(candidate) {
167
213
  const checks = candidate.vettingResult.checks;
168
214
  if (!checks.noExistingPR)
169
- return 'has_pr';
215
+ return "has_pr";
170
216
  if (!checks.notClaimed)
171
- return 'claimed';
172
- return 'still_available';
217
+ return "claimed";
218
+ return "still_available";
173
219
  }
174
220
  // ── State Reads (ScoutStateReader implementation) ───────────────────
175
221
  getReposWithMergedPRs() {
176
222
  const repoCounts = new Map();
177
223
  for (const pr of this.state.mergedPRs ?? []) {
178
- const repo = this.extractRepoFromUrl(pr.url);
224
+ const repo = extractRepoFromUrl(pr.url);
179
225
  if (repo) {
180
226
  repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
181
227
  }
@@ -353,21 +399,16 @@ export class OssScout {
353
399
  return this.state;
354
400
  }
355
401
  // ── Private helpers ─────────────────────────────────────────────────
356
- extractRepoFromUrl(url) {
357
- const match = url.match(/github\.com\/([^/]+\/[^/]+)\//);
358
- return match ? match[1] : null;
359
- }
360
402
  updateRepoScoreFromPRs(repo) {
361
- const mergedCount = (this.state.mergedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).length;
362
- const closedCount = (this.state.closedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).length;
403
+ const mergedCount = (this.state.mergedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
404
+ const closedCount = (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
363
405
  this.updateRepoScore(repo, {
364
406
  mergedPRCount: mergedCount,
365
407
  closedWithoutMergeCount: closedCount,
366
408
  lastMergedAt: mergedCount > 0
367
409
  ? (this.state.mergedPRs ?? [])
368
- .filter((p) => this.extractRepoFromUrl(p.url) === repo)
369
- .sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]
370
- ?.mergedAt
410
+ .filter((p) => extractRepoFromUrl(p.url) === repo)
411
+ .sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]?.mergedAt
371
412
  : undefined,
372
413
  });
373
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Find open source issues personalized to your contribution history",
5
5
  "type": "module",
6
6
  "bin": {