@oss-autopilot/core 0.42.3 ā 0.42.5
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 +171 -51
- package/dist/cli.js +28 -10
- package/dist/commands/comments.js +24 -16
- package/dist/commands/daily.js +35 -9
- package/dist/commands/dashboard-data.js +6 -1
- package/dist/commands/dashboard-server.js +41 -2
- package/dist/commands/dashboard.js +3 -3
- package/dist/commands/local-repos.js +1 -1
- package/dist/commands/setup.js +30 -12
- package/dist/core/daily-logic.js +2 -1
- package/dist/core/pr-monitor.js +17 -1
- package/package.json +1 -1
package/dist/cli.bundle.cjs
CHANGED
|
@@ -11213,7 +11213,24 @@ var init_pr_monitor = __esm({
|
|
|
11213
11213
|
);
|
|
11214
11214
|
const ciPromise = this.getCIStatus(owner, repo, ghPR.head.sha);
|
|
11215
11215
|
const needCommitDate = hasUnrespondedComment || reviewDecision === "changes_requested";
|
|
11216
|
-
const commitDatePromise = needCommitDate ? this.octokit.repos.getCommit({ owner, repo, ref: ghPR.head.sha }).then((res) => res.data.commit.author?.date).catch(() =>
|
|
11216
|
+
const commitDatePromise = needCommitDate ? this.octokit.repos.getCommit({ owner, repo, ref: ghPR.head.sha }).then((res) => res.data.commit.author?.date).catch((err) => {
|
|
11217
|
+
const status2 = getHttpStatusCode(err);
|
|
11218
|
+
if (status2 === 429) throw err;
|
|
11219
|
+
if (status2 === 403) {
|
|
11220
|
+
const msg = errorMessage(err).toLowerCase();
|
|
11221
|
+
if (msg.includes("rate limit") || msg.includes("abuse detection")) throw err;
|
|
11222
|
+
warn(
|
|
11223
|
+
"pr-monitor",
|
|
11224
|
+
`403 fetching commit date for ${owner}/${repo}@${ghPR.head.sha.slice(0, 7)}: ${errorMessage(err)}`
|
|
11225
|
+
);
|
|
11226
|
+
return void 0;
|
|
11227
|
+
}
|
|
11228
|
+
warn(
|
|
11229
|
+
"pr-monitor",
|
|
11230
|
+
`Failed to fetch commit date for ${owner}/${repo}@${ghPR.head.sha.slice(0, 7)}: ${errorMessage(err)}`
|
|
11231
|
+
);
|
|
11232
|
+
return void 0;
|
|
11233
|
+
}) : Promise.resolve(void 0);
|
|
11217
11234
|
const [{ status: ciStatus, failingCheckNames, failingCheckConclusions }, latestCommitDate] = await Promise.all([
|
|
11218
11235
|
ciPromise,
|
|
11219
11236
|
commitDatePromise
|
|
@@ -12926,7 +12943,7 @@ function buildRepoMap(prs, label) {
|
|
|
12926
12943
|
const repoMap = /* @__PURE__ */ new Map();
|
|
12927
12944
|
for (const pr of prs) {
|
|
12928
12945
|
if (!pr.repo) {
|
|
12929
|
-
|
|
12946
|
+
warn(label, `Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
|
|
12930
12947
|
continue;
|
|
12931
12948
|
}
|
|
12932
12949
|
const existing = repoMap.get(pr.repo) || [];
|
|
@@ -13331,6 +13348,7 @@ var init_daily_logic = __esm({
|
|
|
13331
13348
|
"src/core/daily-logic.ts"() {
|
|
13332
13349
|
"use strict";
|
|
13333
13350
|
init_utils();
|
|
13351
|
+
init_logger();
|
|
13334
13352
|
CRITICAL_STATUSES = /* @__PURE__ */ new Set([
|
|
13335
13353
|
"needs_response",
|
|
13336
13354
|
"needs_changes",
|
|
@@ -13685,11 +13703,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
13685
13703
|
const dismissTime = new Date(dismissedAt).getTime();
|
|
13686
13704
|
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
13687
13705
|
console.error(`[DAILY] Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
13688
|
-
stateManager2.undismissIssue(issue.url);
|
|
13689
|
-
hasAutoUndismissed = true;
|
|
13690
13706
|
return true;
|
|
13691
13707
|
}
|
|
13692
13708
|
if (responseTime > dismissTime) {
|
|
13709
|
+
console.error(
|
|
13710
|
+
`[DAILY] Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`
|
|
13711
|
+
);
|
|
13693
13712
|
stateManager2.undismissIssue(issue.url);
|
|
13694
13713
|
hasAutoUndismissed = true;
|
|
13695
13714
|
return true;
|
|
@@ -13697,9 +13716,6 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
13697
13716
|
}
|
|
13698
13717
|
return false;
|
|
13699
13718
|
});
|
|
13700
|
-
if (hasAutoUndismissed) {
|
|
13701
|
-
stateManager2.save();
|
|
13702
|
-
}
|
|
13703
13719
|
const issueResponses = filteredCommentedIssues.filter(
|
|
13704
13720
|
(i) => i.status === "new_response"
|
|
13705
13721
|
);
|
|
@@ -13707,8 +13723,32 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
13707
13723
|
const snoozedUrls = new Set(
|
|
13708
13724
|
Object.keys(stateManager2.getState().config.snoozedPRs ?? {}).filter((url) => stateManager2.isSnoozed(url))
|
|
13709
13725
|
);
|
|
13710
|
-
const
|
|
13711
|
-
|
|
13726
|
+
const nonDismissedPRs = activePRs.filter((pr) => {
|
|
13727
|
+
const dismissedAt = stateManager2.getIssueDismissedAt(pr.url);
|
|
13728
|
+
if (!dismissedAt) return true;
|
|
13729
|
+
const activityTime = new Date(pr.updatedAt).getTime();
|
|
13730
|
+
const dismissTime = new Date(dismissedAt).getTime();
|
|
13731
|
+
if (isNaN(activityTime) || isNaN(dismissTime)) {
|
|
13732
|
+
console.error(`[DAILY] Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
|
|
13733
|
+
return true;
|
|
13734
|
+
}
|
|
13735
|
+
if (activityTime > dismissTime) {
|
|
13736
|
+
console.error(
|
|
13737
|
+
`[DAILY] Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`
|
|
13738
|
+
);
|
|
13739
|
+
stateManager2.undismissIssue(pr.url);
|
|
13740
|
+
hasAutoUndismissed = true;
|
|
13741
|
+
return true;
|
|
13742
|
+
}
|
|
13743
|
+
return false;
|
|
13744
|
+
});
|
|
13745
|
+
if (hasAutoUndismissed) {
|
|
13746
|
+
try {
|
|
13747
|
+
stateManager2.save();
|
|
13748
|
+
} catch (error) {
|
|
13749
|
+
console.error("[DAILY] Failed to persist auto-undismissed state:", errorMessage(error));
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13712
13752
|
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
13713
13753
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
13714
13754
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
@@ -13908,12 +13948,13 @@ function validateGitHubUsername(username) {
|
|
|
13908
13948
|
}
|
|
13909
13949
|
return trimmed;
|
|
13910
13950
|
}
|
|
13911
|
-
var PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
|
|
13951
|
+
var PR_URL_PATTERN, ISSUE_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
|
|
13912
13952
|
var init_validation = __esm({
|
|
13913
13953
|
"src/commands/validation.ts"() {
|
|
13914
13954
|
"use strict";
|
|
13915
13955
|
init_errors();
|
|
13916
13956
|
PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
|
|
13957
|
+
ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
|
|
13917
13958
|
ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
|
|
13918
13959
|
MAX_URL_LENGTH = 2048;
|
|
13919
13960
|
MAX_MESSAGE_LENGTH = 1e3;
|
|
@@ -14033,6 +14074,7 @@ __export(comments_exports, {
|
|
|
14033
14074
|
});
|
|
14034
14075
|
async function runComments(options) {
|
|
14035
14076
|
validateUrl(options.prUrl);
|
|
14077
|
+
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, "PR");
|
|
14036
14078
|
const token = requireGitHubToken();
|
|
14037
14079
|
const stateManager2 = getStateManager();
|
|
14038
14080
|
const octokit = getOctokit(token);
|
|
@@ -14116,6 +14158,7 @@ async function runComments(options) {
|
|
|
14116
14158
|
}
|
|
14117
14159
|
async function runPost(options) {
|
|
14118
14160
|
validateUrl(options.url);
|
|
14161
|
+
validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
|
|
14119
14162
|
if (!options.message.trim()) {
|
|
14120
14163
|
throw new Error("No message provided");
|
|
14121
14164
|
}
|
|
@@ -14140,6 +14183,7 @@ async function runPost(options) {
|
|
|
14140
14183
|
}
|
|
14141
14184
|
async function runClaim(options) {
|
|
14142
14185
|
validateUrl(options.issueUrl);
|
|
14186
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
14143
14187
|
const token = requireGitHubToken();
|
|
14144
14188
|
const message = options.message || "Hi! I'd like to work on this issue. Could you assign it to me?";
|
|
14145
14189
|
validateMessage(message);
|
|
@@ -14155,20 +14199,26 @@ async function runClaim(options) {
|
|
|
14155
14199
|
issue_number: number,
|
|
14156
14200
|
body: message
|
|
14157
14201
|
});
|
|
14158
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14202
|
+
try {
|
|
14203
|
+
const stateManager2 = getStateManager();
|
|
14204
|
+
stateManager2.addIssue({
|
|
14205
|
+
id: number,
|
|
14206
|
+
url: options.issueUrl,
|
|
14207
|
+
repo: `${owner}/${repo}`,
|
|
14208
|
+
number,
|
|
14209
|
+
title: "(claimed)",
|
|
14210
|
+
status: "claimed",
|
|
14211
|
+
labels: [],
|
|
14212
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14213
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14214
|
+
vetted: false
|
|
14215
|
+
});
|
|
14216
|
+
stateManager2.save();
|
|
14217
|
+
} catch (error) {
|
|
14218
|
+
console.error(
|
|
14219
|
+
`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`
|
|
14220
|
+
);
|
|
14221
|
+
}
|
|
14172
14222
|
return {
|
|
14173
14223
|
commentUrl: comment.html_url,
|
|
14174
14224
|
issueUrl: options.issueUrl
|
|
@@ -14281,6 +14331,13 @@ __export(setup_exports, {
|
|
|
14281
14331
|
runCheckSetup: () => runCheckSetup,
|
|
14282
14332
|
runSetup: () => runSetup
|
|
14283
14333
|
});
|
|
14334
|
+
function parsePositiveInt(value, settingName) {
|
|
14335
|
+
const parsed = Number(value);
|
|
14336
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
14337
|
+
throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
|
|
14338
|
+
}
|
|
14339
|
+
return parsed;
|
|
14340
|
+
}
|
|
14284
14341
|
async function runSetup(options) {
|
|
14285
14342
|
const stateManager2 = getStateManager();
|
|
14286
14343
|
const config = stateManager2.getState().config;
|
|
@@ -14296,18 +14353,24 @@ async function runSetup(options) {
|
|
|
14296
14353
|
stateManager2.updateConfig({ githubUsername: value });
|
|
14297
14354
|
results[key] = value;
|
|
14298
14355
|
break;
|
|
14299
|
-
case "maxActivePRs":
|
|
14300
|
-
|
|
14301
|
-
|
|
14356
|
+
case "maxActivePRs": {
|
|
14357
|
+
const maxPRs = parsePositiveInt(value, "maxActivePRs");
|
|
14358
|
+
stateManager2.updateConfig({ maxActivePRs: maxPRs });
|
|
14359
|
+
results[key] = String(maxPRs);
|
|
14302
14360
|
break;
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14361
|
+
}
|
|
14362
|
+
case "dormantDays": {
|
|
14363
|
+
const dormant = parsePositiveInt(value, "dormantDays");
|
|
14364
|
+
stateManager2.updateConfig({ dormantThresholdDays: dormant });
|
|
14365
|
+
results[key] = String(dormant);
|
|
14306
14366
|
break;
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14367
|
+
}
|
|
14368
|
+
case "approachingDays": {
|
|
14369
|
+
const approaching = parsePositiveInt(value, "approachingDays");
|
|
14370
|
+
stateManager2.updateConfig({ approachingDormantDays: approaching });
|
|
14371
|
+
results[key] = String(approaching);
|
|
14310
14372
|
break;
|
|
14373
|
+
}
|
|
14311
14374
|
case "languages":
|
|
14312
14375
|
stateManager2.updateConfig({ languages: value.split(",").map((l) => l.trim()) });
|
|
14313
14376
|
results[key] = value;
|
|
@@ -14330,9 +14393,12 @@ async function runSetup(options) {
|
|
|
14330
14393
|
}
|
|
14331
14394
|
break;
|
|
14332
14395
|
case "minStars": {
|
|
14333
|
-
const
|
|
14334
|
-
|
|
14335
|
-
|
|
14396
|
+
const stars = Number(value);
|
|
14397
|
+
if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
|
|
14398
|
+
throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
|
|
14399
|
+
}
|
|
14400
|
+
stateManager2.updateConfig({ minStars: stars });
|
|
14401
|
+
results[key] = String(stars);
|
|
14336
14402
|
break;
|
|
14337
14403
|
}
|
|
14338
14404
|
case "includeDocIssues":
|
|
@@ -14456,6 +14522,7 @@ var init_setup = __esm({
|
|
|
14456
14522
|
"src/commands/setup.ts"() {
|
|
14457
14523
|
"use strict";
|
|
14458
14524
|
init_core();
|
|
14525
|
+
init_errors();
|
|
14459
14526
|
init_validation();
|
|
14460
14527
|
}
|
|
14461
14528
|
});
|
|
@@ -14531,7 +14598,11 @@ async function fetchDashboardData(token) {
|
|
|
14531
14598
|
digest.autoUnshelvedPRs = [];
|
|
14532
14599
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
14533
14600
|
stateManager2.setLastDigest(digest);
|
|
14534
|
-
|
|
14601
|
+
try {
|
|
14602
|
+
stateManager2.save();
|
|
14603
|
+
} catch (error) {
|
|
14604
|
+
console.error("Warning: Failed to save dashboard digest to state:", errorMessage(error));
|
|
14605
|
+
}
|
|
14535
14606
|
console.error(`Refreshed: ${prs.length} PRs fetched`);
|
|
14536
14607
|
return { digest, commentedIssues };
|
|
14537
14608
|
}
|
|
@@ -16325,6 +16396,38 @@ async function startDashboardServer(options) {
|
|
|
16325
16396
|
sendError(res, 400, 'Missing or invalid "url" field');
|
|
16326
16397
|
return;
|
|
16327
16398
|
}
|
|
16399
|
+
try {
|
|
16400
|
+
validateUrl(body.url);
|
|
16401
|
+
validateGitHubUrl(body.url, PR_URL_PATTERN, "PR");
|
|
16402
|
+
} catch (err) {
|
|
16403
|
+
if (err instanceof ValidationError) {
|
|
16404
|
+
sendError(res, 400, err.message);
|
|
16405
|
+
} else {
|
|
16406
|
+
console.error("Unexpected error during URL validation:", err);
|
|
16407
|
+
sendError(res, 400, "Invalid URL");
|
|
16408
|
+
}
|
|
16409
|
+
return;
|
|
16410
|
+
}
|
|
16411
|
+
if (body.action === "snooze") {
|
|
16412
|
+
const days = body.days ?? 7;
|
|
16413
|
+
if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
|
|
16414
|
+
sendError(res, 400, "Snooze days must be a positive finite number");
|
|
16415
|
+
return;
|
|
16416
|
+
}
|
|
16417
|
+
if (body.reason !== void 0) {
|
|
16418
|
+
try {
|
|
16419
|
+
validateMessage(String(body.reason));
|
|
16420
|
+
} catch (err) {
|
|
16421
|
+
if (err instanceof ValidationError) {
|
|
16422
|
+
sendError(res, 400, err.message);
|
|
16423
|
+
} else {
|
|
16424
|
+
console.error("Unexpected error during message validation:", err);
|
|
16425
|
+
sendError(res, 400, "Invalid reason");
|
|
16426
|
+
}
|
|
16427
|
+
return;
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
}
|
|
16328
16431
|
try {
|
|
16329
16432
|
switch (body.action) {
|
|
16330
16433
|
case "shelve":
|
|
@@ -16334,7 +16437,7 @@ async function startDashboardServer(options) {
|
|
|
16334
16437
|
stateManager2.unshelvePR(body.url);
|
|
16335
16438
|
break;
|
|
16336
16439
|
case "snooze":
|
|
16337
|
-
stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days
|
|
16440
|
+
stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days ?? 7);
|
|
16338
16441
|
break;
|
|
16339
16442
|
case "unsnooze":
|
|
16340
16443
|
stateManager2.unsnoozePR(body.url);
|
|
@@ -16488,6 +16591,7 @@ var init_dashboard_server = __esm({
|
|
|
16488
16591
|
path5 = __toESM(require("path"), 1);
|
|
16489
16592
|
init_core();
|
|
16490
16593
|
init_errors();
|
|
16594
|
+
init_validation();
|
|
16491
16595
|
init_dashboard_data();
|
|
16492
16596
|
init_dashboard_templates();
|
|
16493
16597
|
VALID_ACTIONS = /* @__PURE__ */ new Set(["shelve", "unshelve", "snooze", "unsnooze"]);
|
|
@@ -16541,6 +16645,8 @@ async function runDashboard(options) {
|
|
|
16541
16645
|
digest = stateManager2.getState().lastDigest;
|
|
16542
16646
|
}
|
|
16543
16647
|
} else {
|
|
16648
|
+
console.error("Warning: No GitHub token found. Using cached data (may be stale).");
|
|
16649
|
+
console.error("Set GITHUB_TOKEN or run `gh auth login` for fresh data.");
|
|
16544
16650
|
digest = stateManager2.getState().lastDigest;
|
|
16545
16651
|
}
|
|
16546
16652
|
if (!digest) {
|
|
@@ -16579,7 +16685,6 @@ async function runDashboard(options) {
|
|
|
16579
16685
|
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses);
|
|
16580
16686
|
const dashboardPath = getDashboardPath();
|
|
16581
16687
|
fs6.writeFileSync(dashboardPath, html, { mode: 420 });
|
|
16582
|
-
fs6.chmodSync(dashboardPath, 420);
|
|
16583
16688
|
if (options.offline) {
|
|
16584
16689
|
const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
16585
16690
|
console.log(`
|
|
@@ -16615,7 +16720,6 @@ function writeDashboardFromState() {
|
|
|
16615
16720
|
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
|
|
16616
16721
|
const dashboardPath = getDashboardPath();
|
|
16617
16722
|
fs6.writeFileSync(dashboardPath, html, { mode: 420 });
|
|
16618
|
-
fs6.chmodSync(dashboardPath, 420);
|
|
16619
16723
|
return dashboardPath;
|
|
16620
16724
|
}
|
|
16621
16725
|
function resolveAssetsDir() {
|
|
@@ -17004,7 +17108,7 @@ async function runLocalRepos(options) {
|
|
|
17004
17108
|
stateManager2.save();
|
|
17005
17109
|
} catch (error) {
|
|
17006
17110
|
const msg = errorMessage(error);
|
|
17007
|
-
|
|
17111
|
+
console.error(`Warning: Failed to cache scan results: ${msg}`);
|
|
17008
17112
|
}
|
|
17009
17113
|
return {
|
|
17010
17114
|
repos,
|
|
@@ -17385,12 +17489,20 @@ Last Run: ${data.lastRunAt || "Never"}`);
|
|
|
17385
17489
|
program2.command("search [count]").description("Search for new issues to work on").option("--json", "Output as JSON").action(async (count, options) => {
|
|
17386
17490
|
try {
|
|
17387
17491
|
const { runSearch: runSearch2 } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
17492
|
+
let maxResults = 5;
|
|
17493
|
+
if (count !== void 0) {
|
|
17494
|
+
const parsed = Number(count);
|
|
17495
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
17496
|
+
throw new Error(`Invalid count "${count}". Must be a positive integer.`);
|
|
17497
|
+
}
|
|
17498
|
+
maxResults = parsed;
|
|
17499
|
+
}
|
|
17388
17500
|
if (!options.json) {
|
|
17389
17501
|
console.log(`
|
|
17390
|
-
Searching for issues (max ${
|
|
17502
|
+
Searching for issues (max ${maxResults})...
|
|
17391
17503
|
`);
|
|
17392
17504
|
}
|
|
17393
|
-
const data = await runSearch2({ maxResults
|
|
17505
|
+
const data = await runSearch2({ maxResults });
|
|
17394
17506
|
if (options.json) {
|
|
17395
17507
|
outputJson(data);
|
|
17396
17508
|
} else {
|
|
@@ -17689,17 +17801,25 @@ program2.command("checkSetup").description("Check if setup is complete").option(
|
|
|
17689
17801
|
});
|
|
17690
17802
|
var dashboardCmd = program2.command("dashboard").description("Dashboard commands");
|
|
17691
17803
|
dashboardCmd.command("serve").description("Start interactive dashboard server").option("--port <port>", "Port to listen on", "3000").option("--no-open", "Do not open browser automatically").action(async (options) => {
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17804
|
+
try {
|
|
17805
|
+
const port = parseInt(options.port, 10);
|
|
17806
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
17807
|
+
console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
|
|
17808
|
+
process.exit(1);
|
|
17809
|
+
}
|
|
17810
|
+
const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17811
|
+
await serveDashboard2({ port, open: options.open });
|
|
17812
|
+
} catch (err) {
|
|
17813
|
+
handleCommandError(err);
|
|
17696
17814
|
}
|
|
17697
|
-
const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17698
|
-
await serveDashboard2({ port, open: options.open });
|
|
17699
17815
|
});
|
|
17700
17816
|
dashboardCmd.option("--open", "Open in browser").option("--json", "Output as JSON").option("--offline", "Use cached data only (no GitHub API calls)").action(async (options) => {
|
|
17701
|
-
|
|
17702
|
-
|
|
17817
|
+
try {
|
|
17818
|
+
const { runDashboard: runDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17819
|
+
await runDashboard2({ open: options.open, json: options.json, offline: options.offline });
|
|
17820
|
+
} catch (err) {
|
|
17821
|
+
handleCommandError(err, options.json);
|
|
17822
|
+
}
|
|
17703
17823
|
});
|
|
17704
17824
|
program2.command("parse-issue-list <path>").description("Parse a markdown issue list into structured JSON").option("--json", "Output as JSON").action(async (filePath, options) => {
|
|
17705
17825
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -127,10 +127,18 @@ program
|
|
|
127
127
|
.action(async (count, options) => {
|
|
128
128
|
try {
|
|
129
129
|
const { runSearch } = await import('./commands/search.js');
|
|
130
|
+
let maxResults = 5;
|
|
131
|
+
if (count !== undefined) {
|
|
132
|
+
const parsed = Number(count);
|
|
133
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
134
|
+
throw new Error(`Invalid count "${count}". Must be a positive integer.`);
|
|
135
|
+
}
|
|
136
|
+
maxResults = parsed;
|
|
137
|
+
}
|
|
130
138
|
if (!options.json) {
|
|
131
|
-
console.log(`\nSearching for issues (max ${
|
|
139
|
+
console.log(`\nSearching for issues (max ${maxResults})...\n`);
|
|
132
140
|
}
|
|
133
|
-
const data = await runSearch({ maxResults
|
|
141
|
+
const data = await runSearch({ maxResults });
|
|
134
142
|
if (options.json) {
|
|
135
143
|
outputJson(data);
|
|
136
144
|
}
|
|
@@ -516,13 +524,18 @@ dashboardCmd
|
|
|
516
524
|
.option('--port <port>', 'Port to listen on', '3000')
|
|
517
525
|
.option('--no-open', 'Do not open browser automatically')
|
|
518
526
|
.action(async (options) => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
527
|
+
try {
|
|
528
|
+
const port = parseInt(options.port, 10);
|
|
529
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
530
|
+
console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
const { serveDashboard } = await import('./commands/dashboard.js');
|
|
534
|
+
await serveDashboard({ port, open: options.open });
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
handleCommandError(err);
|
|
523
538
|
}
|
|
524
|
-
const { serveDashboard } = await import('./commands/dashboard.js');
|
|
525
|
-
await serveDashboard({ port, open: options.open });
|
|
526
539
|
});
|
|
527
540
|
// Keep bare `dashboard` (no subcommand) for backward compat ā generates static HTML
|
|
528
541
|
dashboardCmd
|
|
@@ -530,8 +543,13 @@ dashboardCmd
|
|
|
530
543
|
.option('--json', 'Output as JSON')
|
|
531
544
|
.option('--offline', 'Use cached data only (no GitHub API calls)')
|
|
532
545
|
.action(async (options) => {
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
try {
|
|
547
|
+
const { runDashboard } = await import('./commands/dashboard.js');
|
|
548
|
+
await runDashboard({ open: options.open, json: options.json, offline: options.offline });
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
handleCommandError(err, options.json);
|
|
552
|
+
}
|
|
535
553
|
});
|
|
536
554
|
// Parse issue list command (#82)
|
|
537
555
|
program
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager, getOctokit, parseGitHubUrl, requireGitHubToken } from '../core/index.js';
|
|
6
6
|
import { paginateAll } from '../core/pagination.js';
|
|
7
|
-
import { validateUrl, validateMessage } from './validation.js';
|
|
7
|
+
import { validateUrl, validateMessage, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, ISSUE_URL_PATTERN, } from './validation.js';
|
|
8
8
|
export async function runComments(options) {
|
|
9
9
|
validateUrl(options.prUrl);
|
|
10
|
+
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
10
11
|
const token = requireGitHubToken();
|
|
11
12
|
const stateManager = getStateManager();
|
|
12
13
|
const octokit = getOctokit(token);
|
|
@@ -97,6 +98,7 @@ export async function runComments(options) {
|
|
|
97
98
|
}
|
|
98
99
|
export async function runPost(options) {
|
|
99
100
|
validateUrl(options.url);
|
|
101
|
+
validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
|
|
100
102
|
if (!options.message.trim()) {
|
|
101
103
|
throw new Error('No message provided');
|
|
102
104
|
}
|
|
@@ -122,6 +124,7 @@ export async function runPost(options) {
|
|
|
122
124
|
}
|
|
123
125
|
export async function runClaim(options) {
|
|
124
126
|
validateUrl(options.issueUrl);
|
|
127
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
|
|
125
128
|
const token = requireGitHubToken();
|
|
126
129
|
// Default claim message or custom
|
|
127
130
|
const message = options.message || "Hi! I'd like to work on this issue. Could you assign it to me?";
|
|
@@ -139,21 +142,26 @@ export async function runClaim(options) {
|
|
|
139
142
|
issue_number: number,
|
|
140
143
|
body: message,
|
|
141
144
|
});
|
|
142
|
-
// Add to tracked issues
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
// Add to tracked issues ā non-fatal if state save fails (comment already posted)
|
|
146
|
+
try {
|
|
147
|
+
const stateManager = getStateManager();
|
|
148
|
+
stateManager.addIssue({
|
|
149
|
+
id: number,
|
|
150
|
+
url: options.issueUrl,
|
|
151
|
+
repo: `${owner}/${repo}`,
|
|
152
|
+
number,
|
|
153
|
+
title: '(claimed)',
|
|
154
|
+
status: 'claimed',
|
|
155
|
+
labels: [],
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
updatedAt: new Date().toISOString(),
|
|
158
|
+
vetted: false,
|
|
159
|
+
});
|
|
160
|
+
stateManager.save();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error(`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`);
|
|
164
|
+
}
|
|
157
165
|
return {
|
|
158
166
|
commentUrl: comment.html_url,
|
|
159
167
|
issueUrl: options.issueUrl,
|
package/dist/commands/daily.js
CHANGED
|
@@ -305,14 +305,14 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
305
305
|
const responseTime = new Date(issue.lastResponseAt).getTime();
|
|
306
306
|
const dismissTime = new Date(dismissedAt).getTime();
|
|
307
307
|
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
308
|
-
// Invalid timestamp ā fail open (include issue to be safe)
|
|
308
|
+
// Invalid timestamp ā fail open (include issue to be safe) without
|
|
309
|
+
// permanently removing dismiss record (may be a transient data issue)
|
|
309
310
|
console.error(`[DAILY] Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
310
|
-
stateManager.undismissIssue(issue.url);
|
|
311
|
-
hasAutoUndismissed = true;
|
|
312
311
|
return true;
|
|
313
312
|
}
|
|
314
313
|
if (responseTime > dismissTime) {
|
|
315
314
|
// New activity after dismiss ā auto-undismiss and resurface
|
|
315
|
+
console.error(`[DAILY] Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
|
|
316
316
|
stateManager.undismissIssue(issue.url);
|
|
317
317
|
hasAutoUndismissed = true;
|
|
318
318
|
return true;
|
|
@@ -321,15 +321,41 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
321
321
|
// Still dismissed (last response is at or before dismiss timestamp)
|
|
322
322
|
return false;
|
|
323
323
|
});
|
|
324
|
-
if (hasAutoUndismissed) {
|
|
325
|
-
stateManager.save();
|
|
326
|
-
}
|
|
327
324
|
const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
|
|
328
325
|
const summary = formatSummary(digest, capacity, issueResponses);
|
|
329
326
|
const snoozedUrls = new Set(Object.keys(stateManager.getState().config.snoozedPRs ?? {}).filter((url) => stateManager.isSnoozed(url)));
|
|
330
|
-
// Filter dismissed
|
|
331
|
-
const
|
|
332
|
-
|
|
327
|
+
// Filter dismissed PRs: suppress if dismissed after last activity, auto-undismiss if new activity (#416, #468)
|
|
328
|
+
const nonDismissedPRs = activePRs.filter((pr) => {
|
|
329
|
+
const dismissedAt = stateManager.getIssueDismissedAt(pr.url);
|
|
330
|
+
if (!dismissedAt)
|
|
331
|
+
return true; // Not dismissed ā include
|
|
332
|
+
const activityTime = new Date(pr.updatedAt).getTime();
|
|
333
|
+
const dismissTime = new Date(dismissedAt).getTime();
|
|
334
|
+
if (isNaN(activityTime) || isNaN(dismissTime)) {
|
|
335
|
+
// Invalid timestamp ā fail open (include PR to be safe) without
|
|
336
|
+
// permanently removing dismiss record (may be a transient data issue)
|
|
337
|
+
console.error(`[DAILY] Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (activityTime > dismissTime) {
|
|
341
|
+
// New activity after dismiss ā auto-undismiss and resurface
|
|
342
|
+
console.error(`[DAILY] Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
|
|
343
|
+
stateManager.undismissIssue(pr.url);
|
|
344
|
+
hasAutoUndismissed = true;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
// Still dismissed (last activity is at or before dismiss timestamp)
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
// Persist auto-undismiss state changes (issue + PR combined into one save)
|
|
351
|
+
if (hasAutoUndismissed) {
|
|
352
|
+
try {
|
|
353
|
+
stateManager.save();
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error('[DAILY] Failed to persist auto-undismissed state:', errorMessage(error));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
333
359
|
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
334
360
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
335
361
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
@@ -88,7 +88,12 @@ export async function fetchDashboardData(token) {
|
|
|
88
88
|
digest.autoUnshelvedPRs = [];
|
|
89
89
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
90
90
|
stateManager.setLastDigest(digest);
|
|
91
|
-
|
|
91
|
+
try {
|
|
92
|
+
stateManager.save();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('Warning: Failed to save dashboard digest to state:', errorMessage(error));
|
|
96
|
+
}
|
|
92
97
|
console.error(`Refreshed: ${prs.length} PRs fetched`);
|
|
93
98
|
return { digest, commentedIssues };
|
|
94
99
|
}
|
|
@@ -9,7 +9,8 @@ import * as http from 'http';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getStateManager, getGitHubToken } from '../core/index.js';
|
|
12
|
-
import { errorMessage } from '../core/errors.js';
|
|
12
|
+
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
|
+
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
|
|
13
14
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
|
|
14
15
|
import { buildDashboardStats } from './dashboard-templates.js';
|
|
15
16
|
// āā Constants āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -185,6 +186,44 @@ export async function startDashboardServer(options) {
|
|
|
185
186
|
sendError(res, 400, 'Missing or invalid "url" field');
|
|
186
187
|
return;
|
|
187
188
|
}
|
|
189
|
+
// Validate URL format ā same checks as CLI commands
|
|
190
|
+
try {
|
|
191
|
+
validateUrl(body.url);
|
|
192
|
+
validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (err instanceof ValidationError) {
|
|
196
|
+
sendError(res, 400, err.message);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.error('Unexpected error during URL validation:', err);
|
|
200
|
+
sendError(res, 400, 'Invalid URL');
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Validate snooze-specific fields
|
|
205
|
+
if (body.action === 'snooze') {
|
|
206
|
+
const days = body.days ?? 7;
|
|
207
|
+
if (typeof days !== 'number' || !Number.isFinite(days) || days <= 0) {
|
|
208
|
+
sendError(res, 400, 'Snooze days must be a positive finite number');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (body.reason !== undefined) {
|
|
212
|
+
try {
|
|
213
|
+
validateMessage(String(body.reason));
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err instanceof ValidationError) {
|
|
217
|
+
sendError(res, 400, err.message);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.error('Unexpected error during message validation:', err);
|
|
221
|
+
sendError(res, 400, 'Invalid reason');
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
188
227
|
try {
|
|
189
228
|
switch (body.action) {
|
|
190
229
|
case 'shelve':
|
|
@@ -194,7 +233,7 @@ export async function startDashboardServer(options) {
|
|
|
194
233
|
stateManager.unshelvePR(body.url);
|
|
195
234
|
break;
|
|
196
235
|
case 'snooze':
|
|
197
|
-
stateManager.snoozePR(body.url, body.reason || 'Snoozed via dashboard', body.days
|
|
236
|
+
stateManager.snoozePR(body.url, body.reason || 'Snoozed via dashboard', body.days ?? 7);
|
|
198
237
|
break;
|
|
199
238
|
case 'unsnooze':
|
|
200
239
|
stateManager.unsnoozePR(body.url);
|
|
@@ -46,7 +46,9 @@ export async function runDashboard(options) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
49
|
-
// No token and not offline ā fall back to cached digest
|
|
49
|
+
// No token and not offline ā fall back to cached digest with warning
|
|
50
|
+
console.error('Warning: No GitHub token found. Using cached data (may be stale).');
|
|
51
|
+
console.error('Set GITHUB_TOKEN or run `gh auth login` for fresh data.');
|
|
50
52
|
digest = stateManager.getState().lastDigest;
|
|
51
53
|
}
|
|
52
54
|
// Check if we have a digest to display
|
|
@@ -89,7 +91,6 @@ export async function runDashboard(options) {
|
|
|
89
91
|
// Write to file in ~/.oss-autopilot/
|
|
90
92
|
const dashboardPath = getDashboardPath();
|
|
91
93
|
fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
|
|
92
|
-
fs.chmodSync(dashboardPath, 0o644);
|
|
93
94
|
if (options.offline) {
|
|
94
95
|
const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
95
96
|
console.log(`\nš Dashboard generated (offline, cached data from ${lastUpdated}): ${dashboardPath}`);
|
|
@@ -131,7 +132,6 @@ export function writeDashboardFromState() {
|
|
|
131
132
|
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
|
|
132
133
|
const dashboardPath = getDashboardPath();
|
|
133
134
|
fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
|
|
134
|
-
fs.chmodSync(dashboardPath, 0o644);
|
|
135
135
|
return dashboardPath;
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
@@ -117,7 +117,7 @@ export async function runLocalRepos(options) {
|
|
|
117
117
|
}
|
|
118
118
|
catch (error) {
|
|
119
119
|
const msg = errorMessage(error);
|
|
120
|
-
|
|
120
|
+
console.error(`Warning: Failed to cache scan results: ${msg}`);
|
|
121
121
|
}
|
|
122
122
|
return {
|
|
123
123
|
repos,
|
package/dist/commands/setup.js
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
* Interactive setup / configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
|
|
6
|
+
import { ValidationError } from '../core/errors.js';
|
|
6
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
|
+
/** Parse and validate a positive integer setting value. */
|
|
9
|
+
function parsePositiveInt(value, settingName) {
|
|
10
|
+
const parsed = Number(value);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
12
|
+
throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
7
16
|
export async function runSetup(options) {
|
|
8
17
|
const stateManager = getStateManager();
|
|
9
18
|
const config = stateManager.getState().config;
|
|
@@ -20,18 +29,24 @@ export async function runSetup(options) {
|
|
|
20
29
|
stateManager.updateConfig({ githubUsername: value });
|
|
21
30
|
results[key] = value;
|
|
22
31
|
break;
|
|
23
|
-
case 'maxActivePRs':
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
case 'maxActivePRs': {
|
|
33
|
+
const maxPRs = parsePositiveInt(value, 'maxActivePRs');
|
|
34
|
+
stateManager.updateConfig({ maxActivePRs: maxPRs });
|
|
35
|
+
results[key] = String(maxPRs);
|
|
26
36
|
break;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
}
|
|
38
|
+
case 'dormantDays': {
|
|
39
|
+
const dormant = parsePositiveInt(value, 'dormantDays');
|
|
40
|
+
stateManager.updateConfig({ dormantThresholdDays: dormant });
|
|
41
|
+
results[key] = String(dormant);
|
|
30
42
|
break;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
}
|
|
44
|
+
case 'approachingDays': {
|
|
45
|
+
const approaching = parsePositiveInt(value, 'approachingDays');
|
|
46
|
+
stateManager.updateConfig({ approachingDormantDays: approaching });
|
|
47
|
+
results[key] = String(approaching);
|
|
34
48
|
break;
|
|
49
|
+
}
|
|
35
50
|
case 'languages':
|
|
36
51
|
stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
|
|
37
52
|
results[key] = value;
|
|
@@ -55,9 +70,12 @@ export async function runSetup(options) {
|
|
|
55
70
|
}
|
|
56
71
|
break;
|
|
57
72
|
case 'minStars': {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
const stars = Number(value);
|
|
74
|
+
if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
|
|
75
|
+
throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
|
|
76
|
+
}
|
|
77
|
+
stateManager.updateConfig({ minStars: stars });
|
|
78
|
+
results[key] = String(stars);
|
|
61
79
|
break;
|
|
62
80
|
}
|
|
63
81
|
case 'includeDocIssues':
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - formatBriefSummary / formatSummary / printDigest ā rendering
|
|
12
12
|
*/
|
|
13
13
|
import { formatRelativeTime } from './utils.js';
|
|
14
|
+
import { warn } from './logger.js';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Constants
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -45,7 +46,7 @@ function buildRepoMap(prs, label) {
|
|
|
45
46
|
const repoMap = new Map();
|
|
46
47
|
for (const pr of prs) {
|
|
47
48
|
if (!pr.repo) {
|
|
48
|
-
|
|
49
|
+
warn(label, `Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
|
|
49
50
|
continue;
|
|
50
51
|
}
|
|
51
52
|
const existing = repoMap.get(pr.repo) || [];
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -204,7 +204,23 @@ export class PRMonitor {
|
|
|
204
204
|
? this.octokit.repos
|
|
205
205
|
.getCommit({ owner, repo, ref: ghPR.head.sha })
|
|
206
206
|
.then((res) => res.data.commit.author?.date)
|
|
207
|
-
.catch(() =>
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
// Rate limit errors must propagate ā silently swallowing them produces
|
|
209
|
+
// misleading status (e.g. needs_changes when changes were addressed) (#469).
|
|
210
|
+
const status = getHttpStatusCode(err);
|
|
211
|
+
if (status === 429)
|
|
212
|
+
throw err;
|
|
213
|
+
if (status === 403) {
|
|
214
|
+
const msg = errorMessage(err).toLowerCase();
|
|
215
|
+
if (msg.includes('rate limit') || msg.includes('abuse detection'))
|
|
216
|
+
throw err;
|
|
217
|
+
// Non-rate-limit 403 (DMCA, private repo, SSO) ā degrade gracefully
|
|
218
|
+
warn('pr-monitor', `403 fetching commit date for ${owner}/${repo}@${ghPR.head.sha.slice(0, 7)}: ${errorMessage(err)}`);
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
warn('pr-monitor', `Failed to fetch commit date for ${owner}/${repo}@${ghPR.head.sha.slice(0, 7)}: ${errorMessage(err)}`);
|
|
222
|
+
return undefined;
|
|
223
|
+
})
|
|
208
224
|
: Promise.resolve(undefined);
|
|
209
225
|
const [{ status: ciStatus, failingCheckNames, failingCheckConclusions }, latestCommitDate] = await Promise.all([
|
|
210
226
|
ciPromise,
|