@oss-autopilot/core 0.42.0 → 0.42.2
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 +1026 -1018
- package/dist/cli.js +18 -30
- package/dist/commands/check-integration.js +5 -4
- package/dist/commands/comments.js +24 -24
- package/dist/commands/daily.d.ts +0 -1
- package/dist/commands/daily.js +18 -16
- package/dist/commands/dashboard-components.d.ts +33 -0
- package/dist/commands/dashboard-components.js +57 -0
- package/dist/commands/dashboard-data.js +7 -6
- package/dist/commands/dashboard-formatters.d.ts +20 -0
- package/dist/commands/dashboard-formatters.js +33 -0
- package/dist/commands/dashboard-scripts.d.ts +7 -0
- package/dist/commands/dashboard-scripts.js +281 -0
- package/dist/commands/dashboard-server.js +3 -2
- package/dist/commands/dashboard-styles.d.ts +5 -0
- package/dist/commands/dashboard-styles.js +765 -0
- package/dist/commands/dashboard-templates.d.ts +6 -18
- package/dist/commands/dashboard-templates.js +30 -1134
- package/dist/commands/dashboard.js +2 -1
- package/dist/commands/dismiss.d.ts +6 -6
- package/dist/commands/dismiss.js +13 -13
- package/dist/commands/local-repos.js +2 -1
- package/dist/commands/parse-list.js +2 -1
- package/dist/commands/startup.js +6 -16
- package/dist/commands/validation.d.ts +3 -1
- package/dist/commands/validation.js +12 -6
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/github-stats.d.ts +14 -21
- package/dist/core/github-stats.js +84 -138
- package/dist/core/http-cache.d.ts +6 -0
- package/dist/core/http-cache.js +16 -4
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +3 -2
- package/dist/core/issue-conversation.js +4 -4
- package/dist/core/issue-discovery.d.ts +5 -0
- package/dist/core/issue-discovery.js +70 -93
- package/dist/core/issue-vetting.js +17 -17
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/pr-monitor.d.ts +6 -20
- package/dist/core/pr-monitor.js +16 -52
- package/dist/core/review-analysis.js +8 -6
- package/dist/core/state.js +4 -5
- package/dist/core/test-utils.d.ts +14 -0
- package/dist/core/test-utils.js +125 -0
- package/dist/core/utils.d.ts +11 -0
- package/dist/core/utils.js +21 -0
- package/dist/formatters/json.d.ts +0 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* async token fetch to avoid blocking the event loop on `gh auth token`.
|
|
11
11
|
*/
|
|
12
12
|
import { Command } from 'commander';
|
|
13
|
-
import { getGitHubTokenAsync, enableDebug, debug, formatRelativeTime } from './core/index.js';
|
|
13
|
+
import { getGitHubTokenAsync, enableDebug, debug, formatRelativeTime, getCLIVersion } from './core/index.js';
|
|
14
|
+
import { errorMessage } from './core/errors.js';
|
|
14
15
|
import { outputJson, outputJsonError } from './formatters/json.js';
|
|
15
16
|
/** Print local repos in human-readable format */
|
|
16
17
|
function printRepos(repos) {
|
|
@@ -23,7 +24,7 @@ function printRepos(repos) {
|
|
|
23
24
|
}
|
|
24
25
|
/** Shared error handler for CLI action catch blocks. */
|
|
25
26
|
function handleCommandError(err, json) {
|
|
26
|
-
const msg =
|
|
27
|
+
const msg = errorMessage(err);
|
|
27
28
|
if (json) {
|
|
28
29
|
outputJsonError(msg);
|
|
29
30
|
}
|
|
@@ -32,20 +33,7 @@ function handleCommandError(err, json) {
|
|
|
32
33
|
}
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
35
|
-
const VERSION = (
|
|
36
|
-
try {
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
-
const fs = require('fs');
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
-
const path = require('path');
|
|
41
|
-
const pkgPath = path.join(path.dirname(process.argv[1]), '..', 'package.json');
|
|
42
|
-
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
43
|
-
}
|
|
44
|
-
catch (_err) {
|
|
45
|
-
// package.json may not be readable in all bundle/install configurations — fall back to safe default
|
|
46
|
-
return '0.0.0';
|
|
47
|
-
}
|
|
48
|
-
})();
|
|
36
|
+
const VERSION = getCLIVersion();
|
|
49
37
|
// Commands that skip the preAction GitHub token check.
|
|
50
38
|
// startup handles auth internally (returns authError in JSON instead of process.exit).
|
|
51
39
|
const LOCAL_ONLY_COMMANDS = [
|
|
@@ -727,23 +715,23 @@ program
|
|
|
727
715
|
});
|
|
728
716
|
// Dismiss command
|
|
729
717
|
program
|
|
730
|
-
.command('dismiss <
|
|
731
|
-
.description('Dismiss issue
|
|
718
|
+
.command('dismiss <url>')
|
|
719
|
+
.description('Dismiss notifications for an issue or PR (resurfaces on new activity)')
|
|
732
720
|
.option('--json', 'Output as JSON')
|
|
733
|
-
.action(async (
|
|
721
|
+
.action(async (url, options) => {
|
|
734
722
|
try {
|
|
735
723
|
const { runDismiss } = await import('./commands/dismiss.js');
|
|
736
|
-
const data = await runDismiss({
|
|
724
|
+
const data = await runDismiss({ url });
|
|
737
725
|
if (options.json) {
|
|
738
726
|
outputJson(data);
|
|
739
727
|
}
|
|
740
728
|
else if (data.dismissed) {
|
|
741
|
-
console.log(`Dismissed: ${
|
|
742
|
-
console.log('
|
|
729
|
+
console.log(`Dismissed: ${url}`);
|
|
730
|
+
console.log('Notifications are now muted.');
|
|
743
731
|
console.log('New responses after this point will resurface automatically.');
|
|
744
732
|
}
|
|
745
733
|
else {
|
|
746
|
-
console.log('
|
|
734
|
+
console.log('Already dismissed.');
|
|
747
735
|
}
|
|
748
736
|
}
|
|
749
737
|
catch (err) {
|
|
@@ -752,22 +740,22 @@ program
|
|
|
752
740
|
});
|
|
753
741
|
// Undismiss command
|
|
754
742
|
program
|
|
755
|
-
.command('undismiss <
|
|
756
|
-
.description('Undismiss an issue (re-enable
|
|
743
|
+
.command('undismiss <url>')
|
|
744
|
+
.description('Undismiss an issue or PR (re-enable notifications)')
|
|
757
745
|
.option('--json', 'Output as JSON')
|
|
758
|
-
.action(async (
|
|
746
|
+
.action(async (url, options) => {
|
|
759
747
|
try {
|
|
760
748
|
const { runUndismiss } = await import('./commands/dismiss.js');
|
|
761
|
-
const data = await runUndismiss({
|
|
749
|
+
const data = await runUndismiss({ url });
|
|
762
750
|
if (options.json) {
|
|
763
751
|
outputJson(data);
|
|
764
752
|
}
|
|
765
753
|
else if (data.undismissed) {
|
|
766
|
-
console.log(`Undismissed: ${
|
|
767
|
-
console.log('
|
|
754
|
+
console.log(`Undismissed: ${url}`);
|
|
755
|
+
console.log('Notifications are active again.');
|
|
768
756
|
}
|
|
769
757
|
else {
|
|
770
|
-
console.log('
|
|
758
|
+
console.log('Was not dismissed.');
|
|
771
759
|
}
|
|
772
760
|
}
|
|
773
761
|
catch (err) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import { debug } from '../core/index.js';
|
|
8
|
+
import { errorMessage } from '../core/errors.js';
|
|
8
9
|
/** File extensions we consider "code" that should be imported/referenced */
|
|
9
10
|
const CODE_EXTENSIONS = new Set([
|
|
10
11
|
'.ts',
|
|
@@ -76,7 +77,7 @@ export async function runCheckIntegration(options) {
|
|
|
76
77
|
newFiles = output ? output.split('\n').filter(Boolean) : [];
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
|
-
const msg =
|
|
80
|
+
const msg = errorMessage(error);
|
|
80
81
|
throw new Error(`Failed to run git diff: ${msg}`, { cause: error });
|
|
81
82
|
}
|
|
82
83
|
// Filter to code files, excluding tests, configs, etc.
|
|
@@ -133,9 +134,9 @@ export async function runCheckIntegration(options) {
|
|
|
133
134
|
}
|
|
134
135
|
catch (error) {
|
|
135
136
|
// git grep exit code 1 = no matches (expected), exit code 2+ = real error
|
|
136
|
-
const exitCode = error && typeof error === 'object' && 'status' in error ? error.status :
|
|
137
|
-
if (exitCode !==
|
|
138
|
-
const msg =
|
|
137
|
+
const exitCode = error && typeof error === 'object' && 'status' in error ? error.status : undefined;
|
|
138
|
+
if (exitCode !== undefined && exitCode !== 1) {
|
|
139
|
+
const msg = errorMessage(error);
|
|
139
140
|
debug('check-integration', `git grep failed for "${pattern}": ${msg}`);
|
|
140
141
|
}
|
|
141
142
|
}
|
|
@@ -18,30 +18,30 @@ export async function runComments(options) {
|
|
|
18
18
|
const { owner, repo, number: pull_number } = parsed;
|
|
19
19
|
// Get PR details
|
|
20
20
|
const { data: pr } = await octokit.pulls.get({ owner, repo, pull_number });
|
|
21
|
-
//
|
|
22
|
-
const reviewComments = await
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
// Fetch review comments, issue comments, and reviews in parallel
|
|
22
|
+
const [reviewComments, issueComments, reviews] = await Promise.all([
|
|
23
|
+
paginateAll((page) => octokit.pulls.listReviewComments({
|
|
24
|
+
owner,
|
|
25
|
+
repo,
|
|
26
|
+
pull_number,
|
|
27
|
+
per_page: 100,
|
|
28
|
+
page,
|
|
29
|
+
})),
|
|
30
|
+
paginateAll((page) => octokit.issues.listComments({
|
|
31
|
+
owner,
|
|
32
|
+
repo,
|
|
33
|
+
issue_number: pull_number,
|
|
34
|
+
per_page: 100,
|
|
35
|
+
page,
|
|
36
|
+
})),
|
|
37
|
+
paginateAll((page) => octokit.pulls.listReviews({
|
|
38
|
+
owner,
|
|
39
|
+
repo,
|
|
40
|
+
pull_number,
|
|
41
|
+
per_page: 100,
|
|
42
|
+
page,
|
|
43
|
+
})),
|
|
44
|
+
]);
|
|
45
45
|
// Filter out own comments, optionally show bots
|
|
46
46
|
const username = stateManager.getState().config.githubUsername;
|
|
47
47
|
const filterComment = (c) => {
|
package/dist/commands/daily.d.ts
CHANGED
package/dist/commands/daily.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
|
+
import { errorMessage } from '../core/errors.js';
|
|
10
11
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
|
|
11
12
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
12
13
|
// can continue importing from './daily.js' without changes.
|
|
@@ -33,15 +34,15 @@ async function fetchPRData(prMonitor, token) {
|
|
|
33
34
|
prMonitor.fetchUserMergedPRCounts(),
|
|
34
35
|
prMonitor.fetchUserClosedPRCounts(),
|
|
35
36
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
36
|
-
console.error(`Warning: Failed to fetch recently closed PRs: ${err
|
|
37
|
+
console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
37
38
|
return [];
|
|
38
39
|
}),
|
|
39
40
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
40
|
-
console.error(`Warning: Failed to fetch recently merged PRs: ${err
|
|
41
|
+
console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
41
42
|
return [];
|
|
42
43
|
}),
|
|
43
44
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
44
|
-
const msg =
|
|
45
|
+
const msg = errorMessage(error);
|
|
45
46
|
if (msg.includes('No GitHub username configured')) {
|
|
46
47
|
console.error(`[DAILY] Issue conversation tracking requires setup: ${msg}`);
|
|
47
48
|
}
|
|
@@ -103,7 +104,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
103
104
|
}
|
|
104
105
|
catch (error) {
|
|
105
106
|
mergedCountFailures++;
|
|
106
|
-
console.error(`[DAILY] Failed to update merged count for ${repo}:`, error
|
|
107
|
+
console.error(`[DAILY] Failed to update merged count for ${repo}:`, errorMessage(error));
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
@@ -123,7 +124,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
123
124
|
}
|
|
124
125
|
catch (error) {
|
|
125
126
|
closedCountFailures++;
|
|
126
|
-
console.error(`[DAILY] Failed to update closed count for ${repo}:`, error
|
|
127
|
+
console.error(`[DAILY] Failed to update closed count for ${repo}:`, errorMessage(error));
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
|
|
@@ -142,7 +143,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
142
143
|
}
|
|
143
144
|
catch (error) {
|
|
144
145
|
signalUpdateFailures++;
|
|
145
|
-
console.error(`[DAILY] Failed to update signals for ${repo}:`, error
|
|
146
|
+
console.error(`[DAILY] Failed to update signals for ${repo}:`, errorMessage(error));
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
|
|
@@ -155,7 +156,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
155
156
|
starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
|
|
156
157
|
}
|
|
157
158
|
catch (error) {
|
|
158
|
-
console.error('[DAILY] Failed to fetch repo star counts:', error
|
|
159
|
+
console.error('[DAILY] Failed to fetch repo star counts:', errorMessage(error));
|
|
159
160
|
console.error('[DAILY] Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
|
|
160
161
|
starCounts = new Map();
|
|
161
162
|
}
|
|
@@ -166,7 +167,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
166
167
|
}
|
|
167
168
|
catch (error) {
|
|
168
169
|
starUpdateFailures++;
|
|
169
|
-
console.error(`[DAILY] Failed to update star count for ${repo}:`, error
|
|
170
|
+
console.error(`[DAILY] Failed to update star count for ${repo}:`, errorMessage(error));
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
|
|
@@ -180,7 +181,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
180
181
|
}
|
|
181
182
|
catch (error) {
|
|
182
183
|
trustSyncFailures++;
|
|
183
|
-
console.error(`[DAILY] Failed to sync trusted project ${repo}:`, error
|
|
184
|
+
console.error(`[DAILY] Failed to sync trusted project ${repo}:`, errorMessage(error));
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
@@ -199,13 +200,13 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
199
200
|
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
200
201
|
}
|
|
201
202
|
catch (error) {
|
|
202
|
-
console.error('[DAILY] Failed to store monthly merged counts:', error
|
|
203
|
+
console.error('[DAILY] Failed to store monthly merged counts:', errorMessage(error));
|
|
203
204
|
}
|
|
204
205
|
try {
|
|
205
206
|
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
206
207
|
}
|
|
207
208
|
catch (error) {
|
|
208
|
-
console.error('[DAILY] Failed to store monthly closed counts:', error
|
|
209
|
+
console.error('[DAILY] Failed to store monthly closed counts:', errorMessage(error));
|
|
209
210
|
}
|
|
210
211
|
try {
|
|
211
212
|
// Build combined monthly opened counts from merged + closed + currently-open PRs
|
|
@@ -223,7 +224,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
223
224
|
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
224
225
|
}
|
|
225
226
|
catch (error) {
|
|
226
|
-
console.error('[DAILY] Failed to compute/store monthly opened counts:', error
|
|
227
|
+
console.error('[DAILY] Failed to compute/store monthly opened counts:', errorMessage(error));
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
/**
|
|
@@ -246,7 +247,7 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
catch (error) {
|
|
249
|
-
console.error('[DAILY] Failed to expire/persist snoozes:', error
|
|
250
|
+
console.error('[DAILY] Failed to expire/persist snoozes:', errorMessage(error));
|
|
250
251
|
}
|
|
251
252
|
// Partition PRs into active vs shelved, auto-unshelving when maintainers engage
|
|
252
253
|
const shelvedPRs = [];
|
|
@@ -326,14 +327,16 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
326
327
|
const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
|
|
327
328
|
const summary = formatSummary(digest, capacity, issueResponses);
|
|
328
329
|
const snoozedUrls = new Set(Object.keys(stateManager.getState().config.snoozedPRs ?? {}).filter((url) => stateManager.isSnoozed(url)));
|
|
329
|
-
|
|
330
|
+
// Filter dismissed PR URLs from actionable issues (#416)
|
|
331
|
+
const dismissedUrls = new Set(Object.keys(stateManager.getState().config.dismissedIssues ?? {}));
|
|
332
|
+
const nonDismissedPRs = activePRs.filter((pr) => !dismissedUrls.has(pr.url));
|
|
333
|
+
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
330
334
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
331
335
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
332
336
|
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
|
|
333
337
|
const repoGroups = groupPRsByRepo(activePRs);
|
|
334
338
|
return {
|
|
335
339
|
digest,
|
|
336
|
-
updates: [],
|
|
337
340
|
capacity,
|
|
338
341
|
summary,
|
|
339
342
|
briefSummary,
|
|
@@ -355,7 +358,6 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
355
358
|
function toDailyOutput(result) {
|
|
356
359
|
return {
|
|
357
360
|
digest: deduplicateDigest(result.digest),
|
|
358
|
-
updates: result.updates,
|
|
359
361
|
capacity: result.capacity,
|
|
360
362
|
summary: result.summary,
|
|
361
363
|
briefSummary: result.briefSummary,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable HTML component generators for the dashboard:
|
|
3
|
+
* SVG icons, truncateTitle, renderHealthItems, titleMeta.
|
|
4
|
+
*/
|
|
5
|
+
import type { FetchedPR } from '../core/types.js';
|
|
6
|
+
/** SVG path constants for health item icons. */
|
|
7
|
+
export declare const SVG_ICONS: {
|
|
8
|
+
comment: string;
|
|
9
|
+
edit: string;
|
|
10
|
+
xCircle: string;
|
|
11
|
+
conflict: string;
|
|
12
|
+
checklist: string;
|
|
13
|
+
file: string;
|
|
14
|
+
checkCircle: string;
|
|
15
|
+
clock: string;
|
|
16
|
+
lock: string;
|
|
17
|
+
infoCircle: string;
|
|
18
|
+
refresh: string;
|
|
19
|
+
box: string;
|
|
20
|
+
bell: string;
|
|
21
|
+
gitMerge: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function truncateTitle(title: string, max?: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Render health status items. labelFn output is automatically HTML-escaped.
|
|
26
|
+
* metaFn output is injected raw — callers must ensure metaFn returns safe HTML
|
|
27
|
+
* (use escapeHtml for any user-controlled content within metaFn).
|
|
28
|
+
*
|
|
29
|
+
* Accepts both full FetchedPR objects and lightweight ShelvedPRRef objects.
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderHealthItems<T extends Pick<FetchedPR, 'repo' | 'title' | 'url' | 'number'>>(prs: T[], cssClass: string, svgPaths: string, labelFn: string | ((pr: T) => string), metaFn: (pr: T) => string): string;
|
|
32
|
+
/** Default meta: truncated PR title (works for both FetchedPR and ShelvedPRRef). */
|
|
33
|
+
export declare function titleMeta(pr: Pick<FetchedPR, 'title'>): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable HTML component generators for the dashboard:
|
|
3
|
+
* SVG icons, truncateTitle, renderHealthItems, titleMeta.
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from './dashboard-formatters.js';
|
|
6
|
+
/** SVG path constants for health item icons. */
|
|
7
|
+
export const SVG_ICONS = {
|
|
8
|
+
comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
|
9
|
+
edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
|
10
|
+
xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
|
|
11
|
+
conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
|
|
12
|
+
checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
|
13
|
+
file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
|
|
14
|
+
checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
|
15
|
+
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
16
|
+
lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
17
|
+
infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
|
|
18
|
+
refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
|
|
19
|
+
box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
|
20
|
+
bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
|
|
21
|
+
gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>',
|
|
22
|
+
};
|
|
23
|
+
export function truncateTitle(title, max = 50) {
|
|
24
|
+
const truncated = title.length <= max ? title : title.slice(0, max) + '...';
|
|
25
|
+
return escapeHtml(truncated);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render health status items. labelFn output is automatically HTML-escaped.
|
|
29
|
+
* metaFn output is injected raw — callers must ensure metaFn returns safe HTML
|
|
30
|
+
* (use escapeHtml for any user-controlled content within metaFn).
|
|
31
|
+
*
|
|
32
|
+
* Accepts both full FetchedPR objects and lightweight ShelvedPRRef objects.
|
|
33
|
+
*/
|
|
34
|
+
export function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
|
|
35
|
+
return prs
|
|
36
|
+
.map((pr) => {
|
|
37
|
+
const rawLabel = typeof labelFn === 'string' ? labelFn : labelFn(pr);
|
|
38
|
+
const label = escapeHtml(rawLabel);
|
|
39
|
+
return `
|
|
40
|
+
<div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
|
|
41
|
+
<div class="health-icon">
|
|
42
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
43
|
+
${svgPaths}
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="health-content">
|
|
47
|
+
<div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
|
|
48
|
+
<div class="health-meta">${metaFn(pr)}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>`;
|
|
51
|
+
})
|
|
52
|
+
.join('');
|
|
53
|
+
}
|
|
54
|
+
/** Default meta: truncated PR title (works for both FetchedPR and ShelvedPRRef). */
|
|
55
|
+
export function titleMeta(pr) {
|
|
56
|
+
return truncateTitle(pr.title);
|
|
57
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Separates data concerns from template generation and command orchestration.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
|
|
7
|
+
import { errorMessage } from '../core/errors.js';
|
|
7
8
|
import { toShelvedPRRef } from './daily.js';
|
|
8
9
|
/**
|
|
9
10
|
* Fetch fresh dashboard data from GitHub.
|
|
@@ -17,17 +18,17 @@ export async function fetchDashboardData(token) {
|
|
|
17
18
|
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
|
|
18
19
|
prMonitor.fetchUserOpenPRs(),
|
|
19
20
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
20
|
-
console.error(`Warning: Failed to fetch recently closed PRs: ${err
|
|
21
|
+
console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
21
22
|
return [];
|
|
22
23
|
}),
|
|
23
24
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
24
|
-
console.error(`Warning: Failed to fetch recently merged PRs: ${err
|
|
25
|
+
console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
25
26
|
return [];
|
|
26
27
|
}),
|
|
27
28
|
prMonitor.fetchUserMergedPRCounts(),
|
|
28
29
|
prMonitor.fetchUserClosedPRCounts(),
|
|
29
30
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
30
|
-
const msg =
|
|
31
|
+
const msg = errorMessage(error);
|
|
31
32
|
if (msg.includes('No GitHub username configured')) {
|
|
32
33
|
console.error(`[DASHBOARD] Issue conversation tracking requires setup: ${msg}`);
|
|
33
34
|
}
|
|
@@ -54,13 +55,13 @@ export async function fetchDashboardData(token) {
|
|
|
54
55
|
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
|
-
console.error('[DASHBOARD] Failed to store monthly merged counts:', error
|
|
58
|
+
console.error('[DASHBOARD] Failed to store monthly merged counts:', errorMessage(error));
|
|
58
59
|
}
|
|
59
60
|
try {
|
|
60
61
|
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
61
62
|
}
|
|
62
63
|
catch (error) {
|
|
63
|
-
console.error('[DASHBOARD] Failed to store monthly closed counts:', error
|
|
64
|
+
console.error('[DASHBOARD] Failed to store monthly closed counts:', errorMessage(error));
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
67
|
const combinedOpenedCounts = { ...openedFromMerged };
|
|
@@ -76,7 +77,7 @@ export async function fetchDashboardData(token) {
|
|
|
76
77
|
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
|
-
console.error('[DASHBOARD] Failed to store monthly opened counts:', error
|
|
80
|
+
console.error('[DASHBOARD] Failed to store monthly opened counts:', errorMessage(error));
|
|
80
81
|
}
|
|
81
82
|
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
82
83
|
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard data formatting helpers: escapeHtml, DashboardStats, and stats builder.
|
|
3
|
+
*/
|
|
4
|
+
import type { DailyDigest, AgentState } from '../core/types.js';
|
|
5
|
+
export interface DashboardStats {
|
|
6
|
+
activePRs: number;
|
|
7
|
+
shelvedPRs: number;
|
|
8
|
+
mergedPRs: number;
|
|
9
|
+
closedPRs: number;
|
|
10
|
+
mergeRate: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Escape HTML special characters to prevent XSS when interpolating
|
|
14
|
+
* user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
|
|
15
|
+
* Note: This escapes HTML entity characters only. It does not sanitize URL schemes
|
|
16
|
+
* (e.g., javascript:) — callers placing values in href attributes should validate
|
|
17
|
+
* the URL scheme if the source is untrusted. GitHub API URLs are trusted.
|
|
18
|
+
*/
|
|
19
|
+
export declare function escapeHtml(text: string): string;
|
|
20
|
+
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard data formatting helpers: escapeHtml, DashboardStats, and stats builder.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Escape HTML special characters to prevent XSS when interpolating
|
|
6
|
+
* user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
|
|
7
|
+
* Note: This escapes HTML entity characters only. It does not sanitize URL schemes
|
|
8
|
+
* (e.g., javascript:) — callers placing values in href attributes should validate
|
|
9
|
+
* the URL scheme if the source is untrusted. GitHub API URLs are trusted.
|
|
10
|
+
*/
|
|
11
|
+
export function escapeHtml(text) {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
export function buildDashboardStats(digest, state) {
|
|
20
|
+
const summary = digest.summary || {
|
|
21
|
+
totalActivePRs: 0,
|
|
22
|
+
totalMergedAllTime: 0,
|
|
23
|
+
mergeRate: 0,
|
|
24
|
+
totalNeedingAttention: 0,
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
activePRs: summary.totalActivePRs,
|
|
28
|
+
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
29
|
+
mergedPRs: summary.totalMergedAllTime,
|
|
30
|
+
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
|
|
31
|
+
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
|
|
3
|
+
*/
|
|
4
|
+
import type { DailyDigest, AgentState } from '../core/types.js';
|
|
5
|
+
import type { DashboardStats } from './dashboard-formatters.js';
|
|
6
|
+
/** Generate the Chart.js JavaScript for the dashboard. */
|
|
7
|
+
export declare function generateDashboardScripts(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>): string;
|