@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.
@@ -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
@@ -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
- const res = await api.get(`/v1/connection-info/${projectId}`, token, { reveal: "true" });
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 = conns.find((x) => x.is_primary) ?? conns[0];
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(args) ?? resolveProjectId().projectId;
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
- const conn = await getConnectionInfo(projectId, token);
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.15",
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/zvndev/batadata.git",
31
+ "url": "https://github.com/ZVN-DEV/batadata.git",
32
32
  "directory": "packages/cli"
33
33
  },
34
34
  "keywords": [