@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/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
- process.exit(1);
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
- const VERSION = "0.1.1";
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("Headless / agents")}`);
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
- log();
148
- log(` ${colors.red("Error:")} Unknown command ${colors.white(command)}`);
149
- log();
150
- log(` Run ${colors.cyan("bata --help")} to see available commands.`);
151
- log();
152
- process.exit(1);
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: "CLI_ERROR" }, null, 2));
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(1);
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
+ }
@@ -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.0")} ${colors.dim("— serverless Postgres platform")}`);
127
+ log(` ${colors.cyan(colors.bold("BataDB"))} ${colors.dim("v0.1.2")} ${colors.dim("— serverless Postgres platform")}`);
128
128
  log();
129
129
  }
@@ -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;
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batadata/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for BataDB — serverless Postgres platform",
5
5
  "bin": {
6
6
  "bata": "./dist/index.js"