@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.
Files changed (46) hide show
  1. package/dist/cli-registry.js +78 -0
  2. package/dist/cli.bundle.cjs +58 -1396
  3. package/dist/cli.bundle.cjs.map +4 -4
  4. package/dist/commands/dashboard-lifecycle.js +1 -1
  5. package/dist/commands/dashboard-process.d.ts +19 -0
  6. package/dist/commands/dashboard-process.js +93 -0
  7. package/dist/commands/dashboard-server.d.ts +1 -15
  8. package/dist/commands/dashboard-server.js +27 -84
  9. package/dist/commands/dashboard.d.ts +0 -10
  10. package/dist/commands/dashboard.js +1 -27
  11. package/dist/commands/index.d.ts +52 -8
  12. package/dist/commands/index.js +57 -9
  13. package/dist/commands/pr-template.d.ts +9 -0
  14. package/dist/commands/pr-template.js +14 -0
  15. package/dist/commands/rate-limiter.d.ts +31 -0
  16. package/dist/commands/rate-limiter.js +36 -0
  17. package/dist/commands/startup.d.ts +1 -1
  18. package/dist/commands/startup.js +21 -22
  19. package/dist/commands/stats.d.ts +15 -0
  20. package/dist/commands/stats.js +57 -0
  21. package/dist/core/github-stats.d.ts +0 -4
  22. package/dist/core/github-stats.js +1 -9
  23. package/dist/core/index.d.ts +3 -1
  24. package/dist/core/index.js +3 -1
  25. package/dist/core/pr-monitor.js +3 -13
  26. package/dist/core/pr-template.d.ts +27 -0
  27. package/dist/core/pr-template.js +65 -0
  28. package/dist/core/state.d.ts +20 -17
  29. package/dist/core/state.js +29 -30
  30. package/dist/core/stats.d.ts +25 -0
  31. package/dist/core/stats.js +33 -0
  32. package/dist/core/types.d.ts +2 -2
  33. package/dist/core/utils.d.ts +16 -9
  34. package/dist/core/utils.js +45 -11
  35. package/dist/formatters/json.d.ts +8 -2
  36. package/package.json +1 -1
  37. package/dist/commands/dashboard-components.d.ts +0 -33
  38. package/dist/commands/dashboard-components.js +0 -57
  39. package/dist/commands/dashboard-formatters.d.ts +0 -12
  40. package/dist/commands/dashboard-formatters.js +0 -19
  41. package/dist/commands/dashboard-scripts.d.ts +0 -7
  42. package/dist/commands/dashboard-scripts.js +0 -281
  43. package/dist/commands/dashboard-styles.d.ts +0 -5
  44. package/dist/commands/dashboard-styles.js +0 -765
  45. package/dist/commands/dashboard-templates.d.ts +0 -12
  46. package/dist/commands/dashboard-templates.js +0 -470
@@ -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?, dashboardPath?, issueList? }
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
- return { version, setupComplete: false };
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 (with static HTML fallback)
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, falling back to static HTML dashboard');
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, excluded repos/orgs.
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`);
@@ -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, 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';
@@ -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, 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';
@@ -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
- const ownerLower = parsed.owner.toLowerCase();
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, shelved, and excluded orgs/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
+ }
@@ -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
- * Check whether a repository matches any exclusion rule from the current config.
161
- * A repo is excluded if it matches an entry in `excludeRepos` (case-insensitive)
162
- * or if its owner segment matches an entry in `excludeOrgs` (case-insensitive).
163
- * @param repo - Repository in "owner/repo" format.
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, excluding repos that match `excludeRepos` or `excludeOrgs`
355
- * in the config (#211). `totalTracked` reflects the number of non-excluded repositories with
356
- * score records.
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 (excludes repos/orgs in exclusion config). */
368
+ /** Total merged PRs across scored repositories (above minStars threshold). */
366
369
  mergedPRs: number;
367
- /** Total PRs closed without merge across scored repositories (excludes repos/orgs in exclusion config). */
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 (excludes repos/orgs in exclusion config). */
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 (excludes repos/orgs in exclusion config). */
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;
@@ -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
- * Check whether a repository matches any exclusion rule from the current config.
621
- * A repo is excluded if it matches an entry in `excludeRepos` (case-insensitive)
622
- * or if its owner segment matches an entry in `excludeOrgs` (case-insensitive).
623
- * @param repo - Repository in "owner/repo" format.
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
- let removedScoreCount = 0;
642
- for (const key of Object.keys(this.state.repoScores)) {
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, excluding repos that match `excludeRepos` or `excludeOrgs`
1098
- * in the config (#211). `totalTracked` reflects the number of non-excluded repositories with
1099
- * score records.
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 [repoKey, score] of Object.entries(this.state.repoScores)) {
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.filter((p) => !this.isExcluded(p)).length,
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
+ }
@@ -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 and stats, in `"owner/repo"` format. */
440
+ /** Repos to exclude from issue discovery/search, in `"owner/repo"` format. */
441
441
  excludeRepos: string[];
442
- /** Organizations to exclude from search and stats (case-insensitive match on owner segment). */
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[];