@tokenbuddy/tb-admin 1.0.35 → 1.0.37

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 (93) hide show
  1. package/dist/src/cli.js +92 -19
  2. package/dist/src/config.d.ts +7 -1
  3. package/dist/src/config.js +16 -4
  4. package/dist/src/display-format.js +6 -14
  5. package/dist/src/init-command.d.ts +50 -0
  6. package/dist/src/init-command.js +347 -0
  7. package/dist/src/providers/fly-io.d.ts +3 -0
  8. package/dist/src/providers/fly-io.js +137 -0
  9. package/dist/src/providers/provider-definition.d.ts +38 -0
  10. package/dist/src/providers/provider-definition.js +2 -0
  11. package/dist/src/seller.d.ts +2 -0
  12. package/dist/src/seller.js +30 -13
  13. package/dist/src/server-cmd.d.ts +1 -0
  14. package/dist/src/server-cmd.js +9 -2
  15. package/dist/src/ui-actions.d.ts +3 -0
  16. package/dist/src/ui-actions.js +199 -27
  17. package/dist/src/ui-command.js +3 -2
  18. package/dist/src/ui-state.d.ts +1 -3
  19. package/dist/src/ui-state.js +4 -8
  20. package/dist/src/ui-static.js +43 -15
  21. package/dist/src/workdir.d.ts +21 -0
  22. package/dist/src/workdir.js +50 -0
  23. package/package.json +8 -2
  24. package/templates/providers/fly.io/admin.toml.example +18 -0
  25. package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
  26. package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
  27. package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
  28. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
  29. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
  30. package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
  31. package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
  32. package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
  33. package/templates/providers/fly.io/env/deploy.env.example +12 -0
  34. package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
  35. package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
  36. package/templates/providers/fly.io/provider.toml.example +10 -0
  37. package/dist/src/bootstrap-registry.d.ts.map +0 -1
  38. package/dist/src/bootstrap-registry.js.map +0 -1
  39. package/dist/src/cli.d.ts.map +0 -1
  40. package/dist/src/cli.js.map +0 -1
  41. package/dist/src/client.d.ts.map +0 -1
  42. package/dist/src/client.js.map +0 -1
  43. package/dist/src/config.d.ts.map +0 -1
  44. package/dist/src/config.js.map +0 -1
  45. package/dist/src/display-format.d.ts.map +0 -1
  46. package/dist/src/display-format.js.map +0 -1
  47. package/dist/src/index.d.ts.map +0 -1
  48. package/dist/src/index.js.map +0 -1
  49. package/dist/src/provider.d.ts.map +0 -1
  50. package/dist/src/provider.js.map +0 -1
  51. package/dist/src/seller.d.ts.map +0 -1
  52. package/dist/src/seller.js.map +0 -1
  53. package/dist/src/server-cmd.d.ts.map +0 -1
  54. package/dist/src/server-cmd.js.map +0 -1
  55. package/dist/src/ui-actions.d.ts.map +0 -1
  56. package/dist/src/ui-actions.js.map +0 -1
  57. package/dist/src/ui-command.d.ts.map +0 -1
  58. package/dist/src/ui-command.js.map +0 -1
  59. package/dist/src/ui-server.d.ts.map +0 -1
  60. package/dist/src/ui-server.js.map +0 -1
  61. package/dist/src/ui-state.d.ts.map +0 -1
  62. package/dist/src/ui-state.js.map +0 -1
  63. package/dist/src/ui-static.d.ts.map +0 -1
  64. package/dist/src/ui-static.js.map +0 -1
  65. package/dist/src/upstream-balance-probe.d.ts.map +0 -1
  66. package/dist/src/upstream-balance-probe.js.map +0 -1
  67. package/dist/src/vendor-client.d.ts.map +0 -1
  68. package/dist/src/vendor-client.js.map +0 -1
  69. package/dist/src/vendor-commands.d.ts.map +0 -1
  70. package/dist/src/vendor-commands.js.map +0 -1
  71. package/src/bootstrap-registry.ts +0 -90
  72. package/src/cli.ts +0 -1614
  73. package/src/client.ts +0 -179
  74. package/src/config.ts +0 -194
  75. package/src/display-format.ts +0 -411
  76. package/src/index.ts +0 -11
  77. package/src/provider.ts +0 -150
  78. package/src/seller.ts +0 -538
  79. package/src/server-cmd.ts +0 -362
  80. package/src/ui-actions.ts +0 -1040
  81. package/src/ui-command.ts +0 -44
  82. package/src/ui-server.ts +0 -353
  83. package/src/ui-state.ts +0 -1318
  84. package/src/ui-static.ts +0 -673
  85. package/src/upstream-balance-probe.ts +0 -13
  86. package/src/vendor-client.ts +0 -23
  87. package/src/vendor-commands.ts +0 -65
  88. package/tests/admin.test.ts +0 -2162
  89. package/tests/seller.test.ts +0 -388
  90. package/tests/ui-state-fleet.test.ts +0 -526
  91. package/tests/ui-static-row.test.ts +0 -467
  92. package/tests/vendor-cli.test.ts +0 -241
  93. package/tsconfig.json +0 -8
package/src/cli.ts DELETED
@@ -1,1614 +0,0 @@
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 { SellerCommandRunner } from "./seller.js";
6
- import { bindAdminUiCommand } from "./ui-command.js";
7
- import {
8
- loadRegistryFile,
9
- SellerRegistryDocument,
10
- validateRegistryDocument
11
- } from "./bootstrap-registry.js";
12
- import Table from "cli-table3";
13
- import * as fs from "fs";
14
- import * as path from "path";
15
- import { fileURLToPath } from "url";
16
- import YAML from "js-yaml";
17
-
18
- interface BootstrapConfigDocument {
19
- bind?: {
20
- host?: string;
21
- port?: number;
22
- };
23
- clawtip: {
24
- payTo: string;
25
- sm4KeyBase64: string;
26
- skillSlug: string;
27
- skillId: string;
28
- description: string;
29
- resourceUrl: string;
30
- activationFeeFen: number;
31
- microsPerFen: number;
32
- };
33
- sellerRegistryPath: string;
34
- allowLocalSellerUrls?: boolean;
35
- }
36
-
37
- type SellerConfigDocument = Record<string, any>;
38
-
39
- function currentModuleDir(): string {
40
- if (typeof __dirname !== "undefined") {
41
- return __dirname;
42
- }
43
-
44
- const stack = new Error().stack || "";
45
- const fileUrlMatch = stack.match(/(file:\/\/\/[^)\n]+\/cli\.js):\d+:\d+/);
46
- if (fileUrlMatch) {
47
- return path.dirname(fileURLToPath(fileUrlMatch[1]));
48
- }
49
-
50
- const filePathMatch = stack.match(/(\/[^)\n]+\/cli\.(?:js|ts)):\d+:\d+/);
51
- if (filePathMatch) {
52
- return path.dirname(filePathMatch[1]);
53
- }
54
-
55
- return process.cwd();
56
- }
57
-
58
- function readAdminPackageVersion(): string {
59
- let current = path.resolve(currentModuleDir(), "..");
60
- const seen = new Set<string>();
61
- while (!seen.has(current)) {
62
- seen.add(current);
63
- const packageJsonPath = path.join(current, "package.json");
64
- if (fs.existsSync(packageJsonPath)) {
65
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
66
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
67
- return packageJson.version;
68
- }
69
- }
70
- const parent = path.dirname(current);
71
- if (parent === current) break;
72
- current = parent;
73
- }
74
- return "0.0.0";
75
- }
76
-
77
- function requireString(value: unknown, field: string): string {
78
- const normalized = String(value || "").trim();
79
- if (!normalized) {
80
- throw new Error(`${field} is required`);
81
- }
82
- return normalized;
83
- }
84
-
85
- function positiveInteger(value: unknown, field: string): number {
86
- const raw = Number(value);
87
- if (!Number.isInteger(raw) || raw < 1) {
88
- throw new Error(`${field} must be >= 1`);
89
- }
90
- return raw;
91
- }
92
-
93
- function validateBootstrapConfigDocument(input: any): BootstrapConfigDocument {
94
- if (!input || typeof input !== "object") {
95
- throw new Error("bootstrap config document is required");
96
- }
97
- if (!input.clawtip || typeof input.clawtip !== "object") {
98
- throw new Error("clawtip config is required");
99
- }
100
- return {
101
- bind: {
102
- host: requireString(input.bind?.host || "0.0.0.0", "bind.host"),
103
- port: positiveInteger(input.bind?.port || 8080, "bind.port")
104
- },
105
- clawtip: {
106
- payTo: requireString(input.clawtip.payTo, "pay_to"),
107
- sm4KeyBase64: requireString(input.clawtip.sm4KeyBase64, "sm4_key_base64"),
108
- skillSlug: requireString(input.clawtip.skillSlug, "skill_slug"),
109
- skillId: requireString(input.clawtip.skillId, "skill_id"),
110
- description: requireString(input.clawtip.description, "description"),
111
- resourceUrl: requireString(input.clawtip.resourceUrl, "resource_url"),
112
- activationFeeFen: positiveInteger(input.clawtip.activationFeeFen, "activation_fee_fen"),
113
- microsPerFen: positiveInteger(input.clawtip.microsPerFen, "micros_per_fen")
114
- },
115
- sellerRegistryPath: requireString(input.sellerRegistryPath, "seller_registry_path"),
116
- allowLocalSellerUrls: Boolean(input.allowLocalSellerUrls)
117
- };
118
- }
119
-
120
- function loadBootstrapConfigFile(filePath: string): BootstrapConfigDocument {
121
- const content = fs.readFileSync(filePath, "utf8");
122
- const parsed = YAML.load(content);
123
- return validateBootstrapConfigDocument(parsed);
124
- }
125
-
126
- function validateSellerConfigDocument(input: any): SellerConfigDocument {
127
- if (!input || typeof input !== "object" || Array.isArray(input)) {
128
- throw new Error("seller config document is required");
129
- }
130
- requireString(input.upstreamUrl, "upstreamUrl");
131
- if (input.clawtip !== undefined && input.clawtip !== null) {
132
- requireString(input.clawtip.payTo, "clawtip.payTo");
133
- requireString(input.clawtip.sm4KeyBase64, "clawtip.sm4KeyBase64");
134
- requireString(input.clawtip.skillSlug, "clawtip.skillSlug");
135
- requireString(input.clawtip.skillId, "clawtip.skillId");
136
- requireString(input.clawtip.description, "clawtip.description");
137
- requireString(input.clawtip.resourceUrl, "clawtip.resourceUrl");
138
- positiveInteger(input.clawtip.activationFeeFen ?? 1, "clawtip.activationFeeFen");
139
- positiveInteger(input.clawtip.microsPerFen ?? 10000, "clawtip.microsPerFen");
140
- }
141
- return input;
142
- }
143
-
144
- function loadSellerConfigFile(filePath: string): SellerConfigDocument {
145
- const content = fs.readFileSync(filePath, "utf8");
146
- const parsed = YAML.load(content);
147
- return validateSellerConfigDocument(parsed);
148
- }
149
-
150
- function sellerConfigUpdateSummary(response: any, document: SellerConfigDocument): string {
151
- const updated = response.config || document;
152
- return `path=${response.configPath || "memory"} upstream=${updated.upstreamUrl} allowMock=${Boolean(updated.allowMock)} clawtip=${updated.clawtip ? "configured" : "disabled"}`;
153
- }
154
-
155
- function collectOption(value: string, previous: string[]): string[] {
156
- previous.push(value);
157
- return previous;
158
- }
159
-
160
- function optionalNumber(value: unknown, field: string): number | undefined {
161
- if (value === undefined || value === null || value === "") {
162
- return undefined;
163
- }
164
- const parsed = Number(value);
165
- if (!Number.isFinite(parsed)) {
166
- throw new Error(`${field} must be a number`);
167
- }
168
- return parsed;
169
- }
170
-
171
- function parseModelAliases(values: string[] | undefined): Record<string, string> {
172
- const aliases: Record<string, string> = {};
173
- for (const value of values || []) {
174
- const index = value.indexOf("=");
175
- if (index <= 0 || index === value.length - 1) {
176
- throw new Error(`model alias must use alias=target format: ${value}`);
177
- }
178
- aliases[value.slice(0, index)] = value.slice(index + 1);
179
- }
180
- return aliases;
181
- }
182
-
183
- function loadYamlOrJsonFile(filePath: string): any {
184
- return YAML.load(fs.readFileSync(filePath, "utf8"));
185
- }
186
-
187
- function loadSellerRegistryEntryFile(filePath: string): any {
188
- const parsed = loadYamlOrJsonFile(filePath);
189
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
190
- throw new Error("seller registry entry document is required");
191
- }
192
- validateRegistryDocument({ version: 1, sellers: [parsed as any] });
193
- return parsed;
194
- }
195
-
196
- function loadSellerRegistryPatchFile(filePath: string): any {
197
- const parsed = loadYamlOrJsonFile(filePath);
198
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
199
- throw new Error("seller registry patch document is required");
200
- }
201
- if ("id" in parsed) {
202
- throw new Error("seller registry patch must not include id");
203
- }
204
- return parsed;
205
- }
206
-
207
- function withExpectedVersion(body: Record<string, any>, expectedVersion: unknown): Record<string, any> {
208
- if (expectedVersion === undefined) {
209
- return body;
210
- }
211
- const version = Number(expectedVersion);
212
- // Step 14 (v1.1): version 0 means empty db. CLI 跟 server 一起接受 0.
213
- if (!Number.isInteger(version) || version < 0) {
214
- throw new Error("expected version must be >= 0");
215
- }
216
- return { ...body, expectedVersion: version };
217
- }
218
-
219
- function requireUpstreamModels(data: any): any[] {
220
- if (Array.isArray(data?.models)) {
221
- return data.models;
222
- }
223
- if (Array.isArray(data?.upstreams)) {
224
- const firstWithModels = data.upstreams.find((entry: any) => Array.isArray(entry?.models));
225
- if (firstWithModels) {
226
- return firstWithModels.models;
227
- }
228
- }
229
- throw new Error("operator upstream summary must contain models array");
230
- }
231
-
232
- /**
233
- * 构造 admin commander program,绑定所有 `tb-admin` 子命令(profile / registry / seller / config / backup 等)。
234
- * 顶层选项支持 `--url` / `--token` / `--profile` / `--config`,与 `ConfigManager` 协同解析。
235
- *
236
- * @param configManager 配置管理器
237
- * @returns 配置完整的 commander Command 实例
238
- */
239
- export function buildAdminCli(configManager: ConfigManager): Command {
240
- const program = new Command();
241
- program
242
- .name("tb-admin")
243
- .description("Remote admin CLI for TokenBuddy seller apps")
244
- .version(readAdminPackageVersion())
245
- .option("--url <url>", "Remote seller core API url")
246
- .option("--token <token>", "Operator Bearer token")
247
- .option("--profile <profile>", "Use custom profile instead of default")
248
- .option("--config <path>", "Use custom config file path");
249
-
250
- bindAdminUiCommand(program, configManager);
251
-
252
- // Helper to resolve client
253
- function getClient(): AdminClient {
254
- const opts = program.opts();
255
- const configPath = opts.config;
256
- const mgr = configPath ? new ConfigManager(configPath) : configManager;
257
-
258
- const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
259
- const token = opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
260
- const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
261
-
262
- if (url && token) {
263
- return new AdminClient(url, token);
264
- }
265
-
266
- const activeProfile = mgr.getProfile(profileName);
267
- if (!activeProfile) {
268
- throw new Error(
269
- "No active profile found. Provide --url and --token, or set TOKENBUDDY_ADMIN_URL, or register config profiles."
270
- );
271
- }
272
- return new AdminClient(activeProfile.url, activeProfile.token);
273
- }
274
-
275
- function getBaseUrl(): string {
276
- const opts = program.opts();
277
- const configPath = opts.config;
278
- const mgr = configPath ? new ConfigManager(configPath) : configManager;
279
-
280
- const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
281
- const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
282
- if (url) {
283
- return url.replace(/\/+$/, "");
284
- }
285
-
286
- const activeProfile = mgr.getProfile(profileName);
287
- if (!activeProfile) {
288
- throw new Error("No active profile found. Provide --url, set TOKENBUDDY_ADMIN_URL, or register config profiles.");
289
- }
290
- return activeProfile.url.replace(/\/+$/, "");
291
- }
292
-
293
- async function publicGet(path: string): Promise<any> {
294
- const response = await fetch(`${getBaseUrl()}${path}`);
295
- if (!response.ok) {
296
- const errorText = await response.text();
297
- throw new Error(`HTTP Error ${response.status}: ${errorText || response.statusText}`);
298
- }
299
- const text = await response.text();
300
- return text ? JSON.parse(text) : {};
301
- }
302
-
303
- async function getSellerConfig(client: AdminClient): Promise<SellerConfigDocument> {
304
- const data = await client.get("/operator/admin/config");
305
- return validateSellerConfigDocument(data.config || data);
306
- }
307
-
308
- async function putSellerConfig(client: AdminClient, document: SellerConfigDocument): Promise<any> {
309
- validateSellerConfigDocument(document);
310
- return client.put("/operator/admin/config", { config: document });
311
- }
312
-
313
- // 1. Status Command
314
- program
315
- .command("status")
316
- .description("Show status of the seller server")
317
- .action(async () => {
318
- try {
319
- const client = getClient();
320
- const data = await client.get("/operator/status");
321
- console.log("=== Seller Server Operator Status ===");
322
- console.log(JSON.stringify(data, null, 2));
323
- } catch (err: any) {
324
- console.error("Error:", err.message);
325
- process.exit(1);
326
- }
327
- });
328
-
329
- // 2. Service Command
330
- program
331
- .command("service")
332
- .description("Show admin service info")
333
- .action(async () => {
334
- try {
335
- const client = getClient();
336
- const data = await client.get("/operator/admin/service");
337
- console.log("=== Seller Service Info ===");
338
- console.log(JSON.stringify(data, null, 2));
339
- } catch (err: any) {
340
- console.error("Error:", err.message);
341
- process.exit(1);
342
- }
343
- });
344
-
345
- // 3. Payments Group
346
- const payments = program.command("payments").description("Manage payment methods");
347
-
348
- payments
349
- .command("list")
350
- .description("List payment methods")
351
- .action(async () => {
352
- try {
353
- const client = getClient();
354
- const data = await client.get("/operator/admin/payments");
355
- console.log("=== Configured Payments ===");
356
- console.log(JSON.stringify(data, null, 2));
357
- } catch (err: any) {
358
- console.error("Error:", err.message);
359
- }
360
- });
361
-
362
- payments
363
- .command("set-clawtip")
364
- .description("Configure ClawTip payment parameters")
365
- .requiredOption("--pay-to <pay_to>", "Pay target account")
366
- .requiredOption("--sm4-key-base64 <key>", "JD SM4 key encoded in base64")
367
- .requiredOption("--skill-slug <slug>", "Skill Slug")
368
- .requiredOption("--skill-id <id>", "Skill ID")
369
- .requiredOption("--description <desc>", "Order description")
370
- .requiredOption("--resource-url <url>", "Resource verification url")
371
- .option("--micros-per-fen <micros>", "Micros per Fen exchange ratio", "10000")
372
- .action(async (options) => {
373
- try {
374
- const client = getClient();
375
- const document = await getSellerConfig(client);
376
- document.clawtip = {
377
- payTo: options.payTo,
378
- sm4KeyBase64: options.sm4KeyBase64,
379
- skillSlug: options.skillSlug,
380
- skillId: options.skillId,
381
- description: options.description,
382
- resourceUrl: options.resourceUrl,
383
- activationFeeFen: 1,
384
- microsPerFen: parseInt(options.microsPerFen, 10)
385
- };
386
- const data = await putSellerConfig(client, document);
387
- console.log(`ClawTip parameters successfully set: ${sellerConfigUpdateSummary(data, document)}`);
388
- } catch (err: any) {
389
- console.error("Error:", err.message);
390
- }
391
- });
392
-
393
- payments
394
- .command("clear-clawtip")
395
- .description("Disable ClawTip payment parameters")
396
- .action(async () => {
397
- try {
398
- const client = getClient();
399
- const document = await getSellerConfig(client);
400
- delete document.clawtip;
401
- const data = await putSellerConfig(client, document);
402
- console.log(`ClawTip parameters cleared: ${sellerConfigUpdateSummary(data, document)}`);
403
- } catch (err: any) {
404
- console.error("Error:", err.message);
405
- }
406
- });
407
-
408
- payments
409
- .command("enable-mock")
410
- .description("Enable internal mock payment for developers")
411
- .option("--internal", "Only enable internal mock purchase/complete; do not advertise mock publicly")
412
- .action(async (options) => {
413
- try {
414
- const client = getClient();
415
- const document = await getSellerConfig(client);
416
- document.allowMock = true;
417
- if (options.internal) {
418
- document.publicMockPayments = false;
419
- }
420
- const data = await putSellerConfig(client, document);
421
- console.log(`Internal mock payment enabled: ${sellerConfigUpdateSummary(data, document)}`);
422
- } catch (err: any) {
423
- console.error("Error:", err.message);
424
- }
425
- });
426
-
427
- payments
428
- .command("advertise-mock")
429
- .description("Advertise mock payment in the public seller manifest")
430
- .action(async () => {
431
- try {
432
- const client = getClient();
433
- const document = await getSellerConfig(client);
434
- document.allowMock = true;
435
- document.publicMockPayments = true;
436
- const data = await putSellerConfig(client, document);
437
- console.log(`Public mock payment advertised: ${sellerConfigUpdateSummary(data, document)}`);
438
- } catch (err: any) {
439
- console.error("Error:", err.message);
440
- }
441
- });
442
-
443
- payments
444
- .command("hide-mock")
445
- .description("Stop advertising mock payment publicly while preserving internal mock config")
446
- .action(async () => {
447
- try {
448
- const client = getClient();
449
- const document = await getSellerConfig(client);
450
- document.publicMockPayments = false;
451
- const data = await putSellerConfig(client, document);
452
- console.log(`Public mock payment hidden: ${sellerConfigUpdateSummary(data, document)}`);
453
- } catch (err: any) {
454
- console.error("Error:", err.message);
455
- }
456
- });
457
-
458
- payments
459
- .command("disable-mock")
460
- .description("Disable mock payment")
461
- .action(async () => {
462
- try {
463
- const client = getClient();
464
- const document = await getSellerConfig(client);
465
- document.allowMock = false;
466
- document.publicMockPayments = false;
467
- const data = await putSellerConfig(client, document);
468
- console.log(`Mock payment disabled: ${sellerConfigUpdateSummary(data, document)}`);
469
- } catch (err: any) {
470
- console.error("Error:", err.message);
471
- }
472
- });
473
-
474
- // 4. Models Command
475
- program
476
- .command("models")
477
- .description("List available upstream models")
478
- .option("--json", "Print current upstream model summary as JSON")
479
- .action(async (options) => {
480
- try {
481
- const client = getClient();
482
- const data = await client.get("/operator/admin/upstreams");
483
- const models = requireUpstreamModels(data);
484
- if (options.json) {
485
- console.log(JSON.stringify({ models }, null, 2));
486
- return;
487
- }
488
- const table = new Table({ head: ["Model ID", "Input Price/1M", "Output Price/1M", "Streaming"] });
489
-
490
- for (const model of models) {
491
- table.push([
492
- model.id,
493
- `${model.inputPriceMicrosPer1m ?? "unknown"} micros`,
494
- `${model.outputPriceMicrosPer1m ?? "unknown"} micros`,
495
- model.streaming ? "Yes" : "No"
496
- ]);
497
- }
498
- console.log("=== Upstream Model Configurations ===");
499
- console.log(table.toString());
500
- } catch (err: any) {
501
- console.error("Error:", err.message);
502
- process.exit(1);
503
- }
504
- });
505
-
506
- const upstreams = program.command("upstreams").description("Manage seller upstream config through full seller config updates");
507
-
508
- upstreams
509
- .command("get")
510
- .description("Fetch seller upstream config summary")
511
- .action(async () => {
512
- try {
513
- const client = getClient();
514
- const data = await client.get("/operator/admin/upstreams");
515
- console.log(JSON.stringify(data, null, 2));
516
- } catch (err: any) {
517
- console.error("Error:", err.message);
518
- process.exit(1);
519
- }
520
- });
521
-
522
- upstreams
523
- .command("update")
524
- .description("Update upstream fields by fetching and pushing the full seller config")
525
- .option("--upstream-url <url>", "Upstream base URL")
526
- .option("--api-key <key>", "Upstream API key")
527
- .option("--chat-completions <state>", "chatCompletions probe policy (unsupported disables probing; support is detected by seller)")
528
- .option("--responses <state>", "responses probe policy (unsupported disables probing; support is detected by seller)")
529
- .option("--messages <state>", "messages probe policy (unsupported disables probing; support is detected by seller)")
530
- .option("--markup-ratio <ratio>", "Markup ratio")
531
- .option("--discount-ratio <ratio>", "Discount ratio")
532
- .option("--models-file <path>", "YAML/JSON file containing an array of model configs")
533
- .option("--clear-aliases", "Clear model aliases before applying --model-alias")
534
- .option("--model-alias <alias=target>", "Add or update model alias", collectOption, [])
535
- .option("--no-refresh", "Skip auto-refresh of upstream model catalog (default: refresh when --upstream-url or --api-key changes)")
536
- .action(async (options) => {
537
- try {
538
- const client = getClient();
539
-
540
- // If upstream URL or key changes, refresh the model catalog from
541
- // the new upstream before pushing config. Without this, the seller
542
- // would keep the old model's list (e.g. Claude names) even after
543
- // switching to a different upstream (e.g. MiniMax) — every
544
- // subsequent inference call would 400/404 upstream-side.
545
- // Skipped if --models-file is provided (user has explicit list) or
546
- // if --no-refresh is passed.
547
- const upstreamChanged = Boolean(options.upstreamUrl) || options.apiKey !== undefined;
548
- if (upstreamChanged && !options.modelsFile && !options.noRefresh) {
549
- console.log("Auto-refreshing upstream model catalog (upstream URL or key changed)...");
550
- const refreshResp = await client.post("/operator/admin/upstreams/refresh", { autoModels: true });
551
- console.log(` refreshed: ${refreshResp.refreshedModels ?? 0} models from upstream`);
552
- }
553
-
554
- const document = await getSellerConfig(client);
555
-
556
- if (options.upstreamUrl) {
557
- document.upstreamUrl = options.upstreamUrl;
558
- }
559
- if (options.apiKey !== undefined) {
560
- document.upstreamApiKey = options.apiKey;
561
- }
562
- const capabilities = {
563
- ...(document.upstreamCapabilities || {})
564
- };
565
- if (options.chatCompletions) {
566
- capabilities.chatCompletions = options.chatCompletions;
567
- }
568
- if (options.responses) {
569
- capabilities.responses = options.responses;
570
- }
571
- if (options.messages) {
572
- capabilities.messages = options.messages;
573
- }
574
- if (Object.keys(capabilities).length > 0) {
575
- document.upstreamCapabilities = capabilities;
576
- }
577
- const markupRatio = optionalNumber(options.markupRatio, "markupRatio");
578
- if (markupRatio !== undefined) {
579
- document.markupRatio = markupRatio;
580
- }
581
- const discountRatio = optionalNumber(options.discountRatio, "discountRatio");
582
- if (discountRatio !== undefined) {
583
- document.discountRatio = discountRatio;
584
- }
585
- if (options.modelsFile) {
586
- const models = loadYamlOrJsonFile(options.modelsFile);
587
- if (!Array.isArray(models)) {
588
- throw new Error("models file must contain a YAML/JSON array");
589
- }
590
- document.models = models;
591
- }
592
- if (options.clearAliases) {
593
- document.modelAliases = {};
594
- }
595
- const aliases = parseModelAliases(options.modelAlias);
596
- if (Object.keys(aliases).length > 0) {
597
- document.modelAliases = {
598
- ...(document.modelAliases || {}),
599
- ...aliases
600
- };
601
- }
602
-
603
- const response = await putSellerConfig(client, document);
604
- const updated = response.config || document;
605
- console.log(`Updated upstream config: path=${response.configPath || "memory"} upstream=${updated.upstreamUrl} models=${Array.isArray(updated.models) ? updated.models.length : 0}`);
606
- } catch (err: any) {
607
- console.error("Error:", err.message);
608
- process.exit(1);
609
- }
610
- });
611
-
612
- upstreams
613
- .command("refresh")
614
- .description("Ask seller to refresh its transient upstream model catalog")
615
- .option("--auto-models", "Replace configured models from refreshed OpenRouter catalog")
616
- .action(async (options) => {
617
- try {
618
- const client = getClient();
619
- const response = await client.post("/operator/admin/upstreams/refresh", { autoModels: Boolean(options.autoModels) });
620
- console.log(`Refreshed upstream catalog: path=${response.configPath || "memory"} models=${response.refreshedModels} autoModels=${Boolean(response.autoModels)}`);
621
- } catch (err: any) {
622
- console.error("Error:", err.message);
623
- process.exit(1);
624
- }
625
- });
626
-
627
- const pricingMonitor = program.command("pricing-monitor").description("Manage seller pricing drift monitor");
628
-
629
- pricingMonitor
630
- .command("status")
631
- .description("Show whether seller pricing drift monitor is enabled")
632
- .action(async () => {
633
- try {
634
- const client = getClient();
635
- const document = await getSellerConfig(client);
636
- console.log(`Pricing drift monitor: ${document.pricingDriftMonitorEnabled === true ? "enabled" : "disabled"}`);
637
- } catch (err: any) {
638
- console.error("Error:", err.message);
639
- process.exit(1);
640
- }
641
- });
642
-
643
- pricingMonitor
644
- .command("enable")
645
- .description("Enable seller pricing drift monitor")
646
- .action(async () => {
647
- try {
648
- const client = getClient();
649
- const document = await getSellerConfig(client);
650
- document.pricingDriftMonitorEnabled = true;
651
- const data = await putSellerConfig(client, document);
652
- console.log(`Pricing drift monitor enabled: ${sellerConfigUpdateSummary(data, document)}`);
653
- } catch (err: any) {
654
- console.error("Error:", err.message);
655
- process.exit(1);
656
- }
657
- });
658
-
659
- pricingMonitor
660
- .command("disable")
661
- .description("Disable seller pricing drift monitor")
662
- .action(async () => {
663
- try {
664
- const client = getClient();
665
- const document = await getSellerConfig(client);
666
- document.pricingDriftMonitorEnabled = false;
667
- const data = await putSellerConfig(client, document);
668
- console.log(`Pricing drift monitor disabled: ${sellerConfigUpdateSummary(data, document)}`);
669
- } catch (err: any) {
670
- console.error("Error:", err.message);
671
- process.exit(1);
672
- }
673
- });
674
-
675
- // 5. Seller Runtime Config Command
676
- const sellerConfig = program.command("seller-config").description("Manage seller runtime YAML config");
677
-
678
- sellerConfig
679
- .command("get")
680
- .description("Fetch seller runtime config")
681
- .action(async () => {
682
- try {
683
- const client = getClient();
684
- const document = await getSellerConfig(client);
685
- console.log(YAML.dump(document, { lineWidth: 120, noRefs: true, sortKeys: false }));
686
- } catch (err: any) {
687
- console.error("Error:", err.message);
688
- process.exit(1);
689
- }
690
- });
691
-
692
- sellerConfig
693
- .command("put")
694
- .description("Update seller runtime config from YAML")
695
- .requiredOption("--file <path>", "Seller runtime YAML config file")
696
- .action(async (options) => {
697
- try {
698
- const document = loadSellerConfigFile(options.file);
699
- const client = getClient();
700
- const response = await putSellerConfig(client, document);
701
- console.log(`Updated seller config: path=${response.configPath || "memory"} upstream=${response.config?.upstreamUrl || document.upstreamUrl}`);
702
- } catch (err: any) {
703
- console.error("Error:", err.message);
704
- process.exit(1);
705
- }
706
- });
707
-
708
- sellerConfig
709
- .command("validate")
710
- .description("Validate seller runtime YAML config")
711
- .requiredOption("--file <path>", "Seller runtime YAML config file")
712
- .action((options) => {
713
- try {
714
- const document = loadSellerConfigFile(options.file);
715
- console.log(`Seller config valid: upstream=${document.upstreamUrl} models=${Array.isArray(document.models) ? document.models.length : 0}`);
716
- } catch (err: any) {
717
- console.error("Error:", err.message);
718
- process.exit(1);
719
- }
720
- });
721
-
722
- // 5. Billing Command
723
- const billing = program.command("billing").description("Query payment and inference billing records");
724
-
725
- billing
726
- .command("purchases")
727
- .description("List all remote token purchase / payment transactions")
728
- .action(async () => {
729
- try {
730
- const client = getClient();
731
- const data = await client.get("/operator/admin/purchases");
732
- const table = new Table({ head: ["Purchase ID", "Provider", "Amount", "State", "Date"] });
733
-
734
- for (const p of data.purchases || []) {
735
- table.push([
736
- p.purchase_id,
737
- p.payment_provider,
738
- `${p.amount_micros} micros`,
739
- p.state,
740
- p.created_at
741
- ]);
742
- }
743
- console.log("=== Remote Token Purchase / Payment History ===");
744
- console.log(table.toString());
745
- } catch (err: any) {
746
- console.error("Error:", err.message);
747
- }
748
- });
749
-
750
- billing
751
- .command("requests")
752
- .description("List all remote inference usage consumption logs")
753
- .action(async () => {
754
- try {
755
- const client = getClient();
756
- const data = await client.get("/operator/admin/requests");
757
- const table = new Table({ head: ["Request ID", "Model", "State", "Reserved", "Settled", "Tokens (I/O)", "Date"] });
758
-
759
- for (const r of data.requests || []) {
760
- table.push([
761
- r.request_id,
762
- r.model,
763
- r.state,
764
- `${r.reserved_micros} micros`,
765
- `${r.settled_micros} micros`,
766
- `${r.prompt_tokens} / ${r.completion_tokens}`,
767
- r.created_at
768
- ]);
769
- }
770
- console.log("=== Inference Usage Consumption Ledger ===");
771
- console.log(table.toString());
772
- } catch (err: any) {
773
- console.error("Error:", err.message);
774
- }
775
- });
776
-
777
- // 6. Bootstrap Registry Command
778
- const bootstrap = program.command("bootstrap").description("Manage wallet bootstrap service");
779
- const bootstrapSellers = bootstrap.command("sellers").description("Manage public seller registry");
780
- const bootstrapDefaultSeller = bootstrap.command("default-seller").description("Manage bootstrap default seller");
781
- const bootstrapRegistry = bootstrap.command("registry").description("Manage signed registry versions and publishing");
782
- const bootstrapConfig = bootstrap.command("config").description("Manage wallet bootstrap YAML config");
783
-
784
- bootstrapSellers
785
- .command("get")
786
- .description("Fetch public seller registry or one seller by id")
787
- .argument("[id]", "Seller id")
788
- .action(async (id?: string) => {
789
- try {
790
- const data = id
791
- ? await getClient().get(`/operator/registry/sellers/${encodeURIComponent(id)}`)
792
- : await publicGet("/registry/sellers") as SellerRegistryDocument;
793
- console.log(JSON.stringify(data, null, 2));
794
- } catch (err: any) {
795
- console.error("Error:", err.message);
796
- process.exit(1);
797
- }
798
- });
799
-
800
- bootstrapSellers
801
- .command("list")
802
- .description("List seller entries from the operator registry")
803
- .action(async () => {
804
- try {
805
- const data = await getClient().get("/operator/registry/sellers") as { sellers: any[] };
806
- const table = new Table({ head: ["ID", "Status", "URL", "Models", "Protocols", "Payments"] });
807
- for (const seller of data.sellers || []) {
808
- table.push([
809
- seller.id,
810
- seller.status || "active",
811
- seller.url,
812
- String(seller.models?.length || seller.modelsCount || 0),
813
- (seller.supportedProtocols || []).join(","),
814
- (seller.paymentMethods || []).join(",")
815
- ]);
816
- }
817
- console.log(table.toString());
818
- } catch (err: any) {
819
- console.error("Error:", err.message);
820
- process.exit(1);
821
- }
822
- });
823
-
824
- bootstrapSellers
825
- .command("add")
826
- .description("Add one seller entry without replacing the full registry")
827
- .requiredOption("--file <path>", "Seller entry YAML/JSON file")
828
- .requiredOption("--expect-version <n>", "Expected current registry version")
829
- .option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
830
- .action(async (options) => {
831
- try {
832
- const seller = loadSellerRegistryEntryFile(options.file);
833
- const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
834
- const response = await getClient().post(
835
- "/operator/registry/sellers",
836
- withExpectedVersion(seller, options.expectVersion),
837
- headers
838
- ) as SellerRegistryDocument;
839
- console.log(`Added seller ${seller.id}: version=${response.version} sellers=${response.sellers.length} publish=pending`);
840
- } catch (err: any) {
841
- console.error("Error:", err.message);
842
- process.exit(1);
843
- }
844
- });
845
-
846
- bootstrapSellers
847
- .command("update <id>")
848
- .description("Patch one seller entry without replacing the full registry")
849
- .requiredOption("--file <path>", "Seller patch YAML/JSON file")
850
- .requiredOption("--expect-version <n>", "Expected current registry version")
851
- .option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
852
- .action(async (id, options) => {
853
- try {
854
- const patch = loadSellerRegistryPatchFile(options.file);
855
- const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
856
- const response = await getClient().patch(
857
- `/operator/registry/sellers/${encodeURIComponent(id)}`,
858
- withExpectedVersion(patch, options.expectVersion),
859
- headers
860
- ) as SellerRegistryDocument;
861
- console.log(`Updated seller ${id}: version=${response.version} publish=pending`);
862
- } catch (err: any) {
863
- console.error("Error:", err.message);
864
- process.exit(1);
865
- }
866
- });
867
-
868
- bootstrapSellers
869
- .command("status <id> <status>")
870
- .description("Set one seller registry status: active, draining, or offline")
871
- .requiredOption("--expect-version <n>", "Expected current registry version")
872
- .option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
873
- .action(async (id, status, options) => {
874
- try {
875
- const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
876
- const response = await getClient().put(
877
- `/operator/registry/sellers/${encodeURIComponent(id)}/status`,
878
- withExpectedVersion({ status }, options.expectVersion),
879
- headers
880
- ) as SellerRegistryDocument;
881
- console.log(`Set seller ${id} status=${status}: version=${response.version} publish=pending`);
882
- } catch (err: any) {
883
- console.error("Error:", err.message);
884
- process.exit(1);
885
- }
886
- });
887
-
888
- bootstrapSellers
889
- .command("remove <id>")
890
- .description("Remove one non-active seller entry")
891
- .requiredOption("--expect-version <n>", "Expected current registry version")
892
- .option("--force", "Allow forced removal when the service permits it")
893
- .option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
894
- .action(async (id, options) => {
895
- try {
896
- const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
897
- const response = await getClient().delete(
898
- `/operator/registry/sellers/${encodeURIComponent(id)}`,
899
- withExpectedVersion({ force: Boolean(options.force) }, options.expectVersion),
900
- headers
901
- ) as SellerRegistryDocument;
902
- console.log(`Removed seller ${id}: version=${response.version} sellers=${response.sellers.length} publish=pending`);
903
- } catch (err: any) {
904
- console.error("Error:", err.message);
905
- process.exit(1);
906
- }
907
- });
908
-
909
- bootstrapSellers
910
- .command("put")
911
- .description("Deprecated: dangerously replace public seller registry")
912
- .requiredOption("--file <path>", "Seller registry JSON file")
913
- .option("--force", "Acknowledge this full replacement is dangerous")
914
- .action(async (options) => {
915
- try {
916
- if (!options.force) {
917
- throw new Error("bootstrap sellers put is deprecated; use bootstrap registry import --dry-run first, then --force");
918
- }
919
- const document = loadRegistryFile(options.file);
920
- const client = getClient();
921
- const response = await client.put("/operator/registry/sellers", document) as SellerRegistryDocument;
922
- console.log(`Dangerously replaced seller registry: version=${response.version} sellers=${response.sellers.length} publish=pending`);
923
- } catch (err: any) {
924
- console.error("Error:", err.message);
925
- process.exit(1);
926
- }
927
- });
928
-
929
- bootstrapSellers
930
- .command("validate")
931
- .description("Validate seller registry JSON")
932
- .requiredOption("--file <path>", "Seller registry JSON file")
933
- .action((options) => {
934
- try {
935
- const document = loadRegistryFile(options.file);
936
- validateRegistryDocument(document);
937
- console.log(`Seller registry valid: version=${document.version} sellers=${document.sellers.length}`);
938
- } catch (err: any) {
939
- console.error("Error:", err.message);
940
- process.exit(1);
941
- }
942
- });
943
-
944
- bootstrapDefaultSeller
945
- .command("set <id>")
946
- .description("Set default seller")
947
- .requiredOption("--expect-version <n>", "Expected current registry version")
948
- .option("--idempotency-key <key>", "Idempotency key for retry-safe writes")
949
- .action(async (id, options) => {
950
- try {
951
- const headers = options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined;
952
- const response = await getClient().put(
953
- "/operator/registry/default-seller",
954
- withExpectedVersion({ sellerId: id }, options.expectVersion),
955
- headers
956
- ) as SellerRegistryDocument;
957
- console.log(`Set default seller ${id}: version=${response.version} publish=pending`);
958
- } catch (err: any) {
959
- console.error("Error:", err.message);
960
- process.exit(1);
961
- }
962
- });
963
-
964
- bootstrapRegistry
965
- .command("diff")
966
- .description("Compare a local registry JSON file against the operator registry")
967
- .requiredOption("--file <path>", "Seller registry JSON file")
968
- .action(async (options) => {
969
- try {
970
- const local = loadRegistryFile(options.file);
971
- const remote = await getClient().get("/operator/registry") as SellerRegistryDocument;
972
- const localIds = new Set(local.sellers.map((seller) => seller.id));
973
- const remoteIds = new Set(remote.sellers.map((seller) => seller.id));
974
- const added = local.sellers.filter((seller) => !remoteIds.has(seller.id)).map((seller) => seller.id);
975
- const removed = remote.sellers.filter((seller) => !localIds.has(seller.id)).map((seller) => seller.id);
976
- const changed = local.sellers
977
- .filter((seller) => remoteIds.has(seller.id))
978
- .filter((seller) => JSON.stringify(seller) !== JSON.stringify(remote.sellers.find((entry) => entry.id === seller.id)))
979
- .map((seller) => seller.id);
980
- console.log(JSON.stringify({
981
- remoteVersion: remote.version,
982
- localVersion: local.version,
983
- added,
984
- removed,
985
- changed,
986
- defaultSellerChanged: remote.defaultSeller !== local.defaultSeller
987
- }, null, 2));
988
- } catch (err: any) {
989
- console.error("Error:", err.message);
990
- process.exit(1);
991
- }
992
- });
993
-
994
- bootstrapRegistry
995
- .command("import")
996
- .description("Import a full registry JSON as a dangerous disaster-recovery operation")
997
- .requiredOption("--file <path>", "Seller registry JSON file")
998
- .option("--dry-run", "Only validate and show diff")
999
- .option("--force", "Actually perform the full replacement")
1000
- .option("--expect-version <n>", "Expected current registry version before import")
1001
- .option("--force-delete-count <n>", "Maximum allowed delete count when forcing", "1")
1002
- .action(async (options) => {
1003
- try {
1004
- const local = loadRegistryFile(options.file);
1005
- const remote = await getClient().get("/operator/registry") as SellerRegistryDocument;
1006
- const localIds = new Set(local.sellers.map((seller) => seller.id));
1007
- const removed = remote.sellers.filter((seller) => !localIds.has(seller.id));
1008
- const forceDeleteCount = Number(options.forceDeleteCount);
1009
- if (!Number.isInteger(forceDeleteCount) || forceDeleteCount < 0) {
1010
- throw new Error("--force-delete-count must be >= 0");
1011
- }
1012
- console.log(`Import diff: remoteVersion=${remote.version} localVersion=${local.version} removes=${removed.length}`);
1013
- if (options.dryRun || !options.force) {
1014
- if (!options.dryRun) {
1015
- throw new Error("refusing full import without --force; run with --dry-run first");
1016
- }
1017
- return;
1018
- }
1019
- if (removed.length > forceDeleteCount) {
1020
- throw new Error(`import would remove ${removed.length} sellers; pass --force-delete-count ${removed.length} to allow`);
1021
- }
1022
- const body = withExpectedVersion(local as any, options.expectVersion);
1023
- const response = await getClient().put("/operator/registry/sellers", body) as SellerRegistryDocument;
1024
- console.log(`Imported seller registry: version=${response.version} sellers=${response.sellers.length} publish=pending`);
1025
- } catch (err: any) {
1026
- console.error("Error:", err.message);
1027
- process.exit(1);
1028
- }
1029
- });
1030
-
1031
- bootstrapRegistry
1032
- .command("publish")
1033
- .description("Publish the current signed registry to R2")
1034
- .option("--version <n>", "Registry version to publish")
1035
- .action(async (options) => {
1036
- try {
1037
- const version = options.version ? Number(options.version) : undefined;
1038
- if (version !== undefined && (!Number.isInteger(version) || version < 1)) {
1039
- throw new Error("--version must be a positive integer");
1040
- }
1041
- const body = version === undefined ? {} : { version };
1042
- const response = await getClient().post("/operator/registry/publish", body) as {
1043
- version: number;
1044
- registrySha256: string;
1045
- signingKeyId: string;
1046
- artifacts?: Array<{ key: string; url: string }>;
1047
- };
1048
- console.log(`Published registry version=${response.version} sha256=${response.registrySha256} signingKey=${response.signingKeyId}`);
1049
- for (const artifact of response.artifacts || []) {
1050
- console.log(` ${artifact.key} -> ${artifact.url}`);
1051
- }
1052
- } catch (err: any) {
1053
- console.error("Error:", err.message);
1054
- process.exit(1);
1055
- }
1056
- });
1057
-
1058
- bootstrapRegistry
1059
- .command("versions")
1060
- .description("List registry versions")
1061
- .option("--limit <n>", "Maximum versions to list", "20")
1062
- .action(async (options) => {
1063
- try {
1064
- const limit = Number(options.limit);
1065
- if (!Number.isInteger(limit) || limit < 1) {
1066
- throw new Error("--limit must be a positive integer");
1067
- }
1068
- const response = await getClient().get(`/operator/registry/versions?limit=${encodeURIComponent(String(limit))}`) as {
1069
- versions?: Array<{
1070
- version: number;
1071
- registrySha256: string;
1072
- signingKeyId?: string;
1073
- signed?: boolean;
1074
- sellerCount?: number;
1075
- defaultSeller?: string;
1076
- createdAt?: string;
1077
- publishedAt?: string;
1078
- }>;
1079
- };
1080
- for (const version of response.versions || []) {
1081
- console.log([
1082
- `version=${version.version}`,
1083
- `sellers=${version.sellerCount ?? "?"}`,
1084
- `default=${version.defaultSeller || "-"}`,
1085
- `signed=${version.signed ? "yes" : "no"}`,
1086
- `published=${version.publishedAt || "pending"}`,
1087
- `sha256=${version.registrySha256}`,
1088
- `key=${version.signingKeyId || "-"}`
1089
- ].join(" "));
1090
- }
1091
- } catch (err: any) {
1092
- console.error("Error:", err.message);
1093
- process.exit(1);
1094
- }
1095
- });
1096
-
1097
- bootstrapConfig
1098
- .command("get")
1099
- .description("Fetch wallet bootstrap runtime config")
1100
- .action(async () => {
1101
- try {
1102
- const client = getClient();
1103
- const data = await client.get("/operator/config") as BootstrapConfigDocument;
1104
- console.log(YAML.dump(data, { lineWidth: 120, noRefs: true }));
1105
- } catch (err: any) {
1106
- console.error("Error:", err.message);
1107
- process.exit(1);
1108
- }
1109
- });
1110
-
1111
- bootstrapConfig
1112
- .command("put")
1113
- .description("Update wallet bootstrap runtime config from YAML")
1114
- .requiredOption("--file <path>", "Wallet bootstrap YAML config file")
1115
- .action(async (options) => {
1116
- try {
1117
- const document = loadBootstrapConfigFile(options.file);
1118
- const client = getClient();
1119
- const response = await client.put("/operator/config", document) as BootstrapConfigDocument;
1120
- console.log(`Updated wallet bootstrap config: skillSlug=${response.clawtip.skillSlug} registry=${response.sellerRegistryPath}`);
1121
- } catch (err: any) {
1122
- console.error("Error:", err.message);
1123
- process.exit(1);
1124
- }
1125
- });
1126
-
1127
- bootstrapConfig
1128
- .command("validate")
1129
- .description("Validate wallet bootstrap YAML config")
1130
- .requiredOption("--file <path>", "Wallet bootstrap YAML config file")
1131
- .action((options) => {
1132
- try {
1133
- const document = loadBootstrapConfigFile(options.file);
1134
- console.log(`Wallet bootstrap config valid: skillSlug=${document.clawtip.skillSlug} registry=${document.sellerRegistryPath}`);
1135
- } catch (err: any) {
1136
- console.error("Error:", err.message);
1137
- process.exit(1);
1138
- }
1139
- });
1140
-
1141
- // 7. Config Command (Local)
1142
- const configCmd = program.command("config").description("Manage local admin profiles");
1143
-
1144
- configCmd
1145
- .command("set <profileName>")
1146
- .description("Add or update an admin connection profile")
1147
- .option("--url <url>", "Remote seller core API url")
1148
- .option("--token <token>", "Bearer Operator secret token")
1149
- .action((profileName, options) => {
1150
- const opts = program.opts();
1151
- const configPath = opts.config;
1152
- const mgr = configPath ? new ConfigManager(configPath) : configManager;
1153
-
1154
- const url = options.url || opts.url || process.env.TOKENBUDDY_ADMIN_URL;
1155
- const token = options.token || opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
1156
-
1157
- if (!url || !token) {
1158
- console.error("error: required option '--url <url>' and '--token <token>' not specified");
1159
- process.exit(1);
1160
- }
1161
-
1162
- mgr.setProfile(profileName, { url, token });
1163
- console.log(`Profile \`${profileName}\` successfully configured.`);
1164
- });
1165
-
1166
- configCmd
1167
- .command("use <profileName>")
1168
- .description("Switch default profile to select")
1169
- .action((profileName) => {
1170
- const opts = program.opts();
1171
- const configPath = opts.config;
1172
- const mgr = configPath ? new ConfigManager(configPath) : configManager;
1173
-
1174
- try {
1175
- mgr.useProfile(profileName);
1176
- console.log(`Now using profile \`${profileName}\` by default.`);
1177
- } catch (err: any) {
1178
- console.error("Error:", err.message);
1179
- }
1180
- });
1181
-
1182
- configCmd
1183
- .command("list")
1184
- .description("List all configured profiles in the active config file")
1185
- .action(() => {
1186
- const opts = program.opts();
1187
- const configPath = opts.config;
1188
- const mgr = configPath ? new ConfigManager(configPath) : configManager;
1189
-
1190
- const profiles = mgr.listProfiles();
1191
- const config = mgr.load();
1192
- console.log(`=== Configured Local Profiles (config: ${mgr.getConfigPath() || "default"}) ===`);
1193
- for (const p of profiles) {
1194
- const isDefault = p === config.default_profile ? "* " : " ";
1195
- const details = config.profiles[p];
1196
- console.log(`${isDefault}${p} -> ${details.url}`);
1197
- }
1198
- });
1199
-
1200
- // Step 14 (v1.1): 多 vendor 实例隔离.
1201
- // 默认 config dir 改成 ~/.config/tokenbuddy/profiles/*.toml, 每个 vendor 一份 config.
1202
- // 启动时 tb-admin (没传 --config) 会扫描 profiles/ 合并成一个虚拟配置 (default_profile 仍来自
1203
- // ~/.config/tokenbuddy/admin.toml). `tb-admin config ls` 列出当前 active file,
1204
- // `tb-admin config profiles` 扫描 profiles/ 列出所有 vendor file.
1205
- configCmd
1206
- .command("profiles")
1207
- .description("Scan the vendor profiles directory (~/.config/tokenbuddy/profiles/) and list every vendor config")
1208
- .action(() => {
1209
- const profileDir = defaultProfileDir();
1210
- const files = listProfileFiles(profileDir);
1211
- if (files.length === 0) {
1212
- console.log(`(no vendor profiles in ${profileDir})`);
1213
- console.log("Create one with: tb-admin --config <file> config set <vendor> --url <url> --token <token>");
1214
- return;
1215
- }
1216
- console.log(`=== Vendor Profiles (${profileDir}) ===`);
1217
- for (const file of files) {
1218
- try {
1219
- const mgr = new ConfigManager(file);
1220
- const config = mgr.load();
1221
- const profileNames = mgr.listProfiles();
1222
- const firstProfile = profileNames[0];
1223
- const url = firstProfile ? config.profiles[firstProfile]?.url : "(no profiles)";
1224
- console.log(` ${file}\n -> default=${config.default_profile || firstProfile || "?"} url=${url || "?"}`);
1225
- } catch (err: any) {
1226
- console.log(` ${file}\n -> (parse failed: ${err.message})`);
1227
- }
1228
- }
1229
- });
1230
-
1231
- configCmd
1232
- .command("where")
1233
- .description("Print the config file path in use (resolves --config, env, or default)")
1234
- .action(() => {
1235
- const opts = program.opts();
1236
- const configPath = opts.config || process.env.TOKENBUDDY_ADMIN_CONFIG;
1237
- const resolved = configPath || `${process.env.HOME || ""}/.config/tokenbuddy/admin.toml`;
1238
- console.log(resolved);
1239
- });
1240
-
1241
- // 8. Seller Command (Fly.io)
1242
- const sellerCmd = program.command("seller").description("Deploy and manage seller containers on Fly.io");
1243
-
1244
- function getFlyProvider(): FlyProvider {
1245
- const opts = program.opts();
1246
- const mgr = opts.config ? new ConfigManager(opts.config) : configManager;
1247
- return new FlyProvider(mgr.getSellerProvider("fly"));
1248
- }
1249
-
1250
- function getSellerRunner(): SellerCommandRunner {
1251
- const opts = program.opts();
1252
- const mgr = opts.config ? new ConfigManager(opts.config) : configManager;
1253
- return new SellerCommandRunner(mgr);
1254
- }
1255
-
1256
- function printResult(res: unknown, json: boolean): void {
1257
- if (json) {
1258
- // json 路径 seller.ts 返回的是结构化 object; 直接 stringify 美化
1259
- if (typeof res === "string") {
1260
- // 防御: 任何子命令意外返回 string 都直接原样打
1261
- console.log(res);
1262
- } else {
1263
- console.log(JSON.stringify(res, null, 2));
1264
- }
1265
- } else {
1266
- // 文本路径: seller.ts 返回 string (1.0.31 行为) 或 dry-run string
1267
- console.log(res);
1268
- }
1269
- }
1270
-
1271
- sellerCmd
1272
- .command("ls")
1273
- .description("List all deployed apps on Fly.io")
1274
- .option("--json", "Output structured JSON instead of human-readable table")
1275
- .action((options) => {
1276
- try {
1277
- const res = getSellerRunner().ls(Boolean(options.json));
1278
- printResult(res, Boolean(options.json));
1279
- } catch (err: any) {
1280
- console.error("Error:", err.message);
1281
- process.exit(1);
1282
- }
1283
- });
1284
-
1285
- sellerCmd
1286
- .command("status <app>")
1287
- .description("Show Fly.io status for a specific seller app")
1288
- .option("--json", "Output structured JSON")
1289
- .action((app, options) => {
1290
- try {
1291
- const res = getSellerRunner().status(app, Boolean(options.json));
1292
- printResult(res, Boolean(options.json));
1293
- } catch (err: any) {
1294
- console.error("Error:", err.message);
1295
- process.exit(1);
1296
- }
1297
- });
1298
-
1299
- sellerCmd
1300
- .command("create <name>")
1301
- .description("Deploy a new machines instance on Fly.io")
1302
- .option("--app <app>", "Fly.io app name (bypasses tb-seller- prefix; use tbs-<random> format)")
1303
- .option("--region <region>", "Fly region (default: sin)", "sin")
1304
- .requiredOption("--image <image>", "Published Docker image, for example registry.fly.io/tb-seller:<v>")
1305
- .requiredOption("--fly-config <path>", "Fly.io config file path, for example deploy/fly.io/fly.tb-seller.toml")
1306
- .option("--volume-name <name>", "Persistent volume name")
1307
- .option("--volume-size-gb <gb>", "Persistent volume size in GB", (v) => parseInt(v, 10))
1308
- .option("--volume-id <id>", "Attach existing volume by ID (skips volume creation)")
1309
- .option("--volume-snapshot-retention-days <days>", "Volume snapshot retention days", (v) => parseInt(v, 10))
1310
- .option("--initial-config <path>", "Initial seller YAML config to inject as TOKENBUDDY_SELLER_CONFIG_B64")
1311
- .requiredOption("--operator-secret <secret>", "Operator secret to configure")
1312
- .option("--dry-run", "Dry run display without actual execution")
1313
- .option("--json", "Output structured JSON (dry-run lists planned flyctl commands; real run captures summary)")
1314
- .action((name, options) => {
1315
- try {
1316
- const res = getSellerRunner().create(
1317
- {
1318
- name,
1319
- app: options.app,
1320
- region: options.region,
1321
- image: options.image,
1322
- flyConfig: options.flyConfig,
1323
- volumeName: options.volumeName,
1324
- volumeSizeGb: options.volumeSizeGb,
1325
- volumeId: options.volumeId,
1326
- volumeSnapshotRetentionDays: options.volumeSnapshotRetentionDays,
1327
- initialConfigPath: options.initialConfig,
1328
- operatorSecret: options.operatorSecret,
1329
- dryRun: options.dryRun
1330
- },
1331
- Boolean(options.json)
1332
- );
1333
- printResult(res, Boolean(options.json));
1334
- } catch (err: any) {
1335
- console.error("Error:", err.message);
1336
- process.exit(1);
1337
- }
1338
- });
1339
-
1340
- sellerCmd
1341
- .command("deploy <app>")
1342
- .description("Update an existing seller app's Machines to an explicit image without changing Fly.io config or volumes")
1343
- .requiredOption("--image <image>", "Published Docker image, for example registry.fly.io/tb-seller:<v>")
1344
- .option("--dry-run", "Dry run")
1345
- .option("--json", "Output structured JSON")
1346
- .action((app, options) => {
1347
- try {
1348
- const res = getSellerRunner().deploy(
1349
- { app, image: options.image, dryRun: options.dryRun },
1350
- Boolean(options.json)
1351
- );
1352
- printResult(res, Boolean(options.json));
1353
- } catch (err: any) {
1354
- console.error("Error:", err.message);
1355
- process.exit(1);
1356
- }
1357
- });
1358
-
1359
- sellerCmd
1360
- .command("roll")
1361
- .description("Sequentially redeploy every tbs-* seller app (live candidate list from `fly apps list`) to a single image, image-only, fail-fast. Replaces deploy/fly.io/deploy-seller-fleet-to-flyio.sh.")
1362
- .requiredOption("--image <image>", "Published Docker image, for example registry.fly.io/tb-seller:<v>")
1363
- .option("--exclude <list>", "Comma-separated list of seller app names to skip (e.g. tbs-86d81e,tbs-719577)")
1364
- .option("--dry-run", "List the planned candidate set and per-app plans, do not actually redeploy")
1365
- .option("--json", "Output structured JSON: candidates / excluded / attempts / completed")
1366
- .action((options) => {
1367
- try {
1368
- const exclude = options.exclude
1369
- ? String(options.exclude).split(",").map((s) => s.trim()).filter(Boolean)
1370
- : [];
1371
- const res = getSellerRunner().roll(
1372
- { image: options.image, exclude, dryRun: options.dryRun },
1373
- Boolean(options.json)
1374
- );
1375
- printResult(res, Boolean(options.json));
1376
- // 文本路径下, 失败时给非零退出码 (CI / script 可感知)
1377
- if (!options.json && typeof res === "string" && res.includes("roll stopped at failure")) {
1378
- process.exit(1);
1379
- }
1380
- // json 路径下, completed=false 也给非零
1381
- if (options.json && typeof res !== "string" && res.completed === false) {
1382
- process.exit(1);
1383
- }
1384
- } catch (err: any) {
1385
- console.error("Error:", err.message);
1386
- process.exit(1);
1387
- }
1388
- });
1389
-
1390
- sellerCmd
1391
- .command("remove <name>")
1392
- .description("Completely destroy a seller app on Fly.io")
1393
- .option("--app <app>", "Fly.io app name (use if name is ambiguous or already a full app name)")
1394
- .option("--remove-profile", "Also delete the corresponding profile from local admin.toml")
1395
- .option("--dry-run", "Dry run")
1396
- .option("--json", "Output structured JSON")
1397
- .action((name, options) => {
1398
- try {
1399
- const appName = options.app || name;
1400
- const res = getSellerRunner().remove(appName, Boolean(options.dryRun), Boolean(options.json));
1401
- printResult(res, Boolean(options.json));
1402
- if (options.removeProfile && !options.dryRun && !options.json) {
1403
- const appBase = appName.includes("-") ? appName.replace(/^tb-seller-/, "") : appName;
1404
- const profileNames = [`tbs-1`, `tbs-2`, `tbs-3`, `tbs-4`, `tbs-5`];
1405
- const toRemove = profileNames.find((p) => {
1406
- const cfg = configManager.listProfiles().includes(p) &&
1407
- configManager.getProfile(p)?.url?.includes(appName);
1408
- return cfg;
1409
- });
1410
- if (toRemove) {
1411
- configManager.removeProfile(toRemove);
1412
- console.log(`Removed local profile \`${toRemove}\`.`);
1413
- } else {
1414
- console.log(`No matching local profile found for ${appName} — skipped.`);
1415
- }
1416
- }
1417
- } catch (err: any) {
1418
- console.error("Error:", err.message);
1419
- process.exit(1);
1420
- }
1421
- });
1422
-
1423
- // ------------------------------------------------------------------------
1424
- // 7. Vendor domain commands (Step 5 of the registry redesign)
1425
- //
1426
- // vendor 走 `/platform/*` (Bearer vendor token, 不是 super-admin key).
1427
- // 这些命令不替代 platform publish / config* / default-seller / seller
1428
- // create-deploy-remove-ls; vendor 现在只能 stage 一个 seller + 提交
1429
- // release request. 旧命令保留 (兼容迁移期 smoke), 但 `bootstrap
1430
- // registry publish` / `bootstrap config*` / `bootstrap default-seller
1431
- // set` / `seller create|deploy|remove|ls` 在 server 端已不再服务
1432
- // super-admin 域 (Step 5 删 / Step 7 删 server). 客户端先行禁用:
1433
- // 这些子命令保留 parser, 但 runtime 立刻报 "deprecated" 错误.
1434
- // ------------------------------------------------------------------------
1435
-
1436
- const vendorBootstrap = program.command("vendor-bootstrap")
1437
- .description("Vendor-domain seller and release management (registry redesign Step 5+)")
1438
- .option("--base-url <url>", "Override wallet-bootstrap base URL");
1439
-
1440
- vendorBootstrap
1441
- .command("me")
1442
- .description("Show the authenticated vendor identity")
1443
- .action(async () => {
1444
- try {
1445
- const result = await getClient().get("/platform/me");
1446
- console.log(JSON.stringify(result, null, 2));
1447
- } catch (err: any) {
1448
- console.error("Error:", err.message);
1449
- process.exit(1);
1450
- }
1451
- });
1452
-
1453
- vendorBootstrap
1454
- .command("sellers")
1455
- .description("List live sellers visible to the authenticated vendor")
1456
- .action(async () => {
1457
- try {
1458
- const result = await getClient().get("/platform/sellers");
1459
- const rows = (result.sellers as any[]) || [];
1460
- const table = new Table({ head: ["ID", "URL", "Status", "Models"] });
1461
- for (const seller of rows) {
1462
- table.push([
1463
- seller.id,
1464
- seller.url,
1465
- seller.status || "active",
1466
- String(seller.models?.length || 0)
1467
- ]);
1468
- }
1469
- console.log(table.toString());
1470
- } catch (err: any) {
1471
- console.error("Error:", err.message);
1472
- process.exit(1);
1473
- }
1474
- });
1475
-
1476
- vendorBootstrap
1477
- .command("stage")
1478
- .description("Stage a seller entry for inclusion in the next release request")
1479
- .requiredOption("--file <path>", "Seller entry JSON file")
1480
- .action(async (options) => {
1481
- try {
1482
- const result = await getClient().post("/platform/sellers/stage", JSON.parse(fs.readFileSync(options.file, "utf8")));
1483
- console.log(`Staged ${(result as any).pendingSeller.id} (status: ${(result as any).pendingSeller.status})`);
1484
- } catch (err: any) {
1485
- console.error("Error:", err.message);
1486
- process.exit(1);
1487
- }
1488
- });
1489
-
1490
- vendorBootstrap
1491
- .command("pending")
1492
- .description("List staged pending sellers for the authenticated vendor")
1493
- .option("--status <status>", "Filter by status: staged|published|rejected", "staged")
1494
- .action(async (options) => {
1495
- try {
1496
- const result = await getClient().get(`/platform/sellers/pending?status=${encodeURIComponent(options.status)}`);
1497
- const rows = (result as any).pendingSellers as any[];
1498
- if (rows.length === 0) {
1499
- console.log(`(no ${options.status} pending sellers)`);
1500
- return;
1501
- }
1502
- const table = new Table({ head: ["ID", "Status", "Submitted", "Decided"] });
1503
- for (const row of rows) {
1504
- table.push([row.id, row.status, row.submittedAt, row.decidedAt || "—"]);
1505
- }
1506
- console.log(table.toString());
1507
- } catch (err: any) {
1508
- console.error("Error:", err.message);
1509
- process.exit(1);
1510
- }
1511
- });
1512
-
1513
- const releaseCmd = vendorBootstrap
1514
- .command("release")
1515
- .description("Submit and inspect release requests");
1516
-
1517
- releaseCmd
1518
- .command("submit")
1519
- .description("Submit a release request that includes one or more staged sellers")
1520
- .option("--staged <id...>", "One or more staged seller ids to include", collectIds, [])
1521
- .option("--note <text>", "Note explaining the release")
1522
- .action(async (options) => {
1523
- try {
1524
- const result = await getClient().post("/platform/release-requests", {
1525
- stagedSellerIds: options.staged,
1526
- note: options.note
1527
- });
1528
- const record = (result as any).releaseRequest;
1529
- console.log(`Submitted release request #${record.id} (status: ${record.status})`);
1530
- console.log(` sellers: ${record.payloadSummary.count} (${(record.payloadSummary.sellerIds || []).join(", ") || "—"})`);
1531
- } catch (err: any) {
1532
- console.error("Error:", err.message);
1533
- process.exit(1);
1534
- }
1535
- });
1536
-
1537
- releaseCmd
1538
- .command("list")
1539
- .description("List release requests for the authenticated vendor")
1540
- .option("--limit <n>", "Maximum number of records to return", "20")
1541
- .action(async (options) => {
1542
- try {
1543
- const result = await getClient().get(`/platform/release-requests?limit=${encodeURIComponent(options.limit)}`);
1544
- const rows = (result as any).releaseRequests as any[];
1545
- if (rows.length === 0) {
1546
- console.log("(no release requests)");
1547
- return;
1548
- }
1549
- const table = new Table({ head: ["ID", "Status", "Sellers", "Submitted", "Version"] });
1550
- for (const r of rows) {
1551
- table.push([
1552
- `#${r.id}`,
1553
- r.status,
1554
- String(r.payloadSummary?.count || 0),
1555
- r.submittedAt,
1556
- r.publishedVersion !== null ? `v${r.publishedVersion}` : "—"
1557
- ]);
1558
- }
1559
- console.log(table.toString());
1560
- } catch (err: any) {
1561
- console.error("Error:", err.message);
1562
- process.exit(1);
1563
- }
1564
- });
1565
-
1566
- releaseCmd
1567
- .command("show <id>")
1568
- .description("Show a single release request")
1569
- .action(async (id) => {
1570
- try {
1571
- const result = await getClient().get(`/platform/release-requests/${encodeURIComponent(id)}`);
1572
- const r = (result as any).releaseRequest;
1573
- if (!r) {
1574
- console.error("Release request not found");
1575
- process.exit(1);
1576
- }
1577
- console.log(JSON.stringify(r, null, 2));
1578
- } catch (err: any) {
1579
- console.error("Error:", err.message);
1580
- process.exit(1);
1581
- }
1582
- });
1583
-
1584
- return program;
1585
- }
1586
-
1587
- function collectIds(value: string, previous: string[]): string[] {
1588
- return previous.concat([value]);
1589
- }
1590
-
1591
- // Step 14 (v1.1): 多 vendor 实例 config 隔离.
1592
- // 默认 profiles 目录是 ~/.config/tokenbuddy/profiles/, 每个 vendor 一份独立 toml.
1593
- // 默认主 config 仍走 ~/.config/tokenbuddy/admin.toml (default_profile 在此设).
1594
- // 用户可以:
1595
- // - tb-admin --config /path/to/vendor-A.toml config ls (单 vendor 显式)
1596
- // - tb-admin config profiles (扫 profiles/ 看所有 vendor)
1597
- // - TOKENBUDDY_CONFIG_DIR=/somewhere tb-admin ... (env override)
1598
- function defaultProfileDir(): string {
1599
- const override = process.env.TOKENBUDDY_CONFIG_DIR;
1600
- if (override) {
1601
- return path.join(override, "profiles");
1602
- }
1603
- return path.join(process.env.HOME || "", ".config", "tokenbuddy", "profiles");
1604
- }
1605
-
1606
- function listProfileFiles(dir: string): string[] {
1607
- if (!fs.existsSync(dir)) {
1608
- return [];
1609
- }
1610
- return fs.readdirSync(dir)
1611
- .filter((name) => name.endsWith(".toml") || name.endsWith(".config"))
1612
- .map((name) => path.join(dir, name))
1613
- .sort();
1614
- }