@indigoai-us/hq-cli 5.161.0 → 5.163.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 +27 -2
- 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 +10 -1
- 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/lanes/delivery.d.ts +5 -3
- package/dist/lib/lanes/delivery.js +14 -6
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +25 -3
- package/dist/lib/lanes/types.d.ts +11 -2
- package/dist/lib/lanes/types.js +11 -0
- package/dist/lib/lanes/watcher.d.ts +2 -2
- package/dist/lib/lanes/watcher.js +2 -2
- 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/utils/cognito-session.d.ts +4 -0
- package/dist/utils/cognito-session.js +11 -10
- 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/package.json +1 -1
|
@@ -23,6 +23,7 @@ export interface SessionEventsPostResult {
|
|
|
23
23
|
body: unknown;
|
|
24
24
|
ok: boolean;
|
|
25
25
|
retryable: boolean;
|
|
26
|
+
retryAfterMs?: number;
|
|
26
27
|
networkError?: boolean;
|
|
27
28
|
}
|
|
28
29
|
/** Contract §2.3 batch ingest ack (parsed from a 2xx body). */
|
|
@@ -51,6 +52,7 @@ export declare function createSessionEventsPoster(opts: {
|
|
|
51
52
|
post?: (path: string, body: SessionEventsBatchBody) => Promise<{
|
|
52
53
|
status: number;
|
|
53
54
|
body: unknown;
|
|
55
|
+
headers?: Pick<Headers, "get">;
|
|
54
56
|
}>;
|
|
55
57
|
}): SessionEventsPoster;
|
|
56
58
|
/** Resolve API base: HQ_VAULT_API_URL overrides DEFAULT_VAULT_API_URL. */
|
|
@@ -15,6 +15,27 @@ export const WRITABLE_CONTEXT_STATUSES = new Set([
|
|
|
15
15
|
"bound",
|
|
16
16
|
"migration_pending",
|
|
17
17
|
]);
|
|
18
|
+
/** Parse the Retry-After delta-seconds or HTTP-date form into milliseconds. */
|
|
19
|
+
function parseRetryAfterMs(value, nowMs = Date.now()) {
|
|
20
|
+
if (value === null || !value.trim())
|
|
21
|
+
return undefined;
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
if (/^\d+$/.test(trimmed)) {
|
|
24
|
+
const delayMs = Number(trimmed) * 1_000;
|
|
25
|
+
return Number.isSafeInteger(delayMs) ? delayMs : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (/^[+-]/.test(trimmed))
|
|
28
|
+
return undefined;
|
|
29
|
+
const retryAtMs = Date.parse(trimmed);
|
|
30
|
+
if (!Number.isFinite(retryAtMs))
|
|
31
|
+
return undefined;
|
|
32
|
+
return Math.max(0, retryAtMs - nowMs);
|
|
33
|
+
}
|
|
34
|
+
function retryAfterForStatus(status, headers) {
|
|
35
|
+
if (status !== 429 && status !== 503)
|
|
36
|
+
return undefined;
|
|
37
|
+
return parseRetryAfterMs(headers?.get("retry-after") ?? null);
|
|
38
|
+
}
|
|
18
39
|
/**
|
|
19
40
|
* Parse a batch ingest response body. Returns null when the body is not an
|
|
20
41
|
* object (caller should retain events rather than treating them as posted).
|
|
@@ -65,9 +86,11 @@ export function createSessionEventsPoster(opts) {
|
|
|
65
86
|
try {
|
|
66
87
|
if (opts.post) {
|
|
67
88
|
const res = await opts.post(SESSION_EVENTS_PATH, body);
|
|
89
|
+
const retryAfterMs = retryAfterForStatus(res.status, res.headers);
|
|
68
90
|
return {
|
|
69
91
|
status: res.status,
|
|
70
92
|
body: res.body,
|
|
93
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
71
94
|
...classifySessionEventsResponse(res.status),
|
|
72
95
|
};
|
|
73
96
|
}
|
|
@@ -88,9 +111,11 @@ export function createSessionEventsPoster(opts) {
|
|
|
88
111
|
parsed = { raw: text };
|
|
89
112
|
}
|
|
90
113
|
}
|
|
114
|
+
const retryAfterMs = retryAfterForStatus(res.status, res.headers);
|
|
91
115
|
return {
|
|
92
116
|
status: res.status,
|
|
93
117
|
body: parsed,
|
|
118
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
94
119
|
...classifySessionEventsResponse(res.status),
|
|
95
120
|
};
|
|
96
121
|
}
|
|
@@ -10,8 +10,8 @@ import { type ServiceHostPaths, type ServiceSpec } from "./types.js";
|
|
|
10
10
|
import type { RunCommand } from "./launchd.js";
|
|
11
11
|
export declare const SYSTEMD_PATH_ENV = "/usr/local/bin:/usr/bin:/bin";
|
|
12
12
|
export declare function systemdUserDir(home: string): string;
|
|
13
|
-
export declare function systemdUnitName(spec: Pick<ServiceSpec, "name">): string;
|
|
14
|
-
export declare function systemdUnitPath(home: string, spec: Pick<ServiceSpec, "name">): string;
|
|
13
|
+
export declare function systemdUnitName(spec: Pick<ServiceSpec, "name" | "unitName">): string;
|
|
14
|
+
export declare function systemdUnitPath(home: string, spec: Pick<ServiceSpec, "name" | "unitName">): string;
|
|
15
15
|
export declare function renderSystemdUserUnit(spec: ServiceSpec, host: ServiceHostPaths): string;
|
|
16
16
|
export declare function activateSystemd(unitName: string, run: RunCommand): void;
|
|
17
17
|
export declare function deactivateSystemd(unitName: string, run: RunCommand): void;
|
|
@@ -13,7 +13,7 @@ export function systemdUserDir(home) {
|
|
|
13
13
|
return path.join(home, ".config", "systemd", "user");
|
|
14
14
|
}
|
|
15
15
|
export function systemdUnitName(spec) {
|
|
16
|
-
return `hq-agent-${spec.name}.service`;
|
|
16
|
+
return spec.unitName ?? `hq-agent-${spec.name}.service`;
|
|
17
17
|
}
|
|
18
18
|
export function systemdUnitPath(home, spec) {
|
|
19
19
|
return path.join(systemdUserDir(home), systemdUnitName(spec));
|
|
@@ -19,6 +19,8 @@ export interface ServiceSpec {
|
|
|
19
19
|
env: Record<string, string>;
|
|
20
20
|
/** Seconds to wait before a restart after exit. */
|
|
21
21
|
restartSec: number;
|
|
22
|
+
/** systemd unit file name; defaults to `hq-agent-<name>.service`. */
|
|
23
|
+
unitName?: string;
|
|
22
24
|
}
|
|
23
25
|
export interface ServiceHostPaths {
|
|
24
26
|
home: string;
|
package/dist/main.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import "./node-preflight.js";
|
|
6
6
|
import "./node-network-compat.js";
|
|
7
7
|
import { Sentry } from "./sentry.js";
|
|
8
|
+
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
8
9
|
/**
|
|
9
10
|
* Total time one CLI invocation may await foreground network work. This spans
|
|
10
11
|
* the hard version decision, pre-action observability, and release-health
|
|
@@ -21,6 +22,11 @@ export type TopLevelErrorDependencies = {
|
|
|
21
22
|
sentry: Pick<typeof Sentry, "captureException">;
|
|
22
23
|
stderr: Pick<typeof process.stderr, "write">;
|
|
23
24
|
setExitCode: (code: number) => void;
|
|
25
|
+
interactive?: boolean;
|
|
26
|
+
argv?: readonly string[];
|
|
27
|
+
ask?: (question: string) => Promise<string>;
|
|
28
|
+
openUrl?: (url: string) => Promise<void>;
|
|
29
|
+
upgradeToTeam?: typeof upgradeToTeam;
|
|
24
30
|
};
|
|
25
31
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
26
32
|
export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): Promise<void>;
|
package/dist/main.js
CHANGED
|
@@ -35,7 +35,7 @@ import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js
|
|
|
35
35
|
import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
|
|
36
36
|
import { DEFAULT_COGNITO } from "./utils/cognito-session.js";
|
|
37
37
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
38
|
-
import { canOfferTeamUpgrade, formatPlanGateError, isPlanGateError, offerTeamUpgrade, } from "./utils/plan-gate-error.js";
|
|
38
|
+
import { canOfferTeamUpgrade, formatPlanGateError, isPlanGateError, offerTeamUpgrade, offerUpgradeLink, } from "./utils/plan-gate-error.js";
|
|
39
39
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
40
40
|
import { refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
41
41
|
import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
|
|
@@ -379,20 +379,60 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
379
379
|
deps.setExitCode(1);
|
|
380
380
|
}
|
|
381
381
|
else if (isPlanGateError(err)) {
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
deps.
|
|
382
|
+
// Plan denials are expected product limits, never a CLI crash. The
|
|
383
|
+
// server URL is always shown. Company refusals that the Workforce
|
|
384
|
+
// checkout satisfies retain the direct Stripe checkout action.
|
|
385
|
+
const interactive = deps.interactive ?? canOfferTeamUpgrade(deps.argv);
|
|
386
|
+
const message = formatPlanGateError(err, { urlOnly: !interactive });
|
|
387
|
+
deps.stderr.write(interactive ? `hq: ${message}\n` : `${message}\n`);
|
|
386
388
|
deps.setExitCode(1);
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
389
|
+
if (interactive && err.details.upgradeUrl) {
|
|
390
|
+
const companyUid = err.details.companyUid;
|
|
391
|
+
const isCompanyWorkforceCheckout = typeof companyUid === "string" &&
|
|
392
|
+
/^cmp_[A-Za-z0-9_-]+$/.test(companyUid) &&
|
|
393
|
+
(err.code === "PLAN_REQUIRED" ||
|
|
394
|
+
err.code === "MEETING_PLAN_REQUIRED" ||
|
|
395
|
+
err.code === "BILLING_REQUIRED");
|
|
396
|
+
if (isCompanyWorkforceCheckout && companyUid) {
|
|
397
|
+
try {
|
|
398
|
+
await offerTeamUpgrade(async () => {
|
|
399
|
+
await (deps.upgradeToTeam ?? upgradeToTeam)({ companyUid });
|
|
400
|
+
}, {
|
|
401
|
+
interactive: true,
|
|
402
|
+
argv: deps.argv,
|
|
403
|
+
ask: deps.ask,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (upgradeErr) {
|
|
407
|
+
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
408
|
+
deps.stderr.write(`hq: ${message}\n`);
|
|
409
|
+
try {
|
|
410
|
+
await offerUpgradeLink(err.details.upgradeUrl, {
|
|
411
|
+
interactive: true,
|
|
412
|
+
argv: deps.argv,
|
|
413
|
+
ask: deps.ask,
|
|
414
|
+
openUrl: deps.openUrl,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (linkErr) {
|
|
418
|
+
const linkMessage = linkErr instanceof Error ? linkErr.message : String(linkErr);
|
|
419
|
+
deps.stderr.write(`hq: ${linkMessage}\n`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
392
422
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
else {
|
|
424
|
+
try {
|
|
425
|
+
await offerUpgradeLink(err.details.upgradeUrl, {
|
|
426
|
+
interactive: true,
|
|
427
|
+
argv: deps.argv,
|
|
428
|
+
ask: deps.ask,
|
|
429
|
+
openUrl: deps.openUrl,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (upgradeErr) {
|
|
433
|
+
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
434
|
+
deps.stderr.write(`hq: ${message}\n`);
|
|
435
|
+
}
|
|
396
436
|
}
|
|
397
437
|
}
|
|
398
438
|
}
|
|
@@ -81,6 +81,10 @@ export declare const DEFAULT_VAULT_API_URL: string;
|
|
|
81
81
|
*/
|
|
82
82
|
export declare function resolveDefaultHqRoot(opts?: {
|
|
83
83
|
onMissing?: "throw" | "fallback";
|
|
84
|
+
/** Defaults to process.env, process.cwd() and os.homedir(). */
|
|
85
|
+
env?: NodeJS.ProcessEnv;
|
|
86
|
+
cwd?: string;
|
|
87
|
+
home?: string;
|
|
84
88
|
}): string;
|
|
85
89
|
export declare const DEFAULT_HQ_ROOT: string;
|
|
86
90
|
/**
|
|
@@ -102,22 +102,24 @@ export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ?? "https://hq
|
|
|
102
102
|
* Re-importable as a function for command-time resolution and tests.
|
|
103
103
|
*/
|
|
104
104
|
export function resolveDefaultHqRoot(opts = {}) {
|
|
105
|
+
const env = opts.env ?? process.env;
|
|
106
|
+
const home = opts.home ?? os.homedir();
|
|
105
107
|
// Tier 1: explicit override.
|
|
106
|
-
if (
|
|
107
|
-
return path.resolve(
|
|
108
|
+
if (env.HQ_ROOT)
|
|
109
|
+
return path.resolve(env.HQ_ROOT);
|
|
108
110
|
// Tier 2: working-tree walk-up — operate on the HQ tree you're inside.
|
|
109
|
-
let cur = path.resolve(process.cwd());
|
|
111
|
+
let cur = path.resolve(opts.cwd ?? process.cwd());
|
|
110
112
|
while (cur !== path.dirname(cur)) {
|
|
111
113
|
if (isHqRoot(cur))
|
|
112
114
|
return cur;
|
|
113
115
|
cur = path.dirname(cur);
|
|
114
116
|
}
|
|
115
117
|
// Tiers 3–4: the installer/menubar's canonical "where is HQ" records.
|
|
116
|
-
const fromConfig = readHqRootFromHqConfig(path.join(
|
|
118
|
+
const fromConfig = readHqRootFromHqConfig(path.join(home, ".hq"));
|
|
117
119
|
if (fromConfig)
|
|
118
120
|
return path.resolve(fromConfig);
|
|
119
121
|
// Tier 5: discover a valid HQ root in the well-known locations.
|
|
120
|
-
const discovered = discoverHqRootViaCoreYaml();
|
|
122
|
+
const discovered = discoverHqRootViaCoreYaml(home);
|
|
121
123
|
if (discovered)
|
|
122
124
|
return discovered;
|
|
123
125
|
// Tier 6: nothing resolved.
|
|
@@ -127,7 +129,7 @@ export function resolveDefaultHqRoot(opts = {}) {
|
|
|
127
129
|
"or install HQ via the installer/menubar so ~/.hq/menubar.json records " +
|
|
128
130
|
"its location.");
|
|
129
131
|
}
|
|
130
|
-
return path.join(
|
|
132
|
+
return path.join(home, "hq");
|
|
131
133
|
}
|
|
132
134
|
/**
|
|
133
135
|
* Read the configured HQ root from the installer/menubar records under
|
|
@@ -156,8 +158,7 @@ function readJsonStringField(file, field) {
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
/** Well-known parent locations an HQ tree commonly lives in (mirrors hq-sync). */
|
|
159
|
-
function hqDiscoveryCandidates() {
|
|
160
|
-
const home = os.homedir();
|
|
161
|
+
function hqDiscoveryCandidates(home = os.homedir()) {
|
|
161
162
|
return [
|
|
162
163
|
path.join(home, "HQ"),
|
|
163
164
|
path.join(home, "hq"),
|
|
@@ -172,8 +173,8 @@ function hqDiscoveryCandidates() {
|
|
|
172
173
|
* signature. First match wins; returns undefined if none qualify. Cheap — a
|
|
173
174
|
* few stats plus one small YAML parse on a hit.
|
|
174
175
|
*/
|
|
175
|
-
function discoverHqRootViaCoreYaml() {
|
|
176
|
-
return hqDiscoveryCandidates().find(isValidHqRootSignature);
|
|
176
|
+
function discoverHqRootViaCoreYaml(home) {
|
|
177
|
+
return hqDiscoveryCandidates(home).find(isValidHqRootSignature);
|
|
177
178
|
}
|
|
178
179
|
/**
|
|
179
180
|
* True iff `dir` carries a valid hq-core `core.yaml` — canonical
|
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED"
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* code rather than a reuse of `PLAN_REQUIRED` with company copy.
|
|
7
|
-
*/
|
|
8
|
-
| "PERSONAL_PLAN_REQUIRED";
|
|
1
|
+
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
|
|
2
|
+
/** A PERSONAL scope asked for a feature the paid Individual plan includes. */
|
|
3
|
+
| "PERSONAL_PLAN_REQUIRED"
|
|
4
|
+
/** External agent enrollment was denied by the company plan. */
|
|
5
|
+
| "BILLING_REQUIRED";
|
|
9
6
|
export interface PlanGateDetails {
|
|
10
7
|
resource?: string;
|
|
11
|
-
/** The refused feature, on a `PERSONAL_PLAN_REQUIRED` denial. */
|
|
12
8
|
feature?: string;
|
|
13
9
|
used?: number;
|
|
14
10
|
limit?: number;
|
|
15
11
|
upgradeUrl?: string;
|
|
16
|
-
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Company target from request context or the server's UID-keyed billing URL. */
|
|
17
14
|
companyUid?: string;
|
|
18
15
|
}
|
|
16
|
+
/** One renderer for server-selected upgrade links across CLI plan refusals. */
|
|
17
|
+
export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
|
|
18
|
+
urlOnly?: boolean;
|
|
19
|
+
}): string;
|
|
20
|
+
/** Recover a company UID only from the server's UID-keyed billing callback. */
|
|
21
|
+
export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
|
|
19
22
|
export declare class PlanGateError extends Error {
|
|
20
23
|
readonly code: PlanGateCode;
|
|
21
24
|
readonly details: PlanGateDetails;
|
|
22
25
|
constructor(code: PlanGateCode, details: PlanGateDetails);
|
|
23
26
|
}
|
|
24
27
|
export declare function isPlanGateError(err: unknown): err is PlanGateError;
|
|
25
|
-
|
|
28
|
+
/** Plain, stable CLI copy for plan-gated resource creation. */
|
|
29
|
+
export declare function formatPlanGateError(err: PlanGateError, options?: {
|
|
30
|
+
urlOnly?: boolean;
|
|
31
|
+
}): string;
|
|
26
32
|
export declare function canOfferTeamUpgrade(argv?: readonly string[]): boolean;
|
|
27
|
-
|
|
33
|
+
export interface UpgradeLinkOfferDependencies {
|
|
34
|
+
ask?: (question: string) => Promise<string>;
|
|
35
|
+
openUrl?: (url: string) => Promise<void>;
|
|
36
|
+
argv?: readonly string[];
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Ask before opening the exact URL returned by the server. */
|
|
40
|
+
export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
|
|
41
|
+
/** Legacy checkout callback retained for callers outside the shared refusal paths. */
|
|
28
42
|
export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
|
|
29
43
|
ask?: (question: string) => Promise<string>;
|
|
30
44
|
argv?: readonly string[];
|
|
@@ -33,7 +47,7 @@ export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?:
|
|
|
33
47
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
34
48
|
export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
|
|
35
49
|
/**
|
|
36
|
-
* Decode
|
|
50
|
+
* Decode the supported plan-gate envelopes. Malformed, unrelated,
|
|
37
51
|
* or non-402 responses retain their existing command-specific handling.
|
|
38
52
|
*/
|
|
39
53
|
export declare function planGateErrorFromResponse(response: Response, companyUid?: string): Promise<{
|
|
@@ -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
|