@indigoai-us/hq-cli 5.345.159 → 5.345.161
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 +48 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +222 -9
- package/dist/command-catalog.generated.js +273 -9
- 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/agents.js +5 -11
- package/dist/commands/auth.js +6 -5
- 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/cloud-provision.js +2 -2
- package/dist/commands/cloud.js +7 -7
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/files.js +19 -7
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +15 -4
- package/dist/commands/meetings.js +58 -19
- package/dist/commands/skill.js +6 -6
- 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 +3236 -1988
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- 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/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -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/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
package/dist/commands/agents.js
CHANGED
|
@@ -131,12 +131,6 @@ export function agentCreatePlanLimitFromPayload(status, body) {
|
|
|
131
131
|
: {}),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
function formatServerPrice(amountMinor, currency) {
|
|
135
|
-
return new Intl.NumberFormat("en-US", {
|
|
136
|
-
style: "currency",
|
|
137
|
-
currency,
|
|
138
|
-
}).format(amountMinor / 100);
|
|
139
|
-
}
|
|
140
134
|
async function offerAgentCheckout(url) {
|
|
141
135
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
142
136
|
try {
|
|
@@ -146,7 +140,7 @@ async function offerAgentCheckout(url) {
|
|
|
146
140
|
});
|
|
147
141
|
}
|
|
148
142
|
catch {
|
|
149
|
-
console.error("Could not open the
|
|
143
|
+
console.error("Could not open the billing page automatically. Use the URL above.");
|
|
150
144
|
return false;
|
|
151
145
|
}
|
|
152
146
|
finally {
|
|
@@ -156,7 +150,7 @@ async function offerAgentCheckout(url) {
|
|
|
156
150
|
/** Print a server-priced plan block without minting or rewriting its URL. */
|
|
157
151
|
export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
|
|
158
152
|
if (isUsageOnly(opts.pricingPlan)) {
|
|
159
|
-
// Usage pricing (US-012): no
|
|
153
|
+
// Usage pricing (US-012): no flat plan to buy. The company adds a
|
|
160
154
|
// card and each agent box bills by size from the usage card.
|
|
161
155
|
const card = opts.usageCard ?? BUNDLED_USAGE_CARD;
|
|
162
156
|
for (const line of usageOnlyCardRequiredLines("An agent box", card)) {
|
|
@@ -166,13 +160,13 @@ export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
|
|
|
166
160
|
queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
|
|
167
161
|
return;
|
|
168
162
|
}
|
|
169
|
-
|
|
170
|
-
const reason =
|
|
163
|
+
// The server's price was the flat plan's, which is closed: name no plan or price.
|
|
164
|
+
const reason = "Agents are not included on this company's current billing.";
|
|
171
165
|
const argv = opts.json ? [...process.argv, "--json"] : process.argv;
|
|
172
166
|
const interactive = canOfferTeamUpgrade(argv);
|
|
173
167
|
if (!planLimit.checkoutUrl) {
|
|
174
168
|
console.error(reason);
|
|
175
|
-
console.error("Ask a company owner to
|
|
169
|
+
console.error("Ask a company owner to check the company's Billing page before creating an agent.");
|
|
176
170
|
queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
|
|
177
171
|
return;
|
|
178
172
|
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -385,7 +385,10 @@ export function registerAuthCommands(program) {
|
|
|
385
385
|
if (!options.retry) {
|
|
386
386
|
const state = keychain.readKeychainAccessState();
|
|
387
387
|
if (state.deniedAt) {
|
|
388
|
-
console.log("Keychain access
|
|
388
|
+
console.log("Keychain access did not complete, so HQ uses ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again.");
|
|
389
|
+
}
|
|
390
|
+
else if (state.lastFailureAt) {
|
|
391
|
+
console.log("Keychain access did not complete. HQ uses your saved sign-in instead and can ask again from a terminal. Run `hq auth keychain --retry` to retry now.");
|
|
389
392
|
}
|
|
390
393
|
else if (state.allowedAt) {
|
|
391
394
|
console.log(chalk.green("HQ can read your sign-in from the Keychain."));
|
|
@@ -395,9 +398,7 @@ export function registerAuthCommands(program) {
|
|
|
395
398
|
}
|
|
396
399
|
return;
|
|
397
400
|
}
|
|
398
|
-
|
|
399
|
-
if (!state.allowedAt)
|
|
400
|
-
console.error(keychain.KEYCHAIN_FIRST_READ_NOTE);
|
|
401
|
+
console.error(keychain.KEYCHAIN_FIRST_READ_NOTE);
|
|
401
402
|
const result = keychain.readKeychainTokensDetailed({ retry: true, interactive: true, notify: () => { } });
|
|
402
403
|
if (result.outcome === "hit") {
|
|
403
404
|
console.log(result.tokens
|
|
@@ -408,7 +409,7 @@ export function registerAuthCommands(program) {
|
|
|
408
409
|
console.log("No HQ sign-in is saved in the Keychain yet. HQ will use ~/.hq/cognito-tokens.json.");
|
|
409
410
|
}
|
|
410
411
|
else {
|
|
411
|
-
console.log(chalk.yellow("Keychain access
|
|
412
|
+
console.log(chalk.yellow("Keychain access did not complete. HQ will use ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again."));
|
|
412
413
|
}
|
|
413
414
|
});
|
|
414
415
|
authCmd
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* `hq billing sponsor` — the individual-pull half of plan sponsorship, from the
|
|
3
3
|
* terminal ("ask a company to sponsor me", hq-pro #3609).
|
|
4
4
|
*
|
|
5
|
-
* A person who is over a personal limit has exactly three ways out: pay
|
|
6
|
-
* themselves, reduce what they store, or ask a company to
|
|
7
|
-
*
|
|
5
|
+
* A person who is over a personal limit has exactly three ways out: pay for
|
|
6
|
+
* the personal plan themselves, reduce what they store, or ask a company to
|
|
7
|
+
* cover it. This command is the third one, plus the admin side that answers it.
|
|
8
8
|
*
|
|
9
9
|
* hq billing sponsor request <company> — ask that company to cover my plan
|
|
10
10
|
* hq billing sponsor list — the queue (admins), or my own
|
|
@@ -36,7 +36,7 @@ export declare const PLAN_RESOURCE: "plan";
|
|
|
36
36
|
/** What a person is told when hq-pro's plan-sponsorship switch is off. */
|
|
37
37
|
export declare const FEATURE_OFF_MESSAGE = "Asking a company to sponsor your plan isn't available yet.";
|
|
38
38
|
/** What v1 sponsorship covers, stated wherever a person confirms or reads it. */
|
|
39
|
-
export declare const COVERAGE_SENTENCE = "Your company pays your
|
|
39
|
+
export declare const COVERAGE_SENTENCE = "Your company pays your personal plan. Storage above your plan's allowance is still yours.";
|
|
40
40
|
/** What a person is told while an approval waits on the sponsor's card. */
|
|
41
41
|
export declare const HELD_FOR_CARD_SENTENCE = "Your company said yes and is adding a card. Your limits still apply until then.";
|
|
42
42
|
/** One request row as hq-pro persists and returns it. */
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* `hq billing sponsor` — the individual-pull half of plan sponsorship, from the
|
|
3
3
|
* terminal ("ask a company to sponsor me", hq-pro #3609).
|
|
4
4
|
*
|
|
5
|
-
* A person who is over a personal limit has exactly three ways out: pay
|
|
6
|
-
* themselves, reduce what they store, or ask a company to
|
|
7
|
-
*
|
|
5
|
+
* A person who is over a personal limit has exactly three ways out: pay for
|
|
6
|
+
* the personal plan themselves, reduce what they store, or ask a company to
|
|
7
|
+
* cover it. This command is the third one, plus the admin side that answers it.
|
|
8
8
|
*
|
|
9
9
|
* hq billing sponsor request <company> — ask that company to cover my plan
|
|
10
10
|
* hq billing sponsor list — the queue (admins), or my own
|
|
@@ -41,7 +41,7 @@ export const PLAN_RESOURCE = "plan";
|
|
|
41
41
|
/** What a person is told when hq-pro's plan-sponsorship switch is off. */
|
|
42
42
|
export const FEATURE_OFF_MESSAGE = "Asking a company to sponsor your plan isn't available yet.";
|
|
43
43
|
/** What v1 sponsorship covers, stated wherever a person confirms or reads it. */
|
|
44
|
-
export const COVERAGE_SENTENCE = "Your company pays your
|
|
44
|
+
export const COVERAGE_SENTENCE = "Your company pays your personal plan. Storage above your plan's allowance is still yours.";
|
|
45
45
|
/** What a person is told while an approval waits on the sponsor's card. */
|
|
46
46
|
export const HELD_FOR_CARD_SENTENCE = "Your company said yes and is adding a card. Your limits still apply until then.";
|
|
47
47
|
/**
|
|
@@ -85,7 +85,7 @@ export function messageForError(status, body) {
|
|
|
85
85
|
if (status === 401)
|
|
86
86
|
return "Your sign-in has expired. Run `hq login` and try again.";
|
|
87
87
|
if (status === 403 && code === "SPONSOR_PLAN_REQUIRED") {
|
|
88
|
-
return "That company
|
|
88
|
+
return "That company can't cover a personal plan on its current billing. Ask an owner of that company to check its Billing page.";
|
|
89
89
|
}
|
|
90
90
|
if (status === 403) {
|
|
91
91
|
return "You're not allowed to do that. Only an owner or admin of the company can answer a sponsorship request.";
|
|
@@ -209,7 +209,7 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
209
209
|
process.stdout.write(JSON.stringify(data.request, null, 2) + "\n");
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
console.log(`Asked ${company} to cover your
|
|
212
|
+
console.log(`Asked ${company} to cover your personal HQ plan.`);
|
|
213
213
|
console.log(COVERAGE_SENTENCE);
|
|
214
214
|
console.log(`An owner or admin there answers next. Check with \`hq billing sponsor list\`.`);
|
|
215
215
|
}
|
|
@@ -276,11 +276,11 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
276
276
|
}
|
|
277
277
|
const where = named ?? "that company";
|
|
278
278
|
if (!state.active) {
|
|
279
|
-
console.log(`No company is covering your
|
|
279
|
+
console.log(`No company is covering your personal HQ plan right now.`);
|
|
280
280
|
console.log(`If you asked ${where}, they haven't answered yet — an owner or admin there answers next.`);
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
|
-
console.log(`Your
|
|
283
|
+
console.log(`Your personal HQ plan: ${statusLabel(state.status ?? "active")}`);
|
|
284
284
|
if (state.requestId)
|
|
285
285
|
console.log(` ${chalk.bold(state.requestId)}`);
|
|
286
286
|
console.log(COVERAGE_SENTENCE);
|
|
@@ -328,7 +328,7 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
328
328
|
}
|
|
329
329
|
console.log((await companyIsUsageOnly(result.companyUid))
|
|
330
330
|
? "Approved. Their personal plan is now on the company bill."
|
|
331
|
-
: "Approved. Their
|
|
331
|
+
: "Approved. Their personal plan is now on the company bill.");
|
|
332
332
|
}
|
|
333
333
|
catch (err) {
|
|
334
334
|
fail(err);
|
|
@@ -370,7 +370,7 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
370
370
|
}
|
|
371
371
|
console.log((await companyIsUsageOnly(result.companyUid))
|
|
372
372
|
? "Ended. Their personal plan goes back to them from the next bill."
|
|
373
|
-
: "Ended. Their
|
|
373
|
+
: "Ended. Their personal plan goes back to them from the next bill.");
|
|
374
374
|
}
|
|
375
375
|
catch (err) {
|
|
376
376
|
fail(err);
|
|
@@ -396,7 +396,7 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
396
396
|
console.log(usageOnlySponsorLines()[2]);
|
|
397
397
|
return;
|
|
398
398
|
}
|
|
399
|
-
console.log("Withdrawn. You can pay
|
|
399
|
+
console.log("Withdrawn. You can pay for your plan yourself, reduce what you store, or ask another company.");
|
|
400
400
|
}
|
|
401
401
|
catch (err) {
|
|
402
402
|
fail(err);
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Tenant boundary: the company comes from the caller's own active memberships
|
|
14
14
|
* (`/membership/me`); a slug outside them is refused before any read. The
|
|
15
|
-
*
|
|
15
|
+
* company statement is owner-gated on the server as well. With no cloud
|
|
16
16
|
* company (or `--personal`) it reads the caller's personal scope.
|
|
17
17
|
*
|
|
18
|
-
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
|
|
19
|
-
* the
|
|
18
|
+
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
|
|
19
|
+
* from the usage statement, `GET /v1/billing/usage` (hq-pro US-006). Only a
|
|
20
|
+
* server without that route (404) falls back to the usage-limits readings.
|
|
20
21
|
*
|
|
21
22
|
* Only an account on usage pricing gets costs. The plan comes from the
|
|
22
|
-
* server
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* server says nothing about get readings only, with no rate, cost or
|
|
23
|
+
* server's shared plan status (`billingState`, hq-pro #5088) on the billing
|
|
24
|
+
* summary, with the legacy `pricingPlan` / `usageOnly` fields for a server
|
|
25
|
+
* that does not send it. An account on an earlier flat plan and an account
|
|
26
|
+
* the server says nothing about get readings only, with no rate, cost or
|
|
27
27
|
* projected bill.
|
|
28
28
|
*/
|
|
29
29
|
import type { Command } from "commander";
|
|
@@ -34,8 +34,19 @@ export interface MembershipRef {
|
|
|
34
34
|
slug: string;
|
|
35
35
|
role?: string;
|
|
36
36
|
}
|
|
37
|
+
/** One `GET /v1/billing/usage` answer: the HTTP status and the decoded body. */
|
|
38
|
+
export interface UsageStatementResponse {
|
|
39
|
+
status: number;
|
|
40
|
+
body: unknown | null;
|
|
41
|
+
}
|
|
37
42
|
export interface BillingUsageDeps {
|
|
38
43
|
listCompanies(): Promise<MembershipRef[]>;
|
|
44
|
+
/**
|
|
45
|
+
* `GET /v1/billing/usage` for the scope (US-015). Absent, or a 404 from a
|
|
46
|
+
* server without the route, falls back to {@link fetchUsageLimits}.
|
|
47
|
+
*/
|
|
48
|
+
fetchUsageStatement?: (scope: UsageScope) => Promise<UsageStatementResponse>;
|
|
49
|
+
/** Legacy readings for a server without the statement route. */
|
|
39
50
|
fetchUsageLimits: ReadMonthUsageDeps["fetchUsageLimits"];
|
|
40
51
|
readStorageSplit?: (company: MembershipRef, opts: {
|
|
41
52
|
fresh: boolean;
|
|
@@ -77,6 +88,9 @@ export declare function pricingPlanFromSummary(body: unknown): PricingPlan | nul
|
|
|
77
88
|
* only when usage-limits does not say.
|
|
78
89
|
*/
|
|
79
90
|
export declare function resolveOnUsagePricing(limitsPlan: PricingPlan | null, summaryPlan: PricingPlan | null): boolean | null;
|
|
91
|
+
/** Thrown when the company statement is refused to a caller who is not its owner. */
|
|
92
|
+
export declare class UsageStatementForbiddenError extends Error {
|
|
93
|
+
}
|
|
80
94
|
export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<BillingUsageReport>;
|
|
81
95
|
/**
|
|
82
96
|
* `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
|
|
@@ -12,27 +12,27 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Tenant boundary: the company comes from the caller's own active memberships
|
|
14
14
|
* (`/membership/me`); a slug outside them is refused before any read. The
|
|
15
|
-
*
|
|
15
|
+
* company statement is owner-gated on the server as well. With no cloud
|
|
16
16
|
* company (or `--personal`) it reads the caller's personal scope.
|
|
17
17
|
*
|
|
18
|
-
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
|
|
19
|
-
* the
|
|
18
|
+
* Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
|
|
19
|
+
* from the usage statement, `GET /v1/billing/usage` (hq-pro US-006). Only a
|
|
20
|
+
* server without that route (404) falls back to the usage-limits readings.
|
|
20
21
|
*
|
|
21
22
|
* Only an account on usage pricing gets costs. The plan comes from the
|
|
22
|
-
* server
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* server says nothing about get readings only, with no rate, cost or
|
|
23
|
+
* server's shared plan status (`billingState`, hq-pro #5088) on the billing
|
|
24
|
+
* summary, with the legacy `pricingPlan` / `usageOnly` fields for a server
|
|
25
|
+
* that does not send it. An account on an earlier flat plan and an account
|
|
26
|
+
* the server says nothing about get readings only, with no rate, cost or
|
|
27
27
|
* projected bill.
|
|
28
28
|
*/
|
|
29
29
|
import chalk from "chalk";
|
|
30
30
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
31
31
|
import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
|
|
32
|
-
import { buildReadingsReport, buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
|
|
32
|
+
import { buildReadingsReport, buildReport, formatReport, readMonthUsage, usageLimitsSource, usageStatementSource, } from "../lib/billing/usage-cost.js";
|
|
33
33
|
import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
|
|
34
34
|
import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
|
|
35
|
-
import { fetchCompanyBillingSummary, pricingPlanFromBody, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
|
|
35
|
+
import { fetchCompanyBillingSummary, pricingPlanFromBody, hasBillingStateField, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
|
|
36
36
|
export class UsageScopeError extends Error {
|
|
37
37
|
}
|
|
38
38
|
/** Pick the scope. Never reaches outside the caller's own memberships. */
|
|
@@ -67,7 +67,7 @@ function asRecord(value) {
|
|
|
67
67
|
/** The plan on a usage-limits body: `pricingPlan`, else the `usageOnly` / `onUsagePricing` boolean. */
|
|
68
68
|
export function pricingPlanFromUsageLimits(body) {
|
|
69
69
|
const plan = pricingPlanFromBody(body);
|
|
70
|
-
if (plan)
|
|
70
|
+
if (plan || hasBillingStateField(body))
|
|
71
71
|
return plan;
|
|
72
72
|
const rec = asRecord(body);
|
|
73
73
|
const flag = typeof rec?.onUsagePricing === "boolean" ? rec.onUsagePricing : rec?.usageOnly;
|
|
@@ -85,9 +85,12 @@ export function pricingPlanFromSummary(body) {
|
|
|
85
85
|
const rec = asRecord(body);
|
|
86
86
|
if (!rec)
|
|
87
87
|
return null;
|
|
88
|
-
// `plan: "usage"` only means "no
|
|
89
|
-
// so it never turns pricing on.
|
|
90
|
-
|
|
88
|
+
// `plan: "usage"` only means "no flat-plan subscription", not usage pricing,
|
|
89
|
+
// so it never turns pricing on. A server that sends billingState answers
|
|
90
|
+
// from it alone (unknown stays unknown); an older one without pricingPlan
|
|
91
|
+
// shows no cost.
|
|
92
|
+
const plan = pricingPlanFromBody(rec);
|
|
93
|
+
return hasBillingStateField(rec) ? plan : plan ?? "current";
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* True: price the meters. False: flat plan. Null: unknown, show no cost.
|
|
@@ -101,20 +104,44 @@ export function resolveOnUsagePricing(limitsPlan, summaryPlan) {
|
|
|
101
104
|
return null;
|
|
102
105
|
return plan === "usage-only";
|
|
103
106
|
}
|
|
107
|
+
/** Thrown when the company statement is refused to a caller who is not its owner. */
|
|
108
|
+
export class UsageStatementForbiddenError extends Error {
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read the statement, or say the caller should use the legacy readings: null
|
|
112
|
+
* means the server has no statement route (404) or no reader was given.
|
|
113
|
+
*/
|
|
114
|
+
async function readStatementBody(scope, deps) {
|
|
115
|
+
if (!deps.fetchUsageStatement)
|
|
116
|
+
return null;
|
|
117
|
+
const res = await deps.fetchUsageStatement(scope);
|
|
118
|
+
if (res.status === 404)
|
|
119
|
+
return null;
|
|
120
|
+
if (res.status === 403 && scope.kind === "company") {
|
|
121
|
+
throw new UsageStatementForbiddenError(`Only the owner of '${scope.companySlug}' can see its usage statement. ` +
|
|
122
|
+
"Run `hq billing usage --personal` for your own usage.");
|
|
123
|
+
}
|
|
124
|
+
return { body: res.status >= 200 && res.status < 300 ? res.body : null };
|
|
125
|
+
}
|
|
104
126
|
export async function runBillingUsage(opts, deps) {
|
|
105
127
|
const memberships = await deps.listCompanies();
|
|
106
128
|
const scope = resolveUsageScope(memberships, opts);
|
|
107
129
|
const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
|
|
108
130
|
let limitsBody = null;
|
|
131
|
+
const statement = await readStatementBody(scope, deps);
|
|
132
|
+
const usageDeps = {
|
|
133
|
+
fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
|
|
134
|
+
now: deps.now,
|
|
135
|
+
...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
|
|
136
|
+
...(opts.storageScan !== false && deps.readStorageSplit && membership
|
|
137
|
+
? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
|
|
138
|
+
: {}),
|
|
139
|
+
};
|
|
140
|
+
const source = statement
|
|
141
|
+
? usageStatementSource(async () => statement.body)
|
|
142
|
+
: usageLimitsSource(usageDeps);
|
|
109
143
|
const [usage, pricing] = await Promise.all([
|
|
110
|
-
readMonthUsage(scope,
|
|
111
|
-
fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
|
|
112
|
-
now: deps.now,
|
|
113
|
-
...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
|
|
114
|
-
...(opts.storageScan !== false && deps.readStorageSplit && membership
|
|
115
|
-
? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
|
|
116
|
-
: {}),
|
|
117
|
-
}),
|
|
144
|
+
readMonthUsage(scope, usageDeps, source),
|
|
118
145
|
deps.fetchPricing().catch(() => null),
|
|
119
146
|
]);
|
|
120
147
|
const limitsPlan = pricingPlanFromUsageLimits(limitsBody);
|
|
@@ -142,6 +169,14 @@ function productionDeps() {
|
|
|
142
169
|
const { companiesFromMemberships } = await import("./storage.js");
|
|
143
170
|
return companiesFromMemberships(await res.json());
|
|
144
171
|
},
|
|
172
|
+
async fetchUsageStatement(scope) {
|
|
173
|
+
const res = await vaultApiFetch(scope.kind === "company"
|
|
174
|
+
? { token: await token(), path: "/v1/billing/usage", query: { companyUid: scope.companyUid } }
|
|
175
|
+
: { token: await token(), path: "/v1/billing/usage", query: { scope: "person" } });
|
|
176
|
+
if (!res.ok)
|
|
177
|
+
return { status: res.status, body: null };
|
|
178
|
+
return { status: res.status, body: await res.json().catch(() => null) };
|
|
179
|
+
},
|
|
145
180
|
async fetchUsageLimits(scope) {
|
|
146
181
|
const res = await vaultApiFetch(scope.kind === "company"
|
|
147
182
|
? { token: await token(), path: "/v1/billing/usage-limits", query: { companyUid: scope.companyUid } }
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
14
|
* hq billing usage [--company <slug>] [--personal] — usage, projected bill,
|
|
15
15
|
* drivers (read-only)
|
|
16
|
-
* hq billing upgrade [--company <slug>] —
|
|
17
|
-
* (
|
|
16
|
+
* hq billing upgrade [--company <slug>] — the switch to usage
|
|
17
|
+
* pricing (earlier plans)
|
|
18
18
|
* hq billing sponsor … — ask a company to cover
|
|
19
|
-
* your
|
|
19
|
+
* your personal plan, or
|
|
20
20
|
* answer such a request
|
|
21
21
|
*
|
|
22
|
-
* Usage pricing (US-012): a company
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* checkout
|
|
22
|
+
* Usage pricing (US-012, US-024): a company on usage pricing has no plan to
|
|
23
|
+
* upgrade to; `hq billing upgrade` tells it so and points at
|
|
24
|
+
* `hq billing usage`. Every other company is pointed at the switch to usage
|
|
25
|
+
* pricing: the flat-plan checkout is closed, so the CLI never opens it.
|
|
26
26
|
*/
|
|
27
27
|
import { Command } from "commander";
|
|
28
28
|
interface DatabaseBilling {
|
package/dist/commands/billing.js
CHANGED
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
* hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
|
|
14
14
|
* hq billing usage [--company <slug>] [--personal] — usage, projected bill,
|
|
15
15
|
* drivers (read-only)
|
|
16
|
-
* hq billing upgrade [--company <slug>] —
|
|
17
|
-
* (
|
|
16
|
+
* hq billing upgrade [--company <slug>] — the switch to usage
|
|
17
|
+
* pricing (earlier plans)
|
|
18
18
|
* hq billing sponsor … — ask a company to cover
|
|
19
|
-
* your
|
|
19
|
+
* your personal plan, or
|
|
20
20
|
* answer such a request
|
|
21
21
|
*
|
|
22
|
-
* Usage pricing (US-012): a company
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* checkout
|
|
22
|
+
* Usage pricing (US-012, US-024): a company on usage pricing has no plan to
|
|
23
|
+
* upgrade to; `hq billing upgrade` tells it so and points at
|
|
24
|
+
* `hq billing usage`. Every other company is pointed at the switch to usage
|
|
25
|
+
* pricing: the flat-plan checkout is closed, so the CLI never opens it.
|
|
26
26
|
*/
|
|
27
27
|
import chalk from "chalk";
|
|
28
28
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
29
29
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
30
30
|
import { mintPaymentLink } from "../utils/billing-gate.js";
|
|
31
|
-
import {
|
|
31
|
+
import { openUsageSwitch } from "../utils/team-upgrade.js";
|
|
32
32
|
import { registerBillingSponsorCommands } from "./billing-sponsor.js";
|
|
33
33
|
import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
|
|
34
34
|
import { fetchUsageCard } from "../lib/billing/usage-card.js";
|
|
@@ -157,10 +157,10 @@ export function registerBillingCommand(program) {
|
|
|
157
157
|
});
|
|
158
158
|
billing
|
|
159
159
|
.command("upgrade")
|
|
160
|
-
.description("Open
|
|
160
|
+
.description("Open the switch to usage pricing for a company on an earlier plan (usage-pricing companies have nothing to change)")
|
|
161
161
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
162
|
-
.option("--no-browser", "Print the
|
|
163
|
-
.option("--json", "Emit the
|
|
162
|
+
.option("--no-browser", "Print the URL instead of opening it")
|
|
163
|
+
.option("--json", "Emit the URL as JSON")
|
|
164
164
|
.action(async function (opts) {
|
|
165
165
|
try {
|
|
166
166
|
const token = await ensureCognitoToken();
|
|
@@ -177,8 +177,8 @@ export function registerBillingCommand(program) {
|
|
|
177
177
|
console.log(line);
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
|
-
await
|
|
181
|
-
|
|
180
|
+
await openUsageSwitch({
|
|
181
|
+
companySlug: companyOf(this),
|
|
182
182
|
noBrowser: opts.browser === false,
|
|
183
183
|
json: opts.json,
|
|
184
184
|
});
|
|
@@ -484,8 +484,8 @@ async function defaultRunInitialSync(args) {
|
|
|
484
484
|
if (event.type === "plan-limit") {
|
|
485
485
|
failureCount += Math.max(1, event.count ?? 0, event.paths?.length ?? 0);
|
|
486
486
|
firstFailure ??= {
|
|
487
|
-
path: event.paths?.[0] ?? "
|
|
488
|
-
message: event.message ?? "
|
|
487
|
+
path: event.paths?.[0] ?? "new files",
|
|
488
|
+
message: event.message ?? "new files are paused for this account",
|
|
489
489
|
};
|
|
490
490
|
return;
|
|
491
491
|
}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1119,7 +1119,7 @@ export function registerCloudCommands(program) {
|
|
|
1119
1119
|
// and keep their exit status.
|
|
1120
1120
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
1121
1121
|
`${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
|
|
1122
|
-
"refused
|
|
1122
|
+
"refused: new files are paused for this account."));
|
|
1123
1123
|
for (const line of renderPlanLimitRefusalLines(planLimitRefused)) {
|
|
1124
1124
|
log(chalk.yellow(line));
|
|
1125
1125
|
}
|
|
@@ -1811,7 +1811,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
|
|
|
1811
1811
|
`${r.conflictPaths.length} conflict(s)` +
|
|
1812
1812
|
(r.aborted ? " — aborted" : "") +
|
|
1813
1813
|
(legRefused
|
|
1814
|
-
? ` — ${describeRefusedCount(legRefused)} refused
|
|
1814
|
+
? ` — ${describeRefusedCount(legRefused)} refused (new files paused)`
|
|
1815
1815
|
: ""));
|
|
1816
1816
|
if (legRefused) {
|
|
1817
1817
|
printPlanLimitRefusal(legRefused);
|
|
@@ -1828,7 +1828,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
|
|
|
1828
1828
|
`${result.filesRefusedStale ?? 0} stale-refused; ` +
|
|
1829
1829
|
`${result.filesUploadFailed ?? 0} failed` +
|
|
1830
1830
|
(refusedTargets > 0
|
|
1831
|
-
? `; new files
|
|
1831
|
+
? `; new files paused on ${refusedTargets} target(s)`
|
|
1832
1832
|
: "");
|
|
1833
1833
|
console.log(errored > 0 ||
|
|
1834
1834
|
result.filesExcludedByScope > 0 ||
|
|
@@ -1973,7 +1973,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1973
1973
|
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
1974
1974
|
(pushResult.aborted ? " — aborted" : "") +
|
|
1975
1975
|
(pushRefused
|
|
1976
|
-
? ` — ${describeRefusedCount(pushRefused)} refused
|
|
1976
|
+
? ` — ${describeRefusedCount(pushRefused)} refused (new files paused)`
|
|
1977
1977
|
: ""));
|
|
1978
1978
|
if (pushRefused)
|
|
1979
1979
|
printPlanLimitRefusal(pushRefused, " ");
|
|
@@ -2105,7 +2105,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2105
2105
|
if (pushRefused) {
|
|
2106
2106
|
// The pull ran, but the push did not upload everything: no success line.
|
|
2107
2107
|
console.log(chalk.yellow(`\n⚠ Sync now finished, but ${describeRefusedFiles(pushRefused)} ` +
|
|
2108
|
-
"refused
|
|
2108
|
+
"refused: new files are paused for this account (listed above)."));
|
|
2109
2109
|
throw new SyncExitError(1, { health: "succeeded" });
|
|
2110
2110
|
}
|
|
2111
2111
|
if (pushResult.filesExcludedByScope > 0 ||
|
|
@@ -2159,9 +2159,9 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
|
|
|
2159
2159
|
// like a clean sync. Its per-target details were printed above.
|
|
2160
2160
|
if (refused.planLimitRefused) {
|
|
2161
2161
|
console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused.planLimitRefused)} refused ` +
|
|
2162
|
-
"
|
|
2162
|
+
"because new files are paused for this account (listed under push --all above)."));
|
|
2163
2163
|
if (refused.planLimitRefused.upgradeUrl) {
|
|
2164
|
-
console.log(chalk.yellow(`
|
|
2164
|
+
console.log(chalk.yellow(` Billing: ${refused.planLimitRefused.upgradeUrl}`));
|
|
2165
2165
|
}
|
|
2166
2166
|
}
|
|
2167
2167
|
console.log(chalk.yellow("\n⚠ Sync finished, but one or more requested uploads were refused."));
|