@oss-autopilot/core 0.42.4 → 0.42.6
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 +176 -57
- package/dist/cli.js +28 -10
- package/dist/commands/comments.js +24 -16
- package/dist/commands/daily.js +22 -7
- package/dist/commands/dashboard-data.js +25 -6
- 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/github-stats.d.ts +3 -1
- package/dist/core/github-stats.js +26 -6
- package/package.json +1 -1
package/dist/cli.bundle.cjs
CHANGED
|
@@ -10847,6 +10847,9 @@ var init_display_utils = __esm({
|
|
|
10847
10847
|
});
|
|
10848
10848
|
|
|
10849
10849
|
// src/core/github-stats.ts
|
|
10850
|
+
function emptyPRCountsResult() {
|
|
10851
|
+
return { repos: /* @__PURE__ */ new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
|
|
10852
|
+
}
|
|
10850
10853
|
function isCachedPRCounts(v) {
|
|
10851
10854
|
if (typeof v !== "object" || v === null) return false;
|
|
10852
10855
|
const obj = v;
|
|
@@ -10854,7 +10857,7 @@ function isCachedPRCounts(v) {
|
|
|
10854
10857
|
}
|
|
10855
10858
|
async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
|
|
10856
10859
|
if (!githubUsername) {
|
|
10857
|
-
return
|
|
10860
|
+
return emptyPRCountsResult();
|
|
10858
10861
|
}
|
|
10859
10862
|
const cache = getHttpCache();
|
|
10860
10863
|
const cacheKey = `pr-counts:${label}:${githubUsername}`;
|
|
@@ -10875,6 +10878,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
10875
10878
|
const dailyActivityCounts = {};
|
|
10876
10879
|
let page = 1;
|
|
10877
10880
|
let fetched = 0;
|
|
10881
|
+
let totalCount;
|
|
10878
10882
|
while (true) {
|
|
10879
10883
|
const { data } = await octokit.search.issuesAndPullRequests({
|
|
10880
10884
|
q: `is:pr ${query} author:${githubUsername}`,
|
|
@@ -10883,6 +10887,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
10883
10887
|
per_page: 100,
|
|
10884
10888
|
page
|
|
10885
10889
|
});
|
|
10890
|
+
totalCount = data.total_count;
|
|
10886
10891
|
for (const item of data.items) {
|
|
10887
10892
|
const parsed = extractOwnerRepo(item.html_url);
|
|
10888
10893
|
if (!parsed) {
|
|
@@ -10907,11 +10912,17 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
10907
10912
|
}
|
|
10908
10913
|
}
|
|
10909
10914
|
fetched += data.items.length;
|
|
10910
|
-
if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0) {
|
|
10915
|
+
if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
10911
10916
|
break;
|
|
10912
10917
|
}
|
|
10913
10918
|
page++;
|
|
10914
10919
|
}
|
|
10920
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
10921
|
+
warn(
|
|
10922
|
+
MODULE6,
|
|
10923
|
+
`Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${label} PRs. Stats may be incomplete for prolific contributors.`
|
|
10924
|
+
);
|
|
10925
|
+
}
|
|
10915
10926
|
debug(MODULE6, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
|
|
10916
10927
|
cache.set(cacheKey, "", {
|
|
10917
10928
|
reposEntries: Array.from(repos.entries()),
|
|
@@ -11020,7 +11031,7 @@ async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
|
|
|
11020
11031
|
}
|
|
11021
11032
|
);
|
|
11022
11033
|
}
|
|
11023
|
-
var MODULE6, PR_COUNTS_CACHE_TTL_MS;
|
|
11034
|
+
var MODULE6, PR_COUNTS_CACHE_TTL_MS, MAX_PAGINATION_PAGES;
|
|
11024
11035
|
var init_github_stats = __esm({
|
|
11025
11036
|
"src/core/github-stats.ts"() {
|
|
11026
11037
|
"use strict";
|
|
@@ -11028,7 +11039,8 @@ var init_github_stats = __esm({
|
|
|
11028
11039
|
init_logger();
|
|
11029
11040
|
init_http_cache();
|
|
11030
11041
|
MODULE6 = "github-stats";
|
|
11031
|
-
PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
11042
|
+
PR_COUNTS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11043
|
+
MAX_PAGINATION_PAGES = 3;
|
|
11032
11044
|
}
|
|
11033
11045
|
});
|
|
11034
11046
|
|
|
@@ -12943,7 +12955,7 @@ function buildRepoMap(prs, label) {
|
|
|
12943
12955
|
const repoMap = /* @__PURE__ */ new Map();
|
|
12944
12956
|
for (const pr of prs) {
|
|
12945
12957
|
if (!pr.repo) {
|
|
12946
|
-
|
|
12958
|
+
warn(label, `Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
|
|
12947
12959
|
continue;
|
|
12948
12960
|
}
|
|
12949
12961
|
const existing = repoMap.get(pr.repo) || [];
|
|
@@ -13348,6 +13360,7 @@ var init_daily_logic = __esm({
|
|
|
13348
13360
|
"src/core/daily-logic.ts"() {
|
|
13349
13361
|
"use strict";
|
|
13350
13362
|
init_utils();
|
|
13363
|
+
init_logger();
|
|
13351
13364
|
CRITICAL_STATUSES = /* @__PURE__ */ new Set([
|
|
13352
13365
|
"needs_response",
|
|
13353
13366
|
"needs_changes",
|
|
@@ -13475,8 +13488,14 @@ async function fetchPRData(prMonitor, token) {
|
|
|
13475
13488
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
13476
13489
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all(
|
|
13477
13490
|
[
|
|
13478
|
-
prMonitor.fetchUserMergedPRCounts()
|
|
13479
|
-
|
|
13491
|
+
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
13492
|
+
console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
13493
|
+
return emptyPRCountsResult();
|
|
13494
|
+
}),
|
|
13495
|
+
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
13496
|
+
console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
13497
|
+
return emptyPRCountsResult();
|
|
13498
|
+
}),
|
|
13480
13499
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
13481
13500
|
console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
13482
13501
|
return [];
|
|
@@ -13625,12 +13644,16 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
13625
13644
|
function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
|
|
13626
13645
|
const stateManager2 = getStateManager();
|
|
13627
13646
|
try {
|
|
13628
|
-
|
|
13647
|
+
if (Object.keys(monthlyCounts).length > 0) {
|
|
13648
|
+
stateManager2.setMonthlyMergedCounts(monthlyCounts);
|
|
13649
|
+
}
|
|
13629
13650
|
} catch (error) {
|
|
13630
13651
|
console.error("[DAILY] Failed to store monthly merged counts:", errorMessage(error));
|
|
13631
13652
|
}
|
|
13632
13653
|
try {
|
|
13633
|
-
|
|
13654
|
+
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
13655
|
+
stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
13656
|
+
}
|
|
13634
13657
|
} catch (error) {
|
|
13635
13658
|
console.error("[DAILY] Failed to store monthly closed counts:", errorMessage(error));
|
|
13636
13659
|
}
|
|
@@ -13645,7 +13668,9 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
13645
13668
|
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
13646
13669
|
}
|
|
13647
13670
|
}
|
|
13648
|
-
|
|
13671
|
+
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
13672
|
+
stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
13673
|
+
}
|
|
13649
13674
|
} catch (error) {
|
|
13650
13675
|
console.error("[DAILY] Failed to compute/store monthly opened counts:", errorMessage(error));
|
|
13651
13676
|
}
|
|
@@ -13815,6 +13840,7 @@ var init_daily = __esm({
|
|
|
13815
13840
|
"use strict";
|
|
13816
13841
|
init_core();
|
|
13817
13842
|
init_errors();
|
|
13843
|
+
init_github_stats();
|
|
13818
13844
|
init_json();
|
|
13819
13845
|
init_core();
|
|
13820
13846
|
}
|
|
@@ -13947,12 +13973,13 @@ function validateGitHubUsername(username) {
|
|
|
13947
13973
|
}
|
|
13948
13974
|
return trimmed;
|
|
13949
13975
|
}
|
|
13950
|
-
var PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
|
|
13976
|
+
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;
|
|
13951
13977
|
var init_validation = __esm({
|
|
13952
13978
|
"src/commands/validation.ts"() {
|
|
13953
13979
|
"use strict";
|
|
13954
13980
|
init_errors();
|
|
13955
13981
|
PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
|
|
13982
|
+
ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
|
|
13956
13983
|
ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
|
|
13957
13984
|
MAX_URL_LENGTH = 2048;
|
|
13958
13985
|
MAX_MESSAGE_LENGTH = 1e3;
|
|
@@ -14072,6 +14099,7 @@ __export(comments_exports, {
|
|
|
14072
14099
|
});
|
|
14073
14100
|
async function runComments(options) {
|
|
14074
14101
|
validateUrl(options.prUrl);
|
|
14102
|
+
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, "PR");
|
|
14075
14103
|
const token = requireGitHubToken();
|
|
14076
14104
|
const stateManager2 = getStateManager();
|
|
14077
14105
|
const octokit = getOctokit(token);
|
|
@@ -14155,6 +14183,7 @@ async function runComments(options) {
|
|
|
14155
14183
|
}
|
|
14156
14184
|
async function runPost(options) {
|
|
14157
14185
|
validateUrl(options.url);
|
|
14186
|
+
validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
|
|
14158
14187
|
if (!options.message.trim()) {
|
|
14159
14188
|
throw new Error("No message provided");
|
|
14160
14189
|
}
|
|
@@ -14179,6 +14208,7 @@ async function runPost(options) {
|
|
|
14179
14208
|
}
|
|
14180
14209
|
async function runClaim(options) {
|
|
14181
14210
|
validateUrl(options.issueUrl);
|
|
14211
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
14182
14212
|
const token = requireGitHubToken();
|
|
14183
14213
|
const message = options.message || "Hi! I'd like to work on this issue. Could you assign it to me?";
|
|
14184
14214
|
validateMessage(message);
|
|
@@ -14194,20 +14224,26 @@ async function runClaim(options) {
|
|
|
14194
14224
|
issue_number: number,
|
|
14195
14225
|
body: message
|
|
14196
14226
|
});
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14227
|
+
try {
|
|
14228
|
+
const stateManager2 = getStateManager();
|
|
14229
|
+
stateManager2.addIssue({
|
|
14230
|
+
id: number,
|
|
14231
|
+
url: options.issueUrl,
|
|
14232
|
+
repo: `${owner}/${repo}`,
|
|
14233
|
+
number,
|
|
14234
|
+
title: "(claimed)",
|
|
14235
|
+
status: "claimed",
|
|
14236
|
+
labels: [],
|
|
14237
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14238
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14239
|
+
vetted: false
|
|
14240
|
+
});
|
|
14241
|
+
stateManager2.save();
|
|
14242
|
+
} catch (error) {
|
|
14243
|
+
console.error(
|
|
14244
|
+
`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`
|
|
14245
|
+
);
|
|
14246
|
+
}
|
|
14211
14247
|
return {
|
|
14212
14248
|
commentUrl: comment.html_url,
|
|
14213
14249
|
issueUrl: options.issueUrl
|
|
@@ -14320,6 +14356,13 @@ __export(setup_exports, {
|
|
|
14320
14356
|
runCheckSetup: () => runCheckSetup,
|
|
14321
14357
|
runSetup: () => runSetup
|
|
14322
14358
|
});
|
|
14359
|
+
function parsePositiveInt(value, settingName) {
|
|
14360
|
+
const parsed = Number(value);
|
|
14361
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
14362
|
+
throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
|
|
14363
|
+
}
|
|
14364
|
+
return parsed;
|
|
14365
|
+
}
|
|
14323
14366
|
async function runSetup(options) {
|
|
14324
14367
|
const stateManager2 = getStateManager();
|
|
14325
14368
|
const config = stateManager2.getState().config;
|
|
@@ -14335,18 +14378,24 @@ async function runSetup(options) {
|
|
|
14335
14378
|
stateManager2.updateConfig({ githubUsername: value });
|
|
14336
14379
|
results[key] = value;
|
|
14337
14380
|
break;
|
|
14338
|
-
case "maxActivePRs":
|
|
14339
|
-
|
|
14340
|
-
|
|
14381
|
+
case "maxActivePRs": {
|
|
14382
|
+
const maxPRs = parsePositiveInt(value, "maxActivePRs");
|
|
14383
|
+
stateManager2.updateConfig({ maxActivePRs: maxPRs });
|
|
14384
|
+
results[key] = String(maxPRs);
|
|
14341
14385
|
break;
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14386
|
+
}
|
|
14387
|
+
case "dormantDays": {
|
|
14388
|
+
const dormant = parsePositiveInt(value, "dormantDays");
|
|
14389
|
+
stateManager2.updateConfig({ dormantThresholdDays: dormant });
|
|
14390
|
+
results[key] = String(dormant);
|
|
14345
14391
|
break;
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14392
|
+
}
|
|
14393
|
+
case "approachingDays": {
|
|
14394
|
+
const approaching = parsePositiveInt(value, "approachingDays");
|
|
14395
|
+
stateManager2.updateConfig({ approachingDormantDays: approaching });
|
|
14396
|
+
results[key] = String(approaching);
|
|
14349
14397
|
break;
|
|
14398
|
+
}
|
|
14350
14399
|
case "languages":
|
|
14351
14400
|
stateManager2.updateConfig({ languages: value.split(",").map((l) => l.trim()) });
|
|
14352
14401
|
results[key] = value;
|
|
@@ -14369,9 +14418,12 @@ async function runSetup(options) {
|
|
|
14369
14418
|
}
|
|
14370
14419
|
break;
|
|
14371
14420
|
case "minStars": {
|
|
14372
|
-
const
|
|
14373
|
-
|
|
14374
|
-
|
|
14421
|
+
const stars = Number(value);
|
|
14422
|
+
if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
|
|
14423
|
+
throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
|
|
14424
|
+
}
|
|
14425
|
+
stateManager2.updateConfig({ minStars: stars });
|
|
14426
|
+
results[key] = String(stars);
|
|
14375
14427
|
break;
|
|
14376
14428
|
}
|
|
14377
14429
|
case "includeDocIssues":
|
|
@@ -14495,6 +14547,7 @@ var init_setup = __esm({
|
|
|
14495
14547
|
"src/commands/setup.ts"() {
|
|
14496
14548
|
"use strict";
|
|
14497
14549
|
init_core();
|
|
14550
|
+
init_errors();
|
|
14498
14551
|
init_validation();
|
|
14499
14552
|
}
|
|
14500
14553
|
});
|
|
@@ -14514,8 +14567,14 @@ async function fetchDashboardData(token) {
|
|
|
14514
14567
|
console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
14515
14568
|
return [];
|
|
14516
14569
|
}),
|
|
14517
|
-
prMonitor.fetchUserMergedPRCounts()
|
|
14518
|
-
|
|
14570
|
+
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
14571
|
+
console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
14572
|
+
return emptyPRCountsResult();
|
|
14573
|
+
}),
|
|
14574
|
+
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
14575
|
+
console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
14576
|
+
return emptyPRCountsResult();
|
|
14577
|
+
}),
|
|
14519
14578
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
14520
14579
|
const msg = errorMessage(error);
|
|
14521
14580
|
if (msg.includes("No GitHub username configured")) {
|
|
@@ -14539,12 +14598,16 @@ async function fetchDashboardData(token) {
|
|
|
14539
14598
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
14540
14599
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
14541
14600
|
try {
|
|
14542
|
-
|
|
14601
|
+
if (Object.keys(monthlyCounts).length > 0) {
|
|
14602
|
+
stateManager2.setMonthlyMergedCounts(monthlyCounts);
|
|
14603
|
+
}
|
|
14543
14604
|
} catch (error) {
|
|
14544
14605
|
console.error("[DASHBOARD] Failed to store monthly merged counts:", errorMessage(error));
|
|
14545
14606
|
}
|
|
14546
14607
|
try {
|
|
14547
|
-
|
|
14608
|
+
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
14609
|
+
stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
14610
|
+
}
|
|
14548
14611
|
} catch (error) {
|
|
14549
14612
|
console.error("[DASHBOARD] Failed to store monthly closed counts:", errorMessage(error));
|
|
14550
14613
|
}
|
|
@@ -14559,7 +14622,9 @@ async function fetchDashboardData(token) {
|
|
|
14559
14622
|
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
14560
14623
|
}
|
|
14561
14624
|
}
|
|
14562
|
-
|
|
14625
|
+
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
14626
|
+
stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
14627
|
+
}
|
|
14563
14628
|
} catch (error) {
|
|
14564
14629
|
console.error("[DASHBOARD] Failed to store monthly opened counts:", errorMessage(error));
|
|
14565
14630
|
}
|
|
@@ -14570,7 +14635,11 @@ async function fetchDashboardData(token) {
|
|
|
14570
14635
|
digest.autoUnshelvedPRs = [];
|
|
14571
14636
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
14572
14637
|
stateManager2.setLastDigest(digest);
|
|
14573
|
-
|
|
14638
|
+
try {
|
|
14639
|
+
stateManager2.save();
|
|
14640
|
+
} catch (error) {
|
|
14641
|
+
console.error("Warning: Failed to save dashboard digest to state:", errorMessage(error));
|
|
14642
|
+
}
|
|
14574
14643
|
console.error(`Refreshed: ${prs.length} PRs fetched`);
|
|
14575
14644
|
return { digest, commentedIssues };
|
|
14576
14645
|
}
|
|
@@ -14602,6 +14671,7 @@ var init_dashboard_data = __esm({
|
|
|
14602
14671
|
"use strict";
|
|
14603
14672
|
init_core();
|
|
14604
14673
|
init_errors();
|
|
14674
|
+
init_github_stats();
|
|
14605
14675
|
init_daily();
|
|
14606
14676
|
}
|
|
14607
14677
|
});
|
|
@@ -16364,6 +16434,38 @@ async function startDashboardServer(options) {
|
|
|
16364
16434
|
sendError(res, 400, 'Missing or invalid "url" field');
|
|
16365
16435
|
return;
|
|
16366
16436
|
}
|
|
16437
|
+
try {
|
|
16438
|
+
validateUrl(body.url);
|
|
16439
|
+
validateGitHubUrl(body.url, PR_URL_PATTERN, "PR");
|
|
16440
|
+
} catch (err) {
|
|
16441
|
+
if (err instanceof ValidationError) {
|
|
16442
|
+
sendError(res, 400, err.message);
|
|
16443
|
+
} else {
|
|
16444
|
+
console.error("Unexpected error during URL validation:", err);
|
|
16445
|
+
sendError(res, 400, "Invalid URL");
|
|
16446
|
+
}
|
|
16447
|
+
return;
|
|
16448
|
+
}
|
|
16449
|
+
if (body.action === "snooze") {
|
|
16450
|
+
const days = body.days ?? 7;
|
|
16451
|
+
if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
|
|
16452
|
+
sendError(res, 400, "Snooze days must be a positive finite number");
|
|
16453
|
+
return;
|
|
16454
|
+
}
|
|
16455
|
+
if (body.reason !== void 0) {
|
|
16456
|
+
try {
|
|
16457
|
+
validateMessage(String(body.reason));
|
|
16458
|
+
} catch (err) {
|
|
16459
|
+
if (err instanceof ValidationError) {
|
|
16460
|
+
sendError(res, 400, err.message);
|
|
16461
|
+
} else {
|
|
16462
|
+
console.error("Unexpected error during message validation:", err);
|
|
16463
|
+
sendError(res, 400, "Invalid reason");
|
|
16464
|
+
}
|
|
16465
|
+
return;
|
|
16466
|
+
}
|
|
16467
|
+
}
|
|
16468
|
+
}
|
|
16367
16469
|
try {
|
|
16368
16470
|
switch (body.action) {
|
|
16369
16471
|
case "shelve":
|
|
@@ -16373,7 +16475,7 @@ async function startDashboardServer(options) {
|
|
|
16373
16475
|
stateManager2.unshelvePR(body.url);
|
|
16374
16476
|
break;
|
|
16375
16477
|
case "snooze":
|
|
16376
|
-
stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days
|
|
16478
|
+
stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days ?? 7);
|
|
16377
16479
|
break;
|
|
16378
16480
|
case "unsnooze":
|
|
16379
16481
|
stateManager2.unsnoozePR(body.url);
|
|
@@ -16527,6 +16629,7 @@ var init_dashboard_server = __esm({
|
|
|
16527
16629
|
path5 = __toESM(require("path"), 1);
|
|
16528
16630
|
init_core();
|
|
16529
16631
|
init_errors();
|
|
16632
|
+
init_validation();
|
|
16530
16633
|
init_dashboard_data();
|
|
16531
16634
|
init_dashboard_templates();
|
|
16532
16635
|
VALID_ACTIONS = /* @__PURE__ */ new Set(["shelve", "unshelve", "snooze", "unsnooze"]);
|
|
@@ -16580,6 +16683,8 @@ async function runDashboard(options) {
|
|
|
16580
16683
|
digest = stateManager2.getState().lastDigest;
|
|
16581
16684
|
}
|
|
16582
16685
|
} else {
|
|
16686
|
+
console.error("Warning: No GitHub token found. Using cached data (may be stale).");
|
|
16687
|
+
console.error("Set GITHUB_TOKEN or run `gh auth login` for fresh data.");
|
|
16583
16688
|
digest = stateManager2.getState().lastDigest;
|
|
16584
16689
|
}
|
|
16585
16690
|
if (!digest) {
|
|
@@ -16618,7 +16723,6 @@ async function runDashboard(options) {
|
|
|
16618
16723
|
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses);
|
|
16619
16724
|
const dashboardPath = getDashboardPath();
|
|
16620
16725
|
fs6.writeFileSync(dashboardPath, html, { mode: 420 });
|
|
16621
|
-
fs6.chmodSync(dashboardPath, 420);
|
|
16622
16726
|
if (options.offline) {
|
|
16623
16727
|
const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
|
|
16624
16728
|
console.log(`
|
|
@@ -16654,7 +16758,6 @@ function writeDashboardFromState() {
|
|
|
16654
16758
|
const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
|
|
16655
16759
|
const dashboardPath = getDashboardPath();
|
|
16656
16760
|
fs6.writeFileSync(dashboardPath, html, { mode: 420 });
|
|
16657
|
-
fs6.chmodSync(dashboardPath, 420);
|
|
16658
16761
|
return dashboardPath;
|
|
16659
16762
|
}
|
|
16660
16763
|
function resolveAssetsDir() {
|
|
@@ -17043,7 +17146,7 @@ async function runLocalRepos(options) {
|
|
|
17043
17146
|
stateManager2.save();
|
|
17044
17147
|
} catch (error) {
|
|
17045
17148
|
const msg = errorMessage(error);
|
|
17046
|
-
|
|
17149
|
+
console.error(`Warning: Failed to cache scan results: ${msg}`);
|
|
17047
17150
|
}
|
|
17048
17151
|
return {
|
|
17049
17152
|
repos,
|
|
@@ -17424,12 +17527,20 @@ Last Run: ${data.lastRunAt || "Never"}`);
|
|
|
17424
17527
|
program2.command("search [count]").description("Search for new issues to work on").option("--json", "Output as JSON").action(async (count, options) => {
|
|
17425
17528
|
try {
|
|
17426
17529
|
const { runSearch: runSearch2 } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
17530
|
+
let maxResults = 5;
|
|
17531
|
+
if (count !== void 0) {
|
|
17532
|
+
const parsed = Number(count);
|
|
17533
|
+
if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
|
|
17534
|
+
throw new Error(`Invalid count "${count}". Must be a positive integer.`);
|
|
17535
|
+
}
|
|
17536
|
+
maxResults = parsed;
|
|
17537
|
+
}
|
|
17427
17538
|
if (!options.json) {
|
|
17428
17539
|
console.log(`
|
|
17429
|
-
Searching for issues (max ${
|
|
17540
|
+
Searching for issues (max ${maxResults})...
|
|
17430
17541
|
`);
|
|
17431
17542
|
}
|
|
17432
|
-
const data = await runSearch2({ maxResults
|
|
17543
|
+
const data = await runSearch2({ maxResults });
|
|
17433
17544
|
if (options.json) {
|
|
17434
17545
|
outputJson(data);
|
|
17435
17546
|
} else {
|
|
@@ -17728,17 +17839,25 @@ program2.command("checkSetup").description("Check if setup is complete").option(
|
|
|
17728
17839
|
});
|
|
17729
17840
|
var dashboardCmd = program2.command("dashboard").description("Dashboard commands");
|
|
17730
17841
|
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) => {
|
|
17731
|
-
|
|
17732
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17842
|
+
try {
|
|
17843
|
+
const port = parseInt(options.port, 10);
|
|
17844
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
17845
|
+
console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
|
|
17846
|
+
process.exit(1);
|
|
17847
|
+
}
|
|
17848
|
+
const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17849
|
+
await serveDashboard2({ port, open: options.open });
|
|
17850
|
+
} catch (err) {
|
|
17851
|
+
handleCommandError(err);
|
|
17735
17852
|
}
|
|
17736
|
-
const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17737
|
-
await serveDashboard2({ port, open: options.open });
|
|
17738
17853
|
});
|
|
17739
17854
|
dashboardCmd.option("--open", "Open in browser").option("--json", "Output as JSON").option("--offline", "Use cached data only (no GitHub API calls)").action(async (options) => {
|
|
17740
|
-
|
|
17741
|
-
|
|
17855
|
+
try {
|
|
17856
|
+
const { runDashboard: runDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
|
|
17857
|
+
await runDashboard2({ open: options.open, json: options.json, offline: options.offline });
|
|
17858
|
+
} catch (err) {
|
|
17859
|
+
handleCommandError(err, options.json);
|
|
17860
|
+
}
|
|
17742
17861
|
});
|
|
17743
17862
|
program2.command("parse-issue-list <path>").description("Parse a markdown issue list into structured JSON").option("--json", "Output as JSON").action(async (filePath, options) => {
|
|
17744
17863
|
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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
10
|
import { errorMessage } from '../core/errors.js';
|
|
11
|
+
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
11
12
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
|
|
12
13
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
13
14
|
// can continue importing from './daily.js' without changes.
|
|
@@ -28,11 +29,17 @@ async function fetchPRData(prMonitor, token) {
|
|
|
28
29
|
console.error(`Warning: ${failures.length} PR fetch(es) failed`);
|
|
29
30
|
}
|
|
30
31
|
// Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
|
|
31
|
-
//
|
|
32
|
+
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
32
33
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
33
34
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
34
|
-
prMonitor.fetchUserMergedPRCounts()
|
|
35
|
-
|
|
35
|
+
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
36
|
+
console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
37
|
+
return emptyPRCountsResult();
|
|
38
|
+
}),
|
|
39
|
+
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
40
|
+
console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
41
|
+
return emptyPRCountsResult();
|
|
42
|
+
}),
|
|
36
43
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
37
44
|
console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
38
45
|
return [];
|
|
@@ -195,15 +202,21 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
195
202
|
*/
|
|
196
203
|
function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
|
|
197
204
|
const stateManager = getStateManager();
|
|
198
|
-
// Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state)
|
|
205
|
+
// Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state).
|
|
206
|
+
// Guard: skip overwriting when the data is empty to avoid wiping existing chart data on transient API failures.
|
|
207
|
+
// An empty object means the fetch failed and fell back to emptyPRCountsResult(), so we preserve previous state.
|
|
199
208
|
try {
|
|
200
|
-
|
|
209
|
+
if (Object.keys(monthlyCounts).length > 0) {
|
|
210
|
+
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
211
|
+
}
|
|
201
212
|
}
|
|
202
213
|
catch (error) {
|
|
203
214
|
console.error('[DAILY] Failed to store monthly merged counts:', errorMessage(error));
|
|
204
215
|
}
|
|
205
216
|
try {
|
|
206
|
-
|
|
217
|
+
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
218
|
+
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
219
|
+
}
|
|
207
220
|
}
|
|
208
221
|
catch (error) {
|
|
209
222
|
console.error('[DAILY] Failed to store monthly closed counts:', errorMessage(error));
|
|
@@ -221,7 +234,9 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
221
234
|
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
|
-
|
|
237
|
+
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
238
|
+
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
239
|
+
}
|
|
225
240
|
}
|
|
226
241
|
catch (error) {
|
|
227
242
|
console.error('[DAILY] Failed to compute/store monthly opened counts:', errorMessage(error));
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
|
|
7
7
|
import { errorMessage } from '../core/errors.js';
|
|
8
|
+
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
8
9
|
import { toShelvedPRRef } from './daily.js';
|
|
9
10
|
/**
|
|
10
11
|
* Fetch fresh dashboard data from GitHub.
|
|
@@ -25,8 +26,14 @@ export async function fetchDashboardData(token) {
|
|
|
25
26
|
console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
26
27
|
return [];
|
|
27
28
|
}),
|
|
28
|
-
prMonitor.fetchUserMergedPRCounts()
|
|
29
|
-
|
|
29
|
+
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
30
|
+
console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
31
|
+
return emptyPRCountsResult();
|
|
32
|
+
}),
|
|
33
|
+
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
34
|
+
console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
35
|
+
return emptyPRCountsResult();
|
|
36
|
+
}),
|
|
30
37
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
31
38
|
const msg = errorMessage(error);
|
|
32
39
|
if (msg.includes('No GitHub username configured')) {
|
|
@@ -51,14 +58,19 @@ export async function fetchDashboardData(token) {
|
|
|
51
58
|
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
52
59
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
53
60
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
61
|
+
// Guard: skip overwriting when data is empty to avoid wiping chart data on transient API failures.
|
|
54
62
|
try {
|
|
55
|
-
|
|
63
|
+
if (Object.keys(monthlyCounts).length > 0) {
|
|
64
|
+
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
catch (error) {
|
|
58
68
|
console.error('[DASHBOARD] Failed to store monthly merged counts:', errorMessage(error));
|
|
59
69
|
}
|
|
60
70
|
try {
|
|
61
|
-
|
|
71
|
+
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
72
|
+
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
catch (error) {
|
|
64
76
|
console.error('[DASHBOARD] Failed to store monthly closed counts:', errorMessage(error));
|
|
@@ -74,7 +86,9 @@ export async function fetchDashboardData(token) {
|
|
|
74
86
|
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
90
|
+
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
catch (error) {
|
|
80
94
|
console.error('[DASHBOARD] Failed to store monthly opened counts:', errorMessage(error));
|
|
@@ -88,7 +102,12 @@ export async function fetchDashboardData(token) {
|
|
|
88
102
|
digest.autoUnshelvedPRs = [];
|
|
89
103
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
90
104
|
stateManager.setLastDigest(digest);
|
|
91
|
-
|
|
105
|
+
try {
|
|
106
|
+
stateManager.save();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('Warning: Failed to save dashboard digest to state:', errorMessage(error));
|
|
110
|
+
}
|
|
92
111
|
console.error(`Refreshed: ${prs.length} PRs fetched`);
|
|
93
112
|
return { digest, commentedIssues };
|
|
94
113
|
}
|
|
@@ -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) || [];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
6
|
import { ClosedPR, MergedPR } from './types.js';
|
|
7
|
-
/** TTL for cached PR count results (
|
|
7
|
+
/** TTL for cached PR count results (24 hours — these stats change slowly). */
|
|
8
8
|
export declare const PR_COUNTS_CACHE_TTL_MS: number;
|
|
9
9
|
/** Return type shared by both merged and closed PR count functions. */
|
|
10
10
|
export interface PRCountsResult<R> {
|
|
@@ -13,6 +13,8 @@ export interface PRCountsResult<R> {
|
|
|
13
13
|
monthlyOpenedCounts: Record<string, number>;
|
|
14
14
|
dailyActivityCounts: Record<string, number>;
|
|
15
15
|
}
|
|
16
|
+
/** Returns an empty PRCountsResult. Used as the fallback when stats fetches fail or username is empty. */
|
|
17
|
+
export declare function emptyPRCountsResult<R>(): PRCountsResult<R>;
|
|
16
18
|
/**
|
|
17
19
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
18
20
|
* Also builds a monthly histogram of all merges for the contribution timeline.
|
|
@@ -6,8 +6,18 @@ import { extractOwnerRepo, parseGitHubUrl, isOwnRepo } from './utils.js';
|
|
|
6
6
|
import { debug, warn } from './logger.js';
|
|
7
7
|
import { getHttpCache } from './http-cache.js';
|
|
8
8
|
const MODULE = 'github-stats';
|
|
9
|
-
/** TTL for cached PR count results (
|
|
10
|
-
export const PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
/** TTL for cached PR count results (24 hours — these stats change slowly). */
|
|
10
|
+
export const PR_COUNTS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of pages to fetch during paginated PR count searches.
|
|
13
|
+
* GitHub's Search API becomes unreliable (500s, ECONNRESET) at deep pages (4-5+).
|
|
14
|
+
* 3 pages × 100 per_page = 300 results, which covers the vast majority of users.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_PAGINATION_PAGES = 3;
|
|
17
|
+
/** Returns an empty PRCountsResult. Used as the fallback when stats fetches fail or username is empty. */
|
|
18
|
+
export function emptyPRCountsResult() {
|
|
19
|
+
return { repos: new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
|
|
20
|
+
}
|
|
11
21
|
/** Type guard for deserialized cache data — prevents crashes on corrupt/stale cache. */
|
|
12
22
|
function isCachedPRCounts(v) {
|
|
13
23
|
if (typeof v !== 'object' || v === null)
|
|
@@ -31,7 +41,7 @@ function isCachedPRCounts(v) {
|
|
|
31
41
|
*/
|
|
32
42
|
async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
|
|
33
43
|
if (!githubUsername) {
|
|
34
|
-
return
|
|
44
|
+
return emptyPRCountsResult();
|
|
35
45
|
}
|
|
36
46
|
// Check for a fresh cached result (avoids 10-20 paginated API calls)
|
|
37
47
|
const cache = getHttpCache();
|
|
@@ -53,6 +63,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
53
63
|
const dailyActivityCounts = {};
|
|
54
64
|
let page = 1;
|
|
55
65
|
let fetched = 0;
|
|
66
|
+
let totalCount;
|
|
56
67
|
while (true) {
|
|
57
68
|
const { data } = await octokit.search.issuesAndPullRequests({
|
|
58
69
|
q: `is:pr ${query} author:${githubUsername}`,
|
|
@@ -61,6 +72,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
61
72
|
per_page: 100,
|
|
62
73
|
page,
|
|
63
74
|
});
|
|
75
|
+
totalCount = data.total_count;
|
|
64
76
|
for (const item of data.items) {
|
|
65
77
|
const parsed = extractOwnerRepo(item.html_url);
|
|
66
78
|
if (!parsed) {
|
|
@@ -95,14 +107,22 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
fetched += data.items.length;
|
|
98
|
-
// Stop if we've fetched all results
|
|
99
|
-
|
|
110
|
+
// Stop if we've fetched all results, hit the API limit (1000), or reached max pages
|
|
111
|
+
// GitHub Search API returns 500/ECONNRESET on deep pagination (page 4-5+),
|
|
112
|
+
// so we cap at MAX_PAGINATION_PAGES to avoid taking down the entire startup flow.
|
|
113
|
+
if (fetched >= data.total_count || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
100
114
|
break;
|
|
101
115
|
}
|
|
102
116
|
page++;
|
|
103
117
|
}
|
|
118
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
119
|
+
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${label} PRs. Stats may be incomplete for prolific contributors.`);
|
|
120
|
+
}
|
|
104
121
|
debug(MODULE, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
|
|
105
|
-
// Cache the aggregated result (Map → entries array for JSON serialization)
|
|
122
|
+
// Cache the aggregated result (Map → entries array for JSON serialization).
|
|
123
|
+
// Note: truncated results (from pagination cap) are cached with the same TTL. This is an
|
|
124
|
+
// accepted tradeoff — prolific contributors (300+ PRs) may see slightly incomplete stats
|
|
125
|
+
// for up to 24 hours, but these are cosmetic and self-heal on the next cache expiry.
|
|
106
126
|
cache.set(cacheKey, '', {
|
|
107
127
|
reposEntries: Array.from(repos.entries()),
|
|
108
128
|
monthlyCounts,
|