@batadata/cli 0.2.6 → 0.2.7

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.
@@ -18,18 +18,18 @@ export declare function parseBranchCreateArgs(args: string[]): {
18
18
  expiresIn?: string;
19
19
  purpose?: string;
20
20
  };
21
- export declare function connect(): Promise<void>;
21
+ export declare function connect(args?: string[]): Promise<void>;
22
22
  export declare function url(args?: string[]): Promise<void>;
23
- export declare function branches(): Promise<void>;
23
+ export declare function branches(args?: string[]): Promise<void>;
24
24
  export declare function branchCreate(args?: string[]): Promise<void>;
25
- export declare function branchDelete(name?: string): Promise<void>;
25
+ export declare function branchDelete(args?: string[]): Promise<void>;
26
26
  /**
27
27
  * `bata db branch protect <name-or-id>` / `unprotect <name-or-id>` — flip a
28
28
  * branch's protection flag (PATCH /v1/branches/:id). A protected branch refuses
29
29
  * delete/reset/rollback/reap — the safety rail for headless/agent ops. The ref
30
30
  * is resolved by name OR id, like the other branch commands.
31
31
  */
32
- export declare function branchSetProtected(ref: string | undefined, wantProtected: boolean): Promise<void>;
32
+ export declare function branchSetProtected(args: string[], wantProtected: boolean): Promise<void>;
33
33
  /**
34
34
  * `bata db branch checkout <name-or-id>` — pin a branch into the directory's
35
35
  * `.batadata/project.json` so later `db query` / `db url` target it with no
@@ -37,7 +37,7 @@ export declare function branchSetProtected(ref: string | undefined, wantProtecte
37
37
  * branch ref is resolved by name OR id, just like `db query --branch`.
38
38
  */
39
39
  export declare function branchCheckout(args: string[]): Promise<void>;
40
- export declare function studio(): Promise<void>;
40
+ export declare function studio(args?: string[]): Promise<void>;
41
41
  /**
42
42
  * Pull `--branch <ref>` / `--at <ISO-timestamp|LSN>` flags (and their `=` forms)
43
43
  * out of the query args and return them plus the remaining args, which join into
@@ -6,7 +6,7 @@ import { colors, log, json, error, success, spinner, table, heading, info as log
6
6
  import { prompt, confirmDestructive } from "../utils/prompts.js";
7
7
  import { openBrowser } from "../utils/open.js";
8
8
  import { emitError, isRetryable } from "../utils/errors.js";
9
- import { resolveProjectId, resolveBranchId, findLinkFile, writeLinkFile } from "../link.js";
9
+ import { resolveProjectId, readLinkFile, findLinkFile, writeLinkFile } from "../link.js";
10
10
  // Share the exact LSN/timestamp discriminator `bata restore` uses so `db query
11
11
  // --at` classifies a restore point identically.
12
12
  import { classifyRestorePoint } from "./restore.js";
@@ -140,7 +140,7 @@ async function resolveBranchRef(projectId, token, teamId, ref) {
140
140
  return null;
141
141
  return result.branches.find((b) => b.id === ref || b.name === ref) ?? null;
142
142
  }
143
- export async function connect() {
143
+ export async function connect(args = []) {
144
144
  // psql is an interactive session — there's no headless equivalent. Don't spawn
145
145
  // it in --json or non-TTY contexts; point agents at the headless surfaces.
146
146
  if (isJsonMode() || !process.stdin.isTTY) {
@@ -148,7 +148,8 @@ export async function connect() {
148
148
  }
149
149
  const token = requireToken();
150
150
  const config = loadConfig();
151
- const projectId = resolveProjectId().projectId;
151
+ const { projectId: projectFlag } = parseProjectFlag(args);
152
+ const projectId = resolveProjectId(projectFlag).projectId;
152
153
  if (!projectId) {
153
154
  emitError("NO_PROJECT", "No default project.", "Run bata projects create or set one with bata projects info <id>.");
154
155
  }
@@ -215,11 +216,15 @@ export async function url(args = []) {
215
216
  "\n");
216
217
  process.stdout.write(conn.connection_uri + "\n");
217
218
  }
218
- export async function branches() {
219
+ export async function branches(args = []) {
219
220
  const token = requireToken();
220
221
  const config = loadConfig();
221
222
  const jsonMode = isJsonMode();
222
- const projectId = resolveProjectId().projectId;
223
+ // `db branches` used to take NO args, so an explicit `--project <id>` was
224
+ // SILENTLY IGNORED and the default/linked project answered — the wrong-target
225
+ // class the 0.1.6 projects fix closed for projects info/delete.
226
+ const { projectId: projectFlag } = parseProjectFlag(args);
227
+ const projectId = resolveProjectId(projectFlag).projectId;
223
228
  if (!projectId) {
224
229
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
225
230
  }
@@ -278,11 +283,16 @@ export async function branchCreate(args = []) {
278
283
  const jsonMode = isJsonMode();
279
284
  const token = requireToken();
280
285
  const config = loadConfig();
281
- const projectId = resolveProjectId().projectId;
286
+ // Strip --project BEFORE the positional parse. Pre-fix, `--project <id>` was
287
+ // both ignored (branch landed on the default project) AND its VALUE bled into
288
+ // the positional name when it led the args (`db branch create --project X dev`
289
+ // created a branch literally named "X" — on the wrong project).
290
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
291
+ const projectId = resolveProjectId(projectFlag).projectId;
282
292
  if (!projectId) {
283
293
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
284
294
  }
285
- const { name, expiresIn, purpose } = parseBranchCreateArgs(args);
295
+ const { name, expiresIn, purpose } = parseBranchCreateArgs(rest);
286
296
  // A relative TTL (--expires-in 2h) becomes an absolute expires_at the server
287
297
  // enforces. Parse it up front so a bad duration fails fast, before any call.
288
298
  let expiresAt;
@@ -330,9 +340,11 @@ export async function branchCreate(args = []) {
330
340
  purpose: res.data.purpose ?? purpose ?? null,
331
341
  },
332
342
  // The branch row exists immediately, but its compute may still be
333
- // provisioning — poll `db branches` for status before connecting.
343
+ // provisioning — poll `db branches` for status before connecting. The
344
+ // poll command carries the resolved project explicitly so following it
345
+ // verbatim can never land on a different linked/default project.
334
346
  ready: false,
335
- poll: "bata db branches --json",
347
+ poll: `bata db branches --project ${projectId} --json`,
336
348
  });
337
349
  return;
338
350
  }
@@ -341,29 +353,35 @@ export async function branchCreate(args = []) {
341
353
  if (expiresAt) {
342
354
  log(` ${colors.dim("Expires")} ${formatDate(expiresAt)} ${colors.dim("(auto-deleted)")}`);
343
355
  }
344
- log(` ${colors.dim("Poll readiness with")} ${colors.cyan("bata db branches")}`);
356
+ log(` ${colors.dim("Poll readiness with")} ${colors.cyan(`bata db branches --project ${projectId}`)}`);
345
357
  log();
346
358
  }
347
- export async function branchDelete(name) {
359
+ export async function branchDelete(args = []) {
348
360
  const jsonMode = isJsonMode();
349
361
  const token = requireToken();
350
362
  const config = loadConfig();
351
- const projectId = resolveProjectId().projectId;
363
+ // --project used to be silently ignored here (the delete hit the default/
364
+ // linked project's branch list) — same wrong-target class as `db branches`.
365
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
366
+ const projectId = resolveProjectId(projectFlag).projectId;
367
+ const ref = rest[0];
352
368
  if (!projectId) {
353
369
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
354
370
  }
355
- if (!name) {
356
- emitError("MISSING_ARG", "Branch name is required.", "Usage: bata db branch delete <name>");
371
+ if (!ref) {
372
+ emitError("MISSING_ARG", "Branch name or id is required.", "Usage: bata db branch delete <name-or-id>");
357
373
  }
358
374
  // Skip the prompt headlessly (--yes / --json / no TTY) so agents and CI can
359
375
  // actually delete — and never report success without deleting.
360
- const ok = await confirmDestructive(`Delete branch ${colors.cyan(name)}?`);
376
+ const ok = await confirmDestructive(`Delete branch ${colors.cyan(ref)}?`);
361
377
  if (!ok) {
362
378
  log(" Aborted.");
363
379
  return;
364
380
  }
365
- const s = jsonMode ? null : spinner(`Deleting branch ${name}`);
366
- // We need to find the branch ID first
381
+ const s = jsonMode ? null : spinner(`Deleting branch ${ref}`);
382
+ // Fetch the branch list explicitly (not via resolveBranchRef, which folds an
383
+ // API outage into null) so a 5xx stays API_UNAVAILABLE/exit 6 — an agent must
384
+ // never read a retryable outage as "branch doesn't exist".
367
385
  const query = {};
368
386
  if (config.defaultTeam)
369
387
  query.team_id = config.defaultTeam;
@@ -372,10 +390,12 @@ export async function branchDelete(name) {
372
390
  s?.stop();
373
391
  emitError(projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(projRes, "Failed to fetch branches."), "");
374
392
  }
375
- const branch = projRes.data.branches.find((b) => b.name === name);
393
+ // Match by name OR id, like checkout/protect/query — delete used to match
394
+ // by name only, so a real branch id got "not found".
395
+ const branch = projRes.data.branches.find((b) => b.id === ref || b.name === ref);
376
396
  if (!branch) {
377
397
  s?.stop();
378
- emitError("BRANCH_NOT_FOUND", `Branch "${name}" not found.`, "List branches with: bata db branches --json");
398
+ emitError("BRANCH_NOT_FOUND", `Branch "${ref}" not found in this project.`, "List branches with: bata db branches --json");
379
399
  }
380
400
  const res = await api.del(`/v1/branches/${branch.id}`, token, query);
381
401
  s?.stop();
@@ -383,11 +403,11 @@ export async function branchDelete(name) {
383
403
  emitError(res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(res, "Failed to delete branch."), "");
384
404
  }
385
405
  if (jsonMode) {
386
- json({ branch: { id: branch.id, name, project_id: projectId }, deleted: true });
406
+ json({ branch: { id: branch.id, name: branch.name, project_id: projectId }, deleted: true });
387
407
  return;
388
408
  }
389
409
  log();
390
- log(` ${colors.green(">")} Branch ${colors.cyan(name)} deleted`);
410
+ log(` ${colors.green(">")} Branch ${colors.cyan(branch.name)} deleted`);
391
411
  log();
392
412
  }
393
413
  /**
@@ -396,11 +416,13 @@ export async function branchDelete(name) {
396
416
  * delete/reset/rollback/reap — the safety rail for headless/agent ops. The ref
397
417
  * is resolved by name OR id, like the other branch commands.
398
418
  */
399
- export async function branchSetProtected(ref, wantProtected) {
419
+ export async function branchSetProtected(args, wantProtected) {
400
420
  const jsonMode = isJsonMode();
401
421
  const token = requireToken();
402
422
  const config = loadConfig();
403
- const projectId = resolveProjectId().projectId;
423
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
424
+ const projectId = resolveProjectId(projectFlag).projectId;
425
+ const ref = rest[0];
404
426
  const verb = wantProtected ? "protect" : "unprotect";
405
427
  if (!projectId) {
406
428
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
@@ -505,8 +527,9 @@ export async function branchCheckout(args) {
505
527
  log(` ${colors.dim("Link file:")} ${colors.dim(linkFile)}`);
506
528
  log();
507
529
  }
508
- export async function studio() {
509
- const projectId = resolveProjectId().projectId;
530
+ export async function studio(args = []) {
531
+ const { projectId: projectFlag } = parseProjectFlag(args);
532
+ const projectId = resolveProjectId(projectFlag).projectId;
510
533
  const studioUrl = projectId
511
534
  ? `https://bench-app-one.vercel.app/studio?project=${projectId}`
512
535
  : "https://bench-app-one.vercel.app/studio";
@@ -652,9 +675,6 @@ async function runTimeTravelQuery(opts) {
652
675
  export async function query(args = []) {
653
676
  const jsonMode = isJsonMode();
654
677
  const { branchId: branchFlag, at, projectId: projectFlag, rest } = parseQueryFlags(args);
655
- // Branch precedence: an explicit --branch wins, else the branch pinned by
656
- // `bata db branch checkout` in .batadata/project.json, else the primary.
657
- const branchId = resolveBranchId(branchFlag).branchId;
658
678
  const sql = rest.join(" ").trim();
659
679
  if (!sql) {
660
680
  emitError("MISSING_ARG", "SQL query is required.", 'Usage: bata db query "SELECT 1"');
@@ -663,6 +683,14 @@ export async function query(args = []) {
663
683
  const config = loadConfig();
664
684
  // Project precedence: --project flag > .batadata link > config default.
665
685
  const projectId = resolveProjectId(projectFlag).projectId;
686
+ // Branch precedence: an explicit --branch wins, else the branch pinned by
687
+ // `bata db branch checkout` in .batadata/project.json — but ONLY when the
688
+ // query targets the linked project. A pinned branch from project A must never
689
+ // resolve against an explicit `--project B` (it misses → BRANCH_NOT_FOUND, or
690
+ // worse, a name collision silently targets the wrong branch).
691
+ const link = readLinkFile()?.link;
692
+ const pinnedBranch = link && link.projectId === projectId ? link.branchId ?? undefined : undefined;
693
+ const branchId = branchFlag ?? pinnedBranch;
666
694
  if (!projectId) {
667
695
  emitError("NO_PROJECT", "No project for db query.", "Pass --project <id>, or run `bata link <project>` to set a default.");
668
696
  }
@@ -786,8 +814,9 @@ function dbHelp(sub) {
786
814
  log(` ${colors.bold("bata db query")} — run a SQL query against a branch`);
787
815
  log();
788
816
  usage('bata db query "SELECT 1"');
789
- usage("bata db query <sql> [--branch <id-or-name>] [--at <timestamp|LSN>] [--json]");
817
+ usage("bata db query <sql> [--project <id>] [--branch <id-or-name>] [--at <timestamp|LSN>] [--json]");
790
818
  log();
819
+ note("--project <id> Target a specific project (default: linked/default project)");
791
820
  note("--branch <id-or-name> Target a specific branch by id OR name (default: the project's primary)");
792
821
  note("--at <timestamp|LSN> Time-travel: run the query AS OF a past point (ISO-8601");
793
822
  note(" timestamp e.g. 2026-07-04T12:00:00Z, or an LSN e.g. 0/15994B0).");
@@ -799,7 +828,7 @@ function dbHelp(sub) {
799
828
  case "branches":
800
829
  log(` ${colors.bold("bata db branches")} — list branches and their compute status`);
801
830
  log();
802
- usage("bata db branches [--json]");
831
+ usage("bata db branches [--project <id>] [--json]");
803
832
  log();
804
833
  note("STATUS shows the live compute state; a ✓ means ready to query.");
805
834
  note("--json includes computeStatus + ready — poll these after create/cold start.");
@@ -807,12 +836,13 @@ function dbHelp(sub) {
807
836
  case "branch":
808
837
  log(` ${colors.bold("bata db branch")} — create, delete, or check out a branch`);
809
838
  log();
810
- usage("bata db branch create <name> [--expires-in <2h|30m|7d>] [--purpose <text>]");
811
- usage("bata db branch delete <name> [--yes]");
839
+ usage("bata db branch create <name> [--project <id>] [--expires-in <2h|30m|7d>] [--purpose <text>]");
840
+ usage("bata db branch delete <name-or-id> [--project <id>] [--yes]");
812
841
  usage("bata db branch checkout <name-or-id> [--project <id>]");
813
- usage("bata db branch protect <name-or-id>");
814
- usage("bata db branch unprotect <name-or-id>");
842
+ usage("bata db branch protect <name-or-id> [--project <id>]");
843
+ usage("bata db branch unprotect <name-or-id> [--project <id>]");
815
844
  log();
845
+ note("--project Target a specific project (default: linked/default project).");
816
846
  note("--expires-in Auto-delete the branch after this long (units: s/m/h/d/w).");
817
847
  note("--purpose Free-text note describing why the branch exists.");
818
848
  note("checkout pins the branch into .batadata/project.json so db query/url");
@@ -823,12 +853,12 @@ function dbHelp(sub) {
823
853
  case "url":
824
854
  log(` ${colors.bold("bata db url")} — print the connection string`);
825
855
  log();
826
- usage("bata db url [--json]");
856
+ usage("bata db url [--project <id>] [--json]");
827
857
  break;
828
858
  case "connect":
829
859
  log(` ${colors.bold("bata db connect")} — open an interactive psql session`);
830
860
  log();
831
- usage("bata db connect");
861
+ usage("bata db connect [--project <id>]");
832
862
  note("Interactive only — use `bata db query` / `bata db url` headlessly.");
833
863
  break;
834
864
  case "studio":
@@ -865,27 +895,27 @@ export async function handleDb(args) {
865
895
  }
866
896
  switch (sub) {
867
897
  case "connect":
868
- return connect();
898
+ return connect(args.slice(1));
869
899
  case "url":
870
900
  return url(args.slice(1));
871
901
  case "branches":
872
- return branches();
902
+ return branches(args.slice(1));
873
903
  case "branch": {
874
904
  const action = args[1];
875
905
  if (action === "create")
876
906
  return branchCreate(args.slice(2));
877
907
  if (action === "delete")
878
- return branchDelete(args[2]);
908
+ return branchDelete(args.slice(2));
879
909
  if (action === "checkout")
880
910
  return branchCheckout(args.slice(2));
881
911
  if (action === "protect")
882
- return branchSetProtected(args[2], true);
912
+ return branchSetProtected(args.slice(2), true);
883
913
  if (action === "unprotect")
884
- return branchSetProtected(args[2], false);
914
+ return branchSetProtected(args.slice(2), false);
885
915
  emitError("INVALID_FLAG", `Unknown: db branch ${action || ""}`, "Available: create, delete, checkout, protect, unprotect");
886
916
  }
887
917
  case "studio":
888
- return studio();
918
+ return studio(args.slice(1));
889
919
  case "query":
890
920
  return query(args.slice(1));
891
921
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batadata/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "CLI for BataDB — serverless Postgres platform",
5
5
  "bin": {
6
6
  "bata": "./dist/index.js"