@batadata/cli 0.1.1 → 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 +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.js +123 -81
- package/dist/commands/migrate.js +20 -20
- package/dist/commands/projects.js +39 -37
- 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 +47 -11
- package/dist/utils/errors.d.ts +29 -0
- package/dist/utils/errors.js +65 -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 +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -13,9 +13,12 @@ interface RuntimeContext {
|
|
|
13
13
|
apiKey?: string;
|
|
14
14
|
apiUrl?: string;
|
|
15
15
|
json: boolean;
|
|
16
|
+
yes: boolean;
|
|
16
17
|
}
|
|
17
18
|
export declare function setRuntime(ctx: Partial<RuntimeContext>): void;
|
|
18
19
|
export declare function isJsonMode(): boolean;
|
|
20
|
+
/** Was `--yes` / `-y` passed? (Skips destructive confirmation prompts.) */
|
|
21
|
+
export declare function isYes(): boolean;
|
|
19
22
|
export declare function loadConfig(): BataConfig;
|
|
20
23
|
export declare function saveConfig(config: Partial<BataConfig>): void;
|
|
21
24
|
export declare function clearConfig(): void;
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
const CONFIG_PATH = path.join(os.homedir(), ".batarc");
|
|
5
5
|
const DEFAULT_API_URL = "https://api.batadata.com";
|
|
6
|
-
const runtime = { json: false };
|
|
6
|
+
const runtime = { json: false, yes: false };
|
|
7
7
|
export function setRuntime(ctx) {
|
|
8
8
|
if (ctx.apiKey !== undefined)
|
|
9
9
|
runtime.apiKey = ctx.apiKey;
|
|
@@ -11,10 +11,16 @@ export function setRuntime(ctx) {
|
|
|
11
11
|
runtime.apiUrl = ctx.apiUrl;
|
|
12
12
|
if (ctx.json !== undefined)
|
|
13
13
|
runtime.json = ctx.json;
|
|
14
|
+
if (ctx.yes !== undefined)
|
|
15
|
+
runtime.yes = ctx.yes;
|
|
14
16
|
}
|
|
15
17
|
export function isJsonMode() {
|
|
16
18
|
return runtime.json;
|
|
17
19
|
}
|
|
20
|
+
/** Was `--yes` / `-y` passed? (Skips destructive confirmation prompts.) */
|
|
21
|
+
export function isYes() {
|
|
22
|
+
return runtime.yes;
|
|
23
|
+
}
|
|
18
24
|
export function loadConfig() {
|
|
19
25
|
try {
|
|
20
26
|
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
@@ -70,7 +76,8 @@ export function requireToken() {
|
|
|
70
76
|
"\x1b[36m--api-key\x1b[0m, set \x1b[36mBATA_API_KEY\x1b[0m, or run " +
|
|
71
77
|
"\x1b[36mbata login\x1b[0m.");
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
// Exit 4 = auth/credentials per the documented exit-code contract.
|
|
80
|
+
process.exit(4);
|
|
74
81
|
}
|
|
75
82
|
return token;
|
|
76
83
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,12 @@ import { dev } from "./commands/dev.js";
|
|
|
10
10
|
import { create } from "./commands/create.js";
|
|
11
11
|
import { status } from "./commands/status.js";
|
|
12
12
|
import { connect } from "./commands/connect.js";
|
|
13
|
+
import { usage } from "./commands/usage.js";
|
|
13
14
|
import { parseGlobalFlags } from "./args.js";
|
|
14
15
|
import { isJsonMode } from "./config.js";
|
|
15
16
|
import { colors, log, banner } from "./utils/logger.js";
|
|
16
|
-
|
|
17
|
+
import { exitCodeFor } from "./utils/errors.js";
|
|
18
|
+
const VERSION = "0.1.2";
|
|
17
19
|
function help() {
|
|
18
20
|
banner();
|
|
19
21
|
log(` ${colors.bold("Usage")}`);
|
|
@@ -23,6 +25,7 @@ function help() {
|
|
|
23
25
|
log(` ${colors.cyan("create <name>")} Create a project and wait for it to be ready`);
|
|
24
26
|
log(` ${colors.cyan("connect <name>")} Open psql to a project (auto-wakes if suspended)`);
|
|
25
27
|
log(` ${colors.cyan("status")} Show all projects and their status`);
|
|
28
|
+
log(` ${colors.cyan("usage")} Per-dimension cost for the current period`);
|
|
26
29
|
log();
|
|
27
30
|
log(` ${colors.bold("Auth")}`);
|
|
28
31
|
log(` ${colors.cyan("login")} Log in to BataDB`);
|
|
@@ -63,9 +66,25 @@ function help() {
|
|
|
63
66
|
log(` ${colors.dim("--help, -h")} Show this help message`);
|
|
64
67
|
log(` ${colors.dim("--version, -v")} Show version`);
|
|
65
68
|
log();
|
|
66
|
-
log(` ${colors.bold("
|
|
69
|
+
log(` ${colors.bold("Agents")} ${colors.dim("(headless, machine-readable)")}`);
|
|
67
70
|
log(` ${colors.dim("Every command works with just")} ${colors.cyan("BATA_API_KEY")} ${colors.dim("set — no login needed.")}`);
|
|
68
|
-
log(` ${colors.dim("Mint a key with")} ${colors.cyan("bata api-keys create")}${colors.dim(".")}`);
|
|
71
|
+
log(` ${colors.dim("Mint a key with")} ${colors.cyan("bata api-keys create --json")}${colors.dim(".")}`);
|
|
72
|
+
log();
|
|
73
|
+
log(` ${colors.cyan("schema check <file.sql> --fail-on breaking")} Gate a migration (exit 2 if breaking)`);
|
|
74
|
+
log(` ${colors.cyan("db url --json")} Print connection string as JSON`);
|
|
75
|
+
log(` ${colors.cyan("db query <sql> --json")} Run SQL headlessly, rows as JSON objects`);
|
|
76
|
+
log(` ${colors.cyan("usage --json")} Per-dimension cost (honest: un-metered → null)`);
|
|
77
|
+
log();
|
|
78
|
+
log(` ${colors.dim("All errors in --json mode share one envelope:")} ${colors.dim('{ "error", "code", "hint" }')} ${colors.dim("on stderr.")}`);
|
|
79
|
+
log();
|
|
80
|
+
log(` ${colors.bold("Exit codes")}`);
|
|
81
|
+
log(` ${colors.dim("0")} success`);
|
|
82
|
+
log(` ${colors.dim("1")} generic error ${colors.dim("(CLI_ERROR)")}`);
|
|
83
|
+
log(` ${colors.dim("2")} gate tripped ${colors.dim("(schema check --fail-on)")}`);
|
|
84
|
+
log(` ${colors.dim("3")} not implemented ${colors.dim("(NOT_IMPLEMENTED)")}`);
|
|
85
|
+
log(` ${colors.dim("4")} auth / credentials ${colors.dim("(NO_CREDENTIALS, INVALID_KEY)")}`);
|
|
86
|
+
log(` ${colors.dim("5")} not-found / bad input ${colors.dim("(NO_PROJECT, BRANCH_NOT_FOUND, INVALID_FLAG, INTERACTIVE_ONLY)")}`);
|
|
87
|
+
log(` ${colors.dim("6")} upstream / transient ${colors.dim("(API_UNAVAILABLE, TIMEOUT — retryable)")}`);
|
|
69
88
|
log();
|
|
70
89
|
log(` ${colors.dim("Documentation:")} ${colors.cyan("https://www.npmjs.com/package/@batadata/cli")}`);
|
|
71
90
|
log();
|
|
@@ -99,6 +118,9 @@ async function main() {
|
|
|
99
118
|
case "connect":
|
|
100
119
|
await connect(rest);
|
|
101
120
|
break;
|
|
121
|
+
case "usage":
|
|
122
|
+
await usage(rest);
|
|
123
|
+
break;
|
|
102
124
|
// Auth
|
|
103
125
|
case "login":
|
|
104
126
|
await login();
|
|
@@ -144,25 +166,39 @@ async function main() {
|
|
|
144
166
|
await dev();
|
|
145
167
|
break;
|
|
146
168
|
default:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
if (isJsonMode()) {
|
|
170
|
+
log(JSON.stringify({
|
|
171
|
+
error: `Unknown command "${command}".`,
|
|
172
|
+
code: "INVALID_FLAG",
|
|
173
|
+
hint: "Run `bata --help` to see available commands.",
|
|
174
|
+
}, null, 2));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
log();
|
|
178
|
+
log(` ${colors.red("Error:")} Unknown command ${colors.white(command)}`);
|
|
179
|
+
log();
|
|
180
|
+
log(` Run ${colors.cyan("bata --help")} to see available commands.`);
|
|
181
|
+
log();
|
|
182
|
+
}
|
|
183
|
+
process.exit(exitCodeFor("INVALID_FLAG"));
|
|
153
184
|
}
|
|
154
185
|
}
|
|
155
186
|
catch (err) {
|
|
156
187
|
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
// Network/transport throws (e.g. the request timed out, DNS/connection
|
|
189
|
+
// refused) are upstream/transient — surface them with a retryable code so
|
|
190
|
+
// agents know to back off rather than treating it as a hard CLI bug.
|
|
191
|
+
const transient = /timed out|ECONNREFUSED|ENOTFOUND|ECONNRESET|EAI_AGAIN|socket hang up/i.test(message);
|
|
192
|
+
const code = transient ? (/timed out/i.test(message) ? "TIMEOUT" : "API_UNAVAILABLE") : "CLI_ERROR";
|
|
157
193
|
if (isJsonMode()) {
|
|
158
|
-
log(JSON.stringify({ error: message, code: "
|
|
194
|
+
log(JSON.stringify({ error: message, code, hint: "" }, null, 2));
|
|
159
195
|
}
|
|
160
196
|
else {
|
|
161
197
|
log();
|
|
162
198
|
log(` ${colors.red("Error:")} ${message}`);
|
|
163
199
|
log();
|
|
164
200
|
}
|
|
165
|
-
process.exit(
|
|
201
|
+
process.exit(exitCodeFor(code));
|
|
166
202
|
}
|
|
167
203
|
}
|
|
168
204
|
main();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error / exit-code contract for the whole CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every command emits ONE JSON error envelope in --json mode:
|
|
5
|
+
* { "error": string, "code": SCREAMING_SNAKE, "hint": string }
|
|
6
|
+
* Errors always go to stderr (via error()); the envelope is the only thing on
|
|
7
|
+
* stdout in --json mode, so machine consumers can parse stdout cleanly.
|
|
8
|
+
*
|
|
9
|
+
* ── Exit-code contract (documented + surfaced in `bata --help`) ──
|
|
10
|
+
* 0 success
|
|
11
|
+
* 1 generic CLI_ERROR (genuinely-unknown failure)
|
|
12
|
+
* 2 gate tripped (schema check --fail-on)
|
|
13
|
+
* 3 NOT_IMPLEMENTED (coming-soon command — never a silent no-op exit 0)
|
|
14
|
+
* 4 auth / creds — NO_CREDENTIALS, INVALID_KEY
|
|
15
|
+
* 5 not-found/bad-input — NO_PROJECT, BRANCH_NOT_FOUND, INVALID_FLAG, INTERACTIVE_ONLY, ...
|
|
16
|
+
* 6 upstream/transient — API_UNAVAILABLE, TIMEOUT (retryable)
|
|
17
|
+
*/
|
|
18
|
+
export type ErrorCode = "CLI_ERROR" | "NOT_IMPLEMENTED" | "NO_CREDENTIALS" | "INVALID_KEY" | "NO_PROJECT" | "BRANCH_NOT_FOUND" | "INVALID_FLAG" | "INTERACTIVE_ONLY" | "NO_TEAM" | "EMPTY_INPUT" | "FILE_NOT_FOUND" | "MISSING_ARG" | "NOT_FOUND" | "API_UNAVAILABLE" | "TIMEOUT" | "GATE_TRIPPED";
|
|
19
|
+
/** Map an error code to its documented process exit code. */
|
|
20
|
+
export declare function exitCodeFor(code: string): number;
|
|
21
|
+
/**
|
|
22
|
+
* Emit the single error envelope and exit with the code that matches `code`.
|
|
23
|
+
* In --json mode: `{ error, code, hint }` to stdout-as-JSON (errors stay clean).
|
|
24
|
+
* In human mode: a red "Error:" line to stderr plus a dim hint.
|
|
25
|
+
*
|
|
26
|
+
* Promoted from schema.ts so every command shares one contract. Pass an explicit
|
|
27
|
+
* `exitCode` only to override the table (rare — e.g. forcing 1 on an unknown).
|
|
28
|
+
*/
|
|
29
|
+
export declare function emitError(code: ErrorCode | string, message: string, hint?: string, exitCode?: number): never;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error / exit-code contract for the whole CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every command emits ONE JSON error envelope in --json mode:
|
|
5
|
+
* { "error": string, "code": SCREAMING_SNAKE, "hint": string }
|
|
6
|
+
* Errors always go to stderr (via error()); the envelope is the only thing on
|
|
7
|
+
* stdout in --json mode, so machine consumers can parse stdout cleanly.
|
|
8
|
+
*
|
|
9
|
+
* ── Exit-code contract (documented + surfaced in `bata --help`) ──
|
|
10
|
+
* 0 success
|
|
11
|
+
* 1 generic CLI_ERROR (genuinely-unknown failure)
|
|
12
|
+
* 2 gate tripped (schema check --fail-on)
|
|
13
|
+
* 3 NOT_IMPLEMENTED (coming-soon command — never a silent no-op exit 0)
|
|
14
|
+
* 4 auth / creds — NO_CREDENTIALS, INVALID_KEY
|
|
15
|
+
* 5 not-found/bad-input — NO_PROJECT, BRANCH_NOT_FOUND, INVALID_FLAG, INTERACTIVE_ONLY, ...
|
|
16
|
+
* 6 upstream/transient — API_UNAVAILABLE, TIMEOUT (retryable)
|
|
17
|
+
*/
|
|
18
|
+
import { isJsonMode } from "../config.js";
|
|
19
|
+
import { colors, log, json, error } from "./logger.js";
|
|
20
|
+
/** Map an error code to its documented process exit code. */
|
|
21
|
+
export function exitCodeFor(code) {
|
|
22
|
+
switch (code) {
|
|
23
|
+
case "NOT_IMPLEMENTED":
|
|
24
|
+
return 3;
|
|
25
|
+
case "NO_CREDENTIALS":
|
|
26
|
+
case "INVALID_KEY":
|
|
27
|
+
return 4;
|
|
28
|
+
case "NO_PROJECT":
|
|
29
|
+
case "BRANCH_NOT_FOUND":
|
|
30
|
+
case "INVALID_FLAG":
|
|
31
|
+
case "INTERACTIVE_ONLY":
|
|
32
|
+
case "NO_TEAM":
|
|
33
|
+
case "EMPTY_INPUT":
|
|
34
|
+
case "FILE_NOT_FOUND":
|
|
35
|
+
case "MISSING_ARG":
|
|
36
|
+
case "NOT_FOUND":
|
|
37
|
+
return 5;
|
|
38
|
+
case "API_UNAVAILABLE":
|
|
39
|
+
case "TIMEOUT":
|
|
40
|
+
return 6;
|
|
41
|
+
case "GATE_TRIPPED":
|
|
42
|
+
return 2;
|
|
43
|
+
default:
|
|
44
|
+
return 1; // CLI_ERROR / unknown
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Emit the single error envelope and exit with the code that matches `code`.
|
|
49
|
+
* In --json mode: `{ error, code, hint }` to stdout-as-JSON (errors stay clean).
|
|
50
|
+
* In human mode: a red "Error:" line to stderr plus a dim hint.
|
|
51
|
+
*
|
|
52
|
+
* Promoted from schema.ts so every command shares one contract. Pass an explicit
|
|
53
|
+
* `exitCode` only to override the table (rare — e.g. forcing 1 on an unknown).
|
|
54
|
+
*/
|
|
55
|
+
export function emitError(code, message, hint = "", exitCode) {
|
|
56
|
+
if (isJsonMode()) {
|
|
57
|
+
json({ error: message, code, hint });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
error(message);
|
|
61
|
+
if (hint)
|
|
62
|
+
log(` ${colors.dim(hint)}`);
|
|
63
|
+
}
|
|
64
|
+
process.exit(exitCode ?? exitCodeFor(code));
|
|
65
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -124,6 +124,6 @@ export function kvList(items) {
|
|
|
124
124
|
// Banner
|
|
125
125
|
export function banner() {
|
|
126
126
|
log();
|
|
127
|
-
log(` ${colors.cyan(colors.bold("BataDB"))} ${colors.dim("v0.1.
|
|
127
|
+
log(` ${colors.cyan(colors.bold("BataDB"))} ${colors.dim("v0.1.2")} ${colors.dim("— serverless Postgres platform")}`);
|
|
128
128
|
log();
|
|
129
129
|
}
|
package/dist/utils/prompts.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export declare function prompt(message: string, defaultValue?: string): Promise<string>;
|
|
2
2
|
export declare function promptSecret(message: string): Promise<string>;
|
|
3
3
|
export declare function confirm(message: string, defaultYes?: boolean): Promise<boolean>;
|
|
4
|
+
/**
|
|
5
|
+
* Confirmation for a destructive action that MUST proceed unattended.
|
|
6
|
+
*
|
|
7
|
+
* Returns true (proceed) without prompting whenever the caller can't answer:
|
|
8
|
+
* --yes / -y · --json mode · no TTY (piped / agent / CI).
|
|
9
|
+
* This is the one place the headless-skip rule lives so it can't drift between
|
|
10
|
+
* `db branch delete`, `projects delete`, and `api-keys revoke`. In an
|
|
11
|
+
* interactive TTY it asks `message` and defaults to NO.
|
|
12
|
+
*/
|
|
13
|
+
export declare function confirmDestructive(message: string): Promise<boolean>;
|
|
4
14
|
export declare function select(message: string, options: {
|
|
5
15
|
label: string;
|
|
6
16
|
value: string;
|
package/dist/utils/prompts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { colors, log } from "./logger.js";
|
|
3
|
+
import { isJsonMode, isYes } from "../config.js";
|
|
3
4
|
function createInterface() {
|
|
4
5
|
return readline.createInterface({
|
|
5
6
|
input: process.stdin,
|
|
@@ -74,6 +75,20 @@ export async function confirm(message, defaultYes = true) {
|
|
|
74
75
|
return defaultYes;
|
|
75
76
|
return answer.toLowerCase().startsWith("y");
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Confirmation for a destructive action that MUST proceed unattended.
|
|
80
|
+
*
|
|
81
|
+
* Returns true (proceed) without prompting whenever the caller can't answer:
|
|
82
|
+
* --yes / -y · --json mode · no TTY (piped / agent / CI).
|
|
83
|
+
* This is the one place the headless-skip rule lives so it can't drift between
|
|
84
|
+
* `db branch delete`, `projects delete`, and `api-keys revoke`. In an
|
|
85
|
+
* interactive TTY it asks `message` and defaults to NO.
|
|
86
|
+
*/
|
|
87
|
+
export async function confirmDestructive(message) {
|
|
88
|
+
if (isYes() || isJsonMode() || !process.stdin.isTTY)
|
|
89
|
+
return true;
|
|
90
|
+
return confirm(message, false);
|
|
91
|
+
}
|
|
77
92
|
export async function select(message, options) {
|
|
78
93
|
log(` ${colors.cyan("?")} ${message}`);
|
|
79
94
|
log();
|