@batadata/cli 0.1.0 → 0.1.2

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.
Files changed (44) hide show
  1. package/README.md +155 -0
  2. package/dist/api.js +1 -2
  3. package/dist/args.js +1 -2
  4. package/dist/commands/api-keys.js +20 -38
  5. package/dist/commands/auth.js +0 -1
  6. package/dist/commands/connect.js +12 -9
  7. package/dist/commands/create.js +0 -1
  8. package/dist/commands/db.js +123 -82
  9. package/dist/commands/dev.js +0 -1
  10. package/dist/commands/generate.js +0 -1
  11. package/dist/commands/migrate.js +20 -21
  12. package/dist/commands/projects.js +39 -38
  13. package/dist/commands/schema.js +184 -20
  14. package/dist/commands/status.js +16 -14
  15. package/dist/commands/usage.d.ts +13 -0
  16. package/dist/commands/usage.js +164 -0
  17. package/dist/config.d.ts +3 -0
  18. package/dist/config.js +9 -3
  19. package/dist/index.js +48 -13
  20. package/dist/utils/errors.d.ts +29 -0
  21. package/dist/utils/errors.js +65 -0
  22. package/dist/utils/logger.js +1 -2
  23. package/dist/utils/open.js +0 -1
  24. package/dist/utils/prompts.d.ts +10 -0
  25. package/dist/utils/prompts.js +15 -1
  26. package/package.json +1 -1
  27. package/dist/api.js.map +0 -1
  28. package/dist/args.js.map +0 -1
  29. package/dist/commands/api-keys.js.map +0 -1
  30. package/dist/commands/auth.js.map +0 -1
  31. package/dist/commands/connect.js.map +0 -1
  32. package/dist/commands/create.js.map +0 -1
  33. package/dist/commands/db.js.map +0 -1
  34. package/dist/commands/dev.js.map +0 -1
  35. package/dist/commands/generate.js.map +0 -1
  36. package/dist/commands/migrate.js.map +0 -1
  37. package/dist/commands/projects.js.map +0 -1
  38. package/dist/commands/schema.js.map +0 -1
  39. package/dist/commands/status.js.map +0 -1
  40. package/dist/config.js.map +0 -1
  41. package/dist/index.js.map +0 -1
  42. package/dist/utils/logger.js.map +0 -1
  43. package/dist/utils/open.js.map +0 -1
  44. package/dist/utils/prompts.js.map +0 -1
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @batadata/cli
2
+
3
+ Command-line interface for [BataDB](https://batadata.com) — a serverless Postgres
4
+ platform. Create projects, open `psql`, run queries, manage branches and API keys,
5
+ inspect cost, and generate types, all from your terminal.
6
+
7
+ Every command also runs **headlessly** with a single API key and `--json` output,
8
+ so an AI coding agent can provision and operate a database with no human in the
9
+ loop. See [Headless / agent use](#headless--agent-use).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @batadata/cli
15
+ ```
16
+
17
+ This installs the `bata` command. Requires Node.js >= 20.
18
+
19
+ ## Authenticate
20
+
21
+ Either log in interactively:
22
+
23
+ ```bash
24
+ bata login
25
+ ```
26
+
27
+ …or, for CI and agents, set an API key — **every command works with just
28
+ `BATA_API_KEY` set, no login needed**:
29
+
30
+ ```bash
31
+ export BATA_API_KEY=bata_xxx # mint one with: bata api-keys create
32
+ ```
33
+
34
+ You can also pass `--api-key <key>` per command, and point at a different
35
+ control plane with `--api-url <url>` or `BATA_API_URL`.
36
+
37
+ ## Quick start
38
+
39
+ ```bash
40
+ bata create my-app # create a project and wait until it's ready
41
+ bata status # list all projects and their status
42
+ bata connect my-app # open psql (auto-wakes the compute if suspended)
43
+ bata usage # per-dimension cost for the current billing period
44
+ ```
45
+
46
+ ## Commands
47
+
48
+ | Command | Description |
49
+ |---------|-------------|
50
+ | `create <name>` | Create a project and wait for it to be ready |
51
+ | `connect <name>` | Open `psql` to a project (auto-wakes if suspended) |
52
+ | `status` | Show all projects and their status |
53
+ | `usage` | Show per-dimension cost (compute, storage, transfer) for the current period |
54
+ | `login` / `logout` / `whoami` | Manage your session |
55
+ | `api-keys` | Create / list / revoke API keys |
56
+ | `projects` | `list`, `create`, `info`, `delete` |
57
+ | `db url` | Print a connection string |
58
+ | `db connect` | Open `psql` to your database (interactive only) |
59
+ | `db query <sql>` | Run a SQL query and print the rows |
60
+ | `db branches` | List database branches |
61
+ | `db branch create` / `db branch delete` | Manage branches |
62
+ | `db studio` | Open the table browser in your browser |
63
+ | `schema check <file>` | Check a proposed schema change against live query traffic |
64
+ | `generate` | Generate types from your database schema (`--watch` for watch mode) |
65
+ | `dev` | Print the local development setup guide |
66
+
67
+ `schema check` is implemented today. The remaining `schema` subcommands
68
+ (`init`, `push`, `pull`, `diff`) and all of `migrate` are not implemented yet —
69
+ they exit `3` (`NOT_IMPLEMENTED`) and point you at `schema check` for migration
70
+ safety.
71
+
72
+ Run `bata --help` for the full, authoritative command list, or `bata --version`.
73
+
74
+ ## Headless / agent use
75
+
76
+ Every command runs headlessly with just `BATA_API_KEY` set — no `bata login`,
77
+ no interactive prompts, no human in the loop. This is what makes BataDB
78
+ agent-native: an AI coding agent can provision a database, run SQL, branch, and
79
+ clean up entirely on its own.
80
+
81
+ - **`--json`** — machine-readable output. The error envelope is JSON too and
82
+ always the same shape: `{ "error", "code", "hint" }`.
83
+ - **Exit codes** — a stable contract so a script can branch on the failure mode:
84
+
85
+ | Code | Meaning |
86
+ |------|---------|
87
+ | `0` | Success |
88
+ | `1` | Generic error (`CLI_ERROR`) |
89
+ | `2` | Gate tripped (`schema check --fail-on`) |
90
+ | `3` | Not implemented (`NOT_IMPLEMENTED`) |
91
+ | `4` | Auth / credentials (`NO_CREDENTIALS`, `INVALID_KEY`) |
92
+ | `5` | Not found / bad input (`NO_PROJECT`, `BRANCH_NOT_FOUND`, `INVALID_FLAG`, `INTERACTIVE_ONLY`) |
93
+ | `6` | Upstream / transient (`API_UNAVAILABLE`, `TIMEOUT`) — safe to retry |
94
+
95
+ - **`--yes` / `-y`** — skip confirmation prompts on destructive commands.
96
+
97
+ `db connect` and `connect` open an interactive `psql` session and exit `5`
98
+ (`INTERACTIVE_ONLY`) when run headlessly. Agents should use `db url` for a
99
+ connection string or `db query <sql>` to execute SQL.
100
+
101
+ ### A full agent workflow
102
+
103
+ Provision, query, branch, check cost, and tear down — no human, no login:
104
+
105
+ ```bash
106
+ export BATA_API_KEY=bata_xxx
107
+
108
+ # 1. Create a project and wait until it's ready
109
+ bata create my-agent-app --json
110
+
111
+ # 2. Run SQL headlessly (rows come back as JSON objects)
112
+ bata db query "SELECT now()" --json
113
+
114
+ # 3. Branch the database (copy-on-write)
115
+ bata db branch create preview --json
116
+
117
+ # 4. Inspect cost for the current period
118
+ bata usage --json
119
+
120
+ # 5. Clean up
121
+ bata projects delete --yes --json
122
+ ```
123
+
124
+ `db branch create` returns immediately with `"ready": false` — the branch row
125
+ exists but its compute may still be provisioning. Poll `bata db branches --json`
126
+ until the branch reports a ready status before connecting to it.
127
+
128
+ ### Cost truth
129
+
130
+ `bata usage` reports cost per dimension and is deliberately honest about what is
131
+ and isn't metered. Transfer (egress) is **not metered yet**, so it never shows a
132
+ priced `$0` — in `--json` it reports `{ "metered": false, "value": null,
133
+ "cost_cents": null, "status": "not_metered_yet" }`, and in the human view it
134
+ reads `Not metered yet`. You only ever see a dollar figure for a dimension we
135
+ genuinely meter end to end (compute and storage today).
136
+
137
+ ```bash
138
+ bata usage --json | jq '.projects[].transfer.status' # "not_metered_yet"
139
+ bata status --json | jq '.projects[].name' # list project names
140
+ ```
141
+
142
+ ## Global options
143
+
144
+ | Flag | Description |
145
+ |------|-------------|
146
+ | `--json` | Machine-readable JSON output (read commands) |
147
+ | `--api-key <key>` | API key (else `BATA_API_KEY`, else saved login) |
148
+ | `--api-url <url>` | API base URL (else `BATA_API_URL`, else config/default) |
149
+ | `--yes`, `-y` | Skip confirmation prompts |
150
+ | `--help`, `-h` | Show help |
151
+ | `--version`, `-v` | Show version |
152
+
153
+ ## License
154
+
155
+ MIT
package/dist/api.js CHANGED
@@ -14,7 +14,7 @@ export async function request(method, path, options = {}) {
14
14
  }
15
15
  const headers = {
16
16
  "Content-Type": "application/json",
17
- "User-Agent": "@batadata/cli 0.1.0",
17
+ "User-Agent": "@batadata/cli 0.1.2",
18
18
  };
19
19
  if (options.token) {
20
20
  headers["Authorization"] = `Bearer ${options.token}`;
@@ -112,4 +112,3 @@ export const api = {
112
112
  post: (path, body, token) => request("POST", path, { token, body }),
113
113
  del: (path, token, query) => request("DELETE", path, { token, query }),
114
114
  };
115
- //# sourceMappingURL=api.js.map
package/dist/args.js CHANGED
@@ -36,7 +36,6 @@ export function parseGlobalFlags(argv) {
36
36
  rest.push(arg);
37
37
  }
38
38
  }
39
- setRuntime({ json: flags.json, apiKey: flags.apiKey, apiUrl: flags.apiUrl });
39
+ setRuntime({ json: flags.json, apiKey: flags.apiKey, apiUrl: flags.apiUrl, yes: flags.yes });
40
40
  return { rest, flags };
41
41
  }
42
- //# sourceMappingURL=args.js.map
@@ -10,8 +10,15 @@
10
10
  */
11
11
  import { api, apiError } from "../api.js";
12
12
  import { requireToken, isJsonMode } from "../config.js";
13
- import { colors, log, json, success, error, warn, spinner, table, kvList, heading } from "../utils/logger.js";
14
- import { prompt, confirm } from "../utils/prompts.js";
13
+ import { colors, log, json, success, warn, spinner, table, kvList, heading } from "../utils/logger.js";
14
+ import { prompt, confirmDestructive } from "../utils/prompts.js";
15
+ import { emitError } from "../utils/errors.js";
16
+ /** Map an API-key endpoint failure to the right coded exit. */
17
+ function apiKeyError(res, fallbackMsg) {
18
+ emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
19
+ : res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
20
+ : "CLI_ERROR", fallbackMsg, "");
21
+ }
15
22
  function parseFlag(args, name) {
16
23
  for (let i = 0; i < args.length; i++) {
17
24
  if (args[i] === name && args[i + 1])
@@ -60,13 +67,7 @@ async function create(args) {
60
67
  const res = await api.post("/v1/api-keys", { name }, token);
61
68
  s?.stop();
62
69
  if (!res.ok) {
63
- if (jsonMode) {
64
- json({ error: apiError(res, "Failed to create API key") });
65
- }
66
- else {
67
- error(apiError(res, "Failed to create API key"));
68
- }
69
- process.exit(1);
70
+ apiKeyError(res, apiError(res, "Failed to create API key"));
70
71
  }
71
72
  const k = res.data;
72
73
  if (jsonMode) {
@@ -104,13 +105,7 @@ async function list(args) {
104
105
  const res = await api.get("/v1/api-keys", token);
105
106
  s?.stop();
106
107
  if (!res.ok) {
107
- if (jsonMode) {
108
- json({ error: apiError(res, "Failed to list API keys") });
109
- }
110
- else {
111
- error(apiError(res, "Failed to list API keys"));
112
- }
113
- process.exit(1);
108
+ apiKeyError(res, apiError(res, "Failed to list API keys"));
114
109
  }
115
110
  const keys = Array.isArray(res.data) ? res.data : [];
116
111
  if (jsonMode) {
@@ -145,32 +140,22 @@ async function list(args) {
145
140
  async function revoke(args) {
146
141
  const token = requireToken();
147
142
  const jsonMode = isJsonMode();
148
- const skipConfirm = args.includes("--yes") || args.includes("-y");
149
143
  const id = positional(args)[0];
150
144
  if (!id) {
151
- const msg = "API key ID is required. Usage: bata api-keys revoke <id> [--yes]";
152
- if (jsonMode)
153
- json({ error: msg });
154
- else
155
- error(msg);
156
- process.exit(1);
145
+ emitError("MISSING_ARG", "API key ID is required.", "Usage: bata api-keys revoke <id> [--yes]");
157
146
  }
158
- if (!skipConfirm && !jsonMode && process.stdin.isTTY) {
159
- const ok = await confirm(`Revoke API key ${colors.cyan(id)}? This cannot be undone.`, false);
160
- if (!ok) {
161
- log(" Aborted.");
162
- return;
163
- }
147
+ // Shared headless-skip rule (--yes / --json / no TTY) so the prompt can't
148
+ // block an agent and the behavior can't drift from branch/project deletes.
149
+ const ok = await confirmDestructive(`Revoke API key ${colors.cyan(id)}? This cannot be undone.`);
150
+ if (!ok) {
151
+ log(" Aborted.");
152
+ return;
164
153
  }
165
154
  const s = jsonMode ? null : spinner("Revoking API key");
166
155
  const res = await api.del(`/v1/api-keys/${id}`, token);
167
156
  s?.stop();
168
157
  if (!res.ok) {
169
- if (jsonMode)
170
- json({ error: apiError(res, "Failed to revoke API key") });
171
- else
172
- error(apiError(res, "Failed to revoke API key"));
173
- process.exit(1);
158
+ apiKeyError(res, apiError(res, "Failed to revoke API key"));
174
159
  }
175
160
  if (jsonMode) {
176
161
  json({ id, revoked: true });
@@ -215,9 +200,6 @@ export async function handleApiKeys(args) {
215
200
  case "revoke":
216
201
  return revoke(rest);
217
202
  default:
218
- error(`Unknown subcommand: api-keys ${sub}`);
219
- log(` ${colors.dim("Available:")} create, list, revoke`);
220
- process.exit(1);
203
+ emitError("INVALID_FLAG", `Unknown subcommand: api-keys ${sub}`, "Available: create, list, revoke");
221
204
  }
222
205
  }
223
- //# sourceMappingURL=api-keys.js.map
@@ -130,4 +130,3 @@ export async function whoami() {
130
130
  }
131
131
  log();
132
132
  }
133
- //# sourceMappingURL=auth.js.map
@@ -6,9 +6,15 @@
6
6
  */
7
7
  import { execSync, spawn } from "node:child_process";
8
8
  import { api } from "../api.js";
9
- import { requireToken, loadConfig } from "../config.js";
9
+ import { requireToken, loadConfig, isJsonMode } from "../config.js";
10
10
  import { colors, log, error, spinner, info as logInfo } from "../utils/logger.js";
11
+ import { emitError } from "../utils/errors.js";
11
12
  export async function connect(args) {
13
+ // psql is an interactive session — there's no headless equivalent. Don't spawn
14
+ // it in --json or non-TTY contexts; point agents at the headless surfaces.
15
+ if (isJsonMode() || !process.stdin.isTTY) {
16
+ emitError("INTERACTIVE_ONLY", "bata connect opens an interactive psql session and can't run headlessly.", "Use `bata db url` for a connection string or `bata db query <sql>` for headless execution.");
17
+ }
12
18
  const token = requireToken();
13
19
  const config = loadConfig();
14
20
  // Accept project name as argument, or use default
@@ -23,21 +29,19 @@ export async function connect(args) {
23
29
  const projRes = await api.get("/v1/projects", token, query);
24
30
  s.stop();
25
31
  if (!projRes.ok) {
26
- error("Failed to fetch projects.");
27
- process.exit(1);
32
+ emitError(projRes.status === 401 || projRes.status === 403 ? "INVALID_KEY"
33
+ : projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE"
34
+ : "CLI_ERROR", "Failed to fetch projects.", "");
28
35
  }
29
36
  const projects = Array.isArray(projRes.data) ? projRes.data : [];
30
37
  const found = projects.find((p) => p.name === projectName || p.id === projectName);
31
38
  if (!found) {
32
- error(`Project "${projectName}" not found.`);
33
- process.exit(1);
39
+ emitError("NOT_FOUND", `Project "${projectName}" not found.`, "");
34
40
  }
35
41
  projectId = found.id;
36
42
  }
37
43
  if (!projectId) {
38
- error("No project specified. Usage: bata connect <project-name>");
39
- log(` Or set a default with ${colors.cyan("bata create <name>")}`);
40
- process.exit(1);
44
+ emitError("NO_PROJECT", "No project specified.", "Usage: bata connect <project-name> (or set a default with bata create <name>)");
41
45
  }
42
46
  const s = spinner("Fetching connection info");
43
47
  const connRes = await api.get(`/v1/connection-info/${projectId}`, token);
@@ -100,4 +104,3 @@ export async function connect(args) {
100
104
  function sleep(ms) {
101
105
  return new Promise((resolve) => setTimeout(resolve, ms));
102
106
  }
103
- //# sourceMappingURL=connect.js.map
@@ -194,4 +194,3 @@ async function pollOperation(operationId, token, s, teamId) {
194
194
  function sleep(ms) {
195
195
  return new Promise((resolve) => setTimeout(resolve, ms));
196
196
  }
197
- //# sourceMappingURL=create.js.map