@oss-autopilot/core 0.44.18 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-registry.js +78 -0
- package/dist/cli.bundle.cjs +58 -1396
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-process.d.ts +19 -0
- package/dist/commands/dashboard-process.js +93 -0
- package/dist/commands/dashboard-server.d.ts +1 -15
- package/dist/commands/dashboard-server.js +27 -84
- package/dist/commands/dashboard.d.ts +0 -10
- package/dist/commands/dashboard.js +1 -27
- package/dist/commands/index.d.ts +52 -8
- package/dist/commands/index.js +57 -9
- package/dist/commands/pr-template.d.ts +9 -0
- package/dist/commands/pr-template.js +14 -0
- package/dist/commands/rate-limiter.d.ts +31 -0
- package/dist/commands/rate-limiter.js +36 -0
- package/dist/commands/startup.d.ts +1 -1
- package/dist/commands/startup.js +21 -22
- package/dist/commands/stats.d.ts +15 -0
- package/dist/commands/stats.js +57 -0
- package/dist/core/github-stats.d.ts +0 -4
- package/dist/core/github-stats.js +1 -9
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/pr-monitor.js +3 -13
- package/dist/core/pr-template.d.ts +27 -0
- package/dist/core/pr-template.js +65 -0
- package/dist/core/state.d.ts +20 -17
- package/dist/core/state.js +29 -30
- package/dist/core/stats.d.ts +25 -0
- package/dist/core/stats.js +33 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/utils.d.ts +16 -9
- package/dist/core/utils.js +45 -11
- package/dist/formatters/json.d.ts +8 -2
- package/package.json +1 -1
- package/dist/commands/dashboard-components.d.ts +0 -33
- package/dist/commands/dashboard-components.js +0 -57
- package/dist/commands/dashboard-formatters.d.ts +0 -12
- package/dist/commands/dashboard-formatters.js +0 -19
- package/dist/commands/dashboard-scripts.d.ts +0 -7
- package/dist/commands/dashboard-scripts.js +0 -281
- package/dist/commands/dashboard-styles.d.ts +0 -5
- package/dist/commands/dashboard-styles.js +0 -765
- package/dist/commands/dashboard-templates.d.ts +0 -12
- package/dist/commands/dashboard-templates.js +0 -470
package/dist/commands/startup.js
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
|
-
import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
|
|
11
|
+
import { getStateManager, getGitHubToken, getCLIVersion, detectGitHubUsername } from '../core/index.js';
|
|
12
12
|
import { errorMessage } from '../core/errors.js';
|
|
13
13
|
import { executeDailyCheck } from './daily.js';
|
|
14
14
|
import { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
15
|
-
import { writeDashboardFromState } from './dashboard.js';
|
|
16
15
|
/**
|
|
17
16
|
* Parse issueListPath from a config file's YAML frontmatter.
|
|
18
17
|
* Returns the path string or undefined if not found.
|
|
@@ -121,16 +120,30 @@ export function openInBrowser(url) {
|
|
|
121
120
|
* Returns StartupOutput with one of three shapes:
|
|
122
121
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
123
122
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
124
|
-
* 3. Success: { version, setupComplete: true, daily, dashboardUrl?,
|
|
123
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardUrl?, issueList? }
|
|
125
124
|
*
|
|
126
125
|
* Errors from the daily check propagate to the caller.
|
|
127
126
|
*/
|
|
128
127
|
export async function runStartup() {
|
|
129
128
|
const version = getCLIVersion();
|
|
130
129
|
const stateManager = getStateManager();
|
|
131
|
-
// 1. Check setup
|
|
130
|
+
// 1. Check setup — auto-detect if incomplete
|
|
131
|
+
let autoDetected = false;
|
|
132
132
|
if (!stateManager.isSetupComplete()) {
|
|
133
|
-
|
|
133
|
+
const detectedUsername = await detectGitHubUsername();
|
|
134
|
+
if (detectedUsername) {
|
|
135
|
+
try {
|
|
136
|
+
stateManager.initializeWithDefaults(detectedUsername);
|
|
137
|
+
autoDetected = true;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error(`[STARTUP] Auto-detected username "${detectedUsername}" but failed to save config:`, errorMessage(err));
|
|
141
|
+
return { version, setupComplete: false };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return { version, setupComplete: false };
|
|
146
|
+
}
|
|
134
147
|
}
|
|
135
148
|
// 2. Check auth
|
|
136
149
|
const token = getGitHubToken();
|
|
@@ -143,22 +156,10 @@ export async function runStartup() {
|
|
|
143
156
|
}
|
|
144
157
|
// 3. Run daily check
|
|
145
158
|
const daily = await executeDailyCheck(token);
|
|
146
|
-
// 4. Launch interactive SPA dashboard
|
|
159
|
+
// 4. Launch interactive SPA dashboard
|
|
147
160
|
// Skip opening on first run (0 PRs) — the welcome flow handles onboarding
|
|
148
161
|
let dashboardUrl;
|
|
149
|
-
let dashboardPath;
|
|
150
162
|
let dashboardOpened = false;
|
|
151
|
-
function tryStaticHtmlFallback() {
|
|
152
|
-
try {
|
|
153
|
-
dashboardPath = writeDashboardFromState();
|
|
154
|
-
openInBrowser(dashboardPath);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
catch (htmlError) {
|
|
158
|
-
console.error('[STARTUP] Static HTML dashboard fallback also failed:', errorMessage(htmlError));
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
163
|
if (daily.digest.summary.totalActivePRs > 0) {
|
|
163
164
|
try {
|
|
164
165
|
const spaResult = await launchDashboardServer();
|
|
@@ -168,13 +169,11 @@ export async function runStartup() {
|
|
|
168
169
|
dashboardOpened = true;
|
|
169
170
|
}
|
|
170
171
|
else {
|
|
171
|
-
console.error('[STARTUP] Dashboard SPA assets not found
|
|
172
|
-
dashboardOpened = tryStaticHtmlFallback();
|
|
172
|
+
console.error('[STARTUP] Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build');
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
catch (error) {
|
|
176
176
|
console.error('[STARTUP] SPA dashboard launch failed:', errorMessage(error));
|
|
177
|
-
dashboardOpened = tryStaticHtmlFallback();
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
179
|
// Append dashboard status to brief summary (only startup opens the browser, not daily)
|
|
@@ -186,9 +185,9 @@ export async function runStartup() {
|
|
|
186
185
|
return {
|
|
187
186
|
version,
|
|
188
187
|
setupComplete: true,
|
|
188
|
+
autoDetected,
|
|
189
189
|
daily,
|
|
190
190
|
dashboardUrl,
|
|
191
|
-
dashboardPath,
|
|
192
191
|
issueList,
|
|
193
192
|
};
|
|
194
193
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats command
|
|
3
|
+
* Compute and display contribution statistics.
|
|
4
|
+
*/
|
|
5
|
+
import type { StatsOutput } from '../formatters/json.js';
|
|
6
|
+
export declare function runStats(): Promise<StatsOutput>;
|
|
7
|
+
export declare function formatStatsMarkdown(stats: StatsOutput): string;
|
|
8
|
+
interface BadgeData {
|
|
9
|
+
schemaVersion: number;
|
|
10
|
+
label: string;
|
|
11
|
+
message: string;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function formatStatsBadge(stats: StatsOutput): BadgeData;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats command
|
|
3
|
+
* Compute and display contribution statistics.
|
|
4
|
+
*/
|
|
5
|
+
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { computeContributionStats } from '../core/stats.js';
|
|
7
|
+
export async function runStats() {
|
|
8
|
+
const stateManager = getStateManager();
|
|
9
|
+
const state = stateManager.getState();
|
|
10
|
+
const activePRCount = state.lastDigest?.summary?.totalActivePRs ?? 0;
|
|
11
|
+
const stats = computeContributionStats({
|
|
12
|
+
repoScores: state.repoScores ?? {},
|
|
13
|
+
activePRCount,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
...stats,
|
|
17
|
+
mergeRateFormatted: `${(stats.mergeRate * 100).toFixed(1)}%`,
|
|
18
|
+
username: state.config.githubUsername,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function formatStatsMarkdown(stats) {
|
|
22
|
+
const lines = [
|
|
23
|
+
`# OSS Contribution Stats for @${stats.username}`,
|
|
24
|
+
'',
|
|
25
|
+
'| Metric | Value |',
|
|
26
|
+
'|--------|-------|',
|
|
27
|
+
`| Merged PRs | ${stats.totalMerged} |`,
|
|
28
|
+
`| Merge Rate | ${stats.mergeRateFormatted} |`,
|
|
29
|
+
`| Active PRs | ${stats.activePRs} |`,
|
|
30
|
+
`| Repos Contributed | ${stats.reposContributed} |`,
|
|
31
|
+
'',
|
|
32
|
+
];
|
|
33
|
+
if (stats.topRepos.length > 0) {
|
|
34
|
+
lines.push('## Top Repos', '', '| Repo | Merged PRs |', '|------|-----------|');
|
|
35
|
+
for (const repo of stats.topRepos) {
|
|
36
|
+
lines.push(`| ${repo.repo} | ${repo.mergedCount} |`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
}
|
|
40
|
+
lines.push('---', '*Generated by [OSS Autopilot](https://github.com/costajohnt/oss-autopilot)*');
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function pickBadgeColor(stats) {
|
|
44
|
+
if (stats.totalMerged === 0)
|
|
45
|
+
return 'blue';
|
|
46
|
+
if (stats.mergeRate >= 0.8)
|
|
47
|
+
return 'brightgreen';
|
|
48
|
+
if (stats.mergeRate >= 0.6)
|
|
49
|
+
return 'green';
|
|
50
|
+
if (stats.mergeRate >= 0.4)
|
|
51
|
+
return 'yellow';
|
|
52
|
+
return 'orange';
|
|
53
|
+
}
|
|
54
|
+
export function formatStatsBadge(stats) {
|
|
55
|
+
const message = stats.totalMerged > 0 ? `${stats.mergeRateFormatted} merge rate | ${stats.totalMerged} merged` : 'Getting Started';
|
|
56
|
+
return { schemaVersion: 1, label: 'OSS Contributions', message, color: pickBadgeColor(stats) };
|
|
57
|
+
}
|
|
@@ -34,8 +34,6 @@ export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function fetchRecentlyClosedPRs(octokit: Octokit, config: {
|
|
36
36
|
githubUsername: string;
|
|
37
|
-
excludeRepos: string[];
|
|
38
|
-
excludeOrgs?: string[];
|
|
39
37
|
}, days?: number): Promise<ClosedPR[]>;
|
|
40
38
|
/**
|
|
41
39
|
* Fetch PRs merged in the last N days.
|
|
@@ -43,6 +41,4 @@ export declare function fetchRecentlyClosedPRs(octokit: Octokit, config: {
|
|
|
43
41
|
*/
|
|
44
42
|
export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
|
|
45
43
|
githubUsername: string;
|
|
46
|
-
excludeRepos: string[];
|
|
47
|
-
excludeOrgs?: string[];
|
|
48
44
|
}, days?: number): Promise<MergedPR[]>;
|
|
@@ -86,8 +86,6 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
|
|
|
86
86
|
// Skip own repos (PRs to your own repos aren't OSS contributions)
|
|
87
87
|
if (isOwnRepo(owner, githubUsername))
|
|
88
88
|
continue;
|
|
89
|
-
// Note: excludeRepos/excludeOrgs are intentionally NOT filtered here.
|
|
90
|
-
// Those filters control issue discovery/search, not historical statistics.
|
|
91
89
|
// Skip repos below the minimum star threshold (#576).
|
|
92
90
|
// Repos with unknown star counts (not yet fetched) are also excluded (fail-closed).
|
|
93
91
|
if (starFilter && isBelowMinStars(starFilter.knownStarCounts.get(repo), starFilter.minStars)) {
|
|
@@ -172,8 +170,7 @@ export function fetchUserClosedPRCounts(octokit, githubUsername, starFilter) {
|
|
|
172
170
|
}, starFilter);
|
|
173
171
|
}
|
|
174
172
|
/**
|
|
175
|
-
* Shared helper: search for recent PRs and filter out own repos
|
|
176
|
-
* Returns parsed search results that pass all filters.
|
|
173
|
+
* Shared helper: search for recent PRs and filter out own repos.
|
|
177
174
|
*/
|
|
178
175
|
async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
|
|
179
176
|
if (!config.githubUsername) {
|
|
@@ -201,11 +198,6 @@ async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
|
|
|
201
198
|
// Skip own repos
|
|
202
199
|
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
203
200
|
continue;
|
|
204
|
-
// Skip excluded repos and orgs
|
|
205
|
-
if (config.excludeRepos.includes(repo))
|
|
206
|
-
continue;
|
|
207
|
-
if (config.excludeOrgs?.some((org) => parsed.owner.toLowerCase() === org.toLowerCase()))
|
|
208
|
-
continue;
|
|
209
201
|
results.push(mapItem(item, { owner: parsed.owner, repo, number: parsed.number }));
|
|
210
202
|
}
|
|
211
203
|
debug(MODULE, `Found ${results.length} recently ${label} PRs`);
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,9 +8,11 @@ export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssu
|
|
|
8
8
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
9
9
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
10
10
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
11
|
-
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir,
|
|
11
|
+
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
|
+
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
17
|
+
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
16
18
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -8,9 +8,11 @@ export { IssueDiscovery, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } fro
|
|
|
8
8
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
9
9
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
10
10
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
11
|
-
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir,
|
|
11
|
+
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
|
+
export { computeContributionStats } from './stats.js';
|
|
17
|
+
export { fetchPRTemplate } from './pr-template.js';
|
|
16
18
|
export * from './types.js';
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { getOctokit } from './github.js';
|
|
15
15
|
import { getStateManager } from './state.js';
|
|
16
|
-
import { daysBetween, parseGitHubUrl, extractOwnerRepo, DEFAULT_CONCURRENCY } from './utils.js';
|
|
16
|
+
import { daysBetween, parseGitHubUrl, extractOwnerRepo, isOwnRepo, DEFAULT_CONCURRENCY } from './utils.js';
|
|
17
17
|
import { runWorkerPool } from './concurrency.js';
|
|
18
18
|
import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
19
19
|
import { paginateAll } from './pagination.js';
|
|
@@ -79,7 +79,6 @@ export class PRMonitor {
|
|
|
79
79
|
// Filter items to only PRs worth fetching
|
|
80
80
|
const prs = [];
|
|
81
81
|
const failures = [];
|
|
82
|
-
const shelvedUrls = new Set(config.shelvedPRUrls || []);
|
|
83
82
|
const filteredItems = allItems.filter((item) => {
|
|
84
83
|
if (!item.pull_request)
|
|
85
84
|
return false;
|
|
@@ -89,20 +88,11 @@ export class PRMonitor {
|
|
|
89
88
|
warn('pr-monitor', `Skipping PR with unparseable URL: ${item.html_url}`);
|
|
90
89
|
return false;
|
|
91
90
|
}
|
|
92
|
-
|
|
93
|
-
if (ownerLower === config.githubUsername.toLowerCase())
|
|
94
|
-
return false;
|
|
95
|
-
const repoFullName = `${parsed.owner}/${parsed.repo}`;
|
|
96
|
-
// Keep shelved PRs even from excluded repos/orgs — excludeRepos is meant
|
|
97
|
-
// to stop finding *new* issues there, not hide open PRs already being tracked (#175)
|
|
98
|
-
const isShelved = shelvedUrls.has(item.html_url);
|
|
99
|
-
if (config.excludeRepos.includes(repoFullName) && !isShelved)
|
|
100
|
-
return false;
|
|
101
|
-
if (config.excludeOrgs?.some((org) => ownerLower === org.toLowerCase()) && !isShelved)
|
|
91
|
+
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
102
92
|
return false;
|
|
103
93
|
return true;
|
|
104
94
|
});
|
|
105
|
-
debug('pr-monitor', `Filtered to ${filteredItems.length} PRs after excluding own repos
|
|
95
|
+
debug('pr-monitor', `Filtered to ${filteredItems.length} PRs after excluding own repos`);
|
|
106
96
|
// Fetch detailed info using a worker pool for bounded concurrency.
|
|
107
97
|
await timed('pr-monitor', `Fetch details for ${filteredItems.length} PRs`, async () => {
|
|
108
98
|
await runWorkerPool(filteredItems, async (item) => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a repository's PR description template from GitHub.
|
|
3
|
+
*
|
|
4
|
+
* Checks the standard template locations in GitHub's priority order:
|
|
5
|
+
* 1. .github/PULL_REQUEST_TEMPLATE.md
|
|
6
|
+
* 2. .github/pull_request_template.md
|
|
7
|
+
* 3. docs/pull_request_template.md
|
|
8
|
+
* 4. pull_request_template.md (root)
|
|
9
|
+
*
|
|
10
|
+
* Returns the decoded template content and the path where it was found,
|
|
11
|
+
* or null if no template exists.
|
|
12
|
+
*/
|
|
13
|
+
import type { Octokit } from '@octokit/rest';
|
|
14
|
+
export interface PRTemplateResult {
|
|
15
|
+
/** The decoded template content, or null if no template was found. */
|
|
16
|
+
template: string | null;
|
|
17
|
+
/** The path where the template was found (e.g., ".github/PULL_REQUEST_TEMPLATE.md"). */
|
|
18
|
+
source: string | null;
|
|
19
|
+
/** If non-null, an error prevented a complete check of all template paths. */
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch a repository's PR template by trying each standard location.
|
|
24
|
+
* Returns on the first successful match. Rate limit and auth errors
|
|
25
|
+
* propagate to the caller (consistent with project error strategy).
|
|
26
|
+
*/
|
|
27
|
+
export declare function fetchPRTemplate(octokit: Octokit, owner: string, repo: string): Promise<PRTemplateResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a repository's PR description template from GitHub.
|
|
3
|
+
*
|
|
4
|
+
* Checks the standard template locations in GitHub's priority order:
|
|
5
|
+
* 1. .github/PULL_REQUEST_TEMPLATE.md
|
|
6
|
+
* 2. .github/pull_request_template.md
|
|
7
|
+
* 3. docs/pull_request_template.md
|
|
8
|
+
* 4. pull_request_template.md (root)
|
|
9
|
+
*
|
|
10
|
+
* Returns the decoded template content and the path where it was found,
|
|
11
|
+
* or null if no template exists.
|
|
12
|
+
*/
|
|
13
|
+
import { debug, warn } from './logger.js';
|
|
14
|
+
import { errorMessage, getHttpStatusCode, isRateLimitOrAuthError } from './errors.js';
|
|
15
|
+
const MODULE = 'pr-template';
|
|
16
|
+
/** Standard paths GitHub checks for PR templates, in priority order. */
|
|
17
|
+
const TEMPLATE_PATHS = [
|
|
18
|
+
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
19
|
+
'.github/pull_request_template.md',
|
|
20
|
+
'docs/pull_request_template.md',
|
|
21
|
+
'pull_request_template.md',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a repository's PR template by trying each standard location.
|
|
25
|
+
* Returns on the first successful match. Rate limit and auth errors
|
|
26
|
+
* propagate to the caller (consistent with project error strategy).
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchPRTemplate(octokit, owner, repo) {
|
|
29
|
+
for (const path of TEMPLATE_PATHS) {
|
|
30
|
+
try {
|
|
31
|
+
debug(MODULE, `Checking ${owner}/${repo} for template at ${path}`);
|
|
32
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path });
|
|
33
|
+
// getContent returns an array for directories (e.g., multiple templates); we only want files
|
|
34
|
+
if (Array.isArray(data)) {
|
|
35
|
+
debug(MODULE, `${path} is a directory (multiple templates?), skipping`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (data.type !== 'file') {
|
|
39
|
+
debug(MODULE, `${path} is type "${data.type}", skipping`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!data.content) {
|
|
43
|
+
debug(MODULE, `${path} has no content, skipping`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const template = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
47
|
+
debug(MODULE, `Found PR template at ${path} (${template.length} chars)`);
|
|
48
|
+
return { template, source: path };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// 404 = template doesn't exist at this path, try next
|
|
52
|
+
if (getHttpStatusCode(err) === 404)
|
|
53
|
+
continue;
|
|
54
|
+
// Rate limit and auth errors must propagate (project error strategy)
|
|
55
|
+
if (isRateLimitOrAuthError(err))
|
|
56
|
+
throw err;
|
|
57
|
+
// Other errors (500, network) — warn and return with error context
|
|
58
|
+
const msg = errorMessage(err);
|
|
59
|
+
warn(MODULE, `Error checking ${owner}/${repo}/${path}: ${msg}`);
|
|
60
|
+
return { template: null, source: null, error: msg };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
debug(MODULE, `No PR template found for ${owner}/${repo}`);
|
|
64
|
+
return { template: null, source: null };
|
|
65
|
+
}
|
package/dist/core/state.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ export declare class StateManager {
|
|
|
51
51
|
* Mark setup as complete and record the completion timestamp.
|
|
52
52
|
*/
|
|
53
53
|
markSetupComplete(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Initialize state with sensible defaults for zero-config onboarding.
|
|
56
|
+
* Sets the GitHub username, marks setup as complete, and persists.
|
|
57
|
+
* No-op if setup is already complete (prevents overwriting existing config).
|
|
58
|
+
* @param username - The GitHub username to configure.
|
|
59
|
+
*/
|
|
60
|
+
initializeWithDefaults(username: string): void;
|
|
54
61
|
/**
|
|
55
62
|
* Migrate state from legacy ./data/ location to ~/.oss-autopilot/
|
|
56
63
|
* Returns true if migration was performed
|
|
@@ -157,16 +164,11 @@ export declare class StateManager {
|
|
|
157
164
|
*/
|
|
158
165
|
private static matchesExclusion;
|
|
159
166
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
private isExcluded;
|
|
166
|
-
/**
|
|
167
|
-
* Remove repositories matching the given exclusion lists from `trustedProjects`
|
|
168
|
-
* and `repoScores`. Called when a repo or org is newly excluded to keep stored
|
|
169
|
-
* data consistent with current filters.
|
|
167
|
+
* Remove repositories matching the given exclusion lists from `trustedProjects`.
|
|
168
|
+
* Called when a repo or org is newly excluded.
|
|
169
|
+
*
|
|
170
|
+
* Note: `repoScores` are intentionally preserved so historical stats (merge rate,
|
|
171
|
+
* total merged) remain accurate. Exclusion only affects issue discovery (#591).
|
|
170
172
|
* @param repos - Full "owner/repo" strings to exclude (case-insensitive match).
|
|
171
173
|
* @param orgs - Org names to exclude (case-insensitive match against owner segment).
|
|
172
174
|
*/
|
|
@@ -351,9 +353,10 @@ export declare class StateManager {
|
|
|
351
353
|
getLowScoringRepos(maxScore?: number): string[];
|
|
352
354
|
/**
|
|
353
355
|
* Compute aggregate statistics from the current state. `mergedPRs` and `closedPRs` counts
|
|
354
|
-
* are summed from repo score records
|
|
355
|
-
*
|
|
356
|
-
*
|
|
356
|
+
* are summed from repo score records. `totalTracked` reflects the number of repositories with
|
|
357
|
+
* score records above the minStars threshold.
|
|
358
|
+
*
|
|
359
|
+
* Note: `excludeRepos`/`excludeOrgs` only affect issue discovery, not stats (#591).
|
|
357
360
|
* @returns A Stats snapshot computed from the current state.
|
|
358
361
|
*/
|
|
359
362
|
getStats(): Stats;
|
|
@@ -362,17 +365,17 @@ export declare class StateManager {
|
|
|
362
365
|
* Aggregate statistics returned by {@link StateManager.getStats}.
|
|
363
366
|
*/
|
|
364
367
|
export interface Stats {
|
|
365
|
-
/** Total merged PRs across scored repositories (
|
|
368
|
+
/** Total merged PRs across scored repositories (above minStars threshold). */
|
|
366
369
|
mergedPRs: number;
|
|
367
|
-
/** Total PRs closed without merge across scored repositories (
|
|
370
|
+
/** Total PRs closed without merge across scored repositories (above minStars threshold). */
|
|
368
371
|
closedPRs: number;
|
|
369
372
|
/** Number of active issues. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
370
373
|
activeIssues: number;
|
|
371
|
-
/** Number of trusted projects
|
|
374
|
+
/** Number of trusted projects. */
|
|
372
375
|
trustedProjects: number;
|
|
373
376
|
/** Merge success rate as a percentage string (e.g. "75.0%"). */
|
|
374
377
|
mergeRate: string;
|
|
375
|
-
/** Number of scored repositories (
|
|
378
|
+
/** Number of scored repositories (above minStars threshold). */
|
|
376
379
|
totalTracked: number;
|
|
377
380
|
/** Number of PRs needing a response. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
378
381
|
needsResponse: number;
|
package/dist/core/state.js
CHANGED
|
@@ -216,6 +216,22 @@ export class StateManager {
|
|
|
216
216
|
this.state.config.setupComplete = true;
|
|
217
217
|
this.state.config.setupCompletedAt = new Date().toISOString();
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Initialize state with sensible defaults for zero-config onboarding.
|
|
221
|
+
* Sets the GitHub username, marks setup as complete, and persists.
|
|
222
|
+
* No-op if setup is already complete (prevents overwriting existing config).
|
|
223
|
+
* @param username - The GitHub username to configure.
|
|
224
|
+
*/
|
|
225
|
+
initializeWithDefaults(username) {
|
|
226
|
+
if (this.state.config.setupComplete) {
|
|
227
|
+
debug(MODULE, `Setup already complete, skipping initializeWithDefaults for "${username}"`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.state.config.githubUsername = username;
|
|
231
|
+
this.markSetupComplete();
|
|
232
|
+
debug(MODULE, `Initialized with defaults for user "${username}"`);
|
|
233
|
+
this.save();
|
|
234
|
+
}
|
|
219
235
|
/**
|
|
220
236
|
* Migrate state from legacy ./data/ location to ~/.oss-autopilot/
|
|
221
237
|
* Returns true if migration was performed
|
|
@@ -617,19 +633,11 @@ export class StateManager {
|
|
|
617
633
|
return false;
|
|
618
634
|
}
|
|
619
635
|
/**
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
|
|
625
|
-
isExcluded(repo) {
|
|
626
|
-
const { excludeRepos, excludeOrgs } = this.state.config;
|
|
627
|
-
return StateManager.matchesExclusion(repo, excludeRepos, excludeOrgs);
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Remove repositories matching the given exclusion lists from `trustedProjects`
|
|
631
|
-
* and `repoScores`. Called when a repo or org is newly excluded to keep stored
|
|
632
|
-
* data consistent with current filters.
|
|
636
|
+
* Remove repositories matching the given exclusion lists from `trustedProjects`.
|
|
637
|
+
* Called when a repo or org is newly excluded.
|
|
638
|
+
*
|
|
639
|
+
* Note: `repoScores` are intentionally preserved so historical stats (merge rate,
|
|
640
|
+
* total merged) remain accurate. Exclusion only affects issue discovery (#591).
|
|
633
641
|
* @param repos - Full "owner/repo" strings to exclude (case-insensitive match).
|
|
634
642
|
* @param orgs - Org names to exclude (case-insensitive match against owner segment).
|
|
635
643
|
*/
|
|
@@ -638,15 +646,8 @@ export class StateManager {
|
|
|
638
646
|
const beforeTrusted = this.state.config.trustedProjects.length;
|
|
639
647
|
this.state.config.trustedProjects = this.state.config.trustedProjects.filter((p) => !matches(p));
|
|
640
648
|
const removedTrusted = beforeTrusted - this.state.config.trustedProjects.length;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (matches(key)) {
|
|
644
|
-
delete this.state.repoScores[key];
|
|
645
|
-
removedScoreCount++;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (removedTrusted > 0 || removedScoreCount > 0) {
|
|
649
|
-
debug(MODULE, `Removed ${removedTrusted} trusted project(s) and ${removedScoreCount} repo score(s) for excluded repos/orgs`);
|
|
649
|
+
if (removedTrusted > 0) {
|
|
650
|
+
debug(MODULE, `Removed ${removedTrusted} trusted project(s) for excluded repos/orgs`);
|
|
650
651
|
}
|
|
651
652
|
}
|
|
652
653
|
// === Starred Repos Management ===
|
|
@@ -1094,19 +1095,17 @@ export class StateManager {
|
|
|
1094
1095
|
// === Statistics ===
|
|
1095
1096
|
/**
|
|
1096
1097
|
* Compute aggregate statistics from the current state. `mergedPRs` and `closedPRs` counts
|
|
1097
|
-
* are summed from repo score records
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1098
|
+
* are summed from repo score records. `totalTracked` reflects the number of repositories with
|
|
1099
|
+
* score records above the minStars threshold.
|
|
1100
|
+
*
|
|
1101
|
+
* Note: `excludeRepos`/`excludeOrgs` only affect issue discovery, not stats (#591).
|
|
1100
1102
|
* @returns A Stats snapshot computed from the current state.
|
|
1101
1103
|
*/
|
|
1102
1104
|
getStats() {
|
|
1103
|
-
// v2: Calculate from repoScores, filtering out excluded repos/orgs (#211)
|
|
1104
1105
|
let totalMerged = 0;
|
|
1105
1106
|
let totalClosed = 0;
|
|
1106
1107
|
let totalTracked = 0;
|
|
1107
|
-
for (const
|
|
1108
|
-
if (this.isExcluded(repoKey))
|
|
1109
|
-
continue;
|
|
1108
|
+
for (const score of Object.values(this.state.repoScores)) {
|
|
1110
1109
|
if (isBelowMinStars(score.stargazersCount, this.state.config.minStars ?? 50))
|
|
1111
1110
|
continue;
|
|
1112
1111
|
totalTracked++;
|
|
@@ -1119,7 +1118,7 @@ export class StateManager {
|
|
|
1119
1118
|
mergedPRs: totalMerged,
|
|
1120
1119
|
closedPRs: totalClosed,
|
|
1121
1120
|
activeIssues: 0,
|
|
1122
|
-
trustedProjects: this.state.config.trustedProjects.
|
|
1121
|
+
trustedProjects: this.state.config.trustedProjects.length,
|
|
1123
1122
|
mergeRate: mergeRate.toFixed(1) + '%',
|
|
1124
1123
|
totalTracked,
|
|
1125
1124
|
needsResponse: 0,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution statistics computation.
|
|
3
|
+
* Computes metrics from repo scores and PR data for shareable stats and badges.
|
|
4
|
+
*/
|
|
5
|
+
import type { RepoScore } from './types.js';
|
|
6
|
+
export interface ContributionStats {
|
|
7
|
+
totalMerged: number;
|
|
8
|
+
totalClosed: number;
|
|
9
|
+
mergeRate: number;
|
|
10
|
+
activePRs: number;
|
|
11
|
+
reposContributed: number;
|
|
12
|
+
topRepos: Array<{
|
|
13
|
+
repo: string;
|
|
14
|
+
mergedCount: number;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface ComputeStatsInput {
|
|
18
|
+
repoScores: Record<string, Pick<RepoScore, 'mergedPRCount' | 'closedWithoutMergeCount' | 'repo'>>;
|
|
19
|
+
activePRCount: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compute contribution statistics from repo score data.
|
|
23
|
+
* Pure function — no side effects, no API calls.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeContributionStats(input: ComputeStatsInput): ContributionStats;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution statistics computation.
|
|
3
|
+
* Computes metrics from repo scores and PR data for shareable stats and badges.
|
|
4
|
+
*/
|
|
5
|
+
const MAX_TOP_REPOS = 10;
|
|
6
|
+
/**
|
|
7
|
+
* Compute contribution statistics from repo score data.
|
|
8
|
+
* Pure function — no side effects, no API calls.
|
|
9
|
+
*/
|
|
10
|
+
export function computeContributionStats(input) {
|
|
11
|
+
const { repoScores, activePRCount } = input;
|
|
12
|
+
let totalMerged = 0;
|
|
13
|
+
let totalClosed = 0;
|
|
14
|
+
const repoEntries = [];
|
|
15
|
+
for (const score of Object.values(repoScores)) {
|
|
16
|
+
totalMerged += score.mergedPRCount;
|
|
17
|
+
totalClosed += score.closedWithoutMergeCount;
|
|
18
|
+
if (score.mergedPRCount > 0) {
|
|
19
|
+
repoEntries.push({ repo: score.repo, mergedCount: score.mergedPRCount });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const total = totalMerged + totalClosed;
|
|
23
|
+
const mergeRate = total > 0 ? totalMerged / total : 0;
|
|
24
|
+
repoEntries.sort((a, b) => b.mergedCount - a.mergedCount);
|
|
25
|
+
return {
|
|
26
|
+
totalMerged,
|
|
27
|
+
totalClosed,
|
|
28
|
+
mergeRate,
|
|
29
|
+
activePRs: activePRCount,
|
|
30
|
+
reposContributed: repoEntries.length,
|
|
31
|
+
topRepos: repoEntries.slice(0, MAX_TOP_REPOS),
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -437,9 +437,9 @@ export interface AgentConfig {
|
|
|
437
437
|
languages: string[];
|
|
438
438
|
/** GitHub labels to filter issues by (e.g., `["good first issue", "help wanted"]`). */
|
|
439
439
|
labels: string[];
|
|
440
|
-
/** Repos to exclude from search
|
|
440
|
+
/** Repos to exclude from issue discovery/search, in `"owner/repo"` format. */
|
|
441
441
|
excludeRepos: string[];
|
|
442
|
-
/** Organizations to exclude from search
|
|
442
|
+
/** Organizations to exclude from issue discovery/search (case-insensitive match on owner segment). */
|
|
443
443
|
excludeOrgs?: string[];
|
|
444
444
|
/** Repos where the contributor has had PRs merged. Used for prioritization. */
|
|
445
445
|
trustedProjects: string[];
|