@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.
- 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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
function commandOk(ctx, command, args) {
|
|
2
|
+
const result = ctx.spawnSync(command, args, {
|
|
3
|
+
encoding: "utf8",
|
|
4
|
+
env: ctx.env,
|
|
5
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6
|
+
});
|
|
7
|
+
return !result.error && result.status === 0;
|
|
8
|
+
}
|
|
9
|
+
function flyctlCandidates(ctx) {
|
|
10
|
+
const configured = ctx.flyctlPath ? [ctx.flyctlPath] : [];
|
|
11
|
+
return [...configured, "flyctl", "fly"].filter((item, index, all) => all.indexOf(item) === index);
|
|
12
|
+
}
|
|
13
|
+
function findFlyctl(ctx) {
|
|
14
|
+
return flyctlCandidates(ctx).find((candidate) => commandOk(ctx, candidate, ["version"]));
|
|
15
|
+
}
|
|
16
|
+
function flyCliCheck(ctx) {
|
|
17
|
+
const flyctl = findFlyctl(ctx);
|
|
18
|
+
if (flyctl) {
|
|
19
|
+
return {
|
|
20
|
+
id: "fly",
|
|
21
|
+
label: "fly",
|
|
22
|
+
status: "ok",
|
|
23
|
+
message: `${flyctl} is available`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
id: "fly",
|
|
28
|
+
label: "fly",
|
|
29
|
+
status: "missing",
|
|
30
|
+
message: "Fly.io CLI is missing",
|
|
31
|
+
command: "curl -fsSL https://fly.io/install.sh | sh"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function flyAuthCheck(ctx) {
|
|
35
|
+
if (ctx.env.FLY_API_TOKEN || ctx.env.FLY_ACCESS_TOKEN) {
|
|
36
|
+
return {
|
|
37
|
+
id: "fly-auth",
|
|
38
|
+
label: "fly auth",
|
|
39
|
+
status: "ok",
|
|
40
|
+
message: "Fly auth token is present in the environment"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const flyctl = findFlyctl(ctx);
|
|
44
|
+
if (flyctl && commandOk(ctx, flyctl, ["auth", "whoami"])) {
|
|
45
|
+
return {
|
|
46
|
+
id: "fly-auth",
|
|
47
|
+
label: "fly auth",
|
|
48
|
+
status: "ok",
|
|
49
|
+
message: "fly auth whoami succeeded"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
id: "fly-auth",
|
|
54
|
+
label: "fly auth",
|
|
55
|
+
status: "missing",
|
|
56
|
+
message: "Fly auth is missing",
|
|
57
|
+
command: "fly auth login, or set FLY_API_TOKEN"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function dockerCheck(ctx) {
|
|
61
|
+
if (commandOk(ctx, "docker", ["info"])) {
|
|
62
|
+
return {
|
|
63
|
+
id: "docker",
|
|
64
|
+
label: "docker",
|
|
65
|
+
status: "ok",
|
|
66
|
+
message: "Docker daemon is available"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
id: "docker",
|
|
71
|
+
label: "docker",
|
|
72
|
+
status: "missing",
|
|
73
|
+
message: "Docker daemon is not available",
|
|
74
|
+
command: "Start Docker Desktop or your Docker daemon"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function buildxCheck(ctx) {
|
|
78
|
+
if (commandOk(ctx, "docker", ["buildx", "version"])) {
|
|
79
|
+
return {
|
|
80
|
+
id: "buildx",
|
|
81
|
+
label: "buildx",
|
|
82
|
+
status: "ok",
|
|
83
|
+
message: "Docker buildx is available"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id: "buildx",
|
|
88
|
+
label: "buildx",
|
|
89
|
+
status: "missing",
|
|
90
|
+
message: "Docker buildx is not available",
|
|
91
|
+
command: "Install or upgrade Docker Desktop"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function installFlyCli(ctx) {
|
|
95
|
+
const command = "curl -fsSL https://fly.io/install.sh | sh";
|
|
96
|
+
if (ctx.dryRun) {
|
|
97
|
+
return { ok: true, message: command };
|
|
98
|
+
}
|
|
99
|
+
console.log(`Installing Fly.io CLI with: ${command}`);
|
|
100
|
+
const result = ctx.spawnSync("sh", ["-c", command], {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
env: ctx.env,
|
|
103
|
+
stdio: "inherit"
|
|
104
|
+
});
|
|
105
|
+
if (result.error) {
|
|
106
|
+
return { ok: false, message: result.error.message };
|
|
107
|
+
}
|
|
108
|
+
if (result.status !== 0) {
|
|
109
|
+
return { ok: false, message: `Fly.io CLI install exited with code ${result.status}` };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
message: "Fly.io CLI install finished. If fly is still not found, run: export PATH=\"$HOME/.fly/bin:$PATH\""
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export const flyIoProviderDefinition = {
|
|
117
|
+
id: "fly.io",
|
|
118
|
+
displayName: "Fly.io",
|
|
119
|
+
templateRelativeRoot: "templates/providers/fly.io",
|
|
120
|
+
defaultPaths: {
|
|
121
|
+
providerConfig: "providers/fly.io.toml",
|
|
122
|
+
sellerFlyConfig: "fly/fly.tb-seller.toml",
|
|
123
|
+
registryFlyConfig: "fly/fly.tb-registry.toml",
|
|
124
|
+
deployEnv: "env/deploy.env",
|
|
125
|
+
artifactsDir: "artifacts"
|
|
126
|
+
},
|
|
127
|
+
check(ctx) {
|
|
128
|
+
return [
|
|
129
|
+
flyCliCheck(ctx),
|
|
130
|
+
flyAuthCheck(ctx),
|
|
131
|
+
dockerCheck(ctx),
|
|
132
|
+
buildxCheck(ctx)
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
install: installFlyCli
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=fly-io.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SpawnSyncOptions, SpawnSyncReturns } from "child_process";
|
|
2
|
+
export type AdminProviderId = "fly.io";
|
|
3
|
+
export type ProviderCheckStatus = "ok" | "missing" | "warning";
|
|
4
|
+
export type SpawnSyncRunner = (command: string, args?: string[], options?: SpawnSyncOptions) => SpawnSyncReturns<string | Buffer>;
|
|
5
|
+
export interface ProviderCheckContext {
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
spawnSync: SpawnSyncRunner;
|
|
8
|
+
flyctlPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderInstallContext extends ProviderCheckContext {
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ProviderCheckResult {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
status: ProviderCheckStatus;
|
|
17
|
+
message: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProviderInstallResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AdminProviderDefinition {
|
|
25
|
+
id: AdminProviderId;
|
|
26
|
+
displayName: string;
|
|
27
|
+
templateRelativeRoot: string;
|
|
28
|
+
defaultPaths: {
|
|
29
|
+
providerConfig: string;
|
|
30
|
+
sellerFlyConfig: string;
|
|
31
|
+
registryFlyConfig: string;
|
|
32
|
+
deployEnv: string;
|
|
33
|
+
artifactsDir: string;
|
|
34
|
+
};
|
|
35
|
+
check(ctx: ProviderCheckContext): ProviderCheckResult[];
|
|
36
|
+
install?(ctx: ProviderInstallContext): ProviderInstallResult;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=provider-definition.d.ts.map
|
package/dist/src/seller.d.ts
CHANGED
|
@@ -121,8 +121,10 @@ export declare class SellerCommandRunner {
|
|
|
121
121
|
private configManager;
|
|
122
122
|
constructor(configManager: ConfigManager);
|
|
123
123
|
private getProviderConfig;
|
|
124
|
+
private getWorkdir;
|
|
124
125
|
private getProvider;
|
|
125
126
|
private getFlyctl;
|
|
127
|
+
private resolveCreateOptions;
|
|
126
128
|
ls(json: boolean): SellerListResult | string;
|
|
127
129
|
status(appName: string, json: boolean): SellerStatusResult | string;
|
|
128
130
|
machineSpecs(appName: string): SellerMachineSpecs | undefined;
|
package/dist/src/seller.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { execSync, spawnSync } from "node:child_process";
|
|
22
22
|
import { FlyProvider } from "./server-cmd.js";
|
|
23
|
+
import { resolveWorkdirPath } from "./workdir.js";
|
|
23
24
|
export class FlyCliMissingError extends Error {
|
|
24
25
|
flyctl;
|
|
25
26
|
constructor(flyctl) {
|
|
@@ -158,12 +159,31 @@ export class SellerCommandRunner {
|
|
|
158
159
|
getProviderConfig() {
|
|
159
160
|
return this.configManager.getSellerProvider("fly");
|
|
160
161
|
}
|
|
162
|
+
getWorkdir() {
|
|
163
|
+
return this.configManager.getWorkdir();
|
|
164
|
+
}
|
|
161
165
|
getProvider() {
|
|
162
166
|
return new FlyProvider(this.getProviderConfig());
|
|
163
167
|
}
|
|
164
168
|
getFlyctl() {
|
|
165
169
|
return this.getProviderConfig()?.flyctl_path || "flyctl";
|
|
166
170
|
}
|
|
171
|
+
resolveCreateOptions(options) {
|
|
172
|
+
const workdir = this.getWorkdir();
|
|
173
|
+
const providerConfig = this.getProviderConfig();
|
|
174
|
+
const flyConfig = resolveWorkdirPath(workdir, options.flyConfig || "fly/fly.tb-seller.toml");
|
|
175
|
+
const initialConfigPath = options.initialConfigPath
|
|
176
|
+
? resolveWorkdirPath(workdir, options.initialConfigPath)
|
|
177
|
+
: providerConfig?.default_config
|
|
178
|
+
? resolveWorkdirPath(workdir, providerConfig.default_config)
|
|
179
|
+
: undefined;
|
|
180
|
+
return {
|
|
181
|
+
...options,
|
|
182
|
+
flyConfig,
|
|
183
|
+
initialConfigPath,
|
|
184
|
+
resolvedWorkdir: workdir
|
|
185
|
+
};
|
|
186
|
+
}
|
|
167
187
|
// -- ls --
|
|
168
188
|
ls(json) {
|
|
169
189
|
const provider = this.getProvider();
|
|
@@ -207,27 +227,28 @@ export class SellerCommandRunner {
|
|
|
207
227
|
// -- create --
|
|
208
228
|
create(options, json) {
|
|
209
229
|
const provider = this.getProvider();
|
|
230
|
+
const resolvedOptions = this.resolveCreateOptions(options);
|
|
210
231
|
if (!json) {
|
|
211
|
-
return provider.createSeller(
|
|
232
|
+
return provider.createSeller(resolvedOptions);
|
|
212
233
|
}
|
|
213
234
|
// --json 路径: dry-run 模式直接列命令, 不实际调 flyctl
|
|
214
|
-
if (
|
|
235
|
+
if (resolvedOptions.dryRun) {
|
|
215
236
|
return {
|
|
216
237
|
ok: true,
|
|
217
238
|
provider: "fly",
|
|
218
239
|
action: "create",
|
|
219
|
-
app:
|
|
240
|
+
app: resolvedOptions.app || `tb-seller-${resolvedOptions.name}`,
|
|
220
241
|
dryRun: true,
|
|
221
|
-
commands: buildCreateCommands(
|
|
242
|
+
commands: buildCreateCommands(resolvedOptions)
|
|
222
243
|
};
|
|
223
244
|
}
|
|
224
245
|
// 非 dry-run: 调 FlyProvider (它已经会跑 flyctl), 包装 stdout
|
|
225
|
-
const summary = provider.createSeller(
|
|
246
|
+
const summary = provider.createSeller(resolvedOptions);
|
|
226
247
|
return {
|
|
227
248
|
ok: true,
|
|
228
249
|
provider: "fly",
|
|
229
250
|
action: "create",
|
|
230
|
-
app:
|
|
251
|
+
app: resolvedOptions.app || `tb-seller-${resolvedOptions.name}`,
|
|
231
252
|
dryRun: false,
|
|
232
253
|
summary: String(summary)
|
|
233
254
|
};
|
|
@@ -399,13 +420,9 @@ function buildCreateCommands(options) {
|
|
|
399
420
|
const appName = options.app || `tb-seller-${options.name}`;
|
|
400
421
|
const region = options.region || "sin";
|
|
401
422
|
const lines = [];
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
lines.push(`fly deploy --config ${options.flyConfig} --app ${appName} --region ${region}`);
|
|
406
|
-
if (options.image) {
|
|
407
|
-
lines.push(`fly machine update <machine-id> --app ${appName} --image ${options.image} --yes`);
|
|
408
|
-
}
|
|
423
|
+
lines.push(`fly apps create ${appName}`);
|
|
424
|
+
lines.push(`fly secrets import --stage --app ${appName}`);
|
|
425
|
+
lines.push(`fly deploy --config ${options.flyConfig} --image ${options.image} --app ${appName} --primary-region ${region} --now`);
|
|
409
426
|
return lines;
|
|
410
427
|
}
|
|
411
428
|
// re-exports 方便测试
|
package/dist/src/server-cmd.d.ts
CHANGED
package/dist/src/server-cmd.js
CHANGED
|
@@ -150,7 +150,8 @@ export class FlyProvider {
|
|
|
150
150
|
throw new Error("seller create requires --image registry.fly.io/tb-seller:<v>");
|
|
151
151
|
}
|
|
152
152
|
if (!flyConfig) {
|
|
153
|
-
|
|
153
|
+
const suffix = options.resolvedWorkdir ? ` --workdir ${options.resolvedWorkdir}` : "";
|
|
154
|
+
throw new Error(`Fly config not found. Run: tb-admin init --provider fly.io${suffix}`);
|
|
154
155
|
}
|
|
155
156
|
if (dryRun) {
|
|
156
157
|
const lines = [
|
|
@@ -176,7 +177,13 @@ export class FlyProvider {
|
|
|
176
177
|
if (!operatorSecret) {
|
|
177
178
|
throw new Error("operator_secret is required. Provide --operator-secret or configure seller_providers.fly.operator_secret");
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
try {
|
|
181
|
+
requireReadableFile(flyConfig, "Fly config");
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const suffix = options.resolvedWorkdir ? ` --workdir ${options.resolvedWorkdir}` : "";
|
|
185
|
+
throw new Error(`${err.message}. Run: tb-admin init --provider fly.io${suffix}`);
|
|
186
|
+
}
|
|
180
187
|
if (initialConfigPath) {
|
|
181
188
|
requireReadableFile(initialConfigPath, "Initial seller config");
|
|
182
189
|
}
|
package/dist/src/ui-actions.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CreateSellerRequest {
|
|
|
18
18
|
upstreamWebsite: string;
|
|
19
19
|
upstreamUrl: string;
|
|
20
20
|
upstreamApiKey: string;
|
|
21
|
+
upstreamProtocolPreset?: string;
|
|
21
22
|
upstreamBalanceProbeTemplate?: string;
|
|
22
23
|
upstreamBalanceProbeUrl?: string;
|
|
23
24
|
upstreamBalanceProbeUserId?: string;
|
|
@@ -97,6 +98,7 @@ export declare class UiActions {
|
|
|
97
98
|
private waitForSellerReady;
|
|
98
99
|
private refreshSellerModelsWithRetry;
|
|
99
100
|
private publishCreatedSellerRegistryEntry;
|
|
101
|
+
private submitCreatedSellerRelease;
|
|
100
102
|
private fetchSellerOperatorJson;
|
|
101
103
|
private fetchSellerOperatorJsonOptional;
|
|
102
104
|
}
|
|
@@ -113,5 +115,6 @@ export declare function runTbAdmin(args: string[], timeoutMs: number): Promise<U
|
|
|
113
115
|
* stdout 仍是原始字符串, ok=false (CLI exit 0 但 stdout 不可解析视为可恢复错).
|
|
114
116
|
*/
|
|
115
117
|
export declare function runTbAdminJson(args: string[], timeoutMs: number): Promise<UiActionResult>;
|
|
118
|
+
export declare function parseJsonSafely(text: string): unknown | undefined;
|
|
116
119
|
export {};
|
|
117
120
|
//# sourceMappingURL=ui-actions.d.ts.map
|
package/dist/src/ui-actions.js
CHANGED
|
@@ -50,9 +50,6 @@ export class UiActions {
|
|
|
50
50
|
if (!operatorSecret) {
|
|
51
51
|
throw new Error("operatorSecret is required in local admin config seller_providers.fly.operator_secret or request body");
|
|
52
52
|
}
|
|
53
|
-
if (!flyConfig) {
|
|
54
|
-
throw new Error("flyConfig is required before creating a seller deployment");
|
|
55
|
-
}
|
|
56
53
|
const configRequest = {
|
|
57
54
|
...normalizedRequest,
|
|
58
55
|
operatorSecret
|
|
@@ -86,13 +83,14 @@ export class UiActions {
|
|
|
86
83
|
normalizedRequest.region,
|
|
87
84
|
"--image",
|
|
88
85
|
normalizedRequest.image,
|
|
89
|
-
"--fly-config",
|
|
90
|
-
flyConfig,
|
|
91
86
|
"--initial-config",
|
|
92
87
|
filePath,
|
|
93
88
|
"--operator-secret",
|
|
94
89
|
operatorSecret
|
|
95
90
|
];
|
|
91
|
+
if (flyConfig) {
|
|
92
|
+
args.push("--fly-config", flyConfig);
|
|
93
|
+
}
|
|
96
94
|
if (normalizedRequest.app) {
|
|
97
95
|
args.push("--app", normalizedRequest.app);
|
|
98
96
|
}
|
|
@@ -388,25 +386,91 @@ export class UiActions {
|
|
|
388
386
|
});
|
|
389
387
|
return failed;
|
|
390
388
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"--expect-version",
|
|
399
|
-
String(registry.version)
|
|
400
|
-
]), 30000);
|
|
401
|
-
report({
|
|
402
|
-
stepId: "publish_registry",
|
|
403
|
-
status: put.ok ? "succeeded" : "failed",
|
|
404
|
-
title: "Update bootstrap registry",
|
|
405
|
-
message: put.ok ? "Bootstrap registry entry was added. Run registry publish to update R2." : "Bootstrap registry update failed.",
|
|
406
|
-
result: put
|
|
407
|
-
});
|
|
408
|
-
return put;
|
|
389
|
+
const submitted = await this.submitCreatedSellerRelease(entry);
|
|
390
|
+
report({
|
|
391
|
+
stepId: "publish_registry",
|
|
392
|
+
status: submitted.ok ? "succeeded" : "failed",
|
|
393
|
+
title: "Submit registry release",
|
|
394
|
+
message: submitted.ok ? "Seller was staged and a registry release request was submitted." : "Registry release request failed.",
|
|
395
|
+
result: submitted
|
|
409
396
|
});
|
|
397
|
+
return submitted;
|
|
398
|
+
}
|
|
399
|
+
async submitCreatedSellerRelease(entry) {
|
|
400
|
+
const profile = this.state.activeBootstrapProfile();
|
|
401
|
+
const baseUrl = this.options.url || profile.profile?.url;
|
|
402
|
+
const token = this.options.token || profile.profile?.token;
|
|
403
|
+
const command = ["POST", "/platform/sellers/stage", "POST", "/platform/release-requests"];
|
|
404
|
+
if (!baseUrl || !token) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
stdout: "",
|
|
408
|
+
stderr: "No bootstrap vendor profile found. Configure a bootstrap-vendor profile or pass --url and --token.",
|
|
409
|
+
command
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const fetchJson = this.options.fetchJson || defaultFetchJson;
|
|
413
|
+
const headers = {
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
Authorization: `Bearer ${token}`
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
const stage = await fetchJson(`${trimSlash(baseUrl)}/platform/sellers/stage`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
headers,
|
|
421
|
+
body: JSON.stringify({ seller: entry })
|
|
422
|
+
});
|
|
423
|
+
const stagedSellerId = stagedSellerIdFromResponse(stage) || entry.id;
|
|
424
|
+
const release = await fetchJson(`${trimSlash(baseUrl)}/platform/release-requests`, {
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers,
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
stagedSellerIds: [stagedSellerId],
|
|
429
|
+
note: `tb-admin ui create seller ${entry.id}`
|
|
430
|
+
})
|
|
431
|
+
});
|
|
432
|
+
const releaseId = releaseRequestIdFromResponse(release);
|
|
433
|
+
if (releaseId === undefined) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
stdout: "",
|
|
437
|
+
stderr: "Registry release request did not return an id.",
|
|
438
|
+
command
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const published = await waitForSubmittedRelease({
|
|
442
|
+
baseUrl,
|
|
443
|
+
headers,
|
|
444
|
+
fetchJson,
|
|
445
|
+
releaseId,
|
|
446
|
+
sellerId: stagedSellerId
|
|
447
|
+
});
|
|
448
|
+
if (!published.ok) {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
stdout: "",
|
|
452
|
+
stderr: published.error,
|
|
453
|
+
command: [...command, "GET", "/platform/release-requests/:id", "GET", "/platform/sellers"],
|
|
454
|
+
json: { stage, release, stagedSellerId, publishStatus: published }
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const json = { stage, release, stagedSellerId, published };
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
stdout: JSON.stringify(releaseSummary(json), null, 2),
|
|
461
|
+
stderr: "",
|
|
462
|
+
command: [...command, "GET", "/platform/release-requests/:id", "GET", "/platform/sellers"],
|
|
463
|
+
json
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
stdout: "",
|
|
470
|
+
stderr: redactSensitive(err.message || "registry release request failed"),
|
|
471
|
+
command
|
|
472
|
+
};
|
|
473
|
+
}
|
|
410
474
|
}
|
|
411
475
|
async fetchSellerOperatorJson(appName, operatorSecret, pathName) {
|
|
412
476
|
const fetchJson = this.options.fetchJson || defaultFetchJson;
|
|
@@ -544,16 +608,31 @@ export function runTbAdminJson(args, timeoutMs) {
|
|
|
544
608
|
return { ...result, json: parsed };
|
|
545
609
|
});
|
|
546
610
|
}
|
|
547
|
-
function parseJsonSafely(text) {
|
|
611
|
+
export function parseJsonSafely(text) {
|
|
548
612
|
if (!text || text.trim().length === 0) {
|
|
549
613
|
return undefined;
|
|
550
614
|
}
|
|
615
|
+
const trimmed = text.trim();
|
|
551
616
|
try {
|
|
552
|
-
return JSON.parse(
|
|
617
|
+
return JSON.parse(trimmed);
|
|
553
618
|
}
|
|
554
619
|
catch {
|
|
555
|
-
|
|
620
|
+
// Some flyctl-backed commands write human progress logs before the final
|
|
621
|
+
// structured JSON object. Keep the JSON path tolerant of that stdout shape.
|
|
622
|
+
for (let index = trimmed.length - 1; index >= 0; index -= 1) {
|
|
623
|
+
const char = trimmed[index];
|
|
624
|
+
if (char !== "{" && char !== "[") {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
return JSON.parse(trimmed.slice(index));
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Keep walking backward until the outer JSON value is found.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
556
634
|
}
|
|
635
|
+
return undefined;
|
|
557
636
|
}
|
|
558
637
|
async function defaultFetchJson(url, init) {
|
|
559
638
|
const response = await fetch(url, init);
|
|
@@ -663,7 +742,7 @@ function normalizeCreateSellerRequest(request) {
|
|
|
663
742
|
...request,
|
|
664
743
|
app,
|
|
665
744
|
image: stringValue(request.image) || "registry.fly.io/tb-seller:latest",
|
|
666
|
-
flyConfig: stringValue(request.flyConfig) ||
|
|
745
|
+
flyConfig: stringValue(request.flyConfig) || undefined,
|
|
667
746
|
upstreamUrl
|
|
668
747
|
};
|
|
669
748
|
if (paymentMethodsFromRequest(normalized).includes("clawtip")) {
|
|
@@ -688,6 +767,7 @@ function initialSellerConfig(request, masked) {
|
|
|
688
767
|
upstreamUrl: request.upstreamUrl,
|
|
689
768
|
upstreamApiKey: masked ? "********" : request.upstreamApiKey,
|
|
690
769
|
upstreamWebsite: request.upstreamWebsite,
|
|
770
|
+
upstreamCapabilities: upstreamCapabilitiesFromPreset(request.upstreamProtocolPreset),
|
|
691
771
|
upstreamBalanceUrl,
|
|
692
772
|
upstreamUserId,
|
|
693
773
|
upstreamRechargeUrl,
|
|
@@ -700,6 +780,26 @@ function initialSellerConfig(request, masked) {
|
|
|
700
780
|
...paymentConfig
|
|
701
781
|
};
|
|
702
782
|
}
|
|
783
|
+
function upstreamCapabilitiesFromPreset(value) {
|
|
784
|
+
const preset = stringValue(value) || "auto";
|
|
785
|
+
if (preset === "image") {
|
|
786
|
+
return {
|
|
787
|
+
chatCompletions: "unsupported",
|
|
788
|
+
responses: "unsupported",
|
|
789
|
+
messages: "unsupported",
|
|
790
|
+
imagesGenerations: "supported"
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (preset === "chat") {
|
|
794
|
+
return {
|
|
795
|
+
chatCompletions: "supported",
|
|
796
|
+
responses: "unsupported",
|
|
797
|
+
messages: "unsupported",
|
|
798
|
+
imagesGenerations: "unsupported"
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
703
803
|
function paymentConfigFromRequest(request, masked) {
|
|
704
804
|
const paymentMethods = paymentMethodsFromRequest(request);
|
|
705
805
|
const config = {
|
|
@@ -860,6 +960,78 @@ function hostName(value) {
|
|
|
860
960
|
function sellerOperatorUrl(appName) {
|
|
861
961
|
return `https://${appName}.fly.dev`;
|
|
862
962
|
}
|
|
963
|
+
function trimSlash(value) {
|
|
964
|
+
return String(value || "").replace(/\/+$/, "");
|
|
965
|
+
}
|
|
966
|
+
function stagedSellerIdFromResponse(value) {
|
|
967
|
+
const root = objectValue(value);
|
|
968
|
+
const pending = objectValue(root?.pendingSeller);
|
|
969
|
+
return stringValue(pending?.id);
|
|
970
|
+
}
|
|
971
|
+
function releaseRequestIdFromResponse(value) {
|
|
972
|
+
const root = objectValue(value);
|
|
973
|
+
const release = objectValue(root?.releaseRequest);
|
|
974
|
+
const id = release?.id;
|
|
975
|
+
return typeof id === "number" || typeof id === "string" ? id : undefined;
|
|
976
|
+
}
|
|
977
|
+
function releaseRequestStatusFromResponse(value) {
|
|
978
|
+
const root = objectValue(value);
|
|
979
|
+
const release = objectValue(root?.releaseRequest);
|
|
980
|
+
return stringValue(release?.status);
|
|
981
|
+
}
|
|
982
|
+
async function waitForSubmittedRelease(options) {
|
|
983
|
+
const deadline = Date.now() + 60000;
|
|
984
|
+
let lastRelease;
|
|
985
|
+
let lastSellers;
|
|
986
|
+
let lastStatus = "unknown";
|
|
987
|
+
while (Date.now() < deadline) {
|
|
988
|
+
lastRelease = await options.fetchJson(`${trimSlash(options.baseUrl)}/platform/release-requests/${encodeURIComponent(String(options.releaseId))}`, {
|
|
989
|
+
headers: options.headers
|
|
990
|
+
});
|
|
991
|
+
lastStatus = releaseRequestStatusFromResponse(lastRelease) || "unknown";
|
|
992
|
+
if (lastStatus === "rejected" || lastStatus === "failed") {
|
|
993
|
+
return {
|
|
994
|
+
ok: false,
|
|
995
|
+
error: `Registry release request ${options.releaseId} ended with status ${lastStatus}.`,
|
|
996
|
+
release: lastRelease,
|
|
997
|
+
sellers: lastSellers
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
lastSellers = await options.fetchJson(`${trimSlash(options.baseUrl)}/platform/sellers`, {
|
|
1001
|
+
headers: options.headers
|
|
1002
|
+
});
|
|
1003
|
+
if (lastStatus === "published" && sellerRegistryContains(lastSellers, options.sellerId)) {
|
|
1004
|
+
return { ok: true, release: lastRelease, sellers: lastSellers };
|
|
1005
|
+
}
|
|
1006
|
+
await sleep(1000);
|
|
1007
|
+
}
|
|
1008
|
+
const sellerPart = sellerRegistryContains(lastSellers, options.sellerId)
|
|
1009
|
+
? "seller is visible"
|
|
1010
|
+
: "seller is not visible";
|
|
1011
|
+
return {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
error: `Registry release request ${options.releaseId} was not published before timeout (last status: ${lastStatus}; ${sellerPart}).`,
|
|
1014
|
+
release: lastRelease,
|
|
1015
|
+
sellers: lastSellers
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function sellerRegistryContains(value, sellerId) {
|
|
1019
|
+
const root = objectValue(value);
|
|
1020
|
+
const sellers = Array.isArray(root?.sellers) ? root.sellers : Array.isArray(value) ? value : [];
|
|
1021
|
+
return sellers.some((seller) => {
|
|
1022
|
+
const entry = objectValue(seller);
|
|
1023
|
+
return entry?.id === sellerId || entry?.name === sellerId || entry?.app === sellerId;
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
function releaseSummary(value) {
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
action: "submit_registry_release",
|
|
1030
|
+
stagedSellerId: value.stagedSellerId,
|
|
1031
|
+
releaseRequestId: releaseRequestIdFromResponse(value.release),
|
|
1032
|
+
releaseStatus: releaseRequestStatusFromResponse(value.published?.release || value.release)
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
863
1035
|
function sleep(ms) {
|
|
864
1036
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
865
1037
|
}
|
package/dist/src/ui-command.js
CHANGED
|
@@ -10,13 +10,14 @@ export function bindAdminUiCommand(program, configManager) {
|
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
try {
|
|
12
12
|
const rootOptions = program.opts();
|
|
13
|
-
const mgr = rootOptions.config
|
|
13
|
+
const mgr = rootOptions.config || rootOptions.workdir
|
|
14
|
+
? new ConfigManager(rootOptions.config, { cliWorkdir: rootOptions.workdir })
|
|
15
|
+
: configManager;
|
|
14
16
|
const started = await startAdminUiServer({
|
|
15
17
|
host: options.host,
|
|
16
18
|
port: options.port,
|
|
17
19
|
openBrowser: Boolean(options.open),
|
|
18
20
|
configManager: mgr,
|
|
19
|
-
configPath: rootOptions.config,
|
|
20
21
|
profile: rootOptions.profile || process.env.TOKENBUDDY_ADMIN_PROFILE || defaultUiProfile(mgr),
|
|
21
22
|
url: rootOptions.url || process.env.TOKENBUDDY_ADMIN_URL,
|
|
22
23
|
token: rootOptions.token || process.env.TOKENBUDDY_ADMIN_TOKEN
|
package/dist/src/ui-state.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface SellerRow {
|
|
|
60
60
|
error?: string;
|
|
61
61
|
/**
|
|
62
62
|
* Step 13 v1.1: 数据源标记. UI 据此决定:
|
|
63
|
-
* - "fly" → 灰点 +
|
|
63
|
+
* - "fly" → 灰点 + 「未发布」
|
|
64
64
|
* - "registry" → 整行标红 + 立即下线按钮 (registry-only = 重大事故)
|
|
65
65
|
* - "both" → 正常色 + Activate / Drain 走 vendor path
|
|
66
66
|
*/
|
|
@@ -72,8 +72,6 @@ export interface SellerRow {
|
|
|
72
72
|
registryAlert?: boolean;
|
|
73
73
|
/** Step 13 v1.1: 标红原因 (中文短句, UI tooltip 显示). */
|
|
74
74
|
alertReason?: string;
|
|
75
|
-
/** Step 13 v1.1: "未发布" 提示 (fly-only 行的 publishHint 按钮 caption). */
|
|
76
|
-
publishHint?: string;
|
|
77
75
|
/**
|
|
78
76
|
* Step 13 v1.1: 立即下线按钮 caption (registry-only 行). 文案
|
|
79
77
|
* 必须含 "registry-only" 让用户知道这**不**删 fly app. 详见 spec.
|