@youdie006/prodex 0.39.5 → 0.40.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.
- package/dist/blocker-report.js +81 -0
- package/dist/chatgpt-browser.js +27 -2
- package/dist/cli-help.js +2 -0
- package/dist/cli-pro.js +133 -3
- package/dist/registry.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rank what actually blocks consults, across every bridge root on the machine.
|
|
3
|
+
*
|
|
4
|
+
* Half of every consult here ends in a blocker, and until now asking WHICH
|
|
5
|
+
* failure dominates meant writing a throwaway script: `pro list` reads a single
|
|
6
|
+
* repo, and the registry that knows where the others are had no reader. Pure
|
|
7
|
+
* on purpose - the reading lives in the command, so the ranking can be tested
|
|
8
|
+
* without a filesystem.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Codes that describe how a send died rather than why. Counting these whole
|
|
12
|
+
* reports "the catch-all is biggest" and names nothing to fix - measured, one
|
|
13
|
+
* of them held 165 of 267 blockers - so their message decides the group.
|
|
14
|
+
*/
|
|
15
|
+
const CATCH_ALL_CODES = new Set(["browser_send_failed", "consult_failed", "unknown_error"]);
|
|
16
|
+
/**
|
|
17
|
+
* The part of a message that identifies the failure, with the varying parts
|
|
18
|
+
* removed: the same picker failure is written once with "Pro" and once with a
|
|
19
|
+
* model name, and those are one cause, not two.
|
|
20
|
+
*/
|
|
21
|
+
export function blockerCause(message) {
|
|
22
|
+
const firstSentence = /^(.*?)(?:\.\s|\.$|$)/.exec(message.trim())?.[1] ?? message.trim();
|
|
23
|
+
return firstSentence
|
|
24
|
+
.replace(/"[^"]*"/g, '"..."')
|
|
25
|
+
.replace(/\d+/g, "N")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
function groupKey(code, message) {
|
|
30
|
+
if (!CATCH_ALL_CODES.has(code))
|
|
31
|
+
return code;
|
|
32
|
+
const cause = blockerCause(message);
|
|
33
|
+
return cause ? `${code}: ${cause}` : code;
|
|
34
|
+
}
|
|
35
|
+
export function buildBlockerReport(input) {
|
|
36
|
+
const cutoff = input.since ? Date.parse(input.since) : undefined;
|
|
37
|
+
const inWindow = input.consults.filter((c) => {
|
|
38
|
+
if (cutoff === undefined)
|
|
39
|
+
return true;
|
|
40
|
+
if (!c.createdAt)
|
|
41
|
+
return false;
|
|
42
|
+
const at = Date.parse(c.createdAt);
|
|
43
|
+
return Number.isFinite(at) && at >= cutoff;
|
|
44
|
+
});
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
let blocked = 0;
|
|
47
|
+
for (const consult of inWindow) {
|
|
48
|
+
if (!consult.blocker)
|
|
49
|
+
continue;
|
|
50
|
+
blocked += 1;
|
|
51
|
+
const key = groupKey(consult.blocker.code, consult.blocker.message);
|
|
52
|
+
const at = consult.createdAt ?? "";
|
|
53
|
+
const existing = groups.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
existing.count += 1;
|
|
56
|
+
if (at > existing.lastSeen)
|
|
57
|
+
existing.lastSeen = at;
|
|
58
|
+
existing.repos.set(consult.repo, (existing.repos.get(consult.repo) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
groups.set(key, {
|
|
62
|
+
count: 1,
|
|
63
|
+
lastSeen: at,
|
|
64
|
+
example: consult.blocker.message,
|
|
65
|
+
repos: new Map([[consult.repo, 1]])
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const ranked = [...groups.entries()]
|
|
70
|
+
.sort((a, b) => b[1].count - a[1].count || a[0].localeCompare(b[0]))
|
|
71
|
+
.slice(0, input.limit ?? Number.POSITIVE_INFINITY)
|
|
72
|
+
.map(([code, g]) => ({
|
|
73
|
+
code,
|
|
74
|
+
count: g.count,
|
|
75
|
+
share: blocked > 0 ? g.count / blocked : 0,
|
|
76
|
+
lastSeen: g.lastSeen,
|
|
77
|
+
example: g.example,
|
|
78
|
+
repos: [...g.repos.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([repo]) => repo)
|
|
79
|
+
}));
|
|
80
|
+
return { totalConsults: inWindow.length, blocked, roots: input.roots, groups: ranked };
|
|
81
|
+
}
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1602,6 +1602,18 @@ async function reloadPageAndAwaitComposer(page) {
|
|
|
1602
1602
|
cdp.close();
|
|
1603
1603
|
}
|
|
1604
1604
|
}
|
|
1605
|
+
/**
|
|
1606
|
+
* The project a ChatGPT URL belongs to, as its `g-p-<id>` segment.
|
|
1607
|
+
*
|
|
1608
|
+
* The rebind check used to compare whole URLs, which also carry mode in a
|
|
1609
|
+
* query string - and mode is exactly what differs across the reload this
|
|
1610
|
+
* failure showed up in. Identity is the thing the check actually cares about:
|
|
1611
|
+
* that the composer came back bound to THIS project rather than the one the
|
|
1612
|
+
* tab came from.
|
|
1613
|
+
*/
|
|
1614
|
+
export function projectIdentity(url) {
|
|
1615
|
+
return /\/g\/(g-p-[^/?#]+)/.exec(url)?.[1];
|
|
1616
|
+
}
|
|
1605
1617
|
export function powerSliderPresentExpression() {
|
|
1606
1618
|
return `Boolean(document.querySelector('[data-testid="composer-intelligence-picker-content"] [role="slider"]'))`;
|
|
1607
1619
|
}
|
|
@@ -2524,12 +2536,25 @@ async function selectProject(cdp, options) {
|
|
|
2524
2536
|
// prompt posted into the project the tab came from). A hard reload of the
|
|
2525
2537
|
// project home rebinds the composer to THIS project before we send.
|
|
2526
2538
|
const projectHome = await cdp.evaluate("location.href");
|
|
2539
|
+
const home = projectIdentity(projectHome);
|
|
2527
2540
|
// Polled with no delay, the first check could run before the reload had
|
|
2528
2541
|
// committed, on the old document, whose composer and URL both still
|
|
2529
2542
|
// matched. The stamp keeps that document from passing as the new one.
|
|
2530
|
-
|
|
2543
|
+
//
|
|
2544
|
+
// Identity, not the whole URL: the href also carries mode in a query
|
|
2545
|
+
// string, and comparing it byte-for-byte failed a reload that had landed
|
|
2546
|
+
// exactly where it was asked to.
|
|
2547
|
+
const rebound = await reloadAndAwaitComposer(cdp, RELOAD_SETTLE_TIMEOUT_MS, home ? `/\\/g\\/${home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![^/?#])/.test(location.href)` : "true");
|
|
2531
2548
|
if (!rebound) {
|
|
2532
|
-
|
|
2549
|
+
// Say what it saw. This failure is rare and was previously reported as
|
|
2550
|
+
// four words, which named neither the cause nor anything to try.
|
|
2551
|
+
const landed = await cdp.evaluate("location.href").catch(() => "unknown");
|
|
2552
|
+
const hasComposer = await cdp
|
|
2553
|
+
.evaluate(`Boolean(document.querySelector('#prompt-textarea,[contenteditable="true"],textarea'))`)
|
|
2554
|
+
.catch(() => false);
|
|
2555
|
+
throw new Error(`ChatGPT composer did not rebind after entering project "${options.project}": after reloading, the tab was on ` +
|
|
2556
|
+
`${projectIdentity(landed) ?? "no project"} (expected ${home ?? "that project"}) and the composer was ` +
|
|
2557
|
+
`${hasComposer ? "present but the page never settled" : "still missing"}.`);
|
|
2533
2558
|
}
|
|
2534
2559
|
}
|
|
2535
2560
|
const composerReady = await waitForExpressionTrue(cdp, `Boolean(document.querySelector('#prompt-textarea,[contenteditable="true"],textarea'))`, PROJECT_NAVIGATION_TIMEOUT_MS);
|
package/dist/cli-help.js
CHANGED
|
@@ -28,6 +28,7 @@ Ask / consult commands:
|
|
|
28
28
|
prodex pro browser recover [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # recover a finished answer from a thread whose send timed out
|
|
29
29
|
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
30
30
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
31
|
+
prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
|
|
31
32
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
32
33
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
33
34
|
prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
|
|
@@ -171,6 +172,7 @@ Commands:
|
|
|
171
172
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
172
173
|
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt"
|
|
173
174
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
175
|
+
prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
|
|
174
176
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
175
177
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
176
178
|
prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
|
package/dist/cli-pro.js
CHANGED
|
@@ -10,6 +10,8 @@ import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
|
|
|
10
10
|
import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatProLatestCommand, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, computeSendPacingWaitMs, isUntrustedResultError, resolveMinSendIntervalMs, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
|
|
11
11
|
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
|
|
12
12
|
import { withBrowserSendLock } from "./browser-send-lock.js";
|
|
13
|
+
import { blockerCause, buildBlockerReport } from "./blocker-report.js";
|
|
14
|
+
import { readBridgeRoots } from "./registry.js";
|
|
13
15
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
14
16
|
import { CLI_VERSION } from "./cli-help.js";
|
|
15
17
|
import { PRODEX_ISSUE_REPO, buildIssueReport, fileGitHubIssue } from "./issue-report.js";
|
|
@@ -732,6 +734,50 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
732
734
|
// a two-hop dead end that made an agent abandon the diagnosis.
|
|
733
735
|
throw new Error(`Use \`prodex pro browser ${legacyBrowserSubcommandReplacement(subcommand)}\` for explicit browser automation.`);
|
|
734
736
|
}
|
|
737
|
+
if (subcommand === "blockers") {
|
|
738
|
+
if (printHelpIfRequested(proArgs, "pro blockers", io.stdout, printProHelp, { valueFlags: ["--cwd", "--since", "--limit"] }))
|
|
739
|
+
return 0;
|
|
740
|
+
assertOnlyOptions(proArgs, "pro blockers", ["--cwd", "--since", "--limit"], ["--json"]);
|
|
741
|
+
const scopedToOneRepo = readFlag(proArgs, "--cwd") !== undefined;
|
|
742
|
+
// Default to every bridge root: the failures are spread across the repos
|
|
743
|
+
// consults were run from, and `pro list` already covers just this one.
|
|
744
|
+
const roots = scopedToOneRepo ? [resolveCwdFlag(io.cwd, proArgs)] : await readBridgeRoots();
|
|
745
|
+
const since = readSinceFlag(proArgs);
|
|
746
|
+
const limit = readPositiveIntegerFlag(proArgs, "--limit") ?? 10;
|
|
747
|
+
const consults = [];
|
|
748
|
+
let readable = 0;
|
|
749
|
+
for (const root of roots) {
|
|
750
|
+
try {
|
|
751
|
+
const results = await new BridgeStore(root).listResultsReadOnly();
|
|
752
|
+
readable += 1;
|
|
753
|
+
for (const result of results) {
|
|
754
|
+
if (!result.task_id.includes("gpt-pro-consult"))
|
|
755
|
+
continue;
|
|
756
|
+
consults.push({
|
|
757
|
+
repo: path.basename(root),
|
|
758
|
+
createdAt: result.created_at,
|
|
759
|
+
...(result.blocker ? { blocker: { code: result.blocker.code, message: result.blocker.message } } : {})
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// A root that has been deleted or is unreadable is not a failure of
|
|
765
|
+
// the report; it just has nothing to contribute.
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const report = buildBlockerReport({
|
|
769
|
+
consults,
|
|
770
|
+
roots: readable,
|
|
771
|
+
...(since ? { since } : {}),
|
|
772
|
+
limit
|
|
773
|
+
});
|
|
774
|
+
if (proArgs.includes("--json")) {
|
|
775
|
+
io.stdout(JSON.stringify(report, null, 2));
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
io.stdout(formatBlockerReport(report, { since, scopedToOneRepo }));
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
735
781
|
if (subcommand === "list") {
|
|
736
782
|
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
737
783
|
return 0;
|
|
@@ -879,7 +925,7 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
879
925
|
io.stdout(formatDebatePrompt({ topic: readFlag(proArgs, "--topic"), rounds, sourceCli }));
|
|
880
926
|
return 0;
|
|
881
927
|
}
|
|
882
|
-
throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "debate-prompt", "list", "latest", "show"]);
|
|
928
|
+
throw unknownSubcommandError("pro", subcommand, ["ask", "blockers", "browser", "debate-prompt", "list", "latest", "show"]);
|
|
883
929
|
}
|
|
884
930
|
export async function runConsultsCommand(rest, io) {
|
|
885
931
|
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
@@ -1064,6 +1110,12 @@ export async function runAskProCommand(rest, io) {
|
|
|
1064
1110
|
// Requiring --new-chat keeps that explicit rather than quietly turning a
|
|
1065
1111
|
// continuation into a throwaway.
|
|
1066
1112
|
const temporary = parsedAskPro.optionArgs.includes("--temporary");
|
|
1113
|
+
const temporaryConflict = temporaryProjectConflict({
|
|
1114
|
+
temporary,
|
|
1115
|
+
...(explicitProject !== undefined ? { explicitProject } : {})
|
|
1116
|
+
});
|
|
1117
|
+
if (temporaryConflict)
|
|
1118
|
+
throw new Error(temporaryConflict);
|
|
1067
1119
|
if (temporary && !newChat) {
|
|
1068
1120
|
throw new Error("--temporary starts a throwaway chat, so it needs --new-chat. A temporary chat cannot be continued or recovered later.");
|
|
1069
1121
|
}
|
|
@@ -1095,9 +1147,17 @@ export async function runAskProCommand(rest, io) {
|
|
|
1095
1147
|
// fresh chat inside the project is exactly what "--new-chat + project"
|
|
1096
1148
|
// produces, and the whole point of pinning a default project is that
|
|
1097
1149
|
// consults stop landing in the general chat list. Only --target-url
|
|
1098
|
-
// (pinned tab)
|
|
1150
|
+
// (pinned tab), --project-new, and --temporary suppress it.
|
|
1151
|
+
//
|
|
1152
|
+
// --temporary suppresses rather than conflicts: a pinned project must not
|
|
1153
|
+
// turn every throwaway send into an error, and attempting both is what
|
|
1154
|
+
// produced "composer did not rebind after entering project" - a temporary
|
|
1155
|
+
// chat is never saved, a project chat is, and entering a project leaves
|
|
1156
|
+
// temporary mode.
|
|
1099
1157
|
const selectionProject = explicitProject ??
|
|
1100
|
-
(normalizedTargetUrl || selectionProjectNew !== undefined || suppressProject
|
|
1158
|
+
(normalizedTargetUrl || selectionProjectNew !== undefined || suppressProject || temporary
|
|
1159
|
+
? undefined
|
|
1160
|
+
: browserDefaults?.project);
|
|
1101
1161
|
const reasoningAxisChosen = explicitProMode !== undefined || explicitEffort !== undefined;
|
|
1102
1162
|
const selectionProMode = explicitProMode ?? (reasoningAxisChosen ? undefined : browserDefaults?.pro_mode);
|
|
1103
1163
|
const selectionEffort = explicitEffort ?? (reasoningAxisChosen ? undefined : browserDefaults?.effort);
|
|
@@ -1750,6 +1810,19 @@ export function proSelectionVerified(selection) {
|
|
|
1750
1810
|
return undefined;
|
|
1751
1811
|
return /pro/i.test(selection.modelSlug);
|
|
1752
1812
|
}
|
|
1813
|
+
/**
|
|
1814
|
+
* A temporary chat is not saved; a project chat is. Entering a project leaves
|
|
1815
|
+
* temporary mode, so asking for both is asking for two different things, and
|
|
1816
|
+
* prodex used to attempt both and die inside the project step with "composer
|
|
1817
|
+
* did not rebind". A pinned default project is suppressed instead of refused:
|
|
1818
|
+
* the per-call flag is the more specific instruction.
|
|
1819
|
+
*/
|
|
1820
|
+
export function temporaryProjectConflict(input) {
|
|
1821
|
+
if (!input.temporary || input.explicitProject === undefined)
|
|
1822
|
+
return undefined;
|
|
1823
|
+
return (`--temporary and --project cannot be combined: a temporary chat is never saved, and a chat inside a project is. ` +
|
|
1824
|
+
`Drop --temporary to send into "${input.explicitProject}", or drop --project to send a throwaway chat.`);
|
|
1825
|
+
}
|
|
1753
1826
|
export function browserSendBlockerFromError(error) {
|
|
1754
1827
|
const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
|
|
1755
1828
|
if (typeof blocker === "object" &&
|
|
@@ -1934,6 +2007,63 @@ export function formatProConsultArtifact(consult) {
|
|
|
1934
2007
|
lines.push("## Answer", "", consult.answer.trim(), "");
|
|
1935
2008
|
return lines.join("\n");
|
|
1936
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* `--since 7d`, `--since 24h`, or a plain date. A window is how you ask whether
|
|
2012
|
+
* a fix landed, so it has to be quick to type.
|
|
2013
|
+
*/
|
|
2014
|
+
export function readSinceFlag(args) {
|
|
2015
|
+
const raw = readFlag(args, "--since");
|
|
2016
|
+
if (raw === undefined)
|
|
2017
|
+
return undefined;
|
|
2018
|
+
const relative = /^(\d+)\s*([dh])$/i.exec(raw.trim());
|
|
2019
|
+
if (relative) {
|
|
2020
|
+
const amount = Number(relative[1]);
|
|
2021
|
+
const hours = relative[2]?.toLowerCase() === "d" ? amount * 24 : amount;
|
|
2022
|
+
return new Date(Date.now() - hours * 3_600_000).toISOString();
|
|
2023
|
+
}
|
|
2024
|
+
const parsed = Date.parse(raw);
|
|
2025
|
+
if (!Number.isFinite(parsed)) {
|
|
2026
|
+
throw new Error(`--since expects a date or an age like 7d or 24h, not "${raw}".`);
|
|
2027
|
+
}
|
|
2028
|
+
return new Date(parsed).toISOString();
|
|
2029
|
+
}
|
|
2030
|
+
/** The ranked report as a table, widest column first so the counts line up. */
|
|
2031
|
+
export function formatBlockerReport(report, options = {}) {
|
|
2032
|
+
const scope = options.scopedToOneRepo ? "this repo" : `${report.roots} bridge root${report.roots === 1 ? "" : "s"}`;
|
|
2033
|
+
const window = options.since ? ` since ${options.since.slice(0, 10)}` : "";
|
|
2034
|
+
if (report.totalConsults === 0) {
|
|
2035
|
+
return `No consults recorded in ${scope}${window}.`;
|
|
2036
|
+
}
|
|
2037
|
+
const share = report.totalConsults > 0 ? Math.round((report.blocked / report.totalConsults) * 100) : 0;
|
|
2038
|
+
const head = `Consult blockers: ${report.blocked} of ${report.totalConsults} consults (${share}%) across ${scope}${window}`;
|
|
2039
|
+
if (report.groups.length === 0)
|
|
2040
|
+
return `${head}\n\nNothing was blocked.`;
|
|
2041
|
+
// A split catch-all carries the first sentence in its own name, so the
|
|
2042
|
+
// example would repeat it; only a bare code needs one.
|
|
2043
|
+
const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}\u2026` : text);
|
|
2044
|
+
const rows = report.groups.map((group) => {
|
|
2045
|
+
const example = group.example.replace(/\s+/g, " ").trim();
|
|
2046
|
+
// Compare through the same normalisation the cause went through: the code
|
|
2047
|
+
// says `has no "..." step` where the message says `has no "Pro" step`.
|
|
2048
|
+
const causePart = group.code.includes(": ") ? group.code.split(": ").slice(1).join(": ") : undefined;
|
|
2049
|
+
const redundant = causePart !== undefined && blockerCause(example) === causePart;
|
|
2050
|
+
return {
|
|
2051
|
+
count: String(group.count),
|
|
2052
|
+
share: `${Math.round(group.share * 100)}%`,
|
|
2053
|
+
code: clip(group.code, 58),
|
|
2054
|
+
lastSeen: group.lastSeen ? group.lastSeen.slice(0, 10) : "-",
|
|
2055
|
+
example: redundant ? "" : clip(example, 58)
|
|
2056
|
+
};
|
|
2057
|
+
});
|
|
2058
|
+
const width = (pick, header) => Math.max(header.length, ...rows.map((row) => pick(row).length));
|
|
2059
|
+
const wCount = width((r) => r.count, "COUNT");
|
|
2060
|
+
const wShare = width((r) => r.share, "SHARE");
|
|
2061
|
+
const wCode = width((r) => r.code, "CAUSE");
|
|
2062
|
+
const wLast = width((r) => r.lastSeen, "LAST SEEN");
|
|
2063
|
+
const line = (count, sharePct, code, lastSeen, example) => ` ${count.padStart(wCount)} ${sharePct.padStart(wShare)} ${code.padEnd(wCode)} ${lastSeen.padEnd(wLast)} ${example}`;
|
|
2064
|
+
const body = rows.map((row) => line(row.count, row.share, row.code, row.lastSeen, row.example).trimEnd());
|
|
2065
|
+
return [head, "", line("COUNT", "SHARE", "CAUSE", "LAST SEEN", "EXAMPLE"), ...body].join("\n");
|
|
2066
|
+
}
|
|
1937
2067
|
export function formatProListSummary(consult, sourceCli, options = {}) {
|
|
1938
2068
|
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
1939
2069
|
return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
|
package/dist/registry.js
CHANGED
|
@@ -44,6 +44,38 @@ async function directoryExists(dir) {
|
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Every bridge root this machine knows about. The registry has only ever been
|
|
49
|
+
* written; reading it is what lets a report span the repos where the failures
|
|
50
|
+
* actually are, instead of the one the command happens to run in.
|
|
51
|
+
*
|
|
52
|
+
* Advisory like the rest of the registry: a missing or corrupt file is an
|
|
53
|
+
* empty list, not an error, and roots that have since been deleted are
|
|
54
|
+
* dropped rather than reported.
|
|
55
|
+
*/
|
|
56
|
+
export async function readBridgeRoots() {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(await fs.readFile(bridgesRegistryPath(), "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const raw = parsed?.roots;
|
|
65
|
+
if (!Array.isArray(raw))
|
|
66
|
+
return [];
|
|
67
|
+
const roots = [];
|
|
68
|
+
for (const entry of raw.slice(0, MAX_REGISTRY_ROOTS)) {
|
|
69
|
+
const value = typeof entry === "string" ? entry : entry?.path;
|
|
70
|
+
if (typeof value !== "string" || value.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
if (!(await directoryExists(path.join(value, ".bridge"))))
|
|
73
|
+
continue;
|
|
74
|
+
if (!roots.includes(value))
|
|
75
|
+
roots.push(value);
|
|
76
|
+
}
|
|
77
|
+
return roots;
|
|
78
|
+
}
|
|
47
79
|
export function registerBridgeRoot(root) {
|
|
48
80
|
const next = registryQueue.then(() => registerBridgeRootInner(root));
|
|
49
81
|
// Keep the chain alive even if an inner registration rejects.
|