@tokenbuddy/tb-admin 1.0.31 → 1.0.33
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.d.ts.map +1 -1
- package/dist/src/cli.js +280 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +82 -2
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +93 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/provider.d.ts +120 -0
- package/dist/src/provider.d.ts.map +1 -0
- package/dist/src/provider.js +73 -0
- package/dist/src/provider.js.map +1 -0
- package/dist/src/seller.d.ts +104 -0
- package/dist/src/seller.d.ts.map +1 -0
- package/dist/src/seller.js +283 -0
- package/dist/src/seller.js.map +1 -0
- package/dist/src/ui-actions.d.ts +25 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +81 -11
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +15 -2
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +77 -2
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +242 -14
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +98 -20
- package/dist/src/ui-static.js.map +1 -1
- package/dist/src/vendor-client.d.ts +23 -0
- package/dist/src/vendor-client.d.ts.map +1 -0
- package/dist/src/vendor-client.js +2 -0
- package/dist/src/vendor-client.js.map +1 -0
- package/dist/src/vendor-commands.d.ts +35 -0
- package/dist/src/vendor-commands.d.ts.map +1 -0
- package/dist/src/vendor-commands.js +33 -0
- package/dist/src/vendor-commands.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +305 -31
- package/src/client.ts +118 -2
- package/src/provider.ts +150 -0
- package/src/seller.ts +362 -0
- package/src/ui-actions.ts +89 -11
- package/src/ui-server.ts +15 -1
- package/src/ui-state.ts +293 -15
- package/src/ui-static.ts +98 -20
- package/src/vendor-client.ts +23 -0
- package/src/vendor-commands.ts +65 -0
- package/tests/admin.test.ts +81 -3
- package/tests/seller.test.ts +337 -0
- package/tests/ui-state-fleet.test.ts +257 -0
- package/tests/ui-static-row.test.ts +202 -0
- package/tests/vendor-cli.test.ts +241 -0
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { ConfigManager } from "./config.js";
|
|
3
3
|
import { AdminClient } from "./client.js";
|
|
4
4
|
import { FlyProvider } from "./server-cmd.js";
|
|
5
|
+
import { SellerCommandRunner } from "./seller.js";
|
|
5
6
|
import { bindAdminUiCommand } from "./ui-command.js";
|
|
6
7
|
import {
|
|
7
8
|
loadRegistryFile,
|
|
@@ -208,8 +209,9 @@ function withExpectedVersion(body: Record<string, any>, expectedVersion: unknown
|
|
|
208
209
|
return body;
|
|
209
210
|
}
|
|
210
211
|
const version = Number(expectedVersion);
|
|
211
|
-
|
|
212
|
-
|
|
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");
|
|
213
215
|
}
|
|
214
216
|
return { ...body, expectedVersion: version };
|
|
215
217
|
}
|
|
@@ -1179,7 +1181,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1179
1181
|
|
|
1180
1182
|
configCmd
|
|
1181
1183
|
.command("list")
|
|
1182
|
-
.description("List all configured profiles")
|
|
1184
|
+
.description("List all configured profiles in the active config file")
|
|
1183
1185
|
.action(() => {
|
|
1184
1186
|
const opts = program.opts();
|
|
1185
1187
|
const configPath = opts.config;
|
|
@@ -1187,7 +1189,7 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1187
1189
|
|
|
1188
1190
|
const profiles = mgr.listProfiles();
|
|
1189
1191
|
const config = mgr.load();
|
|
1190
|
-
console.log(
|
|
1192
|
+
console.log(`=== Configured Local Profiles (config: ${mgr.getConfigPath() || "default"}) ===`);
|
|
1191
1193
|
for (const p of profiles) {
|
|
1192
1194
|
const isDefault = p === config.default_profile ? "* " : " ";
|
|
1193
1195
|
const details = config.profiles[p];
|
|
@@ -1195,6 +1197,47 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1195
1197
|
}
|
|
1196
1198
|
});
|
|
1197
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
|
+
|
|
1198
1241
|
// 8. Seller Command (Fly.io)
|
|
1199
1242
|
const sellerCmd = program.command("seller").description("Deploy and manage seller containers on Fly.io");
|
|
1200
1243
|
|
|
@@ -1204,13 +1247,49 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1204
1247
|
return new FlyProvider(mgr.getSellerProvider("fly"));
|
|
1205
1248
|
}
|
|
1206
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
|
+
|
|
1207
1271
|
sellerCmd
|
|
1208
1272
|
.command("ls")
|
|
1209
1273
|
.description("List all deployed apps on Fly.io")
|
|
1210
|
-
.
|
|
1274
|
+
.option("--json", "Output structured JSON instead of human-readable table")
|
|
1275
|
+
.action((options) => {
|
|
1211
1276
|
try {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
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));
|
|
1214
1293
|
} catch (err: any) {
|
|
1215
1294
|
console.error("Error:", err.message);
|
|
1216
1295
|
process.exit(1);
|
|
@@ -1231,23 +1310,27 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1231
1310
|
.option("--initial-config <path>", "Initial seller YAML config to inject as TOKENBUDDY_SELLER_CONFIG_B64")
|
|
1232
1311
|
.requiredOption("--operator-secret <secret>", "Operator secret to configure")
|
|
1233
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)")
|
|
1234
1314
|
.action((name, options) => {
|
|
1235
1315
|
try {
|
|
1236
|
-
const res =
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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));
|
|
1251
1334
|
} catch (err: any) {
|
|
1252
1335
|
console.error("Error:", err.message);
|
|
1253
1336
|
process.exit(1);
|
|
@@ -1259,14 +1342,14 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1259
1342
|
.description("Update an existing seller app's Machines to an explicit image without changing Fly.io config or volumes")
|
|
1260
1343
|
.requiredOption("--image <image>", "Published Docker image, for example registry.fly.io/tb-seller:<v>")
|
|
1261
1344
|
.option("--dry-run", "Dry run")
|
|
1345
|
+
.option("--json", "Output structured JSON")
|
|
1262
1346
|
.action((app, options) => {
|
|
1263
1347
|
try {
|
|
1264
|
-
const res =
|
|
1265
|
-
app,
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
console.log(res);
|
|
1348
|
+
const res = getSellerRunner().deploy(
|
|
1349
|
+
{ app, image: options.image, dryRun: options.dryRun },
|
|
1350
|
+
Boolean(options.json)
|
|
1351
|
+
);
|
|
1352
|
+
printResult(res, Boolean(options.json));
|
|
1270
1353
|
} catch (err: any) {
|
|
1271
1354
|
console.error("Error:", err.message);
|
|
1272
1355
|
process.exit(1);
|
|
@@ -1279,12 +1362,13 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1279
1362
|
.option("--app <app>", "Fly.io app name (use if name is ambiguous or already a full app name)")
|
|
1280
1363
|
.option("--remove-profile", "Also delete the corresponding profile from local admin.toml")
|
|
1281
1364
|
.option("--dry-run", "Dry run")
|
|
1365
|
+
.option("--json", "Output structured JSON")
|
|
1282
1366
|
.action((name, options) => {
|
|
1283
1367
|
try {
|
|
1284
1368
|
const appName = options.app || name;
|
|
1285
|
-
const res =
|
|
1286
|
-
|
|
1287
|
-
if (options.removeProfile && !options.dryRun) {
|
|
1369
|
+
const res = getSellerRunner().remove(appName, Boolean(options.dryRun), Boolean(options.json));
|
|
1370
|
+
printResult(res, Boolean(options.json));
|
|
1371
|
+
if (options.removeProfile && !options.dryRun && !options.json) {
|
|
1288
1372
|
const appBase = appName.includes("-") ? appName.replace(/^tb-seller-/, "") : appName;
|
|
1289
1373
|
const profileNames = [`tbs-1`, `tbs-2`, `tbs-3`, `tbs-4`, `tbs-5`];
|
|
1290
1374
|
const toRemove = profileNames.find((p) => {
|
|
@@ -1305,5 +1389,195 @@ export function buildAdminCli(configManager: ConfigManager): Command {
|
|
|
1305
1389
|
}
|
|
1306
1390
|
});
|
|
1307
1391
|
|
|
1392
|
+
// ------------------------------------------------------------------------
|
|
1393
|
+
// 7. Vendor domain commands (Step 5 of the registry redesign)
|
|
1394
|
+
//
|
|
1395
|
+
// vendor 走 `/platform/*` (Bearer vendor token, 不是 super-admin key).
|
|
1396
|
+
// 这些命令不替代 platform publish / config* / default-seller / seller
|
|
1397
|
+
// create-deploy-remove-ls; vendor 现在只能 stage 一个 seller + 提交
|
|
1398
|
+
// release request. 旧命令保留 (兼容迁移期 smoke), 但 `bootstrap
|
|
1399
|
+
// registry publish` / `bootstrap config*` / `bootstrap default-seller
|
|
1400
|
+
// set` / `seller create|deploy|remove|ls` 在 server 端已不再服务
|
|
1401
|
+
// super-admin 域 (Step 5 删 / Step 7 删 server). 客户端先行禁用:
|
|
1402
|
+
// 这些子命令保留 parser, 但 runtime 立刻报 "deprecated" 错误.
|
|
1403
|
+
// ------------------------------------------------------------------------
|
|
1404
|
+
|
|
1405
|
+
const vendorBootstrap = program.command("vendor-bootstrap")
|
|
1406
|
+
.description("Vendor-domain seller and release management (registry redesign Step 5+)")
|
|
1407
|
+
.option("--base-url <url>", "Override wallet-bootstrap base URL");
|
|
1408
|
+
|
|
1409
|
+
vendorBootstrap
|
|
1410
|
+
.command("me")
|
|
1411
|
+
.description("Show the authenticated vendor identity")
|
|
1412
|
+
.action(async () => {
|
|
1413
|
+
try {
|
|
1414
|
+
const result = await getClient().get("/platform/me");
|
|
1415
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1416
|
+
} catch (err: any) {
|
|
1417
|
+
console.error("Error:", err.message);
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
vendorBootstrap
|
|
1423
|
+
.command("sellers")
|
|
1424
|
+
.description("List live sellers visible to the authenticated vendor")
|
|
1425
|
+
.action(async () => {
|
|
1426
|
+
try {
|
|
1427
|
+
const result = await getClient().get("/platform/sellers");
|
|
1428
|
+
const rows = (result.sellers as any[]) || [];
|
|
1429
|
+
const table = new Table({ head: ["ID", "URL", "Status", "Models"] });
|
|
1430
|
+
for (const seller of rows) {
|
|
1431
|
+
table.push([
|
|
1432
|
+
seller.id,
|
|
1433
|
+
seller.url,
|
|
1434
|
+
seller.status || "active",
|
|
1435
|
+
String(seller.models?.length || 0)
|
|
1436
|
+
]);
|
|
1437
|
+
}
|
|
1438
|
+
console.log(table.toString());
|
|
1439
|
+
} catch (err: any) {
|
|
1440
|
+
console.error("Error:", err.message);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
vendorBootstrap
|
|
1446
|
+
.command("stage")
|
|
1447
|
+
.description("Stage a seller entry for inclusion in the next release request")
|
|
1448
|
+
.requiredOption("--file <path>", "Seller entry JSON file")
|
|
1449
|
+
.action(async (options) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const result = await getClient().post("/platform/sellers/stage", JSON.parse(fs.readFileSync(options.file, "utf8")));
|
|
1452
|
+
console.log(`Staged ${(result as any).pendingSeller.id} (status: ${(result as any).pendingSeller.status})`);
|
|
1453
|
+
} catch (err: any) {
|
|
1454
|
+
console.error("Error:", err.message);
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
vendorBootstrap
|
|
1460
|
+
.command("pending")
|
|
1461
|
+
.description("List staged pending sellers for the authenticated vendor")
|
|
1462
|
+
.option("--status <status>", "Filter by status: staged|published|rejected", "staged")
|
|
1463
|
+
.action(async (options) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const result = await getClient().get(`/platform/sellers/pending?status=${encodeURIComponent(options.status)}`);
|
|
1466
|
+
const rows = (result as any).pendingSellers as any[];
|
|
1467
|
+
if (rows.length === 0) {
|
|
1468
|
+
console.log(`(no ${options.status} pending sellers)`);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const table = new Table({ head: ["ID", "Status", "Submitted", "Decided"] });
|
|
1472
|
+
for (const row of rows) {
|
|
1473
|
+
table.push([row.id, row.status, row.submittedAt, row.decidedAt || "—"]);
|
|
1474
|
+
}
|
|
1475
|
+
console.log(table.toString());
|
|
1476
|
+
} catch (err: any) {
|
|
1477
|
+
console.error("Error:", err.message);
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const releaseCmd = vendorBootstrap
|
|
1483
|
+
.command("release")
|
|
1484
|
+
.description("Submit and inspect release requests");
|
|
1485
|
+
|
|
1486
|
+
releaseCmd
|
|
1487
|
+
.command("submit")
|
|
1488
|
+
.description("Submit a release request that includes one or more staged sellers")
|
|
1489
|
+
.option("--staged <id...>", "One or more staged seller ids to include", collectIds, [])
|
|
1490
|
+
.option("--note <text>", "Note explaining the release")
|
|
1491
|
+
.action(async (options) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const result = await getClient().post("/platform/release-requests", {
|
|
1494
|
+
stagedSellerIds: options.staged,
|
|
1495
|
+
note: options.note
|
|
1496
|
+
});
|
|
1497
|
+
const record = (result as any).releaseRequest;
|
|
1498
|
+
console.log(`Submitted release request #${record.id} (status: ${record.status})`);
|
|
1499
|
+
console.log(` sellers: ${record.payloadSummary.count} (${(record.payloadSummary.sellerIds || []).join(", ") || "—"})`);
|
|
1500
|
+
} catch (err: any) {
|
|
1501
|
+
console.error("Error:", err.message);
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
releaseCmd
|
|
1507
|
+
.command("list")
|
|
1508
|
+
.description("List release requests for the authenticated vendor")
|
|
1509
|
+
.option("--limit <n>", "Maximum number of records to return", "20")
|
|
1510
|
+
.action(async (options) => {
|
|
1511
|
+
try {
|
|
1512
|
+
const result = await getClient().get(`/platform/release-requests?limit=${encodeURIComponent(options.limit)}`);
|
|
1513
|
+
const rows = (result as any).releaseRequests as any[];
|
|
1514
|
+
if (rows.length === 0) {
|
|
1515
|
+
console.log("(no release requests)");
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const table = new Table({ head: ["ID", "Status", "Sellers", "Submitted", "Version"] });
|
|
1519
|
+
for (const r of rows) {
|
|
1520
|
+
table.push([
|
|
1521
|
+
`#${r.id}`,
|
|
1522
|
+
r.status,
|
|
1523
|
+
String(r.payloadSummary?.count || 0),
|
|
1524
|
+
r.submittedAt,
|
|
1525
|
+
r.publishedVersion !== null ? `v${r.publishedVersion}` : "—"
|
|
1526
|
+
]);
|
|
1527
|
+
}
|
|
1528
|
+
console.log(table.toString());
|
|
1529
|
+
} catch (err: any) {
|
|
1530
|
+
console.error("Error:", err.message);
|
|
1531
|
+
process.exit(1);
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
releaseCmd
|
|
1536
|
+
.command("show <id>")
|
|
1537
|
+
.description("Show a single release request")
|
|
1538
|
+
.action(async (id) => {
|
|
1539
|
+
try {
|
|
1540
|
+
const result = await getClient().get(`/platform/release-requests/${encodeURIComponent(id)}`);
|
|
1541
|
+
const r = (result as any).releaseRequest;
|
|
1542
|
+
if (!r) {
|
|
1543
|
+
console.error("Release request not found");
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1547
|
+
} catch (err: any) {
|
|
1548
|
+
console.error("Error:", err.message);
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1308
1553
|
return program;
|
|
1309
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
function collectIds(value: string, previous: string[]): string[] {
|
|
1557
|
+
return previous.concat([value]);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Step 14 (v1.1): 多 vendor 实例 config 隔离.
|
|
1561
|
+
// 默认 profiles 目录是 ~/.config/tokenbuddy/profiles/, 每个 vendor 一份独立 toml.
|
|
1562
|
+
// 默认主 config 仍走 ~/.config/tokenbuddy/admin.toml (default_profile 在此设).
|
|
1563
|
+
// 用户可以:
|
|
1564
|
+
// - tb-admin --config /path/to/vendor-A.toml config ls (单 vendor 显式)
|
|
1565
|
+
// - tb-admin config profiles (扫 profiles/ 看所有 vendor)
|
|
1566
|
+
// - TOKENBUDDY_CONFIG_DIR=/somewhere tb-admin ... (env override)
|
|
1567
|
+
function defaultProfileDir(): string {
|
|
1568
|
+
const override = process.env.TOKENBUDDY_CONFIG_DIR;
|
|
1569
|
+
if (override) {
|
|
1570
|
+
return path.join(override, "profiles");
|
|
1571
|
+
}
|
|
1572
|
+
return path.join(process.env.HOME || "", ".config", "tokenbuddy", "profiles");
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function listProfileFiles(dir: string): string[] {
|
|
1576
|
+
if (!fs.existsSync(dir)) {
|
|
1577
|
+
return [];
|
|
1578
|
+
}
|
|
1579
|
+
return fs.readdirSync(dir)
|
|
1580
|
+
.filter((name) => name.endsWith(".toml") || name.endsWith(".config"))
|
|
1581
|
+
.map((name) => path.join(dir, name))
|
|
1582
|
+
.sort();
|
|
1583
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* 内部封装 Bearer Token 鉴权、JSON 序列化、非 2xx 抛错的统一行为。
|
|
4
4
|
*/
|
|
5
5
|
export class AdminClient {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
protected baseUrl: string;
|
|
7
|
+
protected token: string;
|
|
8
8
|
|
|
9
9
|
constructor(baseUrl: string, token: string) {
|
|
10
10
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
@@ -61,3 +61,119 @@ export class AdminClient {
|
|
|
61
61
|
return this.request(path, "DELETE", body, headers);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Vendor domain client. Step 5 of the registry redesign: vendor
|
|
67
|
+
* tokens call `/platform/*` (Bearer auth). The old `bootstrap
|
|
68
|
+
* config*`, `bootstrap registry publish`, and `bootstrap default-
|
|
69
|
+
* seller set` commands have been removed; vendors stage sellers and
|
|
70
|
+
* submit release requests instead.
|
|
71
|
+
*/
|
|
72
|
+
export class RegistryVendorClient extends AdminClient {
|
|
73
|
+
public async me(): Promise<{ vendor: { id: string; name: string; status: string } }> {
|
|
74
|
+
return this.get("/platform/me");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async listSellers(): Promise<{ sellers: unknown[] }> {
|
|
78
|
+
return this.get("/platform/sellers");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Step 14 (v1.1): vendor-scoped seller status mutation.
|
|
83
|
+
* Replaces the old `tb-admin bootstrap sellers status` path which called
|
|
84
|
+
* /operator/registry/sellers/:id/status (operator-level, super-admin scope).
|
|
85
|
+
* Vendor can only mutate sellers in its own scope; the platform vendor
|
|
86
|
+
* (DEFAULT_PLATFORM_VENDOR_ID) can mutate any.
|
|
87
|
+
*/
|
|
88
|
+
public async setSellerStatus(
|
|
89
|
+
id: string,
|
|
90
|
+
status: "active" | "draining" | "offline",
|
|
91
|
+
options: { expectedVersion?: number; idempotencyKey?: string } = {}
|
|
92
|
+
): Promise<{ registry: { version: number; sellers: unknown[] } }> {
|
|
93
|
+
const headers: { [key: string]: string } = {};
|
|
94
|
+
if (options.idempotencyKey) {
|
|
95
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
96
|
+
}
|
|
97
|
+
return this.put(
|
|
98
|
+
`/platform/sellers/${encodeURIComponent(id)}/status`,
|
|
99
|
+
{
|
|
100
|
+
status,
|
|
101
|
+
...(options.expectedVersion !== undefined ? { expectedVersion: options.expectedVersion } : {})
|
|
102
|
+
},
|
|
103
|
+
headers
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async stageSeller(seller: unknown): Promise<{ pendingSeller: unknown }> {
|
|
108
|
+
return this.post("/platform/sellers/stage", { seller });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public async listPendingSellers(status?: string): Promise<{ pendingSellers: unknown[] }> {
|
|
112
|
+
const path = status ? `/platform/sellers/pending?status=${encodeURIComponent(status)}` : "/platform/sellers/pending";
|
|
113
|
+
return this.get(path);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async submitRelease(input: { stagedSellerIds: string[]; note?: string }): Promise<{ releaseRequest: unknown }> {
|
|
117
|
+
return this.post("/platform/release-requests", input);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async listReleaseRequests(limit = 20): Promise<{ releaseRequests: unknown[] }> {
|
|
121
|
+
return this.get(`/platform/release-requests?limit=${limit}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async getReleaseRequest(id: number): Promise<{ releaseRequest: unknown }> {
|
|
125
|
+
return this.get(`/platform/release-requests/${id}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Super-admin client. Used by automated scripts / CI to drive the
|
|
131
|
+
* admin web endpoints (force-publish, registry versions, audit).
|
|
132
|
+
* Step 5 keeps this narrow; the human-facing flow is the browser.
|
|
133
|
+
*/
|
|
134
|
+
export class RegistryAdminClient extends AdminClient {
|
|
135
|
+
constructor(baseUrl: string, superAdminKey: string) {
|
|
136
|
+
// We pass the super-admin key as the "token" but override the
|
|
137
|
+
// Authorization header below — super-admin uses the
|
|
138
|
+
// `X-Registry-Admin-Key` header, not Bearer.
|
|
139
|
+
super(baseUrl, superAdminKey);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected overrideHeaders(headers: Record<string, string>): Record<string, string> {
|
|
143
|
+
return { ...headers, "X-Registry-Admin-Key": this.token };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async requestAdmin(path: string, method: string, body?: any): Promise<any> {
|
|
147
|
+
const url = `${this.baseUrl}${path}`;
|
|
148
|
+
const headers = this.overrideHeaders({ "Content-Type": "application/json" });
|
|
149
|
+
const options: RequestInit = { method, headers };
|
|
150
|
+
if (body) options.body = JSON.stringify(body);
|
|
151
|
+
const response = await fetch(url, options);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const text = await response.text();
|
|
154
|
+
throw new Error(`HTTP Error ${response.status}: ${text || response.statusText}`);
|
|
155
|
+
}
|
|
156
|
+
const text = await response.text();
|
|
157
|
+
return text ? JSON.parse(text) : {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public async listVendors(): Promise<{ vendors: unknown[] }> {
|
|
161
|
+
return this.requestAdmin("/platform/vendors", "GET");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async createVendor(name: string): Promise<{ vendor: unknown; token: string }> {
|
|
165
|
+
return this.requestAdmin("/platform/vendors", "POST", { name });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public async forcePublish(releaseId: number): Promise<{ releaseRequest: unknown; publishedVersion: number | null }> {
|
|
169
|
+
return this.requestAdmin(`/admin/api/release-requests/${releaseId}/force-publish`, "POST");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async listVersions(): Promise<{ versions: unknown[] }> {
|
|
173
|
+
return this.requestAdmin("/admin/api/registry-versions", "GET");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public async listAudit(): Promise<{ records: unknown[] }> {
|
|
177
|
+
return this.requestAdmin("/admin/api/audit", "GET");
|
|
178
|
+
}
|
|
179
|
+
}
|