@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.
- package/README.md +10 -2
- package/dist/cli.js +732 -0
- package/package.json +32 -4
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# rymeDB CLI
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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
|
+
}
|