@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.
@@ -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
- export declare function create(): Promise<void>;
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
- export async function create() {
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 = { name, region };
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
- const VERSION = "0.1.4";
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 (interactive)`);
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;
@@ -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.1.16",
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": "node --test \"test/*.test.mjs\"",
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": {