@tokenbuddy/tb-admin 1.0.36 → 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.
- package/dist/src/cli.js +92 -19
- package/dist/src/config.d.ts +7 -1
- package/dist/src/config.js +16 -4
- package/dist/src/display-format.js +6 -14
- package/dist/src/init-command.d.ts +50 -0
- package/dist/src/init-command.js +347 -0
- package/dist/src/providers/fly-io.d.ts +3 -0
- package/dist/src/providers/fly-io.js +137 -0
- package/dist/src/providers/provider-definition.d.ts +38 -0
- package/dist/src/providers/provider-definition.js +2 -0
- package/dist/src/seller.d.ts +2 -0
- package/dist/src/seller.js +30 -13
- package/dist/src/server-cmd.d.ts +1 -0
- package/dist/src/server-cmd.js +9 -2
- package/dist/src/ui-actions.d.ts +3 -0
- package/dist/src/ui-actions.js +199 -27
- package/dist/src/ui-command.js +3 -2
- package/dist/src/ui-state.d.ts +1 -3
- package/dist/src/ui-state.js +4 -8
- package/dist/src/ui-static.js +43 -15
- package/dist/src/workdir.d.ts +21 -0
- package/dist/src/workdir.js +50 -0
- package/package.json +8 -2
- package/templates/providers/fly.io/admin.toml.example +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
- package/templates/providers/fly.io/env/deploy.env.example +12 -0
- package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
- package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
- package/templates/providers/fly.io/provider.toml.example +10 -0
- package/dist/src/bootstrap-registry.d.ts.map +0 -1
- package/dist/src/bootstrap-registry.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js.map +0 -1
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/display-format.d.ts.map +0 -1
- package/dist/src/display-format.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/provider.d.ts.map +0 -1
- package/dist/src/provider.js.map +0 -1
- package/dist/src/seller.d.ts.map +0 -1
- package/dist/src/seller.js.map +0 -1
- package/dist/src/server-cmd.d.ts.map +0 -1
- package/dist/src/server-cmd.js.map +0 -1
- package/dist/src/ui-actions.d.ts.map +0 -1
- package/dist/src/ui-actions.js.map +0 -1
- package/dist/src/ui-command.d.ts.map +0 -1
- package/dist/src/ui-command.js.map +0 -1
- package/dist/src/ui-server.d.ts.map +0 -1
- package/dist/src/ui-server.js.map +0 -1
- package/dist/src/ui-state.d.ts.map +0 -1
- package/dist/src/ui-state.js.map +0 -1
- package/dist/src/ui-static.d.ts.map +0 -1
- package/dist/src/ui-static.js.map +0 -1
- package/dist/src/upstream-balance-probe.d.ts.map +0 -1
- package/dist/src/upstream-balance-probe.js.map +0 -1
- package/dist/src/vendor-client.d.ts.map +0 -1
- package/dist/src/vendor-client.js.map +0 -1
- package/dist/src/vendor-commands.d.ts.map +0 -1
- package/dist/src/vendor-commands.js.map +0 -1
- package/src/bootstrap-registry.ts +0 -90
- package/src/cli.ts +0 -1614
- package/src/client.ts +0 -179
- package/src/config.ts +0 -194
- package/src/display-format.ts +0 -411
- package/src/index.ts +0 -11
- package/src/provider.ts +0 -150
- package/src/seller.ts +0 -538
- package/src/server-cmd.ts +0 -362
- package/src/ui-actions.ts +0 -1040
- package/src/ui-command.ts +0 -44
- package/src/ui-server.ts +0 -353
- package/src/ui-state.ts +0 -1318
- package/src/ui-static.ts +0 -673
- package/src/upstream-balance-probe.ts +0 -13
- package/src/vendor-client.ts +0 -23
- package/src/vendor-commands.ts +0 -65
- package/tests/admin.test.ts +0 -2162
- package/tests/seller.test.ts +0 -388
- package/tests/ui-state-fleet.test.ts +0 -526
- package/tests/ui-static-row.test.ts +0 -467
- package/tests/vendor-cli.test.ts +0 -241
- 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
|
-
}
|