@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 +75 -10
- package/dist/api.js +1 -1
- package/dist/args.js +1 -1
- package/dist/commands/api-keys.js +20 -37
- package/dist/commands/connect.js +12 -8
- package/dist/commands/db.d.ts +1 -1
- package/dist/commands/db.js +256 -84
- package/dist/commands/db.test.d.ts +1 -0
- package/dist/commands/db.test.js +173 -0
- package/dist/commands/migrate.js +20 -20
- package/dist/commands/projects.js +39 -37
- package/dist/commands/projects.test.d.ts +1 -0
- package/dist/commands/projects.test.js +104 -0
- package/dist/commands/schema.js +184 -19
- package/dist/commands/status.js +16 -13
- package/dist/commands/usage.d.ts +13 -0
- package/dist/commands/usage.js +164 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -2
- package/dist/index.js +52 -13
- package/dist/utils/errors.d.ts +47 -0
- package/dist/utils/errors.js +91 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/prompts.d.ts +10 -0
- package/dist/utils/prompts.js +15 -0
- package/package.json +5 -4
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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.
|
|
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,
|
|
14
|
-
import { prompt,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/connect.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/commands/db.d.ts
CHANGED
|
@@ -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(
|
|
7
|
+
export declare function query(args?: string[]): Promise<void>;
|
|
8
8
|
export declare function handleDb(args: string[]): Promise<void>;
|