@oss-autopilot/core 1.17.1 → 1.17.2
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.bundle.cjs +47 -47
- package/dist/commands/startup.js +33 -24
- package/dist/core/pr-monitor.d.ts +9 -4
- package/dist/core/pr-monitor.js +92 -20
- package/dist/formatters/json.d.ts +7 -0
- package/dist/formatters/json.js +1 -0
- package/package.json +1 -1
package/dist/commands/startup.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
}
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -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
|
|
82
|
-
if (!
|
|
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
|
-
|
|
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:${
|
|
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.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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() !==
|
|
119
|
-
const message = `Configured GitHub username @${
|
|
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 @${
|
|
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:${
|
|
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,
|
|
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
|
/**
|
package/dist/formatters/json.js
CHANGED