@indigoai-us/hq-cli 5.162.0 → 5.164.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/CHANGELOG.md +26 -0
- package/dist/command-catalog.generated.d.ts +188 -0
- package/dist/command-catalog.generated.js +235 -0
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +6 -1
- package/dist/commands/agent-enroll.js +30 -3
- package/dist/commands/agents.js +15 -22
- package/dist/commands/bot.js +56 -6
- package/dist/commands/daemon.d.ts +53 -0
- package/dist/commands/daemon.js +554 -0
- package/dist/commands/lanes.js +7 -0
- package/dist/commands/meetings.js +18 -16
- package/dist/commands/mesh.js +9 -0
- package/dist/lib/daemon/catalog.d.ts +85 -0
- package/dist/lib/daemon/catalog.js +274 -0
- package/dist/lib/daemon/config.d.ts +26 -0
- package/dist/lib/daemon/config.js +85 -0
- package/dist/lib/daemon/handoff.d.ts +53 -0
- package/dist/lib/daemon/handoff.js +136 -0
- package/dist/lib/daemon/install.d.ts +76 -0
- package/dist/lib/daemon/install.js +299 -0
- package/dist/lib/daemon/legacy.d.ts +35 -0
- package/dist/lib/daemon/legacy.js +120 -0
- package/dist/lib/daemon/lock.d.ts +47 -0
- package/dist/lib/daemon/lock.js +138 -0
- package/dist/lib/daemon/machine-type.d.ts +48 -0
- package/dist/lib/daemon/machine-type.js +139 -0
- package/dist/lib/daemon/migrate.d.ts +61 -0
- package/dist/lib/daemon/migrate.js +238 -0
- package/dist/lib/daemon/paths.d.ts +22 -0
- package/dist/lib/daemon/paths.js +42 -0
- package/dist/lib/daemon/remote-control.d.ts +35 -0
- package/dist/lib/daemon/remote-control.js +47 -0
- package/dist/lib/daemon/requests.d.ts +24 -0
- package/dist/lib/daemon/requests.js +63 -0
- package/dist/lib/daemon/run.d.ts +41 -0
- package/dist/lib/daemon/run.js +319 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +523 -0
- package/dist/lib/daemon/updater.d.ts +39 -0
- package/dist/lib/daemon/updater.js +103 -0
- package/dist/lib/flag-registry.d.ts +7 -1
- package/dist/lib/flag-registry.js +8 -0
- package/dist/lib/mesh/live/backoff.d.ts +2 -2
- package/dist/lib/mesh/live/backoff.js +16 -3
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/run.js +5 -2
- package/dist/lib/mesh/live/flush.d.ts +3 -2
- package/dist/lib/mesh/live/flush.js +34 -7
- package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
- package/dist/lib/mesh/live/session-events-client.js +25 -0
- package/dist/lib/service-manager/systemd.d.ts +2 -2
- package/dist/lib/service-manager/systemd.js +1 -1
- package/dist/lib/service-manager/types.d.ts +2 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +53 -13
- package/dist/sentry.js +5 -1
- package/dist/startup-registration.d.ts +3 -1
- package/dist/startup-registration.js +25 -4
- package/dist/utils/cognito-session.d.ts +4 -0
- package/dist/utils/cognito-session.js +11 -10
- package/dist/utils/install-tree-torn.d.ts +29 -10
- package/dist/utils/install-tree-torn.js +262 -29
- package/dist/utils/plan-gate-error.d.ts +27 -13
- package/dist/utils/plan-gate-error.js +133 -58
- package/dist/utils/self-update.d.ts +9 -0
- package/dist/utils/self-update.js +15 -2
- package/dist/utils/sentry-fingerprint.d.ts +7 -1
- package/dist/utils/sentry-fingerprint.js +95 -15
- package/package.json +1 -1
|
@@ -4,19 +4,74 @@
|
|
|
4
4
|
* message instead of every command parsing and printing a server response.
|
|
5
5
|
*/
|
|
6
6
|
import { renderPersonalPlanFeatureNotice } from "../lib/plan-limit-nag.js";
|
|
7
|
-
|
|
7
|
+
function planGateReason(code, details) {
|
|
8
|
+
if (details.reason?.trim())
|
|
9
|
+
return details.reason.trim();
|
|
10
|
+
if (code === "PLAN_LIMIT_EXCEEDED" &&
|
|
11
|
+
typeof details.resource === "string" &&
|
|
12
|
+
typeof details.used === "number" &&
|
|
13
|
+
typeof details.limit === "number") {
|
|
14
|
+
return `Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`;
|
|
15
|
+
}
|
|
16
|
+
if (code === "PERSONAL_PLAN_REQUIRED") {
|
|
17
|
+
return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", null);
|
|
18
|
+
}
|
|
19
|
+
if (code === "MEETING_PLAN_REQUIRED") {
|
|
20
|
+
return "HQ Workforce plan required to invite a meeting bot.";
|
|
21
|
+
}
|
|
22
|
+
if (code === "BILLING_REQUIRED") {
|
|
23
|
+
return "Upgrade the company plan before enrolling an external agent.";
|
|
24
|
+
}
|
|
25
|
+
return "HQ Workforce plan required for this feature.";
|
|
26
|
+
}
|
|
27
|
+
/** One renderer for server-selected upgrade links across CLI plan refusals. */
|
|
28
|
+
export function formatUpgradeRefusal(reason, upgradeUrl, options = {}) {
|
|
29
|
+
const safeUrl = safeUpgradeUrl(upgradeUrl);
|
|
30
|
+
if (options.urlOnly && safeUrl)
|
|
31
|
+
return safeUrl;
|
|
32
|
+
if (!safeUrl)
|
|
33
|
+
return reason;
|
|
34
|
+
return `${reason}\nUpgrade: ${safeUrl}`;
|
|
35
|
+
}
|
|
36
|
+
function safeUpgradeUrl(value) {
|
|
37
|
+
if (typeof value !== "string" ||
|
|
38
|
+
value.trim() !== value ||
|
|
39
|
+
!/^https:\/\//i.test(value)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value);
|
|
44
|
+
if (url.protocol !== "https:" ||
|
|
45
|
+
!url.hostname ||
|
|
46
|
+
url.username ||
|
|
47
|
+
url.password) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** Recover a company UID only from the server's UID-keyed billing callback. */
|
|
57
|
+
export function companyUidFromPlanUpgradeUrl(value) {
|
|
58
|
+
const safeUrl = safeUpgradeUrl(value);
|
|
59
|
+
if (!safeUrl)
|
|
60
|
+
return undefined;
|
|
61
|
+
const callbackUrl = new URL(safeUrl).searchParams.get("callbackUrl");
|
|
62
|
+
if (!callbackUrl)
|
|
63
|
+
return undefined;
|
|
64
|
+
const match = /^\/api\/companies\/(cmp_[A-Za-z0-9_-]+)\/billing\/upgrade\/?$/.exec(callbackUrl);
|
|
65
|
+
return match?.[1];
|
|
66
|
+
}
|
|
67
|
+
function formatPlanGateErrorText(code, details) {
|
|
68
|
+
return formatUpgradeRefusal(planGateReason(code, details), details.upgradeUrl);
|
|
69
|
+
}
|
|
8
70
|
export class PlanGateError extends Error {
|
|
9
71
|
code;
|
|
10
72
|
details;
|
|
11
73
|
constructor(code, details) {
|
|
12
|
-
|
|
13
|
-
// `err.message`. Keeping the friendly copy here means those boundaries
|
|
14
|
-
// retain the same plan-gate voice as main.ts without per-command handling.
|
|
15
|
-
// The HQ Workforce hint is company advice; a personal denial ends at its
|
|
16
|
-
// own notice, which already names the Individual plan and its checkout.
|
|
17
|
-
super(code === "PERSONAL_PLAN_REQUIRED"
|
|
18
|
-
? formatPlanGateDetails(code, details)
|
|
19
|
-
: [formatPlanGateDetails(code, details), TEAM_UPGRADE_HINT].join("\n"));
|
|
74
|
+
super(formatPlanGateErrorText(code, details));
|
|
20
75
|
this.code = code;
|
|
21
76
|
this.details = details;
|
|
22
77
|
this.name = "PlanGateError";
|
|
@@ -26,56 +81,52 @@ export function isPlanGateError(err) {
|
|
|
26
81
|
return err instanceof PlanGateError;
|
|
27
82
|
}
|
|
28
83
|
/** Plain, stable CLI copy for plan-gated resource creation. */
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
if (code === "PLAN_LIMIT_EXCEEDED" &&
|
|
32
|
-
typeof details.resource === "string" &&
|
|
33
|
-
typeof details.used === "number" &&
|
|
34
|
-
typeof details.limit === "number") {
|
|
35
|
-
const upgrade = typeof details.upgradeUrl === "string"
|
|
36
|
-
? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
|
|
37
|
-
: "Upgrade to HQ Workforce ($500/mo) to remove limits.";
|
|
38
|
-
return [
|
|
39
|
-
`Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`,
|
|
40
|
-
upgrade,
|
|
41
|
-
existingResourcesNote,
|
|
42
|
-
].join("\n");
|
|
43
|
-
}
|
|
44
|
-
// A personal scope is not a workspace: it has no members to trim, no $500
|
|
45
|
-
// tier to buy, and nothing of its own is paused. One line, the plan's own
|
|
46
|
-
// copy, and no company remedy.
|
|
47
|
-
if (code === "PERSONAL_PLAN_REQUIRED") {
|
|
48
|
-
return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", details.upgradeUrl ?? null);
|
|
49
|
-
}
|
|
50
|
-
const upgrade = typeof details.upgradeUrl === "string"
|
|
51
|
-
? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
|
|
52
|
-
: "Upgrade to HQ Workforce ($500/mo) to remove limits.";
|
|
53
|
-
return [
|
|
54
|
-
"HQ Workforce plan required for this feature.",
|
|
55
|
-
upgrade,
|
|
56
|
-
existingResourcesNote,
|
|
57
|
-
].join("\n");
|
|
58
|
-
}
|
|
59
|
-
export function formatPlanGateError(err) {
|
|
60
|
-
if (err.code === "PERSONAL_PLAN_REQUIRED") {
|
|
61
|
-
return formatPlanGateDetails(err.code, err.details);
|
|
62
|
-
}
|
|
63
|
-
return [
|
|
64
|
-
formatPlanGateDetails(err.code, err.details),
|
|
65
|
-
TEAM_UPGRADE_HINT,
|
|
66
|
-
].join("\n");
|
|
84
|
+
export function formatPlanGateError(err, options = {}) {
|
|
85
|
+
return formatUpgradeRefusal(planGateReason(err.code, err.details), err.details.upgradeUrl, options);
|
|
67
86
|
}
|
|
68
87
|
export function canOfferTeamUpgrade(argv = process.argv) {
|
|
69
88
|
return Boolean(process.stdin.isTTY && process.stderr.isTTY) &&
|
|
70
89
|
!process.env.CI &&
|
|
71
90
|
!argv.includes("--json");
|
|
72
91
|
}
|
|
73
|
-
/** Ask
|
|
92
|
+
/** Ask before opening the exact URL returned by the server. */
|
|
93
|
+
export async function offerUpgradeLink(url, deps = {}) {
|
|
94
|
+
const safeUrl = safeUpgradeUrl(url);
|
|
95
|
+
if (!safeUrl)
|
|
96
|
+
return false;
|
|
97
|
+
if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
|
|
98
|
+
return false;
|
|
99
|
+
let answer;
|
|
100
|
+
if (deps.ask) {
|
|
101
|
+
answer = await deps.ask("Open upgrade link now? [y/N] ");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const { createInterface } = await import("node:readline/promises");
|
|
105
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
106
|
+
try {
|
|
107
|
+
answer = await readline.question("Open upgrade link now? [y/N] ");
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
readline.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
114
|
+
return false;
|
|
115
|
+
if (deps.openUrl) {
|
|
116
|
+
await deps.openUrl(safeUrl);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const { default: open } = await import("open");
|
|
120
|
+
await open(safeUrl);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/** Legacy checkout callback retained for callers outside the shared refusal paths. */
|
|
74
125
|
export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
75
126
|
if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
|
|
76
127
|
return false;
|
|
77
128
|
if (deps.ask) {
|
|
78
|
-
const answer = await deps.ask("Upgrade
|
|
129
|
+
const answer = await deps.ask("Upgrade now? (opens checkout) [y/N] ");
|
|
79
130
|
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
80
131
|
return false;
|
|
81
132
|
await onConfirm();
|
|
@@ -84,7 +135,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
|
84
135
|
const { createInterface } = await import("node:readline/promises");
|
|
85
136
|
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
86
137
|
try {
|
|
87
|
-
const answer = await readline.question("Upgrade
|
|
138
|
+
const answer = await readline.question("Upgrade now? (opens checkout) [y/N] ");
|
|
88
139
|
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
89
140
|
return false;
|
|
90
141
|
await onConfirm();
|
|
@@ -106,28 +157,52 @@ export function planGateErrorFromPayload(status, body, companyUid) {
|
|
|
106
157
|
// through to the company path below (defensive consumer).
|
|
107
158
|
if (payload.error === "plan_feature_unavailable" &&
|
|
108
159
|
payload.scope === "personal") {
|
|
160
|
+
const upgradeUrl = safeUpgradeUrl(payload.upgradeUrl);
|
|
109
161
|
return new PlanGateError("PERSONAL_PLAN_REQUIRED", {
|
|
110
162
|
...(typeof payload.feature === "string"
|
|
111
163
|
? { feature: payload.feature }
|
|
112
164
|
: {}),
|
|
113
|
-
...(
|
|
114
|
-
? { upgradeUrl: payload.upgradeUrl }
|
|
115
|
-
: {}),
|
|
165
|
+
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
116
166
|
});
|
|
117
167
|
}
|
|
118
|
-
|
|
168
|
+
const code = payload.code;
|
|
169
|
+
const upgradeUrl = safeUpgradeUrl(payload.upgradeUrl);
|
|
170
|
+
const targetCompanyUid = companyUid ?? companyUidFromPlanUpgradeUrl(upgradeUrl);
|
|
171
|
+
const planLimit = payload.planLimit !== null &&
|
|
172
|
+
typeof payload.planLimit === "object" &&
|
|
173
|
+
!Array.isArray(payload.planLimit)
|
|
174
|
+
? payload.planLimit
|
|
175
|
+
: undefined;
|
|
176
|
+
const isTeamPlanBillingRequired = code === "BILLING_REQUIRED" && planLimit?.requiredPlan === "team";
|
|
177
|
+
if (code === "BILLING_REQUIRED") {
|
|
178
|
+
if (!isTeamPlanBillingRequired ||
|
|
179
|
+
!targetCompanyUid ||
|
|
180
|
+
!/^cmp_[A-Za-z0-9_-]+$/.test(targetCompanyUid)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (code !== "PLAN_LIMIT_EXCEEDED" &&
|
|
185
|
+
code !== "PLAN_REQUIRED" &&
|
|
186
|
+
code !== "MEETING_PLAN_REQUIRED") {
|
|
119
187
|
return null;
|
|
120
188
|
}
|
|
121
|
-
|
|
189
|
+
const reason = typeof payload.message === "string"
|
|
190
|
+
? payload.message
|
|
191
|
+
: typeof payload.error === "string" && payload.error !== payload.code
|
|
192
|
+
? payload.error
|
|
193
|
+
: undefined;
|
|
194
|
+
return new PlanGateError(code, {
|
|
122
195
|
...(typeof payload.resource === "string" ? { resource: payload.resource } : {}),
|
|
196
|
+
...(typeof payload.feature === "string" ? { feature: payload.feature } : {}),
|
|
123
197
|
...(typeof payload.used === "number" ? { used: payload.used } : {}),
|
|
124
198
|
...(typeof payload.limit === "number" ? { limit: payload.limit } : {}),
|
|
125
|
-
...(
|
|
126
|
-
...(
|
|
199
|
+
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
200
|
+
...(reason ? { reason } : {}),
|
|
201
|
+
...(targetCompanyUid ? { companyUid: targetCompanyUid } : {}),
|
|
127
202
|
});
|
|
128
203
|
}
|
|
129
204
|
/**
|
|
130
|
-
* Decode
|
|
205
|
+
* Decode the supported plan-gate envelopes. Malformed, unrelated,
|
|
131
206
|
* or non-402 responses retain their existing command-specific handling.
|
|
132
207
|
*/
|
|
133
208
|
export async function planGateErrorFromResponse(response, companyUid) {
|
|
@@ -83,6 +83,8 @@ export type SelfUpdateAction =
|
|
|
83
83
|
| "update-ineffective"
|
|
84
84
|
/** Updated and the command re-ran on the new version; exit with `reexecStatus`. */
|
|
85
85
|
| "reexec"
|
|
86
|
+
/** Updated with no re-exec (the `hq daemon` updater restarts the daemon instead). */
|
|
87
|
+
| "updated"
|
|
86
88
|
/**
|
|
87
89
|
* Unattended caller: replacing the CLI in place is unsafe here, so nothing
|
|
88
90
|
* was attempted and the command runs on the current version. An attended
|
|
@@ -171,4 +173,11 @@ export declare function autoUpdateAndReexec(argv: readonly string[], latest: str
|
|
|
171
173
|
* are preserved.
|
|
172
174
|
*/
|
|
173
175
|
export declare function selfUpdateAndReexec(argv: readonly string[], deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
|
|
176
|
+
/**
|
|
177
|
+
* `hq daemon` updater service: update to npm latest with nobody at a terminal
|
|
178
|
+
* and no re-exec. It runs as its own short-lived child of the daemon, so no
|
|
179
|
+
* user command is waiting on the package being replaced; after an "updated"
|
|
180
|
+
* outcome the daemon restarts itself onto the new version.
|
|
181
|
+
*/
|
|
182
|
+
export declare function daemonSelfUpdate(deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
|
|
174
183
|
//# sourceMappingURL=self-update.d.ts.map
|
|
@@ -292,6 +292,10 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
292
292
|
catch {
|
|
293
293
|
// Defensive only; the default helper already absorbs manager failures.
|
|
294
294
|
}
|
|
295
|
+
if (!flavor.reexec) {
|
|
296
|
+
console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
|
|
297
|
+
return { action: "updated", latest };
|
|
298
|
+
}
|
|
295
299
|
const childEnv = { ...env, [REEXEC_GUARD_ENV]: "1" };
|
|
296
300
|
const status = (deps.reexec ?? reexecHq)([...argv.slice(2)], childEnv);
|
|
297
301
|
if (status === null) {
|
|
@@ -309,7 +313,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
|
|
|
309
313
|
* this resolve it from the registry.
|
|
310
314
|
*/
|
|
311
315
|
export async function autoUpdateAndReexec(argv, latest, deps = {}) {
|
|
312
|
-
return updateAndReexec(argv, { noun: "command", verbose: false, lock: true, onlyWhenAttended: true }, latest, deps);
|
|
316
|
+
return updateAndReexec(argv, { noun: "command", verbose: false, lock: true, onlyWhenAttended: true, reexec: true }, latest, deps);
|
|
313
317
|
}
|
|
314
318
|
/**
|
|
315
319
|
* Rescue path: update to npm latest and re-exec the rescue on the new version.
|
|
@@ -318,6 +322,15 @@ export async function autoUpdateAndReexec(argv, latest, deps = {}) {
|
|
|
318
322
|
* are preserved.
|
|
319
323
|
*/
|
|
320
324
|
export async function selfUpdateAndReexec(argv, deps = {}) {
|
|
321
|
-
return updateAndReexec(argv, { noun: "rescue", verbose: true, lock: true, onlyWhenAttended: false }, null, deps);
|
|
325
|
+
return updateAndReexec(argv, { noun: "rescue", verbose: true, lock: true, onlyWhenAttended: false, reexec: true }, null, deps);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `hq daemon` updater service: update to npm latest with nobody at a terminal
|
|
329
|
+
* and no re-exec. It runs as its own short-lived child of the daemon, so no
|
|
330
|
+
* user command is waiting on the package being replaced; after an "updated"
|
|
331
|
+
* outcome the daemon restarts itself onto the new version.
|
|
332
|
+
*/
|
|
333
|
+
export async function daemonSelfUpdate(deps = {}) {
|
|
334
|
+
return updateAndReexec([], { noun: "daemon update", verbose: false, lock: true, onlyWhenAttended: false, reexec: false }, null, deps);
|
|
322
335
|
}
|
|
323
336
|
//# sourceMappingURL=self-update.js.map
|
|
@@ -9,6 +9,12 @@ export interface SentryMcpCollisionContext {
|
|
|
9
9
|
}
|
|
10
10
|
/** Return bounded, value-free Sentry context for an instrumented Codex name collision. */
|
|
11
11
|
export declare function sentryMcpCollisionContextFor(err: unknown): SentryMcpCollisionContext | null;
|
|
12
|
+
/**
|
|
13
|
+
* Bounded, aggregatable tags for an unrecovered install-tree failure. Phase,
|
|
14
|
+
* outcome, state, and OS are closed values. Paths and raw lock-tool strings stay
|
|
15
|
+
* in context; only a known updater source becomes a tag.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installTreeTornSentryTagsFor(err: unknown): Record<string, string> | null;
|
|
12
18
|
/**
|
|
13
19
|
* Return a bounded `event.fingerprint` array for `err`, or `null` when `err`
|
|
14
20
|
* carries no discriminator from the closed allowlist (in which case the caller
|
|
@@ -23,7 +29,7 @@ export declare function sentryMcpCollisionContextFor(err: unknown): SentryMcpCol
|
|
|
23
29
|
* The InstallTreeTornError branch is the deliberate exception: it returns a
|
|
24
30
|
* fully explicit `["InstallTreeTornError", "outcome:<token>"]` with NO
|
|
25
31
|
* DEFAULT_GROUPING lead, because its chained loader cause makes the default
|
|
26
|
-
* component unbounded
|
|
32
|
+
* component unbounded.
|
|
27
33
|
*
|
|
28
34
|
* `rpcCode` is checked first so a gateway JSON-RPC fault groups by its protocol
|
|
29
35
|
* code; the two carriers are disjoint in practice (the JSON-RPC throw site sets
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
// in three different groups (hashes a4289a5e530e166ebdedea9d34dcad0a,
|
|
39
39
|
// b563c1b891bd975bdc605efc86034b44, b4ee254473ea249758617de1a98848fa —
|
|
40
40
|
// HQ-CLI-21/22/23). So that branch returns a FULLY EXPLICIT key with no
|
|
41
|
-
// default lead: ["InstallTreeTornError", "outcome:<token>"].
|
|
41
|
+
// default lead: ["InstallTreeTornError", "outcome:<token>"]. This PR keeps
|
|
42
|
+
// the existing explicit key so the new stale-tree outcome extends the current
|
|
43
|
+
// groups without splitting them.
|
|
42
44
|
//
|
|
43
45
|
// Genuinely different failure classes therefore form distinct, PREDICTABLE
|
|
44
46
|
// groups, and everything with no bounded discriminator is left to default
|
|
@@ -156,17 +158,96 @@ const KNOWN_ERROR_NAMES = new Set([
|
|
|
156
158
|
"QmdWorkdirMissingError",
|
|
157
159
|
// A torn-install failure the bounded settle-wait + single re-exec could not
|
|
158
160
|
// recover (HQ-CLI-1G/1H/1J/1K, HQ-CLI-1Y). Fingerprinted on its own branch below
|
|
159
|
-
// by the
|
|
161
|
+
// by the normalized recovery result, so the family groups into two stable buckets.
|
|
160
162
|
"InstallTreeTornError",
|
|
161
163
|
]);
|
|
162
164
|
/**
|
|
163
|
-
* The
|
|
164
|
-
*
|
|
165
|
-
* the carrier's message is fixed, so this outcome IS the only discriminator.
|
|
165
|
+
* The closed loader dialects accepted for the install_tree_phase tag. Anything
|
|
166
|
+
* outside this set collapses to unknown, so tags stay safe to aggregate.
|
|
166
167
|
*/
|
|
168
|
+
const INSTALL_TREE_PHASES = new Set([
|
|
169
|
+
"cjs-enoent",
|
|
170
|
+
"cjs-package",
|
|
171
|
+
"cjs-path",
|
|
172
|
+
"cjs-relative",
|
|
173
|
+
"esm-enoent",
|
|
174
|
+
"esm-package",
|
|
175
|
+
"esm-path",
|
|
176
|
+
"eval-throw",
|
|
177
|
+
"resolve-enoent",
|
|
178
|
+
"unknown",
|
|
179
|
+
]);
|
|
180
|
+
const KNOWN_INSTALL_TREE_UPDATERS = new Map([
|
|
181
|
+
["hq-cli-self-update", "cli-self-update"],
|
|
182
|
+
["hq-cli-update", "box-timer"],
|
|
183
|
+
["hq-cli-version-gate", "cli-version-gate"],
|
|
184
|
+
["hq-desktop-app-cli-update", "desktop-app"],
|
|
185
|
+
]);
|
|
167
186
|
const KNOWN_INSTALL_TREE_OUTCOMES = new Set([
|
|
168
|
-
"guarded",
|
|
187
|
+
"guarded",
|
|
188
|
+
"unsettled",
|
|
189
|
+
"reexec-failed",
|
|
190
|
+
"lock-held",
|
|
191
|
+
"stale_tree",
|
|
169
192
|
]);
|
|
193
|
+
function installTreeRecoveryOutcomeTag(value) {
|
|
194
|
+
return typeof value === "string" && KNOWN_INSTALL_TREE_OUTCOMES.has(value)
|
|
195
|
+
? value
|
|
196
|
+
: null;
|
|
197
|
+
}
|
|
198
|
+
function installTreeStateTag(value) {
|
|
199
|
+
return value === "stale" ? "stale" : "in_flight";
|
|
200
|
+
}
|
|
201
|
+
function installTreePhaseTag(value) {
|
|
202
|
+
if (typeof value !== "string" || !INSTALL_TREE_PHASES.has(value))
|
|
203
|
+
return "unknown";
|
|
204
|
+
if (value === "esm-enoent" || value === "cjs-enoent")
|
|
205
|
+
return "loader_read";
|
|
206
|
+
if (value === "eval-throw")
|
|
207
|
+
return "module_evaluation";
|
|
208
|
+
if (value === "esm-package" || value === "cjs-package")
|
|
209
|
+
return "package_resolution";
|
|
210
|
+
if (value === "unknown")
|
|
211
|
+
return "unknown";
|
|
212
|
+
return "path_resolution";
|
|
213
|
+
}
|
|
214
|
+
function installTreeOsTag(platform) {
|
|
215
|
+
if (platform === "win32")
|
|
216
|
+
return "windows";
|
|
217
|
+
if (platform === "darwin")
|
|
218
|
+
return "macos";
|
|
219
|
+
if (platform === "linux")
|
|
220
|
+
return "linux";
|
|
221
|
+
return "other";
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Bounded, aggregatable tags for an unrecovered install-tree failure. Phase,
|
|
225
|
+
* outcome, state, and OS are closed values. Paths and raw lock-tool strings stay
|
|
226
|
+
* in context; only a known updater source becomes a tag.
|
|
227
|
+
*/
|
|
228
|
+
export function installTreeTornSentryTagsFor(err) {
|
|
229
|
+
if (err === null || typeof err !== "object")
|
|
230
|
+
return null;
|
|
231
|
+
const record = err;
|
|
232
|
+
if (record.name !== "InstallTreeTornError")
|
|
233
|
+
return null;
|
|
234
|
+
const diagnostics = record.diagnostics !== null && typeof record.diagnostics === "object"
|
|
235
|
+
? record.diagnostics
|
|
236
|
+
: {};
|
|
237
|
+
const recoveryOutcome = installTreeRecoveryOutcomeTag(diagnostics.outcome);
|
|
238
|
+
const tags = {
|
|
239
|
+
install_tree_phase: installTreePhaseTag(diagnostics.dialect),
|
|
240
|
+
tree_state: installTreeStateTag(diagnostics.treeState),
|
|
241
|
+
os: installTreeOsTag(process.platform),
|
|
242
|
+
};
|
|
243
|
+
if (recoveryOutcome)
|
|
244
|
+
tags.recovery_outcome = recoveryOutcome;
|
|
245
|
+
const lockTool = diagnostics.lockTool;
|
|
246
|
+
const updaterSource = typeof lockTool === "string" ? KNOWN_INSTALL_TREE_UPDATERS.get(lockTool) : undefined;
|
|
247
|
+
if (updaterSource)
|
|
248
|
+
tags.updater_source = updaterSource;
|
|
249
|
+
return tags;
|
|
250
|
+
}
|
|
170
251
|
/** Fixed bucket for any error name outside the closed allowlist. */
|
|
171
252
|
const FALLBACK_ERROR_NAME = "other";
|
|
172
253
|
/**
|
|
@@ -241,7 +322,7 @@ function qmdDispositionToken(status, signal) {
|
|
|
241
322
|
* The InstallTreeTornError branch is the deliberate exception: it returns a
|
|
242
323
|
* fully explicit `["InstallTreeTornError", "outcome:<token>"]` with NO
|
|
243
324
|
* DEFAULT_GROUPING lead, because its chained loader cause makes the default
|
|
244
|
-
* component unbounded
|
|
325
|
+
* component unbounded.
|
|
245
326
|
*
|
|
246
327
|
* `rpcCode` is checked first so a gateway JSON-RPC fault groups by its protocol
|
|
247
328
|
* code; the two carriers are disjoint in practice (the JSON-RPC throw site sets
|
|
@@ -284,10 +365,9 @@ export function sentryFingerprintFor(err) {
|
|
|
284
365
|
];
|
|
285
366
|
}
|
|
286
367
|
// A torn-install failure carries neither an rpcCode nor an HTTP status; its
|
|
287
|
-
// only bounded discriminator is the recovery
|
|
368
|
+
// only bounded discriminator is the existing recovery outcome. Keyed
|
|
288
369
|
// BEFORE the rpc/status branches so the family groups into a bounded set of
|
|
289
|
-
// predictable issues
|
|
290
|
-
// `outcome:other` fallback) rather than one fungible bucket.
|
|
370
|
+
// predictable issues rather than one fungible bucket.
|
|
291
371
|
//
|
|
292
372
|
// The key is FULLY EXPLICIT — it does NOT lead with DEFAULT_GROUPING. Leading
|
|
293
373
|
// with the placeholder salts the appended tokens with Sentry's default grouping
|
|
@@ -300,16 +380,16 @@ export function sentryFingerprintFor(err) {
|
|
|
300
380
|
// a4289a5e530e166ebdedea9d34dcad0a, b563c1b891bd975bdc605efc86034b44 and
|
|
301
381
|
// b4ee254473ea249758617de1a98848fa (HQ-CLI-21/22/23) — defeating the very bound
|
|
302
382
|
// this branch exists to enforce. The explicit key collapses them to one stable
|
|
303
|
-
// group per outcome, independent of machine, install path and
|
|
383
|
+
// group per recovery outcome, independent of machine, install path and module.
|
|
304
384
|
if (rawName === "InstallTreeTornError") {
|
|
305
385
|
const diagnostics = record.diagnostics;
|
|
306
386
|
const outcome = diagnostics !== null && typeof diagnostics === "object"
|
|
307
387
|
? diagnostics.outcome
|
|
308
388
|
: undefined;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
389
|
+
return [
|
|
390
|
+
"InstallTreeTornError",
|
|
391
|
+
`outcome:${installTreeRecoveryOutcomeTag(outcome) ?? "other"}`,
|
|
392
|
+
];
|
|
313
393
|
}
|
|
314
394
|
const name = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
|
|
315
395
|
const rpcCode = record.rpcCode;
|