@oss-autopilot/core 0.43.0 → 0.44.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.
@@ -6,6 +6,7 @@
6
6
  * Pure functions with no side effects — all data is passed in as arguments.
7
7
  */
8
8
  import type { DailyDigest, AgentState, CommentedIssueWithResponse } from '../core/types.js';
9
- import { type DashboardStats } from './dashboard-formatters.js';
10
- export { escapeHtml, buildDashboardStats, type DashboardStats } from './dashboard-formatters.js';
9
+ import type { DashboardStats } from './dashboard-data.js';
10
+ export { escapeHtml } from './dashboard-formatters.js';
11
+ export { buildDashboardStats, type DashboardStats } from './dashboard-data.js';
11
12
  export declare function generateDashboardHtml(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>, issueResponses?: CommentedIssueWithResponse[]): string;
@@ -9,8 +9,9 @@ import { escapeHtml } from './dashboard-formatters.js';
9
9
  import { DASHBOARD_CSS } from './dashboard-styles.js';
10
10
  import { SVG_ICONS, truncateTitle, renderHealthItems, titleMeta } from './dashboard-components.js';
11
11
  import { generateDashboardScripts } from './dashboard-scripts.js';
12
- // Re-export public API so existing consumers don't break
13
- export { escapeHtml, buildDashboardStats } from './dashboard-formatters.js';
12
+ // Re-export public API so consumers can import from this module
13
+ export { escapeHtml } from './dashboard-formatters.js';
14
+ export { buildDashboardStats } from './dashboard-data.js';
14
15
  export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
15
16
  const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
16
17
  const shelvedPRs = digest.shelvedPRs || [];
@@ -1,18 +1,14 @@
1
1
  /**
2
- * Dashboard command — thin orchestrator.
3
- * Coordinates data fetching, template generation, and file output.
4
- * v2: Fetches fresh data from GitHub if token available, otherwise uses cached lastDigest.
2
+ * Dashboard command — serves the interactive Preact SPA dashboard.
3
+ * Also provides writeDashboardFromState() for generating a static HTML fallback
4
+ * when the SPA cannot be launched (e.g., assets not built).
5
5
  */
6
- interface DashboardOptions {
7
- open?: boolean;
8
- json?: boolean;
9
- offline?: boolean;
10
- }
11
- export declare function runDashboard(options: DashboardOptions): Promise<void>;
12
6
  /**
13
7
  * Generate dashboard HTML from state (no GitHub fetch).
14
8
  * Call after executeDailyCheck() which saves fresh data to state.
15
9
  * Returns the path to the generated dashboard HTML file.
10
+ *
11
+ * Used as a safety net when the interactive SPA dashboard cannot be launched.
16
12
  */
17
13
  export declare function writeDashboardFromState(): string;
18
14
  interface ServeOptions {
@@ -1,124 +1,20 @@
1
1
  /**
2
- * Dashboard command — thin orchestrator.
3
- * Coordinates data fetching, template generation, and file output.
4
- * v2: Fetches fresh data from GitHub if token available, otherwise uses cached lastDigest.
2
+ * Dashboard command — serves the interactive Preact SPA dashboard.
3
+ * Also provides writeDashboardFromState() for generating a static HTML fallback
4
+ * when the SPA cannot be launched (e.g., assets not built).
5
5
  */
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
- import { execFile } from 'child_process';
9
8
  import { getStateManager, getDashboardPath, getGitHubToken } from '../core/index.js';
10
- import { errorMessage } from '../core/errors.js';
11
- import { outputJson } from '../formatters/json.js';
12
- import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
9
+ import { getMonthlyData } from './dashboard-data.js';
13
10
  import { buildDashboardStats, generateDashboardHtml } from './dashboard-templates.js';
14
- export async function runDashboard(options) {
15
- const stateManager = getStateManager();
16
- const token = options.offline ? null : getGitHubToken();
17
- let digest;
18
- let commentedIssues = [];
19
- // In offline mode, skip all GitHub API calls
20
- if (options.offline) {
21
- const state = stateManager.getState();
22
- digest = state.lastDigest;
23
- if (!digest) {
24
- if (options.json) {
25
- outputJson({ error: 'No cached data found. Run without --offline first.', offline: true });
26
- }
27
- else {
28
- console.error('No cached data found. Run without --offline first.');
29
- }
30
- return;
31
- }
32
- const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
33
- console.error(`Offline mode: using cached data from ${lastUpdated}`);
34
- }
35
- else if (token) {
36
- console.error('Fetching fresh data from GitHub...');
37
- try {
38
- const result = await fetchDashboardData(token);
39
- digest = result.digest;
40
- commentedIssues = result.commentedIssues;
41
- }
42
- catch (error) {
43
- console.error('Failed to fetch fresh data:', errorMessage(error));
44
- console.error('Falling back to cached data (issue conversations unavailable)...');
45
- digest = stateManager.getState().lastDigest;
46
- }
47
- }
48
- else {
49
- // No token and not offline — fall back to cached digest with warning
50
- console.error('Warning: No GitHub token found. Using cached data (may be stale).');
51
- console.error('Set GITHUB_TOKEN or run `gh auth login` for fresh data.');
52
- digest = stateManager.getState().lastDigest;
53
- }
54
- // Check if we have a digest to display
55
- if (!digest) {
56
- if (options.json) {
57
- outputJson({ error: 'No data available. Run daily check first with GITHUB_TOKEN.' });
58
- }
59
- else {
60
- console.error('No dashboard data available. Run the daily check first:');
61
- console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
62
- }
63
- return;
64
- }
65
- const state = stateManager.getState();
66
- // Gather data for charts from digest
67
- const prsByRepo = computePRsByRepo(digest, state);
68
- const topRepos = computeTopRepos(prsByRepo);
69
- const { monthlyMerged, monthlyClosed, monthlyOpened } = getMonthlyData(state);
70
- const stats = buildDashboardStats(digest, state);
71
- if (options.json) {
72
- const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
73
- const jsonData = {
74
- stats,
75
- prsByRepo,
76
- topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
77
- monthlyMerged,
78
- activePRs: digest.openPRs || [],
79
- commentedIssues,
80
- issueResponses,
81
- };
82
- if (options.offline) {
83
- jsonData.offline = true;
84
- jsonData.lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
85
- }
86
- outputJson(jsonData);
87
- return;
88
- }
89
- const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
90
- const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses);
91
- // Write to file in ~/.oss-autopilot/
92
- const dashboardPath = getDashboardPath();
93
- fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
94
- if (options.offline) {
95
- const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
96
- console.log(`\n📊 Dashboard generated (offline, cached data from ${lastUpdated}): ${dashboardPath}`);
97
- }
98
- else {
99
- console.log(`\n📊 Dashboard generated: ${dashboardPath}`);
100
- }
101
- if (options.open) {
102
- // Use platform-specific open command - path is hardcoded, not user input
103
- const isWindows = process.platform === 'win32';
104
- const openCmd = process.platform === 'darwin' ? 'open' : isWindows ? 'cmd' : 'xdg-open';
105
- const args = isWindows ? ['/c', 'start', '', dashboardPath] : [dashboardPath];
106
- console.log(`Dashboard: ${dashboardPath}`);
107
- execFile(openCmd, args, (error) => {
108
- if (error) {
109
- console.error('Failed to open browser:', error.message);
110
- console.error(`Open manually: ${dashboardPath}`);
111
- }
112
- });
113
- }
114
- else {
115
- console.log('Run with --open to open in browser');
116
- }
117
- }
11
+ // ── Static HTML fallback ────────────────────────────────────────────────────
118
12
  /**
119
13
  * Generate dashboard HTML from state (no GitHub fetch).
120
14
  * Call after executeDailyCheck() which saves fresh data to state.
121
15
  * Returns the path to the generated dashboard HTML file.
16
+ *
17
+ * Used as a safety net when the interactive SPA dashboard cannot be launched.
122
18
  */
123
19
  export function writeDashboardFromState() {
124
20
  const stateManager = getStateManager();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Startup command
3
3
  * Combines all pre-flight checks into a single CLI invocation:
4
- * auth check, setup check, daily fetch, dashboard generation, version detection, issue list detection.
4
+ * auth check, setup check, daily fetch, dashboard launch, version detection, issue list detection.
5
5
  *
6
6
  * Replaces the ~100-line inline bash script in commands/oss.md with a single
7
7
  * `node cli.bundle.cjs startup --json` call, reducing UI noise in Claude Code.
@@ -26,12 +26,13 @@ export declare function countIssueListItems(content: string): {
26
26
  * Returns IssueListInfo or undefined if no list found.
27
27
  */
28
28
  export declare function detectIssueList(): IssueListInfo | undefined;
29
+ export declare function openInBrowser(url: string): void;
29
30
  /**
30
31
  * Run startup checks and return structured output.
31
32
  * Returns StartupOutput with one of three shapes:
32
33
  * 1. Setup incomplete: { version, setupComplete: false }
33
34
  * 2. Auth failure: { version, setupComplete: true, authError: "..." }
34
- * 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
35
+ * 3. Success: { version, setupComplete: true, daily, dashboardUrl?, dashboardPath?, issueList? }
35
36
  *
36
37
  * Errors from the daily check propagate to the caller.
37
38
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Startup command
3
3
  * Combines all pre-flight checks into a single CLI invocation:
4
- * auth check, setup check, daily fetch, dashboard generation, version detection, issue list detection.
4
+ * auth check, setup check, daily fetch, dashboard launch, version detection, issue list detection.
5
5
  *
6
6
  * Replaces the ~100-line inline bash script in commands/oss.md with a single
7
7
  * `node cli.bundle.cjs startup --json` call, reducing UI noise in Claude Code.
@@ -11,8 +11,8 @@ import { execFile } from 'child_process';
11
11
  import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
12
12
  import { errorMessage } from '../core/errors.js';
13
13
  import { executeDailyCheck } from './daily.js';
14
- import { writeDashboardFromState } from './dashboard.js';
15
14
  import { launchDashboardServer } from './dashboard-lifecycle.js';
15
+ import { writeDashboardFromState } from './dashboard.js';
16
16
  /**
17
17
  * Parse issueListPath from a config file's YAML frontmatter.
18
18
  * Returns the path string or undefined if not found.
@@ -93,21 +93,21 @@ export function detectIssueList() {
93
93
  return { path: issueListPath, source, availableCount: 0, completedCount: 0 };
94
94
  }
95
95
  }
96
- function openInBrowser(filePath) {
96
+ export function openInBrowser(url) {
97
97
  let openCmd;
98
98
  let args;
99
99
  switch (process.platform) {
100
100
  case 'darwin':
101
101
  openCmd = 'open';
102
- args = [filePath];
102
+ args = [url];
103
103
  break;
104
104
  case 'win32':
105
105
  openCmd = 'cmd';
106
- args = ['/c', 'start', '', filePath];
106
+ args = ['/c', 'start', '', url];
107
107
  break;
108
108
  default:
109
109
  openCmd = 'xdg-open';
110
- args = [filePath];
110
+ args = [url];
111
111
  break;
112
112
  }
113
113
  execFile(openCmd, args, (error) => {
@@ -121,7 +121,7 @@ function openInBrowser(filePath) {
121
121
  * Returns StartupOutput with one of three shapes:
122
122
  * 1. Setup incomplete: { version, setupComplete: false }
123
123
  * 2. Auth failure: { version, setupComplete: true, authError: "..." }
124
- * 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
124
+ * 3. Success: { version, setupComplete: true, daily, dashboardUrl?, dashboardPath?, issueList? }
125
125
  *
126
126
  * Errors from the daily check propagate to the caller.
127
127
  */
@@ -143,49 +143,52 @@ export async function runStartup() {
143
143
  }
144
144
  // 3. Run daily check
145
145
  const daily = await executeDailyCheck(token);
146
- // 4. Generate static HTML dashboard (always serves as fallback + snapshot)
147
- let dashboardPath;
148
- try {
149
- dashboardPath = writeDashboardFromState();
150
- }
151
- catch (error) {
152
- console.error('[STARTUP] Dashboard generation failed:', errorMessage(error));
153
- }
154
- // 5. Launch interactive SPA dashboard (preferred) with static HTML fallback
146
+ // 4. Launch interactive SPA dashboard (with static HTML fallback)
155
147
  // Skip opening on first run (0 PRs) — the welcome flow handles onboarding
156
148
  let dashboardUrl;
149
+ let dashboardPath;
157
150
  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
+ }
158
162
  if (daily.digest.summary.totalActivePRs > 0) {
159
- let spaResult = null;
160
163
  try {
161
- spaResult = await launchDashboardServer();
164
+ const spaResult = await launchDashboardServer();
165
+ if (spaResult) {
166
+ dashboardUrl = spaResult.url;
167
+ openInBrowser(spaResult.url);
168
+ dashboardOpened = true;
169
+ }
170
+ else {
171
+ console.error('[STARTUP] Dashboard SPA assets not found, falling back to static HTML dashboard');
172
+ dashboardOpened = tryStaticHtmlFallback();
173
+ }
162
174
  }
163
175
  catch (error) {
164
176
  console.error('[STARTUP] SPA dashboard launch failed:', errorMessage(error));
165
- }
166
- if (spaResult) {
167
- dashboardUrl = spaResult.url;
168
- openInBrowser(spaResult.url);
169
- dashboardOpened = true;
170
- }
171
- else if (dashboardPath) {
172
- // SPA unavailable (assets not built) — fall back to static HTML
173
- openInBrowser(dashboardPath);
174
- dashboardOpened = true;
177
+ dashboardOpened = tryStaticHtmlFallback();
175
178
  }
176
179
  }
177
180
  // Append dashboard status to brief summary (only startup opens the browser, not daily)
178
181
  if (dashboardOpened) {
179
182
  daily.briefSummary += ' | Dashboard opened in browser';
180
183
  }
181
- // 6. Detect issue list
184
+ // 5. Detect issue list
182
185
  const issueList = detectIssueList();
183
186
  return {
184
187
  version,
185
188
  setupComplete: true,
186
189
  daily,
187
- dashboardPath,
188
190
  dashboardUrl,
191
+ dashboardPath,
189
192
  issueList,
190
193
  };
191
194
  }
@@ -3,9 +3,10 @@
3
3
  * Vets a specific issue before working on it
4
4
  */
5
5
  import { IssueDiscovery, requireGitHubToken } from '../core/index.js';
6
- import { validateUrl } from './validation.js';
6
+ import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
7
7
  export async function runVet(options) {
8
8
  validateUrl(options.issueUrl);
9
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
9
10
  const token = requireGitHubToken();
10
11
  const discovery = new IssueDiscovery(token);
11
12
  const candidate = await discovery.vetIssue(options.issueUrl);
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Runs a worker pool that processes items with bounded concurrency.
3
- * N workers consume from a shared index simpler than Promise.race + splice.
3
+ * N workers consume from a shared index. On any worker error, remaining
4
+ * workers are aborted via a shared flag and the error is propagated.
4
5
  */
5
6
  export declare function runWorkerPool<T>(items: T[], worker: (item: T) => Promise<void>, concurrency: number): Promise<void>;
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Runs a worker pool that processes items with bounded concurrency.
3
- * N workers consume from a shared index simpler than Promise.race + splice.
3
+ * N workers consume from a shared index. On any worker error, remaining
4
+ * workers are aborted via a shared flag and the error is propagated.
4
5
  */
5
6
  export async function runWorkerPool(items, worker, concurrency) {
6
7
  let index = 0;
8
+ let aborted = false;
7
9
  const poolWorker = async () => {
8
10
  while (index < items.length) {
11
+ if (aborted)
12
+ break;
9
13
  const item = items[index++];
10
- await worker(item);
14
+ try {
15
+ await worker(item);
16
+ }
17
+ catch (err) {
18
+ aborted = true;
19
+ throw err;
20
+ }
11
21
  }
12
22
  };
13
23
  const workerCount = Math.min(concurrency, items.length);
@@ -509,5 +509,5 @@ export function printDigest(digest, capacity, commentedIssues = []) {
509
509
  console.log('');
510
510
  }
511
511
  console.log('Run with --json for structured output');
512
- console.log('Run "dashboard --open" for browser view');
512
+ console.log('Run "dashboard serve" for browser view');
513
513
  }
@@ -350,11 +350,7 @@ export class PRMonitor {
350
350
  * Check if PR has merge conflict
351
351
  */
352
352
  hasMergeConflict(mergeable, mergeableState) {
353
- if (mergeable === false)
354
- return true;
355
- if (mergeableState === 'dirty')
356
- return true;
357
- return false;
353
+ return mergeable === false || mergeableState === 'dirty';
358
354
  }
359
355
  /**
360
356
  * Get CI status from combined status API and check runs.
@@ -371,6 +367,14 @@ export class PRMonitor {
371
367
  // 404 is expected for repos without check runs configured; log other errors for debugging
372
368
  this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
373
369
  const status = getHttpStatusCode(err);
370
+ // Rate limit errors must propagate — matches listReviewComments pattern (#481)
371
+ if (status === 429)
372
+ throw err;
373
+ if (status === 403) {
374
+ const msg = errorMessage(err).toLowerCase();
375
+ if (msg.includes('rate limit') || msg.includes('abuse detection'))
376
+ throw err;
377
+ }
374
378
  if (status === 404) {
375
379
  debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
376
380
  }
@@ -400,12 +404,8 @@ export class PRMonitor {
400
404
  }
401
405
  catch (error) {
402
406
  const statusCode = getHttpStatusCode(error);
403
- const errMsg = errorMessage(error);
404
- if (statusCode === 401) {
405
- warn('pr-monitor', `CI check failed for ${owner}/${repo}: Invalid token`);
406
- }
407
- else if (statusCode === 403) {
408
- warn('pr-monitor', `CI check failed for ${owner}/${repo}: Rate limit exceeded`);
407
+ if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
408
+ throw error;
409
409
  }
410
410
  else if (statusCode === 404) {
411
411
  // Repo might not have CI configured, this is normal
@@ -413,7 +413,7 @@ export class PRMonitor {
413
413
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
414
414
  }
415
415
  else {
416
- warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errMsg}`);
416
+ warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
417
417
  }
418
418
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
419
419
  }
@@ -5,7 +5,7 @@
5
5
  * Returns the oss-autopilot data directory path, creating it if it does not exist.
6
6
  *
7
7
  * The directory is located at `~/.oss-autopilot/` and serves as the root for
8
- * all persisted user data (state, backups, dashboard).
8
+ * all persisted user data (state, backups, cache).
9
9
  *
10
10
  * @returns Absolute path to the data directory (e.g., `/Users/you/.oss-autopilot`)
11
11
  *
@@ -53,11 +53,8 @@ export declare function getBackupDir(): string;
53
53
  */
54
54
  export declare function getCacheDir(): string;
55
55
  /**
56
- * Returns the path to the generated HTML dashboard file (`~/.oss-autopilot/dashboard.html`).
57
- *
58
- * Implicitly creates the data directory via {@link getDataDir} if it does not exist.
59
- *
60
- * @returns Absolute path to `dashboard.html`
56
+ * Returns the path to the static HTML dashboard file (~/.oss-autopilot/dashboard.html).
57
+ * Used as a fallback when the interactive SPA dashboard cannot be launched.
61
58
  *
62
59
  * @example
63
60
  * const dashPath = getDashboardPath();
@@ -144,10 +141,9 @@ export declare function daysBetween(from: Date, to?: Date): number;
144
141
  /**
145
142
  * Splits an `"owner/repo"` string into its owner and repo components.
146
143
  *
147
- * Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
148
- *
149
144
  * @param repoFullName - Full repository name in `"owner/repo"` format
150
145
  * @returns Object with `owner` and `repo` string properties
146
+ * @throws {Error} If the input does not contain both an owner and repo separated by `/`
151
147
  *
152
148
  * @example
153
149
  * splitRepo('facebook/react')
@@ -15,7 +15,7 @@ let tokenFetchAttempted = false;
15
15
  * Returns the oss-autopilot data directory path, creating it if it does not exist.
16
16
  *
17
17
  * The directory is located at `~/.oss-autopilot/` and serves as the root for
18
- * all persisted user data (state, backups, dashboard).
18
+ * all persisted user data (state, backups, cache).
19
19
  *
20
20
  * @returns Absolute path to the data directory (e.g., `/Users/you/.oss-autopilot`)
21
21
  *
@@ -83,11 +83,8 @@ export function getCacheDir() {
83
83
  return dir;
84
84
  }
85
85
  /**
86
- * Returns the path to the generated HTML dashboard file (`~/.oss-autopilot/dashboard.html`).
87
- *
88
- * Implicitly creates the data directory via {@link getDataDir} if it does not exist.
89
- *
90
- * @returns Absolute path to `dashboard.html`
86
+ * Returns the path to the static HTML dashboard file (~/.oss-autopilot/dashboard.html).
87
+ * Used as a fallback when the interactive SPA dashboard cannot be launched.
91
88
  *
92
89
  * @example
93
90
  * const dashPath = getDashboardPath();
@@ -215,10 +212,9 @@ export function daysBetween(from, to = new Date()) {
215
212
  /**
216
213
  * Splits an `"owner/repo"` string into its owner and repo components.
217
214
  *
218
- * Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
219
- *
220
215
  * @param repoFullName - Full repository name in `"owner/repo"` format
221
216
  * @returns Object with `owner` and `repo` string properties
217
+ * @throws {Error} If the input does not contain both an owner and repo separated by `/`
222
218
  *
223
219
  * @example
224
220
  * splitRepo('facebook/react')
@@ -226,6 +222,9 @@ export function daysBetween(from, to = new Date()) {
226
222
  */
227
223
  export function splitRepo(repoFullName) {
228
224
  const [owner, repo] = repoFullName.split('/');
225
+ if (!owner || !repo) {
226
+ throw new Error(`Invalid repo format: expected "owner/repo", got "${repoFullName}"`);
227
+ }
229
228
  return { owner, repo };
230
229
  }
231
230
  /**
@@ -200,16 +200,17 @@ export interface IssueListInfo {
200
200
  * Three valid shapes:
201
201
  * 1. Setup incomplete: { version, setupComplete: false }
202
202
  * 2. Auth failure: { version, setupComplete: true, authError: "..." }
203
- * 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
203
+ * 3. Success: { version, setupComplete: true, daily, dashboardUrl?, issueList? }
204
204
  */
205
205
  export interface StartupOutput {
206
206
  version: string;
207
207
  setupComplete: boolean;
208
208
  authError?: string;
209
209
  daily?: DailyOutput;
210
- dashboardPath?: string;
211
210
  /** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */
212
211
  dashboardUrl?: string;
212
+ /** Path to the static HTML dashboard file (fallback when SPA cannot launch) */
213
+ dashboardPath?: string;
213
214
  issueList?: IssueListInfo;
214
215
  }
215
216
  /** A single parsed issue from a markdown list (#82) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc",
63
- "bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/cli.bundle.cjs",
63
+ "bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --minify --outfile=dist/cli.bundle.cjs",
64
64
  "start": "tsx src/cli.ts",
65
65
  "dev": "tsx watch src/cli.ts",
66
66
  "typecheck": "tsc --noEmit",