@indigoai-us/hq-cli 5.117.0 → 5.117.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 +52 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +44 -3
- package/assets/bot-workers/setup/context/quick-reference.md +1 -1
- package/dist/command-catalog.generated.d.ts +6 -3
- package/dist/command-catalog.generated.js +7 -3
- package/dist/commands/agent-kit.d.ts +23 -3
- package/dist/commands/agent-kit.js +126 -14
- package/dist/commands/agent-probe.d.ts +15 -9
- package/dist/commands/agent-probe.js +55 -35
- package/dist/commands/billing.js +1 -1
- package/dist/commands/db-provision.js +4 -4
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +80 -0
- package/dist/commands/meetings.js +2 -2
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +55 -1
- package/dist/lib/agent-kit/fallback.d.ts +63 -0
- package/dist/lib/agent-kit/fallback.js +129 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +18 -6
- package/dist/lib/agent-kit/run/inbox.js +40 -8
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +22 -6
- package/dist/lib/agent-kit/run/mesh-listener.js +55 -10
- package/dist/lib/agent-kit/run/supervisor.d.ts +35 -0
- package/dist/lib/agent-kit/run/supervisor.js +85 -0
- package/dist/lib/agent-kit/skills.js +2 -2
- package/dist/lib/billing/plan-lock.d.ts +99 -0
- package/dist/lib/billing/plan-lock.js +230 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/credentials.js +95 -0
- package/dist/lib/plan-limit-nag.js +1 -1
- package/dist/utils/plan-gate-error.js +9 -9
- package/dist/utils/team-upgrade.d.ts +1 -1
- package/dist/utils/team-upgrade.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
|
|
3
|
+
* of the workspace plan lock.
|
|
4
|
+
*
|
|
5
|
+
* Starter (free) workspaces are capped at 5 members and 0 integrations. Going
|
|
6
|
+
* over locks the workspace immediately: it becomes read-only until the owner
|
|
7
|
+
* trims back under the caps or upgrades to HQ Workforce. The lock decision is
|
|
8
|
+
* NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
|
|
9
|
+
* truth and ships the answer on `GET /membership/me` as a per-company
|
|
10
|
+
* `planLock` object. This module only reads that field and renders it.
|
|
11
|
+
*
|
|
12
|
+
* Member counts are decoration, never a second opinion: the count comes from
|
|
13
|
+
* `GET /v1/billing/usage-limits` on a best-effort basis and its absence only
|
|
14
|
+
* removes the "n of 5" detail from the notice. An absent field is UNKNOWN, so
|
|
15
|
+
* nothing here ever infers a lock (or an unlock) from missing data
|
|
16
|
+
* (hq-absent-field-never-means-constraining-value).
|
|
17
|
+
*/
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
20
|
+
/** Starter member cap quoted when the server did not send `removeMembersTo`. */
|
|
21
|
+
export const STARTER_MEMBER_TARGET = 5;
|
|
22
|
+
/** Upgrade destination quoted when the server did not send one. */
|
|
23
|
+
export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
24
|
+
/** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
|
|
25
|
+
export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
26
|
+
function asRecord(value) {
|
|
27
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Defensively parse a `planLock` payload. Anything malformed returns null —
|
|
34
|
+
* the caller then behaves exactly as if the server had sent nothing, which is
|
|
35
|
+
* "unknown", not "locked".
|
|
36
|
+
*/
|
|
37
|
+
export function parsePlanLock(value) {
|
|
38
|
+
const rec = asRecord(value);
|
|
39
|
+
if (!rec)
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof rec.locked !== "boolean")
|
|
42
|
+
return null;
|
|
43
|
+
const reasons = [];
|
|
44
|
+
if (Array.isArray(rec.reasons)) {
|
|
45
|
+
for (const reason of rec.reasons) {
|
|
46
|
+
if (reason === "users" || reason === "integrations")
|
|
47
|
+
reasons.push(reason);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fix = asRecord(rec.fixOptions);
|
|
51
|
+
const removeMembersTo = typeof fix?.removeMembersTo === "number" &&
|
|
52
|
+
Number.isFinite(fix.removeMembersTo)
|
|
53
|
+
? fix.removeMembersTo
|
|
54
|
+
: STARTER_MEMBER_TARGET;
|
|
55
|
+
return {
|
|
56
|
+
locked: rec.locked,
|
|
57
|
+
reasons,
|
|
58
|
+
upgradeUrl: typeof rec.upgradeUrl === "string" && rec.upgradeUrl.trim().length > 0
|
|
59
|
+
? rec.upgradeUrl.trim()
|
|
60
|
+
: DEFAULT_UPGRADE_URL,
|
|
61
|
+
fixOptions: {
|
|
62
|
+
removeMembersTo,
|
|
63
|
+
disconnectIntegrations: fix?.disconnectIntegrations === true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
|
|
69
|
+
* `/membership/me` body and return its parsed lock. No matching row — or no
|
|
70
|
+
* `planLock` on it — is UNKNOWN, so this returns null rather than guessing.
|
|
71
|
+
*/
|
|
72
|
+
export function selectPlanLock(body, companyRef) {
|
|
73
|
+
const rec = asRecord(body);
|
|
74
|
+
const rows = Array.isArray(rec?.memberships) ? rec.memberships : [];
|
|
75
|
+
const ref = companyRef.trim().toLowerCase();
|
|
76
|
+
for (const raw of rows) {
|
|
77
|
+
const row = asRecord(raw);
|
|
78
|
+
if (!row)
|
|
79
|
+
continue;
|
|
80
|
+
if (typeof row.status === "string" && row.status !== "active")
|
|
81
|
+
continue;
|
|
82
|
+
const slug = typeof row.companySlug === "string" ? row.companySlug.toLowerCase() : "";
|
|
83
|
+
const uid = typeof row.companyUid === "string" ? row.companyUid.toLowerCase() : "";
|
|
84
|
+
if (slug !== ref && uid !== ref)
|
|
85
|
+
continue;
|
|
86
|
+
const lock = parsePlanLock(row.planLock);
|
|
87
|
+
if (!lock)
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
lock,
|
|
91
|
+
companyUid: typeof row.companyUid === "string" ? row.companyUid : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/** Parse the resolved plan id out of a usage-limits body. Absent → null. */
|
|
97
|
+
export function selectPlan(body) {
|
|
98
|
+
const plan = asRecord(body)?.plan;
|
|
99
|
+
return plan === "free" || plan === "paid" || plan === "enterprise"
|
|
100
|
+
? plan
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
103
|
+
/** Parse the `users` dimension out of a usage-limits body. Absent → null. */
|
|
104
|
+
export function selectMemberUsage(body) {
|
|
105
|
+
const users = asRecord(asRecord(body)?.users);
|
|
106
|
+
if (!users)
|
|
107
|
+
return null;
|
|
108
|
+
if (typeof users.used !== "number" || !Number.isFinite(users.used)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const limit = typeof users.limit === "number" && Number.isFinite(users.limit)
|
|
112
|
+
? users.limit
|
|
113
|
+
: STARTER_MEMBER_TARGET;
|
|
114
|
+
return { used: users.used, limit };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read the live lock for one company: `planLock` from `/membership/me`, plus a
|
|
118
|
+
* best-effort member count for the notice. Returns null when the server did not
|
|
119
|
+
* answer with a lock for this company — callers must then say nothing.
|
|
120
|
+
*/
|
|
121
|
+
export async function fetchPlanLockStatus(token, companyRef, opts = {}) {
|
|
122
|
+
const timeoutMs = opts.timeoutMs ?? 8000;
|
|
123
|
+
const res = await vaultApiFetch({
|
|
124
|
+
token,
|
|
125
|
+
path: "/membership/me",
|
|
126
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok)
|
|
129
|
+
return null;
|
|
130
|
+
const selected = selectPlanLock(await res.json().catch(() => null), companyRef);
|
|
131
|
+
if (!selected)
|
|
132
|
+
return null;
|
|
133
|
+
let members;
|
|
134
|
+
let plan;
|
|
135
|
+
// Decoration only. The lock answer above is authoritative; the usage read
|
|
136
|
+
// adds the member count and the plan id that the notices and the `Plan:`
|
|
137
|
+
// orientation line quote. A failure here must never suppress — or invent —
|
|
138
|
+
// a lock, so every field it feeds stays optional.
|
|
139
|
+
if (selected.companyUid) {
|
|
140
|
+
try {
|
|
141
|
+
const usage = await vaultApiFetch({
|
|
142
|
+
token,
|
|
143
|
+
path: "/v1/billing/usage-limits",
|
|
144
|
+
query: { companyUid: selected.companyUid },
|
|
145
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
146
|
+
});
|
|
147
|
+
if (usage.ok) {
|
|
148
|
+
const body = await usage.json().catch(() => null);
|
|
149
|
+
members = selectMemberUsage(body) ?? undefined;
|
|
150
|
+
plan = selectPlan(body) ?? undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Network/timeout: leave both unknown.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
companySlug: companyRef,
|
|
159
|
+
companyUid: selected.companyUid,
|
|
160
|
+
lock: selected.lock,
|
|
161
|
+
members,
|
|
162
|
+
plan,
|
|
163
|
+
checkedAt: new Date().toISOString(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function reasonLabel(reason) {
|
|
167
|
+
return reason === "users"
|
|
168
|
+
? "too many members"
|
|
169
|
+
: "integrations are not included on Starter";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The full WORKSPACE LOCKED block: why it locked, where the workspace stands
|
|
173
|
+
* against the cap, and the two fixes. Plain text — colour is applied by the
|
|
174
|
+
* caller so scripts capturing stdout get a clean block.
|
|
175
|
+
*/
|
|
176
|
+
export function renderPlanLockNotice(status) {
|
|
177
|
+
const { lock, members, companySlug } = status;
|
|
178
|
+
const target = lock.fixOptions.removeMembersTo;
|
|
179
|
+
const lines = [];
|
|
180
|
+
lines.push(`WORKSPACE LOCKED — ${companySlug} is over its Starter plan.`);
|
|
181
|
+
const reasons = lock.reasons.length
|
|
182
|
+
? lock.reasons.map(reasonLabel).join("; ")
|
|
183
|
+
: "over the Starter plan limits";
|
|
184
|
+
lines.push(` Why: ${reasons}.`);
|
|
185
|
+
if (members) {
|
|
186
|
+
lines.push(` Members: ${members.used} of ${target}.`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(` Members allowed on Starter: ${target}.`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(" This workspace is read-only until it is fixed. Two ways out:");
|
|
192
|
+
lines.push(` 1. Remove members until you are at ${target} or fewer${lock.fixOptions.disconnectIntegrations
|
|
193
|
+
? ", and disconnect the workspace's integrations"
|
|
194
|
+
: ""}.`);
|
|
195
|
+
lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
|
|
196
|
+
lines.push(` Upgrade: ${lock.upgradeUrl}`);
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The one-line form injected on every turn while the workspace stays locked.
|
|
201
|
+
* Kept to a single line on purpose — it repeats each turn.
|
|
202
|
+
*/
|
|
203
|
+
export function renderPlanLockLine(status) {
|
|
204
|
+
const target = status.lock.fixOptions.removeMembersTo;
|
|
205
|
+
const count = status.members
|
|
206
|
+
? `${status.members.used} of ${target} members`
|
|
207
|
+
: `over its ${target}-member limit`;
|
|
208
|
+
return (`Company ${status.companySlug} is locked on Starter (${count}). ` +
|
|
209
|
+
`Writes to HQ cloud will fail until fixed: ${status.lock.upgradeUrl}`);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The `Plan: …` orientation line. Starter only: a paid or enterprise
|
|
213
|
+
* workspace — and an UNKNOWN plan — gets no line at all.
|
|
214
|
+
*/
|
|
215
|
+
export function renderPlanLine(status) {
|
|
216
|
+
if (status.lock.locked)
|
|
217
|
+
return "Plan: Starter — LOCKED";
|
|
218
|
+
if (status.plan !== "free")
|
|
219
|
+
return null;
|
|
220
|
+
const target = status.lock.fixOptions.removeMembersTo;
|
|
221
|
+
if (status.members) {
|
|
222
|
+
return `Plan: Starter — ${status.members.used} of ${target} members`;
|
|
223
|
+
}
|
|
224
|
+
return `Plan: Starter — ${target} members included`;
|
|
225
|
+
}
|
|
226
|
+
/** Colourised block for interactive output. */
|
|
227
|
+
export function colorizePlanLockNotice(notice) {
|
|
228
|
+
return chalk.red(notice);
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=plan-lock.js.map
|
|
@@ -131,4 +131,31 @@ export declare class CredentialRenewalManager {
|
|
|
131
131
|
stop(): void;
|
|
132
132
|
private clear;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Contract-2 (personal) realtime vend. Backed server-side by the personal
|
|
136
|
+
* session policy, which grants Subscribe/Receive on the caller's own
|
|
137
|
+
* `hq/{principalUid}/{dm,sessions,work,notifications,meeting,sync}` topics —
|
|
138
|
+
* unlike contract 3, whose policy only covers company presence and the thread
|
|
139
|
+
* directory. Doorbell listeners on personal topics MUST use this contract;
|
|
140
|
+
* subscribing to personal topics with a contract-3 session makes AWS IoT drop
|
|
141
|
+
* the connection before SUBACK.
|
|
142
|
+
*/
|
|
143
|
+
export interface PersonalRealtimeBundle {
|
|
144
|
+
contractVersion: 2;
|
|
145
|
+
credentials: IotCredentials;
|
|
146
|
+
iotEndpoint: string;
|
|
147
|
+
region: string;
|
|
148
|
+
clientId: string;
|
|
149
|
+
actorUid: string;
|
|
150
|
+
/** Advertised personal topics keyed by kind (dm, sessions, work, notifications, …). */
|
|
151
|
+
topics: Record<string, string>;
|
|
152
|
+
expiresAt: string;
|
|
153
|
+
}
|
|
154
|
+
export type PersonalCredentialsFetcher = () => Promise<PersonalRealtimeBundle>;
|
|
155
|
+
export declare function normalizePersonalRealtimeBundle(raw: unknown): PersonalRealtimeBundle;
|
|
156
|
+
export declare function createPersonalRealtimeFetcher(opts: {
|
|
157
|
+
token: string;
|
|
158
|
+
baseUrl?: string;
|
|
159
|
+
post?: (path: string, body: unknown) => Promise<CredentialVendPostResult>;
|
|
160
|
+
}): PersonalCredentialsFetcher;
|
|
134
161
|
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -385,4 +385,99 @@ export class CredentialRenewalManager {
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
export function normalizePersonalRealtimeBundle(raw) {
|
|
389
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
390
|
+
throw new Error("contract-2 vend response is not an object");
|
|
391
|
+
}
|
|
392
|
+
const r = raw;
|
|
393
|
+
if (r.contractVersion !== 2) {
|
|
394
|
+
throw new Error(`expected contractVersion 2, got ${String(r.contractVersion)}`);
|
|
395
|
+
}
|
|
396
|
+
const creds = r.credentials;
|
|
397
|
+
if (!creds ||
|
|
398
|
+
typeof creds.accessKeyId !== "string" ||
|
|
399
|
+
typeof creds.secretAccessKey !== "string" ||
|
|
400
|
+
typeof creds.sessionToken !== "string") {
|
|
401
|
+
throw new Error("contract-2 vend missing credentials");
|
|
402
|
+
}
|
|
403
|
+
for (const key of ["clientId", "iotEndpoint", "region"]) {
|
|
404
|
+
if (typeof r[key] !== "string" || !r[key].trim()) {
|
|
405
|
+
throw new Error(`contract-2 vend missing ${key}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const topicsRaw = r.topics && typeof r.topics === "object" && !Array.isArray(r.topics)
|
|
409
|
+
? r.topics
|
|
410
|
+
: {};
|
|
411
|
+
const topics = {};
|
|
412
|
+
for (const [k, v] of Object.entries(topicsRaw)) {
|
|
413
|
+
if (typeof v === "string" && v.trim())
|
|
414
|
+
topics[k] = v.trim();
|
|
415
|
+
}
|
|
416
|
+
if (!topics.dm && typeof r.topic === "string")
|
|
417
|
+
topics.dm = r.topic;
|
|
418
|
+
const actorUid = (typeof r.principalUid === "string" && /^(?:prs|agt)_[A-Za-z0-9]+$/.test(r.principalUid)
|
|
419
|
+
? r.principalUid
|
|
420
|
+
: undefined) ||
|
|
421
|
+
actorUidFromPersonalTopic(topics.dm) ||
|
|
422
|
+
"";
|
|
423
|
+
if (!actorUid)
|
|
424
|
+
throw new Error("contract-2 vend could not derive actorUid");
|
|
425
|
+
// Only keep topics that belong to the vended principal.
|
|
426
|
+
for (const [k, v] of Object.entries(topics)) {
|
|
427
|
+
if (actorUidFromPersonalTopic(v) !== actorUid)
|
|
428
|
+
delete topics[k];
|
|
429
|
+
}
|
|
430
|
+
const expiresAt = (typeof r.expiresAt === "string" && r.expiresAt) ||
|
|
431
|
+
(typeof creds.expiration === "string" && creds.expiration) ||
|
|
432
|
+
"";
|
|
433
|
+
if (!expiresAt)
|
|
434
|
+
throw new Error("contract-2 vend missing expiresAt");
|
|
435
|
+
return {
|
|
436
|
+
contractVersion: 2,
|
|
437
|
+
credentials: {
|
|
438
|
+
accessKeyId: creds.accessKeyId,
|
|
439
|
+
secretAccessKey: creds.secretAccessKey,
|
|
440
|
+
sessionToken: creds.sessionToken,
|
|
441
|
+
expiration: expiresAt,
|
|
442
|
+
},
|
|
443
|
+
iotEndpoint: r.iotEndpoint,
|
|
444
|
+
region: r.region,
|
|
445
|
+
clientId: r.clientId,
|
|
446
|
+
actorUid,
|
|
447
|
+
topics,
|
|
448
|
+
expiresAt,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
export function createPersonalRealtimeFetcher(opts) {
|
|
452
|
+
return async () => {
|
|
453
|
+
if (opts.post) {
|
|
454
|
+
const res = await opts.post(REALTIME_CREDENTIALS_PATH, { contractVersion: 2 });
|
|
455
|
+
if (res.status < 200 || res.status >= 300) {
|
|
456
|
+
throw classifyCredentialVendFailure(res.status, res.body, headerGet(res.headers, "Retry-After"));
|
|
457
|
+
}
|
|
458
|
+
return normalizePersonalRealtimeBundle(res.body);
|
|
459
|
+
}
|
|
460
|
+
const res = await vaultApiFetch({
|
|
461
|
+
token: opts.token,
|
|
462
|
+
path: REALTIME_CREDENTIALS_PATH,
|
|
463
|
+
method: "POST",
|
|
464
|
+
body: { contractVersion: 2 },
|
|
465
|
+
baseUrl: opts.baseUrl ?? DEFAULT_VAULT_API_URL,
|
|
466
|
+
});
|
|
467
|
+
const text = await res.text();
|
|
468
|
+
let body = {};
|
|
469
|
+
if (text) {
|
|
470
|
+
try {
|
|
471
|
+
body = JSON.parse(text);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
body = { raw: text.slice(0, 200) };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!res.ok) {
|
|
478
|
+
throw classifyCredentialVendFailure(res.status, body, res.headers.get("Retry-After"));
|
|
479
|
+
}
|
|
480
|
+
return normalizePersonalRealtimeBundle(body);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
388
483
|
//# sourceMappingURL=credentials.js.map
|
|
@@ -224,7 +224,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
224
224
|
if (worst === null)
|
|
225
225
|
return;
|
|
226
226
|
warningShownThisSession = true;
|
|
227
|
-
const line = `⚠ HQ
|
|
227
|
+
const line = `⚠ HQ Starter plan: ${formatEntryLine(worst.key, worst.entry)}. Upgrade: ${resolvedUpgradeUrl}`;
|
|
228
228
|
write(chalk.yellow(line) + "\n");
|
|
229
229
|
}
|
|
230
230
|
catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to
|
|
1
|
+
const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to HQ Workforce.";
|
|
2
2
|
export class PlanGateError extends Error {
|
|
3
3
|
code;
|
|
4
4
|
details;
|
|
@@ -23,19 +23,19 @@ function formatPlanGateDetails(code, details) {
|
|
|
23
23
|
typeof details.used === "number" &&
|
|
24
24
|
typeof details.limit === "number") {
|
|
25
25
|
const upgrade = typeof details.upgradeUrl === "string"
|
|
26
|
-
? `Upgrade to HQ
|
|
27
|
-
: "Upgrade to HQ
|
|
26
|
+
? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
|
|
27
|
+
: "Upgrade to HQ Workforce ($500/mo) to remove limits.";
|
|
28
28
|
return [
|
|
29
|
-
`
|
|
29
|
+
`Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`,
|
|
30
30
|
upgrade,
|
|
31
31
|
existingResourcesNote,
|
|
32
32
|
].join("\n");
|
|
33
33
|
}
|
|
34
34
|
const upgrade = typeof details.upgradeUrl === "string"
|
|
35
|
-
? `Upgrade to HQ
|
|
36
|
-
: "Upgrade to HQ
|
|
35
|
+
? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
|
|
36
|
+
: "Upgrade to HQ Workforce ($500/mo) to remove limits.";
|
|
37
37
|
return [
|
|
38
|
-
"HQ
|
|
38
|
+
"HQ Workforce plan required for this feature.",
|
|
39
39
|
upgrade,
|
|
40
40
|
existingResourcesNote,
|
|
41
41
|
].join("\n");
|
|
@@ -56,7 +56,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
|
56
56
|
if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
|
|
57
57
|
return false;
|
|
58
58
|
if (deps.ask) {
|
|
59
|
-
const answer = await deps.ask("Upgrade to HQ
|
|
59
|
+
const answer = await deps.ask("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
|
|
60
60
|
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
61
61
|
return false;
|
|
62
62
|
await onConfirm();
|
|
@@ -65,7 +65,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
|
65
65
|
const { createInterface } = await import("node:readline/promises");
|
|
66
66
|
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
67
67
|
try {
|
|
68
|
-
const answer = await readline.question("Upgrade to HQ
|
|
68
|
+
const answer = await readline.question("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
|
|
69
69
|
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
70
70
|
return false;
|
|
71
71
|
await onConfirm();
|
|
@@ -10,7 +10,7 @@ export interface TeamUpgradeOptions {
|
|
|
10
10
|
error?: (text: string) => void;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Start the owner-gated HQ
|
|
13
|
+
* Start the owner-gated HQ Workforce Checkout flow. Stripe collects payment details
|
|
14
14
|
* in its hosted browser page; the CLI never receives card data.
|
|
15
15
|
*/
|
|
16
16
|
export declare function upgradeToTeam(opts?: TeamUpgradeOptions): Promise<string>;
|
|
@@ -14,7 +14,7 @@ function ownerUpgradeError() {
|
|
|
14
14
|
return Object.assign(new Error("Only a company owner can upgrade. Ask a company owner to upgrade."), { expected: true });
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Start the owner-gated HQ
|
|
17
|
+
* Start the owner-gated HQ Workforce Checkout flow. Stripe collects payment details
|
|
18
18
|
* in its hosted browser page; the CLI never receives card data.
|
|
19
19
|
*/
|
|
20
20
|
export async function upgradeToTeam(opts = {}) {
|
|
@@ -35,7 +35,7 @@ export async function upgradeToTeam(opts = {}) {
|
|
|
35
35
|
if (response.status === 403 || /owner/i.test(`${body.error ?? ""} ${body.message ?? ""}`)) {
|
|
36
36
|
throw ownerUpgradeError();
|
|
37
37
|
}
|
|
38
|
-
throw Object.assign(new Error(`Could not start HQ
|
|
38
|
+
throw Object.assign(new Error(`Could not start HQ Workforce checkout: ${body.error ?? body.message ?? response.statusText}`), { expected: true });
|
|
39
39
|
}
|
|
40
40
|
const { url } = (await response.json());
|
|
41
41
|
if (typeof url !== "string" || !url) {
|
|
@@ -46,7 +46,7 @@ export async function upgradeToTeam(opts = {}) {
|
|
|
46
46
|
write(`${JSON.stringify({ url })}\n`);
|
|
47
47
|
return url;
|
|
48
48
|
}
|
|
49
|
-
write("Opening HQ
|
|
49
|
+
write("Opening HQ Workforce checkout ($500/mo) in your browser — finish there and you're upgraded.\n");
|
|
50
50
|
const skipBrowser = opts.noBrowser || (opts.headless ?? isHeadless());
|
|
51
51
|
if (skipBrowser) {
|
|
52
52
|
write(`Open this URL to continue: ${url}\n`);
|