@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.
- package/dist/cli.bundle.cjs +42 -42
- package/dist/cli.js +110 -86
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +76 -72
- 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 +27 -21
- 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 +36 -27
- 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 +3 -3
- package/dist/core/issue-discovery.js +325 -277
- 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 -2
- package/dist/core/issue-vetting.js +66 -53
- 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 +1 -1
- package/dist/core/schemas.js +40 -18
- 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 +4 -5
- package/dist/scout.js +72 -31
- package/package.json +1 -1
|
@@ -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,7 +47,7 @@ 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
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
@@ -57,7 +57,7 @@ export class GistStateStore {
|
|
|
57
57
|
[GIST_FILENAME]: { content: JSON.stringify(state, null, 2) },
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
|
-
debug(MODULE,
|
|
60
|
+
debug(MODULE, "State pushed to gist");
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
@@ -104,7 +104,7 @@ export class GistStateStore {
|
|
|
104
104
|
catch (err) {
|
|
105
105
|
debug(MODULE, `Cached gist ID invalid: ${errorMessage(err)}`);
|
|
106
106
|
}
|
|
107
|
-
debug(MODULE,
|
|
107
|
+
debug(MODULE, "Cached gist ID invalid, searching...");
|
|
108
108
|
}
|
|
109
109
|
// 2. Search user's gists
|
|
110
110
|
const foundId = await this.searchForGist();
|
|
@@ -117,9 +117,13 @@ export class GistStateStore {
|
|
|
117
117
|
this.writeCache(state);
|
|
118
118
|
return { gistId: foundId, state, created: false };
|
|
119
119
|
}
|
|
120
|
+
// Gist exists but content failed validation — fall back to cache
|
|
121
|
+
// to avoid overwriting the user's data by creating a new gist.
|
|
122
|
+
warn(MODULE, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
|
|
123
|
+
return this.bootstrapFromCache();
|
|
120
124
|
}
|
|
121
125
|
// 3. Create new gist
|
|
122
|
-
debug(MODULE,
|
|
126
|
+
debug(MODULE, "No existing gist found, creating new one");
|
|
123
127
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
124
128
|
const newId = await this.createGist(freshState);
|
|
125
129
|
this.saveGistId(newId);
|
|
@@ -130,20 +134,20 @@ export class GistStateStore {
|
|
|
130
134
|
bootstrapFromCache() {
|
|
131
135
|
const cached = this.readCache();
|
|
132
136
|
if (cached) {
|
|
133
|
-
debug(MODULE,
|
|
137
|
+
debug(MODULE, "Bootstrapped from local cache (degraded mode)");
|
|
134
138
|
const cachedId = this.readCachedGistId();
|
|
135
139
|
if (cachedId)
|
|
136
140
|
this.gistId = cachedId;
|
|
137
141
|
return {
|
|
138
|
-
gistId: cachedId ??
|
|
142
|
+
gistId: cachedId ?? "",
|
|
139
143
|
state: cached,
|
|
140
144
|
created: false,
|
|
141
145
|
degraded: true,
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
|
-
debug(MODULE,
|
|
148
|
+
debug(MODULE, "No cache available, using fresh state (degraded mode)");
|
|
145
149
|
const fresh = ScoutStateSchema.parse({ version: 1 });
|
|
146
|
-
return { gistId:
|
|
150
|
+
return { gistId: "", state: fresh, created: false, degraded: true };
|
|
147
151
|
}
|
|
148
152
|
// ── Gist API operations ──────────────────────────────────────────────
|
|
149
153
|
async fetchGistState(gistId) {
|
|
@@ -187,28 +191,28 @@ export class GistStateStore {
|
|
|
187
191
|
// ── Local file helpers ───────────────────────────────────────────────
|
|
188
192
|
readCachedGistId() {
|
|
189
193
|
try {
|
|
190
|
-
const id = fs.readFileSync(getGistIdPath(),
|
|
194
|
+
const id = fs.readFileSync(getGistIdPath(), "utf-8").trim();
|
|
191
195
|
return id || null;
|
|
192
196
|
}
|
|
193
197
|
catch (err) {
|
|
194
198
|
const code = err?.code;
|
|
195
|
-
if (code !==
|
|
199
|
+
if (code !== "ENOENT") {
|
|
196
200
|
warn(MODULE, `Failed to read cached gist ID: ${errorMessage(err)}`);
|
|
197
201
|
}
|
|
198
202
|
return null;
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
saveGistId(id) {
|
|
202
|
-
fs.writeFileSync(getGistIdPath(), id +
|
|
206
|
+
fs.writeFileSync(getGistIdPath(), id + "\n", { mode: 0o600 });
|
|
203
207
|
}
|
|
204
208
|
readCache() {
|
|
205
209
|
try {
|
|
206
|
-
const raw = fs.readFileSync(getCachePath(),
|
|
210
|
+
const raw = fs.readFileSync(getCachePath(), "utf-8");
|
|
207
211
|
return ScoutStateSchema.parse(JSON.parse(raw));
|
|
208
212
|
}
|
|
209
213
|
catch (err) {
|
|
210
214
|
const code = err?.code;
|
|
211
|
-
if (code !==
|
|
215
|
+
if (code !== "ENOENT") {
|
|
212
216
|
warn(MODULE, `Failed to read state cache: ${errorMessage(err)}`);
|
|
213
217
|
}
|
|
214
218
|
return null;
|
|
@@ -216,7 +220,9 @@ export class GistStateStore {
|
|
|
216
220
|
}
|
|
217
221
|
writeCache(state) {
|
|
218
222
|
try {
|
|
219
|
-
fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) +
|
|
223
|
+
fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
|
|
224
|
+
mode: 0o600,
|
|
225
|
+
});
|
|
220
226
|
}
|
|
221
227
|
catch (err) {
|
|
222
228
|
warn(MODULE, `Failed to write cache: ${errorMessage(err)}`);
|
|
@@ -243,7 +249,8 @@ export function mergeStates(local, remote) {
|
|
|
243
249
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
244
250
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
245
251
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
246
|
-
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
|
|
252
|
+
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
|
|
253
|
+
new Date().toISOString(),
|
|
247
254
|
gistId: remote.gistId ?? local.gistId,
|
|
248
255
|
};
|
|
249
256
|
}
|
|
@@ -266,7 +273,9 @@ function mergeStarredRepos(local, remote) {
|
|
|
266
273
|
const localTs = local.starredReposLastFetched;
|
|
267
274
|
const remoteTs = remote.starredReposLastFetched;
|
|
268
275
|
if (!localTs && !remoteTs)
|
|
269
|
-
return remote.starredRepos.length >= local.starredRepos.length
|
|
276
|
+
return remote.starredRepos.length >= local.starredRepos.length
|
|
277
|
+
? remote.starredRepos
|
|
278
|
+
: local.starredRepos;
|
|
270
279
|
if (!localTs)
|
|
271
280
|
return remote.starredRepos;
|
|
272
281
|
if (!remoteTs)
|
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 {
|
package/dist/core/http-cache.js
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* for the same endpoint (e.g., star counts for two PRs in the same repo)
|
|
10
10
|
* share a single HTTP round-trip.
|
|
11
11
|
*/
|
|
12
|
-
import * as fs from
|
|
13
|
-
import * as path from
|
|
14
|
-
import * as crypto from
|
|
15
|
-
import { getCacheDir } from
|
|
16
|
-
import { debug, warn } from
|
|
17
|
-
import { errorMessage, getHttpStatusCode } from
|
|
18
|
-
const MODULE =
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as crypto from "crypto";
|
|
15
|
+
import { getCacheDir } from "./utils.js";
|
|
16
|
+
import { debug, warn } from "./logger.js";
|
|
17
|
+
import { errorMessage, getHttpStatusCode } from "./errors.js";
|
|
18
|
+
const MODULE = "http-cache";
|
|
19
19
|
/**
|
|
20
20
|
* Maximum age (in ms) before a cache entry is considered stale and eligible for
|
|
21
21
|
* eviction during cleanup. Defaults to 24 hours. Entries older than this are
|
|
@@ -39,7 +39,7 @@ export class HttpCache {
|
|
|
39
39
|
}
|
|
40
40
|
/** Derive a filesystem-safe cache key from a URL. */
|
|
41
41
|
keyFor(url) {
|
|
42
|
-
return crypto.createHash(
|
|
42
|
+
return crypto.createHash("sha256").update(url).digest("hex");
|
|
43
43
|
}
|
|
44
44
|
/** Full path to the cache file for a given URL. */
|
|
45
45
|
pathFor(url) {
|
|
@@ -65,7 +65,7 @@ export class HttpCache {
|
|
|
65
65
|
get(url) {
|
|
66
66
|
const filePath = this.pathFor(url);
|
|
67
67
|
try {
|
|
68
|
-
const raw = fs.readFileSync(filePath,
|
|
68
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
69
69
|
const entry = JSON.parse(raw);
|
|
70
70
|
// Sanity-check: the file should contain the URL we asked for
|
|
71
71
|
if (entry.url !== url) {
|
|
@@ -76,7 +76,7 @@ export class HttpCache {
|
|
|
76
76
|
}
|
|
77
77
|
catch (err) {
|
|
78
78
|
const code = err?.code;
|
|
79
|
-
if (code ===
|
|
79
|
+
if (code === "ENOENT")
|
|
80
80
|
return null;
|
|
81
81
|
if (err instanceof SyntaxError) {
|
|
82
82
|
debug(MODULE, `Corrupt cache entry, deleting: ${url}`);
|
|
@@ -103,7 +103,10 @@ export class HttpCache {
|
|
|
103
103
|
cachedAt: new Date().toISOString(),
|
|
104
104
|
};
|
|
105
105
|
try {
|
|
106
|
-
fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), {
|
|
106
|
+
fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), {
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
mode: 0o600,
|
|
109
|
+
});
|
|
107
110
|
debug(MODULE, `Cached response for ${url}`);
|
|
108
111
|
}
|
|
109
112
|
catch (err) {
|
|
@@ -137,11 +140,11 @@ export class HttpCache {
|
|
|
137
140
|
const files = fs.readdirSync(this.cacheDir);
|
|
138
141
|
const now = Date.now();
|
|
139
142
|
for (const file of files) {
|
|
140
|
-
if (!file.endsWith(
|
|
143
|
+
if (!file.endsWith(".json"))
|
|
141
144
|
continue;
|
|
142
145
|
const filePath = path.join(this.cacheDir, file);
|
|
143
146
|
try {
|
|
144
|
-
const raw = fs.readFileSync(filePath,
|
|
147
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
145
148
|
const entry = JSON.parse(raw);
|
|
146
149
|
const age = now - new Date(entry.cachedAt).getTime();
|
|
147
150
|
if (age > maxAgeMs) {
|
|
@@ -163,7 +166,7 @@ export class HttpCache {
|
|
|
163
166
|
}
|
|
164
167
|
catch (err) {
|
|
165
168
|
const code = err?.code;
|
|
166
|
-
if (code !==
|
|
169
|
+
if (code !== "ENOENT") {
|
|
167
170
|
warn(MODULE, `Failed to evict stale cache entries: ${errorMessage(err)}`);
|
|
168
171
|
}
|
|
169
172
|
}
|
|
@@ -179,15 +182,15 @@ export class HttpCache {
|
|
|
179
182
|
try {
|
|
180
183
|
const files = fs.readdirSync(this.cacheDir);
|
|
181
184
|
for (const file of files) {
|
|
182
|
-
if (!file.endsWith(
|
|
185
|
+
if (!file.endsWith(".json"))
|
|
183
186
|
continue;
|
|
184
187
|
fs.unlinkSync(path.join(this.cacheDir, file));
|
|
185
188
|
}
|
|
186
|
-
debug(MODULE,
|
|
189
|
+
debug(MODULE, "Cache cleared");
|
|
187
190
|
}
|
|
188
191
|
catch (err) {
|
|
189
192
|
const code = err?.code;
|
|
190
|
-
if (code !==
|
|
193
|
+
if (code !== "ENOENT") {
|
|
191
194
|
warn(MODULE, `Failed to clear cache: ${errorMessage(err)}`);
|
|
192
195
|
}
|
|
193
196
|
}
|
|
@@ -197,11 +200,12 @@ export class HttpCache {
|
|
|
197
200
|
*/
|
|
198
201
|
size() {
|
|
199
202
|
try {
|
|
200
|
-
return fs.readdirSync(this.cacheDir).filter((f) => f.endsWith(
|
|
203
|
+
return fs.readdirSync(this.cacheDir).filter((f) => f.endsWith(".json"))
|
|
204
|
+
.length;
|
|
201
205
|
}
|
|
202
206
|
catch (err) {
|
|
203
207
|
const code = err?.code;
|
|
204
|
-
if (code !==
|
|
208
|
+
if (code !== "ENOENT") {
|
|
205
209
|
debug(MODULE, `Failed to read cache size: ${errorMessage(err)}`);
|
|
206
210
|
}
|
|
207
211
|
return 0;
|
|
@@ -253,12 +257,12 @@ export async function cachedRequest(cache, url, fetcher) {
|
|
|
253
257
|
const extraHeaders = {};
|
|
254
258
|
const cached = cache.get(url);
|
|
255
259
|
if (cached) {
|
|
256
|
-
extraHeaders[
|
|
260
|
+
extraHeaders["if-none-match"] = cached.etag;
|
|
257
261
|
}
|
|
258
262
|
try {
|
|
259
263
|
const response = await fetcher(extraHeaders);
|
|
260
264
|
// Store ETag if present (headers may be absent in test mocks)
|
|
261
|
-
const etag = response.headers?.[
|
|
265
|
+
const etag = response.headers?.["etag"];
|
|
262
266
|
if (etag) {
|
|
263
267
|
cache.set(url, etag, response.data);
|
|
264
268
|
}
|
|
@@ -302,7 +306,7 @@ export async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
|
|
|
302
306
|
return cached;
|
|
303
307
|
}
|
|
304
308
|
const result = await fetcher();
|
|
305
|
-
cache.set(key,
|
|
309
|
+
cache.set(key, "", result);
|
|
306
310
|
return result;
|
|
307
311
|
}
|
|
308
312
|
/**
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* All state is injected via constructor parameters (ScoutStateReader + ScoutPreferences).
|
|
13
13
|
*/
|
|
14
|
-
import { type IssueCandidate } from
|
|
15
|
-
import type { ScoutPreferences, SearchStrategy } from
|
|
16
|
-
import { type ScoutStateReader } from
|
|
14
|
+
import { type IssueCandidate } from "./types.js";
|
|
15
|
+
import type { ScoutPreferences, SearchStrategy } from "./schemas.js";
|
|
16
|
+
import { type ScoutStateReader } from "./issue-vetting.js";
|
|
17
17
|
/**
|
|
18
18
|
* Multi-phase issue discovery engine that searches GitHub for contributable issues.
|
|
19
19
|
*
|