@nodetool-ai/cli 0.7.0-rc.11

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 (77) hide show
  1. package/dist/ExecutionTree.d.ts +19 -0
  2. package/dist/ExecutionTree.d.ts.map +1 -0
  3. package/dist/ExecutionTree.js +100 -0
  4. package/dist/ExecutionTree.js.map +1 -0
  5. package/dist/app.d.ts +50 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +767 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/commands/agent.d.ts +11 -0
  10. package/dist/commands/agent.d.ts.map +1 -0
  11. package/dist/commands/agent.js +483 -0
  12. package/dist/commands/agent.js.map +1 -0
  13. package/dist/commands/deploy-helpers.d.ts +46 -0
  14. package/dist/commands/deploy-helpers.d.ts.map +1 -0
  15. package/dist/commands/deploy-helpers.js +448 -0
  16. package/dist/commands/deploy-helpers.js.map +1 -0
  17. package/dist/commands/deploy.d.ts +8 -0
  18. package/dist/commands/deploy.d.ts.map +1 -0
  19. package/dist/commands/deploy.js +645 -0
  20. package/dist/commands/deploy.js.map +1 -0
  21. package/dist/commands/models-hf.d.ts +9 -0
  22. package/dist/commands/models-hf.d.ts.map +1 -0
  23. package/dist/commands/models-hf.js +235 -0
  24. package/dist/commands/models-hf.js.map +1 -0
  25. package/dist/commands/models-recommended.d.ts +10 -0
  26. package/dist/commands/models-recommended.d.ts.map +1 -0
  27. package/dist/commands/models-recommended.js +128 -0
  28. package/dist/commands/models-recommended.js.map +1 -0
  29. package/dist/commands/package-helpers.d.ts +14 -0
  30. package/dist/commands/package-helpers.d.ts.map +1 -0
  31. package/dist/commands/package-helpers.js +76 -0
  32. package/dist/commands/package-helpers.js.map +1 -0
  33. package/dist/commands/package.d.ts +3 -0
  34. package/dist/commands/package.d.ts.map +1 -0
  35. package/dist/commands/package.js +254 -0
  36. package/dist/commands/package.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +233 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/markdown.d.ts +9 -0
  42. package/dist/markdown.d.ts.map +1 -0
  43. package/dist/markdown.js +48 -0
  44. package/dist/markdown.js.map +1 -0
  45. package/dist/nodetool.d.ts +16 -0
  46. package/dist/nodetool.d.ts.map +1 -0
  47. package/dist/nodetool.js +1154 -0
  48. package/dist/nodetool.js.map +1 -0
  49. package/dist/providers.d.ts +47 -0
  50. package/dist/providers.d.ts.map +1 -0
  51. package/dist/providers.js +173 -0
  52. package/dist/providers.js.map +1 -0
  53. package/dist/readline-input.d.ts +29 -0
  54. package/dist/readline-input.d.ts.map +1 -0
  55. package/dist/readline-input.js +199 -0
  56. package/dist/readline-input.js.map +1 -0
  57. package/dist/run-dsl.d.ts +11 -0
  58. package/dist/run-dsl.d.ts.map +1 -0
  59. package/dist/run-dsl.js +31 -0
  60. package/dist/run-dsl.js.map +1 -0
  61. package/dist/settings.d.ts +18 -0
  62. package/dist/settings.d.ts.map +1 -0
  63. package/dist/settings.js +86 -0
  64. package/dist/settings.js.map +1 -0
  65. package/dist/stdin.d.ts +48 -0
  66. package/dist/stdin.d.ts.map +1 -0
  67. package/dist/stdin.js +352 -0
  68. package/dist/stdin.js.map +1 -0
  69. package/dist/useExecutionState.d.ts +44 -0
  70. package/dist/useExecutionState.d.ts.map +1 -0
  71. package/dist/useExecutionState.js +281 -0
  72. package/dist/useExecutionState.js.map +1 -0
  73. package/dist/websocket-client.d.ts +100 -0
  74. package/dist/websocket-client.d.ts.map +1 -0
  75. package/dist/websocket-client.js +351 -0
  76. package/dist/websocket-client.js.map +1 -0
  77. package/package.json +85 -0
@@ -0,0 +1,645 @@
1
+ /**
2
+ * `nodetool deploy` CLI commands — registered on a Commander `Command`
3
+ * via `registerDeployCommands(program)`. See docs/ for the full spec.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as fsp from "node:fs/promises";
7
+ import * as yaml from "js-yaml";
8
+ import { ComputeType, CPUFlavor, DataCenter, GPUType, configureDocker, configureGCP, configureRunPod, getDeploymentConfigPath, initDeploymentConfig, loadDeploymentConfig, saveDeploymentConfig, WorkflowSyncer } from "@nodetool-ai/deploy";
9
+ import { getCollection } from "@nodetool-ai/vectorstore";
10
+ import { asJson, buildStubDeployment, confirm, getAdminClient, getDeploymentOrExit, getManager, getUserManager, loadConfigOrExit, makeSyncerDeps, parseParamPairs, printTable, promptLine, runEditor } from "./deploy-helpers.js";
11
+ const SUPPORTED_TYPES = [
12
+ "docker",
13
+ "runpod",
14
+ "gcp",
15
+ "fly",
16
+ "railway",
17
+ "huggingface"
18
+ ];
19
+ /** Common GCP Cloud Run regions. Hardcoded because not exported by @nodetool-ai/deploy. */
20
+ const GCP_REGIONS = [
21
+ "us-central1",
22
+ "us-east1",
23
+ "us-east4",
24
+ "us-east5",
25
+ "us-south1",
26
+ "us-west1",
27
+ "us-west2",
28
+ "us-west3",
29
+ "us-west4",
30
+ "northamerica-northeast1",
31
+ "northamerica-northeast2",
32
+ "southamerica-east1",
33
+ "southamerica-west1",
34
+ "europe-central2",
35
+ "europe-north1",
36
+ "europe-southwest1",
37
+ "europe-west1",
38
+ "europe-west2",
39
+ "europe-west3",
40
+ "europe-west4",
41
+ "europe-west6",
42
+ "europe-west8",
43
+ "europe-west9",
44
+ "europe-west10",
45
+ "europe-west12",
46
+ "asia-east1",
47
+ "asia-east2",
48
+ "asia-northeast1",
49
+ "asia-northeast2",
50
+ "asia-northeast3",
51
+ "asia-south1",
52
+ "asia-south2",
53
+ "asia-southeast1",
54
+ "asia-southeast2",
55
+ "australia-southeast1",
56
+ "australia-southeast2",
57
+ "me-central1",
58
+ "me-central2",
59
+ "me-west1",
60
+ "africa-south1"
61
+ ];
62
+ // ---------------------------------------------------------------------------
63
+ // Wrapper to unify error handling on every sub-command
64
+ // ---------------------------------------------------------------------------
65
+ function runAction(fn) {
66
+ return async (...args) => {
67
+ try {
68
+ await fn(...args);
69
+ }
70
+ catch (err) {
71
+ console.error(String(err));
72
+ process.exit(1);
73
+ }
74
+ };
75
+ }
76
+ /** Parse a positive integer string or throw a clear error. */
77
+ function parsePositiveInt(raw, label) {
78
+ const n = Number.parseInt(raw, 10);
79
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
80
+ throw new Error(`Invalid ${label} '${raw}': expected a positive integer.`);
81
+ }
82
+ return n;
83
+ }
84
+ /** Parse a port number (1–65535) or throw. */
85
+ function parsePort(raw, label) {
86
+ const n = Number.parseInt(raw, 10);
87
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 65535) {
88
+ throw new Error(`Invalid ${label} '${raw}': expected an integer in the range 1–65535.`);
89
+ }
90
+ return n;
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Public registration hooks
94
+ // ---------------------------------------------------------------------------
95
+ export function registerDeployCommands(program) {
96
+ const deploy = program
97
+ .command("deploy")
98
+ .description("Manage deployments (docker, runpod, gcp, fly, railway, huggingface)");
99
+ registerInit(deploy);
100
+ registerList(deploy);
101
+ registerShow(deploy);
102
+ registerAdd(deploy);
103
+ registerEdit(deploy);
104
+ registerPlan(deploy);
105
+ registerApply(deploy);
106
+ registerStatus(deploy);
107
+ registerLogs(deploy);
108
+ registerDestroy(deploy);
109
+ registerWorkflows(deploy);
110
+ registerDatabase(deploy);
111
+ registerCollections(deploy);
112
+ registerUsers(deploy);
113
+ }
114
+ export function registerListGcpOptions(program) {
115
+ program
116
+ .command("list-gcp-options")
117
+ .description("List supported GCP Cloud Run regions and RunPod GPU/CPU/data-center options")
118
+ .option("--json", "Output as JSON")
119
+ .action(runAction(async (opts) => {
120
+ const data = {
121
+ gcp_regions: GCP_REGIONS,
122
+ runpod_gpu_types: Object.values(GPUType),
123
+ runpod_cpu_flavors: Object.values(CPUFlavor),
124
+ runpod_data_centers: Object.values(DataCenter),
125
+ runpod_compute_types: Object.values(ComputeType)
126
+ };
127
+ if (opts.json) {
128
+ asJson(data);
129
+ return;
130
+ }
131
+ console.log("\nGCP Cloud Run regions:");
132
+ printTable(data.gcp_regions.map((r) => ({ region: r })));
133
+ console.log("\nRunPod GPU types:");
134
+ printTable(data.runpod_gpu_types.map((g) => ({ gpu_type: g })));
135
+ console.log("\nRunPod CPU flavors:");
136
+ printTable(data.runpod_cpu_flavors.map((c) => ({ cpu_flavor: c })));
137
+ console.log("\nRunPod compute types:");
138
+ printTable(data.runpod_compute_types.map((c) => ({ compute_type: c })));
139
+ console.log("\nRunPod data centers:");
140
+ printTable(data.runpod_data_centers.map((d) => ({ data_center: d })));
141
+ console.log();
142
+ }));
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // init / list / show / add / edit
146
+ // ---------------------------------------------------------------------------
147
+ function registerInit(deploy) {
148
+ deploy
149
+ .command("init")
150
+ .description("Initialize deployment.yaml with defaults")
151
+ .action(runAction(async () => {
152
+ const configPath = getDeploymentConfigPath();
153
+ if (fs.existsSync(configPath)) {
154
+ const ok = await confirm(`${configPath} already exists. Overwrite?`);
155
+ if (!ok)
156
+ return;
157
+ await fsp.unlink(configPath);
158
+ }
159
+ await initDeploymentConfig();
160
+ console.log(`Created ${configPath}`);
161
+ console.log("\nNext steps:");
162
+ console.log(" nodetool deploy add <name> --type docker|runpod|gcp|fly|railway|huggingface");
163
+ console.log(" nodetool deploy plan <name>");
164
+ console.log(" nodetool deploy apply <name>");
165
+ }));
166
+ }
167
+ function registerList(deploy) {
168
+ deploy
169
+ .command("list")
170
+ .description("List configured deployments with status")
171
+ .option("--json", "Output as JSON")
172
+ .action(runAction(async (opts) => {
173
+ const mgr = await getManager();
174
+ const rows = await mgr.listDeployments();
175
+ if (opts.json) {
176
+ asJson(rows);
177
+ return;
178
+ }
179
+ if (rows.length === 0) {
180
+ console.log("(no deployments configured)");
181
+ return;
182
+ }
183
+ printTable(rows, [
184
+ "name",
185
+ "type",
186
+ "status",
187
+ "last_deployed",
188
+ "host",
189
+ "container",
190
+ "pod_id",
191
+ "project",
192
+ "region",
193
+ "service"
194
+ ]);
195
+ }));
196
+ }
197
+ function registerShow(deploy) {
198
+ deploy
199
+ .command("show <name>")
200
+ .description("Print deployment config as YAML")
201
+ .action(runAction(async (name) => {
202
+ const config = await loadConfigOrExit();
203
+ const dep = getDeploymentOrExit(config, name);
204
+ process.stdout.write(yaml.dump({ [name]: dep }, { sortKeys: false, noCompatMode: true }));
205
+ }));
206
+ }
207
+ function registerAdd(deploy) {
208
+ deploy
209
+ .command("add <name>")
210
+ .description("Add a deployment via interactive prompts")
211
+ .requiredOption("--type <type>", `Deployment type (${SUPPORTED_TYPES.join("|")})`)
212
+ .action(runAction(async (name, opts) => {
213
+ const type = opts.type;
214
+ if (!SUPPORTED_TYPES.includes(type)) {
215
+ throw new Error(`Invalid --type '${opts.type}'. Must be one of: ${SUPPORTED_TYPES.join(", ")}`);
216
+ }
217
+ let config;
218
+ try {
219
+ config = await loadDeploymentConfig();
220
+ }
221
+ catch {
222
+ const shouldInit = await confirm("No deployment.yaml found. Create one now?");
223
+ if (!shouldInit) {
224
+ console.error("Run 'nodetool deploy init' first to create deployment.yaml.");
225
+ process.exit(1);
226
+ }
227
+ config = await initDeploymentConfig();
228
+ }
229
+ if (config.deployments[name]) {
230
+ throw new Error(`Deployment '${name}' already exists`);
231
+ }
232
+ let deployment;
233
+ switch (type) {
234
+ case "docker": {
235
+ const host = await promptLine("Docker host (IP/hostname or localhost)", {
236
+ default: "localhost"
237
+ });
238
+ const isLocal = host === "localhost" ||
239
+ host === "127.0.0.1" ||
240
+ host === "::1" ||
241
+ host === "0.0.0.0";
242
+ const sshUser = isLocal
243
+ ? undefined
244
+ : await promptLine("SSH user", { default: "root" });
245
+ const sshKeyPath = isLocal
246
+ ? undefined
247
+ : await promptLine("SSH key path", { default: "~/.ssh/id_rsa" });
248
+ const imageName = await promptLine("Docker image name", {
249
+ default: "ghcr.io/nodetool-ai/nodetool"
250
+ });
251
+ const imageTag = await promptLine("Image tag", {
252
+ default: "latest"
253
+ });
254
+ const containerName = await promptLine("Container name", {
255
+ default: `nodetool-${name}`
256
+ });
257
+ const containerPortRaw = await promptLine("Container port", {
258
+ default: "8000"
259
+ });
260
+ const containerPort = parsePort(containerPortRaw, "Container port");
261
+ deployment = configureDocker(name, {
262
+ host,
263
+ sshUser,
264
+ sshKeyPath,
265
+ imageName,
266
+ imageTag,
267
+ containerName,
268
+ containerPort
269
+ });
270
+ break;
271
+ }
272
+ case "runpod": {
273
+ const imageName = await promptLine("RunPod Docker image name (e.g. user/nodetool)");
274
+ const imageTag = await promptLine("Image tag", {
275
+ default: "latest"
276
+ });
277
+ const registry = await promptLine("Registry", {
278
+ default: "docker.io"
279
+ });
280
+ deployment = configureRunPod(name, { imageName, imageTag, registry });
281
+ break;
282
+ }
283
+ case "gcp": {
284
+ const projectId = await promptLine("GCP project ID");
285
+ const region = await promptLine("Region", {
286
+ default: "us-central1"
287
+ });
288
+ const serviceName = await promptLine("Cloud Run service name", {
289
+ default: name
290
+ });
291
+ const imageRepository = await promptLine("Image repository (e.g. project/repo/image)");
292
+ const imageTag = await promptLine("Image tag", {
293
+ default: "latest"
294
+ });
295
+ deployment = configureGCP(name, {
296
+ projectId,
297
+ region,
298
+ serviceName,
299
+ imageRepository,
300
+ imageTag
301
+ });
302
+ break;
303
+ }
304
+ case "fly":
305
+ case "railway":
306
+ case "huggingface":
307
+ deployment = buildStubDeployment(type, name);
308
+ break;
309
+ }
310
+ config.deployments[name] = deployment;
311
+ await saveDeploymentConfig(config);
312
+ console.log(`Added '${name}' to ${getDeploymentConfigPath()}`);
313
+ if (type === "fly" || type === "railway" || type === "huggingface") {
314
+ console.log(`Stub deployment written — run 'nodetool deploy edit ${name}' to finish configuring.`);
315
+ }
316
+ }));
317
+ }
318
+ function registerEdit(deploy) {
319
+ deploy
320
+ .command("edit")
321
+ .description("Open deployment.yaml in $EDITOR")
322
+ .action(runAction(async () => {
323
+ const configPath = getDeploymentConfigPath();
324
+ if (!fs.existsSync(configPath)) {
325
+ throw new Error(`No deployment.yaml at ${configPath}. Run 'nodetool deploy init' first.`);
326
+ }
327
+ const status = runEditor(configPath);
328
+ if (status !== 0)
329
+ process.exit(status);
330
+ }));
331
+ }
332
+ // ---------------------------------------------------------------------------
333
+ // lifecycle: plan / apply / status / logs / destroy
334
+ // ---------------------------------------------------------------------------
335
+ function registerPlan(deploy) {
336
+ deploy
337
+ .command("plan <name>")
338
+ .description("Show what 'apply' would do")
339
+ .action(runAction(async (name) => {
340
+ const mgr = await getManager();
341
+ const plan = await mgr.plan(name);
342
+ asJson(plan);
343
+ }));
344
+ }
345
+ function registerApply(deploy) {
346
+ deploy
347
+ .command("apply <name>")
348
+ .description("Deploy to its target platform")
349
+ .option("--dry-run", "Show what would be done without executing")
350
+ .action(runAction(async (name, opts) => {
351
+ const mgr = await getManager();
352
+ const result = await mgr.apply(name, { dryRun: Boolean(opts.dryRun) });
353
+ asJson(result);
354
+ if (result["status"] === "error")
355
+ process.exit(1);
356
+ }));
357
+ }
358
+ function registerStatus(deploy) {
359
+ deploy
360
+ .command("status <name>")
361
+ .description("Show the current status of a deployment")
362
+ .action(runAction(async (name) => {
363
+ const mgr = await getManager();
364
+ const status = await mgr.status(name);
365
+ asJson(status);
366
+ }));
367
+ }
368
+ function registerLogs(deploy) {
369
+ deploy
370
+ .command("logs <name>")
371
+ .description("Fetch deployment logs (streaming for --follow depends on platform support)")
372
+ .option("--service <service>", "Service/container name")
373
+ .option("-f, --follow", "Follow logs")
374
+ .option("--tail <n>", "Number of lines to tail", "100")
375
+ .action(runAction(async (name, opts) => {
376
+ const tail = parsePositiveInt(opts.tail, "--tail");
377
+ const mgr = await getManager();
378
+ const text = await mgr.logs(name, {
379
+ service: opts.service,
380
+ follow: Boolean(opts.follow),
381
+ tail
382
+ });
383
+ process.stdout.write(text);
384
+ if (!text.endsWith("\n"))
385
+ process.stdout.write("\n");
386
+ }));
387
+ }
388
+ function registerDestroy(deploy) {
389
+ deploy
390
+ .command("destroy <name>")
391
+ .description("Tear down a deployment")
392
+ .option("--force", "Skip confirmation")
393
+ .action(runAction(async (name, opts) => {
394
+ const ok = await confirm(`Destroy '${name}'? This action is irreversible.`, { force: Boolean(opts.force) });
395
+ if (!ok)
396
+ return;
397
+ const mgr = await getManager();
398
+ const result = await mgr.destroy(name, {
399
+ force: Boolean(opts.force)
400
+ });
401
+ asJson(result);
402
+ }));
403
+ }
404
+ // ---------------------------------------------------------------------------
405
+ // workflows subgroup
406
+ // ---------------------------------------------------------------------------
407
+ function registerWorkflows(deploy) {
408
+ const wf = deploy
409
+ .command("workflows")
410
+ .description("Manage workflows on a remote deployment");
411
+ wf.command("sync <deployment> <workflow_id>")
412
+ .description("Sync a local workflow (and its assets/models) to the deployment")
413
+ .option("--token <token>", "Admin bearer token")
414
+ .action(runAction(async (deploymentName, workflowId, opts) => {
415
+ const config = await loadConfigOrExit();
416
+ const dep = getDeploymentOrExit(config, deploymentName);
417
+ const client = await getAdminClient(dep, deploymentName, opts);
418
+ const syncer = new WorkflowSyncer(client, makeSyncerDeps());
419
+ const ok = await syncer.syncWorkflow(workflowId);
420
+ if (!ok)
421
+ process.exit(1);
422
+ console.log(`Synced '${workflowId}' to '${deploymentName}'`);
423
+ }));
424
+ wf.command("list <deployment>")
425
+ .description("List workflows on a deployment")
426
+ .option("--token <token>", "Admin bearer token")
427
+ .option("--json", "Output as JSON")
428
+ .action(runAction(async (deploymentName, opts) => {
429
+ const config = await loadConfigOrExit();
430
+ const dep = getDeploymentOrExit(config, deploymentName);
431
+ const client = await getAdminClient(dep, deploymentName, opts);
432
+ const result = await client.listWorkflows();
433
+ const rows = (result["workflows"] ?? []);
434
+ if (opts.json) {
435
+ asJson(rows);
436
+ return;
437
+ }
438
+ printTable(rows.map((r) => ({
439
+ id: r["id"],
440
+ name: r["name"],
441
+ updated_at: r["updated_at"]
442
+ })));
443
+ }));
444
+ wf.command("delete <deployment> <workflow_id>")
445
+ .description("Delete a workflow on the deployment")
446
+ .option("--force", "Skip confirmation")
447
+ .option("--token <token>", "Admin bearer token")
448
+ .action(runAction(async (deploymentName, workflowId, opts) => {
449
+ const ok = await confirm(`Delete workflow '${workflowId}' on '${deploymentName}'?`, { force: Boolean(opts.force) });
450
+ if (!ok)
451
+ return;
452
+ const config = await loadConfigOrExit();
453
+ const dep = getDeploymentOrExit(config, deploymentName);
454
+ const client = await getAdminClient(dep, deploymentName, opts);
455
+ await client.deleteWorkflow(workflowId);
456
+ console.log(`deleted '${workflowId}'`);
457
+ }));
458
+ wf.command("run <deployment> <workflow_id>")
459
+ .description("Run a workflow on the deployment")
460
+ .option("-p, --param <k=v>", "Parameter k=v (repeatable)", (val, acc = []) => {
461
+ acc.push(val);
462
+ return acc;
463
+ }, [])
464
+ .option("--token <token>", "Admin bearer token")
465
+ .action(runAction(async (deploymentName, workflowId, opts) => {
466
+ const params = parseParamPairs(opts.param ?? []);
467
+ const config = await loadConfigOrExit();
468
+ const dep = getDeploymentOrExit(config, deploymentName);
469
+ const client = await getAdminClient(dep, deploymentName, opts);
470
+ const result = await client.runWorkflow(workflowId, params);
471
+ asJson(result);
472
+ }));
473
+ }
474
+ // ---------------------------------------------------------------------------
475
+ // database subgroup
476
+ // ---------------------------------------------------------------------------
477
+ function registerDatabase(deploy) {
478
+ const db = deploy.command("database").description("Remote DB row management");
479
+ db.command("get <deployment> <table> <key>")
480
+ .description("Read a DB row")
481
+ .option("--token <token>", "Admin bearer token")
482
+ .action(runAction(async (deploymentName, table, key, opts) => {
483
+ const config = await loadConfigOrExit();
484
+ const dep = getDeploymentOrExit(config, deploymentName);
485
+ const client = await getAdminClient(dep, deploymentName, opts);
486
+ const row = await client.dbGet(table, key);
487
+ asJson(row);
488
+ }));
489
+ db.command("save <deployment> <table> <json_data>")
490
+ .description("Upsert a DB row (positional JSON string)")
491
+ .option("--token <token>", "Admin bearer token")
492
+ .action(runAction(async (deploymentName, table, jsonData, opts) => {
493
+ let parsed;
494
+ try {
495
+ parsed = JSON.parse(jsonData);
496
+ }
497
+ catch (e) {
498
+ throw new Error(`Invalid JSON for <json_data>: ${String(e)}`);
499
+ }
500
+ const config = await loadConfigOrExit();
501
+ const dep = getDeploymentOrExit(config, deploymentName);
502
+ const client = await getAdminClient(dep, deploymentName, opts);
503
+ const row = await client.dbSave(table, parsed);
504
+ asJson(row);
505
+ }));
506
+ db.command("delete <deployment> <table> <key>")
507
+ .description("Delete a DB row")
508
+ .option("--force", "Skip confirmation")
509
+ .option("--token <token>", "Admin bearer token")
510
+ .action(runAction(async (deploymentName, table, key, opts) => {
511
+ const ok = await confirm(`Delete '${table}/${key}' on '${deploymentName}'?`, { force: Boolean(opts.force) });
512
+ if (!ok)
513
+ return;
514
+ const config = await loadConfigOrExit();
515
+ const dep = getDeploymentOrExit(config, deploymentName);
516
+ const client = await getAdminClient(dep, deploymentName, opts);
517
+ await client.dbDelete(table, key);
518
+ console.log("deleted");
519
+ }));
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // collections sync
523
+ // ---------------------------------------------------------------------------
524
+ function registerCollections(deploy) {
525
+ const coll = deploy
526
+ .command("collections")
527
+ .description("Sync local vector collections to a deployment");
528
+ coll
529
+ .command("sync <deployment> <collection>")
530
+ .description("Sync a local collection to the deployment")
531
+ .option("--token <token>", "Admin bearer token")
532
+ .option("--batch-size <n>", "Batch size for uploads", "100")
533
+ .action(runAction(async (deploymentName, collectionName, opts) => {
534
+ const batchSize = parsePositiveInt(opts.batchSize, "--batch-size");
535
+ const collection = await getCollection(collectionName);
536
+ if (!collection) {
537
+ throw new Error(`Local collection not found: ${collectionName}`);
538
+ }
539
+ const metadata = collection
540
+ .metadata ?? {};
541
+ const embeddingModel = metadata["embedding_model"] ?? "";
542
+ if (!embeddingModel) {
543
+ throw new Error(`Local collection '${collectionName}' has no embedding_model in metadata — cannot sync without embeddings.`);
544
+ }
545
+ // Inspect the local collection BEFORE creating anything on the
546
+ // remote, so partial failures don't leave an empty/orphan collection
547
+ // on the deployment.
548
+ const withGet = collection;
549
+ const page = await withGet.get({ limit: batchSize, offset: 0 });
550
+ if (page.ids.length > 0) {
551
+ throw new Error("deploy collections sync: re-embedding of local documents during sync is not yet implemented in the Node CLI. " +
552
+ `Found ${page.ids.length} document(s) locally but cannot upload without embeddings. ` +
553
+ "No remote collection was created.");
554
+ }
555
+ // Empty collection — safe to create on the remote as a no-op sync.
556
+ const config = await loadConfigOrExit();
557
+ const dep = getDeploymentOrExit(config, deploymentName);
558
+ const client = await getAdminClient(dep, deploymentName, opts);
559
+ await client.createCollection(collectionName, embeddingModel);
560
+ console.log(`Collection '${collectionName}' is empty — created an empty collection on '${deploymentName}'.`);
561
+ }));
562
+ }
563
+ // ---------------------------------------------------------------------------
564
+ // users-* commands
565
+ // ---------------------------------------------------------------------------
566
+ function registerUsers(deploy) {
567
+ deploy
568
+ .command("users-add <deployment> <username>")
569
+ .description("Add an API user to the deployment")
570
+ .option("--role <role>", "User role (admin|user)", "user")
571
+ .option("--token <token>", "Admin bearer token")
572
+ .action(runAction(async (deploymentName, username, opts) => {
573
+ if (opts.role !== "admin" && opts.role !== "user") {
574
+ throw new Error(`Invalid --role '${opts.role}'. Must be 'admin' or 'user'.`);
575
+ }
576
+ const config = await loadConfigOrExit();
577
+ const dep = getDeploymentOrExit(config, deploymentName);
578
+ const mgr = await getUserManager(dep, deploymentName, opts);
579
+ const result = await mgr.addUser(username, opts.role);
580
+ console.log(`Created user '${result.username}' (role: ${result.role}, id: ${result.user_id})`);
581
+ if (result.token) {
582
+ console.log("");
583
+ console.log(` Token: ${result.token}`);
584
+ console.log("");
585
+ console.log(" WARNING: save this token now — it will not be shown again.");
586
+ }
587
+ }));
588
+ deploy
589
+ .command("users-list <deployment>")
590
+ .description("List API users on the deployment")
591
+ .option("--token <token>", "Admin bearer token")
592
+ .option("--json", "Output as JSON")
593
+ .action(runAction(async (deploymentName, opts) => {
594
+ const config = await loadConfigOrExit();
595
+ const dep = getDeploymentOrExit(config, deploymentName);
596
+ const mgr = await getUserManager(dep, deploymentName, opts);
597
+ const users = await mgr.listUsers();
598
+ if (opts.json) {
599
+ asJson(users);
600
+ return;
601
+ }
602
+ printTable(users.map((u) => ({
603
+ username: u.username,
604
+ user_id: u.user_id,
605
+ role: u.role,
606
+ token_hash_preview: u.token_hash
607
+ ? `${u.token_hash.slice(0, 16)}...`
608
+ : "",
609
+ created_at: (u.created_at ?? "").slice(0, 19)
610
+ })), ["username", "user_id", "role", "token_hash_preview", "created_at"]);
611
+ }));
612
+ deploy
613
+ .command("users-remove <deployment> <username>")
614
+ .description("Remove an API user")
615
+ .option("--force", "Skip confirmation")
616
+ .option("--token <token>", "Admin bearer token")
617
+ .action(runAction(async (deploymentName, username, opts) => {
618
+ const ok = await confirm(`Remove user '${username}' from '${deploymentName}'?`, { force: Boolean(opts.force) });
619
+ if (!ok)
620
+ return;
621
+ const config = await loadConfigOrExit();
622
+ const dep = getDeploymentOrExit(config, deploymentName);
623
+ const mgr = await getUserManager(dep, deploymentName, opts);
624
+ await mgr.removeUser(username);
625
+ console.log(`removed '${username}'`);
626
+ }));
627
+ deploy
628
+ .command("users-reset-token <deployment> <username>")
629
+ .description("Rotate a user's API token")
630
+ .option("--token <token>", "Admin bearer token")
631
+ .action(runAction(async (deploymentName, username, opts) => {
632
+ const config = await loadConfigOrExit();
633
+ const dep = getDeploymentOrExit(config, deploymentName);
634
+ const mgr = await getUserManager(dep, deploymentName, opts);
635
+ const result = await mgr.resetToken(username);
636
+ console.log(`Rotated token for '${result.username}'`);
637
+ if (result.token) {
638
+ console.log("");
639
+ console.log(` New token: ${result.token}`);
640
+ console.log("");
641
+ console.log(" The previous token is now invalid. Save this token — it will not be shown again.");
642
+ }
643
+ }));
644
+ }
645
+ //# sourceMappingURL=deploy.js.map