@oss-autopilot/core 0.43.1 → 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.
package/dist/cli.js CHANGED
@@ -45,7 +45,6 @@ const LOCAL_ONLY_COMMANDS = [
45
45
  'version',
46
46
  'setup',
47
47
  'checkSetup',
48
- 'dashboard',
49
48
  'serve',
50
49
  'parse-issue-list',
51
50
  'check-integration',
@@ -537,20 +536,6 @@ dashboardCmd
537
536
  handleCommandError(err);
538
537
  }
539
538
  });
540
- // Keep bare `dashboard` (no subcommand) for backward compat — generates static HTML
541
- dashboardCmd
542
- .option('--open', 'Open in browser')
543
- .option('--json', 'Output as JSON')
544
- .option('--offline', 'Use cached data only (no GitHub API calls)')
545
- .action(async (options) => {
546
- try {
547
- const { runDashboard } = await import('./commands/dashboard.js');
548
- await runDashboard({ open: options.open, json: options.json, offline: options.offline });
549
- }
550
- catch (err) {
551
- handleCommandError(err, options.json);
552
- }
553
- });
554
539
  // Parse issue list command (#82)
555
540
  program
556
541
  .command('parse-issue-list <path>')
@@ -673,8 +658,6 @@ program
673
658
  else {
674
659
  console.log(`OSS Autopilot v${data.version}`);
675
660
  console.log(data.daily?.briefSummary ?? '');
676
- if (data.dashboardPath)
677
- console.log(`Dashboard: ${data.dashboardPath}`);
678
661
  }
679
662
  }
680
663
  }
@@ -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;
@@ -11,8 +11,8 @@ 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
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';
14
+ import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
15
+ import { openInBrowser } from './startup.js';
16
16
  // ── Constants ────────────────────────────────────────────────────────────────
17
17
  const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'snooze', 'unsnooze']);
18
18
  const MAX_BODY_BYTES = 10_240;
@@ -109,7 +109,6 @@ export async function findRunningDashboardServer() {
109
109
  // ── Helpers ────────────────────────────────────────────────────────────────────
110
110
  /**
111
111
  * Build the JSON payload that the SPA expects from GET /api/data.
112
- * Same shape as the existing `dashboard --json` output.
113
112
  */
114
113
  function buildDashboardJson(digest, state, commentedIssues) {
115
114
  const prsByRepo = computePRsByRepo(digest, state);
@@ -126,6 +125,9 @@ function buildDashboardJson(digest, state, commentedIssues) {
126
125
  monthlyClosed,
127
126
  activePRs: digest.openPRs || [],
128
127
  shelvedPRUrls: state.config.shelvedPRUrls || [],
128
+ recentlyMergedPRs: digest.recentlyMergedPRs || [],
129
+ recentlyClosedPRs: digest.recentlyClosedPRs || [],
130
+ autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
129
131
  commentedIssues,
130
132
  issueResponses,
131
133
  };
@@ -183,25 +185,11 @@ export async function startDashboardServer(options) {
183
185
  const stateManager = getStateManager();
184
186
  const resolvedAssetsDir = path.resolve(assetsDir);
185
187
  // ── Cached data ──────────────────────────────────────────────────────────
186
- let cachedDigest;
188
+ // Start immediately with state.json data (written by the daily check that
189
+ // precedes this server launch). A background GitHub fetch refreshes the
190
+ // cache after the port is bound, so the startup poller sees us in time.
191
+ let cachedDigest = stateManager.getState().lastDigest;
187
192
  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
193
  if (!cachedDigest) {
206
194
  console.error('No dashboard data available. Run the daily check first:');
207
195
  console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
@@ -330,9 +318,7 @@ export async function startDashboardServer(options) {
330
318
  return;
331
319
  }
332
320
  // Rebuild dashboard data from cached digest + updated state
333
- if (cachedDigest) {
334
- cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
335
- }
321
+ cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
336
322
  sendJson(res, 200, cachedJsonData);
337
323
  }
338
324
  // ── POST /api/refresh handler ────────────────────────────────────────────
@@ -446,31 +432,24 @@ export async function startDashboardServer(options) {
446
432
  writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: new Date().toISOString() });
447
433
  const serverUrl = `http://localhost:${actualPort}`;
448
434
  console.error(`Dashboard server running at ${serverUrl}`);
435
+ // ── Background refresh ─────────────────────────────────────────────────
436
+ // Port is bound and PID file written — now fetch fresh data from GitHub
437
+ // so subsequent /api/data requests get live data instead of cached state.
438
+ if (token) {
439
+ fetchDashboardData(token)
440
+ .then((result) => {
441
+ cachedDigest = result.digest;
442
+ cachedCommentedIssues = result.commentedIssues;
443
+ cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
444
+ console.error('Background data refresh complete');
445
+ })
446
+ .catch((error) => {
447
+ console.error('Background data refresh failed (serving cached data):', errorMessage(error));
448
+ });
449
+ }
449
450
  // ── Open browser ─────────────────────────────────────────────────────────
450
451
  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
- });
452
+ openInBrowser(serverUrl);
474
453
  }
475
454
  // ── Clean shutdown ───────────────────────────────────────────────────────
476
455
  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
  */