@indigoai-us/hq-cli 5.345.143 → 5.345.144
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 +71 -0
- package/dist/command-catalog.generated.d.ts +278 -6
- package/dist/command-catalog.generated.js +341 -6
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +1 -0
- package/dist/commands/agent-enroll.js +7 -0
- package/dist/commands/agents.d.ts +25 -3
- package/dist/commands/agents.js +57 -4
- package/dist/commands/api-keys.js +66 -8
- package/dist/commands/billing-sponsor.d.ts +4 -0
- package/dist/commands/billing-sponsor.js +48 -5
- package/dist/commands/billing-usage.d.ts +59 -0
- package/dist/commands/billing-usage.js +162 -0
- package/dist/commands/billing.d.ts +9 -0
- package/dist/commands/billing.js +29 -2
- package/dist/commands/core-policy.js +76 -13
- package/dist/commands/library-add.d.ts +120 -0
- package/dist/commands/library-add.js +434 -0
- package/dist/commands/library-caption.d.ts +112 -0
- package/dist/commands/library-caption.js +393 -0
- package/dist/commands/library-link.d.ts +32 -0
- package/dist/commands/library-link.js +95 -0
- package/dist/commands/library.d.ts +72 -0
- package/dist/commands/library.js +527 -0
- package/dist/commands/meetings.d.ts +8 -0
- package/dist/commands/meetings.js +25 -2
- package/dist/commands/storage-offload.d.ts +16 -2
- package/dist/commands/storage-offload.js +55 -4
- package/dist/commands/storage.d.ts +8 -1
- package/dist/commands/storage.js +6 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2410 -2063
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/billing/plan-lock.d.ts +15 -1
- package/dist/lib/billing/plan-lock.js +45 -5
- package/dist/lib/billing/usage-card.d.ts +82 -0
- package/dist/lib/billing/usage-card.js +198 -0
- package/dist/lib/billing/usage-cost.d.ts +214 -0
- package/dist/lib/billing/usage-cost.js +440 -0
- package/dist/lib/billing/usage-only.d.ts +87 -0
- package/dist/lib/billing/usage-only.js +262 -0
- package/dist/lib/billing/usage-rate-card.d.ts +51 -0
- package/dist/lib/billing/usage-rate-card.js +62 -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/plan-limit-nag.d.ts +4 -6
- package/dist/lib/plan-limit-nag.js +19 -1
- package/dist/lib/provider-launcher.d.ts +22 -0
- package/dist/lib/provider-launcher.js +65 -0
- package/dist/lib/search-index/index.d.ts +2 -0
- package/dist/lib/search-index/index.js +34 -0
- package/dist/lib/search-index/max-doc-bytes.d.ts +2 -1
- package/dist/lib/search-index/max-doc-bytes.js +2 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +20 -3
- package/dist/utils/billing-gate.d.ts +19 -4
- package/dist/utils/billing-gate.js +25 -4
- package/dist/utils/plan-gate-error.d.ts +10 -0
- package/dist/utils/plan-gate-error.js +54 -0
- package/dist/utils/vault-api.js +4 -1
- package/package.json +2 -2
|
@@ -10,6 +10,45 @@ async function requireCognitoForApiKeys(label) {
|
|
|
10
10
|
requireApiKeyCapability(null, label);
|
|
11
11
|
return ensureCognitoToken();
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Named capability bundles for `api-keys create --preset`. A preset grants
|
|
15
|
+
* exactly its listed capabilities — no secret scopes, no deploy block — so a
|
|
16
|
+
* headless agent's key cannot reach anything the preset does not name.
|
|
17
|
+
*/
|
|
18
|
+
const API_KEY_PRESETS = {
|
|
19
|
+
/** Media library agent: search assets and mint download links. */
|
|
20
|
+
"library-agent": {
|
|
21
|
+
capabilities: ["search:query", "files:read"],
|
|
22
|
+
uploadCapability: "files:write",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
function isApiKeyPresetName(value) {
|
|
26
|
+
return Object.prototype.hasOwnProperty.call(API_KEY_PRESETS, value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Capabilities for `--preset`. Rejects an unknown preset and any flag that
|
|
30
|
+
* would widen or reshape the grant, so the minted key matches the preset.
|
|
31
|
+
*/
|
|
32
|
+
function resolvePresetCapabilities(opts) {
|
|
33
|
+
const name = opts.preset ?? "";
|
|
34
|
+
if (!isApiKeyPresetName(name)) {
|
|
35
|
+
throw new Error(`Unknown preset '${name}'. Supported: ${Object.keys(API_KEY_PRESETS).join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
const conflicts = [
|
|
38
|
+
opts.capability.length > 0 ? "--capability" : null,
|
|
39
|
+
opts.scope.length > 0 ? "--scope" : null,
|
|
40
|
+
opts.deploy === true ? "--deploy" : null,
|
|
41
|
+
opts.deployApp.length > 0 ? "--deploy-app" : null,
|
|
42
|
+
opts.permission !== undefined ? "--permission" : null,
|
|
43
|
+
].filter((flag) => flag !== null);
|
|
44
|
+
if (conflicts.length > 0) {
|
|
45
|
+
throw new Error(`--preset ${name} grants a fixed capability set; remove ${conflicts.join(", ")}.`);
|
|
46
|
+
}
|
|
47
|
+
const preset = API_KEY_PRESETS[name];
|
|
48
|
+
return opts.allowUpload === true
|
|
49
|
+
? [...preset.capabilities, preset.uploadCapability]
|
|
50
|
+
: [...preset.capabilities];
|
|
51
|
+
}
|
|
13
52
|
function collectRepeatedOption(value, previous) {
|
|
14
53
|
return [...previous, value];
|
|
15
54
|
}
|
|
@@ -160,9 +199,16 @@ export function registerApiKeysCommand(program) {
|
|
|
160
199
|
.option("--deploy-app <id>", "Deploy app id/slug allowed for publish (repeatable); narrows the deploy grant", collectRepeatedOption, [])
|
|
161
200
|
.option("--permission <level>", "Secret permission level: read | write | admin (applies to secret scopes; defaults to read)")
|
|
162
201
|
.option("--expires <ISO8601>", "Optional ISO-8601 expiry timestamp")
|
|
202
|
+
.option("--preset <name>", `Grant a fixed capability bundle: ${Object.keys(API_KEY_PRESETS).join(" | ")} (library-agent = search:query, files:read)`)
|
|
203
|
+
.option("--allow-upload", "With --preset library-agent, also grant files:write (hq library add)")
|
|
163
204
|
.action(async (opts) => {
|
|
164
205
|
try {
|
|
165
|
-
|
|
206
|
+
if (opts.allowUpload === true && opts.preset === undefined) {
|
|
207
|
+
throw new Error("--allow-upload requires --preset library-agent.");
|
|
208
|
+
}
|
|
209
|
+
const capabilities = opts.preset !== undefined
|
|
210
|
+
? resolvePresetCapabilities(opts)
|
|
211
|
+
: parseCapabilities(opts.capability);
|
|
166
212
|
const wantsDeploy = hasDeployCapability(capabilities) ||
|
|
167
213
|
opts.deploy === true ||
|
|
168
214
|
opts.deployApp.length > 0;
|
|
@@ -250,13 +296,25 @@ export function registerApiKeysCommand(program) {
|
|
|
250
296
|
}
|
|
251
297
|
console.log(" Export it for automation (never falls back to a session):");
|
|
252
298
|
console.log(`\n export HQ_API_KEY='${data.key.value}'\n`);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
299
|
+
if (opts.preset === "library-agent") {
|
|
300
|
+
console.log(" Media library:");
|
|
301
|
+
console.log(` hq library search <query> --company ${companyRef ?? companyUid} (search:query)`);
|
|
302
|
+
console.log(` hq library link <vault-key> --company ${companyRef ?? companyUid} (files:read)`);
|
|
303
|
+
if (capabilities.includes("files:write")) {
|
|
304
|
+
console.log(` hq library add <file> --company ${companyRef ?? companyUid} (files:write)`);
|
|
305
|
+
}
|
|
306
|
+
console.log(" Or HTTP: POST /v1/keys/search and /v1/keys/files/presign with Authorization: Bearer <key>");
|
|
307
|
+
console.log(chalk.dim(" Download links are bearer URLs — never post them in Slack or shared channels."));
|
|
308
|
+
}
|
|
309
|
+
if (opts.preset === undefined) {
|
|
310
|
+
console.log(" Vault secrets:");
|
|
311
|
+
console.log(" hq secrets get <NAME> --reveal");
|
|
312
|
+
console.log(" hq secrets exec --only <NAME> -- <command>");
|
|
313
|
+
console.log(" hq secrets list --company <cmp_uid> (secrets:read)");
|
|
314
|
+
console.log(" hq secrets set <NAME> --from-stdin --company <cmp_uid> (secrets:write)");
|
|
315
|
+
console.log(" Or HTTP: POST /v1/keys/secrets/fetch with Authorization: Bearer <key>");
|
|
316
|
+
console.log(chalk.dim(" Company-scoped commands need --company <cmp_uid> (or HQ_COMPANY_UID) — slug lookup needs a session."));
|
|
317
|
+
}
|
|
260
318
|
if (data.apiKey.scope.deploy?.apps?.length) {
|
|
261
319
|
const deployApps = data.apiKey.scope.deploy.apps;
|
|
262
320
|
console.log(deployApps.includes(DEPLOY_APP_ALL)
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* them: it resolves the caller's person id and the company's admin set
|
|
20
20
|
* server-side, so nothing here is trusted from the client.
|
|
21
21
|
*
|
|
22
|
+
* USAGE PRICING (US-012). A person whose person billing summary carries a
|
|
23
|
+
* `personalUsage` block is on usage pricing and has no personal plan to
|
|
24
|
+
* sponsor. `request` tells them so and files nothing.
|
|
25
|
+
*
|
|
22
26
|
* FEATURE SWITCH. The `plan` request kind only exists while hq-pro's
|
|
23
27
|
* `PLAN_SPONSORSHIP_REQUESTS_ENABLED` switch is on. With the switch off the
|
|
24
28
|
* backend does not recognise `plan` and answers 400 `MISSING_FIELDS` — which
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* them: it resolves the caller's person id and the company's admin set
|
|
20
20
|
* server-side, so nothing here is trusted from the client.
|
|
21
21
|
*
|
|
22
|
+
* USAGE PRICING (US-012). A person whose person billing summary carries a
|
|
23
|
+
* `personalUsage` block is on usage pricing and has no personal plan to
|
|
24
|
+
* sponsor. `request` tells them so and files nothing.
|
|
25
|
+
*
|
|
22
26
|
* FEATURE SWITCH. The `plan` request kind only exists while hq-pro's
|
|
23
27
|
* `PLAN_SPONSORSHIP_REQUESTS_ENABLED` switch is on. With the switch off the
|
|
24
28
|
* backend does not recognise `plan` and answers 400 `MISSING_FIELDS` — which
|
|
@@ -31,6 +35,7 @@ import { diagnosticsEnabled } from "../lib/diagnostics.js";
|
|
|
31
35
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
32
36
|
import { isCompanySelectionError } from "../utils/company-selection-error.js";
|
|
33
37
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
38
|
+
import { fetchCompanyPricingPlan, fetchPersonPricingPlan, isUsageOnly, usageOnlySponsorLines, } from "../lib/billing/usage-only.js";
|
|
34
39
|
/** The request kind this command files. Matches hq-pro's contract. */
|
|
35
40
|
export const PLAN_RESOURCE = "plan";
|
|
36
41
|
/** What a person is told when hq-pro's plan-sponsorship switch is off. */
|
|
@@ -163,18 +168,41 @@ async function transition(requestId, op) {
|
|
|
163
168
|
});
|
|
164
169
|
return { ...data.request, code: data.code, addCardUrl: data.addCardUrl };
|
|
165
170
|
}
|
|
171
|
+
/** Whether the sponsoring company is on usage pricing. Unknown counts as no. */
|
|
172
|
+
async function companyIsUsageOnly(companyUid) {
|
|
173
|
+
if (!companyUid)
|
|
174
|
+
return false;
|
|
175
|
+
try {
|
|
176
|
+
return isUsageOnly(await fetchCompanyPricingPlan(await ensureCognitoToken(), companyUid));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
166
182
|
export function registerBillingSponsorCommands(billing) {
|
|
167
183
|
const sponsor = billing
|
|
168
184
|
.command("sponsor")
|
|
169
|
-
.description("Ask a company to cover your
|
|
185
|
+
.description("Ask a company to cover your personal HQ plan, or answer a request");
|
|
170
186
|
sponsor
|
|
171
187
|
.command("request")
|
|
172
188
|
.argument("<company>", "Company slug or uid to ask")
|
|
173
|
-
.description("Ask a company to cover your
|
|
189
|
+
.description("Ask a company to cover your personal HQ plan")
|
|
174
190
|
.option("--json", "Emit raw JSON")
|
|
175
191
|
.action(async function (company, opts) {
|
|
176
192
|
try {
|
|
177
193
|
const token = await ensureCognitoToken();
|
|
194
|
+
// Usage pricing (US-012): a person on usage pricing has no personal
|
|
195
|
+
// plan for a company to cover, so nothing is filed.
|
|
196
|
+
if (isUsageOnly(await fetchPersonPricingPlan(token))) {
|
|
197
|
+
const lines = usageOnlySponsorLines();
|
|
198
|
+
if (opts.json) {
|
|
199
|
+
process.stdout.write(JSON.stringify({ pricingPlan: "usage-only", request: null, message: lines.join(" ") }, null, 2) + "\n");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const line of lines)
|
|
203
|
+
console.log(line);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
178
206
|
const companyUid = await getCompanyUid(token, company);
|
|
179
207
|
const data = await callApi(token, "/v1/billing/sponsorship/request", { method: "POST", body: { companyUid, resource: PLAN_RESOURCE } });
|
|
180
208
|
if (opts.json) {
|
|
@@ -235,6 +263,12 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
235
263
|
console.log(line);
|
|
236
264
|
return;
|
|
237
265
|
}
|
|
266
|
+
// Usage pricing (US-012): no personal plan, so no plan status to show.
|
|
267
|
+
if (!opts.json && isUsageOnly(await fetchPersonPricingPlan(token))) {
|
|
268
|
+
for (const line of usageOnlySponsorLines())
|
|
269
|
+
console.log(line);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
238
272
|
const state = await readSelfState(token, companyUid);
|
|
239
273
|
if (opts.json) {
|
|
240
274
|
process.stdout.write(JSON.stringify(state, null, 2) + "\n");
|
|
@@ -277,7 +311,7 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
277
311
|
sponsor
|
|
278
312
|
.command("approve")
|
|
279
313
|
.argument("<id>", "Request id")
|
|
280
|
-
.description("Say yes — put someone's
|
|
314
|
+
.description("Say yes — put someone's personal plan on the company bill")
|
|
281
315
|
.option("--json", "Emit raw JSON")
|
|
282
316
|
.action(async function (id, opts) {
|
|
283
317
|
try {
|
|
@@ -292,7 +326,9 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
292
326
|
console.log(` ${chalk.cyan(result.addCardUrl)}`);
|
|
293
327
|
return;
|
|
294
328
|
}
|
|
295
|
-
console.log(
|
|
329
|
+
console.log((await companyIsUsageOnly(result.companyUid))
|
|
330
|
+
? "Approved. Their personal plan is now on the company bill."
|
|
331
|
+
: "Approved. Their $50 plan is now on the company bill.");
|
|
296
332
|
}
|
|
297
333
|
catch (err) {
|
|
298
334
|
fail(err);
|
|
@@ -332,7 +368,9 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
332
368
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
333
369
|
return;
|
|
334
370
|
}
|
|
335
|
-
console.log(
|
|
371
|
+
console.log((await companyIsUsageOnly(result.companyUid))
|
|
372
|
+
? "Ended. Their personal plan goes back to them from the next bill."
|
|
373
|
+
: "Ended. Their $50 plan goes back to them from the next bill.");
|
|
336
374
|
}
|
|
337
375
|
catch (err) {
|
|
338
376
|
fail(err);
|
|
@@ -353,6 +391,11 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
353
391
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
354
392
|
return;
|
|
355
393
|
}
|
|
394
|
+
if (isUsageOnly(await fetchPersonPricingPlan(await ensureCognitoToken()))) {
|
|
395
|
+
console.log("Withdrawn.");
|
|
396
|
+
console.log(usageOnlySponsorLines()[2]);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
356
399
|
console.log("Withdrawn. You can pay the $50 yourself, reduce what you store, or ask another company.");
|
|
357
400
|
}
|
|
358
401
|
catch (err) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing usage` (usage-only-pricing US-015, rates per the 2026-10-16 card
|
|
3
|
+
* in US-012): this month's usage per meter, the projected bill, the included
|
|
4
|
+
* usage left, and the biggest drivers with a fix for each. Rates come from the
|
|
5
|
+
* `usage` block of `GET /v1/pricing`, or the bundled card
|
|
6
|
+
* (src/lib/billing/usage-card.ts) when the feed has none. The /hq-cost skill in hq-core runs this, previews a fix, and only
|
|
7
|
+
* applies it after the user says yes.
|
|
8
|
+
*
|
|
9
|
+
* Read-only. It never prunes, deletes, runs gc or reclaims space: the cloud
|
|
10
|
+
* storage split uses the same listing as `hq storage status`, which is
|
|
11
|
+
* read-only too.
|
|
12
|
+
*
|
|
13
|
+
* Tenant boundary: the company comes from the caller's own active memberships
|
|
14
|
+
* (`/membership/me`); a slug outside them is refused before any read. The
|
|
15
|
+
* usage-limits routes are member-gated on the server as well. With no cloud
|
|
16
|
+
* company (or `--personal`) it reads the caller's personal scope.
|
|
17
|
+
*
|
|
18
|
+
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
|
|
19
|
+
* the single seam to switch to the US-005 daily rollup once it has a read route.
|
|
20
|
+
*/
|
|
21
|
+
import type { Command } from "commander";
|
|
22
|
+
import { type ReadMonthUsageDeps, type StorageSplit, type UsageReport, type UsageScope } from "../lib/billing/usage-cost.js";
|
|
23
|
+
export interface MembershipRef {
|
|
24
|
+
uid: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface BillingUsageDeps {
|
|
29
|
+
listCompanies(): Promise<MembershipRef[]>;
|
|
30
|
+
fetchUsageLimits: ReadMonthUsageDeps["fetchUsageLimits"];
|
|
31
|
+
readStorageSplit?: (company: MembershipRef, opts: {
|
|
32
|
+
fresh: boolean;
|
|
33
|
+
}) => Promise<StorageSplit | null>;
|
|
34
|
+
fetchPricing(): Promise<unknown | null>;
|
|
35
|
+
/** Whether the usage trial is running for the scope. Null = unknown. */
|
|
36
|
+
readTrialActive?: (scope: UsageScope) => Promise<boolean | null>;
|
|
37
|
+
now(): Date;
|
|
38
|
+
}
|
|
39
|
+
export interface BillingUsageOptions {
|
|
40
|
+
company?: string;
|
|
41
|
+
personal?: boolean;
|
|
42
|
+
storageScan?: boolean;
|
|
43
|
+
fresh?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class UsageScopeError extends Error {
|
|
46
|
+
}
|
|
47
|
+
/** Pick the scope. Never reaches outside the caller's own memberships. */
|
|
48
|
+
export declare function resolveUsageScope(memberships: MembershipRef[], opts: BillingUsageOptions): UsageScope;
|
|
49
|
+
export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<UsageReport>;
|
|
50
|
+
/**
|
|
51
|
+
* `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
|
|
52
|
+
* card when the feed has none. Needs no sign-in.
|
|
53
|
+
*/
|
|
54
|
+
export declare function renderUsageCard(opts?: {
|
|
55
|
+
json?: boolean;
|
|
56
|
+
fetchPricing?: () => Promise<unknown | null>;
|
|
57
|
+
}): Promise<string>;
|
|
58
|
+
export declare function registerBillingUsageCommand(billing: Command): void;
|
|
59
|
+
//# sourceMappingURL=billing-usage.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing usage` (usage-only-pricing US-015, rates per the 2026-10-16 card
|
|
3
|
+
* in US-012): this month's usage per meter, the projected bill, the included
|
|
4
|
+
* usage left, and the biggest drivers with a fix for each. Rates come from the
|
|
5
|
+
* `usage` block of `GET /v1/pricing`, or the bundled card
|
|
6
|
+
* (src/lib/billing/usage-card.ts) when the feed has none. The /hq-cost skill in hq-core runs this, previews a fix, and only
|
|
7
|
+
* applies it after the user says yes.
|
|
8
|
+
*
|
|
9
|
+
* Read-only. It never prunes, deletes, runs gc or reclaims space: the cloud
|
|
10
|
+
* storage split uses the same listing as `hq storage status`, which is
|
|
11
|
+
* read-only too.
|
|
12
|
+
*
|
|
13
|
+
* Tenant boundary: the company comes from the caller's own active memberships
|
|
14
|
+
* (`/membership/me`); a slug outside them is refused before any read. The
|
|
15
|
+
* usage-limits routes are member-gated on the server as well. With no cloud
|
|
16
|
+
* company (or `--personal`) it reads the caller's personal scope.
|
|
17
|
+
*
|
|
18
|
+
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
|
|
19
|
+
* the single seam to switch to the US-005 daily rollup once it has a read route.
|
|
20
|
+
*/
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
23
|
+
import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
|
|
24
|
+
import { buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
|
|
25
|
+
import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
|
|
26
|
+
import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
|
|
27
|
+
import { fetchCompanyBillingSummary, usageTrialActiveFromBody } from "../lib/billing/usage-only.js";
|
|
28
|
+
export class UsageScopeError extends Error {
|
|
29
|
+
}
|
|
30
|
+
/** Pick the scope. Never reaches outside the caller's own memberships. */
|
|
31
|
+
export function resolveUsageScope(memberships, opts) {
|
|
32
|
+
if (opts.personal && opts.company) {
|
|
33
|
+
throw new UsageScopeError("Pass --company or --personal, not both.");
|
|
34
|
+
}
|
|
35
|
+
if (opts.personal)
|
|
36
|
+
return { kind: "person" };
|
|
37
|
+
if (opts.company) {
|
|
38
|
+
const match = memberships.find((m) => m.slug === opts.company || m.uid === opts.company);
|
|
39
|
+
if (!match) {
|
|
40
|
+
throw new UsageScopeError(`You are not an active member of '${opts.company}'. hq billing usage only shows your own companies.`);
|
|
41
|
+
}
|
|
42
|
+
return { kind: "company", companyUid: match.uid, companySlug: match.slug, ...(match.role ? { role: match.role } : {}) };
|
|
43
|
+
}
|
|
44
|
+
if (memberships.length === 0)
|
|
45
|
+
return { kind: "person" };
|
|
46
|
+
if (memberships.length === 1) {
|
|
47
|
+
const [m] = memberships;
|
|
48
|
+
return { kind: "company", companyUid: m.uid, companySlug: m.slug, ...(m.role ? { role: m.role } : {}) };
|
|
49
|
+
}
|
|
50
|
+
throw new UsageScopeError("You are in more than one company. Re-run with one of:\n" +
|
|
51
|
+
memberships.map((m) => ` --company ${m.slug}`).join("\n") +
|
|
52
|
+
"\n --personal");
|
|
53
|
+
}
|
|
54
|
+
export async function runBillingUsage(opts, deps) {
|
|
55
|
+
const memberships = await deps.listCompanies();
|
|
56
|
+
const scope = resolveUsageScope(memberships, opts);
|
|
57
|
+
const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
|
|
58
|
+
const [usage, pricing] = await Promise.all([
|
|
59
|
+
readMonthUsage(scope, {
|
|
60
|
+
fetchUsageLimits: deps.fetchUsageLimits,
|
|
61
|
+
now: deps.now,
|
|
62
|
+
...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
|
|
63
|
+
...(opts.storageScan !== false && deps.readStorageSplit && membership
|
|
64
|
+
? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
|
|
65
|
+
: {}),
|
|
66
|
+
}),
|
|
67
|
+
deps.fetchPricing().catch(() => null),
|
|
68
|
+
]);
|
|
69
|
+
return buildReport(usage, ratesFromPricingStatement(pricing));
|
|
70
|
+
}
|
|
71
|
+
// ── Production deps ─────────────────────────────────────────────────────────
|
|
72
|
+
function productionDeps() {
|
|
73
|
+
let tokenP;
|
|
74
|
+
const token = () => (tokenP ??= ensureCognitoToken());
|
|
75
|
+
return {
|
|
76
|
+
async listCompanies() {
|
|
77
|
+
const res = await vaultApiFetch({ token: await token(), path: "/membership/me" });
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`Could not load your companies (HTTP ${res.status}). Run \`hq login\`.`);
|
|
80
|
+
const { companiesFromMemberships } = await import("./storage.js");
|
|
81
|
+
return companiesFromMemberships(await res.json());
|
|
82
|
+
},
|
|
83
|
+
async fetchUsageLimits(scope) {
|
|
84
|
+
const res = await vaultApiFetch(scope.kind === "company"
|
|
85
|
+
? { token: await token(), path: "/v1/billing/usage-limits", query: { companyUid: scope.companyUid } }
|
|
86
|
+
: { token: await token(), path: "/v1/billing/person/usage-limits" });
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
return null;
|
|
89
|
+
return res.json().catch(() => null);
|
|
90
|
+
},
|
|
91
|
+
async readStorageSplit(company, { fresh }) {
|
|
92
|
+
const storage = await import("./storage.js");
|
|
93
|
+
const deps = await storage.buildStorageDeps();
|
|
94
|
+
const hit = fresh ? undefined : deps.cloudCache?.read(company);
|
|
95
|
+
const status = hit ?? (await storage.getCloudStatus(company, deps));
|
|
96
|
+
if (!hit)
|
|
97
|
+
deps.cloudCache?.write(company, status);
|
|
98
|
+
if (!status.available)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
currentBytes: status.current_bytes,
|
|
102
|
+
oldVersionBytes: status.noncurrent_bytes,
|
|
103
|
+
oldVersionCount: status.noncurrent_count,
|
|
104
|
+
...(status.listed_at ? { listedAt: status.listed_at } : {}),
|
|
105
|
+
canDelete: status.can_delete,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
async fetchPricing() {
|
|
109
|
+
const res = await vaultApiFetchPublic({ path: "/v1/pricing" });
|
|
110
|
+
return res.ok ? res.json().catch(() => null) : null;
|
|
111
|
+
},
|
|
112
|
+
async readTrialActive(scope) {
|
|
113
|
+
// The trial is a company thing; the owner-gated summary says whether it
|
|
114
|
+
// runs. A member who is not an owner gets 403 and the answer is unknown.
|
|
115
|
+
if (scope.kind !== "company")
|
|
116
|
+
return null;
|
|
117
|
+
const body = await fetchCompanyBillingSummary(await token(), scope.companyUid);
|
|
118
|
+
return body === null ? null : usageTrialActiveFromBody(body);
|
|
119
|
+
},
|
|
120
|
+
now: () => new Date(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
|
|
125
|
+
* card when the feed has none. Needs no sign-in.
|
|
126
|
+
*/
|
|
127
|
+
export async function renderUsageCard(opts = {}) {
|
|
128
|
+
const { card, source } = await fetchUsageCard(opts.fetchPricing ? { fetchPricing: opts.fetchPricing } : {});
|
|
129
|
+
if (opts.json)
|
|
130
|
+
return JSON.stringify({ source, card }, null, 2);
|
|
131
|
+
const from = source === "feed"
|
|
132
|
+
? "from GET /v1/pricing"
|
|
133
|
+
: "bundled copy; the pricing feed has no usage card yet";
|
|
134
|
+
return [`HQ usage pricing (card ${card.version}, ${from})`, "", ...formatUsageCardLines(card)].join("\n");
|
|
135
|
+
}
|
|
136
|
+
export function registerBillingUsageCommand(billing) {
|
|
137
|
+
billing
|
|
138
|
+
.command("usage")
|
|
139
|
+
.description("Show this month's usage, projected bill, included usage left and what drives it (read-only)")
|
|
140
|
+
.option("--company <slug>", "One of your companies (default: your only company)")
|
|
141
|
+
.option("--personal", "Your personal HQ instead of a company")
|
|
142
|
+
.option("--no-storage-scan", "Skip listing cloud file versions (faster; no old-versions split)")
|
|
143
|
+
.option("--fresh", "List cloud file versions live instead of using figures from the last 15 minutes")
|
|
144
|
+
.option("--card", "Print the usage price card (meters, included usage, trial, boxes, domains) and exit")
|
|
145
|
+
.option("--json", "Print JSON")
|
|
146
|
+
.action(async function (opts) {
|
|
147
|
+
try {
|
|
148
|
+
if (opts.card) {
|
|
149
|
+
console.log(await renderUsageCard({ json: opts.json === true }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const parentCompany = this.parent?.opts().company ?? undefined;
|
|
153
|
+
const report = await runBillingUsage({ ...opts, company: opts.company ?? parentCompany }, productionDeps());
|
|
154
|
+
console.log(opts.json ? JSON.stringify(report, null, 2) : formatReport(report));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=billing-usage.js.map
|
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* Subcommands:
|
|
12
12
|
* hq billing status [--company <slug>] — subscription + card on file
|
|
13
13
|
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
|
+
* hq billing usage [--company <slug>] [--personal] — usage, projected bill,
|
|
15
|
+
* drivers (read-only)
|
|
16
|
+
* hq billing upgrade [--company <slug>] — Workforce checkout
|
|
17
|
+
* (legacy plans only)
|
|
14
18
|
* hq billing sponsor … — ask a company to cover
|
|
15
19
|
* your $50 plan, or
|
|
16
20
|
* answer such a request
|
|
21
|
+
*
|
|
22
|
+
* Usage pricing (US-012): a company whose billing summary says
|
|
23
|
+
* `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
|
|
24
|
+
* tells it so and points at `hq billing usage` instead of opening Workforce
|
|
25
|
+
* checkout. Legacy companies keep the Workforce flow unchanged.
|
|
17
26
|
*/
|
|
18
27
|
import { Command } from "commander";
|
|
19
28
|
interface DatabaseBilling {
|
package/dist/commands/billing.js
CHANGED
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* Subcommands:
|
|
12
12
|
* hq billing status [--company <slug>] — subscription + card on file
|
|
13
13
|
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
|
+
* hq billing usage [--company <slug>] [--personal] — usage, projected bill,
|
|
15
|
+
* drivers (read-only)
|
|
16
|
+
* hq billing upgrade [--company <slug>] — Workforce checkout
|
|
17
|
+
* (legacy plans only)
|
|
14
18
|
* hq billing sponsor … — ask a company to cover
|
|
15
19
|
* your $50 plan, or
|
|
16
20
|
* answer such a request
|
|
21
|
+
*
|
|
22
|
+
* Usage pricing (US-012): a company whose billing summary says
|
|
23
|
+
* `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
|
|
24
|
+
* tells it so and points at `hq billing usage` instead of opening Workforce
|
|
25
|
+
* checkout. Legacy companies keep the Workforce flow unchanged.
|
|
17
26
|
*/
|
|
18
27
|
import chalk from "chalk";
|
|
19
28
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -21,6 +30,9 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
|
21
30
|
import { mintPaymentLink } from "../utils/billing-gate.js";
|
|
22
31
|
import { upgradeToTeam } from "../utils/team-upgrade.js";
|
|
23
32
|
import { registerBillingSponsorCommands } from "./billing-sponsor.js";
|
|
33
|
+
import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
|
|
34
|
+
import { fetchUsageCard } from "../lib/billing/usage-card.js";
|
|
35
|
+
import { registerBillingUsageCommand } from "./billing-usage.js";
|
|
24
36
|
const CHARGED_ON = {
|
|
25
37
|
team: "the Team subscription",
|
|
26
38
|
dedicated: "the separate HQ database apps subscription",
|
|
@@ -145,14 +157,28 @@ export function registerBillingCommand(program) {
|
|
|
145
157
|
});
|
|
146
158
|
billing
|
|
147
159
|
.command("upgrade")
|
|
148
|
-
.description("Open Stripe Checkout to upgrade
|
|
160
|
+
.description("Open Stripe Checkout to upgrade a company on an earlier plan (usage-pricing companies have no plan to upgrade)")
|
|
149
161
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
150
162
|
.option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
|
|
151
163
|
.option("--json", "Emit the Checkout URL as JSON")
|
|
152
164
|
.action(async function (opts) {
|
|
153
165
|
try {
|
|
166
|
+
const token = await ensureCognitoToken();
|
|
167
|
+
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
168
|
+
const plan = await fetchCompanyPricingPlan(token, companyUid);
|
|
169
|
+
if (isUsageOnly(plan)) {
|
|
170
|
+
const { card } = await fetchUsageCard();
|
|
171
|
+
const lines = usageOnlyUpgradeLines(card);
|
|
172
|
+
if (opts.json) {
|
|
173
|
+
process.stdout.write(JSON.stringify({ pricingPlan: "usage-only", url: null, message: lines.join(" ") }, null, 2) + "\n");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const line of lines)
|
|
177
|
+
console.log(line);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
154
180
|
await upgradeToTeam({
|
|
155
|
-
|
|
181
|
+
companyUid,
|
|
156
182
|
noBrowser: opts.browser === false,
|
|
157
183
|
json: opts.json,
|
|
158
184
|
});
|
|
@@ -162,5 +188,6 @@ export function registerBillingCommand(program) {
|
|
|
162
188
|
}
|
|
163
189
|
});
|
|
164
190
|
registerBillingSponsorCommands(billing);
|
|
191
|
+
registerBillingUsageCommand(billing);
|
|
165
192
|
}
|
|
166
193
|
//# sourceMappingURL=billing.js.map
|