@indigoai-us/hq-cli 5.101.1 → 5.101.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/CHANGELOG.md +9 -0
- package/dist/commands/agents.js +5 -0
- package/dist/commands/billing.js +19 -0
- package/dist/commands/outposts.js +5 -1
- package/dist/lib/db/control-plane.js +1 -1
- package/dist/main.d.ts +1 -1
- package/dist/main.js +15 -3
- package/dist/utils/plan-gate-error.d.ts +11 -2
- package/dist/utils/plan-gate-error.js +39 -5
- package/dist/utils/self-update.d.ts +4 -4
- package/dist/utils/self-update.js +7 -5
- package/dist/utils/team-upgrade.d.ts +17 -0
- package/dist/utils/team-upgrade.js +63 -0
- package/dist/utils/vault-api.js +7 -1
- package/dist/utils/version-gate.d.ts +8 -2
- package/dist/utils/version-gate.js +48 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.101.2] — 2026-08-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Self-update now detects Bun-managed global installs and runs `bun add -g`
|
|
10
|
+
through the owning package manager. Existing pnpm-managed and npm prefix
|
|
11
|
+
installs, including Homebrew prefixes such as `/opt/homebrew`, continue to
|
|
12
|
+
update the copy that is actually running without an unnecessary `sudo`.
|
|
13
|
+
|
|
5
14
|
## [5.101.1] — 2026-08-14
|
|
6
15
|
|
|
7
16
|
### Fixed
|
package/dist/commands/agents.js
CHANGED
|
@@ -27,6 +27,7 @@ import * as readline from "node:readline";
|
|
|
27
27
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
28
28
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
29
29
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
30
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
30
31
|
import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
31
32
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
32
33
|
export const VALID_EFFORTS = new Set([
|
|
@@ -381,6 +382,10 @@ function sleep(ms) {
|
|
|
381
382
|
// Command registration
|
|
382
383
|
// ---------------------------------------------------------------------------
|
|
383
384
|
function fail(err) {
|
|
385
|
+
// Let the CLI boundary render and offer the one shared Team checkout flow.
|
|
386
|
+
// Local exits would otherwise prevent interactive plan-limit remediation.
|
|
387
|
+
if (isPlanGateError(err))
|
|
388
|
+
throw err;
|
|
384
389
|
if (err instanceof AgentsHttpError) {
|
|
385
390
|
console.error(chalk.red(err.message));
|
|
386
391
|
}
|
package/dist/commands/billing.js
CHANGED
|
@@ -16,6 +16,7 @@ import chalk from "chalk";
|
|
|
16
16
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
17
17
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
18
18
|
import { mintPaymentLink } from "../utils/billing-gate.js";
|
|
19
|
+
import { upgradeToTeam } from "../utils/team-upgrade.js";
|
|
19
20
|
function fail(err) {
|
|
20
21
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
21
22
|
process.exit(1);
|
|
@@ -107,5 +108,23 @@ export function registerBillingCommand(program) {
|
|
|
107
108
|
fail(err);
|
|
108
109
|
}
|
|
109
110
|
});
|
|
111
|
+
billing
|
|
112
|
+
.command("upgrade")
|
|
113
|
+
.description("Open Stripe Checkout to upgrade this company to HQ Team ($500/mo)")
|
|
114
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
115
|
+
.option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
|
|
116
|
+
.option("--json", "Emit the Checkout URL as JSON")
|
|
117
|
+
.action(async function (opts) {
|
|
118
|
+
try {
|
|
119
|
+
await upgradeToTeam({
|
|
120
|
+
company: companyOf(this),
|
|
121
|
+
noBrowser: opts.browser === false,
|
|
122
|
+
json: opts.json,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
fail(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
110
129
|
}
|
|
111
130
|
//# sourceMappingURL=billing.js.map
|
|
@@ -439,6 +439,10 @@ function bashSingleQuote(value) {
|
|
|
439
439
|
function systemdQuoted(value) {
|
|
440
440
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
441
441
|
}
|
|
442
|
+
// U60 records this compatible hq-cloud artifact. Outpost launchers must stay
|
|
443
|
+
// reproducible: `@latest` could silently opt a long-lived box into a different
|
|
444
|
+
// sync protocol before its server-side eligibility is evaluated.
|
|
445
|
+
const OUTPOST_HQ_CLOUD_VERSION = "6.15.1";
|
|
442
446
|
function renderSelfDeploySyncScript(hqRoot) {
|
|
443
447
|
return `#!/usr/bin/env bash
|
|
444
448
|
set -u
|
|
@@ -448,7 +452,7 @@ HQ_ROOT=${bashSingleQuote(hqRoot)}
|
|
|
448
452
|
cd "$HQ_ROOT"
|
|
449
453
|
while true; do
|
|
450
454
|
hq auth refresh || echo "[outpost-sync] auth refresh failed, continuing"
|
|
451
|
-
npx -y --package=@indigoai-us/hq-cloud
|
|
455
|
+
npx -y --package=@indigoai-us/hq-cloud@${OUTPOST_HQ_CLOUD_VERSION} hq-sync-runner \\
|
|
452
456
|
--companies \\
|
|
453
457
|
--direction both \\
|
|
454
458
|
--on-conflict keep \\
|
|
@@ -36,7 +36,7 @@ export class ControlPlaneDbClient {
|
|
|
36
36
|
catch {
|
|
37
37
|
/* keep */
|
|
38
38
|
}
|
|
39
|
-
const planGate = planGateErrorFromPayload(res.status, payload);
|
|
39
|
+
const planGate = planGateErrorFromPayload(res.status, payload, input.companyUid);
|
|
40
40
|
if (planGate)
|
|
41
41
|
throw planGate;
|
|
42
42
|
const err = new Error(msg);
|
package/dist/main.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export type TopLevelErrorDependencies = {
|
|
|
12
12
|
setExitCode: (code: number) => void;
|
|
13
13
|
};
|
|
14
14
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
15
|
-
export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): void
|
|
15
|
+
export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): Promise<void>;
|
|
16
16
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -70,7 +70,8 @@ import { isEpipe } from "./utils/epipe.js";
|
|
|
70
70
|
import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
|
|
71
71
|
import { isAuthError } from "./utils/auth-error.js";
|
|
72
72
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
73
|
-
import { formatPlanGateError, isPlanGateError, } from "./utils/plan-gate-error.js";
|
|
73
|
+
import { canOfferTeamUpgrade, formatPlanGateError, isPlanGateError, offerTeamUpgrade, } from "./utils/plan-gate-error.js";
|
|
74
|
+
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
74
75
|
import { refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
75
76
|
import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
|
|
76
77
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
@@ -292,7 +293,7 @@ export async function runCli() {
|
|
|
292
293
|
await program.parseAsync();
|
|
293
294
|
}
|
|
294
295
|
catch (err) {
|
|
295
|
-
handleTopLevelError(err);
|
|
296
|
+
await handleTopLevelError(err);
|
|
296
297
|
}
|
|
297
298
|
finally {
|
|
298
299
|
// Plan-limit nag (US-016): stderr-only, never throws, never touches
|
|
@@ -319,7 +320,7 @@ const defaultTopLevelErrorDependencies = {
|
|
|
319
320
|
},
|
|
320
321
|
};
|
|
321
322
|
/** Classify a top-level failure without making the CLI process boundary opaque to tests. */
|
|
322
|
-
export function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
|
|
323
|
+
export async function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
|
|
323
324
|
// A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
|
|
324
325
|
// (`hq … | head`, `source <(hq …)`, a parent that exited). That is normal
|
|
325
326
|
// Unix behavior with no user-facing degradation — exit cleanly (0) and
|
|
@@ -366,6 +367,17 @@ export function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies
|
|
|
366
367
|
// envelope; do not expose its raw body or retry a rejected creation.
|
|
367
368
|
deps.stderr.write(`hq: ${formatPlanGateError(err)}\n`);
|
|
368
369
|
deps.setExitCode(1);
|
|
370
|
+
if (canOfferTeamUpgrade()) {
|
|
371
|
+
try {
|
|
372
|
+
await offerTeamUpgrade(async () => {
|
|
373
|
+
await upgradeToTeam({ companyUid: err.details.companyUid });
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (upgradeErr) {
|
|
377
|
+
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
378
|
+
deps.stderr.write(`hq: ${message}\n`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
369
381
|
}
|
|
370
382
|
else if (isExpectedUserError(err)) {
|
|
371
383
|
// HQ-CLI-6: a user-facing, client-caused error (a non-owner running
|
|
@@ -9,6 +9,8 @@ export interface PlanGateDetails {
|
|
|
9
9
|
used?: number;
|
|
10
10
|
limit?: number;
|
|
11
11
|
upgradeUrl?: string;
|
|
12
|
+
/** Best-effort target captured by the shared HTTP client for interactive checkout. */
|
|
13
|
+
companyUid?: string;
|
|
12
14
|
}
|
|
13
15
|
export declare class PlanGateError extends Error {
|
|
14
16
|
readonly code: PlanGateCode;
|
|
@@ -17,13 +19,20 @@ export declare class PlanGateError extends Error {
|
|
|
17
19
|
}
|
|
18
20
|
export declare function isPlanGateError(err: unknown): err is PlanGateError;
|
|
19
21
|
export declare function formatPlanGateError(err: PlanGateError): string;
|
|
22
|
+
export declare function canOfferTeamUpgrade(argv?: readonly string[]): boolean;
|
|
23
|
+
/** Ask once, only on a real interactive terminal. */
|
|
24
|
+
export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
|
|
25
|
+
ask?: (question: string) => Promise<string>;
|
|
26
|
+
argv?: readonly string[];
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
}): Promise<boolean>;
|
|
20
29
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
21
|
-
export declare function planGateErrorFromPayload(status: number, body: unknown): PlanGateError | null;
|
|
30
|
+
export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
|
|
22
31
|
/**
|
|
23
32
|
* Decode only the two deliberate plan-gate envelopes. Malformed, unrelated,
|
|
24
33
|
* or non-402 responses retain their existing command-specific handling.
|
|
25
34
|
*/
|
|
26
|
-
export declare function planGateErrorFromResponse(response: Response): Promise<{
|
|
35
|
+
export declare function planGateErrorFromResponse(response: Response, companyUid?: string): Promise<{
|
|
27
36
|
error: PlanGateError | null;
|
|
28
37
|
response: Response;
|
|
29
38
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to Team.";
|
|
1
2
|
export class PlanGateError extends Error {
|
|
2
3
|
code;
|
|
3
4
|
details;
|
|
@@ -5,7 +6,7 @@ export class PlanGateError extends Error {
|
|
|
5
6
|
// Commands with their own expected-error boundary commonly print
|
|
6
7
|
// `err.message`. Keeping the friendly copy here means those boundaries
|
|
7
8
|
// retain the same plan-gate voice as main.ts without per-command handling.
|
|
8
|
-
super(formatPlanGateDetails(code, details));
|
|
9
|
+
super([formatPlanGateDetails(code, details), TEAM_UPGRADE_HINT].join("\n"));
|
|
9
10
|
this.code = code;
|
|
10
11
|
this.details = details;
|
|
11
12
|
this.name = "PlanGateError";
|
|
@@ -40,10 +41,42 @@ function formatPlanGateDetails(code, details) {
|
|
|
40
41
|
].join("\n");
|
|
41
42
|
}
|
|
42
43
|
export function formatPlanGateError(err) {
|
|
43
|
-
return
|
|
44
|
+
return [
|
|
45
|
+
formatPlanGateDetails(err.code, err.details),
|
|
46
|
+
TEAM_UPGRADE_HINT,
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
export function canOfferTeamUpgrade(argv = process.argv) {
|
|
50
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY) &&
|
|
51
|
+
!process.env.CI &&
|
|
52
|
+
!argv.includes("--json");
|
|
53
|
+
}
|
|
54
|
+
/** Ask once, only on a real interactive terminal. */
|
|
55
|
+
export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
56
|
+
if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
|
|
57
|
+
return false;
|
|
58
|
+
if (deps.ask) {
|
|
59
|
+
const answer = await deps.ask("Upgrade to HQ Team ($500/mo) now? [y/N] ");
|
|
60
|
+
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
61
|
+
return false;
|
|
62
|
+
await onConfirm();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const { createInterface } = await import("node:readline/promises");
|
|
66
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
67
|
+
try {
|
|
68
|
+
const answer = await readline.question("Upgrade to HQ Team ($500/mo) now? [y/N] ");
|
|
69
|
+
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
70
|
+
return false;
|
|
71
|
+
await onConfirm();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
readline.close();
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
46
|
-
export function planGateErrorFromPayload(status, body) {
|
|
79
|
+
export function planGateErrorFromPayload(status, body, companyUid) {
|
|
47
80
|
if (status !== 402 || !body || typeof body !== "object")
|
|
48
81
|
return null;
|
|
49
82
|
const payload = body;
|
|
@@ -55,13 +88,14 @@ export function planGateErrorFromPayload(status, body) {
|
|
|
55
88
|
...(typeof payload.used === "number" ? { used: payload.used } : {}),
|
|
56
89
|
...(typeof payload.limit === "number" ? { limit: payload.limit } : {}),
|
|
57
90
|
...(typeof payload.upgradeUrl === "string" ? { upgradeUrl: payload.upgradeUrl } : {}),
|
|
91
|
+
...(companyUid ? { companyUid } : {}),
|
|
58
92
|
});
|
|
59
93
|
}
|
|
60
94
|
/**
|
|
61
95
|
* Decode only the two deliberate plan-gate envelopes. Malformed, unrelated,
|
|
62
96
|
* or non-402 responses retain their existing command-specific handling.
|
|
63
97
|
*/
|
|
64
|
-
export async function planGateErrorFromResponse(response) {
|
|
98
|
+
export async function planGateErrorFromResponse(response, companyUid) {
|
|
65
99
|
// Leave every non-402 response untouched for its command-specific handler.
|
|
66
100
|
// A non-plan 402 is buffered and re-wrapped below so its useful error
|
|
67
101
|
// payload remains available to the existing command-specific handler.
|
|
@@ -82,7 +116,7 @@ export async function planGateErrorFromResponse(response) {
|
|
|
82
116
|
// Preserve the original non-JSON 402 body below for its existing handler.
|
|
83
117
|
}
|
|
84
118
|
return {
|
|
85
|
-
error: planGateErrorFromPayload(response.status, body),
|
|
119
|
+
error: planGateErrorFromPayload(response.status, body, companyUid),
|
|
86
120
|
response: new Response(buffer, {
|
|
87
121
|
status: response.status,
|
|
88
122
|
statusText: response.statusText,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - `version-check.ts` maintains the cached npm `latest` that the startup
|
|
24
24
|
* path reads, so the common case costs a small file read, not a fetch.
|
|
25
25
|
*
|
|
26
|
-
* Best-effort by design: registry unreachable, npm/pnpm missing, install
|
|
26
|
+
* Best-effort by design: registry unreachable, npm/pnpm/Bun missing, install
|
|
27
27
|
* failure, or `hq` not on PATH for the re-exec all degrade to running the
|
|
28
28
|
* command on the current version. Self-updating must never make a command less
|
|
29
29
|
* available than it was before.
|
|
@@ -56,9 +56,9 @@ export interface SelfUpdateOutcome {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* The manager-aware install argv for this layout — same routing as the hard
|
|
59
|
-
* gate:
|
|
60
|
-
*
|
|
61
|
-
* prefix so the copy that is actually running is
|
|
59
|
+
* gate: pnpm- and Bun-managed installs must be updated by their owning manager
|
|
60
|
+
* (npm would drop a copy their shims never read), and an npm install goes
|
|
61
|
+
* through the resolved prefix so the copy that is actually running is replaced.
|
|
62
62
|
*/
|
|
63
63
|
export declare function buildSelfUpdatePlan(install: RunningInstall): {
|
|
64
64
|
cmd: string;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - `version-check.ts` maintains the cached npm `latest` that the startup
|
|
24
24
|
* path reads, so the common case costs a small file read, not a fetch.
|
|
25
25
|
*
|
|
26
|
-
* Best-effort by design: registry unreachable, npm/pnpm missing, install
|
|
26
|
+
* Best-effort by design: registry unreachable, npm/pnpm/Bun missing, install
|
|
27
27
|
* failure, or `hq` not on PATH for the re-exec all degrade to running the
|
|
28
28
|
* command on the current version. Self-updating must never make a command less
|
|
29
29
|
* available than it was before.
|
|
@@ -38,7 +38,7 @@ import * as path from "node:path";
|
|
|
38
38
|
import semver from "semver";
|
|
39
39
|
import chalk from "chalk";
|
|
40
40
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
41
|
-
import { buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, resolveRunningInstall, runUpdateCommand, } from "./version-gate.js";
|
|
41
|
+
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, resolveRunningInstall, runUpdateCommand, } from "./version-gate.js";
|
|
42
42
|
/**
|
|
43
43
|
* Set on the re-exec'd child so it can never self-update (and re-exec) again.
|
|
44
44
|
* One update + one re-exec per user invocation, ever.
|
|
@@ -68,13 +68,15 @@ async function fetchLatestVersion() {
|
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* The manager-aware install argv for this layout — same routing as the hard
|
|
71
|
-
* gate:
|
|
72
|
-
*
|
|
73
|
-
* prefix so the copy that is actually running is
|
|
71
|
+
* gate: pnpm- and Bun-managed installs must be updated by their owning manager
|
|
72
|
+
* (npm would drop a copy their shims never read), and an npm install goes
|
|
73
|
+
* through the resolved prefix so the copy that is actually running is replaced.
|
|
74
74
|
*/
|
|
75
75
|
export function buildSelfUpdatePlan(install) {
|
|
76
76
|
if (install.manager === "pnpm")
|
|
77
77
|
return { cmd: "pnpm", args: buildPnpmInstallArgv() };
|
|
78
|
+
if (install.manager === "bun")
|
|
79
|
+
return { cmd: "bun", args: buildBunInstallArgv() };
|
|
78
80
|
if (install.prefix)
|
|
79
81
|
return { cmd: "npm", args: buildPrefixedInstallArgv(install.prefix) };
|
|
80
82
|
return { cmd: "npm", args: ["install", "-g", `${CLI_NAME}@latest`] };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TeamUpgradeOptions {
|
|
2
|
+
company?: string;
|
|
3
|
+
companyUid?: string;
|
|
4
|
+
noBrowser?: boolean;
|
|
5
|
+
/** Testable override for environments where browser availability is known. */
|
|
6
|
+
headless?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
openUrl?: (url: string) => Promise<unknown>;
|
|
9
|
+
write?: (text: string) => void;
|
|
10
|
+
error?: (text: string) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Start the owner-gated HQ Team Checkout flow. Stripe collects payment details
|
|
14
|
+
* in its hosted browser page; the CLI never receives card data.
|
|
15
|
+
*/
|
|
16
|
+
export declare function upgradeToTeam(opts?: TeamUpgradeOptions): Promise<string>;
|
|
17
|
+
//# sourceMappingURL=team-upgrade.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import open from "open";
|
|
2
|
+
import { ensureCognitoToken } from "./cognito-session.js";
|
|
3
|
+
import { getCompanyUid, vaultApiFetch } from "./vault-api.js";
|
|
4
|
+
const CHECKOUT_SUCCESS_URL = "https://hq.computer/billing?checkout=success";
|
|
5
|
+
const CHECKOUT_CANCEL_URL = "https://hq.computer/billing?checkout=cancel";
|
|
6
|
+
function isHeadless() {
|
|
7
|
+
if (process.env.CI)
|
|
8
|
+
return true;
|
|
9
|
+
if (process.platform !== "linux")
|
|
10
|
+
return false;
|
|
11
|
+
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
12
|
+
}
|
|
13
|
+
function ownerUpgradeError() {
|
|
14
|
+
return Object.assign(new Error("Only a company owner can upgrade. Ask a company owner to upgrade."), { expected: true });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Start the owner-gated HQ Team Checkout flow. Stripe collects payment details
|
|
18
|
+
* in its hosted browser page; the CLI never receives card data.
|
|
19
|
+
*/
|
|
20
|
+
export async function upgradeToTeam(opts = {}) {
|
|
21
|
+
const token = await ensureCognitoToken();
|
|
22
|
+
const companyUid = opts.companyUid ?? await getCompanyUid(token, opts.company);
|
|
23
|
+
const response = await vaultApiFetch({
|
|
24
|
+
token,
|
|
25
|
+
path: "/v1/billing/checkout/team",
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: {
|
|
28
|
+
companyUid,
|
|
29
|
+
successUrl: CHECKOUT_SUCCESS_URL,
|
|
30
|
+
cancelUrl: CHECKOUT_CANCEL_URL,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const body = (await response.json().catch(() => ({})));
|
|
35
|
+
if (response.status === 403 || /owner/i.test(`${body.error ?? ""} ${body.message ?? ""}`)) {
|
|
36
|
+
throw ownerUpgradeError();
|
|
37
|
+
}
|
|
38
|
+
throw Object.assign(new Error(`Could not start HQ Team checkout: ${body.error ?? body.message ?? response.statusText}`), { expected: true });
|
|
39
|
+
}
|
|
40
|
+
const { url } = (await response.json());
|
|
41
|
+
if (typeof url !== "string" || !url) {
|
|
42
|
+
throw new Error("Checkout response did not include a hosted checkout URL.");
|
|
43
|
+
}
|
|
44
|
+
const write = opts.write ?? ((text) => process.stdout.write(text));
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
write(`${JSON.stringify({ url })}\n`);
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
write("Opening HQ Team checkout ($500/mo) in your browser — finish there and you're upgraded.\n");
|
|
50
|
+
const skipBrowser = opts.noBrowser || (opts.headless ?? isHeadless());
|
|
51
|
+
if (skipBrowser) {
|
|
52
|
+
write(`Open this URL to continue: ${url}\n`);
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await (opts.openUrl ?? open)(url);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
(opts.error ?? ((text) => process.stderr.write(text)))(`Couldn't launch your browser (${err instanceof Error ? err.message : String(err)}). Open this URL to continue: ${url}\n`);
|
|
60
|
+
}
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=team-upgrade.js.map
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -187,7 +187,13 @@ export async function vaultApiFetch(opts) {
|
|
|
187
187
|
// A plan gate is a normal, user-actionable denial. Decode it at the one
|
|
188
188
|
// shared HTTP seam so every resource-creation command reaches main.ts's
|
|
189
189
|
// friendly renderer without duplicating response parsing or retrying.
|
|
190
|
-
|
|
190
|
+
// The failed request usually already carries the target company in its
|
|
191
|
+
// body or path. Preserve it on the typed gate so an interactive checkout
|
|
192
|
+
// upgrades the same company without asking a multi-company caller again.
|
|
193
|
+
const companyUid = typeof opts.body?.companyUid === "string"
|
|
194
|
+
? opts.body.companyUid
|
|
195
|
+
: /\/(cmp_[^/?]+)/.exec(opts.path)?.[1];
|
|
196
|
+
const planGate = await planGateErrorFromResponse(response, companyUid);
|
|
191
197
|
if (planGate.error)
|
|
192
198
|
throw planGate.error;
|
|
193
199
|
return planGate.response;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
|
|
31
31
|
/** Which package manager owns the running global install. */
|
|
32
|
-
export type InstallManager = "npm" | "pnpm";
|
|
32
|
+
export type InstallManager = "npm" | "pnpm" | "bun";
|
|
33
33
|
export interface VersionCheckResponse {
|
|
34
34
|
clientId: string;
|
|
35
35
|
currentVersion: string;
|
|
@@ -67,13 +67,15 @@ export declare function npmPrefixFromPackageDir(pkgDir: string): string | null;
|
|
|
67
67
|
* Those layouts fall through to the ordinary npm-prefix/manual handling.
|
|
68
68
|
*/
|
|
69
69
|
export declare function isPnpmManagedPackageDir(pkgDir: string): boolean;
|
|
70
|
+
/** Whether the running package lives inside Bun's global install tree. */
|
|
71
|
+
export declare function isBunManagedPackageDir(pkgDir: string): boolean;
|
|
70
72
|
/**
|
|
71
73
|
* Where the running CLI is installed and who owns it. Resolved in ONE pass so
|
|
72
74
|
* the package-root walk (which reads and parses a `package.json` per directory
|
|
73
75
|
* level) happens once per invocation, and so the manager and the prefix can
|
|
74
76
|
* never disagree because the filesystem shifted between two separate walks.
|
|
75
77
|
*
|
|
76
|
-
* `prefix` is `null` for
|
|
78
|
+
* `prefix` is `null` for pnpm- and Bun-managed installs even though a prefix-shaped
|
|
77
79
|
* string *can* be derived from those paths: `npmPrefixFromPackageDir` would
|
|
78
80
|
* happily hand back the pnpm store directory, and `npm install -g --prefix
|
|
79
81
|
* <store>` then unpacks a fresh copy into `<store>/lib/node_modules`, which
|
|
@@ -101,6 +103,8 @@ export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
|
101
103
|
* the new version — which is the whole point of routing here instead of npm.
|
|
102
104
|
*/
|
|
103
105
|
export declare function buildPnpmInstallArgv(): string[];
|
|
106
|
+
/** Argv for updating a Bun-managed global install. */
|
|
107
|
+
export declare function buildBunInstallArgv(): string[];
|
|
104
108
|
/**
|
|
105
109
|
* Filesystem surface used by {@link cleanStalePartialInstall}. Injected so the
|
|
106
110
|
* cleanup logic is unit-testable without touching a real global prefix.
|
|
@@ -209,11 +213,13 @@ export declare const __test__: {
|
|
|
209
213
|
CLIENT_ID: string;
|
|
210
214
|
ENDPOINT_PATH: string;
|
|
211
215
|
FETCH_TIMEOUT_MS: number;
|
|
216
|
+
buildBunInstallArgv: typeof buildBunInstallArgv;
|
|
212
217
|
buildPnpmInstallArgv: typeof buildPnpmInstallArgv;
|
|
213
218
|
buildPrefixedInstallArgv: typeof buildPrefixedInstallArgv;
|
|
214
219
|
buildSpawnPlan: typeof buildSpawnPlan;
|
|
215
220
|
cleanStalePartialInstall: typeof cleanStalePartialInstall;
|
|
216
221
|
enforceUpdateRequired: typeof enforceUpdateRequired;
|
|
222
|
+
isBunManagedPackageDir: typeof isBunManagedPackageDir;
|
|
217
223
|
isPnpmManagedPackageDir: typeof isPnpmManagedPackageDir;
|
|
218
224
|
npmPrefixFromPackageDir: typeof npmPrefixFromPackageDir;
|
|
219
225
|
nudgeUpdateRecommended: typeof nudgeUpdateRecommended;
|
|
@@ -125,6 +125,19 @@ export function isPnpmManagedPackageDir(pkgDir) {
|
|
|
125
125
|
}
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
+
/** Whether the running package lives inside Bun's global install tree. */
|
|
129
|
+
export function isBunManagedPackageDir(pkgDir) {
|
|
130
|
+
const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
131
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
132
|
+
for (let i = 0; i < segments.length - 2; i += 1) {
|
|
133
|
+
if (segments[i] === "install" &&
|
|
134
|
+
segments[i + 1] === "global" &&
|
|
135
|
+
segments[i + 2] === "node_modules") {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
128
141
|
export function resolveRunningInstall() {
|
|
129
142
|
try {
|
|
130
143
|
const packageRoot = findRunningPackageRoot();
|
|
@@ -133,6 +146,9 @@ export function resolveRunningInstall() {
|
|
|
133
146
|
if (isPnpmManagedPackageDir(packageRoot)) {
|
|
134
147
|
return { manager: "pnpm", prefix: null, packageRoot };
|
|
135
148
|
}
|
|
149
|
+
if (isBunManagedPackageDir(packageRoot)) {
|
|
150
|
+
return { manager: "bun", prefix: null, packageRoot };
|
|
151
|
+
}
|
|
136
152
|
return {
|
|
137
153
|
manager: "npm",
|
|
138
154
|
prefix: npmPrefixFromPackageDir(packageRoot),
|
|
@@ -162,6 +178,10 @@ export function buildPrefixedInstallArgv(prefix) {
|
|
|
162
178
|
export function buildPnpmInstallArgv() {
|
|
163
179
|
return ["add", "-g", LATEST_PACKAGE_SPEC];
|
|
164
180
|
}
|
|
181
|
+
/** Argv for updating a Bun-managed global install. */
|
|
182
|
+
export function buildBunInstallArgv() {
|
|
183
|
+
return ["add", "-g", LATEST_PACKAGE_SPEC];
|
|
184
|
+
}
|
|
165
185
|
const nodeStaleInstallFs = {
|
|
166
186
|
readdirSync: (dir) => readdirSync(dir),
|
|
167
187
|
existsSync,
|
|
@@ -338,6 +358,8 @@ function performUpdate(command, runner = runUpdateCommand) {
|
|
|
338
358
|
function manualUpdateCommand(install, decision) {
|
|
339
359
|
if (install.manager === "pnpm")
|
|
340
360
|
return `pnpm ${buildPnpmInstallArgv().join(" ")}`;
|
|
361
|
+
if (install.manager === "bun")
|
|
362
|
+
return `bun ${buildBunInstallArgv().join(" ")}`;
|
|
341
363
|
if (install.prefix)
|
|
342
364
|
return `npm ${buildPrefixedInstallArgv(install.prefix).join(" ")}`;
|
|
343
365
|
return decision.updateCommand;
|
|
@@ -360,7 +382,9 @@ function nudgeUpdateRecommended(decision, install = resolveRunningInstall()) {
|
|
|
360
382
|
console.error(msg);
|
|
361
383
|
const command = install.manager === "pnpm"
|
|
362
384
|
? `pnpm ${buildPnpmInstallArgv().join(" ")}`
|
|
363
|
-
:
|
|
385
|
+
: install.manager === "bun"
|
|
386
|
+
? `bun ${buildBunInstallArgv().join(" ")}`
|
|
387
|
+
: decision.updateCommand;
|
|
364
388
|
if (command) {
|
|
365
389
|
console.error(chalk.dim(` Update: ${command}`));
|
|
366
390
|
}
|
|
@@ -382,13 +406,14 @@ function enforceUpdateRequired(decision, deps = {}) {
|
|
|
382
406
|
console.error(chalk.dim(` ${decision.message}`));
|
|
383
407
|
const command = decision.updateCommand;
|
|
384
408
|
// ONE package-root walk decides both the manager and the prefix. A
|
|
385
|
-
// pnpm-managed install must be updated by
|
|
386
|
-
// and any npm prefix are
|
|
409
|
+
// A pnpm- or Bun-managed install must be updated by its owning manager; the
|
|
410
|
+
// server's `updateCommand` and any npm prefix are wrong for those layouts, so
|
|
411
|
+
// `resolveRunningInstall`
|
|
387
412
|
// already reports `prefix: null` there and neither is consulted below.
|
|
388
413
|
const install = (deps.resolveInstall ?? resolveRunningInstall)();
|
|
389
|
-
const
|
|
414
|
+
const isManagedOutsideNpm = install.manager !== "npm";
|
|
390
415
|
const prefix = install.prefix;
|
|
391
|
-
if (!
|
|
416
|
+
if (!isManagedOutsideNpm && !command && !prefix) {
|
|
392
417
|
console.error(chalk.red(" No updateCommand provided by hq-pro — see https://hq.indigo.ai/docs/cli-update for manual steps."));
|
|
393
418
|
if (decision.downloadUrl) {
|
|
394
419
|
console.error(chalk.dim(` Download: ${decision.downloadUrl}`));
|
|
@@ -400,10 +425,14 @@ function enforceUpdateRequired(decision, deps = {}) {
|
|
|
400
425
|
// the EXACT same command under elevation.
|
|
401
426
|
let primaryCmd;
|
|
402
427
|
let primaryArgs;
|
|
403
|
-
if (
|
|
428
|
+
if (install.manager === "pnpm") {
|
|
404
429
|
primaryCmd = "pnpm";
|
|
405
430
|
primaryArgs = buildPnpmInstallArgv();
|
|
406
431
|
}
|
|
432
|
+
else if (install.manager === "bun") {
|
|
433
|
+
primaryCmd = "bun";
|
|
434
|
+
primaryArgs = buildBunInstallArgv();
|
|
435
|
+
}
|
|
407
436
|
else if (prefix) {
|
|
408
437
|
primaryCmd = "npm";
|
|
409
438
|
primaryArgs = buildPrefixedInstallArgv(prefix);
|
|
@@ -414,10 +443,10 @@ function enforceUpdateRequired(decision, deps = {}) {
|
|
|
414
443
|
primaryArgs = parts.slice(1);
|
|
415
444
|
}
|
|
416
445
|
let result;
|
|
417
|
-
if (
|
|
418
|
-
console.error(chalk.dim(
|
|
419
|
-
console.error(chalk.dim(` Running:
|
|
420
|
-
result = performUpdateCommand(
|
|
446
|
+
if (isManagedOutsideNpm) {
|
|
447
|
+
console.error(chalk.dim(` Detected a ${install.manager}-managed global install; updating with ${install.manager}`));
|
|
448
|
+
console.error(chalk.dim(` Running: ${primaryCmd} ${primaryArgs.join(" ")}`));
|
|
449
|
+
result = performUpdateCommand(primaryCmd, primaryArgs, runner);
|
|
421
450
|
}
|
|
422
451
|
else if (prefix) {
|
|
423
452
|
console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
|
|
@@ -455,10 +484,10 @@ function enforceUpdateRequired(decision, deps = {}) {
|
|
|
455
484
|
// through to the manual path unchanged, while headless boxes with passwordless
|
|
456
485
|
// sudo self-update cleanly.
|
|
457
486
|
//
|
|
458
|
-
// Never for pnpm
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
if (!result.ok && primaryCmd && !
|
|
487
|
+
// Never for pnpm or Bun: elevation changes the package manager's global home,
|
|
488
|
+
// leaving the user's shim untouched while reporting success. A failed
|
|
489
|
+
// non-npm update must surface instead.
|
|
490
|
+
if (!result.ok && primaryCmd && !isManagedOutsideNpm) {
|
|
462
491
|
console.error(chalk.dim(` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`));
|
|
463
492
|
const sudoResult = performUpdateCommand("sudo", ["-n", primaryCmd, ...primaryArgs], runner);
|
|
464
493
|
if (sudoResult.ok)
|
|
@@ -476,9 +505,9 @@ function enforceUpdateRequired(decision, deps = {}) {
|
|
|
476
505
|
const manual = manualUpdateCommand(install, decision) ?? command;
|
|
477
506
|
console.error(chalk.dim(` Try manually: ${manual}`));
|
|
478
507
|
// Only informational, and only when we could not run the right manager at
|
|
479
|
-
// all: hq-pro's command is npm-shaped, so following it on a pnpm
|
|
480
|
-
//
|
|
481
|
-
if (
|
|
508
|
+
// all: hq-pro's command is npm-shaped, so following it on a pnpm or Bun
|
|
509
|
+
// layout would update a different copy and leave the running shim stale.
|
|
510
|
+
if (isManagedOutsideNpm && result.code === "ENOENT" && command) {
|
|
482
511
|
console.error(chalk.dim(` (hq-pro suggests \`${command}\` — that is for npm-managed installs; use it only if you have switched this install to npm.)`));
|
|
483
512
|
}
|
|
484
513
|
process.exit(75);
|
|
@@ -524,11 +553,13 @@ export const __test__ = {
|
|
|
524
553
|
CLIENT_ID,
|
|
525
554
|
ENDPOINT_PATH,
|
|
526
555
|
FETCH_TIMEOUT_MS,
|
|
556
|
+
buildBunInstallArgv,
|
|
527
557
|
buildPnpmInstallArgv,
|
|
528
558
|
buildPrefixedInstallArgv,
|
|
529
559
|
buildSpawnPlan,
|
|
530
560
|
cleanStalePartialInstall,
|
|
531
561
|
enforceUpdateRequired,
|
|
562
|
+
isBunManagedPackageDir,
|
|
532
563
|
isPnpmManagedPackageDir,
|
|
533
564
|
npmPrefixFromPackageDir,
|
|
534
565
|
nudgeUpdateRecommended,
|