@batadata/cli 0.2.15 → 0.2.16
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/db.d.ts +11 -0
- package/dist/commands/db.js +174 -8
- package/package.json +2 -2
package/dist/commands/db.d.ts
CHANGED
|
@@ -23,6 +23,17 @@ export declare function url(args?: string[]): Promise<void>;
|
|
|
23
23
|
export declare function branches(args?: string[]): Promise<void>;
|
|
24
24
|
export declare function branchCreate(args?: string[]): Promise<void>;
|
|
25
25
|
export declare function branchDelete(args?: string[]): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* `bata db branch reset <name-or-id> [--yes] [--wait] [--json]` (#38): reset a
|
|
28
|
+
* branch to its parent's CURRENT state (fresh data for a re-run) via
|
|
29
|
+
* POST /v1/branches/:id/reset-from-parent. The server answers with an
|
|
30
|
+
* operation id and `pending`; by default this exits 0 on accepted and prints
|
|
31
|
+
* the id, and `--wait` polls the operation to completion (exit 6 retryable on
|
|
32
|
+
* timeout, so a CI step can just re-run). A protected branch is refused by the
|
|
33
|
+
* server (400 BRANCH_PROTECTED); that surfaces as exit 5 with the server's own
|
|
34
|
+
* hint, never as a silent no-op.
|
|
35
|
+
*/
|
|
36
|
+
export declare function branchReset(args?: string[]): Promise<void>;
|
|
26
37
|
/**
|
|
27
38
|
* `bata db branch protect <name-or-id>` / `unprotect <name-or-id>` — flip a
|
|
28
39
|
* branch's protection flag (PATCH /v1/branches/:id). A protected branch refuses
|
package/dist/commands/db.js
CHANGED
|
@@ -12,13 +12,20 @@ import { resolveProjectId, readLinkFile, findLinkFile, writeLinkFile } from "../
|
|
|
12
12
|
import { classifyRestorePoint } from "./restore.js";
|
|
13
13
|
// Same --project/positional resolution as projects info/delete (0.1.6 fix).
|
|
14
14
|
import { resolveProjectArg } from "./projects.js";
|
|
15
|
-
async function getConnectionInfo(projectId, token) {
|
|
15
|
+
async function getConnectionInfo(projectId, token, branchId) {
|
|
16
16
|
// reveal=true so the returned string is actually usable (the owner is asking).
|
|
17
|
-
|
|
17
|
+
// branch_id narrows the server-side answer to one branch (#37); without it
|
|
18
|
+
// the primary wins, which is what `bata db url` always meant before.
|
|
19
|
+
const query = { reveal: "true" };
|
|
20
|
+
if (branchId)
|
|
21
|
+
query.branch_id = branchId;
|
|
22
|
+
const res = await api.get(`/v1/connection-info/${projectId}`, token, query);
|
|
18
23
|
if (!res.ok)
|
|
19
24
|
return null;
|
|
20
25
|
const conns = res.data?.connections ?? [];
|
|
21
|
-
const c =
|
|
26
|
+
const c = branchId
|
|
27
|
+
? conns.find((x) => x.branch_id === branchId) ?? null
|
|
28
|
+
: conns.find((x) => x.is_primary) ?? conns[0];
|
|
22
29
|
if (!c)
|
|
23
30
|
return null;
|
|
24
31
|
return {
|
|
@@ -183,15 +190,27 @@ export async function connect(args = []) {
|
|
|
183
190
|
export async function url(args = []) {
|
|
184
191
|
const token = requireToken();
|
|
185
192
|
const config = loadConfig();
|
|
193
|
+
// `--branch <id-or-name>` (#37): the branch-per-PR actions had to curl the
|
|
194
|
+
// REST connection-info endpoint because `db url` only ever answered for the
|
|
195
|
+
// primary. Same resolution as `db query --branch`: id OR name.
|
|
196
|
+
const { ref: branchRef, rest } = parseBranchFlag(args);
|
|
186
197
|
// Precedence: --project <id> / positional arg > .batadata link > config
|
|
187
198
|
// default. `db url` used to IGNORE --project and silently serve the linked/
|
|
188
199
|
// default project — the same flag-ignored class as the 0.1.6 projects
|
|
189
200
|
// delete/info fix, and the root of the wrong-target benchmark incident.
|
|
190
|
-
const projectId = resolveProjectArg(
|
|
201
|
+
const projectId = resolveProjectArg(rest) ?? resolveProjectId().projectId;
|
|
191
202
|
if (!projectId) {
|
|
192
203
|
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
193
204
|
}
|
|
194
|
-
|
|
205
|
+
let branchId;
|
|
206
|
+
if (branchRef) {
|
|
207
|
+
const branch = await resolveBranchRef(projectId, token, config.defaultTeam, branchRef);
|
|
208
|
+
if (!branch) {
|
|
209
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${branchRef}" not found in this project.`, "List branches with: bata db branches --json");
|
|
210
|
+
}
|
|
211
|
+
branchId = branch.id;
|
|
212
|
+
}
|
|
213
|
+
const conn = await getConnectionInfo(projectId, token, branchId);
|
|
195
214
|
if (!conn || !conn.connection_uri) {
|
|
196
215
|
emitError("NOT_FOUND", "Could not fetch connection string.", "");
|
|
197
216
|
}
|
|
@@ -410,6 +429,145 @@ export async function branchDelete(args = []) {
|
|
|
410
429
|
log(` ${colors.green(">")} Branch ${colors.cyan(branch.name)} deleted`);
|
|
411
430
|
log();
|
|
412
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Pull `--branch <ref>` (or `--branch=<ref>`) out of an arg list, returning the
|
|
434
|
+
* remaining args untouched so positional/--project parsing is order-independent.
|
|
435
|
+
*/
|
|
436
|
+
function parseBranchFlag(args) {
|
|
437
|
+
const rest = [];
|
|
438
|
+
let ref;
|
|
439
|
+
for (let i = 0; i < args.length; i++) {
|
|
440
|
+
const a = args[i];
|
|
441
|
+
if (a === "--branch") {
|
|
442
|
+
ref = args[i + 1];
|
|
443
|
+
i++;
|
|
444
|
+
}
|
|
445
|
+
else if (a.startsWith("--branch=")) {
|
|
446
|
+
ref = a.slice("--branch=".length);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
rest.push(a);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { ref, rest };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* `bata db branch reset <name-or-id> [--yes] [--wait] [--json]` (#38): reset a
|
|
456
|
+
* branch to its parent's CURRENT state (fresh data for a re-run) via
|
|
457
|
+
* POST /v1/branches/:id/reset-from-parent. The server answers with an
|
|
458
|
+
* operation id and `pending`; by default this exits 0 on accepted and prints
|
|
459
|
+
* the id, and `--wait` polls the operation to completion (exit 6 retryable on
|
|
460
|
+
* timeout, so a CI step can just re-run). A protected branch is refused by the
|
|
461
|
+
* server (400 BRANCH_PROTECTED); that surfaces as exit 5 with the server's own
|
|
462
|
+
* hint, never as a silent no-op.
|
|
463
|
+
*/
|
|
464
|
+
export async function branchReset(args = []) {
|
|
465
|
+
const jsonMode = isJsonMode();
|
|
466
|
+
const token = requireToken();
|
|
467
|
+
const config = loadConfig();
|
|
468
|
+
const wait = args.includes("--wait");
|
|
469
|
+
const { projectId: projectFlag, rest } = parseProjectFlag(args.filter((a) => a !== "--wait"));
|
|
470
|
+
const projectId = resolveProjectId(projectFlag).projectId;
|
|
471
|
+
const ref = rest[0];
|
|
472
|
+
if (!projectId) {
|
|
473
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
474
|
+
}
|
|
475
|
+
if (!ref) {
|
|
476
|
+
emitError("MISSING_ARG", "Branch name or id is required.", "Usage: bata db branch reset <name-or-id>");
|
|
477
|
+
}
|
|
478
|
+
// Destructive: the branch's own data is replaced by the parent's.
|
|
479
|
+
const ok = await confirmDestructive(`Reset branch ${colors.cyan(ref)} to its parent's current state? Its own changes are lost.`);
|
|
480
|
+
if (!ok) {
|
|
481
|
+
log(" Aborted.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const s = jsonMode ? null : spinner(`Resolving branch ${ref}`);
|
|
485
|
+
// Explicit fetch (not resolveBranchRef) so a 5xx stays API_UNAVAILABLE/exit 6:
|
|
486
|
+
// an agent must never read an outage as "branch doesn't exist".
|
|
487
|
+
const query = {};
|
|
488
|
+
if (config.defaultTeam)
|
|
489
|
+
query.team_id = config.defaultTeam;
|
|
490
|
+
const projRes = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
491
|
+
if (!projRes.ok || !projRes.data.branches) {
|
|
492
|
+
s?.stop();
|
|
493
|
+
emitError(projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(projRes, "Failed to fetch branches."), "");
|
|
494
|
+
}
|
|
495
|
+
const branch = projRes.data.branches.find((b) => b.id === ref || b.name === ref);
|
|
496
|
+
if (!branch) {
|
|
497
|
+
s?.stop();
|
|
498
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${ref}" not found in this project.`, "List branches with: bata db branches --json");
|
|
499
|
+
}
|
|
500
|
+
if (branch.isPrimary) {
|
|
501
|
+
s?.stop();
|
|
502
|
+
emitError("INVALID_FLAG", `Branch "${branch.name}" is the primary branch; it has no parent to reset from.`, "");
|
|
503
|
+
}
|
|
504
|
+
s?.update(`Resetting branch ${branch.name} from its parent`);
|
|
505
|
+
const res = await api.post(`/v1/branches/${branch.id}/reset-from-parent`, {}, token);
|
|
506
|
+
if (!res.ok) {
|
|
507
|
+
s?.stop();
|
|
508
|
+
// Exit-code contract: 5xx/0 is retryable (6); a 400/404 is the caller's
|
|
509
|
+
// input (5), e.g. BRANCH_PROTECTED, so an agent does not retry it forever.
|
|
510
|
+
const code = res.status >= 500 || res.status === 0
|
|
511
|
+
? "API_UNAVAILABLE"
|
|
512
|
+
: res.status === 404
|
|
513
|
+
? "NOT_FOUND"
|
|
514
|
+
: res.status === 400
|
|
515
|
+
? "INVALID_FLAG"
|
|
516
|
+
: "CLI_ERROR";
|
|
517
|
+
emitError(code, apiError(res, "Failed to reset branch."), res.data && res.data.code === "BRANCH_PROTECTED"
|
|
518
|
+
? `Unprotect it first: bata db branch unprotect ${branch.name}`
|
|
519
|
+
: "");
|
|
520
|
+
}
|
|
521
|
+
const operationId = res.data.operation_id;
|
|
522
|
+
let finalStatus = res.data.status ?? "pending";
|
|
523
|
+
if (wait && operationId) {
|
|
524
|
+
finalStatus = await waitForOperation(operationId, token, config.defaultTeam, (msg) => s?.update(msg));
|
|
525
|
+
s?.stop();
|
|
526
|
+
if (finalStatus === "failed") {
|
|
527
|
+
emitError("CLI_ERROR", `Reset of branch ${branch.name} failed (operation ${operationId}).`, "");
|
|
528
|
+
}
|
|
529
|
+
if (finalStatus !== "completed") {
|
|
530
|
+
emitError("API_UNAVAILABLE", `Reset of branch ${branch.name} is still ${finalStatus} after 2 minutes (operation ${operationId}).`, `Poll it: bata db branches --project ${projectId} --json`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
s?.stop();
|
|
535
|
+
}
|
|
536
|
+
if (jsonMode) {
|
|
537
|
+
json({
|
|
538
|
+
branch: { id: branch.id, name: branch.name, project_id: projectId },
|
|
539
|
+
operation_id: operationId,
|
|
540
|
+
status: finalStatus,
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
log();
|
|
545
|
+
log(` ${colors.green(">")} Branch ${colors.cyan(branch.name)} reset ${finalStatus === "completed" ? "complete" : "accepted"}`);
|
|
546
|
+
if (operationId)
|
|
547
|
+
log(` ${colors.dim(`operation ${operationId} (${finalStatus})`)}`);
|
|
548
|
+
log();
|
|
549
|
+
}
|
|
550
|
+
/** Poll an async operation until it settles or 2 minutes pass. Returns the last status seen. */
|
|
551
|
+
async function waitForOperation(operationId, token, teamId, onProgress) {
|
|
552
|
+
const maxWait = 120_000;
|
|
553
|
+
const pollInterval = 2_000;
|
|
554
|
+
const start = Date.now();
|
|
555
|
+
let last = "pending";
|
|
556
|
+
while (Date.now() - start < maxWait) {
|
|
557
|
+
const query = {};
|
|
558
|
+
if (teamId)
|
|
559
|
+
query.team_id = teamId;
|
|
560
|
+
const res = await api.get(`/v1/operations/${operationId}`, token, query);
|
|
561
|
+
if (res.ok && res.data?.status) {
|
|
562
|
+
last = res.data.status;
|
|
563
|
+
if (last === "completed" || last === "failed")
|
|
564
|
+
return last;
|
|
565
|
+
onProgress(`Waiting for reset (${last})`);
|
|
566
|
+
}
|
|
567
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
568
|
+
}
|
|
569
|
+
return last;
|
|
570
|
+
}
|
|
413
571
|
/**
|
|
414
572
|
* `bata db branch protect <name-or-id>` / `unprotect <name-or-id>` — flip a
|
|
415
573
|
* branch's protection flag (PATCH /v1/branches/:id). A protected branch refuses
|
|
@@ -838,6 +996,7 @@ function dbHelp(sub) {
|
|
|
838
996
|
log();
|
|
839
997
|
usage("bata db branch create <name> [--project <id>] [--expires-in <2h|30m|7d>] [--purpose <text>]");
|
|
840
998
|
usage("bata db branch delete <name-or-id> [--project <id>] [--yes]");
|
|
999
|
+
usage("bata db branch reset <name-or-id> [--project <id>] [--yes] [--wait]");
|
|
841
1000
|
usage("bata db branch checkout <name-or-id> [--project <id>]");
|
|
842
1001
|
usage("bata db branch protect <name-or-id> [--project <id>]");
|
|
843
1002
|
usage("bata db branch unprotect <name-or-id> [--project <id>]");
|
|
@@ -847,13 +1006,17 @@ function dbHelp(sub) {
|
|
|
847
1006
|
note("--purpose Free-text note describing why the branch exists.");
|
|
848
1007
|
note("checkout pins the branch into .batadata/project.json so db query/url");
|
|
849
1008
|
note("target it without a --branch flag. Needs a linked project (bata link).");
|
|
1009
|
+
note("reset replaces the branch's data with its parent's CURRENT state (async;");
|
|
1010
|
+
note("prints an operation id, exit 0 on accepted; --wait polls to completion).");
|
|
850
1011
|
note("protect/unprotect lock a branch against delete/reset/rollback/auto-reap.");
|
|
851
1012
|
note("A protected branch cannot also carry a TTL (they're mutually exclusive).");
|
|
852
1013
|
break;
|
|
853
1014
|
case "url":
|
|
854
1015
|
log(` ${colors.bold("bata db url")} — print the connection string`);
|
|
855
1016
|
log();
|
|
856
|
-
usage("bata db url [--project <id>] [--json]");
|
|
1017
|
+
usage("bata db url [--project <id>] [--branch <id-or-name>] [--json]");
|
|
1018
|
+
log();
|
|
1019
|
+
note("--branch <id-or-name> Connection string for a specific branch (default: primary).");
|
|
857
1020
|
break;
|
|
858
1021
|
case "connect":
|
|
859
1022
|
log(` ${colors.bold("bata db connect")} — open an interactive psql session`);
|
|
@@ -870,10 +1033,11 @@ function dbHelp(sub) {
|
|
|
870
1033
|
log(` ${colors.bold("bata db")} — database commands`);
|
|
871
1034
|
log();
|
|
872
1035
|
usage("bata db connect Open psql to your database");
|
|
873
|
-
usage("bata db url Print connection string");
|
|
1036
|
+
usage("bata db url Print connection string (--branch <id-or-name>)");
|
|
874
1037
|
usage("bata db branches List branches + compute status");
|
|
875
1038
|
usage("bata db branch create Create a new branch");
|
|
876
1039
|
usage("bata db branch delete Delete a branch");
|
|
1040
|
+
usage("bata db branch reset Reset a branch to its parent's current state");
|
|
877
1041
|
usage("bata db branch checkout Pin a branch into .batadata/project.json");
|
|
878
1042
|
usage("bata db branch protect Lock a branch against destructive ops");
|
|
879
1043
|
usage("bata db branch unprotect Remove a branch's protection");
|
|
@@ -906,13 +1070,15 @@ export async function handleDb(args) {
|
|
|
906
1070
|
return branchCreate(args.slice(2));
|
|
907
1071
|
if (action === "delete")
|
|
908
1072
|
return branchDelete(args.slice(2));
|
|
1073
|
+
if (action === "reset")
|
|
1074
|
+
return branchReset(args.slice(2));
|
|
909
1075
|
if (action === "checkout")
|
|
910
1076
|
return branchCheckout(args.slice(2));
|
|
911
1077
|
if (action === "protect")
|
|
912
1078
|
return branchSetProtected(args.slice(2), true);
|
|
913
1079
|
if (action === "unprotect")
|
|
914
1080
|
return branchSetProtected(args.slice(2), false);
|
|
915
|
-
emitError("INVALID_FLAG", `Unknown: db branch ${action || ""}`, "Available: create, delete, checkout, protect, unprotect");
|
|
1081
|
+
emitError("INVALID_FLAG", `Unknown: db branch ${action || ""}`, "Available: create, delete, reset, checkout, protect, unprotect");
|
|
916
1082
|
}
|
|
917
1083
|
case "studio":
|
|
918
1084
|
return studio(args.slice(1));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@batadata/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "CLI for BataDB — serverless Postgres platform",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bata": "./dist/index.js"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/ZVN-DEV/batadata.git",
|
|
32
32
|
"directory": "packages/cli"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|