@oss-autopilot/core 0.43.1 → 0.44.1

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.
@@ -1,22 +1,25 @@
1
1
  /**
2
2
  * Dashboard data fetching and aggregation.
3
3
  * Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
4
- * Separates data concerns from template generation and command orchestration.
4
+ * Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
5
5
  */
6
6
  import type { DailyDigest, AgentState, CommentedIssue } from '../core/types.js';
7
+ export interface DashboardStats {
8
+ activePRs: number;
9
+ shelvedPRs: number;
10
+ mergedPRs: number;
11
+ closedPRs: number;
12
+ mergeRate: string;
13
+ }
14
+ export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
7
15
  export interface DashboardFetchResult {
8
16
  digest: DailyDigest;
9
17
  commentedIssues: CommentedIssue[];
10
18
  }
11
- /**
12
- * Fetch fresh dashboard data from GitHub.
13
- * Returns the digest and commented issues, updating state as a side effect.
14
- * Throws if the fetch fails entirely (caller should fall back to cached data).
15
- */
16
19
  export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
17
20
  /**
18
21
  * Compute PRs grouped by repository from a digest and state.
19
- * Used for chart data in both JSON and HTML output.
22
+ * Used for chart data in the dashboard API.
20
23
  */
21
24
  export declare function computePRsByRepo(digest: DailyDigest, state: Readonly<AgentState>): Record<string, {
22
25
  active: number;
@@ -1,17 +1,42 @@
1
1
  /**
2
2
  * Dashboard data fetching and aggregation.
3
3
  * Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
4
- * Separates data concerns from template generation and command orchestration.
4
+ * Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
5
5
  */
6
6
  import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
7
- import { errorMessage } from '../core/errors.js';
7
+ import { errorMessage, getHttpStatusCode } from '../core/errors.js';
8
8
  import { emptyPRCountsResult } from '../core/github-stats.js';
9
9
  import { toShelvedPRRef } from './daily.js';
10
+ export function buildDashboardStats(digest, state) {
11
+ const summary = digest.summary || {
12
+ totalActivePRs: 0,
13
+ totalMergedAllTime: 0,
14
+ mergeRate: 0,
15
+ totalNeedingAttention: 0,
16
+ };
17
+ return {
18
+ activePRs: summary.totalActivePRs,
19
+ shelvedPRs: (digest.shelvedPRs || []).length,
20
+ mergedPRs: summary.totalMergedAllTime,
21
+ closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
22
+ mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
23
+ };
24
+ }
10
25
  /**
11
26
  * Fetch fresh dashboard data from GitHub.
12
27
  * Returns the digest and commented issues, updating state as a side effect.
13
28
  * Throws if the fetch fails entirely (caller should fall back to cached data).
14
29
  */
30
+ function isRateLimitOrAuthError(err) {
31
+ const status = getHttpStatusCode(err);
32
+ if (status === 401 || status === 429)
33
+ return true;
34
+ if (status === 403) {
35
+ const msg = errorMessage(err).toLowerCase();
36
+ return msg.includes('rate limit') || msg.includes('abuse detection');
37
+ }
38
+ return false;
39
+ }
15
40
  export async function fetchDashboardData(token) {
16
41
  const stateManager = getStateManager();
17
42
  const prMonitor = new PRMonitor(token);
@@ -19,18 +44,26 @@ export async function fetchDashboardData(token) {
19
44
  const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
20
45
  prMonitor.fetchUserOpenPRs(),
21
46
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
47
+ if (isRateLimitOrAuthError(err))
48
+ throw err;
22
49
  console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
23
50
  return [];
24
51
  }),
25
52
  prMonitor.fetchRecentlyMergedPRs().catch((err) => {
53
+ if (isRateLimitOrAuthError(err))
54
+ throw err;
26
55
  console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
27
56
  return [];
28
57
  }),
29
58
  prMonitor.fetchUserMergedPRCounts().catch((err) => {
59
+ if (isRateLimitOrAuthError(err))
60
+ throw err;
30
61
  console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
31
62
  return emptyPRCountsResult();
32
63
  }),
33
64
  prMonitor.fetchUserClosedPRCounts().catch((err) => {
65
+ if (isRateLimitOrAuthError(err))
66
+ throw err;
34
67
  console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
35
68
  return emptyPRCountsResult();
36
69
  }),
@@ -113,7 +146,7 @@ export async function fetchDashboardData(token) {
113
146
  }
114
147
  /**
115
148
  * Compute PRs grouped by repository from a digest and state.
116
- * Used for chart data in both JSON and HTML output.
149
+ * Used for chart data in the dashboard API.
117
150
  */
118
151
  export function computePRsByRepo(digest, state) {
119
152
  const prsByRepo = {};
@@ -1,14 +1,7 @@
1
1
  /**
2
- * Dashboard data formatting helpers: escapeHtml, DashboardStats, and stats builder.
2
+ * Dashboard HTML formatting helpers.
3
+ * Used by dashboard-templates.ts and dashboard-components.ts for static HTML generation.
3
4
  */
4
- import type { DailyDigest, AgentState } from '../core/types.js';
5
- export interface DashboardStats {
6
- activePRs: number;
7
- shelvedPRs: number;
8
- mergedPRs: number;
9
- closedPRs: number;
10
- mergeRate: string;
11
- }
12
5
  /**
13
6
  * Escape HTML special characters to prevent XSS when interpolating
14
7
  * user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
@@ -17,4 +10,3 @@ export interface DashboardStats {
17
10
  * the URL scheme if the source is untrusted. GitHub API URLs are trusted.
18
11
  */
19
12
  export declare function escapeHtml(text: string): string;
20
- export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Dashboard data formatting helpers: escapeHtml, DashboardStats, and stats builder.
2
+ * Dashboard HTML formatting helpers.
3
+ * Used by dashboard-templates.ts and dashboard-components.ts for static HTML generation.
3
4
  */
4
5
  /**
5
6
  * Escape HTML special characters to prevent XSS when interpolating
@@ -16,18 +17,3 @@ export function escapeHtml(text) {
16
17
  .replace(/"/g, '&quot;')
17
18
  .replace(/'/g, '&#39;');
18
19
  }
19
- export function buildDashboardStats(digest, state) {
20
- const summary = digest.summary || {
21
- totalActivePRs: 0,
22
- totalMergedAllTime: 0,
23
- mergeRate: 0,
24
- totalNeedingAttention: 0,
25
- };
26
- return {
27
- activePRs: summary.totalActivePRs,
28
- shelvedPRs: (digest.shelvedPRs || []).length,
29
- mergedPRs: summary.totalMergedAllTime,
30
- closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
31
- mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
32
- };
33
- }
@@ -12,7 +12,7 @@ export interface LaunchResult {
12
12
  * Launch the interactive dashboard SPA server as a detached background process.
13
13
  *
14
14
  * Returns the server URL if launched successfully, or null if the SPA assets
15
- * are not available (caller should fall back to static HTML).
15
+ * are not available (dashboard is skipped).
16
16
  *
17
17
  * If a server is already running (detected via PID file + health probe),
18
18
  * returns its URL without launching a new one.
@@ -16,7 +16,7 @@ function sleep(ms) {
16
16
  * Launch the interactive dashboard SPA server as a detached background process.
17
17
  *
18
18
  * Returns the server URL if launched successfully, or null if the SPA assets
19
- * are not available (caller should fall back to static HTML).
19
+ * are not available (dashboard is skipped).
20
20
  *
21
21
  * If a server is already running (detected via PID file + health probe),
22
22
  * returns its URL without launching a new one.
@@ -2,6 +2,6 @@
2
2
  * Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
3
3
  */
4
4
  import type { DailyDigest, AgentState } from '../core/types.js';
5
- import type { DashboardStats } from './dashboard-formatters.js';
5
+ import type { DashboardStats } from './dashboard-data.js';
6
6
  /** Generate the Chart.js JavaScript for the dashboard. */
7
7
  export declare function generateDashboardScripts(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>): string;
@@ -10,11 +10,18 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
12
12
  import { errorMessage, ValidationError } from '../core/errors.js';
13
- import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
14
- import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
15
- import { buildDashboardStats } from './dashboard-templates.js';
13
+ import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, } from './validation.js';
14
+ import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
15
+ import { openInBrowser } from './startup.js';
16
16
  // ── Constants ────────────────────────────────────────────────────────────────
17
- const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'snooze', 'unsnooze']);
17
+ const VALID_ACTIONS = new Set([
18
+ 'shelve',
19
+ 'unshelve',
20
+ 'snooze',
21
+ 'unsnooze',
22
+ 'dismiss',
23
+ 'undismiss',
24
+ ]);
18
25
  const MAX_BODY_BYTES = 10_240;
19
26
  const MIME_TYPES = {
20
27
  '.html': 'text/html',
@@ -109,7 +116,6 @@ export async function findRunningDashboardServer() {
109
116
  // ── Helpers ────────────────────────────────────────────────────────────────────
110
117
  /**
111
118
  * Build the JSON payload that the SPA expects from GET /api/data.
112
- * Same shape as the existing `dashboard --json` output.
113
119
  */
114
120
  function buildDashboardJson(digest, state, commentedIssues) {
115
121
  const prsByRepo = computePRsByRepo(digest, state);
@@ -126,6 +132,10 @@ function buildDashboardJson(digest, state, commentedIssues) {
126
132
  monthlyClosed,
127
133
  activePRs: digest.openPRs || [],
128
134
  shelvedPRUrls: state.config.shelvedPRUrls || [],
135
+ dismissedUrls: Object.keys(state.config.dismissedIssues || {}),
136
+ recentlyMergedPRs: digest.recentlyMergedPRs || [],
137
+ recentlyClosedPRs: digest.recentlyClosedPRs || [],
138
+ autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
129
139
  commentedIssues,
130
140
  issueResponses,
131
141
  };
@@ -183,25 +193,11 @@ export async function startDashboardServer(options) {
183
193
  const stateManager = getStateManager();
184
194
  const resolvedAssetsDir = path.resolve(assetsDir);
185
195
  // ── Cached data ──────────────────────────────────────────────────────────
186
- let cachedDigest;
196
+ // Start immediately with state.json data (written by the daily check that
197
+ // precedes this server launch). A background GitHub fetch refreshes the
198
+ // cache after the port is bound, so the startup poller sees us in time.
199
+ let cachedDigest = stateManager.getState().lastDigest;
187
200
  let cachedCommentedIssues = [];
188
- // Fetch initial data
189
- if (token) {
190
- try {
191
- console.error('Fetching dashboard data from GitHub...');
192
- const result = await fetchDashboardData(token);
193
- cachedDigest = result.digest;
194
- cachedCommentedIssues = result.commentedIssues;
195
- }
196
- catch (error) {
197
- console.error('Failed to fetch data from GitHub:', error);
198
- console.error('Falling back to cached data...');
199
- cachedDigest = stateManager.getState().lastDigest;
200
- }
201
- }
202
- else {
203
- cachedDigest = stateManager.getState().lastDigest;
204
- }
205
201
  if (!cachedDigest) {
206
202
  console.error('No dashboard data available. Run the daily check first:');
207
203
  console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
@@ -269,10 +265,14 @@ export async function startDashboardServer(options) {
269
265
  sendError(res, 400, 'Missing or invalid "url" field');
270
266
  return;
271
267
  }
272
- // Validate URL format — same checks as CLI commands
268
+ // Validate URL format — same checks as CLI commands.
269
+ // Dismiss/undismiss accepts both PR and issue URLs; other actions are PR-only.
270
+ const isDismissAction = body.action === 'dismiss' || body.action === 'undismiss';
271
+ const urlPattern = isDismissAction ? ISSUE_OR_PR_URL_PATTERN : PR_URL_PATTERN;
272
+ const urlType = isDismissAction ? 'issue or PR' : 'PR';
273
273
  try {
274
274
  validateUrl(body.url);
275
- validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
275
+ validateGitHubUrl(body.url, urlPattern, urlType);
276
276
  }
277
277
  catch (err) {
278
278
  if (err instanceof ValidationError) {
@@ -311,6 +311,7 @@ export async function startDashboardServer(options) {
311
311
  switch (body.action) {
312
312
  case 'shelve':
313
313
  stateManager.shelvePR(body.url);
314
+ stateManager.undismissIssue(body.url); // prevent dual state
314
315
  break;
315
316
  case 'unshelve':
316
317
  stateManager.unshelvePR(body.url);
@@ -321,6 +322,13 @@ export async function startDashboardServer(options) {
321
322
  case 'unsnooze':
322
323
  stateManager.unsnoozePR(body.url);
323
324
  break;
325
+ case 'dismiss':
326
+ stateManager.dismissIssue(body.url, new Date().toISOString());
327
+ stateManager.unshelvePR(body.url); // prevent dual state
328
+ break;
329
+ case 'undismiss':
330
+ stateManager.undismissIssue(body.url);
331
+ break;
324
332
  }
325
333
  stateManager.save();
326
334
  }
@@ -330,9 +338,7 @@ export async function startDashboardServer(options) {
330
338
  return;
331
339
  }
332
340
  // Rebuild dashboard data from cached digest + updated state
333
- if (cachedDigest) {
334
- cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
335
- }
341
+ cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
336
342
  sendJson(res, 200, cachedJsonData);
337
343
  }
338
344
  // ── POST /api/refresh handler ────────────────────────────────────────────
@@ -446,31 +452,24 @@ export async function startDashboardServer(options) {
446
452
  writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: new Date().toISOString() });
447
453
  const serverUrl = `http://localhost:${actualPort}`;
448
454
  console.error(`Dashboard server running at ${serverUrl}`);
455
+ // ── Background refresh ─────────────────────────────────────────────────
456
+ // Port is bound and PID file written — now fetch fresh data from GitHub
457
+ // so subsequent /api/data requests get live data instead of cached state.
458
+ if (token) {
459
+ fetchDashboardData(token)
460
+ .then((result) => {
461
+ cachedDigest = result.digest;
462
+ cachedCommentedIssues = result.commentedIssues;
463
+ cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
464
+ console.error('Background data refresh complete');
465
+ })
466
+ .catch((error) => {
467
+ console.error('Background data refresh failed (serving cached data):', errorMessage(error));
468
+ });
469
+ }
449
470
  // ── Open browser ─────────────────────────────────────────────────────────
450
471
  if (open) {
451
- const { execFile } = await import('child_process');
452
- let openCmd;
453
- let args;
454
- switch (process.platform) {
455
- case 'darwin':
456
- openCmd = 'open';
457
- args = [serverUrl];
458
- break;
459
- case 'win32':
460
- openCmd = 'cmd';
461
- args = ['/c', 'start', '', serverUrl];
462
- break;
463
- default:
464
- openCmd = 'xdg-open';
465
- args = [serverUrl];
466
- break;
467
- }
468
- execFile(openCmd, args, (error) => {
469
- if (error) {
470
- console.error('Failed to open browser:', error.message);
471
- console.error(`Open manually: ${serverUrl}`);
472
- }
473
- });
472
+ openInBrowser(serverUrl);
474
473
  }
475
474
  // ── Clean shutdown ───────────────────────────────────────────────────────
476
475
  const shutdown = () => {
@@ -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
  */