@indigoai-us/hq-cli 5.118.0 → 5.118.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 +35 -0
- package/dist/command-catalog.generated.d.ts +7 -1
- package/dist/command-catalog.generated.js +9 -1
- package/dist/commands/agents.d.ts +2 -1
- package/dist/commands/agents.js +16 -4
- package/dist/lib/billing/plan-lock.d.ts +37 -9
- package/dist/lib/billing/plan-lock.js +127 -30
- package/dist/lib/plan-limit-nag.d.ts +8 -0
- package/dist/lib/plan-limit-nag.js +22 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.118.2] — 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The locked-workspace notice now says what you can still do. A locked
|
|
10
|
+
workspace is read-only for new things, but deleting still works — which
|
|
11
|
+
matters, because every fix the notice lists (remove members, disconnect
|
|
12
|
+
integrations, delete secrets, deprovision agents) is a delete. The notice
|
|
13
|
+
states that outright instead of leaving "read-only" to be guessed at.
|
|
14
|
+
|
|
15
|
+
- The plan-limit box names the plan: "HQ Starter plan limit exceeded" rather
|
|
16
|
+
than "HQ plan limit exceeded".
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `hq agents provision --model` now sets the brain model during creation. A
|
|
21
|
+
model such as `gpt-5.5` or `grok-4.6` selects the agents-v2 runtime unless
|
|
22
|
+
`--provider` is specified, avoiding a later box recreation to change brains.
|
|
23
|
+
|
|
24
|
+
## [5.118.1] — 2026-09-17
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The locked-workspace notice now names the real reason. A workspace locked
|
|
29
|
+
because of secrets or agents no longer reads "over its 5-member limit"; each
|
|
30
|
+
reason gets its own explanation, its own Starter cap and its own fix, and a
|
|
31
|
+
lock with no recognised reason says only that the workspace is over the
|
|
32
|
+
Starter limits. The plan-limit warning also names the dimension it is quoting
|
|
33
|
+
("users at 9/10 (90%) (members)").
|
|
34
|
+
|
|
35
|
+
- You can now remove an HQ Agents v2 agent from the CLI without bypassing its
|
|
36
|
+
safety check. `hq agents rm` accepts `--confirm-destroy-agents-v2
|
|
37
|
+
<instance-id>` alongside `--yes` and sends only the instance ID you name; HQ
|
|
38
|
+
still refuses an ID that does not match the live box.
|
|
39
|
+
|
|
5
40
|
## [5.118.0] — 2026-09-16
|
|
6
41
|
|
|
7
42
|
### Added
|
|
@@ -3812,7 +3812,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3812
3812
|
readonly description: "Agent slug (defaults to a slug of <name>)";
|
|
3813
3813
|
}, {
|
|
3814
3814
|
readonly flags: "--provider <provider>";
|
|
3815
|
-
readonly description: "Runtime:
|
|
3815
|
+
readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default";
|
|
3816
|
+
}, {
|
|
3817
|
+
readonly flags: "--model <model>";
|
|
3818
|
+
readonly description: "Brain model, for example gpt-5.5 or grok-4.6";
|
|
3816
3819
|
}, {
|
|
3817
3820
|
readonly flags: "--auth-mode <mode>";
|
|
3818
3821
|
readonly description: "Auth: subscription | apiKey (default subscription)";
|
|
@@ -4023,6 +4026,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4023
4026
|
}, {
|
|
4024
4027
|
readonly flags: "--yes";
|
|
4025
4028
|
readonly description: "Confirm the irreversible teardown (required)";
|
|
4029
|
+
}, {
|
|
4030
|
+
readonly flags: "--confirm-destroy-agents-v2 <instance-id>";
|
|
4031
|
+
readonly description: "Confirm agents-v2 teardown by naming the live EC2 instance ID";
|
|
4026
4032
|
}];
|
|
4027
4033
|
readonly subcommands: readonly [];
|
|
4028
4034
|
}, {
|
|
@@ -4926,7 +4926,11 @@ export const COMMAND_CATALOG = [
|
|
|
4926
4926
|
},
|
|
4927
4927
|
{
|
|
4928
4928
|
"flags": "--provider <provider>",
|
|
4929
|
-
"description": "Runtime:
|
|
4929
|
+
"description": "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default"
|
|
4930
|
+
},
|
|
4931
|
+
{
|
|
4932
|
+
"flags": "--model <model>",
|
|
4933
|
+
"description": "Brain model, for example gpt-5.5 or grok-4.6"
|
|
4930
4934
|
},
|
|
4931
4935
|
{
|
|
4932
4936
|
"flags": "--auth-mode <mode>",
|
|
@@ -5205,6 +5209,10 @@ export const COMMAND_CATALOG = [
|
|
|
5205
5209
|
{
|
|
5206
5210
|
"flags": "--yes",
|
|
5207
5211
|
"description": "Confirm the irreversible teardown (required)"
|
|
5212
|
+
},
|
|
5213
|
+
{
|
|
5214
|
+
"flags": "--confirm-destroy-agents-v2 <instance-id>",
|
|
5215
|
+
"description": "Confirm agents-v2 teardown by naming the live EC2 instance ID"
|
|
5208
5216
|
}
|
|
5209
5217
|
],
|
|
5210
5218
|
"subcommands": []
|
|
@@ -171,6 +171,7 @@ export interface ProvisionAgentInput {
|
|
|
171
171
|
slug: string;
|
|
172
172
|
codexAuthMode: "subscription" | "apiKey";
|
|
173
173
|
provider?: "codex" | "grok" | "claude" | "agents-v2";
|
|
174
|
+
codexModel?: string;
|
|
174
175
|
codexApiKey?: string;
|
|
175
176
|
idempotencyKey: string;
|
|
176
177
|
title?: string;
|
|
@@ -258,7 +259,7 @@ export type AgentTerminalResponse = ({
|
|
|
258
259
|
};
|
|
259
260
|
export declare function openAgentTerminal(token: string, agentUid: string, body: Record<string, unknown>): Promise<AgentTerminalResponse>;
|
|
260
261
|
export declare function retryAgent(token: string, agentUid: string): Promise<Record<string, unknown>>;
|
|
261
|
-
export declare function deprovisionAgent(token: string, agentUid: string): Promise<{
|
|
262
|
+
export declare function deprovisionAgent(token: string, agentUid: string, confirmDestroyAgentsV2?: string): Promise<{
|
|
262
263
|
uid: string;
|
|
263
264
|
setupState?: string;
|
|
264
265
|
terminal?: boolean;
|
package/dist/commands/agents.js
CHANGED
|
@@ -564,11 +564,14 @@ export async function retryAgent(token, agentUid) {
|
|
|
564
564
|
method: "POST",
|
|
565
565
|
});
|
|
566
566
|
}
|
|
567
|
-
export async function deprovisionAgent(token, agentUid) {
|
|
567
|
+
export async function deprovisionAgent(token, agentUid, confirmDestroyAgentsV2) {
|
|
568
568
|
return agentsRequest({
|
|
569
569
|
token,
|
|
570
570
|
path: `/v1/agents/${encodeURIComponent(agentUid)}`,
|
|
571
571
|
method: "DELETE",
|
|
572
|
+
body: confirmDestroyAgentsV2 === undefined
|
|
573
|
+
? undefined
|
|
574
|
+
: { confirmDestroyAgentsV2 },
|
|
572
575
|
});
|
|
573
576
|
}
|
|
574
577
|
export async function listAgentJobs(token, agentUid) {
|
|
@@ -1071,7 +1074,8 @@ export function registerAgentsCommand(program) {
|
|
|
1071
1074
|
.description("Provision a new cloud agent (company-specific monthly price shown before creation)")
|
|
1072
1075
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1073
1076
|
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
1074
|
-
.option("--provider <provider>", "Runtime:
|
|
1077
|
+
.option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default")
|
|
1078
|
+
.option("--model <model>", "Brain model, for example gpt-5.5 or grok-4.6")
|
|
1075
1079
|
.option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
|
|
1076
1080
|
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
1077
1081
|
.option("--title <title>", "Org-chart job title")
|
|
@@ -1085,7 +1089,13 @@ export function registerAgentsCommand(program) {
|
|
|
1085
1089
|
// unrecognised value, silently provisioning a billable box the operator
|
|
1086
1090
|
// did not ask for. They now exit(1) instead — see parseEnumOption.
|
|
1087
1091
|
const authMode = parseEnumOption(opts.authMode, VALID_AUTH_MODES, "--auth-mode") ?? "subscription";
|
|
1088
|
-
const
|
|
1092
|
+
const explicitProvider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
|
|
1093
|
+
if (opts.model !== undefined &&
|
|
1094
|
+
!/^[A-Za-z0-9._-]{1,64}$/.test(opts.model)) {
|
|
1095
|
+
console.error(chalk.red("--model must be a model identifier (letters, digits, dot, dash, underscore; max 64 chars)."));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const provider = explicitProvider ?? (opts.model ? "agents-v2" : undefined);
|
|
1089
1099
|
// claude is subscription-only on hq-pro (rejectIncompatibleProviderAuthMode
|
|
1090
1100
|
// returns AGENT_PROVIDER_INCOMPATIBLE_WITH_AUTH_MODE). Catch it here so the
|
|
1091
1101
|
// operator gets a direct message instead of a 400 from the control plane
|
|
@@ -1136,6 +1146,7 @@ export function registerAgentsCommand(program) {
|
|
|
1136
1146
|
slug,
|
|
1137
1147
|
codexAuthMode: authMode,
|
|
1138
1148
|
...(provider ? { provider } : {}),
|
|
1149
|
+
...(opts.model ? { codexModel: opts.model } : {}),
|
|
1139
1150
|
...(codexApiKey ? { codexApiKey } : {}),
|
|
1140
1151
|
idempotencyKey,
|
|
1141
1152
|
...(opts.title ? { title: opts.title } : {}),
|
|
@@ -1477,6 +1488,7 @@ export function registerAgentsCommand(program) {
|
|
|
1477
1488
|
.description("Deprovision (permanently tear down) an agent")
|
|
1478
1489
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1479
1490
|
.option("--yes", "Confirm the irreversible teardown (required)")
|
|
1491
|
+
.option("--confirm-destroy-agents-v2 <instance-id>", "Confirm agents-v2 teardown by naming the live EC2 instance ID")
|
|
1480
1492
|
.action(async function (agentUid, opts) {
|
|
1481
1493
|
if (!opts.yes) {
|
|
1482
1494
|
console.error(chalk.yellow(`This will permanently deprovision agent ${agentUid} and tear down its ` +
|
|
@@ -1486,7 +1498,7 @@ export function registerAgentsCommand(program) {
|
|
|
1486
1498
|
}
|
|
1487
1499
|
try {
|
|
1488
1500
|
const token = (await resolveVaultCredential()).token;
|
|
1489
|
-
const result = await deprovisionAgent(token, agentUid);
|
|
1501
|
+
const result = await deprovisionAgent(token, agentUid, opts.confirmDestroyAgentsV2);
|
|
1490
1502
|
if (result.terminal === false) {
|
|
1491
1503
|
console.log(chalk.yellow(`Deprovision in progress for agent ${agentUid} (not yet fully torn down — re-run to continue).`));
|
|
1492
1504
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
|
|
3
3
|
* of the workspace plan lock.
|
|
4
4
|
*
|
|
5
|
-
* Starter (free) workspaces are capped
|
|
6
|
-
*
|
|
5
|
+
* Starter (free) workspaces are capped on four locking dimensions — members,
|
|
6
|
+
* integrations, secrets and agents (owner decision 7, 2026-09-17; deployments
|
|
7
|
+
* and storage nag but never lock). Going over locks the workspace immediately: it becomes read-only until the owner
|
|
7
8
|
* trims back under the caps or upgrades to HQ Workforce. The lock decision is
|
|
8
9
|
* NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
|
|
9
10
|
* truth and ships the answer on `GET /membership/me` as a per-company
|
|
@@ -15,17 +16,33 @@
|
|
|
15
16
|
* nothing here ever infers a lock (or an unlock) from missing data
|
|
16
17
|
* (hq-absent-field-never-means-constraining-value).
|
|
17
18
|
*/
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Mirror of hq-pro's `PlanLockReason`. Owner decision 7 (2026-09-17) fixes the
|
|
21
|
+
* locking set at these four: `deployments` and `storageBytes` are nag-only and
|
|
22
|
+
* never appear here. An unrecognised reason is dropped by `parsePlanLock`, so a
|
|
23
|
+
* server that adds a fifth dimension renders as the generic line rather than as
|
|
24
|
+
* a false claim about members.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PLAN_LOCK_REASONS: readonly ["users", "integrations", "secrets", "agents"];
|
|
27
|
+
export type PlanLockReason = (typeof PLAN_LOCK_REASONS)[number];
|
|
20
28
|
/** Mirror of hq-pro's `PlanLock` wire shape (see src/billing/plan-lock.ts). */
|
|
21
29
|
export interface PlanLock {
|
|
22
30
|
locked: boolean;
|
|
23
31
|
reasons: PlanLockReason[];
|
|
24
32
|
upgradeUrl: string;
|
|
25
|
-
fixOptions:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
fixOptions: PlanLockFixOptions;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The remedies hq-pro ships with the lock. The two original fields are always
|
|
37
|
+
* present (defaulted when the server omits them) because shipped copy reads
|
|
38
|
+
* them unconditionally; every dimension added since is optional, so an older
|
|
39
|
+
* server simply leaves it absent and the renderer quotes the Starter cap.
|
|
40
|
+
*/
|
|
41
|
+
export interface PlanLockFixOptions {
|
|
42
|
+
removeMembersTo: number;
|
|
43
|
+
disconnectIntegrations: boolean;
|
|
44
|
+
removeSecretsTo?: number;
|
|
45
|
+
deprovisionAgentsTo?: number;
|
|
29
46
|
}
|
|
30
47
|
/** Optional member decoration for the notice ("7 of 5 members"). */
|
|
31
48
|
export interface PlanLockMembers {
|
|
@@ -47,6 +64,10 @@ export interface PlanLockStatus {
|
|
|
47
64
|
}
|
|
48
65
|
/** Starter member cap quoted when the server did not send `removeMembersTo`. */
|
|
49
66
|
export declare const STARTER_MEMBER_TARGET = 5;
|
|
67
|
+
/** Starter secret cap quoted when the server did not send `removeSecretsTo`. */
|
|
68
|
+
export declare const STARTER_SECRET_TARGET = 10;
|
|
69
|
+
/** Starter agent cap quoted when the server did not send `deprovisionAgentsTo`. */
|
|
70
|
+
export declare const STARTER_AGENT_TARGET = 0;
|
|
50
71
|
/** Upgrade destination quoted when the server did not send one. */
|
|
51
72
|
export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
52
73
|
/** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
|
|
@@ -78,10 +99,17 @@ export declare function selectMemberUsage(body: unknown): PlanLockMembers | null
|
|
|
78
99
|
export declare function fetchPlanLockStatus(token: string, companyRef: string, opts?: {
|
|
79
100
|
timeoutMs?: number;
|
|
80
101
|
}): Promise<PlanLockStatus | null>;
|
|
102
|
+
/** Why the workspace locked, one clause per reason. */
|
|
103
|
+
export declare function reasonLabel(reason: PlanLockReason): string;
|
|
81
104
|
/**
|
|
82
105
|
* The full WORKSPACE LOCKED block: why it locked, where the workspace stands
|
|
83
|
-
* against the cap, and the
|
|
106
|
+
* against the cap, and the ways out. Plain text — colour is applied by the
|
|
84
107
|
* caller so scripts capturing stdout get a clean block.
|
|
108
|
+
*
|
|
109
|
+
* Every line is derived from `lock.reasons`. A workspace locked on secrets is
|
|
110
|
+
* never told it has too many members, and an empty reason list (a server
|
|
111
|
+
* dimension this CLI does not know) renders a generic line rather than a claim
|
|
112
|
+
* about a dimension nobody measured.
|
|
85
113
|
*/
|
|
86
114
|
export declare function renderPlanLockNotice(status: PlanLockStatus): string;
|
|
87
115
|
/**
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
|
|
3
3
|
* of the workspace plan lock.
|
|
4
4
|
*
|
|
5
|
-
* Starter (free) workspaces are capped
|
|
6
|
-
*
|
|
5
|
+
* Starter (free) workspaces are capped on four locking dimensions — members,
|
|
6
|
+
* integrations, secrets and agents (owner decision 7, 2026-09-17; deployments
|
|
7
|
+
* and storage nag but never lock). Going over locks the workspace immediately: it becomes read-only until the owner
|
|
7
8
|
* trims back under the caps or upgrades to HQ Workforce. The lock decision is
|
|
8
9
|
* NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
|
|
9
10
|
* truth and ships the answer on `GET /membership/me` as a per-company
|
|
@@ -17,12 +18,35 @@
|
|
|
17
18
|
*/
|
|
18
19
|
import chalk from "chalk";
|
|
19
20
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
21
|
+
/**
|
|
22
|
+
* Mirror of hq-pro's `PlanLockReason`. Owner decision 7 (2026-09-17) fixes the
|
|
23
|
+
* locking set at these four: `deployments` and `storageBytes` are nag-only and
|
|
24
|
+
* never appear here. An unrecognised reason is dropped by `parsePlanLock`, so a
|
|
25
|
+
* server that adds a fifth dimension renders as the generic line rather than as
|
|
26
|
+
* a false claim about members.
|
|
27
|
+
*/
|
|
28
|
+
export const PLAN_LOCK_REASONS = [
|
|
29
|
+
"users",
|
|
30
|
+
"integrations",
|
|
31
|
+
"secrets",
|
|
32
|
+
"agents",
|
|
33
|
+
];
|
|
34
|
+
function isPlanLockReason(value) {
|
|
35
|
+
return PLAN_LOCK_REASONS.includes(value);
|
|
36
|
+
}
|
|
20
37
|
/** Starter member cap quoted when the server did not send `removeMembersTo`. */
|
|
21
38
|
export const STARTER_MEMBER_TARGET = 5;
|
|
39
|
+
/** Starter secret cap quoted when the server did not send `removeSecretsTo`. */
|
|
40
|
+
export const STARTER_SECRET_TARGET = 10;
|
|
41
|
+
/** Starter agent cap quoted when the server did not send `deprovisionAgentsTo`. */
|
|
42
|
+
export const STARTER_AGENT_TARGET = 0;
|
|
22
43
|
/** Upgrade destination quoted when the server did not send one. */
|
|
23
44
|
export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
24
45
|
/** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
|
|
25
46
|
export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
47
|
+
function finiteNumber(value) {
|
|
48
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
49
|
+
}
|
|
26
50
|
function asRecord(value) {
|
|
27
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
28
52
|
return null;
|
|
@@ -43,25 +67,32 @@ export function parsePlanLock(value) {
|
|
|
43
67
|
const reasons = [];
|
|
44
68
|
if (Array.isArray(rec.reasons)) {
|
|
45
69
|
for (const reason of rec.reasons) {
|
|
46
|
-
if (reason
|
|
70
|
+
if (isPlanLockReason(reason))
|
|
47
71
|
reasons.push(reason);
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
const fix = asRecord(rec.fixOptions);
|
|
51
|
-
const removeMembersTo =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
const removeMembersTo = finiteNumber(fix?.removeMembersTo) ?? STARTER_MEMBER_TARGET;
|
|
76
|
+
const removeSecretsTo = finiteNumber(fix?.removeSecretsTo);
|
|
77
|
+
const deprovisionAgentsTo = finiteNumber(fix?.deprovisionAgentsTo);
|
|
78
|
+
const fixOptions = {
|
|
79
|
+
removeMembersTo,
|
|
80
|
+
disconnectIntegrations: fix?.disconnectIntegrations === true,
|
|
81
|
+
};
|
|
82
|
+
// Absent stays absent: an omitted remedy target is UNKNOWN, and the renderer
|
|
83
|
+
// quotes the published Starter cap rather than inventing a server answer.
|
|
84
|
+
if (removeSecretsTo !== null)
|
|
85
|
+
fixOptions.removeSecretsTo = removeSecretsTo;
|
|
86
|
+
if (deprovisionAgentsTo !== null) {
|
|
87
|
+
fixOptions.deprovisionAgentsTo = deprovisionAgentsTo;
|
|
88
|
+
}
|
|
55
89
|
return {
|
|
56
90
|
locked: rec.locked,
|
|
57
91
|
reasons,
|
|
58
92
|
upgradeUrl: typeof rec.upgradeUrl === "string" && rec.upgradeUrl.trim().length > 0
|
|
59
93
|
? rec.upgradeUrl.trim()
|
|
60
94
|
: DEFAULT_UPGRADE_URL,
|
|
61
|
-
fixOptions
|
|
62
|
-
removeMembersTo,
|
|
63
|
-
disconnectIntegrations: fix?.disconnectIntegrations === true,
|
|
64
|
-
},
|
|
95
|
+
fixOptions,
|
|
65
96
|
};
|
|
66
97
|
}
|
|
67
98
|
/**
|
|
@@ -163,35 +194,100 @@ export async function fetchPlanLockStatus(token, companyRef, opts = {}) {
|
|
|
163
194
|
checkedAt: new Date().toISOString(),
|
|
164
195
|
};
|
|
165
196
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
/** Why the workspace locked, one clause per reason. */
|
|
198
|
+
export function reasonLabel(reason) {
|
|
199
|
+
switch (reason) {
|
|
200
|
+
case "users":
|
|
201
|
+
return "too many members";
|
|
202
|
+
case "integrations":
|
|
203
|
+
return "integrations are not included on Starter";
|
|
204
|
+
case "secrets":
|
|
205
|
+
return "too many secrets";
|
|
206
|
+
case "agents":
|
|
207
|
+
return "agents are not included on Starter";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function secretTarget(lock) {
|
|
211
|
+
return lock.fixOptions.removeSecretsTo ?? STARTER_SECRET_TARGET;
|
|
212
|
+
}
|
|
213
|
+
function agentTarget(lock) {
|
|
214
|
+
return lock.fixOptions.deprovisionAgentsTo ?? STARTER_AGENT_TARGET;
|
|
215
|
+
}
|
|
216
|
+
/** The remedy sentence for one reason — what the owner does to clear it. */
|
|
217
|
+
function reasonRemedy(reason, lock) {
|
|
218
|
+
switch (reason) {
|
|
219
|
+
case "users":
|
|
220
|
+
return `remove members until you are at ${lock.fixOptions.removeMembersTo} or fewer`;
|
|
221
|
+
case "integrations":
|
|
222
|
+
return "disconnect the workspace's integrations";
|
|
223
|
+
case "secrets":
|
|
224
|
+
return `delete secrets until you are at ${secretTarget(lock)} or fewer`;
|
|
225
|
+
case "agents": {
|
|
226
|
+
const target = agentTarget(lock);
|
|
227
|
+
return target > 0
|
|
228
|
+
? `deprovision agents until you are at ${target} or fewer`
|
|
229
|
+
: "deprovision the workspace's agents";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** The short per-reason clause used inside the one-line turn notice. */
|
|
234
|
+
function reasonDetail(reason, status) {
|
|
235
|
+
const { lock, members } = status;
|
|
236
|
+
switch (reason) {
|
|
237
|
+
case "users": {
|
|
238
|
+
const target = lock.fixOptions.removeMembersTo;
|
|
239
|
+
return members
|
|
240
|
+
? `${members.used} of ${target} members`
|
|
241
|
+
: `over its ${target}-member limit`;
|
|
242
|
+
}
|
|
243
|
+
case "integrations":
|
|
244
|
+
return "integrations are not included on Starter";
|
|
245
|
+
case "secrets":
|
|
246
|
+
return `over its ${secretTarget(status.lock)}-secret limit`;
|
|
247
|
+
case "agents":
|
|
248
|
+
return "agents are not included on Starter";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function capitalize(text) {
|
|
252
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
170
253
|
}
|
|
171
254
|
/**
|
|
172
255
|
* The full WORKSPACE LOCKED block: why it locked, where the workspace stands
|
|
173
|
-
* against the cap, and the
|
|
256
|
+
* against the cap, and the ways out. Plain text — colour is applied by the
|
|
174
257
|
* caller so scripts capturing stdout get a clean block.
|
|
258
|
+
*
|
|
259
|
+
* Every line is derived from `lock.reasons`. A workspace locked on secrets is
|
|
260
|
+
* never told it has too many members, and an empty reason list (a server
|
|
261
|
+
* dimension this CLI does not know) renders a generic line rather than a claim
|
|
262
|
+
* about a dimension nobody measured.
|
|
175
263
|
*/
|
|
176
264
|
export function renderPlanLockNotice(status) {
|
|
177
265
|
const { lock, members, companySlug } = status;
|
|
178
|
-
const target = lock.fixOptions.removeMembersTo;
|
|
179
266
|
const lines = [];
|
|
180
267
|
lines.push(`WORKSPACE LOCKED — ${companySlug} is over its Starter plan.`);
|
|
181
268
|
const reasons = lock.reasons.length
|
|
182
269
|
? lock.reasons.map(reasonLabel).join("; ")
|
|
183
270
|
: "over the Starter plan limits";
|
|
184
271
|
lines.push(` Why: ${reasons}.`);
|
|
185
|
-
if (
|
|
186
|
-
|
|
272
|
+
if (lock.reasons.includes("users")) {
|
|
273
|
+
const target = lock.fixOptions.removeMembersTo;
|
|
274
|
+
lines.push(members
|
|
275
|
+
? ` Members: ${members.used} of ${target}.`
|
|
276
|
+
: ` Members allowed on Starter: ${target}.`);
|
|
277
|
+
}
|
|
278
|
+
if (lock.reasons.includes("secrets")) {
|
|
279
|
+
lines.push(` Secrets allowed on Starter: ${secretTarget(lock)}.`);
|
|
187
280
|
}
|
|
188
|
-
|
|
189
|
-
lines.push(`
|
|
281
|
+
if (lock.reasons.includes("agents")) {
|
|
282
|
+
lines.push(` Agents allowed on Starter: ${agentTarget(lock)}.`);
|
|
190
283
|
}
|
|
191
|
-
lines.push(" This workspace is read-only until it is fixed
|
|
192
|
-
lines.push(
|
|
193
|
-
|
|
194
|
-
|
|
284
|
+
lines.push(" Nothing has been deleted. This workspace is read-only until it is fixed:");
|
|
285
|
+
lines.push(" you can still delete things, but you cannot create, invite or upload.");
|
|
286
|
+
lines.push(" Two ways to fix it:");
|
|
287
|
+
const remedy = lock.reasons.length
|
|
288
|
+
? lock.reasons.map((reason) => reasonRemedy(reason, lock)).join(", and ")
|
|
289
|
+
: "come back under the Starter plan limits";
|
|
290
|
+
lines.push(` 1. ${capitalize(remedy)}.`);
|
|
195
291
|
lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
|
|
196
292
|
lines.push(` Upgrade: ${lock.upgradeUrl}`);
|
|
197
293
|
return lines.join("\n");
|
|
@@ -201,11 +297,12 @@ export function renderPlanLockNotice(status) {
|
|
|
201
297
|
* Kept to a single line on purpose — it repeats each turn.
|
|
202
298
|
*/
|
|
203
299
|
export function renderPlanLockLine(status) {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
300
|
+
const detail = status.lock.reasons.length
|
|
301
|
+
? status.lock.reasons
|
|
302
|
+
.map((reason) => reasonDetail(reason, status))
|
|
303
|
+
.join("; ")
|
|
304
|
+
: "over the Starter plan limits";
|
|
305
|
+
return (`Company ${status.companySlug} is locked on Starter (${detail}). ` +
|
|
209
306
|
`Writes to HQ cloud will fail until fixed: ${status.lock.upgradeUrl}`);
|
|
210
307
|
}
|
|
211
308
|
/**
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* writes to stdout. Env off-switch: `HQ_NO_PLAN_LIMIT_NAG=1`.
|
|
17
17
|
*/
|
|
18
18
|
export declare const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
|
|
19
|
+
/**
|
|
20
|
+
* Plain-English name for each plan-limit resource key, so a nag line says which
|
|
21
|
+
* dimension is tight rather than only the wire key. The key itself stays in the
|
|
22
|
+
* copy — hooks and support transcripts match on it — and the label is appended
|
|
23
|
+
* only when it differs. An unknown key (a resource this CLI predates) renders
|
|
24
|
+
* as the bare key rather than a guess.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PLAN_LIMIT_DIMENSION_LABELS: Record<string, string>;
|
|
19
27
|
export interface PlanLimitEntry {
|
|
20
28
|
used: number;
|
|
21
29
|
limit: number;
|
|
@@ -21,6 +21,25 @@ import * as os from "node:os";
|
|
|
21
21
|
import * as path from "node:path";
|
|
22
22
|
export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
|
|
23
23
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Plain-English name for each plan-limit resource key, so a nag line says which
|
|
26
|
+
* dimension is tight rather than only the wire key. The key itself stays in the
|
|
27
|
+
* copy — hooks and support transcripts match on it — and the label is appended
|
|
28
|
+
* only when it differs. An unknown key (a resource this CLI predates) renders
|
|
29
|
+
* as the bare key rather than a guess.
|
|
30
|
+
*/
|
|
31
|
+
export const PLAN_LIMIT_DIMENSION_LABELS = {
|
|
32
|
+
users: "members",
|
|
33
|
+
integrations: "integrations",
|
|
34
|
+
secrets: "secrets",
|
|
35
|
+
agents: "agents",
|
|
36
|
+
deployments: "deployments",
|
|
37
|
+
storageBytes: "storage",
|
|
38
|
+
};
|
|
39
|
+
function dimensionSuffix(key) {
|
|
40
|
+
const label = PLAN_LIMIT_DIMENSION_LABELS[key];
|
|
41
|
+
return label && label !== key ? ` (${label})` : "";
|
|
42
|
+
}
|
|
24
43
|
/** Module-level last-seen cell — overwritten by each successful parse. */
|
|
25
44
|
let lastSeen = null;
|
|
26
45
|
/** Session dedupe for the ≥80% one-line warning. */
|
|
@@ -142,7 +161,7 @@ function formatPct(entry) {
|
|
|
142
161
|
return `${Math.round((entry.used / entry.limit) * 100)}%`;
|
|
143
162
|
}
|
|
144
163
|
function formatEntryLine(key, entry) {
|
|
145
|
-
return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})`;
|
|
164
|
+
return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})${dimensionSuffix(key)}`;
|
|
146
165
|
}
|
|
147
166
|
function readShownAt(statePath) {
|
|
148
167
|
try {
|
|
@@ -171,9 +190,9 @@ function withinDayWindow(shownAt, nowMs) {
|
|
|
171
190
|
return nowMs - shownAt < DAY_MS;
|
|
172
191
|
}
|
|
173
192
|
function buildOverBox(overEntries, upgradeUrl) {
|
|
174
|
-
const title = "⚠ HQ plan limit exceeded";
|
|
193
|
+
const title = "⚠ HQ Starter plan limit exceeded";
|
|
175
194
|
const upgrade = `Upgrade: ${upgradeUrl}`;
|
|
176
|
-
const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}`);
|
|
195
|
+
const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}${dimensionSuffix(key)}`);
|
|
177
196
|
const contentLines = [title, "", ...resourceLines, "", upgrade];
|
|
178
197
|
const innerWidth = Math.max(...contentLines.map((l) => l.length), 40);
|
|
179
198
|
const top = `┌${"─".repeat(innerWidth + 2)}┐`;
|