@batadata/cli 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  Command-line interface for [BataDB](https://batadata.com) — a serverless Postgres
4
4
  platform. Create projects, open `psql`, run queries, manage branches and API keys,
5
- and generate types, all from your terminal.
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).
6
10
 
7
11
  ## Install
8
12
 
@@ -36,6 +40,7 @@ control plane with `--api-url <url>` or `BATA_API_URL`.
36
40
  bata create my-app # create a project and wait until it's ready
37
41
  bata status # list all projects and their status
38
42
  bata connect my-app # open psql (auto-wakes the compute if suspended)
43
+ bata usage # per-dimension cost for the current billing period
39
44
  ```
40
45
 
41
46
  ## Commands
@@ -45,33 +50,93 @@ bata connect my-app # open psql (auto-wakes the compute if suspended)
45
50
  | `create <name>` | Create a project and wait for it to be ready |
46
51
  | `connect <name>` | Open `psql` to a project (auto-wakes if suspended) |
47
52
  | `status` | Show all projects and their status |
53
+ | `usage` | Show per-dimension cost (compute, storage, transfer) for the current period |
48
54
  | `login` / `logout` / `whoami` | Manage your session |
49
55
  | `api-keys` | Create / list / revoke API keys |
50
56
  | `projects` | `list`, `create`, `info`, `delete` |
51
57
  | `db url` | Print a connection string |
52
- | `db connect` | Open `psql` to your database |
53
- | `db query` | Run a SQL query |
58
+ | `db connect` | Open `psql` to your database (interactive only) |
59
+ | `db query <sql>` | Run a SQL query and print the rows |
54
60
  | `db branches` | List database branches |
55
61
  | `db branch create` / `db branch delete` | Manage branches |
56
62
  | `db studio` | Open the table browser in your browser |
63
+ | `schema check <file>` | Check a proposed schema change against live query traffic |
57
64
  | `generate` | Generate types from your database schema (`--watch` for watch mode) |
58
65
  | `dev` | Print the local development setup guide |
59
66
 
60
- `schema` and `migrate` are reserved and currently print a "coming soon" notice.
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.
61
71
 
62
72
  Run `bata --help` for the full, authoritative command list, or `bata --version`.
63
73
 
64
74
  ## Headless / agent use
65
75
 
66
- The CLI is built to be scriptable:
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
67
129
 
68
- - **`--json`** machine-readable output on read commands. Errors are emitted as
69
- JSON too, e.g. `{"error":"Not authenticated","code":"NO_CREDENTIALS","hint":"..."}`.
70
- - **Exit codes** — `0` on success, `1` on error (auth failures, unknown commands).
71
- - **`--yes` / `-y`** skip confirmation prompts.
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).
72
136
 
73
137
  ```bash
74
- bata status --json | jq '.[].name'
138
+ bata usage --json | jq '.projects[].transfer.status' # "not_metered_yet"
139
+ bata status --json | jq '.projects[].name' # list project names
75
140
  ```
76
141
 
77
142
  ## Global options
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.3",
18
18
  };
19
19
  if (options.token) {
20
20
  headers["Authorization"] = `Bearer ${options.token}`;
package/dist/args.js CHANGED
@@ -36,6 +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
  }
@@ -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,8 +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
  }
@@ -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);
@@ -4,5 +4,5 @@ export declare function branches(): Promise<void>;
4
4
  export declare function branchCreate(name?: string): Promise<void>;
5
5
  export declare function branchDelete(name?: string): Promise<void>;
6
6
  export declare function studio(): Promise<void>;
7
- export declare function query(sql?: string): Promise<void>;
7
+ export declare function query(args?: string[]): Promise<void>;
8
8
  export declare function handleDb(args: string[]): Promise<void>;