@batadata/cli 0.1.16 → 0.2.0
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/README.md +42 -0
- package/dist/commands/compute.d.ts +37 -0
- package/dist/commands/compute.js +347 -0
- package/dist/commands/import.d.ts +178 -0
- package/dist/commands/import.js +1232 -0
- package/dist/commands/projects.d.ts +17 -1
- package/dist/commands/projects.js +82 -3
- package/dist/index.js +23 -2
- package/dist/pricing.d.ts +35 -0
- package/dist/pricing.js +44 -0
- package/package.json +4 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComputeTier } from "../pricing.js";
|
|
1
2
|
/**
|
|
2
3
|
* Only surface `defaultProject` if it's actually one of the caller's real
|
|
3
4
|
* projects — a saved default from another team/key/stale config must never
|
|
@@ -7,7 +8,22 @@ export declare function validDefaultProject(projects: Array<{
|
|
|
7
8
|
id: string;
|
|
8
9
|
}>, defaultProject: string | undefined): string | null;
|
|
9
10
|
export declare function list(): Promise<void>;
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Parse the compute tier/size flags for `projects create`:
|
|
13
|
+
* --tier serverless|always-on (default: serverless) also --tier=…
|
|
14
|
+
* --size 1|2|4|8|16|32|64|128 (default: 1; >16 = serverless only) also --size=…
|
|
15
|
+
*
|
|
16
|
+
* Returns the API tier value (`always_on` / `serverless`) and the size, or an
|
|
17
|
+
* `error` describing the first invalid flag so the caller can exit non-zero.
|
|
18
|
+
* `always-on` (hyphen, the CLI spelling) maps to the API's `always_on`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseCreateComputeArgs(args: string[]): {
|
|
21
|
+
tier: ComputeTier;
|
|
22
|
+
sizeCu: number;
|
|
23
|
+
} | {
|
|
24
|
+
error: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function create(args?: string[]): Promise<void>;
|
|
11
27
|
export declare function info(projectId?: string): Promise<void>;
|
|
12
28
|
export declare function deleteProject(projectId?: string): Promise<void>;
|
|
13
29
|
/**
|
|
@@ -4,6 +4,7 @@ import { colors, log, json, success, spinner, table, kvList, heading } from "../
|
|
|
4
4
|
import { prompt, confirmDestructive, select } from "../utils/prompts.js";
|
|
5
5
|
import { emitError } from "../utils/errors.js";
|
|
6
6
|
import { resolveProjectId } from "../link.js";
|
|
7
|
+
import { computePriceSummary, ALL_SIZE_CU, FLY_MAX_SIZE_CU } from "../pricing.js";
|
|
7
8
|
function projectCreatedAt(p) {
|
|
8
9
|
return p.created_at ?? p.createdAt ?? "";
|
|
9
10
|
}
|
|
@@ -95,9 +96,78 @@ export async function list() {
|
|
|
95
96
|
]));
|
|
96
97
|
log();
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Parse the compute tier/size flags for `projects create`:
|
|
101
|
+
* --tier serverless|always-on (default: serverless) also --tier=…
|
|
102
|
+
* --size 1|2|4|8|16|32|64|128 (default: 1; >16 = serverless only) also --size=…
|
|
103
|
+
*
|
|
104
|
+
* Returns the API tier value (`always_on` / `serverless`) and the size, or an
|
|
105
|
+
* `error` describing the first invalid flag so the caller can exit non-zero.
|
|
106
|
+
* `always-on` (hyphen, the CLI spelling) maps to the API's `always_on`.
|
|
107
|
+
*/
|
|
108
|
+
export function parseCreateComputeArgs(args) {
|
|
109
|
+
let tierRaw;
|
|
110
|
+
let sizeRaw;
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
if (arg === "--tier" || arg === "--size") {
|
|
114
|
+
// The spaced form needs a value: reject a missing one (end of args, or
|
|
115
|
+
// the next token is another flag) instead of silently using the default.
|
|
116
|
+
const next = args[i + 1];
|
|
117
|
+
if (next === undefined || next.startsWith("-")) {
|
|
118
|
+
const usage = arg === "--tier" ? "serverless|always-on" : "1|2|4|8|16|32|64|128";
|
|
119
|
+
return { error: `${arg} requires a value (${usage}).` };
|
|
120
|
+
}
|
|
121
|
+
if (arg === "--tier")
|
|
122
|
+
tierRaw = next;
|
|
123
|
+
else
|
|
124
|
+
sizeRaw = next;
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
else if (arg.startsWith("--tier="))
|
|
128
|
+
tierRaw = arg.slice("--tier=".length);
|
|
129
|
+
else if (arg.startsWith("--size="))
|
|
130
|
+
sizeRaw = arg.slice("--size=".length);
|
|
131
|
+
}
|
|
132
|
+
let tier = "serverless";
|
|
133
|
+
if (tierRaw !== undefined) {
|
|
134
|
+
const normalized = tierRaw.toLowerCase();
|
|
135
|
+
if (normalized === "serverless")
|
|
136
|
+
tier = "serverless";
|
|
137
|
+
else if (normalized === "always-on" || normalized === "always_on")
|
|
138
|
+
tier = "always_on";
|
|
139
|
+
else {
|
|
140
|
+
return {
|
|
141
|
+
error: `Invalid --tier "${tierRaw}". Use "serverless" or "always-on".`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let sizeCu = 1;
|
|
146
|
+
if (sizeRaw !== undefined) {
|
|
147
|
+
const parsed = Number(sizeRaw);
|
|
148
|
+
if (!ALL_SIZE_CU.includes(parsed)) {
|
|
149
|
+
return {
|
|
150
|
+
error: `Invalid --size "${sizeRaw}". Use one of ${ALL_SIZE_CU.join(", ")} CU (sizes above ${FLY_MAX_SIZE_CU} are serverless/density only).`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Density-only sizes (>16 CU) can't run always-on (always-on is Fly-hosted).
|
|
154
|
+
if (parsed > FLY_MAX_SIZE_CU && tier === "always_on") {
|
|
155
|
+
return {
|
|
156
|
+
error: `--size ${parsed} CU is serverless-only (density-hosted); it can't be used with --tier always-on.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
sizeCu = parsed;
|
|
160
|
+
}
|
|
161
|
+
return { tier, sizeCu };
|
|
162
|
+
}
|
|
163
|
+
export async function create(args = []) {
|
|
99
164
|
const token = requireToken();
|
|
100
165
|
const config = loadConfig();
|
|
166
|
+
const compute = parseCreateComputeArgs(args);
|
|
167
|
+
if ("error" in compute) {
|
|
168
|
+
emitError("INVALID_FLAG", compute.error, "");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
101
171
|
heading("Create a new project");
|
|
102
172
|
const name = await prompt("Project name");
|
|
103
173
|
if (!name) {
|
|
@@ -106,7 +176,13 @@ export async function create() {
|
|
|
106
176
|
const region = await select("Select a region", REGIONS);
|
|
107
177
|
const s = spinner("Creating project");
|
|
108
178
|
const teamId = await resolveTeamId(token);
|
|
109
|
-
const body = {
|
|
179
|
+
const body = {
|
|
180
|
+
name,
|
|
181
|
+
region,
|
|
182
|
+
// One friendly step: the compute is born on the chosen tier + size (no
|
|
183
|
+
// follow-up `bata compute set` needed). Defaults to serverless / 1 CU.
|
|
184
|
+
compute: { tier: compute.tier, size_cu: compute.sizeCu },
|
|
185
|
+
};
|
|
110
186
|
if (teamId) {
|
|
111
187
|
body.team_id = teamId;
|
|
112
188
|
}
|
|
@@ -121,12 +197,15 @@ export async function create() {
|
|
|
121
197
|
const project = res.data;
|
|
122
198
|
// Set as default project
|
|
123
199
|
saveConfig({ defaultProject: project.id });
|
|
200
|
+
const tierLabel = compute.tier === "always_on" ? "Always-on (dedicated)" : "Serverless";
|
|
124
201
|
log();
|
|
125
202
|
success(`Project ${colors.cyan(project.name)} created`);
|
|
126
203
|
log();
|
|
127
204
|
kvList([
|
|
128
205
|
["ID", colors.dim(project.id)],
|
|
129
206
|
["Region", project.region],
|
|
207
|
+
["Compute", `${tierLabel} · ${compute.sizeCu} CU`],
|
|
208
|
+
["Pricing", colors.dim(computePriceSummary(compute.tier, compute.sizeCu))],
|
|
130
209
|
["Status", statusBadge(project.status)],
|
|
131
210
|
]);
|
|
132
211
|
log();
|
|
@@ -272,7 +351,7 @@ export async function handleProjects(args) {
|
|
|
272
351
|
const sub = args[0];
|
|
273
352
|
switch (sub) {
|
|
274
353
|
case "create":
|
|
275
|
-
return create();
|
|
354
|
+
return create(args.slice(1));
|
|
276
355
|
case "info":
|
|
277
356
|
return info(resolveProjectArg(args.slice(1)));
|
|
278
357
|
case "delete":
|
package/dist/index.js
CHANGED
|
@@ -13,14 +13,20 @@ import { claim } from "./commands/claim.js";
|
|
|
13
13
|
import { status } from "./commands/status.js";
|
|
14
14
|
import { connect } from "./commands/connect.js";
|
|
15
15
|
import { usage } from "./commands/usage.js";
|
|
16
|
+
import { handleCompute } from "./commands/compute.js";
|
|
16
17
|
import { handleRestore } from "./commands/restore.js";
|
|
17
18
|
import { handlePowdb } from "./commands/powdb.js";
|
|
19
|
+
import { importDb } from "./commands/import.js";
|
|
18
20
|
import { link, unlink } from "./commands/link.js";
|
|
19
21
|
import { parseGlobalFlags } from "./args.js";
|
|
20
22
|
import { isJsonMode } from "./config.js";
|
|
21
23
|
import { colors, log, banner } from "./utils/logger.js";
|
|
22
24
|
import { exitCodeFor, isRetryable } from "./utils/errors.js";
|
|
23
|
-
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
// Read the version from package.json (dist/index.js → ../package.json) instead
|
|
27
|
+
// of a hardcoded constant — the constant sat at "0.1.4" while releases shipped
|
|
28
|
+
// through 0.1.19, so every published CLI misreported `--version`.
|
|
29
|
+
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
24
30
|
function help() {
|
|
25
31
|
banner();
|
|
26
32
|
log(` ${colors.bold("Usage")}`);
|
|
@@ -45,7 +51,7 @@ function help() {
|
|
|
45
51
|
log(` ${colors.bold("Projects")}`);
|
|
46
52
|
log(` ${colors.cyan("projects")} List all projects`);
|
|
47
53
|
log(` ${colors.cyan("projects list")} Alias for status`);
|
|
48
|
-
log(` ${colors.cyan("projects create")} Create a new project (
|
|
54
|
+
log(` ${colors.cyan("projects create")} Create a new project ${colors.dim("(--tier serverless|always-on, --size 1|2|4|8|16|32|64|128)")}`);
|
|
49
55
|
log(` ${colors.cyan("projects info")} Show project details`);
|
|
50
56
|
log(` ${colors.cyan("projects delete")} Delete a project`);
|
|
51
57
|
log();
|
|
@@ -59,6 +65,10 @@ function help() {
|
|
|
59
65
|
log(` ${colors.cyan("db studio")} Open table browser in browser`);
|
|
60
66
|
log(` ${colors.cyan("db query")} Run a SQL query ${colors.dim("(--branch <id> to target a branch)")}`);
|
|
61
67
|
log();
|
|
68
|
+
log(` ${colors.bold("Compute")}`);
|
|
69
|
+
log(` ${colors.cyan("compute status")} Show each branch's compute size + always-on state`);
|
|
70
|
+
log(` ${colors.cyan("compute set")} Dedicated always-on tier / fixed size ${colors.dim("(--always-on, --size <cu>)")}`);
|
|
71
|
+
log();
|
|
62
72
|
log(` ${colors.bold("Restore (PITR)")}`);
|
|
63
73
|
log(` ${colors.cyan("restore points")} List recovery points + the PITR window`);
|
|
64
74
|
log(` ${colors.cyan("restore create")} Restore a branch to a timestamp/LSN ${colors.dim("(creates a NEW branch)")}`);
|
|
@@ -69,6 +79,9 @@ function help() {
|
|
|
69
79
|
log(` ${colors.cyan("schema check|dump|diff")} Schema safety gate + introspection`);
|
|
70
80
|
log(` ${colors.cyan("migrate check")} Gate a migration against live query traffic ${colors.dim("(exit 2 if breaking)")}`);
|
|
71
81
|
log();
|
|
82
|
+
log(` ${colors.bold("Migrate in (Neon → BataDB)")}`);
|
|
83
|
+
log(` ${colors.cyan("import")} Migrate a Postgres/Neon DB into BataDB ${colors.dim("(--source <uri> [--name|--project])")}`);
|
|
84
|
+
log();
|
|
72
85
|
log(` ${colors.bold("PowDB (embedded lane, Stage A)")}`);
|
|
73
86
|
log(` ${colors.cyan("powdb pull")} Pull a branch's schema + data into a local PowDB-loadable PowQL script`);
|
|
74
87
|
log();
|
|
@@ -179,6 +192,10 @@ async function main() {
|
|
|
179
192
|
case "db":
|
|
180
193
|
await handleDb(rest);
|
|
181
194
|
break;
|
|
195
|
+
// Compute (dedicated always-on tier + size picker)
|
|
196
|
+
case "compute":
|
|
197
|
+
await handleCompute(rest);
|
|
198
|
+
break;
|
|
182
199
|
// Restore (PITR)
|
|
183
200
|
case "restore":
|
|
184
201
|
await handleRestore(rest);
|
|
@@ -199,6 +216,10 @@ async function main() {
|
|
|
199
216
|
case "migrate":
|
|
200
217
|
await handleMigrate(rest);
|
|
201
218
|
break;
|
|
219
|
+
// Import (Neon → BataDB)
|
|
220
|
+
case "import":
|
|
221
|
+
await importDb(rest);
|
|
222
|
+
break;
|
|
202
223
|
// Dev
|
|
203
224
|
case "dev":
|
|
204
225
|
await dev();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer-facing compute pricing for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* SOURCE OF TRUTH: control-plane/src/lib/pricing.ts (`computePerCuHourCents`).
|
|
5
|
+
* The CLI is a zero-dependency package that can't import across workspaces, so
|
|
6
|
+
* the rate is re-declared here. If the canonical rate changes, update this too.
|
|
7
|
+
*
|
|
8
|
+
* COST-TRUTH RULE: every displayed price is DERIVED from this one rate — never a
|
|
9
|
+
* hardcoded dollar figure — so the CLI can only ever quote a price that billing
|
|
10
|
+
* actually produces. Serverless copy deliberately makes no wake-latency promise.
|
|
11
|
+
*/
|
|
12
|
+
/** Cents per CU-hour of compute. Mirrors pricing.ts `computePerCuHourCents`. */
|
|
13
|
+
export declare const COMPUTE_PER_CU_HOUR_CENTS = 1;
|
|
14
|
+
/** Standard billing month: 365d × 24h ÷ 12. An always-on primary runs all of it. */
|
|
15
|
+
export declare const HOURS_PER_MONTH = 730;
|
|
16
|
+
/** Fly-capable fixed sizes (any tier). 1 CU = 512MB / 0.25 vCPU → {1,2,4,8,16} = 512MB→8GB. */
|
|
17
|
+
export declare const DEDICATED_SIZE_CU: readonly [1, 2, 4, 8, 16];
|
|
18
|
+
/** Density-only sizes (16/32/64GB) — serverless computes on a density host only.
|
|
19
|
+
* Mirrors DENSITY_ONLY_SIZE_CU in control-plane/src/lib/compute-sizes.ts. */
|
|
20
|
+
export declare const DENSITY_ONLY_SIZE_CU: readonly [32, 64, 128];
|
|
21
|
+
/** Every selectable size (Fly-capable + density-only). */
|
|
22
|
+
export declare const ALL_SIZE_CU: readonly [1, 2, 4, 8, 16, 32, 64, 128];
|
|
23
|
+
/** Largest size a Fly-hosted compute may run; above this needs serverless/density. */
|
|
24
|
+
export declare const FLY_MAX_SIZE_CU = 16;
|
|
25
|
+
export type ComputeTier = "serverless" | "always_on";
|
|
26
|
+
/** The base compute rate as a dollar string, e.g. "$0.01". */
|
|
27
|
+
export declare function computePerCuHourDollars(): string;
|
|
28
|
+
/** Flat monthly cost of an always-on `sizeCu` compute (730h), e.g. 4 → "$29.20". */
|
|
29
|
+
export declare function alwaysOnMonthlyDollars(sizeCu: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* One-line price implication for the create confirmation. Always-on quotes the
|
|
32
|
+
* flat monthly bill (with the arithmetic shown); serverless states the usage
|
|
33
|
+
* rate and scale-to-zero — with no wake-latency claim.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computePriceSummary(tier: ComputeTier, sizeCu: number): string;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer-facing compute pricing for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* SOURCE OF TRUTH: control-plane/src/lib/pricing.ts (`computePerCuHourCents`).
|
|
5
|
+
* The CLI is a zero-dependency package that can't import across workspaces, so
|
|
6
|
+
* the rate is re-declared here. If the canonical rate changes, update this too.
|
|
7
|
+
*
|
|
8
|
+
* COST-TRUTH RULE: every displayed price is DERIVED from this one rate — never a
|
|
9
|
+
* hardcoded dollar figure — so the CLI can only ever quote a price that billing
|
|
10
|
+
* actually produces. Serverless copy deliberately makes no wake-latency promise.
|
|
11
|
+
*/
|
|
12
|
+
/** Cents per CU-hour of compute. Mirrors pricing.ts `computePerCuHourCents`. */
|
|
13
|
+
export const COMPUTE_PER_CU_HOUR_CENTS = 1; // $0.01 / CU-hour
|
|
14
|
+
/** Standard billing month: 365d × 24h ÷ 12. An always-on primary runs all of it. */
|
|
15
|
+
export const HOURS_PER_MONTH = 730;
|
|
16
|
+
/** Fly-capable fixed sizes (any tier). 1 CU = 512MB / 0.25 vCPU → {1,2,4,8,16} = 512MB→8GB. */
|
|
17
|
+
export const DEDICATED_SIZE_CU = [1, 2, 4, 8, 16];
|
|
18
|
+
/** Density-only sizes (16/32/64GB) — serverless computes on a density host only.
|
|
19
|
+
* Mirrors DENSITY_ONLY_SIZE_CU in control-plane/src/lib/compute-sizes.ts. */
|
|
20
|
+
export const DENSITY_ONLY_SIZE_CU = [32, 64, 128];
|
|
21
|
+
/** Every selectable size (Fly-capable + density-only). */
|
|
22
|
+
export const ALL_SIZE_CU = [...DEDICATED_SIZE_CU, ...DENSITY_ONLY_SIZE_CU];
|
|
23
|
+
/** Largest size a Fly-hosted compute may run; above this needs serverless/density. */
|
|
24
|
+
export const FLY_MAX_SIZE_CU = 16;
|
|
25
|
+
/** The base compute rate as a dollar string, e.g. "$0.01". */
|
|
26
|
+
export function computePerCuHourDollars() {
|
|
27
|
+
return `$${(COMPUTE_PER_CU_HOUR_CENTS / 100).toFixed(2)}`;
|
|
28
|
+
}
|
|
29
|
+
/** Flat monthly cost of an always-on `sizeCu` compute (730h), e.g. 4 → "$29.20". */
|
|
30
|
+
export function alwaysOnMonthlyDollars(sizeCu) {
|
|
31
|
+
const dollars = (sizeCu * HOURS_PER_MONTH * COMPUTE_PER_CU_HOUR_CENTS) / 100;
|
|
32
|
+
return `$${dollars.toFixed(2)}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One-line price implication for the create confirmation. Always-on quotes the
|
|
36
|
+
* flat monthly bill (with the arithmetic shown); serverless states the usage
|
|
37
|
+
* rate and scale-to-zero — with no wake-latency claim.
|
|
38
|
+
*/
|
|
39
|
+
export function computePriceSummary(tier, sizeCu) {
|
|
40
|
+
if (tier === "always_on") {
|
|
41
|
+
return `fixed ~${alwaysOnMonthlyDollars(sizeCu)}/mo (${sizeCu} CU × ${computePerCuHourDollars()}/CU-hr × ${HOURS_PER_MONTH}h)`;
|
|
42
|
+
}
|
|
43
|
+
return `usage-billed at ${computePerCuHourDollars()}/CU-hr while awake, scale-to-zero when idle`;
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@batadata/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for BataDB — serverless Postgres platform",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bata": "./dist/index.js"
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"build": "tsc -p tsconfig.build.json",
|
|
11
11
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "npm run test:node && npm run test:vitest",
|
|
14
|
+
"test:node": "node --test \"test/*.test.mjs\"",
|
|
15
|
+
"test:vitest": "vitest run",
|
|
14
16
|
"pretest": "npm run build"
|
|
15
17
|
},
|
|
16
18
|
"engines": {
|