@tokenbuddy/tb-admin 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/src/cli.js +92 -19
  2. package/dist/src/config.d.ts +7 -1
  3. package/dist/src/config.js +16 -4
  4. package/dist/src/display-format.js +6 -14
  5. package/dist/src/init-command.d.ts +50 -0
  6. package/dist/src/init-command.js +347 -0
  7. package/dist/src/providers/fly-io.d.ts +3 -0
  8. package/dist/src/providers/fly-io.js +137 -0
  9. package/dist/src/providers/provider-definition.d.ts +38 -0
  10. package/dist/src/providers/provider-definition.js +2 -0
  11. package/dist/src/seller.d.ts +2 -0
  12. package/dist/src/seller.js +30 -13
  13. package/dist/src/server-cmd.d.ts +1 -0
  14. package/dist/src/server-cmd.js +9 -2
  15. package/dist/src/ui-actions.d.ts +3 -0
  16. package/dist/src/ui-actions.js +199 -27
  17. package/dist/src/ui-command.js +3 -2
  18. package/dist/src/ui-state.d.ts +1 -3
  19. package/dist/src/ui-state.js +4 -8
  20. package/dist/src/ui-static.js +43 -15
  21. package/dist/src/workdir.d.ts +21 -0
  22. package/dist/src/workdir.js +50 -0
  23. package/package.json +8 -2
  24. package/templates/providers/fly.io/admin.toml.example +18 -0
  25. package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
  26. package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
  27. package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
  28. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
  29. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
  30. package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
  31. package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
  32. package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
  33. package/templates/providers/fly.io/env/deploy.env.example +12 -0
  34. package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
  35. package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
  36. package/templates/providers/fly.io/provider.toml.example +10 -0
  37. package/dist/src/bootstrap-registry.d.ts.map +0 -1
  38. package/dist/src/bootstrap-registry.js.map +0 -1
  39. package/dist/src/cli.d.ts.map +0 -1
  40. package/dist/src/cli.js.map +0 -1
  41. package/dist/src/client.d.ts.map +0 -1
  42. package/dist/src/client.js.map +0 -1
  43. package/dist/src/config.d.ts.map +0 -1
  44. package/dist/src/config.js.map +0 -1
  45. package/dist/src/display-format.d.ts.map +0 -1
  46. package/dist/src/display-format.js.map +0 -1
  47. package/dist/src/index.d.ts.map +0 -1
  48. package/dist/src/index.js.map +0 -1
  49. package/dist/src/provider.d.ts.map +0 -1
  50. package/dist/src/provider.js.map +0 -1
  51. package/dist/src/seller.d.ts.map +0 -1
  52. package/dist/src/seller.js.map +0 -1
  53. package/dist/src/server-cmd.d.ts.map +0 -1
  54. package/dist/src/server-cmd.js.map +0 -1
  55. package/dist/src/ui-actions.d.ts.map +0 -1
  56. package/dist/src/ui-actions.js.map +0 -1
  57. package/dist/src/ui-command.d.ts.map +0 -1
  58. package/dist/src/ui-command.js.map +0 -1
  59. package/dist/src/ui-server.d.ts.map +0 -1
  60. package/dist/src/ui-server.js.map +0 -1
  61. package/dist/src/ui-state.d.ts.map +0 -1
  62. package/dist/src/ui-state.js.map +0 -1
  63. package/dist/src/ui-static.d.ts.map +0 -1
  64. package/dist/src/ui-static.js.map +0 -1
  65. package/dist/src/upstream-balance-probe.d.ts.map +0 -1
  66. package/dist/src/upstream-balance-probe.js.map +0 -1
  67. package/dist/src/vendor-client.d.ts.map +0 -1
  68. package/dist/src/vendor-client.js.map +0 -1
  69. package/dist/src/vendor-commands.d.ts.map +0 -1
  70. package/dist/src/vendor-commands.js.map +0 -1
  71. package/src/bootstrap-registry.ts +0 -90
  72. package/src/cli.ts +0 -1614
  73. package/src/client.ts +0 -179
  74. package/src/config.ts +0 -194
  75. package/src/display-format.ts +0 -411
  76. package/src/index.ts +0 -11
  77. package/src/provider.ts +0 -150
  78. package/src/seller.ts +0 -538
  79. package/src/server-cmd.ts +0 -362
  80. package/src/ui-actions.ts +0 -1040
  81. package/src/ui-command.ts +0 -44
  82. package/src/ui-server.ts +0 -353
  83. package/src/ui-state.ts +0 -1318
  84. package/src/ui-static.ts +0 -673
  85. package/src/upstream-balance-probe.ts +0 -13
  86. package/src/vendor-client.ts +0 -23
  87. package/src/vendor-commands.ts +0 -65
  88. package/tests/admin.test.ts +0 -2162
  89. package/tests/seller.test.ts +0 -388
  90. package/tests/ui-state-fleet.test.ts +0 -526
  91. package/tests/ui-static-row.test.ts +0 -467
  92. package/tests/vendor-cli.test.ts +0 -241
  93. package/tsconfig.json +0 -8
package/src/server-cmd.ts DELETED
@@ -1,362 +0,0 @@
1
- import { execSync, spawnSync, type SpawnSyncReturns } from "child_process";
2
- import * as fs from "fs";
3
- import { SellerProviderConfig } from "./config.js";
4
-
5
- type ExecRunner = (command: string, options?: Parameters<typeof execSync>[1]) => string | Buffer;
6
- type SpawnRunner = (command: string, args?: string[], options?: Parameters<typeof spawnSync>[2]) => SpawnSyncReturns<string | Buffer>;
7
-
8
- export interface DockerImageInspection {
9
- ok: boolean;
10
- error?: string;
11
- exitCode?: number;
12
- }
13
-
14
- export type ImageInspectRunner = (image: string) => DockerImageInspection;
15
-
16
- export interface FlyProviderRuntime {
17
- checkFlyctlInstalled?: (flyctlPath?: string) => boolean;
18
- execSync?: ExecRunner;
19
- spawnSync?: SpawnRunner;
20
- imageInspector?: ImageInspectRunner;
21
- }
22
-
23
- /**
24
- * 检查 flyctl 是否在 PATH 中(或在指定路径)。
25
- *
26
- * @param flyctlPath 可选 flyctl 完整路径
27
- * @returns 是否可用
28
- */
29
- export function checkFlyctlInstalled(flyctlPath?: string): boolean {
30
- try {
31
- execSync(`which ${flyctlPath || "flyctl"}`, { stdio: "ignore" });
32
- return true;
33
- } catch {
34
- return false;
35
- }
36
- }
37
-
38
- export function inspectDockerImage(image: string): DockerImageInspection {
39
- const result = spawnSync("docker", ["buildx", "imagetools", "inspect", image], {
40
- encoding: "utf8",
41
- stdio: ["ignore", "pipe", "pipe"]
42
- });
43
- return dockerImageInspectionFromResult(result);
44
- }
45
-
46
- export function requirePublishedDockerImage(image: string, inspectImage: ImageInspectRunner = inspectDockerImage): void {
47
- const inspection = inspectImage(image);
48
- if (inspection.ok) {
49
- return;
50
- }
51
- const detail = inspection.error ? ` Detail: ${inspection.error.trim()}` : "";
52
- throw new Error(
53
- `seller image is not published or is not accessible: ${image}. ` +
54
- `Publish it first with RELEASE_VERSION=<v> bash scripts/release/all.sh, or pass an existing registry.fly.io/tb-seller:<v> tag. ` +
55
- `No Fly app was created.${detail}`
56
- );
57
- }
58
-
59
- function dockerImageInspectionFromResult(result: SpawnSyncReturns<string>): DockerImageInspection {
60
- if (result.error) {
61
- return { ok: false, error: result.error.message };
62
- }
63
- if (result.status !== 0) {
64
- return {
65
- ok: false,
66
- exitCode: result.status ?? undefined,
67
- error: (result.stderr || result.stdout || "").toString()
68
- };
69
- }
70
- return { ok: true };
71
- }
72
-
73
- export function parseFlyMachineIds(json: string, app: string): string[] {
74
- let parsed: unknown;
75
- try {
76
- parsed = JSON.parse(json || "[]");
77
- } catch {
78
- throw new Error(`fly machines list returned invalid JSON for ${app}`);
79
- }
80
-
81
- if (!Array.isArray(parsed)) {
82
- throw new Error(`fly machines list returned an unexpected shape for ${app}`);
83
- }
84
-
85
- const ids = parsed
86
- .map((item) => {
87
- if (item && typeof item === "object" && "id" in item) {
88
- const id = (item as { id?: unknown }).id;
89
- return typeof id === "string" && id.length > 0 ? id : undefined;
90
- }
91
- return undefined;
92
- })
93
- .filter((id): id is string => Boolean(id));
94
-
95
- if (ids.length === 0) {
96
- throw new Error(`fly app ${app} has no machines to update`);
97
- }
98
-
99
- return ids;
100
- }
101
-
102
- /**
103
- * `FlyProvider.createSeller` 的输入。
104
- * 字段未提供时回退到 `SellerProviderConfig` 的默认值。
105
- */
106
- export interface SellerCreateOptions {
107
- name: string;
108
- app?: string;
109
- region?: string;
110
- image?: string;
111
- operatorSecret: string;
112
- flyConfig?: string;
113
- volumeName?: string;
114
- volumeSizeGb?: number;
115
- volumeId?: string;
116
- volumeSnapshotRetentionDays?: number;
117
- initialConfigPath?: string;
118
- dryRun?: boolean;
119
- }
120
-
121
- /**
122
- * Fly.io seller provider:把 admin CLI 的 `tb-admin server create` 命令转成 `flyctl` 调用。
123
- * 支持 `dryRun` 模式:只打印会执行的命令而不实际修改 Fly 资源。
124
- */
125
- export class FlyProvider {
126
- private providerConfig?: SellerProviderConfig;
127
- private readonly runtime: Required<FlyProviderRuntime>;
128
-
129
- constructor(providerConfig?: SellerProviderConfig, runtime?: FlyProviderRuntime) {
130
- this.providerConfig = providerConfig;
131
- this.runtime = {
132
- checkFlyctlInstalled,
133
- execSync,
134
- spawnSync,
135
- imageInspector: inspectDockerImage,
136
- ...runtime
137
- };
138
- }
139
-
140
- private get flyctl(): string {
141
- return this.providerConfig?.flyctl_path || "flyctl";
142
- }
143
-
144
- private flyExecOptions(options: Parameters<typeof execSync>[1] = {}): Parameters<typeof execSync>[1] {
145
- return {
146
- ...options,
147
- env: this.flyEnv(options.env)
148
- };
149
- }
150
-
151
- private flySpawnOptions(options: Parameters<typeof spawnSync>[2] = {}): Parameters<typeof spawnSync>[2] {
152
- return {
153
- ...options,
154
- env: this.flyEnv(options.env)
155
- };
156
- }
157
-
158
- private flyEnv(env: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
159
- const configuredToken = this.providerConfig?.token;
160
- const merged = {
161
- ...process.env,
162
- ...(env || {})
163
- };
164
- if (configuredToken && !merged.FLY_API_TOKEN) {
165
- merged.FLY_API_TOKEN = configuredToken;
166
- }
167
- return merged;
168
- }
169
-
170
- /**
171
- * List apps on Fly.io
172
- */
173
- public listApps(): string {
174
- if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
175
- throw new Error(`\`${this.flyctl}\` is not installed on your system PATH.`);
176
- }
177
- return this.runtime.execSync(`${this.flyctl} apps list`, this.flyExecOptions({ encoding: "utf8" })) as string;
178
- }
179
-
180
- /**
181
- * Create and deploy a new seller app on Fly.io
182
- */
183
- public createSeller(options: SellerCreateOptions): string {
184
- const { name, dryRun } = options;
185
- // --app overrides the auto-prefixed name; allows bare "tbs-86d81e" or "tb-seller-foo"
186
- const appName = options.app || `tb-seller-${name}`;
187
-
188
- const targetImage = options.image;
189
- const targetRegion = options.region
190
- || this.providerConfig?.default_region
191
- || "sin";
192
- const operatorSecret = options.operatorSecret
193
- || this.providerConfig?.operator_secret
194
- || "";
195
- const flyConfig = options.flyConfig;
196
- const volumeName = options.volumeName
197
- || this.providerConfig?.volume_name
198
- || "tb_seller_data";
199
- const volumeSizeGb = options.volumeSizeGb
200
- || this.providerConfig?.volume_size_gb
201
- || 1;
202
- const volumeSnapshotRetentionDays = options.volumeSnapshotRetentionDays;
203
- const volumeId = options.volumeId;
204
- const initialConfigPath = options.initialConfigPath;
205
-
206
- if (!targetImage) {
207
- throw new Error("seller create requires --image registry.fly.io/tb-seller:<v>");
208
- }
209
- if (!flyConfig) {
210
- throw new Error("seller create requires --fly-config deploy/fly.io/fly.tb-seller.toml");
211
- }
212
-
213
- if (dryRun) {
214
- const lines = [
215
- `[DRY-RUN] Will create fly app: ${appName}`,
216
- ` Region: ${targetRegion}`,
217
- ` Image: ${targetImage}`,
218
- ` Secret: OPERATOR_SECRET=***${operatorSecret.slice(-4) || "????"}`,
219
- ` Volume: ${volumeName} (${volumeSizeGb}GB)`,
220
- ];
221
- if (flyConfig) lines.push(` Fly config: ${flyConfig}`);
222
- if (initialConfigPath) lines.push(` Initial config secret: TOKENBUDDY_SELLER_CONFIG_B64 from ${initialConfigPath}`);
223
- if (volumeId) lines.push(` Volume ID: ${volumeId}`);
224
- if (volumeSnapshotRetentionDays !== undefined) lines.push(` Volume snapshot retention: ${volumeSnapshotRetentionDays} days`);
225
- return lines.join("\n");
226
- }
227
-
228
- if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
229
- throw new Error(`\`${this.flyctl}\` is not installed on PATH.`);
230
- }
231
-
232
- if (!operatorSecret) {
233
- throw new Error("operator_secret is required. Provide --operator-secret or configure seller_providers.fly.operator_secret");
234
- }
235
- requireReadableFile(flyConfig, "Fly config");
236
- if (initialConfigPath) {
237
- requireReadableFile(initialConfigPath, "Initial seller config");
238
- }
239
- requirePublishedDockerImage(targetImage, this.runtime.imageInspector);
240
-
241
- console.log(`[Fly.io] Creating app ${appName}...`);
242
- this.runtime.execSync(`${this.flyctl} apps create ${appName} --machines`, this.flyExecOptions({ stdio: "inherit" }));
243
-
244
- console.log(`[Fly.io] Setting secrets...`);
245
- this.importCreateSecrets(appName, operatorSecret, initialConfigPath);
246
-
247
- console.log(`[Fly.io] Deploying image ${targetImage}...`);
248
- const deployCmd = `${this.flyctl} deploy -c ${flyConfig} --image ${targetImage} --primary-region ${targetRegion} --app ${appName} --now`;
249
- this.runtime.execSync(deployCmd, this.flyExecOptions({ stdio: "inherit" }));
250
-
251
- return `Successfully deployed ${appName} on Fly.io`;
252
- }
253
-
254
- private importCreateSecrets(appName: string, operatorSecret: string, initialConfigPath: string | undefined): void {
255
- const lines = [
256
- "ALLOW_MOCK=false",
257
- `OPERATOR_SECRET=${operatorSecret}`
258
- ];
259
- if (initialConfigPath) {
260
- const configContent = fs.readFileSync(initialConfigPath, "utf8");
261
- lines.push(`TOKENBUDDY_SELLER_CONFIG_B64=${Buffer.from(configContent, "utf8").toString("base64")}`);
262
- }
263
- const result = this.runtime.spawnSync(this.flyctl, ["secrets", "import", "--stage", "--app", appName], this.flySpawnOptions({
264
- input: `${lines.join("\n")}\n`,
265
- stdio: ["pipe", "inherit", "inherit"]
266
- }));
267
- if (result.error) {
268
- throw result.error;
269
- }
270
- if (result.status !== 0) {
271
- throw new Error(`flyctl secrets import failed with exit code ${result.status}`);
272
- }
273
- }
274
-
275
- /**
276
- * Destroy a seller app on Fly.io.
277
- * @param nameOrApp Either a bare name (e.g. "86d81e") or a full app name (e.g. "tbs-86d81e").
278
- * If the value contains a hyphen, it is treated as the full app name.
279
- * @param dryRun
280
- */
281
- public removeSeller(nameOrApp: string, dryRun?: boolean): string {
282
- const appName = nameOrApp.includes("-") ? nameOrApp : `tb-seller-${nameOrApp}`;
283
- if (dryRun) {
284
- return `[DRY-RUN] Will destroy fly app: ${appName}`;
285
- }
286
-
287
- if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
288
- throw new Error(`\`${this.flyctl}\` is not installed.`);
289
- }
290
-
291
- console.log(`[Fly.io] Destroying app ${appName}...`);
292
- this.runtime.execSync(`${this.flyctl} apps destroy ${appName} --yes`, this.flyExecOptions({ stdio: "inherit" }));
293
-
294
- return `Successfully destroyed ${appName} on Fly.io`;
295
- }
296
-
297
- /**
298
- * Get status of a specific seller app
299
- */
300
- public statusApp(name: string): string {
301
- const appName = name.includes("-") ? name : `tb-seller-${name}`;
302
- if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
303
- throw new Error(`\`${this.flyctl}\` is not installed.`);
304
- }
305
- return this.runtime.execSync(`${this.flyctl} status --app ${appName}`, this.flyExecOptions({ encoding: "utf8" })) as string;
306
- }
307
-
308
- /**
309
- * Update an existing seller app's Machines to a new image.
310
- *
311
- * This intentionally does not call `fly deploy --config`: existing seller
312
- * deploys must not reconcile volumes, mounts, or service configuration.
313
- */
314
- public deploySeller(options: {
315
- app: string;
316
- config?: string;
317
- image?: string;
318
- dryRun?: boolean;
319
- }): string {
320
- const { app, image, dryRun } = options;
321
- const targetImage = image;
322
-
323
- if (!targetImage) {
324
- throw new Error("seller deploy requires --image registry.fly.io/tb-seller:<v>");
325
- }
326
-
327
- if (dryRun) {
328
- const lines = [`[DRY-RUN] Will update existing app machines: ${app}`];
329
- lines.push(` Image: ${targetImage}`);
330
- lines.push(" Config: unchanged");
331
- lines.push(" Volumes: unchanged");
332
- return lines.join("\n");
333
- }
334
-
335
- if (!this.runtime.checkFlyctlInstalled(this.flyctl)) {
336
- throw new Error(`\`${this.flyctl}\` is not installed.`);
337
- }
338
-
339
- const machinesJson = this.runtime.execSync(`${this.flyctl} machines list --app ${app} --json`, this.flyExecOptions({ encoding: "utf8" })) as string;
340
- const machineIds = parseFlyMachineIds(machinesJson, app);
341
-
342
- console.log(`[Fly.io] Updating ${app} image on ${machineIds.length} machine(s)...`);
343
- for (const machineId of machineIds) {
344
- this.runtime.execSync(
345
- `${this.flyctl} machine update ${machineId} --app ${app} --image ${targetImage} --yes`,
346
- this.flyExecOptions({ stdio: "inherit" })
347
- );
348
- }
349
- return `Successfully updated ${app} image`;
350
- }
351
- }
352
-
353
- function requireReadableFile(filePath: string, label: string): void {
354
- if (!fs.existsSync(filePath)) {
355
- throw new Error(`${label} file does not exist: ${filePath}`);
356
- }
357
- const stat = fs.statSync(filePath);
358
- if (!stat.isFile()) {
359
- throw new Error(`${label} path is not a file: ${filePath}`);
360
- }
361
- fs.accessSync(filePath, fs.constants.R_OK);
362
- }