@rymelabs/rymedb-cli 0.0.0-stage → 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.js +732 -0
  3. package/package.json +32 -4
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # rymeDB CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Install the command-line operator tool:
4
+
5
+ ```sh
6
+ npm install --global @rymelabs/rymedb-cli
7
+ ```
8
+
9
+ Set `RYME_BASE` and `RYME_API_KEY`, then run `rymedb-cli ready` or
10
+ `rymedb-cli --help`. Full commands and setup are in the
11
+ [CLI documentation](https://db.rymelabs.dev/cli/).
package/dist/cli.js ADDED
@@ -0,0 +1,732 @@
1
+ #!/usr/bin/env node
2
+ const HELP = `rymedb-cli — rymeDB operator CLI
3
+
4
+ usage: rymedb-cli [--base URL] [--api-key KEY] <group> <action> [args] [flags]
5
+
6
+ groups:
7
+ health GET /health
8
+ ready GET /ready
9
+ metrics GET /metrics
10
+ observe slow [--limit N] [--table T] recent slow operations
11
+ traces [--limit N] [--name OP] [--table T]
12
+ recent request spans
13
+ kv get <table> <key> fetch a value
14
+ kv put <table> <key> <value> store a value (--stdin reads value from stdin)
15
+ [--ttl SECS]
16
+ kv delete <table> <key> delete a value
17
+ kv ttl <table> <key> remaining TTL
18
+ sql <statement...> POST /v1/sql
19
+ sql copy <table> <k=v...> bulk ingest via /v1/sql/copy
20
+ sql explain <statement...> planned access path
21
+ scan <table> [--limit N] GET /v1/scan/:table
22
+ rest list <table> [query] GET /rest/v1/:table
23
+ rest insert <table> <key> <value> POST /rest/v1/:table
24
+ rest delete <table> <key> DELETE /rest/v1/:table?key=eq.<key>
25
+ graphql <query...> POST /graphql
26
+ metering GET /v1/metering
27
+ autoscale [--cpu N] [--disk N] GET /v1/autoscale
28
+ qos [tier <tenant> <tier>] QoS snapshot or tier set
29
+ auth register <id> <password> register password credential
30
+ auth verify <id> <password> verify password credential
31
+ auth token <id> <password> [--code C]
32
+ issue a usable API key
33
+ auth revoke <key> revoke an API key by value
34
+ auth passkey-register <user> <cred-id> <pubkey-b64>
35
+ enroll a P-256 credential
36
+ auth passkey-verify <user> <cred-id> <auth-data-b64> <client-data-b64> <sig-b64>
37
+ verify an assertion, mint a key
38
+ auth mask <table> <field...> mask JSON fields on reads
39
+ auth oidc-login <uri> [--state S] build OIDC authorization URL
40
+ auth oidc-token <id-token> verify OIDC ID token
41
+ presence join <channel> <member> join presence channel
42
+ presence list <channel> list live members
43
+ topics append <part> <key> <value> append durable topic message
44
+ topics read <part> [--from N] read durable topic from cursor
45
+ vector upsert <table> <id> <v,...> upsert dense vector (exact cosine)
46
+ vector search <table> <v,...> nearest vectors [--top-k N]
47
+ vector ann-search <table> <v,...> HNSW approximate [--top-k N] [--ef N]
48
+ vector delete <table> <id> delete a vector
49
+ text index <table> <id> <text...> index a document (TF-IDF)
50
+ text search <table> <query...> ranked full-text search [--top-k N]
51
+ text delete <table> <id> delete a document
52
+ index stats index partitions and namespaces
53
+ branch create <id> <parent> <ts> create a branch at a commit timestamp
54
+ branch list list all branches
55
+ branch get <id> describe a branch
56
+ branch delete <id> delete a branch
57
+ branch reset <id> <ts> move a branch base commit
58
+ branch promote <id> swap a child branch into its parent
59
+ branch diff <id> <against> segment sets unique to each side
60
+ billing per-tenant usage rollup
61
+ billing invoice [tenant] priced micro-USD invoice
62
+ migrate supabase <dump-file> analyze Supabase dump to tables+policies
63
+ migrate neon <branches-json-file> map Neon branches to rymeDB plans
64
+ migrate apply <id> <sql> [--author A]
65
+ execute SQL and record it in the ledger
66
+ migrate ledger list recorded migrations
67
+ regions region placement and follower status
68
+ backup checkpoint [--manifest ID] write a PITR checkpoint
69
+ backup latest newest checkpoint manifest
70
+ backup pitr <target> query checkpoints covering a timestamp
71
+ backup restore <target> restore to a timestamp (single/sharded only)
72
+ backup archive [--backup ID] archive latest backup now
73
+ backup archives list archives
74
+ backup verify <backup-id> restore drill: sha + envelope decrypt check
75
+ backup drill latest scheduled drill report
76
+ backup copy <backup-id> copy an archive to the replica target
77
+ snapshot create snapshot backend files
78
+ shards layout current shard layout
79
+ shards move <table> [--target N] move a table between shards
80
+ ranges [list] list placement ranges
81
+ ranges route <key> range owning a key
82
+ ranges split <id> <mid> <left> <right> <epoch>
83
+ split a range (epoch-fenced)
84
+ ranges merge <left> <right> <merged> <lepoch> <repoch>
85
+ merge adjacent ranges (epoch-fenced)
86
+ ranges autosplit [min-writes] split hot ranges (threshold override)
87
+ ranges loads per-range counted write load
88
+ cluster members raft membership, term, commit
89
+ cluster add <id> <addr> admit a learner/catch-up node
90
+ cluster remove <id> remove a member
91
+ cluster transfer <target> hand leadership to a member
92
+ cluster replace <id=addr...> swap the member set via joint consensus
93
+ stream watch <table> [--count N] print live change records for a table
94
+ stream query <table> [--limit N] print live query snapshots and updates
95
+ stream broadcast <channel> [--count N]
96
+ print live broadcast messages
97
+ [--count N]
98
+
99
+ flags:
100
+ --base URL server base (default http://127.0.0.1:8080)
101
+ --api-key KEY bearer key (default $RYME_API_KEY)
102
+ --stdin read kv value from stdin
103
+ --ttl SECS expiry on kv put
104
+ --limit N scan row limit
105
+ --target N shard move target
106
+ --manifest ID checkpoint manifest id
107
+ --backup ID archive source backup id
108
+ --count N exit after N stream messages (default: run until interrupted)
109
+ `;
110
+ function parseArgs(argv) {
111
+ const flags = new Map();
112
+ const rest = [];
113
+ let base = process.env["RYME_BASE"] ?? "http://127.0.0.1:8080";
114
+ let apiKey = process.env["RYME_API_KEY"] ?? "";
115
+ for (let i = 0; i < argv.length; i++) {
116
+ const arg = argv[i];
117
+ if (arg === "--base" || arg === "--api-key") {
118
+ const value = argv[++i];
119
+ if (value === undefined)
120
+ fail(`missing value for ${arg}`);
121
+ if (arg === "--base")
122
+ base = value;
123
+ else
124
+ apiKey = value;
125
+ }
126
+ else if (arg.startsWith("--")) {
127
+ const eq = arg.indexOf("=");
128
+ if (eq > 0)
129
+ flags.set(arg.slice(2, eq), arg.slice(eq + 1));
130
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith("--"))
131
+ flags.set(arg.slice(2), argv[++i]);
132
+ else
133
+ flags.set(arg.slice(2), true);
134
+ }
135
+ else {
136
+ rest.push(arg);
137
+ }
138
+ }
139
+ return { base, apiKey, rest, flags };
140
+ }
141
+ function fail(message) {
142
+ console.error(`error: ${message}`);
143
+ process.exit(1);
144
+ }
145
+ function need(args, count, usage) {
146
+ if (args.length < count)
147
+ fail(`usage: ${usage}`);
148
+ }
149
+ function flag(options, name) {
150
+ const value = options.flags.get(name);
151
+ return typeof value === "string" ? value : undefined;
152
+ }
153
+ async function readStdin() {
154
+ let data = "";
155
+ process.stdin.setEncoding("utf8");
156
+ for await (const chunk of process.stdin)
157
+ data += chunk;
158
+ return data;
159
+ }
160
+ async function readFile(path) {
161
+ const fs = await import("node:fs/promises");
162
+ try {
163
+ return await fs.readFile(path, "utf8");
164
+ }
165
+ catch {
166
+ fail(`cannot read file ${path}`);
167
+ }
168
+ }
169
+ async function call(options, method, path, body) {
170
+ const headers = {};
171
+ if (options.apiKey)
172
+ headers["authorization"] = `Bearer ${options.apiKey}`;
173
+ if (body !== undefined)
174
+ headers["content-type"] = "application/json";
175
+ let response;
176
+ try {
177
+ response = await fetch(`${options.base}${path}`, { method, headers, body });
178
+ }
179
+ catch (error) {
180
+ fail(`request failed: ${error instanceof Error ? error.message : String(error)}`);
181
+ }
182
+ const text = await response.text();
183
+ if (!response.ok) {
184
+ console.error(`error: HTTP ${response.status} ${text}`);
185
+ process.exit(1);
186
+ }
187
+ try {
188
+ console.log(JSON.stringify(JSON.parse(text), null, 2));
189
+ }
190
+ catch {
191
+ process.stdout.write(text + (text.endsWith("\n") ? "" : "\n"));
192
+ }
193
+ }
194
+ async function main() {
195
+ const options = parseArgs(process.argv.slice(2));
196
+ const [group, action, ...args] = options.rest;
197
+ if (group === undefined || group === "--help" || group === "-h" || group === "help") {
198
+ console.log(HELP);
199
+ return;
200
+ }
201
+ switch (group) {
202
+ case "health":
203
+ case "ready":
204
+ case "metrics":
205
+ await call(options, "GET", `/${group}`);
206
+ return;
207
+ case "observe": {
208
+ if (action !== "slow" && action !== undefined)
209
+ fail("usage: rymedb-cli observe slow [--limit N] [--table T]");
210
+ const limit = flag(options, "limit");
211
+ const table = flag(options, "table");
212
+ const params = new URLSearchParams();
213
+ if (limit !== undefined)
214
+ params.set("limit", limit);
215
+ if (table !== undefined)
216
+ params.set("table", table);
217
+ const query = params.toString();
218
+ await call(options, "GET", `/v1/observe/slow${query ? `?${query}` : ""}`);
219
+ return;
220
+ }
221
+ case "traces": {
222
+ if (action !== undefined)
223
+ fail("usage: rymedb-cli traces [--limit N] [--name OP] [--table T]");
224
+ const limit = flag(options, "limit");
225
+ const name = flag(options, "name");
226
+ const table = flag(options, "table");
227
+ const params = new URLSearchParams();
228
+ if (limit !== undefined)
229
+ params.set("limit", limit);
230
+ if (name !== undefined)
231
+ params.set("name", name);
232
+ if (table !== undefined)
233
+ params.set("table", table);
234
+ const query = params.toString();
235
+ await call(options, "GET", `/v1/traces${query ? `?${query}` : ""}`);
236
+ return;
237
+ }
238
+ case "kv": {
239
+ need(args, 2, "rymedb-cli kv <get|put|delete|ttl> <table> <key> [value]");
240
+ const op = action;
241
+ const [table, key, ...rest] = args;
242
+ const query = flag(options, "ttl") !== undefined ? `?ttl=${flag(options, "ttl")}` : "";
243
+ if (op === "get")
244
+ await call(options, "GET", `/v1/kv/${table}/${key}`);
245
+ else if (op === "delete")
246
+ await call(options, "DELETE", `/v1/kv/${table}/${key}`);
247
+ else if (op === "ttl")
248
+ await call(options, "GET", `/v1/kv/${table}/${key}/ttl`);
249
+ else if (op === "put") {
250
+ let value = rest.join(" ");
251
+ if (options.flags.has("stdin"))
252
+ value = await readStdin();
253
+ else if (!value)
254
+ fail("usage: rymedb-cli kv put <table> <key> <value> [--stdin]");
255
+ await call(options, "PUT", `/v1/kv/${table}/${key}${query}`, value);
256
+ }
257
+ else
258
+ fail(`unknown kv op ${op}`);
259
+ return;
260
+ }
261
+ case "sql": {
262
+ if (action === "copy") {
263
+ need(args, 2, "rymedb-cli sql copy <table> <k=v...>");
264
+ const [table, ...pairs] = args;
265
+ const rows = pairs.map((pair) => {
266
+ const eq = pair.indexOf("=");
267
+ if (eq < 0)
268
+ fail(`bad pair ${pair}, want <k=v>`);
269
+ return { key: pair.slice(0, eq), value: pair.slice(eq + 1) };
270
+ });
271
+ await call(options, "POST", "/v1/sql/copy", JSON.stringify({ table, rows }));
272
+ return;
273
+ }
274
+ if (action === "explain") {
275
+ const statement = args.join(" ");
276
+ if (!statement)
277
+ fail("usage: rymedb-cli sql explain <statement...>");
278
+ await call(options, "POST", "/v1/sql/explain", JSON.stringify({ sql: statement }));
279
+ return;
280
+ }
281
+ const statement = [action, ...args].filter((part) => part !== undefined).join(" ");
282
+ if (!statement)
283
+ fail("usage: rymedb-cli sql <statement...>");
284
+ await call(options, "POST", "/v1/sql", JSON.stringify({ sql: statement }));
285
+ return;
286
+ }
287
+ case "rest": {
288
+ if (action === "list") {
289
+ need(args, 1, "rymedb-cli rest list <table> [query]");
290
+ const [table, query] = args;
291
+ await call(options, "GET", `/rest/v1/${table}${query ? `?${query}` : ""}`);
292
+ }
293
+ else if (action === "insert") {
294
+ need(args, 3, "rymedb-cli rest insert <table> <key> <value>");
295
+ await call(options, "POST", `/rest/v1/${args[0]}`, JSON.stringify({ key: args[1], value: args[2] }));
296
+ }
297
+ else if (action === "delete") {
298
+ need(args, 2, "rymedb-cli rest delete <table> <key>");
299
+ await call(options, "DELETE", `/rest/v1/${args[0]}?key=eq.${encodeURIComponent(args[1])}`);
300
+ }
301
+ else
302
+ fail("usage: rymedb-cli rest <list|insert|delete> ...");
303
+ return;
304
+ }
305
+ case "graphql": {
306
+ const query = [action, ...args].filter((part) => part !== undefined).join(" ");
307
+ if (!query)
308
+ fail("usage: rymedb-cli graphql <query...>");
309
+ await call(options, "POST", "/graphql", JSON.stringify({ query }));
310
+ return;
311
+ }
312
+ case "metering":
313
+ await call(options, "GET", "/v1/metering");
314
+ return;
315
+ case "qos": {
316
+ if (action === "tier") {
317
+ need(args, 2, "rymedb-cli qos tier <tenant> <shared|dedicated_shard|dedicated_cluster>");
318
+ await call(options, "POST", "/v1/qos/tier", JSON.stringify({ tenant: args[0], tier: args[1] }));
319
+ }
320
+ else {
321
+ await call(options, "GET", "/v1/qos");
322
+ }
323
+ return;
324
+ }
325
+ case "auth": {
326
+ if (action === "register") {
327
+ need(args, 2, "rymedb-cli auth register <id> <password>");
328
+ await call(options, "POST", "/v1/auth/register", JSON.stringify({ id: args[0], password: args[1] }));
329
+ }
330
+ else if (action === "verify") {
331
+ need(args, 2, "rymedb-cli auth verify <id> <password>");
332
+ await call(options, "POST", "/v1/auth/verify", JSON.stringify({ id: args[0], password: args[1] }));
333
+ }
334
+ else if (action === "token") {
335
+ need(args, 2, "rymedb-cli auth token <id> <password> [--code C]");
336
+ const code = flag(options, "code") ?? null;
337
+ await call(options, "POST", "/v1/auth/token", JSON.stringify({ id: args[0], password: args[1], code }));
338
+ }
339
+ else if (action === "revoke") {
340
+ need(args, 1, "rymedb-cli auth revoke <key>");
341
+ await call(options, "DELETE", "/v1/auth/keys", JSON.stringify({ key: args[0] }));
342
+ }
343
+ else if (action === "passkey-register") {
344
+ need(args, 3, "rymedb-cli auth passkey-register <user> <cred-id> <pubkey-b64>");
345
+ await call(options, "POST", "/v1/auth/passkey/register", JSON.stringify({ user: args[0], credential_id: args[1], public_key: args[2] }));
346
+ }
347
+ else if (action === "passkey-verify") {
348
+ need(args, 5, "rymedb-cli auth passkey-verify <user> <cred-id> <auth-data-b64> <client-data-b64> <sig-b64>");
349
+ await call(options, "POST", "/v1/auth/passkey/verify", JSON.stringify({
350
+ user: args[0],
351
+ credential_id: args[1],
352
+ authenticator_data: args[2],
353
+ client_data_json: args[3],
354
+ signature: args[4],
355
+ }));
356
+ }
357
+ else if (action === "mask") {
358
+ need(args, 2, "rymedb-cli auth mask <table> <field...>");
359
+ await call(options, "POST", "/v1/auth/mask", JSON.stringify({ table: args[0], fields: args.slice(1) }));
360
+ }
361
+ else if (action === "oidc-login") {
362
+ need(args, 1, "rymedb-cli auth oidc-login <redirect-uri> [--state S]");
363
+ await call(options, "POST", "/v1/auth/oidc/login", JSON.stringify({ redirect_uri: args[0], state: flag(options, "state") ?? null }));
364
+ }
365
+ else if (action === "oidc-token") {
366
+ need(args, 1, "rymedb-cli auth oidc-token <id-token>");
367
+ await call(options, "POST", "/v1/auth/oidc/token", JSON.stringify({ id_token: args[0] }));
368
+ }
369
+ else
370
+ fail("usage: rymedb-cli auth <register|verify|mask|oidc-login|oidc-token> ...");
371
+ return;
372
+ }
373
+ case "presence": {
374
+ if (action === "join") {
375
+ need(args, 2, "rymedb-cli presence join <channel> <member>");
376
+ await call(options, "POST", "/v1/presence/join", JSON.stringify({ channel: args[0], member: args[1] }));
377
+ }
378
+ else if (action === "list") {
379
+ need(args, 1, "rymedb-cli presence list <channel>");
380
+ await call(options, "GET", `/v1/presence/${encodeURIComponent(args[0])}`);
381
+ }
382
+ else
383
+ fail("usage: rymedb-cli presence <join|list> ...");
384
+ return;
385
+ }
386
+ case "topics": {
387
+ if (action === "append") {
388
+ need(args, 3, "rymedb-cli topics append <partition> <key> <value>");
389
+ await call(options, "POST", "/v1/topics/append", JSON.stringify({ partition: args[0], key: args[1], value: args[2] }));
390
+ }
391
+ else if (action === "read") {
392
+ need(args, 1, "rymedb-cli topics read <partition> [--from N]");
393
+ const from = flag(options, "from") ?? "0";
394
+ await call(options, "GET", `/v1/topics/read?partition=${encodeURIComponent(args[0])}&from=${from}&limit=100`);
395
+ }
396
+ else
397
+ fail("usage: rymedb-cli topics <append|read> ...");
398
+ return;
399
+ }
400
+ case "vector": {
401
+ if (action === "upsert") {
402
+ need(args, 3, "rymedb-cli vector upsert <table> <id> <v1,v2,...>");
403
+ const vector = args[2].split(",").map((v) => Number(v.trim()));
404
+ if (vector.some((v) => Number.isNaN(v)))
405
+ fail("vector must be comma-separated numbers");
406
+ await call(options, "POST", "/v1/vector/upsert", JSON.stringify({ table: args[0], id: args[1], vector }));
407
+ }
408
+ else if (action === "search") {
409
+ need(args, 2, "rymedb-cli vector search <table> <v1,v2,...> [--top-k N]");
410
+ const vector = args[1].split(",").map((v) => Number(v.trim()));
411
+ if (vector.some((v) => Number.isNaN(v)))
412
+ fail("vector must be comma-separated numbers");
413
+ const topK = Number(flag(options, "top-k") ?? "10");
414
+ await call(options, "POST", "/v1/vector/search", JSON.stringify({ table: args[0], vector, top_k: topK }));
415
+ }
416
+ else if (action === "delete") {
417
+ need(args, 2, "rymedb-cli vector delete <table> <id>");
418
+ await call(options, "DELETE", `/v1/vector/${args[0]}/${args[1]}`);
419
+ }
420
+ else if (action === "ann-search") {
421
+ need(args, 2, "rymedb-cli vector ann-search <table> <v1,v2,...> [--top-k N] [--ef N]");
422
+ const vector = args[1].split(",").map((v) => Number(v.trim()));
423
+ if (vector.some((v) => Number.isNaN(v)))
424
+ fail("vector must be comma-separated numbers");
425
+ const topK = Number(flag(options, "top-k") ?? "10");
426
+ const ef = Number(flag(options, "ef") ?? "64");
427
+ await call(options, "POST", "/v1/vector/ann-search", JSON.stringify({ table: args[0], vector, top_k: topK, ef }));
428
+ }
429
+ else
430
+ fail("usage: rymedb-cli vector <upsert|search|ann-search|delete> ...");
431
+ return;
432
+ }
433
+ case "text": {
434
+ if (action === "index") {
435
+ need(args, 3, "rymedb-cli text index <table> <id> <text...>");
436
+ await call(options, "POST", "/v1/text/index", JSON.stringify({ table: args[0], id: args[1], text: args.slice(2).join(" ") }));
437
+ }
438
+ else if (action === "search") {
439
+ need(args, 2, "rymedb-cli text search <table> <query...>");
440
+ const topK = Number(flag(options, "top-k") ?? "10");
441
+ await call(options, "POST", "/v1/text/search", JSON.stringify({ table: args[0], query: args.slice(1).join(" "), top_k: topK }));
442
+ }
443
+ else if (action === "delete") {
444
+ need(args, 2, "rymedb-cli text delete <table> <id>");
445
+ await call(options, "DELETE", `/v1/text/${args[0]}/${args[1]}`);
446
+ }
447
+ else
448
+ fail("usage: rymedb-cli text <index|search|delete> ...");
449
+ return;
450
+ }
451
+ case "index": {
452
+ if (action === "stats" || action === undefined) {
453
+ await call(options, "GET", "/v1/index/stats");
454
+ }
455
+ else
456
+ fail("usage: rymedb-cli index stats");
457
+ return;
458
+ }
459
+ case "autoscale": {
460
+ const params = [];
461
+ const cpu = flag(options, "cpu");
462
+ const disk = flag(options, "disk");
463
+ if (cpu !== undefined)
464
+ params.push(`cpu_pct=${cpu}`);
465
+ if (disk !== undefined)
466
+ params.push(`disk_used_pct=${disk}`);
467
+ await call(options, "GET", `/v1/autoscale${params.length ? `?${params.join("&")}` : ""}`);
468
+ return;
469
+ }
470
+ case "scan": {
471
+ if (!action)
472
+ fail("usage: rymedb-cli scan <table> [--limit N]");
473
+ const limit = flag(options, "limit");
474
+ await call(options, "GET", `/v1/scan/${action}${limit !== undefined ? `?limit=${limit}` : ""}`);
475
+ return;
476
+ }
477
+ case "branch": {
478
+ if (action === "create") {
479
+ need(args, 3, "rymedb-cli branch create <id> <parent> <base_commit_ts>");
480
+ const [id, parent, ts] = args;
481
+ await call(options, "POST", "/v1/branches", JSON.stringify({ id, parent, base_commit_ts: Number(ts) }));
482
+ }
483
+ else if (action === "list" || action === undefined) {
484
+ await call(options, "GET", "/v1/branches");
485
+ }
486
+ else if (action === "get" || action === "delete") {
487
+ need(args, 1, `rymedb-cli branch ${action} <id>`);
488
+ await call(options, action === "get" ? "GET" : "DELETE", `/v1/branches/${args[0]}`);
489
+ }
490
+ else if (action === "reset") {
491
+ need(args, 2, "rymedb-cli branch reset <id> <base_commit_ts>");
492
+ await call(options, "POST", `/v1/branches/${args[0]}/reset`, JSON.stringify({ base_commit_ts: Number(args[1]) }));
493
+ }
494
+ else if (action === "promote") {
495
+ need(args, 1, "rymedb-cli branch promote <id>");
496
+ await call(options, "POST", `/v1/branches/${args[0]}/promote`);
497
+ }
498
+ else if (action === "diff") {
499
+ need(args, 2, "rymedb-cli branch diff <id> <against>");
500
+ await call(options, "GET", `/v1/branches/${args[0]}/diff?against=${encodeURIComponent(args[1])}`);
501
+ }
502
+ else
503
+ fail("usage: rymedb-cli branch <create|list|get|delete|reset|promote|diff> ...");
504
+ return;
505
+ }
506
+ case "billing":
507
+ if (action === "invoice") {
508
+ const tenant = args[0];
509
+ await call(options, "GET", `/v1/billing/invoice${tenant ? `?tenant=${encodeURIComponent(tenant)}` : ""}`);
510
+ }
511
+ else {
512
+ await call(options, "GET", "/v1/billing/summary");
513
+ }
514
+ return;
515
+ case "migrate": {
516
+ if (action === "supabase") {
517
+ need(args, 1, "rymedb-cli migrate supabase <dump-file>");
518
+ const dump = await readFile(args[0]);
519
+ await call(options, "POST", "/v1/migrate/supabase", JSON.stringify({ dump }));
520
+ }
521
+ else if (action === "neon") {
522
+ need(args, 1, "rymedb-cli migrate neon <branches-json-file>");
523
+ const branches = await readFile(args[0]);
524
+ await call(options, "POST", "/v1/migrate/neon", JSON.stringify({ branches }));
525
+ }
526
+ else if (action === "apply") {
527
+ need(args, 2, "rymedb-cli migrate apply <id> <sql> [--author A]");
528
+ const author = flag(options, "author") ?? null;
529
+ await call(options, "POST", "/v1/migrate/apply", JSON.stringify({ id: args[0], sql: args[1], author }));
530
+ }
531
+ else if (action === "ledger") {
532
+ await call(options, "GET", "/v1/migrate/ledger");
533
+ }
534
+ else
535
+ fail("usage: rymedb-cli migrate <supabase|neon|apply|ledger> ...");
536
+ return;
537
+ }
538
+ case "regions":
539
+ await call(options, "GET", "/v1/regions");
540
+ return;
541
+ case "backup": {
542
+ if (action === "checkpoint") {
543
+ const manifest = flag(options, "manifest");
544
+ await call(options, "POST", "/v1/backups/checkpoint", JSON.stringify({ manifest_id: manifest ?? null }));
545
+ }
546
+ else if (action === "latest") {
547
+ await call(options, "GET", "/v1/backups/latest");
548
+ }
549
+ else if (action === "pitr") {
550
+ need(args, 1, "rymedb-cli backup pitr <target>");
551
+ await call(options, "GET", `/v1/backups/pitr?target=${args[0]}`);
552
+ }
553
+ else if (action === "restore") {
554
+ need(args, 1, "rymedb-cli backup restore <target>");
555
+ await call(options, "POST", `/v1/backups/restore?target=${args[0]}`);
556
+ }
557
+ else if (action === "archive") {
558
+ const backup = flag(options, "backup");
559
+ await call(options, "POST", "/v1/backups/archive", JSON.stringify({ backup_id: backup ?? null }));
560
+ }
561
+ else if (action === "archives") {
562
+ await call(options, "GET", "/v1/backups/archives");
563
+ }
564
+ else if (action === "verify") {
565
+ need(args, 1, "rymedb-cli backup verify <backup-id>");
566
+ await call(options, "GET", `/v1/backups/verify?backup_id=${encodeURIComponent(args[0])}`);
567
+ }
568
+ else if (action === "drill") {
569
+ await call(options, "GET", "/v1/backups/drill");
570
+ }
571
+ else if (action === "copy") {
572
+ need(args, 1, "rymedb-cli backup copy <backup-id>");
573
+ await call(options, "POST", "/v1/backups/copy", JSON.stringify({ backup_id: args[0] }));
574
+ }
575
+ else
576
+ fail("usage: rymedb-cli backup <checkpoint|latest|pitr|restore|archive|archives|verify|drill|copy> ...");
577
+ return;
578
+ }
579
+ case "snapshot":
580
+ if (action !== "create")
581
+ fail("usage: rymedb-cli snapshot create");
582
+ await call(options, "POST", "/v1/snapshots");
583
+ return;
584
+ case "shards":
585
+ if (action === "layout" || action === undefined) {
586
+ await call(options, "GET", "/v1/shards");
587
+ }
588
+ else if (action === "move") {
589
+ need(args, 1, "rymedb-cli shards move <table> [--target N]");
590
+ const target = flag(options, "target");
591
+ await call(options, "POST", "/v1/shards/move", JSON.stringify({ table: args[0], target: target === undefined ? null : Number(target) }));
592
+ }
593
+ else
594
+ fail("usage: rymedb-cli shards <layout|move> ...");
595
+ return;
596
+ case "ranges": {
597
+ if (action === "list" || action === undefined) {
598
+ await call(options, "GET", "/v1/ranges");
599
+ }
600
+ else if (action === "route") {
601
+ need(args, 1, "rymedb-cli ranges route <key>");
602
+ await call(options, "GET", `/v1/ranges?key=${encodeURIComponent(args[0])}`);
603
+ }
604
+ else if (action === "split") {
605
+ need(args, 5, "rymedb-cli ranges split <id> <mid> <left> <right> <epoch>");
606
+ await call(options, "POST", "/v1/ranges/split", JSON.stringify({
607
+ id: args[0],
608
+ mid: args[1],
609
+ left_id: args[2],
610
+ right_id: args[3],
611
+ expected_epoch: Number(args[4]),
612
+ }));
613
+ }
614
+ else if (action === "merge") {
615
+ need(args, 5, "rymedb-cli ranges merge <left> <right> <merged> <lepoch> <repoch>");
616
+ await call(options, "POST", "/v1/ranges/merge", JSON.stringify({
617
+ left_id: args[0],
618
+ right_id: args[1],
619
+ merged_id: args[2],
620
+ expected_left_epoch: Number(args[3]),
621
+ expected_right_epoch: Number(args[4]),
622
+ }));
623
+ }
624
+ else if (action === "autosplit") {
625
+ const min = args[0] === undefined ? "" : `?min_writes=${Number(args[0])}`;
626
+ await call(options, "POST", `/v1/ranges/autosplit${min}`);
627
+ }
628
+ else if (action === "loads") {
629
+ await call(options, "GET", "/v1/ranges/loads");
630
+ }
631
+ else
632
+ fail("usage: rymedb-cli ranges <list|route|split|merge|autosplit|loads> [min-writes]");
633
+ return;
634
+ }
635
+ case "cluster": {
636
+ if (action === "members" || action === undefined) {
637
+ await call(options, "GET", "/v1/cluster/members");
638
+ }
639
+ else if (action === "add") {
640
+ need(args, 2, "rymedb-cli cluster add <id> <addr>");
641
+ await call(options, "POST", "/v1/cluster/members", JSON.stringify({ id: Number(args[0]), addr: args[1] }));
642
+ }
643
+ else if (action === "remove") {
644
+ need(args, 1, "rymedb-cli cluster remove <id>");
645
+ await call(options, "DELETE", `/v1/cluster/members/${args[0]}`);
646
+ }
647
+ else if (action === "transfer") {
648
+ need(args, 1, "rymedb-cli cluster transfer <target>");
649
+ await call(options, "POST", "/v1/cluster/transfer", JSON.stringify({ target: Number(args[0]) }));
650
+ }
651
+ else if (action === "replace") {
652
+ if (args.length === 0)
653
+ fail("usage: rymedb-cli cluster replace <id=addr>...");
654
+ const members = args.map((entry) => {
655
+ const eq = entry.indexOf("=");
656
+ if (eq < 0)
657
+ fail(`bad member ${entry}, want <id=addr>`);
658
+ return { id: Number(entry.slice(0, eq)), addr: entry.slice(eq + 1) };
659
+ });
660
+ await call(options, "POST", "/v1/cluster/replace", JSON.stringify({ members }));
661
+ }
662
+ else
663
+ fail("usage: rymedb-cli cluster <members|add|remove|transfer|replace> ...");
664
+ return;
665
+ }
666
+ case "stream": {
667
+ if (action !== "watch" && action !== "query" && action !== "broadcast") {
668
+ fail("usage: rymedb-cli stream <watch|query|broadcast> <table|channel> [--limit N] [--count N]");
669
+ }
670
+ need(args, 1, action === "watch"
671
+ ? "rymedb-cli stream watch <table> [--count N]"
672
+ : action === "query"
673
+ ? "rymedb-cli stream query <table> [--limit N] [--count N]"
674
+ : "rymedb-cli stream broadcast <channel> [--count N]");
675
+ const table = args[0];
676
+ const limit = flag(options, "limit");
677
+ const countRaw = flag(options, "count");
678
+ const count = countRaw === undefined ? -1 : Number(countRaw);
679
+ if (Number.isNaN(count))
680
+ fail("--count must be a number");
681
+ let path = action === "watch"
682
+ ? `/v1/stream?table=${encodeURIComponent(table)}`
683
+ : action === "query"
684
+ ? `/v1/query-stream?table=${encodeURIComponent(table)}`
685
+ : `/v1/broadcast/${encodeURIComponent(table)}`;
686
+ if (action === "query" && limit !== undefined)
687
+ path += `&limit=${limit}`;
688
+ await watch(options, path, count);
689
+ return;
690
+ }
691
+ default:
692
+ fail(`unknown command ${group} (try: rymedb-cli help)`);
693
+ }
694
+ }
695
+ async function watch(options, path, count) {
696
+ const sep = path.includes("?") ? "&" : "?";
697
+ const url = options.base.replace(/\/$/, "").replace(/^http/, "ws") +
698
+ path +
699
+ (options.apiKey ? `${sep}api_key=${encodeURIComponent(options.apiKey)}` : "");
700
+ const socket = new WebSocket(url);
701
+ await new Promise((resolve, reject) => {
702
+ socket.addEventListener("open", () => resolve(), { once: true });
703
+ socket.addEventListener("error", () => reject(new Error("websocket connect failed")), {
704
+ once: true,
705
+ });
706
+ }).catch((error) => fail(error instanceof Error ? error.message : String(error)));
707
+ let seen = 0;
708
+ await new Promise((resolve) => {
709
+ socket.addEventListener("message", (event) => {
710
+ const text = String(event.data);
711
+ try {
712
+ console.log(JSON.stringify(JSON.parse(text)));
713
+ }
714
+ catch {
715
+ process.stdout.write(text + (text.endsWith("\n") ? "" : "\n"));
716
+ }
717
+ seen += 1;
718
+ if (count >= 0 && seen >= count) {
719
+ socket.close();
720
+ resolve();
721
+ }
722
+ });
723
+ socket.addEventListener("close", () => resolve());
724
+ socket.addEventListener("error", () => resolve());
725
+ process.on("SIGINT", () => {
726
+ socket.close();
727
+ resolve();
728
+ });
729
+ });
730
+ }
731
+ await main();
732
+ export {};
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@rymelabs/rymedb-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Command-line operator client for rymeDB",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Ryme-Labs/rymeDB.git",
10
+ "directory": "apps/cli"
11
+ },
12
+ "homepage": "https://db.rymelabs.dev/cli/",
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org"
16
+ },
17
+ "files": [
18
+ "dist/cli.js",
19
+ "README.md"
20
+ ],
21
+ "bin": {
22
+ "rymedb-cli": "dist/cli.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "check": "tsc --noEmit -p tsconfig.json",
27
+ "test": "node --test dist/*.test.js"
28
+ },
29
+ "dependencies": {},
30
+ "devDependencies": {
31
+ "typescript": "7.0.2",
32
+ "@types/node": "26.6.3"
33
+ }
34
+ }