@oss-autopilot/core 1.17.1 → 1.17.3

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.
@@ -222,38 +222,46 @@ export async function runStartup() {
222
222
  }
223
223
  // 3. Run daily check
224
224
  const daily = await executeDailyCheck(token);
225
- // 4. Launch interactive SPA dashboard
226
- // Skip opening on first run (0 PRs) — the welcome flow handles onboarding
225
+ // 4. Launch interactive SPA dashboard.
226
+ //
227
+ // Launched unconditionally once setup and auth pass. A prior heuristic skipped
228
+ // launch whenever `totalActivePRs === 0`, assuming that meant a genuine first
229
+ // run and deferring to the CLI's welcome flow. That gate also swallowed the
230
+ // dashboard in three legitimate cases: a misconfigured/stale `githubUsername`
231
+ // (Search API returns zero), transient GitHub API flakes (state still holds
232
+ // merged PRs but the live count is zero), and users genuinely between PRs.
233
+ // The dashboard's own empty-state UI renders "no PRs" cleanly, so always
234
+ // surfacing it is the right default.
227
235
  let dashboardUrl;
228
236
  let dashboardStatus;
229
- if (daily.digest.summary.totalActivePRs > 0) {
230
- try {
231
- const spaResult = await launchDashboardServer();
232
- if (spaResult) {
233
- dashboardUrl = spaResult.url;
234
- if (spaResult.alreadyRunning) {
235
- const refreshed = await triggerDashboardRefresh(spaResult.port);
236
- dashboardStatus = refreshed ? 'refreshed' : 'running';
237
- }
238
- else {
239
- dashboardStatus = 'opened';
240
- }
241
- // Always surface the dashboard: `open`/`xdg-open`/`start` focus an
242
- // existing tab matching the URL instead of duplicating it, so this is
243
- // safe whether the server was just started or was already running.
244
- // Closes #830 properly. The original fix assumed "server running"
245
- // implied "tab open", but the user can close the tab while the daemon
246
- // keeps running, leaving subsequent /oss runs with no visible dashboard.
247
- openInBrowser(spaResult.url);
237
+ let dashboardError;
238
+ try {
239
+ const spaResult = await launchDashboardServer();
240
+ if (spaResult) {
241
+ dashboardUrl = spaResult.url;
242
+ if (spaResult.alreadyRunning) {
243
+ const refreshed = await triggerDashboardRefresh(spaResult.port);
244
+ dashboardStatus = refreshed ? 'refreshed' : 'running';
248
245
  }
249
246
  else {
250
- console.error('[STARTUP] Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build');
247
+ dashboardStatus = 'opened';
251
248
  }
249
+ // `open`/`xdg-open`/`start` focus an existing tab matching the URL
250
+ // instead of duplicating it, so this is safe whether the server was
251
+ // just started or was already running. Closes #830 properly — a user
252
+ // can close the dashboard tab while the daemon keeps running, leaving
253
+ // subsequent /oss runs with no visible dashboard if we didn't re-open.
254
+ openInBrowser(spaResult.url);
252
255
  }
253
- catch (error) {
254
- console.error('[STARTUP] SPA dashboard launch failed:', errorMessage(error));
256
+ else {
257
+ dashboardError = 'Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build';
258
+ console.error(`[STARTUP] ${dashboardError}`);
255
259
  }
256
260
  }
261
+ catch (error) {
262
+ dashboardError = `SPA dashboard launch failed: ${errorMessage(error)}`;
263
+ console.error(`[STARTUP] ${dashboardError}`);
264
+ }
257
265
  // Append dashboard status to brief summary
258
266
  if (dashboardStatus === 'opened') {
259
267
  daily.briefSummary += ' | Dashboard opened in browser';
@@ -272,6 +280,7 @@ export async function runStartup() {
272
280
  autoDetected,
273
281
  daily,
274
282
  dashboardUrl,
283
+ dashboardError,
275
284
  issueList,
276
285
  };
277
286
  }
@@ -18,6 +18,8 @@ export { computeDisplayLabel } from './display-utils.js';
18
18
  export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
19
19
  export { isConditionalChecklistItem } from './checklist-analysis.js';
20
20
  export { determineStatus } from './status-determination.js';
21
+ declare function isPlaceholderUsername(username: string): boolean;
22
+ export { isPlaceholderUsername };
21
23
  /**
22
24
  * Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
23
25
  * Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
@@ -34,10 +36,13 @@ export interface FetchPRsResult {
34
36
  prs: FetchedPR[];
35
37
  failures: PRCheckFailure[];
36
38
  /**
37
- * Non-fatal warnings accumulated while fetching. Currently populated when
38
- * the GitHub Search API's 1000-result ceiling truncates the user's PR
39
- * list callers (daily, dashboard) surface these so users know the data
40
- * may be incomplete (#1057 M25).
39
+ * Non-fatal warnings accumulated while fetching. Populated by:
40
+ * - Placeholder auto-repair (stale/example `githubUsername` replaced with
41
+ * the authenticated viewer's login before the search runs).
42
+ * - Post-fetch viewer-mismatch guardrail (configured username differs
43
+ * from the authenticated viewer when the search returned zero PRs).
44
+ * - Search API 1000-result truncation (#1057 M25).
45
+ * Callers (daily, dashboard) surface these so users see the signal.
41
46
  */
42
47
  warnings?: string[];
43
48
  }
@@ -32,6 +32,29 @@ export { computeDisplayLabel } from './display-utils.js';
32
32
  export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
33
33
  export { isConditionalChecklistItem } from './checklist-analysis.js';
34
34
  export { determineStatus } from './status-determination.js';
35
+ /**
36
+ * Known placeholder values that can end up in `config.githubUsername` from
37
+ * doc snippets, example configs, or aborted setup flows. When the configured
38
+ * username matches one of these, the PR fetch silently returns zero results
39
+ * and the dashboard looks like a fresh install. Detecting these lets us
40
+ * auto-repair the config from the authenticated viewer before fetching.
41
+ *
42
+ * Entries must be lowercase — `Lowercase<string>` on the source tuple makes
43
+ * a non-lowercase entry a compile error, keeping the case-insensitive lookup
44
+ * contract type-checked instead of comment-documented.
45
+ */
46
+ const PLACEHOLDER_USERNAMES = [
47
+ 'example-user',
48
+ 'your-username',
49
+ 'your-github-username',
50
+ ];
51
+ const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
52
+ function isPlaceholderUsername(username) {
53
+ return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
54
+ }
55
+ // Module-private on purpose: callers should only use the predicate so the
56
+ // `.toLowerCase()` contract can't be bypassed by reading the set directly.
57
+ export { isPlaceholderUsername };
35
58
  /**
36
59
  * Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
37
60
  * Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
@@ -78,17 +101,71 @@ export class PRMonitor {
78
101
  * ```
79
102
  */
80
103
  async fetchUserOpenPRs() {
81
- const config = this.stateManager.getState().config;
82
- if (!config.githubUsername) {
104
+ const initialConfig = this.stateManager.getState().config;
105
+ if (!initialConfig.githubUsername) {
83
106
  throw new ConfigurationError('No GitHub username configured. Run setup first.');
84
107
  }
85
- debug('pr-monitor', `Fetching open PRs for @${config.githubUsername}...`);
108
+ // Non-fatal warnings threaded into the result (#1057 M25). When the
109
+ // Search API's hard 1000-result ceiling truncates the user's PR list we
110
+ // previously silently dropped the overflow; now the caller can surface
111
+ // it so the daily digest doesn't quietly report a partial view.
112
+ const warnings = [];
113
+ // Username used for the search — mutated below if the pre-fetch placeholder
114
+ // repair fires. Writing to config is separate from rebinding this local.
115
+ let searchUsername = initialConfig.githubUsername;
116
+ // Proactive placeholder repair: if the configured username is a known
117
+ // placeholder (e.g. "example-user" carried over from docs or an aborted
118
+ // setup), cross-check against the authenticated viewer and persist the
119
+ // corrected name before fetching. Without this, the search silently
120
+ // returns zero results and the dashboard looks like a fresh install.
121
+ // Errors here are non-fatal; rate-limit/auth failures still abort so we
122
+ // don't mask a revoked token by downgrading to a no-op.
123
+ let didRepair = false;
124
+ if (isPlaceholderUsername(searchUsername)) {
125
+ try {
126
+ const { data: viewer } = await this.octokit.users.getAuthenticated();
127
+ const newLogin = viewer.login?.trim();
128
+ // Guard against an empty/whitespace viewer login (enterprise proxies,
129
+ // stubbed Octokit clients) and against the pathological case where the
130
+ // authenticated viewer's login is itself one of our placeholder strings
131
+ // — persisting either would swap one broken config for another.
132
+ if (!newLogin || isPlaceholderUsername(newLogin)) {
133
+ const message = `Placeholder username "${searchUsername}" detected but authenticated viewer ` +
134
+ `returned an unusable login (${JSON.stringify(viewer.login)}); skipping auto-repair.`;
135
+ warnings.push(message);
136
+ warn(MODULE, message);
137
+ }
138
+ else {
139
+ this.stateManager.updateConfig({ githubUsername: newLogin });
140
+ searchUsername = newLogin;
141
+ didRepair = true;
142
+ const message = `Configured GitHub username "${initialConfig.githubUsername}" looks like a placeholder. ` +
143
+ `Auto-repaired to "${newLogin}" using the authenticated viewer.`;
144
+ warnings.push(message);
145
+ warn(MODULE, message);
146
+ }
147
+ }
148
+ catch (err) {
149
+ if (isRateLimitOrAuthError(err))
150
+ throw err;
151
+ // Non-fatal viewer-lookup failures (5xx, network, unexpected shape):
152
+ // surface as a warning (not debug) so the daily digest shows that
153
+ // auto-repair was attempted and couldn't complete. Falls through to
154
+ // the normal fetch with the placeholder, which will then return zero
155
+ // results — the post-fetch guardrail skips its own getAuthenticated
156
+ // attempt since this one already failed the same way.
157
+ const message = `Could not auto-repair placeholder username "${searchUsername}": ${errorMessage(err)}`;
158
+ warnings.push(message);
159
+ warn(MODULE, message);
160
+ }
161
+ }
162
+ debug('pr-monitor', `Fetching open PRs for @${searchUsername}...`);
86
163
  // Search for all open PRs authored by the user with pagination
87
164
  const allItems = [];
88
165
  let page = 1;
89
166
  const perPage = 100;
90
167
  const firstPage = await this.octokit.search.issuesAndPullRequests({
91
- q: `is:pr is:open is:public author:${config.githubUsername}`,
168
+ q: `is:pr is:open is:public author:${searchUsername}`,
92
169
  sort: 'updated',
93
170
  order: 'desc',
94
171
  per_page: perPage,
@@ -101,22 +178,17 @@ export class PRMonitor {
101
178
  const SEARCH_API_RESULT_CAP = 1000;
102
179
  const MAX_PAGES = Math.ceil(SEARCH_API_RESULT_CAP / perPage); // 10 pages at per_page=100
103
180
  const totalPages = Math.min(Math.ceil(totalCount / perPage), MAX_PAGES);
104
- // Non-fatal warnings threaded into the result (#1057 M25). When the
105
- // Search API's hard 1000-result ceiling truncates the user's PR list we
106
- // previously silently dropped the overflow; now the caller can surface
107
- // it so the daily digest doesn't quietly report a partial view.
108
- const warnings = [];
109
181
  // Guardrail: if the Search API returned zero PRs, cross-check the
110
- // configured username against the authenticated viewer. A real failure
111
- // mode was a stale/placeholder username (e.g. "example-user") silently
112
- // producing zero results with no error — the dashboard just showed
113
- // "0 active PRs" and looked like a fresh install. getAuthenticated is
114
- // advisory; a failure here never breaks the fetch.
115
- if (totalCount === 0) {
182
+ // configured username against the authenticated viewer. This catches
183
+ // stale usernames (e.g. a renamed GitHub account) that are not in the
184
+ // known-placeholder set. Skipped when the pre-fetch repair already
185
+ // reconciled the two no need to spend a second getAuthenticated call
186
+ // just to confirm a match we already established.
187
+ if (totalCount === 0 && !didRepair) {
116
188
  try {
117
189
  const { data: viewer } = await this.octokit.users.getAuthenticated();
118
- if (viewer.login.toLowerCase() !== config.githubUsername.toLowerCase()) {
119
- const message = `Configured GitHub username @${config.githubUsername} does not match ` +
190
+ if (viewer.login.toLowerCase() !== searchUsername.toLowerCase()) {
191
+ const message = `Configured GitHub username @${searchUsername} does not match ` +
120
192
  `authenticated user @${viewer.login}. Did you mean to run ` +
121
193
  `\`oss-autopilot config username ${viewer.login}\`? Zero PRs returned.`;
122
194
  warnings.push(message);
@@ -135,7 +207,7 @@ export class PRMonitor {
135
207
  }
136
208
  }
137
209
  if (totalCount > SEARCH_API_RESULT_CAP) {
138
- warnings.push(`GitHub Search API returned ${totalCount} PRs for @${config.githubUsername}, ` +
210
+ warnings.push(`GitHub Search API returned ${totalCount} PRs for @${searchUsername}, ` +
139
211
  `but results are capped at ${SEARCH_API_RESULT_CAP}. ` +
140
212
  `Showing the ${SEARCH_API_RESULT_CAP} most recently updated PRs.`);
141
213
  warn(MODULE, warnings[warnings.length - 1]);
@@ -143,7 +215,7 @@ export class PRMonitor {
143
215
  while (page < totalPages) {
144
216
  page++;
145
217
  const nextPage = await this.octokit.search.issuesAndPullRequests({
146
- q: `is:pr is:open is:public author:${config.githubUsername}`,
218
+ q: `is:pr is:open is:public author:${searchUsername}`,
147
219
  sort: 'updated',
148
220
  order: 'desc',
149
221
  per_page: perPage,
@@ -163,7 +235,7 @@ export class PRMonitor {
163
235
  warn('pr-monitor', `Skipping PR with unparseable URL: ${item.html_url}`);
164
236
  return false;
165
237
  }
166
- if (isOwnRepo(parsed.owner, config.githubUsername))
238
+ if (isOwnRepo(parsed.owner, searchUsername))
167
239
  return false;
168
240
  return true;
169
241
  });
@@ -274,6 +274,13 @@ export interface StartupOutput {
274
274
  daily?: DailyOutput;
275
275
  /** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */
276
276
  dashboardUrl?: string;
277
+ /**
278
+ * Set when the dashboard launch or refresh failed (assets missing, port
279
+ * conflict, spawn error, etc.). The dashboard is always attempted, so JSON
280
+ * consumers — which previously saw only a missing `dashboardUrl` — now have
281
+ * a structured signal to surface or recover from the failure.
282
+ */
283
+ dashboardError?: string;
277
284
  issueList?: IssueListInfo;
278
285
  }
279
286
  /**
@@ -70,6 +70,7 @@ export function toCompactStartupOutput(output) {
70
70
  ...rest,
71
71
  daily: daily ? toCompactDailyOutput(daily) : undefined,
72
72
  dashboardUrl: output.dashboardUrl,
73
+ dashboardError: output.dashboardError,
73
74
  issueList: output.issueList,
74
75
  };
75
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,10 @@
18
18
  "./commands": {
19
19
  "import": "./dist/commands/index.js",
20
20
  "types": "./dist/commands/index.d.ts"
21
+ },
22
+ "./dashboard-schema": {
23
+ "import": "./dist/core/dashboard-data-schema.js",
24
+ "types": "./dist/core/dashboard-data-schema.d.ts"
21
25
  }
22
26
  },
23
27
  "files": [