@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.
- package/README.md +155 -0
- package/dist/api.js +1 -2
- package/dist/args.js +1 -2
- package/dist/commands/api-keys.js +20 -38
- package/dist/commands/auth.js +0 -1
- package/dist/commands/connect.js +12 -9
- package/dist/commands/create.js +0 -1
- package/dist/commands/db.js +123 -82
- package/dist/commands/dev.js +0 -1
- package/dist/commands/generate.js +0 -1
- package/dist/commands/migrate.js +20 -21
- package/dist/commands/projects.js +39 -38
- package/dist/commands/schema.js +184 -20
- package/dist/commands/status.js +16 -14
- 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 -3
- package/dist/index.js +48 -13
- package/dist/utils/errors.d.ts +29 -0
- package/dist/utils/errors.js +65 -0
- package/dist/utils/logger.js +1 -2
- package/dist/utils/open.js +0 -1
- package/dist/utils/prompts.d.ts +10 -0
- package/dist/utils/prompts.js +15 -1
- package/package.json +1 -1
- package/dist/api.js.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/commands/api-keys.js.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/migrate.js.map +0 -1
- package/dist/commands/projects.js.map +0 -1
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/open.js.map +0 -1
- package/dist/utils/prompts.js.map +0 -1
package/dist/commands/db.js
CHANGED
|
@@ -2,8 +2,9 @@ import { execSync, spawn } from "node:child_process";
|
|
|
2
2
|
import { api, apiError } from "../api.js";
|
|
3
3
|
import { requireToken, loadConfig, isJsonMode } from "../config.js";
|
|
4
4
|
import { colors, log, json, error, spinner, table, heading, info as logInfo } from "../utils/logger.js";
|
|
5
|
-
import { prompt,
|
|
5
|
+
import { prompt, confirmDestructive } from "../utils/prompts.js";
|
|
6
6
|
import { openBrowser } from "../utils/open.js";
|
|
7
|
+
import { emitError } from "../utils/errors.js";
|
|
7
8
|
async function getConnectionInfo(projectId, token) {
|
|
8
9
|
// reveal=true so the returned string is actually usable (the owner is asking).
|
|
9
10
|
const res = await api.get(`/v1/connection-info/${projectId}`, token, { reveal: "true" });
|
|
@@ -29,20 +30,39 @@ function formatDate(iso) {
|
|
|
29
30
|
const d = new Date(iso);
|
|
30
31
|
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a project's primary branch (the one to run a query against). Lists
|
|
35
|
+
* branches the same way `db branches` does (GET /v1/projects/:id) and returns
|
|
36
|
+
* the primary, falling back to the first branch. Returns null if the project
|
|
37
|
+
* has no branches or the lookup failed.
|
|
38
|
+
*/
|
|
39
|
+
async function getPrimaryBranch(projectId, token, teamId) {
|
|
40
|
+
const query = {};
|
|
41
|
+
if (teamId)
|
|
42
|
+
query.team_id = teamId;
|
|
43
|
+
const res = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return null;
|
|
46
|
+
const list = res.data.branches ?? [];
|
|
47
|
+
return list.find((b) => b.is_primary) ?? list[0] ?? null;
|
|
48
|
+
}
|
|
32
49
|
export async function connect() {
|
|
50
|
+
// psql is an interactive session — there's no headless equivalent. Don't spawn
|
|
51
|
+
// it in --json or non-TTY contexts; point agents at the headless surfaces.
|
|
52
|
+
if (isJsonMode() || !process.stdin.isTTY) {
|
|
53
|
+
emitError("INTERACTIVE_ONLY", "bata db 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.");
|
|
54
|
+
}
|
|
33
55
|
const token = requireToken();
|
|
34
56
|
const config = loadConfig();
|
|
35
57
|
const projectId = config.defaultProject;
|
|
36
58
|
if (!projectId) {
|
|
37
|
-
|
|
38
|
-
process.exit(1);
|
|
59
|
+
emitError("NO_PROJECT", "No default project.", "Run bata projects create or set one with bata projects info <id>.");
|
|
39
60
|
}
|
|
40
61
|
const s = spinner("Fetching connection info");
|
|
41
62
|
const conn = await getConnectionInfo(projectId, token);
|
|
42
63
|
s.stop();
|
|
43
64
|
if (!conn || !conn.connection_uri) {
|
|
44
|
-
|
|
45
|
-
process.exit(1);
|
|
65
|
+
emitError("NOT_FOUND", "Could not fetch connection string for this project.", "");
|
|
46
66
|
}
|
|
47
67
|
log();
|
|
48
68
|
logInfo(`Connecting to project ${colors.cyan(projectId)}`);
|
|
@@ -70,17 +90,11 @@ export async function url() {
|
|
|
70
90
|
const config = loadConfig();
|
|
71
91
|
const projectId = config.defaultProject;
|
|
72
92
|
if (!projectId) {
|
|
73
|
-
|
|
74
|
-
process.exit(1);
|
|
93
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
75
94
|
}
|
|
76
95
|
const conn = await getConnectionInfo(projectId, token);
|
|
77
96
|
if (!conn || !conn.connection_uri) {
|
|
78
|
-
|
|
79
|
-
if (isJsonMode())
|
|
80
|
-
json({ error: msg });
|
|
81
|
-
else
|
|
82
|
-
error(msg);
|
|
83
|
-
process.exit(1);
|
|
97
|
+
emitError("NOT_FOUND", "Could not fetch connection string.", "");
|
|
84
98
|
}
|
|
85
99
|
if (isJsonMode()) {
|
|
86
100
|
json({ direct: conn.connection_uri, pooled: conn.pooled_uri ?? null });
|
|
@@ -95,12 +109,7 @@ export async function branches() {
|
|
|
95
109
|
const jsonMode = isJsonMode();
|
|
96
110
|
const projectId = config.defaultProject;
|
|
97
111
|
if (!projectId) {
|
|
98
|
-
|
|
99
|
-
if (jsonMode)
|
|
100
|
-
json({ error: msg });
|
|
101
|
-
else
|
|
102
|
-
error(msg);
|
|
103
|
-
process.exit(1);
|
|
112
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
104
113
|
}
|
|
105
114
|
const s = jsonMode ? null : spinner("Fetching branches");
|
|
106
115
|
const query = {};
|
|
@@ -109,11 +118,10 @@ export async function branches() {
|
|
|
109
118
|
const res = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
110
119
|
s?.stop();
|
|
111
120
|
if (!res.ok) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
process.exit(1);
|
|
121
|
+
emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
122
|
+
: res.status === 404 ? "NO_PROJECT"
|
|
123
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
124
|
+
: "CLI_ERROR", apiError(res, "Failed to fetch branches"), "");
|
|
117
125
|
}
|
|
118
126
|
const branchList = res.data.branches || [];
|
|
119
127
|
if (jsonMode) {
|
|
@@ -146,19 +154,25 @@ export async function branches() {
|
|
|
146
154
|
log();
|
|
147
155
|
}
|
|
148
156
|
export async function branchCreate(name) {
|
|
157
|
+
const jsonMode = isJsonMode();
|
|
149
158
|
const token = requireToken();
|
|
150
159
|
const config = loadConfig();
|
|
151
160
|
const projectId = config.defaultProject;
|
|
152
161
|
if (!projectId) {
|
|
153
|
-
|
|
154
|
-
process.exit(1);
|
|
162
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
155
163
|
}
|
|
156
|
-
|
|
164
|
+
// Don't block on an interactive prompt headlessly.
|
|
165
|
+
let branchName = name;
|
|
157
166
|
if (!branchName) {
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
if (jsonMode || !process.stdin.isTTY) {
|
|
168
|
+
emitError("MISSING_ARG", "Branch name is required.", "Usage: bata db branch create <name>");
|
|
169
|
+
}
|
|
170
|
+
branchName = await prompt("Branch name");
|
|
171
|
+
}
|
|
172
|
+
if (!branchName) {
|
|
173
|
+
emitError("MISSING_ARG", "Branch name is required.", "Usage: bata db branch create <name>");
|
|
160
174
|
}
|
|
161
|
-
const s = spinner(`Creating branch ${colors.cyan(branchName)}`);
|
|
175
|
+
const s = jsonMode ? null : spinner(`Creating branch ${colors.cyan(branchName)}`);
|
|
162
176
|
const body = {
|
|
163
177
|
name: branchName,
|
|
164
178
|
project_id: projectId,
|
|
@@ -166,54 +180,66 @@ export async function branchCreate(name) {
|
|
|
166
180
|
if (config.defaultTeam)
|
|
167
181
|
body.team_id = config.defaultTeam;
|
|
168
182
|
const res = await api.post("/v1/branches", body, token);
|
|
169
|
-
s
|
|
183
|
+
s?.stop();
|
|
170
184
|
if (!res.ok) {
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
emitError(res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(res, "Failed to create branch"), "");
|
|
186
|
+
}
|
|
187
|
+
if (jsonMode) {
|
|
188
|
+
json({
|
|
189
|
+
branch: { id: res.data.id, name: res.data.name ?? branchName, project_id: projectId },
|
|
190
|
+
// The branch row exists immediately, but its compute may still be
|
|
191
|
+
// provisioning — poll `db branches` for status before connecting.
|
|
192
|
+
ready: false,
|
|
193
|
+
poll: "bata db branches --json",
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
173
196
|
}
|
|
174
197
|
log();
|
|
175
198
|
log(` ${colors.green(">")} Branch ${colors.cyan(branchName)} created`);
|
|
199
|
+
log(` ${colors.dim("Poll readiness with")} ${colors.cyan("bata db branches")}`);
|
|
176
200
|
log();
|
|
177
201
|
}
|
|
178
202
|
export async function branchDelete(name) {
|
|
203
|
+
const jsonMode = isJsonMode();
|
|
179
204
|
const token = requireToken();
|
|
180
205
|
const config = loadConfig();
|
|
181
206
|
const projectId = config.defaultProject;
|
|
182
207
|
if (!projectId) {
|
|
183
|
-
|
|
184
|
-
process.exit(1);
|
|
208
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
185
209
|
}
|
|
186
210
|
if (!name) {
|
|
187
|
-
|
|
188
|
-
process.exit(1);
|
|
211
|
+
emitError("MISSING_ARG", "Branch name is required.", "Usage: bata db branch delete <name>");
|
|
189
212
|
}
|
|
190
|
-
|
|
213
|
+
// Skip the prompt headlessly (--yes / --json / no TTY) so agents and CI can
|
|
214
|
+
// actually delete — and never report success without deleting.
|
|
215
|
+
const ok = await confirmDestructive(`Delete branch ${colors.cyan(name)}?`);
|
|
191
216
|
if (!ok) {
|
|
192
217
|
log(" Aborted.");
|
|
193
218
|
return;
|
|
194
219
|
}
|
|
195
|
-
const s = spinner(`Deleting branch ${name}`);
|
|
220
|
+
const s = jsonMode ? null : spinner(`Deleting branch ${name}`);
|
|
196
221
|
// We need to find the branch ID first
|
|
197
222
|
const query = {};
|
|
198
223
|
if (config.defaultTeam)
|
|
199
224
|
query.team_id = config.defaultTeam;
|
|
200
225
|
const projRes = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
201
226
|
if (!projRes.ok || !projRes.data.branches) {
|
|
202
|
-
s
|
|
203
|
-
|
|
204
|
-
process.exit(1);
|
|
227
|
+
s?.stop();
|
|
228
|
+
emitError(projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(projRes, "Failed to fetch branches."), "");
|
|
205
229
|
}
|
|
206
230
|
const branch = projRes.data.branches.find((b) => b.name === name);
|
|
207
231
|
if (!branch) {
|
|
208
|
-
s
|
|
209
|
-
|
|
210
|
-
process.exit(1);
|
|
232
|
+
s?.stop();
|
|
233
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${name}" not found.`, "List branches with: bata db branches --json");
|
|
211
234
|
}
|
|
212
235
|
const res = await api.del(`/v1/branches/${branch.id}`, token, query);
|
|
213
|
-
s
|
|
236
|
+
s?.stop();
|
|
214
237
|
if (!res.ok) {
|
|
215
|
-
|
|
216
|
-
|
|
238
|
+
emitError(res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(res, "Failed to delete branch."), "");
|
|
239
|
+
}
|
|
240
|
+
if (jsonMode) {
|
|
241
|
+
json({ branch: { id: branch.id, name, project_id: projectId }, deleted: true });
|
|
242
|
+
return;
|
|
217
243
|
}
|
|
218
244
|
log();
|
|
219
245
|
log(` ${colors.green(">")} Branch ${colors.cyan(name)} deleted`);
|
|
@@ -234,55 +260,76 @@ export async function studio() {
|
|
|
234
260
|
export async function query(sql) {
|
|
235
261
|
const jsonMode = isJsonMode();
|
|
236
262
|
if (!sql) {
|
|
237
|
-
|
|
238
|
-
if (jsonMode)
|
|
239
|
-
json({ error: msg });
|
|
240
|
-
else
|
|
241
|
-
error(msg);
|
|
242
|
-
process.exit(1);
|
|
263
|
+
emitError("MISSING_ARG", "SQL query is required.", 'Usage: bata db query "SELECT 1"');
|
|
243
264
|
}
|
|
244
265
|
const token = requireToken();
|
|
245
266
|
const config = loadConfig();
|
|
246
267
|
const projectId = config.defaultProject;
|
|
247
268
|
if (!projectId) {
|
|
248
|
-
|
|
249
|
-
if (jsonMode)
|
|
250
|
-
json({ error: msg });
|
|
251
|
-
else
|
|
252
|
-
error(msg);
|
|
253
|
-
process.exit(1);
|
|
269
|
+
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
254
270
|
}
|
|
255
271
|
const s = jsonMode ? null : spinner("Running query");
|
|
256
|
-
|
|
272
|
+
// Resolve the primary branch — /v1/sql/execute is keyed on branch_id, not
|
|
273
|
+
// project. (The old code POSTed to /v1/query/:projectId, which 404s.)
|
|
274
|
+
const branch = await getPrimaryBranch(projectId, token, config.defaultTeam);
|
|
275
|
+
if (!branch) {
|
|
276
|
+
s?.stop();
|
|
277
|
+
emitError("BRANCH_NOT_FOUND", "No branch found for this project to run the query against.", "Check the project with: bata db branches --json");
|
|
278
|
+
}
|
|
279
|
+
const res = await api.post("/v1/sql/execute", { branch_id: branch.id, query: sql }, token);
|
|
257
280
|
s?.stop();
|
|
258
281
|
if (!res.ok) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
process.exit(1);
|
|
282
|
+
const code = res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
283
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
284
|
+
: "CLI_ERROR";
|
|
285
|
+
emitError(code, apiError(res, "Query failed"), "");
|
|
264
286
|
}
|
|
265
|
-
|
|
287
|
+
// The endpoint returns 200 even when the SQL itself errored (it executed and
|
|
288
|
+
// failed). Treat that as a real error, not a silent success.
|
|
289
|
+
if (res.data.error) {
|
|
290
|
+
emitError("CLI_ERROR", res.data.error, "");
|
|
291
|
+
}
|
|
292
|
+
const columns = res.data.columns ?? [];
|
|
293
|
+
const rows = res.data.rows ?? [];
|
|
294
|
+
const rowCount = res.data.rowCount ?? rows.length;
|
|
266
295
|
if (jsonMode) {
|
|
267
296
|
json({
|
|
268
|
-
columns
|
|
269
|
-
rows
|
|
270
|
-
row_count: rowCount
|
|
297
|
+
columns,
|
|
298
|
+
rows, // column-keyed objects, exactly as the server returned them
|
|
299
|
+
row_count: rowCount,
|
|
300
|
+
command: res.data.command ?? null,
|
|
271
301
|
});
|
|
272
302
|
return;
|
|
273
303
|
}
|
|
274
|
-
if (columns && rows) {
|
|
304
|
+
if (columns.length && rows.length) {
|
|
275
305
|
log();
|
|
276
|
-
table(columns.map((c) => c.toUpperCase()), rows.map((r) =>
|
|
306
|
+
table(columns.map((c) => c.toUpperCase()), rows.map((r) => columns.map((col) => fmtCell(r[col]))));
|
|
277
307
|
log();
|
|
278
|
-
log(` ${colors.dim(`${rowCount
|
|
308
|
+
log(` ${colors.dim(`${rowCount} row(s)`)}`);
|
|
309
|
+
}
|
|
310
|
+
else if (rows.length) {
|
|
311
|
+
// Rows with no field metadata (rare) — render whatever keys came back.
|
|
312
|
+
const keys = Object.keys(rows[0]);
|
|
313
|
+
log();
|
|
314
|
+
table(keys.map((k) => k.toUpperCase()), rows.map((r) => keys.map((k) => fmtCell(r[k]))));
|
|
315
|
+
log();
|
|
316
|
+
log(` ${colors.dim(`${rowCount} row(s)`)}`);
|
|
279
317
|
}
|
|
280
318
|
else {
|
|
281
319
|
log();
|
|
282
|
-
|
|
320
|
+
const cmd = res.data.command ? `${res.data.command} ` : "";
|
|
321
|
+
log(` ${colors.dim(`${cmd}OK — ${rowCount} row(s) affected.`)}`);
|
|
283
322
|
}
|
|
284
323
|
log();
|
|
285
324
|
}
|
|
325
|
+
/** Render a column-keyed value for the table (null → empty, objects → JSON). */
|
|
326
|
+
function fmtCell(v) {
|
|
327
|
+
if (v === null || v === undefined)
|
|
328
|
+
return "";
|
|
329
|
+
if (typeof v === "object")
|
|
330
|
+
return JSON.stringify(v);
|
|
331
|
+
return String(v);
|
|
332
|
+
}
|
|
286
333
|
export async function handleDb(args) {
|
|
287
334
|
const sub = args[0];
|
|
288
335
|
switch (sub) {
|
|
@@ -298,19 +345,13 @@ export async function handleDb(args) {
|
|
|
298
345
|
return branchCreate(args[2]);
|
|
299
346
|
if (action === "delete")
|
|
300
347
|
return branchDelete(args[2]);
|
|
301
|
-
|
|
302
|
-
log(` ${colors.dim("Available:")} create, delete`);
|
|
303
|
-
process.exit(1);
|
|
304
|
-
break;
|
|
348
|
+
emitError("INVALID_FLAG", `Unknown: db branch ${action || ""}`, "Available: create, delete");
|
|
305
349
|
}
|
|
306
350
|
case "studio":
|
|
307
351
|
return studio();
|
|
308
352
|
case "query":
|
|
309
353
|
return query(args.slice(1).join(" "));
|
|
310
354
|
default:
|
|
311
|
-
|
|
312
|
-
log(` ${colors.dim("Available:")} connect, url, branches, branch, studio, query`);
|
|
313
|
-
process.exit(1);
|
|
355
|
+
emitError("INVALID_FLAG", `Unknown subcommand: db ${sub || ""}`, "Available: connect, url, branches, branch, studio, query");
|
|
314
356
|
}
|
|
315
357
|
}
|
|
316
|
-
//# sourceMappingURL=db.js.map
|
package/dist/commands/dev.js
CHANGED
package/dist/commands/migrate.js
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
|
-
import { colors, log
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { colors, log } from "../utils/logger.js";
|
|
2
|
+
import { emitError } from "../utils/errors.js";
|
|
3
|
+
const NOT_IMPLEMENTED_HINT = "Use `bata schema check <file.sql> --fail-on breaking` to gate migrations today.";
|
|
4
|
+
/**
|
|
5
|
+
* Unimplemented migrate subcommand. Never exits 0 for a no-op: emits the
|
|
6
|
+
* NOT_IMPLEMENTED envelope (exit 3) so agents/CI see a real failure, with a
|
|
7
|
+
* hint pointing at the migration-gating capability that exists today.
|
|
8
|
+
*/
|
|
9
|
+
function notImplemented(cmd) {
|
|
10
|
+
emitError("NOT_IMPLEMENTED", `Migrate ${cmd} is not yet implemented.`, NOT_IMPLEMENTED_HINT);
|
|
8
11
|
}
|
|
9
12
|
export async function handleMigrate(args) {
|
|
10
13
|
const sub = args[0];
|
|
11
14
|
switch (sub) {
|
|
12
15
|
case "create":
|
|
13
|
-
|
|
14
|
-
break;
|
|
16
|
+
notImplemented("create");
|
|
15
17
|
case "deploy":
|
|
16
|
-
|
|
17
|
-
break;
|
|
18
|
+
notImplemented("deploy");
|
|
18
19
|
case "status":
|
|
19
|
-
|
|
20
|
-
break;
|
|
20
|
+
notImplemented("status");
|
|
21
21
|
case "reset":
|
|
22
|
-
|
|
23
|
-
break;
|
|
22
|
+
notImplemented("reset");
|
|
24
23
|
default:
|
|
25
24
|
log();
|
|
26
25
|
log(` ${colors.bold("bata migrate")} ${colors.dim("— database migration management")}`);
|
|
27
26
|
log();
|
|
28
27
|
log(` ${colors.dim("Commands:")}`);
|
|
29
|
-
log(` ${colors.cyan("create")} Create a new migration`);
|
|
30
|
-
log(` ${colors.cyan("deploy")} Deploy pending migrations`);
|
|
31
|
-
log(` ${colors.cyan("status")} Show migration status`);
|
|
32
|
-
log(` ${colors.cyan("reset")} Reset database (
|
|
28
|
+
log(` ${colors.cyan("create")} Create a new migration ${colors.dim("(not implemented)")}`);
|
|
29
|
+
log(` ${colors.cyan("deploy")} Deploy pending migrations ${colors.dim("(not implemented)")}`);
|
|
30
|
+
log(` ${colors.cyan("status")} Show migration status ${colors.dim("(not implemented)")}`);
|
|
31
|
+
log(` ${colors.cyan("reset")} Reset database ${colors.dim("(not implemented)")}`);
|
|
33
32
|
log();
|
|
34
|
-
log(` ${colors.yellow("!")} ${colors.dim("
|
|
33
|
+
log(` ${colors.yellow("!")} ${colors.dim("Migration commands are not yet implemented. To gate a migration today:")}`);
|
|
34
|
+
log(` ${colors.cyan("bata schema check <file.sql> --fail-on breaking")}`);
|
|
35
35
|
log();
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
//# sourceMappingURL=migrate.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { api, apiError, resolveTeamId, asList } from "../api.js";
|
|
2
2
|
import { requireToken, loadConfig, saveConfig, isJsonMode } from "../config.js";
|
|
3
|
-
import { colors, log, json, success,
|
|
4
|
-
import { prompt,
|
|
3
|
+
import { colors, log, json, success, spinner, table, kvList, heading } from "../utils/logger.js";
|
|
4
|
+
import { prompt, confirmDestructive, select } from "../utils/prompts.js";
|
|
5
|
+
import { emitError } from "../utils/errors.js";
|
|
5
6
|
function projectCreatedAt(p) {
|
|
6
7
|
return p.created_at ?? p.createdAt ?? "";
|
|
7
8
|
}
|
|
@@ -38,18 +39,19 @@ export async function list() {
|
|
|
38
39
|
const config = loadConfig();
|
|
39
40
|
const jsonMode = isJsonMode();
|
|
40
41
|
const s = jsonMode ? null : spinner("Fetching projects");
|
|
42
|
+
// GET /v1/projects requires a team_id — resolve it identically to `create`
|
|
43
|
+
// (falls back to the user's first team) so the API-key path doesn't 400.
|
|
41
44
|
const teamId = await resolveTeamId(token);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (!teamId) {
|
|
46
|
+
s?.stop();
|
|
47
|
+
emitError("NO_TEAM", "No team found for this credential.", "This API key isn't attached to a team. Run `bata login` or check `bata whoami`.");
|
|
48
|
+
}
|
|
49
|
+
const res = await api.get("/v1/projects", token, { team_id: teamId });
|
|
46
50
|
if (!res.ok) {
|
|
47
51
|
s?.stop();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
error(apiError(res, "Failed to fetch projects"));
|
|
52
|
-
process.exit(1);
|
|
52
|
+
emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
53
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
54
|
+
: "CLI_ERROR", apiError(res, "Failed to fetch projects"), "");
|
|
53
55
|
}
|
|
54
56
|
s?.stop();
|
|
55
57
|
const projects = asList(res.data);
|
|
@@ -88,8 +90,7 @@ export async function create() {
|
|
|
88
90
|
heading("Create a new project");
|
|
89
91
|
const name = await prompt("Project name");
|
|
90
92
|
if (!name) {
|
|
91
|
-
|
|
92
|
-
process.exit(1);
|
|
93
|
+
emitError("MISSING_ARG", "Project name is required.", "");
|
|
93
94
|
}
|
|
94
95
|
const region = await select("Select a region", REGIONS);
|
|
95
96
|
const s = spinner("Creating project");
|
|
@@ -101,8 +102,9 @@ export async function create() {
|
|
|
101
102
|
const res = await api.post("/v1/projects", body, token);
|
|
102
103
|
if (!res.ok) {
|
|
103
104
|
s.stop();
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
106
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
107
|
+
: "CLI_ERROR", apiError(res, "Failed to create project"), "");
|
|
106
108
|
}
|
|
107
109
|
s.stop();
|
|
108
110
|
const project = res.data;
|
|
@@ -126,12 +128,7 @@ export async function info(projectId) {
|
|
|
126
128
|
const jsonMode = isJsonMode();
|
|
127
129
|
const id = projectId || config.defaultProject;
|
|
128
130
|
if (!id) {
|
|
129
|
-
|
|
130
|
-
if (jsonMode)
|
|
131
|
-
json({ error: msg });
|
|
132
|
-
else
|
|
133
|
-
error(msg);
|
|
134
|
-
process.exit(1);
|
|
131
|
+
emitError("NO_PROJECT", "No project specified.", "Pass a project ID or set a default with bata projects create.");
|
|
135
132
|
}
|
|
136
133
|
const s = jsonMode ? null : spinner("Fetching project details");
|
|
137
134
|
const teamId = await resolveTeamId(token);
|
|
@@ -141,11 +138,10 @@ export async function info(projectId) {
|
|
|
141
138
|
const res = await api.get(`/v1/projects/${id}`, token, query);
|
|
142
139
|
if (!res.ok) {
|
|
143
140
|
s?.stop();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
process.exit(1);
|
|
141
|
+
emitError(res.status === 404 ? "NOT_FOUND"
|
|
142
|
+
: res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
143
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
144
|
+
: "CLI_ERROR", apiError(res, "Project not found"), "");
|
|
149
145
|
}
|
|
150
146
|
// Also fetch connection info
|
|
151
147
|
const connRes = await api.get(`/v1/connection-info/${id}`, token);
|
|
@@ -204,36 +200,44 @@ export async function info(projectId) {
|
|
|
204
200
|
log();
|
|
205
201
|
}
|
|
206
202
|
export async function deleteProject(projectId) {
|
|
203
|
+
const jsonMode = isJsonMode();
|
|
207
204
|
const token = requireToken();
|
|
208
205
|
const config = loadConfig();
|
|
209
206
|
const id = projectId || config.defaultProject;
|
|
210
207
|
if (!id) {
|
|
211
|
-
|
|
212
|
-
process.exit(1);
|
|
208
|
+
emitError("NO_PROJECT", "No project specified.", "Pass a project ID: bata projects delete <id>");
|
|
213
209
|
}
|
|
214
|
-
|
|
210
|
+
// Skip the prompt headlessly (--yes / --json / no TTY) so agents and CI can
|
|
211
|
+
// actually delete — and never report success without deleting.
|
|
212
|
+
const ok = await confirmDestructive(`Delete project ${colors.cyan(id)}? This cannot be undone.`);
|
|
215
213
|
if (!ok) {
|
|
216
214
|
log();
|
|
217
215
|
log(" Aborted.");
|
|
218
216
|
log();
|
|
219
217
|
return;
|
|
220
218
|
}
|
|
221
|
-
const s = spinner("Deleting project");
|
|
219
|
+
const s = jsonMode ? null : spinner("Deleting project");
|
|
222
220
|
const teamId = await resolveTeamId(token);
|
|
223
221
|
const query = {};
|
|
224
222
|
if (teamId)
|
|
225
223
|
query.team_id = teamId;
|
|
226
224
|
const res = await api.del(`/v1/projects/${id}`, token, query);
|
|
227
225
|
if (!res.ok) {
|
|
228
|
-
s
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
s?.stop();
|
|
227
|
+
emitError(res.status === 404 ? "NOT_FOUND"
|
|
228
|
+
: res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
229
|
+
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
230
|
+
: "CLI_ERROR", apiError(res, "Failed to delete project"), "");
|
|
231
231
|
}
|
|
232
232
|
// Clear default project if it was this one
|
|
233
233
|
if (config.defaultProject === id) {
|
|
234
234
|
saveConfig({ defaultProject: undefined });
|
|
235
235
|
}
|
|
236
|
-
s
|
|
236
|
+
s?.stop();
|
|
237
|
+
if (jsonMode) {
|
|
238
|
+
json({ project: { id }, deleted: true });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
237
241
|
log();
|
|
238
242
|
success(`Project ${id} deleted.`);
|
|
239
243
|
log();
|
|
@@ -251,9 +255,6 @@ export async function handleProjects(args) {
|
|
|
251
255
|
case undefined:
|
|
252
256
|
return list();
|
|
253
257
|
default:
|
|
254
|
-
|
|
255
|
-
log(` ${colors.dim("Available:")} list, create, info, delete`);
|
|
256
|
-
process.exit(1);
|
|
258
|
+
emitError("INVALID_FLAG", `Unknown subcommand: projects ${sub}`, "Available: list, create, info, delete");
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
|
-
//# sourceMappingURL=projects.js.map
|