@tokenbuddy/tb-admin 1.0.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/src/cli.ts ADDED
@@ -0,0 +1,780 @@
1
+ import { Command } from "commander";
2
+ import { ConfigManager } from "./config.js";
3
+ import { AdminClient } from "./client.js";
4
+ import { FlyProvider } from "./server-cmd.js";
5
+ import {
6
+ loadRegistryFile,
7
+ SellerRegistryDocument,
8
+ validateRegistryDocument
9
+ } from "./bootstrap-registry.js";
10
+ import Table from "cli-table3";
11
+ import * as fs from "fs";
12
+ import YAML from "js-yaml";
13
+
14
+ interface BootstrapConfigDocument {
15
+ bind?: {
16
+ host?: string;
17
+ port?: number;
18
+ };
19
+ clawtip: {
20
+ payTo: string;
21
+ sm4KeyBase64: string;
22
+ skillSlug: string;
23
+ skillId: string;
24
+ description: string;
25
+ resourceUrl: string;
26
+ activationFeeFen: number;
27
+ microsPerFen: number;
28
+ };
29
+ sellerRegistryPath: string;
30
+ allowLocalSellerUrls?: boolean;
31
+ }
32
+
33
+ type SellerConfigDocument = Record<string, any>;
34
+
35
+ function requireString(value: unknown, field: string): string {
36
+ const normalized = String(value || "").trim();
37
+ if (!normalized) {
38
+ throw new Error(`${field} is required`);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function positiveInteger(value: unknown, field: string): number {
44
+ const raw = Number(value);
45
+ if (!Number.isInteger(raw) || raw < 1) {
46
+ throw new Error(`${field} must be >= 1`);
47
+ }
48
+ return raw;
49
+ }
50
+
51
+ function validateBootstrapConfigDocument(input: any): BootstrapConfigDocument {
52
+ if (!input || typeof input !== "object") {
53
+ throw new Error("bootstrap config document is required");
54
+ }
55
+ if (!input.clawtip || typeof input.clawtip !== "object") {
56
+ throw new Error("clawtip config is required");
57
+ }
58
+ return {
59
+ bind: {
60
+ host: requireString(input.bind?.host || "0.0.0.0", "bind.host"),
61
+ port: positiveInteger(input.bind?.port || 8080, "bind.port")
62
+ },
63
+ clawtip: {
64
+ payTo: requireString(input.clawtip.payTo, "pay_to"),
65
+ sm4KeyBase64: requireString(input.clawtip.sm4KeyBase64, "sm4_key_base64"),
66
+ skillSlug: requireString(input.clawtip.skillSlug, "skill_slug"),
67
+ skillId: requireString(input.clawtip.skillId, "skill_id"),
68
+ description: requireString(input.clawtip.description, "description"),
69
+ resourceUrl: requireString(input.clawtip.resourceUrl, "resource_url"),
70
+ activationFeeFen: positiveInteger(input.clawtip.activationFeeFen, "activation_fee_fen"),
71
+ microsPerFen: positiveInteger(input.clawtip.microsPerFen, "micros_per_fen")
72
+ },
73
+ sellerRegistryPath: requireString(input.sellerRegistryPath, "seller_registry_path"),
74
+ allowLocalSellerUrls: Boolean(input.allowLocalSellerUrls)
75
+ };
76
+ }
77
+
78
+ function loadBootstrapConfigFile(filePath: string): BootstrapConfigDocument {
79
+ const content = fs.readFileSync(filePath, "utf8");
80
+ const parsed = YAML.load(content);
81
+ return validateBootstrapConfigDocument(parsed);
82
+ }
83
+
84
+ function validateSellerConfigDocument(input: any): SellerConfigDocument {
85
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
86
+ throw new Error("seller config document is required");
87
+ }
88
+ requireString(input.upstreamUrl, "upstreamUrl");
89
+ if (input.clawtip !== undefined && input.clawtip !== null) {
90
+ requireString(input.clawtip.payTo, "clawtip.payTo");
91
+ requireString(input.clawtip.sm4KeyBase64, "clawtip.sm4KeyBase64");
92
+ requireString(input.clawtip.skillSlug, "clawtip.skillSlug");
93
+ requireString(input.clawtip.skillId, "clawtip.skillId");
94
+ requireString(input.clawtip.description, "clawtip.description");
95
+ requireString(input.clawtip.resourceUrl, "clawtip.resourceUrl");
96
+ positiveInteger(input.clawtip.activationFeeFen ?? 1, "clawtip.activationFeeFen");
97
+ positiveInteger(input.clawtip.microsPerFen ?? 10000, "clawtip.microsPerFen");
98
+ }
99
+ return input;
100
+ }
101
+
102
+ function loadSellerConfigFile(filePath: string): SellerConfigDocument {
103
+ const content = fs.readFileSync(filePath, "utf8");
104
+ const parsed = YAML.load(content);
105
+ return validateSellerConfigDocument(parsed);
106
+ }
107
+
108
+ function sellerConfigUpdateSummary(response: any, document: SellerConfigDocument): string {
109
+ const updated = response.config || document;
110
+ return `path=${response.configPath || "memory"} upstream=${updated.upstreamUrl} allowMock=${Boolean(updated.allowMock)} clawtip=${updated.clawtip ? "configured" : "disabled"}`;
111
+ }
112
+
113
+ function collectOption(value: string, previous: string[]): string[] {
114
+ previous.push(value);
115
+ return previous;
116
+ }
117
+
118
+ function optionalNumber(value: unknown, field: string): number | undefined {
119
+ if (value === undefined || value === null || value === "") {
120
+ return undefined;
121
+ }
122
+ const parsed = Number(value);
123
+ if (!Number.isFinite(parsed)) {
124
+ throw new Error(`${field} must be a number`);
125
+ }
126
+ return parsed;
127
+ }
128
+
129
+ function parseModelAliases(values: string[] | undefined): Record<string, string> {
130
+ const aliases: Record<string, string> = {};
131
+ for (const value of values || []) {
132
+ const index = value.indexOf("=");
133
+ if (index <= 0 || index === value.length - 1) {
134
+ throw new Error(`model alias must use alias=target format: ${value}`);
135
+ }
136
+ aliases[value.slice(0, index)] = value.slice(index + 1);
137
+ }
138
+ return aliases;
139
+ }
140
+
141
+ function loadYamlOrJsonFile(filePath: string): any {
142
+ return YAML.load(fs.readFileSync(filePath, "utf8"));
143
+ }
144
+
145
+ export function buildAdminCli(configManager: ConfigManager): Command {
146
+ const program = new Command();
147
+ program
148
+ .name("tb-admin")
149
+ .description("Remote admin CLI for TokenBuddy seller apps")
150
+ .version("1.0.0")
151
+ .option("--url <url>", "Remote seller core API url")
152
+ .option("--token <token>", "Operator Bearer token")
153
+ .option("--profile <profile>", "Use custom profile instead of default")
154
+ .option("--config <path>", "Use custom config file path");
155
+
156
+ // Helper to resolve client
157
+ function getClient(): AdminClient {
158
+ const opts = program.opts();
159
+ const configPath = opts.config;
160
+ const mgr = configPath ? new ConfigManager(configPath) : configManager;
161
+
162
+ const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
163
+ const token = opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
164
+ const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
165
+
166
+ if (url && token) {
167
+ return new AdminClient(url, token);
168
+ }
169
+
170
+ const activeProfile = mgr.getProfile(profileName);
171
+ if (!activeProfile) {
172
+ throw new Error(
173
+ "No active profile found. Provide --url and --token, or set TOKENBUDDY_ADMIN_URL, or register config profiles."
174
+ );
175
+ }
176
+ return new AdminClient(activeProfile.url, activeProfile.token);
177
+ }
178
+
179
+ function getBaseUrl(): string {
180
+ const opts = program.opts();
181
+ const configPath = opts.config;
182
+ const mgr = configPath ? new ConfigManager(configPath) : configManager;
183
+
184
+ const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
185
+ const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
186
+ if (url) {
187
+ return url.replace(/\/+$/, "");
188
+ }
189
+
190
+ const activeProfile = mgr.getProfile(profileName);
191
+ if (!activeProfile) {
192
+ throw new Error("No active profile found. Provide --url, set TOKENBUDDY_ADMIN_URL, or register config profiles.");
193
+ }
194
+ return activeProfile.url.replace(/\/+$/, "");
195
+ }
196
+
197
+ async function publicGet(path: string): Promise<any> {
198
+ const response = await fetch(`${getBaseUrl()}${path}`);
199
+ if (!response.ok) {
200
+ const errorText = await response.text();
201
+ throw new Error(`HTTP Error ${response.status}: ${errorText || response.statusText}`);
202
+ }
203
+ const text = await response.text();
204
+ return text ? JSON.parse(text) : {};
205
+ }
206
+
207
+ async function getSellerConfig(client: AdminClient): Promise<SellerConfigDocument> {
208
+ const data = await client.get("/operator/admin/config");
209
+ return validateSellerConfigDocument(data.config || data);
210
+ }
211
+
212
+ async function putSellerConfig(client: AdminClient, document: SellerConfigDocument): Promise<any> {
213
+ validateSellerConfigDocument(document);
214
+ return client.put("/operator/admin/config", { config: document });
215
+ }
216
+
217
+ // 1. Status Command
218
+ program
219
+ .command("status")
220
+ .description("Show status of the seller server")
221
+ .action(async () => {
222
+ try {
223
+ const client = getClient();
224
+ const data = await client.get("/operator/status");
225
+ console.log("=== Seller Server Operator Status ===");
226
+ console.log(JSON.stringify(data, null, 2));
227
+ } catch (err: any) {
228
+ console.error("Error:", err.message);
229
+ process.exit(1);
230
+ }
231
+ });
232
+
233
+ // 2. Service Command
234
+ program
235
+ .command("service")
236
+ .description("Show admin service info")
237
+ .action(async () => {
238
+ try {
239
+ const client = getClient();
240
+ const data = await client.get("/operator/admin/service");
241
+ console.log("=== Seller Service Info ===");
242
+ console.log(JSON.stringify(data, null, 2));
243
+ } catch (err: any) {
244
+ console.error("Error:", err.message);
245
+ process.exit(1);
246
+ }
247
+ });
248
+
249
+ // 3. Payments Group
250
+ const payments = program.command("payments").description("Manage payment methods");
251
+
252
+ payments
253
+ .command("list")
254
+ .description("List payment methods")
255
+ .action(async () => {
256
+ try {
257
+ const client = getClient();
258
+ const data = await client.get("/operator/admin/payments");
259
+ console.log("=== Configured Payments ===");
260
+ console.log(JSON.stringify(data, null, 2));
261
+ } catch (err: any) {
262
+ console.error("Error:", err.message);
263
+ }
264
+ });
265
+
266
+ payments
267
+ .command("set-clawtip")
268
+ .description("Configure ClawTip payment parameters")
269
+ .requiredOption("--pay-to <pay_to>", "Pay target account")
270
+ .requiredOption("--sm4-key-base64 <key>", "JD SM4 key encoded in base64")
271
+ .requiredOption("--skill-slug <slug>", "Skill Slug")
272
+ .requiredOption("--skill-id <id>", "Skill ID")
273
+ .requiredOption("--description <desc>", "Order description")
274
+ .requiredOption("--resource-url <url>", "Resource verification url")
275
+ .option("--micros-per-fen <micros>", "Micros per Fen exchange ratio", "10000")
276
+ .action(async (options) => {
277
+ try {
278
+ const client = getClient();
279
+ const document = await getSellerConfig(client);
280
+ document.clawtip = {
281
+ payTo: options.payTo,
282
+ sm4KeyBase64: options.sm4KeyBase64,
283
+ skillSlug: options.skillSlug,
284
+ skillId: options.skillId,
285
+ description: options.description,
286
+ resourceUrl: options.resourceUrl,
287
+ activationFeeFen: 1,
288
+ microsPerFen: parseInt(options.microsPerFen, 10)
289
+ };
290
+ const data = await putSellerConfig(client, document);
291
+ console.log(`ClawTip parameters successfully set: ${sellerConfigUpdateSummary(data, document)}`);
292
+ } catch (err: any) {
293
+ console.error("Error:", err.message);
294
+ }
295
+ });
296
+
297
+ payments
298
+ .command("clear-clawtip")
299
+ .description("Disable ClawTip payment parameters")
300
+ .action(async () => {
301
+ try {
302
+ const client = getClient();
303
+ const document = await getSellerConfig(client);
304
+ delete document.clawtip;
305
+ const data = await putSellerConfig(client, document);
306
+ console.log(`ClawTip parameters cleared: ${sellerConfigUpdateSummary(data, document)}`);
307
+ } catch (err: any) {
308
+ console.error("Error:", err.message);
309
+ }
310
+ });
311
+
312
+ payments
313
+ .command("enable-mock")
314
+ .description("Enable mock payment for developers")
315
+ .action(async () => {
316
+ try {
317
+ const client = getClient();
318
+ const document = await getSellerConfig(client);
319
+ document.allowMock = true;
320
+ const data = await putSellerConfig(client, document);
321
+ console.log(`Mock payment enabled: ${sellerConfigUpdateSummary(data, document)}`);
322
+ } catch (err: any) {
323
+ console.error("Error:", err.message);
324
+ }
325
+ });
326
+
327
+ payments
328
+ .command("disable-mock")
329
+ .description("Disable mock payment")
330
+ .action(async () => {
331
+ try {
332
+ const client = getClient();
333
+ const document = await getSellerConfig(client);
334
+ document.allowMock = false;
335
+ const data = await putSellerConfig(client, document);
336
+ console.log(`Mock payment disabled: ${sellerConfigUpdateSummary(data, document)}`);
337
+ } catch (err: any) {
338
+ console.error("Error:", err.message);
339
+ }
340
+ });
341
+
342
+ // 4. Models Command
343
+ program
344
+ .command("models")
345
+ .description("List available upstream models")
346
+ .action(async () => {
347
+ try {
348
+ const client = getClient();
349
+ const data = await client.get("/operator/admin/upstreams");
350
+ const table = new Table({ head: ["Model ID", "Input Price/1M", "Output Price/1M", "Streaming"] });
351
+
352
+ for (const ups of data.upstreams || []) {
353
+ for (const model of ups.models || []) {
354
+ table.push([
355
+ model.id,
356
+ `${model.inputPriceMicrosPer1m} micros`,
357
+ `${model.outputPriceMicrosPer1m} micros`,
358
+ model.streaming ? "Yes" : "No"
359
+ ]);
360
+ }
361
+ }
362
+ console.log("=== Upstream Model Configurations ===");
363
+ console.log(table.toString());
364
+ } catch (err: any) {
365
+ console.error("Error:", err.message);
366
+ }
367
+ });
368
+
369
+ const upstreams = program.command("upstreams").description("Manage seller upstream config through full seller config updates");
370
+
371
+ upstreams
372
+ .command("get")
373
+ .description("Fetch seller upstream config summary")
374
+ .action(async () => {
375
+ try {
376
+ const client = getClient();
377
+ const data = await client.get("/operator/admin/upstreams");
378
+ console.log(JSON.stringify(data, null, 2));
379
+ } catch (err: any) {
380
+ console.error("Error:", err.message);
381
+ process.exit(1);
382
+ }
383
+ });
384
+
385
+ upstreams
386
+ .command("update")
387
+ .description("Update upstream fields by fetching and pushing the full seller config")
388
+ .option("--upstream-url <url>", "Upstream base URL")
389
+ .option("--api-key <key>", "Upstream API key")
390
+ .option("--chat-completions <state>", "chatCompletions capability")
391
+ .option("--responses <state>", "responses capability")
392
+ .option("--messages <state>", "messages capability")
393
+ .option("--markup-ratio <ratio>", "Markup ratio")
394
+ .option("--discount-ratio <ratio>", "Discount ratio")
395
+ .option("--models-file <path>", "YAML/JSON file containing an array of model configs")
396
+ .option("--clear-aliases", "Clear model aliases before applying --model-alias")
397
+ .option("--model-alias <alias=target>", "Add or update model alias", collectOption, [])
398
+ .action(async (options) => {
399
+ try {
400
+ const client = getClient();
401
+ const document = await getSellerConfig(client);
402
+
403
+ if (options.upstreamUrl) {
404
+ document.upstreamUrl = options.upstreamUrl;
405
+ }
406
+ if (options.apiKey !== undefined) {
407
+ document.upstreamApiKey = options.apiKey;
408
+ }
409
+ const capabilities = {
410
+ ...(document.upstreamCapabilities || {})
411
+ };
412
+ if (options.chatCompletions) {
413
+ capabilities.chatCompletions = options.chatCompletions;
414
+ }
415
+ if (options.responses) {
416
+ capabilities.responses = options.responses;
417
+ }
418
+ if (options.messages) {
419
+ capabilities.messages = options.messages;
420
+ }
421
+ if (Object.keys(capabilities).length > 0) {
422
+ document.upstreamCapabilities = capabilities;
423
+ }
424
+ const markupRatio = optionalNumber(options.markupRatio, "markupRatio");
425
+ if (markupRatio !== undefined) {
426
+ document.markupRatio = markupRatio;
427
+ }
428
+ const discountRatio = optionalNumber(options.discountRatio, "discountRatio");
429
+ if (discountRatio !== undefined) {
430
+ document.discountRatio = discountRatio;
431
+ }
432
+ if (options.modelsFile) {
433
+ const models = loadYamlOrJsonFile(options.modelsFile);
434
+ if (!Array.isArray(models)) {
435
+ throw new Error("models file must contain a YAML/JSON array");
436
+ }
437
+ document.models = models;
438
+ }
439
+ if (options.clearAliases) {
440
+ document.modelAliases = {};
441
+ }
442
+ const aliases = parseModelAliases(options.modelAlias);
443
+ if (Object.keys(aliases).length > 0) {
444
+ document.modelAliases = {
445
+ ...(document.modelAliases || {}),
446
+ ...aliases
447
+ };
448
+ }
449
+
450
+ const response = await putSellerConfig(client, document);
451
+ const updated = response.config || document;
452
+ console.log(`Updated upstream config: path=${response.configPath || "memory"} upstream=${updated.upstreamUrl} models=${Array.isArray(updated.models) ? updated.models.length : 0}`);
453
+ } catch (err: any) {
454
+ console.error("Error:", err.message);
455
+ process.exit(1);
456
+ }
457
+ });
458
+
459
+ upstreams
460
+ .command("refresh")
461
+ .description("Ask seller to refresh its transient upstream model catalog")
462
+ .option("--auto-models", "Replace configured models from refreshed OpenRouter catalog")
463
+ .action(async (options) => {
464
+ try {
465
+ const client = getClient();
466
+ const response = await client.post("/operator/admin/upstreams/refresh", { autoModels: Boolean(options.autoModels) });
467
+ console.log(`Refreshed upstream catalog: path=${response.configPath || "memory"} models=${response.refreshedModels} autoModels=${Boolean(response.autoModels)}`);
468
+ } catch (err: any) {
469
+ console.error("Error:", err.message);
470
+ process.exit(1);
471
+ }
472
+ });
473
+
474
+ // 5. Seller Runtime Config Command
475
+ const sellerConfig = program.command("seller-config").description("Manage seller runtime YAML config");
476
+
477
+ sellerConfig
478
+ .command("get")
479
+ .description("Fetch seller runtime config")
480
+ .action(async () => {
481
+ try {
482
+ const client = getClient();
483
+ const document = await getSellerConfig(client);
484
+ console.log(YAML.dump(document, { lineWidth: 120, noRefs: true, sortKeys: false }));
485
+ } catch (err: any) {
486
+ console.error("Error:", err.message);
487
+ process.exit(1);
488
+ }
489
+ });
490
+
491
+ sellerConfig
492
+ .command("put")
493
+ .description("Update seller runtime config from YAML")
494
+ .requiredOption("--file <path>", "Seller runtime YAML config file")
495
+ .action(async (options) => {
496
+ try {
497
+ const document = loadSellerConfigFile(options.file);
498
+ const client = getClient();
499
+ const response = await putSellerConfig(client, document);
500
+ console.log(`Updated seller config: path=${response.configPath || "memory"} upstream=${response.config?.upstreamUrl || document.upstreamUrl}`);
501
+ } catch (err: any) {
502
+ console.error("Error:", err.message);
503
+ process.exit(1);
504
+ }
505
+ });
506
+
507
+ sellerConfig
508
+ .command("validate")
509
+ .description("Validate seller runtime YAML config")
510
+ .requiredOption("--file <path>", "Seller runtime YAML config file")
511
+ .action((options) => {
512
+ try {
513
+ const document = loadSellerConfigFile(options.file);
514
+ console.log(`Seller config valid: upstream=${document.upstreamUrl} models=${Array.isArray(document.models) ? document.models.length : 0}`);
515
+ } catch (err: any) {
516
+ console.error("Error:", err.message);
517
+ process.exit(1);
518
+ }
519
+ });
520
+
521
+ // 5. Billing Command
522
+ const billing = program.command("billing").description("Query payment and inference billing records");
523
+
524
+ billing
525
+ .command("purchases")
526
+ .description("List all remote token purchase / payment transactions")
527
+ .action(async () => {
528
+ try {
529
+ const client = getClient();
530
+ const data = await client.get("/operator/admin/purchases");
531
+ const table = new Table({ head: ["Purchase ID", "Provider", "Amount", "State", "Date"] });
532
+
533
+ for (const p of data.purchases || []) {
534
+ table.push([
535
+ p.purchase_id,
536
+ p.payment_provider,
537
+ `${p.amount_micros} micros`,
538
+ p.state,
539
+ p.created_at
540
+ ]);
541
+ }
542
+ console.log("=== Remote Token Purchase / Payment History ===");
543
+ console.log(table.toString());
544
+ } catch (err: any) {
545
+ console.error("Error:", err.message);
546
+ }
547
+ });
548
+
549
+ billing
550
+ .command("requests")
551
+ .description("List all remote inference usage consumption logs")
552
+ .action(async () => {
553
+ try {
554
+ const client = getClient();
555
+ const data = await client.get("/operator/admin/requests");
556
+ const table = new Table({ head: ["Request ID", "Model", "State", "Reserved", "Settled", "Tokens (I/O)", "Date"] });
557
+
558
+ for (const r of data.requests || []) {
559
+ table.push([
560
+ r.request_id,
561
+ r.model,
562
+ r.state,
563
+ `${r.reserved_micros} micros`,
564
+ `${r.settled_micros} micros`,
565
+ `${r.prompt_tokens} / ${r.completion_tokens}`,
566
+ r.created_at
567
+ ]);
568
+ }
569
+ console.log("=== Inference Usage Consumption Ledger ===");
570
+ console.log(table.toString());
571
+ } catch (err: any) {
572
+ console.error("Error:", err.message);
573
+ }
574
+ });
575
+
576
+ // 6. Bootstrap Registry Command
577
+ const bootstrap = program.command("bootstrap").description("Manage wallet bootstrap service");
578
+ const bootstrapSellers = bootstrap.command("sellers").description("Manage public seller registry");
579
+ const bootstrapConfig = bootstrap.command("config").description("Manage wallet bootstrap YAML config");
580
+
581
+ bootstrapSellers
582
+ .command("get")
583
+ .description("Fetch public seller registry")
584
+ .action(async () => {
585
+ try {
586
+ const data = await publicGet("/registry/sellers") as SellerRegistryDocument;
587
+ console.log(JSON.stringify(data, null, 2));
588
+ } catch (err: any) {
589
+ console.error("Error:", err.message);
590
+ process.exit(1);
591
+ }
592
+ });
593
+
594
+ bootstrapSellers
595
+ .command("put")
596
+ .description("Update public seller registry")
597
+ .requiredOption("--file <path>", "Seller registry JSON file")
598
+ .action(async (options) => {
599
+ try {
600
+ const document = loadRegistryFile(options.file);
601
+ const client = getClient();
602
+ const response = await client.put("/operator/registry/sellers", document) as SellerRegistryDocument;
603
+ console.log(`Updated seller registry: version=${response.version} sellers=${response.sellers.length}`);
604
+ } catch (err: any) {
605
+ console.error("Error:", err.message);
606
+ process.exit(1);
607
+ }
608
+ });
609
+
610
+ bootstrapSellers
611
+ .command("validate")
612
+ .description("Validate seller registry JSON")
613
+ .requiredOption("--file <path>", "Seller registry JSON file")
614
+ .action((options) => {
615
+ try {
616
+ const document = loadRegistryFile(options.file);
617
+ validateRegistryDocument(document);
618
+ console.log(`Seller registry valid: version=${document.version} sellers=${document.sellers.length}`);
619
+ } catch (err: any) {
620
+ console.error("Error:", err.message);
621
+ process.exit(1);
622
+ }
623
+ });
624
+
625
+ bootstrapConfig
626
+ .command("get")
627
+ .description("Fetch wallet bootstrap runtime config")
628
+ .action(async () => {
629
+ try {
630
+ const client = getClient();
631
+ const data = await client.get("/operator/config") as BootstrapConfigDocument;
632
+ console.log(YAML.dump(data, { lineWidth: 120, noRefs: true }));
633
+ } catch (err: any) {
634
+ console.error("Error:", err.message);
635
+ process.exit(1);
636
+ }
637
+ });
638
+
639
+ bootstrapConfig
640
+ .command("put")
641
+ .description("Update wallet bootstrap runtime config from YAML")
642
+ .requiredOption("--file <path>", "Wallet bootstrap YAML config file")
643
+ .action(async (options) => {
644
+ try {
645
+ const document = loadBootstrapConfigFile(options.file);
646
+ const client = getClient();
647
+ const response = await client.put("/operator/config", document) as BootstrapConfigDocument;
648
+ console.log(`Updated wallet bootstrap config: skillSlug=${response.clawtip.skillSlug} registry=${response.sellerRegistryPath}`);
649
+ } catch (err: any) {
650
+ console.error("Error:", err.message);
651
+ process.exit(1);
652
+ }
653
+ });
654
+
655
+ bootstrapConfig
656
+ .command("validate")
657
+ .description("Validate wallet bootstrap YAML config")
658
+ .requiredOption("--file <path>", "Wallet bootstrap YAML config file")
659
+ .action((options) => {
660
+ try {
661
+ const document = loadBootstrapConfigFile(options.file);
662
+ console.log(`Wallet bootstrap config valid: skillSlug=${document.clawtip.skillSlug} registry=${document.sellerRegistryPath}`);
663
+ } catch (err: any) {
664
+ console.error("Error:", err.message);
665
+ process.exit(1);
666
+ }
667
+ });
668
+
669
+ // 7. Config Command (Local)
670
+ const configCmd = program.command("config").description("Manage local admin profiles");
671
+
672
+ configCmd
673
+ .command("set <profileName>")
674
+ .description("Add or update an admin connection profile")
675
+ .option("--url <url>", "Remote seller core API url")
676
+ .option("--token <token>", "Bearer Operator secret token")
677
+ .action((profileName, options) => {
678
+ const opts = program.opts();
679
+ const configPath = opts.config;
680
+ const mgr = configPath ? new ConfigManager(configPath) : configManager;
681
+
682
+ const url = options.url || opts.url || process.env.TOKENBUDDY_ADMIN_URL;
683
+ const token = options.token || opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
684
+
685
+ if (!url || !token) {
686
+ console.error("error: required option '--url <url>' and '--token <token>' not specified");
687
+ process.exit(1);
688
+ }
689
+
690
+ mgr.setProfile(profileName, { url, token });
691
+ console.log(`Profile \`${profileName}\` successfully configured.`);
692
+ });
693
+
694
+ configCmd
695
+ .command("use <profileName>")
696
+ .description("Switch default profile to select")
697
+ .action((profileName) => {
698
+ const opts = program.opts();
699
+ const configPath = opts.config;
700
+ const mgr = configPath ? new ConfigManager(configPath) : configManager;
701
+
702
+ try {
703
+ mgr.useProfile(profileName);
704
+ console.log(`Now using profile \`${profileName}\` by default.`);
705
+ } catch (err: any) {
706
+ console.error("Error:", err.message);
707
+ }
708
+ });
709
+
710
+ configCmd
711
+ .command("list")
712
+ .description("List all configured profiles")
713
+ .action(() => {
714
+ const opts = program.opts();
715
+ const configPath = opts.config;
716
+ const mgr = configPath ? new ConfigManager(configPath) : configManager;
717
+
718
+ const profiles = mgr.listProfiles();
719
+ const config = mgr.load();
720
+ console.log("=== Configured Local Profiles ===");
721
+ for (const p of profiles) {
722
+ const isDefault = p === config.default_profile ? "* " : " ";
723
+ const details = config.profiles[p];
724
+ console.log(`${isDefault}${p} -> ${details.url}`);
725
+ }
726
+ });
727
+
728
+ // 8. Seller Command (Fly.io)
729
+ const sellerCmd = program.command("seller").description("Deploy and manage seller containers on Fly.io");
730
+ const flyProvider = new FlyProvider();
731
+
732
+ sellerCmd
733
+ .command("ls")
734
+ .description("List all deployed apps on Fly.io")
735
+ .action(() => {
736
+ try {
737
+ const out = flyProvider.listApps();
738
+ console.log(out);
739
+ } catch (err: any) {
740
+ console.error("Error:", err.message);
741
+ }
742
+ });
743
+
744
+ sellerCmd
745
+ .command("create <name>")
746
+ .description("Deploy a new machines instance on Fly.io")
747
+ .option("--region <region>", "Deployment fly region (default: hkg)", "hkg")
748
+ .option("--image <image>", "Docker image name target")
749
+ .requiredOption("--operator-secret <secret>", "Operator secret to configure")
750
+ .option("--dry-run", "Dry run display without actual execution")
751
+ .action((name, options) => {
752
+ try {
753
+ const res = flyProvider.createSeller({
754
+ name,
755
+ region: options.region,
756
+ image: options.image,
757
+ operatorSecret: options.operatorSecret,
758
+ dryRun: options.dryRun
759
+ });
760
+ console.log(res);
761
+ } catch (err: any) {
762
+ console.error("Error:", err.message);
763
+ }
764
+ });
765
+
766
+ sellerCmd
767
+ .command("remove <name>")
768
+ .description("Completely destroy a seller app on Fly.io")
769
+ .option("--dry-run", "Dry run")
770
+ .action((name, options) => {
771
+ try {
772
+ const res = flyProvider.removeSeller(name, options.dryRun);
773
+ console.log(res);
774
+ } catch (err: any) {
775
+ console.error("Error:", err.message);
776
+ }
777
+ });
778
+
779
+ return program;
780
+ }