@indigoai-us/hq-cli 5.345.156 → 5.345.160
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 +255 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +535 -10
- package/dist/command-catalog.generated.js +678 -31
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.98","bundleSha256":"70fe5497fffdbb678370515f8b7df63438d7a59a95467102307459e78f9f4715","formatVersion":1}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* item until the bot acks (`POST /v1/agents/{uid}/inbox/{id}/ack`), and the
|
|
16
16
|
* poller dedupes by id so the same item is never mirrored twice.
|
|
17
17
|
*/
|
|
18
|
+
import { createWakeSecretResolver } from "../wake-secrets.js";
|
|
18
19
|
import * as fs from "node:fs";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
@@ -247,7 +248,7 @@ async function maybeWake(deps, freshCount) {
|
|
|
247
248
|
return;
|
|
248
249
|
const notice = buildWakeNotice(deps.agentUid, reason, pending.map((e) => e.id), now);
|
|
249
250
|
try {
|
|
250
|
-
await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now });
|
|
251
|
+
await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now, resolveHeaderSecrets: createWakeSecretResolver(deps.companySlug, deps.getToken, deps.apiBaseUrl) });
|
|
251
252
|
}
|
|
252
253
|
catch (err) {
|
|
253
254
|
deps.log("warn", `wake failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Wake-only vault reads: use the kit machine token, never cache plaintext. */
|
|
2
|
+
import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
3
|
+
export declare function createWakeSecretResolver(companySlug: string | undefined, getToken: () => Promise<string>, baseUrl: string, deps?: {
|
|
4
|
+
getCompanyUid: typeof getCompanyUid;
|
|
5
|
+
vaultApiFetch: typeof vaultApiFetch;
|
|
6
|
+
}): (names: string[]) => Promise<Map<string, string>>;
|
|
7
|
+
//# sourceMappingURL=wake-secrets.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Wake-only vault reads: use the kit machine token, never cache plaintext. */
|
|
2
|
+
import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
|
|
3
|
+
export function createWakeSecretResolver(companySlug, getToken, baseUrl, deps = { getCompanyUid, vaultApiFetch }) {
|
|
4
|
+
return async (names) => {
|
|
5
|
+
if (!companySlug)
|
|
6
|
+
throw new Error("Wake secrets require an enrolled company");
|
|
7
|
+
const token = await getToken();
|
|
8
|
+
const companyUid = await deps.getCompanyUid(token, companySlug, baseUrl);
|
|
9
|
+
const response = await deps.vaultApiFetch({
|
|
10
|
+
token, baseUrl, path: `/secrets/${encodeURIComponent(companyUid)}/load`, method: "POST",
|
|
11
|
+
body: { names, usage: { channel: "exec", reason: "agent kit webhook wake" } },
|
|
12
|
+
signal: AbortSignal.timeout(15_000),
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error("Wake secret authorization failed");
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
if (!Array.isArray(data.secrets) || !Array.isArray(data.errors) || data.errors.length) {
|
|
18
|
+
throw new Error("Wake secret lookup failed");
|
|
19
|
+
}
|
|
20
|
+
const values = new Map();
|
|
21
|
+
for (const row of data.secrets) {
|
|
22
|
+
if (!row || typeof row.name !== "string" || !names.includes(row.name) || values.has(row.name) ||
|
|
23
|
+
typeof row.value !== "string" || row.revokedAt != null)
|
|
24
|
+
throw new Error("Invalid wake secret response");
|
|
25
|
+
values.set(row.name, row.value);
|
|
26
|
+
}
|
|
27
|
+
if (values.size !== new Set(names).size)
|
|
28
|
+
throw new Error("Wake secret missing");
|
|
29
|
+
return values;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=wake-secrets.js.map
|
|
@@ -30,12 +30,15 @@ export type WakeConfig = {
|
|
|
30
30
|
kind: "url";
|
|
31
31
|
url: string;
|
|
32
32
|
retryMs: number;
|
|
33
|
+
headerSecrets?: Record<string, string>;
|
|
33
34
|
} | {
|
|
34
35
|
kind: "command";
|
|
35
36
|
command: string[];
|
|
36
37
|
timeoutMs: number;
|
|
37
38
|
retryMs: number;
|
|
38
39
|
};
|
|
40
|
+
/** Store references only. Reject ambiguous and transport-controlled headers. */
|
|
41
|
+
export declare function parseHeaderSecrets(specs: string[]): Record<string, string>;
|
|
39
42
|
export interface WakeNotice {
|
|
40
43
|
event: "hq.inbox.pending";
|
|
41
44
|
agentUid: string;
|
|
@@ -59,6 +62,7 @@ export interface WakeDeps {
|
|
|
59
62
|
agentUid: string;
|
|
60
63
|
log: KitLogger;
|
|
61
64
|
now?: () => Date;
|
|
65
|
+
resolveHeaderSecrets?: (names: string[]) => Promise<Map<string, string>>;
|
|
62
66
|
fetchImpl?: typeof fetch;
|
|
63
67
|
spawnImpl?: typeof spawn;
|
|
64
68
|
pidAlive?: (pid: number | undefined) => boolean;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* The inbox poller fires the wake when new items are mirrored, and re-fires
|
|
19
19
|
* every `retryMs` while items stay pending, so a missed webhook heals.
|
|
20
20
|
*/
|
|
21
|
+
import { SECRET_NAME_PATTERN } from "../../commands/_patterns.js";
|
|
21
22
|
import { spawn } from "node:child_process";
|
|
22
23
|
import * as fs from "node:fs";
|
|
23
24
|
import * as path from "node:path";
|
|
@@ -27,6 +28,33 @@ export const WAKE_STATE_NAME = "wake-state.json";
|
|
|
27
28
|
export const DEFAULT_WAKE_RETRY_MS = 10 * 60_000;
|
|
28
29
|
export const WAKE_URL_TIMEOUT_MS = 15_000;
|
|
29
30
|
export const DEFAULT_WAKE_COMMAND_TIMEOUT_MS = 10 * 60_000;
|
|
31
|
+
/** Store references only. Reject ambiguous and transport-controlled headers. */
|
|
32
|
+
export function parseHeaderSecrets(specs) {
|
|
33
|
+
const result = {};
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const spec of specs) {
|
|
36
|
+
const at = spec.indexOf("=");
|
|
37
|
+
const header = spec.slice(0, at);
|
|
38
|
+
const name = spec.slice(at + 1);
|
|
39
|
+
const lower = header.toLowerCase();
|
|
40
|
+
if (at < 1 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(header) || !SECRET_NAME_PATTERN.test(name) ||
|
|
41
|
+
["host", "content-length", "transfer-encoding", "connection", "content-type", "user-agent", "__proto__"].includes(lower) || seen.has(lower)) {
|
|
42
|
+
throw new Error("Invalid --header-secret: use a unique Header-Name=VAULT_SECRET_NAME reference");
|
|
43
|
+
}
|
|
44
|
+
seen.add(lower);
|
|
45
|
+
result[header] = name;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function validateHeaderSecrets(raw) {
|
|
50
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
51
|
+
throw new Error("Invalid header secret references");
|
|
52
|
+
return parseHeaderSecrets(Object.entries(raw).map(([header, name]) => {
|
|
53
|
+
if (typeof name !== "string")
|
|
54
|
+
throw new Error("Invalid header secret reference");
|
|
55
|
+
return `${header}=${name}`;
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
30
58
|
export const WAKE_INSTRUCTIONS = "New HQ messages are waiting. Run `hq agent inbox` (or the hq_inbox_read tool). Reply where each message " +
|
|
31
59
|
"arrived: if it has a channelId, post in that channel with `hq dm <channelId> \"…\"`; otherwise reply to the " +
|
|
32
60
|
"sender with `hq dm <sender-uid> \"…\"` (or hq_dm_send). Do not reply to another bot's greeting, thanks or " +
|
|
@@ -82,7 +110,7 @@ export function readWakeConfig(paths) {
|
|
|
82
110
|
const retryMs = typeof raw.retryMs === "number" && raw.retryMs >= 60_000 ? raw.retryMs : DEFAULT_WAKE_RETRY_MS;
|
|
83
111
|
if (raw.kind === "url" && typeof raw.url === "string") {
|
|
84
112
|
try {
|
|
85
|
-
return { kind: "url", url: validateWakeUrl(raw.url), retryMs };
|
|
113
|
+
return { kind: "url", url: validateWakeUrl(raw.url), retryMs, ...(raw.headerSecrets === undefined ? {} : { headerSecrets: validateHeaderSecrets(raw.headerSecrets) }) };
|
|
86
114
|
}
|
|
87
115
|
catch {
|
|
88
116
|
return null;
|
|
@@ -167,9 +195,27 @@ export async function fireWake(config, notice, deps) {
|
|
|
167
195
|
const timer = setTimeout(() => controller.abort(), WAKE_URL_TIMEOUT_MS);
|
|
168
196
|
const target = redactWakeUrl(config.url);
|
|
169
197
|
try {
|
|
198
|
+
const headers = new Headers({ "content-type": "application/json", "user-agent": "hq-agent-kit" });
|
|
199
|
+
if (config.headerSecrets !== undefined) {
|
|
200
|
+
const refs = validateHeaderSecrets(config.headerSecrets);
|
|
201
|
+
const names = [...new Set(Object.values(refs))];
|
|
202
|
+
if (names.length) {
|
|
203
|
+
if (!deps.resolveHeaderSecrets)
|
|
204
|
+
throw new Error("Secret resolution unavailable");
|
|
205
|
+
const values = await deps.resolveHeaderSecrets(names);
|
|
206
|
+
for (const [header, name] of Object.entries(refs)) {
|
|
207
|
+
const value = values.get(name);
|
|
208
|
+
if (!value || /[\r\n\0]/.test(value))
|
|
209
|
+
throw new Error("Invalid secret header value");
|
|
210
|
+
headers.set(header, value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
170
214
|
const res = await doFetch(config.url, {
|
|
171
215
|
method: "POST",
|
|
172
|
-
headers
|
|
216
|
+
headers,
|
|
217
|
+
// Custom authentication headers must never follow a redirect.
|
|
218
|
+
redirect: "error",
|
|
173
219
|
body: JSON.stringify(notice),
|
|
174
220
|
signal: controller.signal,
|
|
175
221
|
});
|
|
@@ -180,7 +226,7 @@ export async function fireWake(config, notice, deps) {
|
|
|
180
226
|
return { fired: true, ok, detail };
|
|
181
227
|
}
|
|
182
228
|
catch (err) {
|
|
183
|
-
const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : err.message :
|
|
229
|
+
const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(err.message) ? err.message : "request or secret resolution failed" : "request or secret resolution failed"}`;
|
|
184
230
|
deps.log("warn", detail);
|
|
185
231
|
return { fired: true, ok: false, detail };
|
|
186
232
|
}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
19
|
* that event; the `error`-message parsing stays for runners older than
|
|
20
20
|
* 6.18.17.
|
|
21
|
+
*
|
|
22
|
+
* US-024: the server's sentence wins. A `plan-limit` event carries the 402
|
|
23
|
+
* body's `message` (for a usage pause, the `usagePause` message hq-pro writes
|
|
24
|
+
* for the person who hit it), and a runner that forwards `usagePause` gives
|
|
25
|
+
* its message and `actionUrl` directly. The CLI adds no plan name and labels
|
|
26
|
+
* the link `Billing:`, never "Upgrade".
|
|
21
27
|
*/
|
|
22
28
|
import { type SyncProgressEvent } from "@indigoai-us/hq-cloud";
|
|
23
29
|
/** What one push leg was refused. `null` from the tracker means nothing was. */
|
|
@@ -29,10 +35,12 @@ export interface PlanLimitRefusalSummary {
|
|
|
29
35
|
count: number;
|
|
30
36
|
/** Vault-relative paths the runner named, de-duplicated, in report order. */
|
|
31
37
|
paths: string[];
|
|
32
|
-
/** Server sentence
|
|
38
|
+
/** Server sentence (usage pause message, event message, or a refused file's error). */
|
|
33
39
|
message?: string;
|
|
34
|
-
/** Server-selected (or company billing fallback)
|
|
40
|
+
/** Server-selected (or company billing fallback) billing link. */
|
|
35
41
|
upgradeUrl?: string;
|
|
42
|
+
/** True when the server said this is a usage pause (`usagePause`). */
|
|
43
|
+
usagePause?: boolean;
|
|
36
44
|
}
|
|
37
45
|
export interface PlanLimitRefusalTracker {
|
|
38
46
|
observe(event: SyncProgressEvent): void;
|
|
@@ -57,9 +65,11 @@ export declare function createPlanLimitRefusalTracker(opts?: {
|
|
|
57
65
|
export declare function trackingProgressRecorder(tracker: PlanLimitRefusalTracker, company: string | null): (event: SyncProgressEvent) => void;
|
|
58
66
|
/** Add a later leg's refusals to an earlier total. */
|
|
59
67
|
export declare function mergePlanLimitRefusals(a: PlanLimitRefusalSummary | null | undefined, b: PlanLimitRefusalSummary | null | undefined): PlanLimitRefusalSummary | null;
|
|
68
|
+
/** The sentence when the server sent none. Names no plan. */
|
|
69
|
+
export declare const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
|
|
60
70
|
/**
|
|
61
71
|
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
62
|
-
*
|
|
72
|
+
* billing link. Colour and the leading status line are the caller's.
|
|
63
73
|
*/
|
|
64
74
|
export declare function renderPlanLimitRefusalLines(summary: PlanLimitRefusalSummary, indent?: string): string[];
|
|
65
75
|
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
19
|
* that event; the `error`-message parsing stays for runners older than
|
|
20
20
|
* 6.18.17.
|
|
21
|
+
*
|
|
22
|
+
* US-024: the server's sentence wins. A `plan-limit` event carries the 402
|
|
23
|
+
* body's `message` (for a usage pause, the `usagePause` message hq-pro writes
|
|
24
|
+
* for the person who hit it), and a runner that forwards `usagePause` gives
|
|
25
|
+
* its message and `actionUrl` directly. The CLI adds no plan name and labels
|
|
26
|
+
* the link `Billing:`, never "Upgrade".
|
|
21
27
|
*/
|
|
22
28
|
import { createSyncProgressRecorder, } from "@indigoai-us/hq-cloud";
|
|
23
29
|
import { planLimitUpgradeFallbackUrl } from "./plan-limit-upgrade-url.js";
|
|
30
|
+
import { usagePauseFromBody } from "./usage-pause.js";
|
|
24
31
|
/** Upgrade destination when neither the event nor the error named one. */
|
|
25
32
|
const FALLBACK_UPGRADE_URL = "https://hq.computer/billing";
|
|
26
33
|
const PLAN_LIMIT_CODE_TOKEN = /\bcode=(?:PLAN_LIMIT_EXCEEDED|PLAN_LIMIT_REACHED|plan_limit_reached)\b/;
|
|
@@ -72,6 +79,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
72
79
|
let reportedCount = 0;
|
|
73
80
|
let message;
|
|
74
81
|
let upgradeUrl;
|
|
82
|
+
let usagePaused = false;
|
|
75
83
|
const addPath = (value) => {
|
|
76
84
|
if (typeof value === "string" && value && !paths.includes(value)) {
|
|
77
85
|
paths.push(value);
|
|
@@ -82,7 +90,17 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
82
90
|
const record = event;
|
|
83
91
|
if (event.type === "plan-limit") {
|
|
84
92
|
noticeSeen = true;
|
|
93
|
+
const pause = usagePauseFromBody(record);
|
|
94
|
+
if (pause) {
|
|
95
|
+
usagePaused = true;
|
|
96
|
+
message = pause.message;
|
|
97
|
+
if (pause.actionUrl)
|
|
98
|
+
upgradeUrl = pause.actionUrl;
|
|
99
|
+
}
|
|
85
100
|
upgradeUrl ??= safeHttpsUrl(record.upgradeUrl);
|
|
101
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
102
|
+
message ??= record.message.trim();
|
|
103
|
+
}
|
|
86
104
|
addPath(record.path);
|
|
87
105
|
if (Array.isArray(record.paths))
|
|
88
106
|
record.paths.forEach(addPath);
|
|
@@ -111,6 +129,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
111
129
|
paths: [...paths],
|
|
112
130
|
...(message ? { message } : {}),
|
|
113
131
|
...(url ? { upgradeUrl: url } : {}),
|
|
132
|
+
...(usagePaused ? { usagePause: true } : {}),
|
|
114
133
|
};
|
|
115
134
|
},
|
|
116
135
|
};
|
|
@@ -141,22 +160,26 @@ export function mergePlanLimitRefusals(a, b) {
|
|
|
141
160
|
paths.push(p);
|
|
142
161
|
const message = a.message ?? b.message;
|
|
143
162
|
const upgradeUrl = a.upgradeUrl ?? b.upgradeUrl;
|
|
163
|
+
const usagePause = a.usagePause === true || b.usagePause === true;
|
|
144
164
|
return {
|
|
145
165
|
count: a.count + b.count,
|
|
146
166
|
paths,
|
|
147
167
|
...(message ? { message } : {}),
|
|
148
168
|
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
169
|
+
...(usagePause ? { usagePause } : {}),
|
|
149
170
|
};
|
|
150
171
|
}
|
|
151
172
|
/** How many named paths to print before summarising the rest. */
|
|
152
173
|
const MAX_LISTED_PATHS = 20;
|
|
174
|
+
/** The sentence when the server sent none. Names no plan. */
|
|
175
|
+
export const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
|
|
153
176
|
/**
|
|
154
177
|
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
155
|
-
*
|
|
178
|
+
* billing link. Colour and the leading status line are the caller's.
|
|
156
179
|
*/
|
|
157
180
|
export function renderPlanLimitRefusalLines(summary, indent = " ") {
|
|
158
181
|
const lines = [];
|
|
159
|
-
lines.push(`${indent}${summary.message ??
|
|
182
|
+
lines.push(`${indent}${summary.message ?? REFUSAL_FALLBACK_SENTENCE}`);
|
|
160
183
|
if (summary.paths.length > 0) {
|
|
161
184
|
lines.push(`${indent}Not uploaded:`);
|
|
162
185
|
for (const p of summary.paths.slice(0, MAX_LISTED_PATHS)) {
|
|
@@ -170,7 +193,7 @@ export function renderPlanLimitRefusalLines(summary, indent = " ") {
|
|
|
170
193
|
lines.push(`${indent}New files stay on this machine and were not uploaded.`);
|
|
171
194
|
}
|
|
172
195
|
if (summary.upgradeUrl)
|
|
173
|
-
lines.push(`${indent}
|
|
196
|
+
lines.push(`${indent}Billing: ${summary.upgradeUrl}`);
|
|
174
197
|
return lines;
|
|
175
198
|
}
|
|
176
199
|
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
@@ -3,6 +3,14 @@ export interface PlanLimitUpgradeUrlChoices {
|
|
|
3
3
|
nestedUrl?: string | null;
|
|
4
4
|
legacyUrl: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The console's self-serve switch to usage pricing for one company. While HQ
|
|
8
|
+
* Workforce is closed to companies not already on it (hq-pro
|
|
9
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
|
|
10
|
+
* upgrade leads; the Workforce checkout refuses. Null when no console origin
|
|
11
|
+
* can be derived; without a slug the generic billing page.
|
|
12
|
+
*/
|
|
13
|
+
export declare function usageSwitchPageUrl(companySlug?: string): string | null;
|
|
6
14
|
/** Resolve and attribute the final selected company billing destination. */
|
|
7
15
|
export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
|
|
8
16
|
export declare function withCliLimitEntrySurface(value: string): string;
|
|
@@ -11,6 +11,24 @@ function companyBillingUrl(companySlug) {
|
|
|
11
11
|
: "/billing";
|
|
12
12
|
return `${origin}${route}`;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The console's self-serve switch to usage pricing for one company. While HQ
|
|
16
|
+
* Workforce is closed to companies not already on it (hq-pro
|
|
17
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
|
|
18
|
+
* upgrade leads; the Workforce checkout refuses. Null when no console origin
|
|
19
|
+
* can be derived; without a slug the generic billing page.
|
|
20
|
+
*/
|
|
21
|
+
export function usageSwitchPageUrl(companySlug) {
|
|
22
|
+
const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
|
|
23
|
+
const origin = consoleOrigin(controlPlaneUrl);
|
|
24
|
+
if (!origin)
|
|
25
|
+
return null;
|
|
26
|
+
const slug = companySlug?.trim();
|
|
27
|
+
const route = slug
|
|
28
|
+
? `/companies/${encodeURIComponent(slug)}/billing/usage-switch?entrySurface=cli_limit`
|
|
29
|
+
: "/billing";
|
|
30
|
+
return `${origin}${route}`;
|
|
31
|
+
}
|
|
14
32
|
/** Resolve and attribute the final selected company billing destination. */
|
|
15
33
|
export function selectPlanLimitUpgradeUrl(companySlug, choices) {
|
|
16
34
|
const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* Copy rule (US-036): no string a customer reads may contain "locked" or
|
|
18
18
|
* "read-only". `plan-lock.test.ts` scans every rendered string for both.
|
|
19
19
|
*
|
|
20
|
+
* Copy rule (US-024): the flat plans are closed, so nothing here offers the
|
|
21
|
+
* Workforce plan, its price or the free month, and nothing says "Upgrade".
|
|
22
|
+
* The plan is named (Starter) only when hq-pro said the company is on an
|
|
23
|
+
* earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
|
|
24
|
+
* gets copy that names no plan, and the way out besides trimming is the
|
|
25
|
+
* switch to usage pricing or the company's Billing page.
|
|
26
|
+
*
|
|
20
27
|
* An absent field is UNKNOWN, so nothing here ever infers over-limit from
|
|
21
28
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
29
|
*/
|
|
@@ -58,8 +65,18 @@ export interface PlanLimitStatus {
|
|
|
58
65
|
* which keeps the legacy copy.
|
|
59
66
|
*/
|
|
60
67
|
pricingPlan?: PricingPlan;
|
|
68
|
+
/**
|
|
69
|
+
* True when hq-pro says HQ Workforce is closed to this company (hq-flags
|
|
70
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19). `upgradeUrl`
|
|
71
|
+
* is then the console's usage-pricing switch page, the free month never
|
|
72
|
+
* stands, and the renderers offer the switch instead of a Workforce
|
|
73
|
+
* upgrade. Absent keeps the legacy copy (older servers).
|
|
74
|
+
*/
|
|
75
|
+
legacyPlansClosed?: boolean;
|
|
61
76
|
checkedAt: string;
|
|
62
77
|
}
|
|
78
|
+
/** The way out while HQ Workforce is closed. Copy lives in ONE place. */
|
|
79
|
+
export declare const USAGE_SWITCH_LABEL = "Switch to usage pricing";
|
|
63
80
|
/** Starter member cap quoted when the server did not send a users.limit. */
|
|
64
81
|
export declare const STARTER_MEMBER_TARGET = 5;
|
|
65
82
|
/**
|
|
@@ -71,10 +88,8 @@ export declare const STARTER_INTEGRATION_TARGET = 3;
|
|
|
71
88
|
export declare const STARTER_SECRET_TARGET = 10;
|
|
72
89
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
73
90
|
export declare const STARTER_AGENT_TARGET = 0;
|
|
74
|
-
/**
|
|
91
|
+
/** Billing destination quoted when the server did not send one. */
|
|
75
92
|
export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
76
|
-
/** The paid plan the nag sends owners to. Copy lives in ONE place. */
|
|
77
|
-
export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
78
93
|
/**
|
|
79
94
|
* Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
|
|
80
95
|
* `/membership/me` body. No matching row is UNKNOWN. Does not read `planLock`.
|
|
@@ -133,9 +148,10 @@ export declare function renderPlanLimitNotice(status: PlanLimitStatus, opts?: {
|
|
|
133
148
|
*/
|
|
134
149
|
export declare function renderPlanLimitLine(status: PlanLimitStatus): string;
|
|
135
150
|
/**
|
|
136
|
-
* The `Plan: …` orientation line. Starter only
|
|
137
|
-
*
|
|
138
|
-
*
|
|
151
|
+
* The `Plan: …` orientation line. Starter only, and only when hq-pro said the
|
|
152
|
+
* company is on an earlier plan: a paid or enterprise workspace gets no line,
|
|
153
|
+
* and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
|
|
154
|
+
* usage line instead.
|
|
139
155
|
*/
|
|
140
156
|
export declare function renderPlanLine(status: PlanLimitStatus): string | null;
|
|
141
157
|
/**
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* Copy rule (US-036): no string a customer reads may contain "locked" or
|
|
18
18
|
* "read-only". `plan-lock.test.ts` scans every rendered string for both.
|
|
19
19
|
*
|
|
20
|
+
* Copy rule (US-024): the flat plans are closed, so nothing here offers the
|
|
21
|
+
* Workforce plan, its price or the free month, and nothing says "Upgrade".
|
|
22
|
+
* The plan is named (Starter) only when hq-pro said the company is on an
|
|
23
|
+
* earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
|
|
24
|
+
* gets copy that names no plan, and the way out besides trimming is the
|
|
25
|
+
* switch to usage pricing or the company's Billing page.
|
|
26
|
+
*
|
|
20
27
|
* An absent field is UNKNOWN, so nothing here ever infers over-limit from
|
|
21
28
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
29
|
*/
|
|
@@ -24,7 +31,7 @@ import chalk from "chalk";
|
|
|
24
31
|
import { diagnosticsEnabled } from "../diagnostics.js";
|
|
25
32
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
26
33
|
import { parseFreeMonth } from "./free-month.js";
|
|
27
|
-
import { selectPlanLimitUpgradeUrl } from "./plan-limit-upgrade-url.js";
|
|
34
|
+
import { selectPlanLimitUpgradeUrl, usageSwitchPageUrl, } from "./plan-limit-upgrade-url.js";
|
|
28
35
|
import { fetchCompanyPricingPlan, isUsageOnly, pricingPlanFromBody, pricingPlanFromMembership, usageOnlyPlanLine, usageOnlyUpgradeLines, } from "./usage-only.js";
|
|
29
36
|
// The free-month parser lives in a leaf module so the plan-gate 402 decoder can
|
|
30
37
|
// share it without importing the HTTP layer; re-exported so existing callers
|
|
@@ -41,6 +48,8 @@ export const PLAN_LIMIT_REASONS = [
|
|
|
41
48
|
"secrets",
|
|
42
49
|
"agents",
|
|
43
50
|
];
|
|
51
|
+
/** The way out while HQ Workforce is closed. Copy lives in ONE place. */
|
|
52
|
+
export const USAGE_SWITCH_LABEL = "Switch to usage pricing";
|
|
44
53
|
/** Starter member cap quoted when the server did not send a users.limit. */
|
|
45
54
|
export const STARTER_MEMBER_TARGET = 5;
|
|
46
55
|
/**
|
|
@@ -52,10 +61,8 @@ export const STARTER_INTEGRATION_TARGET = 3;
|
|
|
52
61
|
export const STARTER_SECRET_TARGET = 10;
|
|
53
62
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
54
63
|
export const STARTER_AGENT_TARGET = 0;
|
|
55
|
-
/**
|
|
64
|
+
/** Billing destination quoted when the server did not send one. */
|
|
56
65
|
export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
57
|
-
/** The paid plan the nag sends owners to. Copy lives in ONE place. */
|
|
58
|
-
export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
59
66
|
function finiteNumber(value) {
|
|
60
67
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
61
68
|
}
|
|
@@ -157,6 +164,17 @@ function selectUpgradeUrl(body, companySlug) {
|
|
|
157
164
|
const nestedUrl = typeof nested?.upgradeUrl === "string" && nested.upgradeUrl.trim().length > 0
|
|
158
165
|
? nested.upgradeUrl.trim()
|
|
159
166
|
: undefined;
|
|
167
|
+
// While HQ Workforce is closed the way out is the usage-pricing switch, not
|
|
168
|
+
// the company billing page the generated URL would land on. Build it from
|
|
169
|
+
// the slug when known; otherwise keep the server's own switch URL.
|
|
170
|
+
if (rec?.legacyPlansClosed === true) {
|
|
171
|
+
const slug = companySlug?.trim();
|
|
172
|
+
return (slug ? usageSwitchPageUrl(slug) : undefined) ??
|
|
173
|
+
topLevelUrl ??
|
|
174
|
+
nestedUrl ??
|
|
175
|
+
usageSwitchPageUrl(undefined) ??
|
|
176
|
+
DEFAULT_UPGRADE_URL;
|
|
177
|
+
}
|
|
160
178
|
return selectPlanLimitUpgradeUrl(companySlug, {
|
|
161
179
|
topLevelUrl,
|
|
162
180
|
nestedUrl,
|
|
@@ -210,6 +228,7 @@ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
|
|
|
210
228
|
upgradeUrl: selectUpgradeUrl(body, upgradeCompanySlug),
|
|
211
229
|
freeMonth: parseFreeMonth(rec?.freeMonth),
|
|
212
230
|
plan: selectPlan(body) ?? undefined,
|
|
231
|
+
...(rec?.legacyPlansClosed === true ? { legacyPlansClosed: true } : {}),
|
|
213
232
|
checkedAt: new Date().toISOString(),
|
|
214
233
|
}, pricingPlan);
|
|
215
234
|
}
|
|
@@ -295,6 +314,10 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
|
|
|
295
314
|
return null;
|
|
296
315
|
}
|
|
297
316
|
}
|
|
317
|
+
/** True when hq-pro said the company is on an earlier (flat) plan. */
|
|
318
|
+
function knownEarlierPlan(status) {
|
|
319
|
+
return status.pricingPlan === "current";
|
|
320
|
+
}
|
|
298
321
|
/** Why the workspace is over, one clause per reason. */
|
|
299
322
|
export function reasonLabel(reason) {
|
|
300
323
|
switch (reason) {
|
|
@@ -305,7 +328,7 @@ export function reasonLabel(reason) {
|
|
|
305
328
|
case "secrets":
|
|
306
329
|
return "too many secrets";
|
|
307
330
|
case "agents":
|
|
308
|
-
return "agents are not included
|
|
331
|
+
return "agents are not included";
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
334
|
function usageTarget(reason, status) {
|
|
@@ -357,7 +380,7 @@ function reasonDetail(reason, status) {
|
|
|
357
380
|
? `${row.used} of ${target} secrets`
|
|
358
381
|
: `over its ${target}-secret limit`;
|
|
359
382
|
case "agents":
|
|
360
|
-
return "agents are not included
|
|
383
|
+
return "agents are not included";
|
|
361
384
|
}
|
|
362
385
|
}
|
|
363
386
|
function usageLine(reason, status) {
|
|
@@ -368,20 +391,20 @@ function usageLine(reason, status) {
|
|
|
368
391
|
const members = row ?? status.members;
|
|
369
392
|
return members
|
|
370
393
|
? ` Members: ${members.used} of ${target}.`
|
|
371
|
-
: ` Members allowed
|
|
394
|
+
: ` Members allowed: ${target}.`;
|
|
372
395
|
}
|
|
373
396
|
case "integrations":
|
|
374
397
|
return row
|
|
375
398
|
? ` Integrations: ${row.used} of ${target}.`
|
|
376
|
-
: ` Integrations allowed
|
|
399
|
+
: ` Integrations allowed: ${target}.`;
|
|
377
400
|
case "secrets":
|
|
378
401
|
return row
|
|
379
402
|
? ` Secrets: ${row.used} of ${target}.`
|
|
380
|
-
: ` Secrets allowed
|
|
403
|
+
: ` Secrets allowed: ${target}.`;
|
|
381
404
|
case "agents":
|
|
382
405
|
return row
|
|
383
406
|
? ` Agents: ${row.used} of ${target}.`
|
|
384
|
-
: ` Agents allowed
|
|
407
|
+
: ` Agents allowed: ${target}.`;
|
|
385
408
|
}
|
|
386
409
|
}
|
|
387
410
|
/**
|
|
@@ -409,15 +432,17 @@ function capitalize(text) {
|
|
|
409
432
|
* about a dimension nobody measured.
|
|
410
433
|
*/
|
|
411
434
|
export function renderPlanLimitNotice(status, opts = {}) {
|
|
412
|
-
const { companySlug
|
|
435
|
+
const { companySlug } = status;
|
|
413
436
|
const lines = [];
|
|
414
437
|
if (isUsageOnly(status.pricingPlan)) {
|
|
415
438
|
return [`${companySlug}:`, ...usageOnlyUpgradeLines().map((l) => ` ${l}`)].join("\n");
|
|
416
439
|
}
|
|
417
|
-
lines.push(
|
|
440
|
+
lines.push(knownEarlierPlan(status)
|
|
441
|
+
? `HQ Starter — ${companySlug} is over its Starter limits.`
|
|
442
|
+
: `${companySlug} is over its limits.`);
|
|
418
443
|
const reasons = status.reasons.length
|
|
419
444
|
? status.reasons.map(reasonLabel).join("; ")
|
|
420
|
-
: "over
|
|
445
|
+
: "over its limits";
|
|
421
446
|
lines.push(` Why: ${reasons}.`);
|
|
422
447
|
for (const reason of status.reasons) {
|
|
423
448
|
lines.push(usageLine(reason, status));
|
|
@@ -426,21 +451,20 @@ export function renderPlanLimitNotice(status, opts = {}) {
|
|
|
426
451
|
lines.push(HARD_STOP_SENTENCE_LINES[1]);
|
|
427
452
|
const remedy = status.reasons.length
|
|
428
453
|
? status.reasons.map((reason) => reasonRemedy(reason, status)).join(", and ")
|
|
429
|
-
: "come back under
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
454
|
+
: "come back under its limits";
|
|
455
|
+
// The flat plans are closed: no Workforce offer and no free month (it led
|
|
456
|
+
// to the closed checkout). The second way out is the switch to usage
|
|
457
|
+
// pricing when the server said the company is on an earlier plan or that
|
|
458
|
+
// the flat plans are closed to it; otherwise the company's Billing page.
|
|
459
|
+
lines.push(" Two ways to sort it:");
|
|
460
|
+
lines.push(` 1. ${capitalize(remedy)}.`);
|
|
461
|
+
if (status.legacyPlansClosed || knownEarlierPlan(status)) {
|
|
462
|
+
lines.push(` 2. ${USAGE_SWITCH_LABEL}: no plan fee, and these limits stop applying.`);
|
|
437
463
|
}
|
|
438
464
|
else {
|
|
439
|
-
lines.push("
|
|
440
|
-
lines.push(` 1. ${capitalize(remedy)}.`);
|
|
441
|
-
lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
|
|
465
|
+
lines.push(" 2. Open the company's Billing page.");
|
|
442
466
|
}
|
|
443
|
-
lines.push(`
|
|
467
|
+
lines.push(` ${status.legacyPlansClosed ? "Switch" : "Billing"}: ${status.upgradeUrl}`);
|
|
444
468
|
if (opts.inviteTeammateEnabled === true &&
|
|
445
469
|
status.plan === "free" &&
|
|
446
470
|
status.over &&
|
|
@@ -459,18 +483,22 @@ export function renderPlanLimitLine(status) {
|
|
|
459
483
|
}
|
|
460
484
|
const detail = status.reasons.length
|
|
461
485
|
? status.reasons.map((reason) => reasonDetail(reason, status)).join("; ")
|
|
462
|
-
: "over
|
|
463
|
-
|
|
486
|
+
: "over its limits";
|
|
487
|
+
const limits = knownEarlierPlan(status) ? "its Starter limits" : "its limits";
|
|
488
|
+
return (`Company ${status.companySlug} is over ${limits} (${detail}). ` +
|
|
464
489
|
`New files and new secrets are paused until this is sorted: ${status.upgradeUrl}`);
|
|
465
490
|
}
|
|
466
491
|
/**
|
|
467
|
-
* The `Plan: …` orientation line. Starter only
|
|
468
|
-
*
|
|
469
|
-
*
|
|
492
|
+
* The `Plan: …` orientation line. Starter only, and only when hq-pro said the
|
|
493
|
+
* company is on an earlier plan: a paid or enterprise workspace gets no line,
|
|
494
|
+
* and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
|
|
495
|
+
* usage line instead.
|
|
470
496
|
*/
|
|
471
497
|
export function renderPlanLine(status) {
|
|
472
498
|
if (isUsageOnly(status.pricingPlan))
|
|
473
499
|
return usageOnlyPlanLine();
|
|
500
|
+
if (!knownEarlierPlan(status))
|
|
501
|
+
return status.over ? "Plan: over its limits" : null;
|
|
474
502
|
if (status.over)
|
|
475
503
|
return "Plan: Starter — over its limits";
|
|
476
504
|
if (status.plan !== "free")
|