@oss-autopilot/core 0.44.17 → 0.45.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 +97 -88
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/dashboard-scripts.js +3 -10
- package/dist/commands/pr-template.d.ts +9 -0
- package/dist/commands/pr-template.js +14 -0
- package/dist/commands/startup.js +18 -3
- 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 -0
- package/dist/core/utils.js +45 -0
- package/dist/formatters/json.d.ts +8 -0
- package/package.json +1 -1
|
@@ -132,21 +132,14 @@ export function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, mo
|
|
|
132
132
|
});`;
|
|
133
133
|
// === Repository Breakdown ===
|
|
134
134
|
const repoChart = (() => {
|
|
135
|
-
// Filter helper: exclude repos
|
|
136
|
-
const {
|
|
135
|
+
// Filter helper: exclude repos below minStars (#216)
|
|
136
|
+
const { minStars } = state.config;
|
|
137
137
|
const starThreshold = minStars ?? 50;
|
|
138
138
|
const shouldExcludeRepo = (repo) => {
|
|
139
|
-
const repoLower = repo.toLowerCase();
|
|
140
|
-
if (exRepos.some((r) => r.toLowerCase() === repoLower))
|
|
141
|
-
return true;
|
|
142
|
-
if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split('/')[0]))
|
|
143
|
-
return true;
|
|
144
139
|
const score = (state.repoScores || {})[repo];
|
|
145
140
|
// Fail-closed: repos without cached star data are excluded from charts.
|
|
146
141
|
// Star data is populated by the daily check; repos appear once stars are fetched.
|
|
147
|
-
|
|
148
|
-
return true;
|
|
149
|
-
return false;
|
|
142
|
+
return isBelowMinStars(score?.stargazersCount, starThreshold);
|
|
150
143
|
};
|
|
151
144
|
// Sort repos by total PRs (merged + active + closed) and build "Other" bucket
|
|
152
145
|
const allRepoEntries = Object.entries(
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pr-template command — Fetch a repository's PR description template.
|
|
3
|
+
*
|
|
4
|
+
* Usage: oss-autopilot pr-template owner/repo --json
|
|
5
|
+
*/
|
|
6
|
+
import { type PRTemplateResult } from '../core/pr-template.js';
|
|
7
|
+
export declare function runPRTemplate(opts: {
|
|
8
|
+
repo: string;
|
|
9
|
+
}): Promise<PRTemplateResult>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pr-template command — Fetch a repository's PR description template.
|
|
3
|
+
*
|
|
4
|
+
* Usage: oss-autopilot pr-template owner/repo --json
|
|
5
|
+
*/
|
|
6
|
+
import { getOctokit } from '../core/github.js';
|
|
7
|
+
import { requireGitHubToken, splitRepo } from '../core/utils.js';
|
|
8
|
+
import { fetchPRTemplate } from '../core/pr-template.js';
|
|
9
|
+
export async function runPRTemplate(opts) {
|
|
10
|
+
const { owner, repo } = splitRepo(opts.repo);
|
|
11
|
+
const token = requireGitHubToken();
|
|
12
|
+
const octokit = getOctokit(token);
|
|
13
|
+
return fetchPRTemplate(octokit, owner, repo);
|
|
14
|
+
}
|
package/dist/commands/startup.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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';
|
|
@@ -128,9 +128,23 @@ export function openInBrowser(url) {
|
|
|
128
128
|
export async function runStartup() {
|
|
129
129
|
const version = getCLIVersion();
|
|
130
130
|
const stateManager = getStateManager();
|
|
131
|
-
// 1. Check setup
|
|
131
|
+
// 1. Check setup — auto-detect if incomplete
|
|
132
|
+
let autoDetected = false;
|
|
132
133
|
if (!stateManager.isSetupComplete()) {
|
|
133
|
-
|
|
134
|
+
const detectedUsername = await detectGitHubUsername();
|
|
135
|
+
if (detectedUsername) {
|
|
136
|
+
try {
|
|
137
|
+
stateManager.initializeWithDefaults(detectedUsername);
|
|
138
|
+
autoDetected = true;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error(`[STARTUP] Auto-detected username "${detectedUsername}" but failed to save config:`, errorMessage(err));
|
|
142
|
+
return { version, setupComplete: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
return { version, setupComplete: false };
|
|
147
|
+
}
|
|
134
148
|
}
|
|
135
149
|
// 2. Check auth
|
|
136
150
|
const token = getGitHubToken();
|
|
@@ -186,6 +200,7 @@ export async function runStartup() {
|
|
|
186
200
|
return {
|
|
187
201
|
version,
|
|
188
202
|
setupComplete: true,
|
|
203
|
+
autoDetected,
|
|
189
204
|
daily,
|
|
190
205
|
dashboardUrl,
|
|
191
206
|
dashboardPath,
|
|
@@ -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, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
11
|
+
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, 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, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
11
|
+
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, 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[];
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -255,4 +255,20 @@ export declare function resetGitHubTokenCache(): void;
|
|
|
255
255
|
* }
|
|
256
256
|
*/
|
|
257
257
|
export declare function getGitHubTokenAsync(): Promise<string | null>;
|
|
258
|
+
/**
|
|
259
|
+
* Detect the authenticated GitHub username via the `gh` CLI.
|
|
260
|
+
*
|
|
261
|
+
* Runs `gh api user --jq '.login'` asynchronously and validates the result
|
|
262
|
+
* against GitHub's username rules. Never throws — returns `null` on any failure
|
|
263
|
+
* (gh not installed, not authenticated, invalid output, etc.).
|
|
264
|
+
*
|
|
265
|
+
* @returns The GitHub username string, or `null` if detection fails
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* const username = await detectGitHubUsername();
|
|
269
|
+
* if (username) {
|
|
270
|
+
* console.log(`Logged in as ${username}`);
|
|
271
|
+
* }
|
|
272
|
+
*/
|
|
273
|
+
export declare function detectGitHubUsername(): Promise<string | null>;
|
|
258
274
|
export {};
|