@oss-scout/core 0.2.0 → 0.3.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 +51 -47
- package/dist/cli.js +218 -87
- package/dist/commands/config.d.ts +2 -4
- package/dist/commands/config.js +76 -78
- package/dist/commands/results.d.ts +1 -1
- package/dist/commands/results.js +1 -1
- package/dist/commands/search.d.ts +2 -2
- package/dist/commands/search.js +16 -6
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +25 -25
- package/dist/commands/skip.d.ts +33 -0
- package/dist/commands/skip.js +89 -0
- package/dist/commands/validation.d.ts +1 -1
- package/dist/commands/validation.js +1 -1
- package/dist/commands/vet-list.d.ts +2 -2
- package/dist/commands/vet-list.js +12 -5
- package/dist/commands/vet.d.ts +3 -3
- package/dist/commands/vet.js +9 -5
- package/dist/core/bootstrap.d.ts +1 -1
- package/dist/core/bootstrap.js +20 -16
- package/dist/core/category-mapping.d.ts +1 -1
- package/dist/core/category-mapping.js +104 -13
- package/dist/core/errors.d.ts +8 -1
- package/dist/core/errors.js +31 -19
- package/dist/core/gist-state-store.d.ts +1 -1
- package/dist/core/gist-state-store.js +55 -28
- package/dist/core/github.d.ts +1 -1
- package/dist/core/github.js +5 -5
- package/dist/core/http-cache.js +26 -22
- package/dist/core/issue-discovery.d.ts +6 -6
- package/dist/core/issue-discovery.js +279 -286
- package/dist/core/issue-eligibility.d.ts +2 -2
- package/dist/core/issue-eligibility.js +26 -21
- package/dist/core/issue-filtering.js +23 -15
- package/dist/core/issue-scoring.js +1 -1
- package/dist/core/issue-vetting.d.ts +2 -4
- package/dist/core/issue-vetting.js +65 -56
- package/dist/core/local-state.d.ts +1 -1
- package/dist/core/local-state.js +16 -14
- package/dist/core/repo-health.d.ts +2 -2
- package/dist/core/repo-health.js +46 -35
- package/dist/core/schemas.d.ts +17 -9
- package/dist/core/schemas.js +47 -19
- package/dist/core/search-budget.js +3 -3
- package/dist/core/search-phases.d.ts +6 -6
- package/dist/core/search-phases.js +23 -19
- package/dist/core/types.d.ts +9 -9
- package/dist/core/types.js +15 -3
- package/dist/core/utils.d.ts +10 -1
- package/dist/core/utils.js +44 -25
- package/dist/formatters/json.d.ts +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -5
- package/dist/scout.d.ts +30 -6
- package/dist/scout.js +141 -34
- package/package.json +7 -3
package/dist/core/bootstrap.js
CHANGED
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
* First-run bootstrap — fetches starred repos and PR history from GitHub
|
|
3
3
|
* to seed the scout's state with the user's contribution context.
|
|
4
4
|
*/
|
|
5
|
-
import { getOctokit, checkRateLimit } from
|
|
6
|
-
import { debug, warn } from
|
|
7
|
-
import { errorMessage } from
|
|
8
|
-
|
|
5
|
+
import { getOctokit, checkRateLimit } from "./github.js";
|
|
6
|
+
import { debug, warn } from "./logger.js";
|
|
7
|
+
import { ConfigurationError, errorMessage } from "./errors.js";
|
|
8
|
+
import { extractRepoFromUrl } from "./utils.js";
|
|
9
|
+
const MODULE = "bootstrap";
|
|
9
10
|
const STARRED_MAX_PAGES = 5;
|
|
10
11
|
const SEARCH_MAX_PAGES = 3;
|
|
11
12
|
const PER_PAGE = 100;
|
|
12
13
|
export async function bootstrapScout(scout, token) {
|
|
13
14
|
const username = scout.getPreferences().githubUsername;
|
|
14
15
|
if (!username) {
|
|
15
|
-
throw new
|
|
16
|
+
throw new ConfigurationError("GitHub username not configured. Run `oss-scout setup` first.");
|
|
16
17
|
}
|
|
17
18
|
const rateLimit = await checkRateLimit(token);
|
|
18
19
|
debug(MODULE, `Rate limit: ${rateLimit.remaining}/${rateLimit.limit}, resets at ${rateLimit.resetAt}`);
|
|
19
20
|
if (rateLimit.remaining < 15) {
|
|
20
|
-
debug(MODULE,
|
|
21
|
+
debug(MODULE, "Insufficient rate limit, skipping bootstrap");
|
|
21
22
|
return {
|
|
22
23
|
starredRepoCount: 0,
|
|
23
24
|
mergedPRCount: 0,
|
|
@@ -33,7 +34,10 @@ export async function bootstrapScout(scout, token) {
|
|
|
33
34
|
const starredRepos = [];
|
|
34
35
|
try {
|
|
35
36
|
let starredPage = 0;
|
|
36
|
-
for await (const response of octokit.paginate.iterator(octokit.activity.listReposStarredByAuthenticatedUser, {
|
|
37
|
+
for await (const response of octokit.paginate.iterator(octokit.activity.listReposStarredByAuthenticatedUser, {
|
|
38
|
+
per_page: PER_PAGE,
|
|
39
|
+
headers: { accept: "application/vnd.github.v3+json" },
|
|
40
|
+
})) {
|
|
37
41
|
for (const repo of response.data) {
|
|
38
42
|
const r = repo;
|
|
39
43
|
starredRepos.push(r.full_name);
|
|
@@ -47,7 +51,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
47
51
|
}
|
|
48
52
|
catch (err) {
|
|
49
53
|
warn(MODULE, `Failed to fetch starred repos: ${errorMessage(err)}`);
|
|
50
|
-
errors.push(
|
|
54
|
+
errors.push("starred repos fetch failed");
|
|
51
55
|
}
|
|
52
56
|
// 2. Fetch merged PRs via Search API
|
|
53
57
|
let mergedPRCount = 0;
|
|
@@ -59,14 +63,14 @@ export async function bootstrapScout(scout, token) {
|
|
|
59
63
|
page,
|
|
60
64
|
});
|
|
61
65
|
for (const item of data.items) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
66
|
+
const repo = extractRepoFromUrl(item.html_url);
|
|
67
|
+
if (!repo)
|
|
64
68
|
continue;
|
|
65
69
|
scout.recordMergedPR({
|
|
66
70
|
url: item.html_url,
|
|
67
71
|
title: item.title,
|
|
68
72
|
mergedAt: item.closed_at ?? new Date().toISOString(),
|
|
69
|
-
repo
|
|
73
|
+
repo,
|
|
70
74
|
});
|
|
71
75
|
mergedPRCount++;
|
|
72
76
|
}
|
|
@@ -77,7 +81,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
77
81
|
}
|
|
78
82
|
catch (err) {
|
|
79
83
|
warn(MODULE, `Failed to fetch merged PRs: ${errorMessage(err)}`);
|
|
80
|
-
errors.push(
|
|
84
|
+
errors.push("merged PR fetch failed");
|
|
81
85
|
}
|
|
82
86
|
// 3. Fetch closed-without-merge PRs via Search API
|
|
83
87
|
let closedPRCount = 0;
|
|
@@ -89,14 +93,14 @@ export async function bootstrapScout(scout, token) {
|
|
|
89
93
|
page,
|
|
90
94
|
});
|
|
91
95
|
for (const item of data.items) {
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
96
|
+
const repo = extractRepoFromUrl(item.html_url);
|
|
97
|
+
if (!repo)
|
|
94
98
|
continue;
|
|
95
99
|
scout.recordClosedPR({
|
|
96
100
|
url: item.html_url,
|
|
97
101
|
title: item.title,
|
|
98
102
|
closedAt: item.closed_at ?? new Date().toISOString(),
|
|
99
|
-
repo
|
|
103
|
+
repo,
|
|
100
104
|
});
|
|
101
105
|
closedPRCount++;
|
|
102
106
|
}
|
|
@@ -107,7 +111,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
107
111
|
}
|
|
108
112
|
catch (err) {
|
|
109
113
|
warn(MODULE, `Failed to fetch closed PRs: ${errorMessage(err)}`);
|
|
110
|
-
errors.push(
|
|
114
|
+
errors.push("closed PR fetch failed");
|
|
111
115
|
}
|
|
112
116
|
const state = scout.getState();
|
|
113
117
|
const reposScoredCount = Object.keys(state.repoScores).length;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Used by issue discovery to prioritize repos matching user's category preferences.
|
|
5
5
|
*/
|
|
6
|
-
import type { ProjectCategory } from
|
|
6
|
+
import type { ProjectCategory } from "./types.js";
|
|
7
7
|
/** GitHub topics associated with each project category, used for `topic:` search queries. */
|
|
8
8
|
export declare const CATEGORY_TOPICS: Record<ProjectCategory, string[]>;
|
|
9
9
|
/** Well-known GitHub organizations associated with each project category. */
|
|
@@ -5,21 +5,112 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** GitHub topics associated with each project category, used for `topic:` search queries. */
|
|
7
7
|
export const CATEGORY_TOPICS = {
|
|
8
|
-
nonprofit: [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
nonprofit: [
|
|
9
|
+
"nonprofit",
|
|
10
|
+
"social-good",
|
|
11
|
+
"humanitarian",
|
|
12
|
+
"charity",
|
|
13
|
+
"social-impact",
|
|
14
|
+
"civic-tech",
|
|
15
|
+
],
|
|
16
|
+
devtools: [
|
|
17
|
+
"developer-tools",
|
|
18
|
+
"devtools",
|
|
19
|
+
"cli",
|
|
20
|
+
"sdk",
|
|
21
|
+
"linter",
|
|
22
|
+
"formatter",
|
|
23
|
+
"build-tool",
|
|
24
|
+
],
|
|
25
|
+
infrastructure: [
|
|
26
|
+
"infrastructure",
|
|
27
|
+
"cloud",
|
|
28
|
+
"kubernetes",
|
|
29
|
+
"docker",
|
|
30
|
+
"devops",
|
|
31
|
+
"monitoring",
|
|
32
|
+
"observability",
|
|
33
|
+
],
|
|
34
|
+
"web-frameworks": [
|
|
35
|
+
"web-framework",
|
|
36
|
+
"frontend",
|
|
37
|
+
"backend",
|
|
38
|
+
"fullstack",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"react",
|
|
41
|
+
"vue",
|
|
42
|
+
],
|
|
43
|
+
"data-ml": [
|
|
44
|
+
"machine-learning",
|
|
45
|
+
"data-science",
|
|
46
|
+
"deep-learning",
|
|
47
|
+
"nlp",
|
|
48
|
+
"data-pipeline",
|
|
49
|
+
"analytics",
|
|
50
|
+
],
|
|
51
|
+
education: [
|
|
52
|
+
"education",
|
|
53
|
+
"learning",
|
|
54
|
+
"tutorial",
|
|
55
|
+
"courseware",
|
|
56
|
+
"edtech",
|
|
57
|
+
"teaching",
|
|
58
|
+
],
|
|
14
59
|
};
|
|
15
60
|
/** Well-known GitHub organizations associated with each project category. */
|
|
16
61
|
export const CATEGORY_ORGS = {
|
|
17
|
-
nonprofit: [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
62
|
+
nonprofit: [
|
|
63
|
+
"code-for-america",
|
|
64
|
+
"opengovfoundation",
|
|
65
|
+
"ushahidi",
|
|
66
|
+
"hotosm",
|
|
67
|
+
"openfn",
|
|
68
|
+
"democracyearth",
|
|
69
|
+
],
|
|
70
|
+
devtools: [
|
|
71
|
+
"eslint",
|
|
72
|
+
"prettier",
|
|
73
|
+
"vitejs",
|
|
74
|
+
"biomejs",
|
|
75
|
+
"oxc-project",
|
|
76
|
+
"ast-grep",
|
|
77
|
+
"turbot",
|
|
78
|
+
],
|
|
79
|
+
infrastructure: [
|
|
80
|
+
"kubernetes",
|
|
81
|
+
"hashicorp",
|
|
82
|
+
"grafana",
|
|
83
|
+
"prometheus",
|
|
84
|
+
"open-telemetry",
|
|
85
|
+
"envoyproxy",
|
|
86
|
+
"cncf",
|
|
87
|
+
],
|
|
88
|
+
"web-frameworks": [
|
|
89
|
+
"vercel",
|
|
90
|
+
"remix-run",
|
|
91
|
+
"sveltejs",
|
|
92
|
+
"nuxt",
|
|
93
|
+
"astro",
|
|
94
|
+
"redwoodjs",
|
|
95
|
+
"blitz-js",
|
|
96
|
+
],
|
|
97
|
+
"data-ml": [
|
|
98
|
+
"huggingface",
|
|
99
|
+
"mlflow",
|
|
100
|
+
"apache",
|
|
101
|
+
"dbt-labs",
|
|
102
|
+
"dagster-io",
|
|
103
|
+
"prefecthq",
|
|
104
|
+
"langchain-ai",
|
|
105
|
+
],
|
|
106
|
+
education: [
|
|
107
|
+
"freeCodeCamp",
|
|
108
|
+
"TheOdinProject",
|
|
109
|
+
"exercism",
|
|
110
|
+
"codecademy",
|
|
111
|
+
"oppia",
|
|
112
|
+
"Khan",
|
|
113
|
+
],
|
|
23
114
|
};
|
|
24
115
|
/**
|
|
25
116
|
* Check if a repo belongs to any of the given categories based on its owner matching a category org.
|
|
@@ -28,7 +119,7 @@ export const CATEGORY_ORGS = {
|
|
|
28
119
|
export function repoBelongsToCategory(repoFullName, categories) {
|
|
29
120
|
if (categories.length === 0)
|
|
30
121
|
return false;
|
|
31
|
-
const owner = repoFullName.split(
|
|
122
|
+
const owner = repoFullName.split("/")[0]?.toLowerCase();
|
|
32
123
|
if (!owner)
|
|
33
124
|
return false;
|
|
34
125
|
for (const category of categories) {
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom error type hierarchy for oss-scout.
|
|
3
|
+
*
|
|
4
|
+
* Error strategy:
|
|
5
|
+
* - Auth errors (401) and rate limit errors (429, 403+rate-limit): ALWAYS propagate
|
|
6
|
+
* - Network errors (ENOTFOUND, ECONNREFUSED, ETIMEDOUT): propagate with context
|
|
7
|
+
* - Validation errors: propagate
|
|
8
|
+
* - Cache/filesystem errors: degrade gracefully with warn logging
|
|
9
|
+
* - API data errors (unexpected shapes): degrade gracefully with warn logging
|
|
3
10
|
*/
|
|
4
11
|
export declare class OssScoutError extends Error {
|
|
5
12
|
readonly code: string;
|
|
@@ -15,7 +22,7 @@ export declare function errorMessage(e: unknown): string;
|
|
|
15
22
|
export declare function getHttpStatusCode(error: unknown): number | undefined;
|
|
16
23
|
export declare function isRateLimitError(error: unknown): boolean;
|
|
17
24
|
/** Error codes for JSON output. */
|
|
18
|
-
export type ErrorCode =
|
|
25
|
+
export type ErrorCode = "AUTH_REQUIRED" | "CONFIGURATION" | "NETWORK" | "NOT_FOUND" | "RATE_LIMITED" | "STATE_CORRUPTED" | "UNKNOWN" | "VALIDATION";
|
|
19
26
|
/**
|
|
20
27
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
21
28
|
*/
|
package/dist/core/errors.js
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom error type hierarchy for oss-scout.
|
|
3
|
+
*
|
|
4
|
+
* Error strategy:
|
|
5
|
+
* - Auth errors (401) and rate limit errors (429, 403+rate-limit): ALWAYS propagate
|
|
6
|
+
* - Network errors (ENOTFOUND, ECONNREFUSED, ETIMEDOUT): propagate with context
|
|
7
|
+
* - Validation errors: propagate
|
|
8
|
+
* - Cache/filesystem errors: degrade gracefully with warn logging
|
|
9
|
+
* - API data errors (unexpected shapes): degrade gracefully with warn logging
|
|
3
10
|
*/
|
|
4
11
|
export class OssScoutError extends Error {
|
|
5
12
|
code;
|
|
6
13
|
constructor(message, code) {
|
|
7
14
|
super(message);
|
|
8
15
|
this.code = code;
|
|
9
|
-
this.name =
|
|
16
|
+
this.name = "OssScoutError";
|
|
10
17
|
}
|
|
11
18
|
}
|
|
12
19
|
export class ConfigurationError extends OssScoutError {
|
|
13
20
|
constructor(message) {
|
|
14
|
-
super(message,
|
|
15
|
-
this.name =
|
|
21
|
+
super(message, "CONFIGURATION_ERROR");
|
|
22
|
+
this.name = "ConfigurationError";
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
export class ValidationError extends OssScoutError {
|
|
19
26
|
constructor(message) {
|
|
20
|
-
super(message,
|
|
21
|
-
this.name =
|
|
27
|
+
super(message, "VALIDATION_ERROR");
|
|
28
|
+
this.name = "ValidationError";
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
export function errorMessage(e) {
|
|
25
32
|
return e instanceof Error ? e.message : String(e);
|
|
26
33
|
}
|
|
27
34
|
export function getHttpStatusCode(error) {
|
|
28
|
-
if (error && typeof error ===
|
|
35
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
29
36
|
const status = error.status;
|
|
30
|
-
return typeof status ===
|
|
37
|
+
return typeof status === "number" && Number.isFinite(status)
|
|
38
|
+
? status
|
|
39
|
+
: undefined;
|
|
31
40
|
}
|
|
32
41
|
return undefined;
|
|
33
42
|
}
|
|
@@ -37,7 +46,7 @@ export function isRateLimitError(error) {
|
|
|
37
46
|
return true;
|
|
38
47
|
if (status === 403) {
|
|
39
48
|
const msg = errorMessage(error).toLowerCase();
|
|
40
|
-
return msg.includes(
|
|
49
|
+
return msg.includes("rate limit");
|
|
41
50
|
}
|
|
42
51
|
return false;
|
|
43
52
|
}
|
|
@@ -46,24 +55,27 @@ export function isRateLimitError(error) {
|
|
|
46
55
|
*/
|
|
47
56
|
export function resolveErrorCode(err) {
|
|
48
57
|
if (err instanceof ConfigurationError)
|
|
49
|
-
return
|
|
58
|
+
return "CONFIGURATION";
|
|
50
59
|
if (err instanceof ValidationError)
|
|
51
|
-
return
|
|
60
|
+
return "VALIDATION";
|
|
52
61
|
const status = getHttpStatusCode(err);
|
|
53
62
|
if (status === 401)
|
|
54
|
-
return
|
|
63
|
+
return "AUTH_REQUIRED";
|
|
55
64
|
if (status === 403) {
|
|
56
65
|
const msg = errorMessage(err).toLowerCase();
|
|
57
|
-
if (msg.includes(
|
|
58
|
-
return
|
|
59
|
-
return
|
|
66
|
+
if (msg.includes("rate limit") || msg.includes("abuse detection"))
|
|
67
|
+
return "RATE_LIMITED";
|
|
68
|
+
return "AUTH_REQUIRED";
|
|
60
69
|
}
|
|
61
70
|
if (status === 404)
|
|
62
|
-
return
|
|
71
|
+
return "NOT_FOUND";
|
|
63
72
|
if (status === 429)
|
|
64
|
-
return
|
|
73
|
+
return "RATE_LIMITED";
|
|
65
74
|
const msg = errorMessage(err).toLowerCase();
|
|
66
|
-
if (msg.includes(
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
if (msg.includes("enotfound") ||
|
|
76
|
+
msg.includes("econnrefused") ||
|
|
77
|
+
msg.includes("etimedout") ||
|
|
78
|
+
msg.includes("fetch failed"))
|
|
79
|
+
return "NETWORK";
|
|
80
|
+
return "UNKNOWN";
|
|
69
81
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Stores ScoutState as a private GitHub Gist, with a local file cache
|
|
5
5
|
* as fallback when the API is unavailable.
|
|
6
6
|
*/
|
|
7
|
-
import type { ScoutState } from
|
|
7
|
+
import type { ScoutState } from "./schemas.js";
|
|
8
8
|
/** Minimal Octokit interface for gist operations — keeps the class testable. */
|
|
9
9
|
export interface GistOctokitLike {
|
|
10
10
|
gists: {
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* Stores ScoutState as a private GitHub Gist, with a local file cache
|
|
5
5
|
* as fallback when the API is unavailable.
|
|
6
6
|
*/
|
|
7
|
-
import * as fs from
|
|
8
|
-
import * as path from
|
|
9
|
-
import { ScoutStateSchema } from
|
|
10
|
-
import { getDataDir } from
|
|
11
|
-
import { debug, warn } from
|
|
12
|
-
import { errorMessage } from
|
|
13
|
-
const MODULE =
|
|
14
|
-
const GIST_DESCRIPTION =
|
|
15
|
-
const GIST_FILENAME =
|
|
16
|
-
const GIST_ID_FILE =
|
|
17
|
-
const CACHE_FILE =
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { ScoutStateSchema } from "./schemas.js";
|
|
10
|
+
import { getDataDir } from "./utils.js";
|
|
11
|
+
import { debug, warn } from "./logger.js";
|
|
12
|
+
import { errorMessage } from "./errors.js";
|
|
13
|
+
const MODULE = "gist-state";
|
|
14
|
+
const GIST_DESCRIPTION = "oss-scout-state";
|
|
15
|
+
const GIST_FILENAME = "state.json";
|
|
16
|
+
const GIST_ID_FILE = "gist-id";
|
|
17
|
+
const CACHE_FILE = "state-cache.json";
|
|
18
18
|
const SEARCH_MAX_PAGES = 5;
|
|
19
19
|
function getGistIdPath() {
|
|
20
20
|
return path.join(getDataDir(), GIST_ID_FILE);
|
|
@@ -47,17 +47,22 @@ export class GistStateStore {
|
|
|
47
47
|
async push(state) {
|
|
48
48
|
this.writeCache(state);
|
|
49
49
|
if (!this.gistId) {
|
|
50
|
-
warn(MODULE,
|
|
50
|
+
warn(MODULE, "No gist ID — cannot push");
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const json = JSON.stringify(state, null, 2);
|
|
54
|
+
if (json.length > 900000) {
|
|
55
|
+
warn(MODULE, `State too large for gist (${Math.round(json.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
51
56
|
return false;
|
|
52
57
|
}
|
|
53
58
|
try {
|
|
54
59
|
await this.octokit.gists.update({
|
|
55
60
|
gist_id: this.gistId,
|
|
56
61
|
files: {
|
|
57
|
-
[GIST_FILENAME]: { content:
|
|
62
|
+
[GIST_FILENAME]: { content: json },
|
|
58
63
|
},
|
|
59
64
|
});
|
|
60
|
-
debug(MODULE,
|
|
65
|
+
debug(MODULE, "State pushed to gist");
|
|
61
66
|
return true;
|
|
62
67
|
}
|
|
63
68
|
catch (err) {
|
|
@@ -104,7 +109,7 @@ export class GistStateStore {
|
|
|
104
109
|
catch (err) {
|
|
105
110
|
debug(MODULE, `Cached gist ID invalid: ${errorMessage(err)}`);
|
|
106
111
|
}
|
|
107
|
-
debug(MODULE,
|
|
112
|
+
debug(MODULE, "Cached gist ID invalid, searching...");
|
|
108
113
|
}
|
|
109
114
|
// 2. Search user's gists
|
|
110
115
|
const foundId = await this.searchForGist();
|
|
@@ -117,9 +122,13 @@ export class GistStateStore {
|
|
|
117
122
|
this.writeCache(state);
|
|
118
123
|
return { gistId: foundId, state, created: false };
|
|
119
124
|
}
|
|
125
|
+
// Gist exists but content failed validation — fall back to cache
|
|
126
|
+
// to avoid overwriting the user's data by creating a new gist.
|
|
127
|
+
warn(MODULE, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
|
|
128
|
+
return this.bootstrapFromCache();
|
|
120
129
|
}
|
|
121
130
|
// 3. Create new gist
|
|
122
|
-
debug(MODULE,
|
|
131
|
+
debug(MODULE, "No existing gist found, creating new one");
|
|
123
132
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
124
133
|
const newId = await this.createGist(freshState);
|
|
125
134
|
this.saveGistId(newId);
|
|
@@ -130,20 +139,20 @@ export class GistStateStore {
|
|
|
130
139
|
bootstrapFromCache() {
|
|
131
140
|
const cached = this.readCache();
|
|
132
141
|
if (cached) {
|
|
133
|
-
debug(MODULE,
|
|
142
|
+
debug(MODULE, "Bootstrapped from local cache (degraded mode)");
|
|
134
143
|
const cachedId = this.readCachedGistId();
|
|
135
144
|
if (cachedId)
|
|
136
145
|
this.gistId = cachedId;
|
|
137
146
|
return {
|
|
138
|
-
gistId: cachedId ??
|
|
147
|
+
gistId: cachedId ?? "",
|
|
139
148
|
state: cached,
|
|
140
149
|
created: false,
|
|
141
150
|
degraded: true,
|
|
142
151
|
};
|
|
143
152
|
}
|
|
144
|
-
debug(MODULE,
|
|
153
|
+
debug(MODULE, "No cache available, using fresh state (degraded mode)");
|
|
145
154
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
146
|
-
return { gistId:
|
|
155
|
+
return { gistId: "", state: fresh, created: false, degraded: true };
|
|
147
156
|
}
|
|
148
157
|
// ── Gist API operations ──────────────────────────────────────────────
|
|
149
158
|
async fetchGistState(gistId) {
|
|
@@ -187,28 +196,28 @@ export class GistStateStore {
|
|
|
187
196
|
// ── Local file helpers ───────────────────────────────────────────────
|
|
188
197
|
readCachedGistId() {
|
|
189
198
|
try {
|
|
190
|
-
const id = fs.readFileSync(getGistIdPath(),
|
|
199
|
+
const id = fs.readFileSync(getGistIdPath(), "utf-8").trim();
|
|
191
200
|
return id || null;
|
|
192
201
|
}
|
|
193
202
|
catch (err) {
|
|
194
203
|
const code = err?.code;
|
|
195
|
-
if (code !==
|
|
204
|
+
if (code !== "ENOENT") {
|
|
196
205
|
warn(MODULE, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
197
206
|
}
|
|
198
207
|
return null;
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
210
|
saveGistId(id) {
|
|
202
|
-
fs.writeFileSync(getGistIdPath(), id +
|
|
211
|
+
fs.writeFileSync(getGistIdPath(), id + "\n", { mode: 0o600 });
|
|
203
212
|
}
|
|
204
213
|
readCache() {
|
|
205
214
|
try {
|
|
206
|
-
const raw = fs.readFileSync(getCachePath(),
|
|
215
|
+
const raw = fs.readFileSync(getCachePath(), "utf-8");
|
|
207
216
|
return ScoutStateSchema.parse(JSON.parse(raw));
|
|
208
217
|
}
|
|
209
218
|
catch (err) {
|
|
210
219
|
const code = err?.code;
|
|
211
|
-
if (code !==
|
|
220
|
+
if (code !== "ENOENT") {
|
|
212
221
|
warn(MODULE, `Failed to read state cache: ${errorMessage(err)}`);
|
|
213
222
|
}
|
|
214
223
|
return null;
|
|
@@ -216,7 +225,9 @@ export class GistStateStore {
|
|
|
216
225
|
}
|
|
217
226
|
writeCache(state) {
|
|
218
227
|
try {
|
|
219
|
-
fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) +
|
|
228
|
+
fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
|
|
229
|
+
mode: 0o600,
|
|
230
|
+
});
|
|
220
231
|
}
|
|
221
232
|
catch (err) {
|
|
222
233
|
warn(MODULE, `Failed to write cache: ${errorMessage(err)}`);
|
|
@@ -242,8 +253,10 @@ export function mergeStates(local, remote) {
|
|
|
242
253
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
243
254
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
244
255
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
256
|
+
skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
|
|
245
257
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
246
|
-
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
|
|
258
|
+
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
|
|
259
|
+
new Date().toISOString(),
|
|
247
260
|
gistId: remote.gistId ?? local.gistId,
|
|
248
261
|
};
|
|
249
262
|
}
|
|
@@ -266,7 +279,9 @@ function mergeStarredRepos(local, remote) {
|
|
|
266
279
|
const localTs = local.starredReposLastFetched;
|
|
267
280
|
const remoteTs = remote.starredReposLastFetched;
|
|
268
281
|
if (!localTs && !remoteTs)
|
|
269
|
-
return remote.starredRepos.length >= local.starredRepos.length
|
|
282
|
+
return remote.starredRepos.length >= local.starredRepos.length
|
|
283
|
+
? remote.starredRepos
|
|
284
|
+
: local.starredRepos;
|
|
270
285
|
if (!localTs)
|
|
271
286
|
return remote.starredRepos;
|
|
272
287
|
if (!remoteTs)
|
|
@@ -293,6 +308,18 @@ function mergeSavedResults(local, remote) {
|
|
|
293
308
|
}
|
|
294
309
|
return [...merged.values()];
|
|
295
310
|
}
|
|
311
|
+
function mergeSkippedIssues(local, remote) {
|
|
312
|
+
const merged = new Map();
|
|
313
|
+
for (const item of local)
|
|
314
|
+
merged.set(item.url, item);
|
|
315
|
+
for (const item of remote) {
|
|
316
|
+
const existing = merged.get(item.url);
|
|
317
|
+
if (!existing || item.skippedAt > existing.skippedAt) {
|
|
318
|
+
merged.set(item.url, item);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return [...merged.values()];
|
|
322
|
+
}
|
|
296
323
|
function pickFresherTimestamp(a, b) {
|
|
297
324
|
if (!a)
|
|
298
325
|
return b;
|
package/dist/core/github.d.ts
CHANGED
package/dist/core/github.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared GitHub API client with rate limiting and throttling.
|
|
3
3
|
*/
|
|
4
|
-
import { Octokit } from
|
|
5
|
-
import { throttling } from
|
|
6
|
-
import { warn } from
|
|
7
|
-
const MODULE =
|
|
4
|
+
import { Octokit } from "@octokit/rest";
|
|
5
|
+
import { throttling } from "@octokit/plugin-throttling";
|
|
6
|
+
import { warn } from "./logger.js";
|
|
7
|
+
const MODULE = "github";
|
|
8
8
|
const ThrottledOctokit = Octokit.plugin(throttling);
|
|
9
9
|
let _octokit = null;
|
|
10
10
|
let _currentToken = null;
|
|
11
11
|
function formatResetTime(date) {
|
|
12
|
-
return date.toLocaleTimeString(
|
|
12
|
+
return date.toLocaleTimeString("en-US", { hour12: false });
|
|
13
13
|
}
|
|
14
14
|
export function getRateLimitCallbacks() {
|
|
15
15
|
return {
|