@batadata/cli 0.2.4 → 0.2.5
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/dist/commands/powdb.js +116 -2
- package/dist/commands/projects.d.ts +5 -0
- package/dist/commands/projects.js +141 -20
- package/package.json +1 -1
package/dist/commands/powdb.js
CHANGED
|
@@ -652,6 +652,106 @@ function renderPullSummary(o) {
|
|
|
652
652
|
log(` ${colors.dim("Load it:")} ${colors.cyan(o.loadCommand)}`);
|
|
653
653
|
log();
|
|
654
654
|
}
|
|
655
|
+
/** `--project <id>` / `--project=<id>` only (the positional arg is the query). */
|
|
656
|
+
function powdbProjectFlag(args) {
|
|
657
|
+
for (let i = 0; i < args.length; i++) {
|
|
658
|
+
if (args[i] === "--project" && args[i + 1])
|
|
659
|
+
return args[i + 1];
|
|
660
|
+
if (args[i].startsWith("--project="))
|
|
661
|
+
return args[i].slice("--project=".length);
|
|
662
|
+
}
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
function resolvePowdbProject(args) {
|
|
666
|
+
const explicit = powdbProjectFlag(args);
|
|
667
|
+
if (explicit)
|
|
668
|
+
return explicit;
|
|
669
|
+
const { projectId } = resolveProjectId(undefined);
|
|
670
|
+
if (!projectId) {
|
|
671
|
+
emitError("MISSING_ARG", "No project specified.", "Pass --project <id>, run `bata link <project>`, or set a default project.");
|
|
672
|
+
}
|
|
673
|
+
return projectId;
|
|
674
|
+
}
|
|
675
|
+
async function powdbQuery(args) {
|
|
676
|
+
const token = requireToken();
|
|
677
|
+
const jsonMode = isJsonMode();
|
|
678
|
+
const query = args.find((a) => !a.startsWith("-") && a !== powdbProjectFlag(args));
|
|
679
|
+
if (!query) {
|
|
680
|
+
emitError("MISSING_ARG", "Usage: bata powdb query \"<PowQL>\" [--project <id>]", "");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const projectId = resolvePowdbProject(args);
|
|
684
|
+
const res = await api.post(`/v1/powdb/${encodeURIComponent(projectId)}/query`, { query }, token);
|
|
685
|
+
if (!res.ok) {
|
|
686
|
+
const code = res.status === 503 ? "RETRYABLE"
|
|
687
|
+
: res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
688
|
+
: res.status === 404 ? "NOT_FOUND"
|
|
689
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
690
|
+
: "CLI_ERROR";
|
|
691
|
+
emitError(code, apiError(res, "PowQL query failed"), code === "RETRYABLE" ? "The database is starting — retry in a few seconds." : "");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const data = res.data;
|
|
695
|
+
if (jsonMode) {
|
|
696
|
+
json(data);
|
|
697
|
+
if ("error" in data && data.error)
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if ("error" in data && data.error) {
|
|
702
|
+
// Query executed, PowQL rejected it — surface verbatim, non-zero exit.
|
|
703
|
+
emitError("QUERY_ERROR", data.error, "");
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
log();
|
|
707
|
+
if ("kind" in data) {
|
|
708
|
+
switch (data.kind) {
|
|
709
|
+
case "rows":
|
|
710
|
+
if (data.rows.length === 0)
|
|
711
|
+
log(` ${colors.dim("(0 rows)")}`);
|
|
712
|
+
else {
|
|
713
|
+
table(data.columns, data.rows);
|
|
714
|
+
log(` ${colors.dim(`(${data.rows.length} row${data.rows.length === 1 ? "" : "s"})`)}`);
|
|
715
|
+
}
|
|
716
|
+
break;
|
|
717
|
+
case "scalar":
|
|
718
|
+
log(` ${data.value}`);
|
|
719
|
+
break;
|
|
720
|
+
case "ok":
|
|
721
|
+
log(` ${colors.green(">")} ${data.affected} row${data.affected === 1 ? "" : "s"} affected`);
|
|
722
|
+
break;
|
|
723
|
+
case "message":
|
|
724
|
+
log(` ${data.message}`);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
if (data.duration_ms !== undefined)
|
|
728
|
+
log(` ${colors.dim(`${data.duration_ms}ms`)}`);
|
|
729
|
+
}
|
|
730
|
+
log();
|
|
731
|
+
}
|
|
732
|
+
async function powdbLifecycle(action, args) {
|
|
733
|
+
const token = requireToken();
|
|
734
|
+
const jsonMode = isJsonMode();
|
|
735
|
+
const positional = args.find((a) => !a.startsWith("-"));
|
|
736
|
+
const projectId = powdbProjectFlag(args) ?? positional ?? resolvePowdbProject(args);
|
|
737
|
+
const res = action === "status"
|
|
738
|
+
? await api.get(`/v1/powdb/${encodeURIComponent(projectId)}`, token)
|
|
739
|
+
: await api.post(`/v1/powdb/${encodeURIComponent(projectId)}/${action}`, {}, token);
|
|
740
|
+
if (!res.ok) {
|
|
741
|
+
emitError(res.status === 404 ? "NOT_FOUND"
|
|
742
|
+
: res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
743
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
744
|
+
: "CLI_ERROR", apiError(res, `PowDB ${action} failed`), "");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (jsonMode) {
|
|
748
|
+
json(res.data);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
log();
|
|
752
|
+
log(` ${colors.green(">")} ${colors.cyan(projectId)} ${colors.dim("engine=powdb state=")}${res.data.state}`);
|
|
753
|
+
log();
|
|
754
|
+
}
|
|
655
755
|
// ─── Help + dispatch ──────────────────────────────────────────────────────────
|
|
656
756
|
function powdbHelp() {
|
|
657
757
|
log();
|
|
@@ -662,7 +762,13 @@ function powdbHelp() {
|
|
|
662
762
|
log(` ${colors.dim("BataDB branching / metering / server insights do NOT apply to it.")}`);
|
|
663
763
|
log();
|
|
664
764
|
log(` ${colors.dim("Commands:")}`);
|
|
665
|
-
log(` ${colors.cyan("pull")} Pull a branch's schema + data into a local PowDB-loadable PowQL script`);
|
|
765
|
+
log(` ${colors.cyan("pull")} Pull a Postgres branch's schema + data into a local PowDB-loadable PowQL script`);
|
|
766
|
+
log();
|
|
767
|
+
log(` ${colors.dim("Hosted PowDB projects (create one with: bata projects create --engine powdb):")}`);
|
|
768
|
+
log(` ${colors.cyan("query")} Run a PowQL statement: bata powdb query "Note" [--project <id>]`);
|
|
769
|
+
log(` ${colors.cyan("status")} Show the server state (running | parked) [--project <id>]`);
|
|
770
|
+
log(` ${colors.cyan("park")} Scale the server to zero (WAL is durable) [--project <id>]`);
|
|
771
|
+
log(` ${colors.cyan("wake")} Start a parked server (~tens of ms) [--project <id>]`);
|
|
666
772
|
log();
|
|
667
773
|
log(` ${colors.dim("Options (pull):")}`);
|
|
668
774
|
log(` ${colors.dim("--project <id> override the linked/default project")}`);
|
|
@@ -688,10 +794,18 @@ export async function handlePowdb(args) {
|
|
|
688
794
|
switch (sub) {
|
|
689
795
|
case "pull":
|
|
690
796
|
return powdbPull(args.slice(1));
|
|
797
|
+
case "query":
|
|
798
|
+
return powdbQuery(args.slice(1));
|
|
799
|
+
case "status":
|
|
800
|
+
return powdbLifecycle("status", args.slice(1));
|
|
801
|
+
case "park":
|
|
802
|
+
return powdbLifecycle("park", args.slice(1));
|
|
803
|
+
case "wake":
|
|
804
|
+
return powdbLifecycle("wake", args.slice(1));
|
|
691
805
|
case undefined:
|
|
692
806
|
powdbHelp();
|
|
693
807
|
return;
|
|
694
808
|
default:
|
|
695
|
-
emitError("INVALID_FLAG", `Unknown subcommand: powdb ${sub}`, "Available: pull. Run `bata powdb --help`.");
|
|
809
|
+
emitError("INVALID_FLAG", `Unknown subcommand: powdb ${sub}`, "Available: pull, query, status, park, wake. Run `bata powdb --help`.");
|
|
696
810
|
}
|
|
697
811
|
}
|
|
@@ -23,6 +23,11 @@ export declare function parseCreateComputeArgs(args: string[]): {
|
|
|
23
23
|
} | {
|
|
24
24
|
error: string;
|
|
25
25
|
};
|
|
26
|
+
/** Parse `--engine postgres|powdb` (default: postgres). PowDB is a different
|
|
27
|
+
* ENGINE, not a different Postgres — the compute tier/size flags don't apply. */
|
|
28
|
+
export declare function parseEngineArg(args: string[]): "postgres" | "powdb" | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
26
31
|
export declare function create(args?: string[]): Promise<void>;
|
|
27
32
|
export declare function info(projectId?: string): Promise<void>;
|
|
28
33
|
export declare function deleteProject(projectId?: string): Promise<void>;
|
|
@@ -160,29 +160,122 @@ export function parseCreateComputeArgs(args) {
|
|
|
160
160
|
}
|
|
161
161
|
return { tier, sizeCu };
|
|
162
162
|
}
|
|
163
|
+
/** `--flag <value>` / `--flag=<value>` reader. Distinguishes "absent"
|
|
164
|
+
* (undefined) from "present but missing its value" ({ error }) so a headless
|
|
165
|
+
* caller's typo fails loudly instead of silently changing behavior. */
|
|
166
|
+
function readFlagValue(args, flag) {
|
|
167
|
+
for (let i = 0; i < args.length; i++) {
|
|
168
|
+
if (args[i] === flag) {
|
|
169
|
+
const next = args[i + 1];
|
|
170
|
+
if (next === undefined || next.startsWith("-")) {
|
|
171
|
+
return { error: `${flag} requires a value.` };
|
|
172
|
+
}
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
if (args[i].startsWith(`${flag}=`)) {
|
|
176
|
+
const v = args[i].slice(flag.length + 1);
|
|
177
|
+
if (!v)
|
|
178
|
+
return { error: `${flag} requires a value.` };
|
|
179
|
+
return v;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
/** Parse `--engine postgres|powdb` (default: postgres). PowDB is a different
|
|
185
|
+
* ENGINE, not a different Postgres — the compute tier/size flags don't apply. */
|
|
186
|
+
export function parseEngineArg(args) {
|
|
187
|
+
let raw;
|
|
188
|
+
for (let i = 0; i < args.length; i++) {
|
|
189
|
+
if (args[i] === "--engine") {
|
|
190
|
+
const next = args[i + 1];
|
|
191
|
+
if (next === undefined || next.startsWith("-")) {
|
|
192
|
+
return { error: "--engine requires a value (postgres|powdb)." };
|
|
193
|
+
}
|
|
194
|
+
raw = next;
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
else if (args[i].startsWith("--engine="))
|
|
198
|
+
raw = args[i].slice("--engine=".length);
|
|
199
|
+
}
|
|
200
|
+
if (raw === undefined)
|
|
201
|
+
return "postgres";
|
|
202
|
+
const normalized = raw.toLowerCase();
|
|
203
|
+
if (normalized === "postgres" || normalized === "powdb")
|
|
204
|
+
return normalized;
|
|
205
|
+
return { error: `Invalid --engine "${raw}". Use "postgres" (default) or "powdb".` };
|
|
206
|
+
}
|
|
163
207
|
export async function create(args = []) {
|
|
164
208
|
const token = requireToken();
|
|
165
209
|
const config = loadConfig();
|
|
210
|
+
const engine = parseEngineArg(args);
|
|
211
|
+
if (typeof engine !== "string") {
|
|
212
|
+
emitError("INVALID_FLAG", engine.error, "");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (engine === "powdb" && args.some((a) => a === "--tier" || a === "--size" || a.startsWith("--tier=") || a.startsWith("--size="))) {
|
|
216
|
+
emitError("INVALID_FLAG", "--tier/--size do not apply to --engine powdb (PowDB has one serverless shape in this phase).", "");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
166
219
|
const compute = parseCreateComputeArgs(args);
|
|
167
220
|
if ("error" in compute) {
|
|
168
221
|
emitError("INVALID_FLAG", compute.error, "");
|
|
169
222
|
return;
|
|
170
223
|
}
|
|
171
|
-
|
|
172
|
-
|
|
224
|
+
const jsonMode = isJsonMode();
|
|
225
|
+
if (!jsonMode)
|
|
226
|
+
heading(engine === "powdb" ? "Create a new PowDB project" : "Create a new project");
|
|
227
|
+
// Agent-native path: --name (+ optional --region) skips the prompts entirely.
|
|
228
|
+
// Piped stdin cannot drive TWO sequential prompts — each prompt() opens a
|
|
229
|
+
// fresh readline that swallows the remaining buffered input, so the second
|
|
230
|
+
// question never resolves and Node exits silently on the unsettled await.
|
|
231
|
+
// Flags are the contract for headless use; prompts are for humans on a TTY.
|
|
232
|
+
const nameFlag = readFlagValue(args, "--name");
|
|
233
|
+
if (typeof nameFlag === "object") {
|
|
234
|
+
emitError("INVALID_FLAG", nameFlag.error, "");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const regionFlag = readFlagValue(args, "--region");
|
|
238
|
+
if (typeof regionFlag === "object") {
|
|
239
|
+
emitError("INVALID_FLAG", regionFlag.error, "");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (jsonMode && nameFlag === undefined) {
|
|
243
|
+
// JSON mode is the machine contract — prompts would pollute stdout and a
|
|
244
|
+
// headless caller can't answer them anyway.
|
|
245
|
+
emitError("MISSING_ARG", "--json requires --name <name>.", "");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const name = nameFlag ?? (await prompt("Project name"));
|
|
173
249
|
if (!name) {
|
|
174
|
-
emitError("MISSING_ARG", "Project name is required.", "");
|
|
250
|
+
emitError("MISSING_ARG", "Project name is required.", "Pass --name <name> for headless use.");
|
|
251
|
+
}
|
|
252
|
+
let region;
|
|
253
|
+
if (regionFlag !== undefined) {
|
|
254
|
+
if (!REGIONS.some((r) => r.value === regionFlag)) {
|
|
255
|
+
emitError("INVALID_FLAG", `Invalid --region "${regionFlag}". Use one of: ${REGIONS.map((r) => r.value).join(", ")}.`, "");
|
|
256
|
+
}
|
|
257
|
+
region = regionFlag;
|
|
258
|
+
}
|
|
259
|
+
else if (nameFlag !== undefined) {
|
|
260
|
+
// --name without --region: default to the first region rather than opening
|
|
261
|
+
// a prompt a headless caller can't answer.
|
|
262
|
+
region = REGIONS[0].value;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
region = await select("Select a region", REGIONS);
|
|
175
266
|
}
|
|
176
|
-
const region = await select("Select a region", REGIONS);
|
|
177
267
|
const s = spinner("Creating project");
|
|
178
268
|
const teamId = await resolveTeamId(token);
|
|
179
|
-
const body =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
const body = engine === "powdb"
|
|
270
|
+
? // PowDB: no compute picker — the server rejects pg-only options.
|
|
271
|
+
{ name, region, engine: "powdb" }
|
|
272
|
+
: {
|
|
273
|
+
name,
|
|
274
|
+
region,
|
|
275
|
+
// One friendly step: the compute is born on the chosen tier + size (no
|
|
276
|
+
// follow-up `bata compute set` needed). Defaults to serverless / 1 CU.
|
|
277
|
+
compute: { tier: compute.tier, size_cu: compute.sizeCu },
|
|
278
|
+
};
|
|
186
279
|
if (teamId) {
|
|
187
280
|
body.team_id = teamId;
|
|
188
281
|
}
|
|
@@ -197,19 +290,47 @@ export async function create(args = []) {
|
|
|
197
290
|
const project = res.data;
|
|
198
291
|
// Set as default project
|
|
199
292
|
saveConfig({ defaultProject: project.id });
|
|
293
|
+
if (jsonMode) {
|
|
294
|
+
json({
|
|
295
|
+
id: project.id,
|
|
296
|
+
name: project.name,
|
|
297
|
+
region: project.region,
|
|
298
|
+
engine,
|
|
299
|
+
status: project.status,
|
|
300
|
+
...(engine === "powdb"
|
|
301
|
+
? { pricing: "not metered yet" }
|
|
302
|
+
: { compute: { tier: compute.tier, size_cu: compute.sizeCu } }),
|
|
303
|
+
default_project_saved: true,
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
200
307
|
const tierLabel = compute.tier === "always_on" ? "Always-on (dedicated)" : "Serverless";
|
|
201
308
|
log();
|
|
202
309
|
success(`Project ${colors.cyan(project.name)} created`);
|
|
203
310
|
log();
|
|
204
|
-
|
|
205
|
-
[
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
311
|
+
if (engine === "powdb") {
|
|
312
|
+
kvList([
|
|
313
|
+
["ID", colors.dim(project.id)],
|
|
314
|
+
["Region", project.region],
|
|
315
|
+
["Engine", "PowDB (serverless — parks to zero when idle)"],
|
|
316
|
+
// Honest: PowDB compute isn't in the usage meter yet — say so, never $0.
|
|
317
|
+
["Pricing", colors.dim("not metered yet")],
|
|
318
|
+
["Status", statusBadge(project.status)],
|
|
319
|
+
]);
|
|
320
|
+
log();
|
|
321
|
+
log(` ${colors.dim("Set as default project. Run")} ${colors.cyan('bata powdb query "<PowQL>"')} ${colors.dim("to query it.")}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
kvList([
|
|
325
|
+
["ID", colors.dim(project.id)],
|
|
326
|
+
["Region", project.region],
|
|
327
|
+
["Compute", `${tierLabel} · ${compute.sizeCu} CU`],
|
|
328
|
+
["Pricing", colors.dim(computePriceSummary(compute.tier, compute.sizeCu))],
|
|
329
|
+
["Status", statusBadge(project.status)],
|
|
330
|
+
]);
|
|
331
|
+
log();
|
|
332
|
+
log(` ${colors.dim("Set as default project. Run")} ${colors.cyan("bata db connect")} ${colors.dim("to connect.")}`);
|
|
333
|
+
}
|
|
213
334
|
log();
|
|
214
335
|
}
|
|
215
336
|
export async function info(projectId) {
|