@indigoai-us/hq-cli 5.345.41 → 5.345.42
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 +3 -0
- package/dist/command-catalog.generated.d.ts +5 -1
- package/dist/command-catalog.generated.js +6 -1
- package/dist/commands/db-remote-deps.js +3 -0
- package/dist/commands/db-usage.js +11 -4
- package/dist/lib/db/control-plane.d.ts +46 -0
- package/dist/lib/db/control-plane.js +14 -0
- package/dist/lib/db/usage-format.d.ts +4 -1
- package/dist/lib/db/usage-format.js +66 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.42] — 2026-10-07
|
|
6
|
+
- `hq db usage --app <name>` shows one database app's included amounts, use, overage, read-only ceiling and estimated monthly charge.
|
|
7
|
+
|
|
5
8
|
## [5.345.41] — 2026-10-07
|
|
6
9
|
- Short-lived hq commands now use a fresh cached company flag snapshot while the registry refresh is pending.
|
|
7
10
|
- Report failed and quota-interrupted keep-alive lead lanes as stranded in the keep-alive audit and `hq doctor`, with the senior recovery command and quota reset time.
|
|
@@ -2012,7 +2012,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2012
2012
|
readonly subcommands: readonly [];
|
|
2013
2013
|
}, {
|
|
2014
2014
|
readonly name: "usage";
|
|
2015
|
-
readonly description: "Show this month's remote database usage
|
|
2015
|
+
readonly description: "Show this month's remote database usage. With --app: included, used, overage, ceiling and the estimated charge for one app";
|
|
2016
2016
|
readonly aliases: readonly [];
|
|
2017
2017
|
readonly hidden: false;
|
|
2018
2018
|
readonly helpOption: true;
|
|
@@ -2024,6 +2024,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2024
2024
|
readonly flags: "--company <slug>";
|
|
2025
2025
|
readonly description: "Company slug";
|
|
2026
2026
|
readonly hidden: false;
|
|
2027
|
+
}, {
|
|
2028
|
+
readonly flags: "--app <name>";
|
|
2029
|
+
readonly description: "Show one app: included amounts, use, overage, ceiling and estimated charge";
|
|
2030
|
+
readonly hidden: false;
|
|
2027
2031
|
}];
|
|
2028
2032
|
readonly subcommands: readonly [];
|
|
2029
2033
|
}, {
|
|
@@ -2414,7 +2414,7 @@ export const COMMAND_CATALOG = [
|
|
|
2414
2414
|
},
|
|
2415
2415
|
{
|
|
2416
2416
|
"name": "usage",
|
|
2417
|
-
"description": "Show this month's remote database usage
|
|
2417
|
+
"description": "Show this month's remote database usage. With --app: included, used, overage, ceiling and the estimated charge for one app",
|
|
2418
2418
|
"aliases": [],
|
|
2419
2419
|
"hidden": false,
|
|
2420
2420
|
"helpOption": true,
|
|
@@ -2427,6 +2427,11 @@ export const COMMAND_CATALOG = [
|
|
|
2427
2427
|
"flags": "--company <slug>",
|
|
2428
2428
|
"description": "Company slug",
|
|
2429
2429
|
"hidden": false
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
"flags": "--app <name>",
|
|
2433
|
+
"description": "Show one app: included amounts, use, overage, ceiling and estimated charge",
|
|
2434
|
+
"hidden": false
|
|
2430
2435
|
}
|
|
2431
2436
|
],
|
|
2432
2437
|
"subcommands": []
|
|
@@ -54,6 +54,9 @@ export function describeRemoteError(error) {
|
|
|
54
54
|
if (status === 404 && code === "DB_USAGE_NOT_PROVISIONED") {
|
|
55
55
|
return "This company has no remote database yet. Run hq db provision first.";
|
|
56
56
|
}
|
|
57
|
+
if (status === 404 && code === "DB_USAGE_APP_NOT_FOUND") {
|
|
58
|
+
return "No database found for that app in this company. Check the app name, or deploy it with database enabled.";
|
|
59
|
+
}
|
|
57
60
|
return null;
|
|
58
61
|
}
|
|
59
62
|
//# sourceMappingURL=db-remote-deps.js.map
|
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
6
|
-
import { formatUsage } from "../lib/db/usage-format.js";
|
|
6
|
+
import { formatAppUsageResponse, formatUsage } from "../lib/db/usage-format.js";
|
|
7
7
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
8
|
import { controlPlaneClient, defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
9
9
|
export function registerDbUsageCommand(db, depsFactory = defaultRemoteDeps) {
|
|
10
10
|
db.command("usage")
|
|
11
|
-
.description("Show this month's remote database usage
|
|
11
|
+
.description("Show this month's remote database usage. With --app: included, used, overage, ceiling and the estimated charge for one app")
|
|
12
12
|
.requiredOption("--company <slug>", "Company slug")
|
|
13
|
+
.option("--app <name>", "Show one app: included amounts, use, overage, ceiling and estimated charge")
|
|
13
14
|
.action(async (opts) => {
|
|
14
15
|
try {
|
|
15
16
|
const deps = depsFactory();
|
|
16
17
|
const resolved = await deps.resolveCompany(String(opts.company ?? "").trim());
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const client = controlPlaneClient(deps);
|
|
19
|
+
const app = String(opts.app ?? "").trim();
|
|
20
|
+
if (app) {
|
|
21
|
+
console.log(formatAppUsageResponse(await client.appUsage(resolved.companyUid, app)));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(formatUsage(await client.usage(resolved.companyUid)));
|
|
25
|
+
}
|
|
19
26
|
}
|
|
20
27
|
catch (error) {
|
|
21
28
|
if (isPlanGateError(error))
|
|
@@ -24,6 +24,38 @@ export interface RemoteStatusResponse {
|
|
|
24
24
|
secretRef: string;
|
|
25
25
|
} | null;
|
|
26
26
|
}
|
|
27
|
+
export type RemoteAppLimitState = "ok" | "warn" | "overage" | "locked";
|
|
28
|
+
/** One app that owns its cluster. Cents are whole US cents; `estimate` is null for an exempt company. */
|
|
29
|
+
export interface RemoteAppUsage {
|
|
30
|
+
appId: string;
|
|
31
|
+
period: string;
|
|
32
|
+
measuredAt: string | null;
|
|
33
|
+
ended: boolean;
|
|
34
|
+
state: RemoteAppLimitState;
|
|
35
|
+
locked: boolean;
|
|
36
|
+
exempt: boolean;
|
|
37
|
+
dpu: {
|
|
38
|
+
used: number;
|
|
39
|
+
included: number;
|
|
40
|
+
overage: number;
|
|
41
|
+
ceiling: number;
|
|
42
|
+
pct: number;
|
|
43
|
+
};
|
|
44
|
+
storage: {
|
|
45
|
+
usedBytes: number;
|
|
46
|
+
billedBytes: number;
|
|
47
|
+
includedBytes: number;
|
|
48
|
+
overageGbMonth: number;
|
|
49
|
+
ceilingBytes: number;
|
|
50
|
+
pct: number;
|
|
51
|
+
};
|
|
52
|
+
estimate: {
|
|
53
|
+
baseCents: number;
|
|
54
|
+
dpuOverageCents: number;
|
|
55
|
+
storageOverageCents: number;
|
|
56
|
+
totalCents: number;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
27
59
|
export interface RemoteUsageResponse {
|
|
28
60
|
companyUid: string;
|
|
29
61
|
dpuUsed: number;
|
|
@@ -34,7 +66,20 @@ export interface RemoteUsageResponse {
|
|
|
34
66
|
pctStorage: number;
|
|
35
67
|
period: string;
|
|
36
68
|
measuredAt: string | null;
|
|
69
|
+
/** False when every app owns its own cluster and there is no shared company database. */
|
|
70
|
+
companyCluster?: boolean;
|
|
71
|
+
apps?: RemoteAppUsage[];
|
|
37
72
|
}
|
|
73
|
+
/** Answer for one app: its own numbers, or the company numbers when it shares the company cluster. */
|
|
74
|
+
export type RemoteAppUsageResponse = (RemoteAppUsage & {
|
|
75
|
+
companyUid: string;
|
|
76
|
+
mode: "app-cluster";
|
|
77
|
+
status?: string;
|
|
78
|
+
}) | (RemoteUsageResponse & {
|
|
79
|
+
appId: string;
|
|
80
|
+
mode: "company-cluster";
|
|
81
|
+
exempt?: boolean;
|
|
82
|
+
});
|
|
38
83
|
/**
|
|
39
84
|
* Scoped temporary AWS credentials for one app's database role. The control
|
|
40
85
|
* plane would issue these from `POST /v1/db/apps/{appId}/session`.
|
|
@@ -74,6 +119,7 @@ export declare class ControlPlaneDbClient {
|
|
|
74
119
|
region?: string;
|
|
75
120
|
}): Promise<RemoteProvisionResponse>;
|
|
76
121
|
status(companyUid: string): Promise<RemoteStatusResponse>;
|
|
122
|
+
appUsage(companyUid: string, appId: string): Promise<RemoteAppUsageResponse>;
|
|
77
123
|
usage(companyUid: string): Promise<RemoteUsageResponse>;
|
|
78
124
|
appSession(input: {
|
|
79
125
|
companyUid: string;
|
|
@@ -78,6 +78,20 @@ export class ControlPlaneDbClient {
|
|
|
78
78
|
}
|
|
79
79
|
return JSON.parse(text);
|
|
80
80
|
}
|
|
81
|
+
async appUsage(companyUid, appId) {
|
|
82
|
+
const token = await this.getAccessToken();
|
|
83
|
+
const query = `companyUid=${encodeURIComponent(companyUid)}&appId=${encodeURIComponent(appId)}`;
|
|
84
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/db/usage?${query}`, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
87
|
+
signal: AbortSignal.timeout(DB_STATUS_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
assertNoPostgresUrl("usage", text);
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
throw httpError("usage", res.status, text);
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
}
|
|
81
95
|
async usage(companyUid) {
|
|
82
96
|
const token = await this.getAccessToken();
|
|
83
97
|
const url = `${this.baseUrl}/v1/db/usage?companyUid=${encodeURIComponent(companyUid)}`;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { RemoteUsageResponse } from "./control-plane.js";
|
|
1
|
+
import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteUsageResponse } from "./control-plane.js";
|
|
2
2
|
export declare function formatBytes(bytes: number): string;
|
|
3
3
|
export declare function formatUsage(usage: RemoteUsageResponse): string;
|
|
4
|
+
export declare function formatAppUsage(usage: RemoteAppUsage): string;
|
|
5
|
+
export declare function formatAppUsageResponse(response: RemoteAppUsageResponse): string;
|
|
6
|
+
export declare function formatCompanyApps(apps: RemoteAppUsage[]): string;
|
|
4
7
|
//# sourceMappingURL=usage-format.d.ts.map
|
|
@@ -36,6 +36,72 @@ export function formatUsage(usage) {
|
|
|
36
36
|
lines.push(`Over the cap for ${over.join(" and ")}.`);
|
|
37
37
|
else if (usage.pctDpu >= 80 || usage.pctStorage >= 80)
|
|
38
38
|
lines.push("Close to a cap (80% or more).");
|
|
39
|
+
if (usage.companyCluster === false) {
|
|
40
|
+
lines.splice(1, 2, "This company has no shared database. Each app has its own, listed below.");
|
|
41
|
+
}
|
|
42
|
+
const apps = formatCompanyApps(usage.apps ?? []);
|
|
43
|
+
if (apps)
|
|
44
|
+
lines.push(apps);
|
|
39
45
|
return lines.join("\n");
|
|
40
46
|
}
|
|
47
|
+
function dollars(cents) {
|
|
48
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
49
|
+
}
|
|
50
|
+
function count(value) {
|
|
51
|
+
return Math.round(value).toLocaleString("en-US");
|
|
52
|
+
}
|
|
53
|
+
function gbMonth(value) {
|
|
54
|
+
return `${value.toFixed(2)} GB-month`;
|
|
55
|
+
}
|
|
56
|
+
const STATE_LINES = {
|
|
57
|
+
ok: "Within the included amounts.",
|
|
58
|
+
warn: "Close to the included amount (80% or more). Use above it is billed as overage.",
|
|
59
|
+
overage: "Above the included amount. The extra use is billed as overage.",
|
|
60
|
+
locked: "At the ceiling. The database is read-only: reads work, writes are refused. Move this app to a larger service, or contact HQ support to raise the limit.",
|
|
61
|
+
};
|
|
62
|
+
export function formatAppUsage(usage) {
|
|
63
|
+
const lines = [
|
|
64
|
+
`Database usage for app ${usage.appId}, ${usage.period}`,
|
|
65
|
+
`Compute: ${count(usage.dpu.used)} of ${count(usage.dpu.included)} included units (${percent(usage.dpu.pct)}), overage ${count(usage.dpu.overage)}, ceiling ${count(usage.dpu.ceiling)}`,
|
|
66
|
+
`Storage: ${formatBytes(usage.storage.usedBytes)} of ${formatBytes(usage.storage.includedBytes)} included (${percent(usage.storage.pct)}), month average ${formatBytes(usage.storage.billedBytes)}, overage ${gbMonth(usage.storage.overageGbMonth)}, ceiling ${formatBytes(usage.storage.ceilingBytes)}`,
|
|
67
|
+
STATE_LINES[usage.locked ? "locked" : usage.state],
|
|
68
|
+
];
|
|
69
|
+
if (usage.estimate) {
|
|
70
|
+
const e = usage.estimate;
|
|
71
|
+
lines.push(`Estimated charge this month: ${dollars(e.totalCents)} (${dollars(e.baseCents)} base, ${dollars(e.dpuOverageCents)} compute overage, ${dollars(e.storageOverageCents)} storage overage)`);
|
|
72
|
+
}
|
|
73
|
+
else if (usage.exempt) {
|
|
74
|
+
lines.push("Not billed: this company is exempt from database app charges.");
|
|
75
|
+
}
|
|
76
|
+
if (usage.ended)
|
|
77
|
+
lines.push("This app was deleted. Usage up to deletion still counts for the month.");
|
|
78
|
+
lines.push(usage.measuredAt === null
|
|
79
|
+
? "Not measured yet, so usage shows as 0. Check again in about 15 minutes."
|
|
80
|
+
: `Last measured ${usage.measuredAt}. Numbers can lag by a few minutes.`);
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
export function formatAppUsageResponse(response) {
|
|
84
|
+
if (response.mode === "company-cluster") {
|
|
85
|
+
return [
|
|
86
|
+
`App ${response.appId} uses the shared company database, so these are the company numbers.`,
|
|
87
|
+
formatUsage(response),
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
return formatAppUsage(response);
|
|
91
|
+
}
|
|
92
|
+
export function formatCompanyApps(apps) {
|
|
93
|
+
if (apps.length === 0)
|
|
94
|
+
return "";
|
|
95
|
+
const rows = apps.map((app) => {
|
|
96
|
+
const note = app.locked
|
|
97
|
+
? ", read-only"
|
|
98
|
+
: app.state === "warn"
|
|
99
|
+
? ", close to the included amount"
|
|
100
|
+
: app.state === "overage"
|
|
101
|
+
? ", over the included amount"
|
|
102
|
+
: "";
|
|
103
|
+
return ` ${app.appId}: ${count(app.dpu.used)} of ${count(app.dpu.included)} compute (${percent(app.dpu.pct)}), ${formatBytes(app.storage.usedBytes)} of ${formatBytes(app.storage.includedBytes)} storage (${percent(app.storage.pct)})${note}${app.ended ? ", deleted" : ""}`;
|
|
104
|
+
});
|
|
105
|
+
return ["Apps with their own database:", ...rows, "Run hq db usage --app <name> for one app."].join("\n");
|
|
106
|
+
}
|
|
41
107
|
//# sourceMappingURL=usage-format.js.map
|