@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
package/src/seller.ts
DELETED
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step 12 v1.1 (增量): tb-admin seller 子命令 CLI 门面层.
|
|
3
|
-
*
|
|
4
|
-
* 1.0.31 现状:
|
|
5
|
-
* - `tb-admin seller ls|create|deploy|remove` 4 个子命令
|
|
6
|
-
* - 每个命令内部直接 `console.log(FlyProvider.<method>() 的 string 返回值)`
|
|
7
|
-
* - 1.0.31 文本输出跟 flyctl 原始 stdout 一致 (人类可读表 / 状态文案)
|
|
8
|
-
*
|
|
9
|
-
* v1.1 增量:
|
|
10
|
-
* 1. 新增 `tb-admin seller status <app>` 第 5 子命令
|
|
11
|
-
* 2. 5 个子命令全部加 `--json` flag
|
|
12
|
-
* 3. `--json` 输出统一包装:
|
|
13
|
-
* - ls/status: 直接拿 flyctl `--json` 解析后的 array / object
|
|
14
|
-
* - create/deploy/remove: `{ ok, provider, action, app, dryRun, ... }`
|
|
15
|
-
* 4. 默认路径 1:1 保留 1.0.31 行为 (FlyProvider 不变, server-cmd.ts 不动)
|
|
16
|
-
* 5. UI 之后通过 spawn `tb-admin seller <cmd> --json` 取结构化数据
|
|
17
|
-
*
|
|
18
|
-
* 不改: FlyProvider 类 / server-cmd.ts / admin web / provider.ts.
|
|
19
|
-
* 不写 UI (Step 13).
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
23
|
-
import { FlyProvider, type SellerCreateOptions } from "./server-cmd.js";
|
|
24
|
-
import type { ConfigManager, SellerProviderConfig } from "./config.js";
|
|
25
|
-
|
|
26
|
-
/** 单个 seller app 的 JSON 形态 (跟 flyctl apps list --json 对齐, 但只保留 UI 关心字段) */
|
|
27
|
-
export interface SellerAppJson {
|
|
28
|
-
name: string;
|
|
29
|
-
status: string;
|
|
30
|
-
owner?: string;
|
|
31
|
-
region?: string;
|
|
32
|
-
version?: number;
|
|
33
|
-
latestDeployAt?: string;
|
|
34
|
-
/** raw flyctl 字段, 保留供未来扩展 */
|
|
35
|
-
raw: Record<string, unknown>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface SellerMachineSpecs {
|
|
39
|
-
machines: number;
|
|
40
|
-
runningMachines?: number;
|
|
41
|
-
cpuKind?: string;
|
|
42
|
-
cpuCores?: number;
|
|
43
|
-
memoryMb?: number;
|
|
44
|
-
volumeGb?: number;
|
|
45
|
-
regions?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** ls --json 输出 */
|
|
49
|
-
export interface SellerListResult {
|
|
50
|
-
ok: true;
|
|
51
|
-
provider: "fly";
|
|
52
|
-
action: "list";
|
|
53
|
-
count: number;
|
|
54
|
-
apps: SellerAppJson[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** status --json 输出 */
|
|
58
|
-
export interface SellerStatusResult {
|
|
59
|
-
ok: true;
|
|
60
|
-
provider: "fly";
|
|
61
|
-
action: "status";
|
|
62
|
-
app: string;
|
|
63
|
-
status: Record<string, unknown>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** create / deploy / remove --json 输出 */
|
|
67
|
-
export interface SellerActionResult<T extends "create" | "deploy" | "remove"> {
|
|
68
|
-
ok: true;
|
|
69
|
-
provider: "fly";
|
|
70
|
-
action: T;
|
|
71
|
-
app: string;
|
|
72
|
-
dryRun: boolean;
|
|
73
|
-
/** dry-run 模式下, 列出实际会跑的 flyctl 命令 */
|
|
74
|
-
commands?: string[];
|
|
75
|
-
/** 实际执行后, flyctl 输出摘录 (limited to stdout summary) */
|
|
76
|
-
summary?: string;
|
|
77
|
-
/** create/deploy 额外信息 */
|
|
78
|
-
machines?: string[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** roll --json 输出: 按顺序对一组 seller 跑 image-only redeploy */
|
|
82
|
-
export interface SellerRollResult {
|
|
83
|
-
ok: true;
|
|
84
|
-
provider: "fly";
|
|
85
|
-
action: "roll";
|
|
86
|
-
image: string;
|
|
87
|
-
dryRun: boolean;
|
|
88
|
-
/** 实际进入 roll 的 seller app 名 (去 exclude 之后) */
|
|
89
|
-
candidates: string[];
|
|
90
|
-
/** 被 --exclude 排除的 seller */
|
|
91
|
-
excluded: string[];
|
|
92
|
-
/** 每一台 deploy 的结果, 按执行顺序. 失败那一台后续不再执行 */
|
|
93
|
-
attempts: SellerRollAttempt[];
|
|
94
|
-
/** true = 全部 success; false = 至少一台失败且后续没跑 */
|
|
95
|
-
completed: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface SellerRollAttempt {
|
|
99
|
-
app: string;
|
|
100
|
-
ok: boolean;
|
|
101
|
-
/** 失败时填 stderr / 异常 message */
|
|
102
|
-
error?: string;
|
|
103
|
-
/** 实际 deploy 后 flyctl 摘录 */
|
|
104
|
-
summary?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export type SellerCommandResult =
|
|
108
|
-
| SellerListResult
|
|
109
|
-
| SellerStatusResult
|
|
110
|
-
| SellerActionResult<"create">
|
|
111
|
-
| SellerActionResult<"deploy">
|
|
112
|
-
| SellerActionResult<"remove">
|
|
113
|
-
| SellerRollResult;
|
|
114
|
-
|
|
115
|
-
export class FlyCliMissingError extends Error {
|
|
116
|
-
constructor(public readonly flyctl: string) {
|
|
117
|
-
super(`\`${flyctl}\` is not installed on your system PATH.`);
|
|
118
|
-
this.name = "FlyCliMissingError";
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export class FlyCommandError extends Error {
|
|
123
|
-
constructor(
|
|
124
|
-
public readonly command: string,
|
|
125
|
-
public readonly exitCode: number,
|
|
126
|
-
public readonly stderr: string
|
|
127
|
-
) {
|
|
128
|
-
super(`flyctl command failed (${exitCode}): ${command}\n${stderr.trim()}`);
|
|
129
|
-
this.name = "FlyCommandError";
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 解析 flyctl apps list 文本输出 (人类可读表) -> 数组.
|
|
135
|
-
* 默认路径不变; --json 路径用 `parseFlyListJson` 走结构化路径.
|
|
136
|
-
*/
|
|
137
|
-
function parseFlyListText(text: string): SellerAppJson[] {
|
|
138
|
-
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
139
|
-
// 第一行 = 表头, 跳过; 后续行 = app 行
|
|
140
|
-
return lines.slice(1).map((line) => {
|
|
141
|
-
const cols = line.split("│").map((c) => c.trim());
|
|
142
|
-
return {
|
|
143
|
-
name: cols[0] || "",
|
|
144
|
-
status: cols[2] || "unknown",
|
|
145
|
-
owner: cols[1] || undefined,
|
|
146
|
-
latestDeployAt: cols[3] || undefined,
|
|
147
|
-
raw: { textLine: line }
|
|
148
|
-
};
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function parseFlyListJson(jsonText: string): SellerAppJson[] {
|
|
153
|
-
let parsed: unknown;
|
|
154
|
-
try {
|
|
155
|
-
parsed = JSON.parse(jsonText || "[]");
|
|
156
|
-
} catch (err) {
|
|
157
|
-
throw new Error(`flyctl apps list --json returned invalid JSON: ${(err as Error).message}`);
|
|
158
|
-
}
|
|
159
|
-
if (!Array.isArray(parsed)) {
|
|
160
|
-
throw new Error(`flyctl apps list --json did not return an array`);
|
|
161
|
-
}
|
|
162
|
-
return parsed.map((item) => {
|
|
163
|
-
if (!item || typeof item !== "object") {
|
|
164
|
-
return { name: String(item), status: "unknown", raw: { value: item } };
|
|
165
|
-
}
|
|
166
|
-
const obj = item as Record<string, unknown>;
|
|
167
|
-
const name = typeof obj.Name === "string" ? obj.Name : typeof obj.name === "string" ? obj.name : "";
|
|
168
|
-
const status = typeof obj.Status === "string" ? obj.Status : typeof obj.status === "string" ? obj.status : "unknown";
|
|
169
|
-
return {
|
|
170
|
-
name,
|
|
171
|
-
status,
|
|
172
|
-
owner: typeof obj.Owner === "string" ? obj.Owner : undefined,
|
|
173
|
-
region: typeof obj.Region === "string" ? obj.Region : undefined,
|
|
174
|
-
version: typeof obj.Version === "number" ? obj.Version : undefined,
|
|
175
|
-
latestDeployAt: typeof obj.Deployed === "boolean" && obj.Deployed ? new Date().toISOString() : undefined,
|
|
176
|
-
raw: obj
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function parseFlyStatusJson(jsonText: string): Record<string, unknown> {
|
|
182
|
-
let parsed: unknown;
|
|
183
|
-
try {
|
|
184
|
-
parsed = JSON.parse(jsonText || "{}");
|
|
185
|
-
} catch (err) {
|
|
186
|
-
throw new Error(`flyctl status --json returned invalid JSON: ${(err as Error).message}`);
|
|
187
|
-
}
|
|
188
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
189
|
-
throw new Error(`flyctl status --json did not return an object`);
|
|
190
|
-
}
|
|
191
|
-
return parsed as Record<string, unknown>;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Low-level flyctl exec helper. Throws FlyCommandError on non-zero exit.
|
|
196
|
-
* Use this for --json path; default 1.0.31 path still goes through FlyProvider
|
|
197
|
-
* (which uses `stdio: "inherit"` for human consumption).
|
|
198
|
-
*/
|
|
199
|
-
function runFlyctlJson(
|
|
200
|
-
flyctl: string,
|
|
201
|
-
args: string[],
|
|
202
|
-
env: NodeJS.ProcessEnv,
|
|
203
|
-
timeoutMs?: number
|
|
204
|
-
): unknown {
|
|
205
|
-
const result = spawnSync(flyctl, args, { encoding: "utf8", env, timeout: timeoutMs });
|
|
206
|
-
if (result.error) {
|
|
207
|
-
throw result.error;
|
|
208
|
-
}
|
|
209
|
-
if (result.status !== 0) {
|
|
210
|
-
throw new FlyCommandError(
|
|
211
|
-
`${flyctl} ${args.join(" ")}`,
|
|
212
|
-
result.status ?? -1,
|
|
213
|
-
result.stderr || result.stdout || ""
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
return result.stdout;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function parseFlyMachinesJson(jsonText: string): SellerMachineSpecs {
|
|
220
|
-
let parsed: unknown;
|
|
221
|
-
try {
|
|
222
|
-
parsed = JSON.parse(jsonText || "[]");
|
|
223
|
-
} catch (err) {
|
|
224
|
-
throw new Error(`flyctl machines list --json returned invalid JSON: ${(err as Error).message}`);
|
|
225
|
-
}
|
|
226
|
-
if (!Array.isArray(parsed)) {
|
|
227
|
-
throw new Error("flyctl machines list --json did not return an array");
|
|
228
|
-
}
|
|
229
|
-
const machines = parsed.filter((item) => item && typeof item === "object") as Array<Record<string, unknown>>;
|
|
230
|
-
const first = machines[0];
|
|
231
|
-
const guest = first && typeof first.config === "object" && first.config
|
|
232
|
-
? (first.config as Record<string, unknown>).guest as Record<string, unknown> | undefined
|
|
233
|
-
: undefined;
|
|
234
|
-
const mount = first && typeof first.config === "object" && first.config && Array.isArray((first.config as Record<string, unknown>).mounts)
|
|
235
|
-
? ((first.config as Record<string, unknown>).mounts as unknown[]).find((item) => item && typeof item === "object") as Record<string, unknown> | undefined
|
|
236
|
-
: undefined;
|
|
237
|
-
const regions = Array.from(new Set(machines.map((machine) => typeof machine.region === "string" ? machine.region : undefined).filter((region): region is string => Boolean(region)))).sort();
|
|
238
|
-
return {
|
|
239
|
-
machines: machines.length,
|
|
240
|
-
runningMachines: machines.filter((machine) => machine.state === "started").length,
|
|
241
|
-
cpuKind: typeof guest?.cpu_kind === "string" ? guest.cpu_kind : undefined,
|
|
242
|
-
cpuCores: typeof guest?.cpus === "number" ? guest.cpus : undefined,
|
|
243
|
-
memoryMb: typeof guest?.memory_mb === "number" ? guest.memory_mb : undefined,
|
|
244
|
-
volumeGb: typeof mount?.size_gb === "number" ? mount.size_gb : undefined,
|
|
245
|
-
regions
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* 顶层 seller 命令入口. 5 个 op 都走这里.
|
|
251
|
-
* - `json=false` (默认): 走 FlyProvider 现有方法, 返回人类可读 string, 调用方 console.log
|
|
252
|
-
* - `json=true`: 走结构化 flyctl --json + 包装, 返回 SellerCommandResult 对象
|
|
253
|
-
*/
|
|
254
|
-
export class SellerCommandRunner {
|
|
255
|
-
constructor(private configManager: ConfigManager) {}
|
|
256
|
-
|
|
257
|
-
private getProviderConfig(): SellerProviderConfig | undefined {
|
|
258
|
-
return this.configManager.getSellerProvider("fly");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private getProvider(): FlyProvider {
|
|
262
|
-
return new FlyProvider(this.getProviderConfig());
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private getFlyctl(): string {
|
|
266
|
-
return this.getProviderConfig()?.flyctl_path || "flyctl";
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// -- ls --
|
|
270
|
-
|
|
271
|
-
public ls(json: boolean): SellerListResult | string {
|
|
272
|
-
const provider = this.getProvider();
|
|
273
|
-
if (!json) {
|
|
274
|
-
return provider.listApps();
|
|
275
|
-
}
|
|
276
|
-
const flyctl = this.getFlyctl();
|
|
277
|
-
if (!providerCheck(flyctl)) {
|
|
278
|
-
throw new FlyCliMissingError(flyctl);
|
|
279
|
-
}
|
|
280
|
-
const env = flyEnv(this.getProviderConfig());
|
|
281
|
-
const stdout = runFlyctlJson(flyctl, ["apps", "list", "--json"], env) as string;
|
|
282
|
-
const apps = parseFlyListJson(stdout);
|
|
283
|
-
return { ok: true, provider: "fly", action: "list", count: apps.length, apps };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// -- status --
|
|
287
|
-
|
|
288
|
-
public status(appName: string, json: boolean): SellerStatusResult | string {
|
|
289
|
-
const provider = this.getProvider();
|
|
290
|
-
if (!json) {
|
|
291
|
-
return provider.statusApp(appName);
|
|
292
|
-
}
|
|
293
|
-
const flyctl = this.getFlyctl();
|
|
294
|
-
if (!providerCheck(flyctl)) {
|
|
295
|
-
throw new FlyCliMissingError(flyctl);
|
|
296
|
-
}
|
|
297
|
-
const resolvedName = appName.includes("-") ? appName : `tb-seller-${appName}`;
|
|
298
|
-
const env = flyEnv(this.getProviderConfig());
|
|
299
|
-
const stdout = runFlyctlJson(flyctl, ["status", "--app", resolvedName, "--json"], env) as string;
|
|
300
|
-
const status = parseFlyStatusJson(stdout);
|
|
301
|
-
return { ok: true, provider: "fly", action: "status", app: resolvedName, status };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
public machineSpecs(appName: string): SellerMachineSpecs | undefined {
|
|
305
|
-
const flyctl = this.getFlyctl();
|
|
306
|
-
if (!providerCheck(flyctl)) {
|
|
307
|
-
throw new FlyCliMissingError(flyctl);
|
|
308
|
-
}
|
|
309
|
-
const env = flyEnv(this.getProviderConfig());
|
|
310
|
-
const stdout = runFlyctlJson(flyctl, ["machines", "list", "--app", appName, "--json"], env, 10000) as string;
|
|
311
|
-
return parseFlyMachinesJson(stdout);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// -- create --
|
|
315
|
-
|
|
316
|
-
public create(options: SellerCreateOptions, json: boolean): SellerActionResult<"create"> | string {
|
|
317
|
-
const provider = this.getProvider();
|
|
318
|
-
if (!json) {
|
|
319
|
-
return provider.createSeller(options);
|
|
320
|
-
}
|
|
321
|
-
// --json 路径: dry-run 模式直接列命令, 不实际调 flyctl
|
|
322
|
-
if (options.dryRun) {
|
|
323
|
-
return {
|
|
324
|
-
ok: true,
|
|
325
|
-
provider: "fly",
|
|
326
|
-
action: "create",
|
|
327
|
-
app: options.app || `tb-seller-${options.name}`,
|
|
328
|
-
dryRun: true,
|
|
329
|
-
commands: buildCreateCommands(options)
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
// 非 dry-run: 调 FlyProvider (它已经会跑 flyctl), 包装 stdout
|
|
333
|
-
const summary = provider.createSeller(options);
|
|
334
|
-
return {
|
|
335
|
-
ok: true,
|
|
336
|
-
provider: "fly",
|
|
337
|
-
action: "create",
|
|
338
|
-
app: options.app || `tb-seller-${options.name}`,
|
|
339
|
-
dryRun: false,
|
|
340
|
-
summary: String(summary)
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// -- deploy --
|
|
345
|
-
|
|
346
|
-
public deploy(
|
|
347
|
-
args: { app: string; image?: string; dryRun?: boolean },
|
|
348
|
-
json: boolean
|
|
349
|
-
): SellerActionResult<"deploy"> | string {
|
|
350
|
-
const provider = this.getProvider();
|
|
351
|
-
if (!json) {
|
|
352
|
-
return provider.deploySeller(args);
|
|
353
|
-
}
|
|
354
|
-
if (args.dryRun) {
|
|
355
|
-
return {
|
|
356
|
-
ok: true,
|
|
357
|
-
provider: "fly",
|
|
358
|
-
action: "deploy",
|
|
359
|
-
app: args.app,
|
|
360
|
-
dryRun: true,
|
|
361
|
-
commands: [`fly machine update <machine-id> --app ${args.app} --image ${args.image} --yes`]
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
const summary = provider.deploySeller(args);
|
|
365
|
-
return {
|
|
366
|
-
ok: true,
|
|
367
|
-
provider: "fly",
|
|
368
|
-
action: "deploy",
|
|
369
|
-
app: args.app,
|
|
370
|
-
dryRun: false,
|
|
371
|
-
summary: String(summary)
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// -- remove --
|
|
376
|
-
|
|
377
|
-
public remove(appName: string, dryRun: boolean, json: boolean): SellerActionResult<"remove"> | string {
|
|
378
|
-
const provider = this.getProvider();
|
|
379
|
-
if (!json) {
|
|
380
|
-
return provider.removeSeller(appName, dryRun);
|
|
381
|
-
}
|
|
382
|
-
if (dryRun) {
|
|
383
|
-
return {
|
|
384
|
-
ok: true,
|
|
385
|
-
provider: "fly",
|
|
386
|
-
action: "remove",
|
|
387
|
-
app: appName,
|
|
388
|
-
dryRun: true,
|
|
389
|
-
commands: [`fly apps destroy ${appName} --yes`]
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
const summary = provider.removeSeller(appName, dryRun);
|
|
393
|
-
return {
|
|
394
|
-
ok: true,
|
|
395
|
-
provider: "fly",
|
|
396
|
-
action: "remove",
|
|
397
|
-
app: appName,
|
|
398
|
-
dryRun: false,
|
|
399
|
-
summary: String(summary)
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// -- roll --
|
|
404
|
-
//
|
|
405
|
-
// Step 13 之后 (v1.1 增量): 不写死 5 台 fixed-fleet, 从 `fly apps list` 拉全部
|
|
406
|
-
// `tbs-*` 候选, 按顺序 image-only redeploy 到指定镜像. 失败停、报告.
|
|
407
|
-
//
|
|
408
|
-
// 设计约束 (跟 1.0.31 deploy 子命令保持一致):
|
|
409
|
-
// - 复用 FlyProvider.deploySeller (image-only, 不动 volume / config / secrets)
|
|
410
|
-
// - 走 --json 路径 (runFlyctlJson), 失败可解析, 不靠 stdio inherit
|
|
411
|
-
// - 候选列表 = fly apps list --json 中 name 以 `tbs-` 开头的, 排除 --exclude
|
|
412
|
-
// - 真跑: 失败那台后续不再执行, 报告 attempts 全部 + completed=false
|
|
413
|
-
// - dry-run: 跑 fly apps list 拿候选, 不真 deploy, attempts 全部 ok=true, summary=计划
|
|
414
|
-
|
|
415
|
-
public roll(
|
|
416
|
-
args: { image: string; exclude?: string[]; dryRun?: boolean },
|
|
417
|
-
json: boolean
|
|
418
|
-
): SellerRollResult | string {
|
|
419
|
-
const provider = this.getProvider();
|
|
420
|
-
const flyctl = this.getFlyctl();
|
|
421
|
-
const targetImage = args.image;
|
|
422
|
-
const excludeSet = new Set<string>((args.exclude || []).filter((s) => s.trim().length > 0));
|
|
423
|
-
|
|
424
|
-
if (!targetImage) {
|
|
425
|
-
throw new Error("seller roll requires --image registry.fly.io/tb-seller:<v>");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// 拿候选: 必须走 --json 路径 (需要结构化 Name 字段做 filter)
|
|
429
|
-
if (!providerCheck(flyctl)) {
|
|
430
|
-
throw new FlyCliMissingError(flyctl);
|
|
431
|
-
}
|
|
432
|
-
const env = flyEnv(this.getProviderConfig());
|
|
433
|
-
const stdout = runFlyctlJson(flyctl, ["apps", "list", "--json"], env) as string;
|
|
434
|
-
const allApps = parseFlyListJson(stdout);
|
|
435
|
-
const tbsOnly = allApps.filter((a) => a.name.startsWith("tbs-"));
|
|
436
|
-
const excluded = tbsOnly.filter((a) => excludeSet.has(a.name)).map((a) => a.name);
|
|
437
|
-
const candidates = tbsOnly.filter((a) => !excludeSet.has(a.name)).map((a) => a.name);
|
|
438
|
-
|
|
439
|
-
if (!json) {
|
|
440
|
-
// 文本路径: 先列计划, 然后按顺序跑
|
|
441
|
-
const lines: string[] = [];
|
|
442
|
-
lines.push(`[Fly.io] roll candidates (tbs-*, excludes applied): ${candidates.length}`);
|
|
443
|
-
for (const app of candidates) lines.push(` - ${app}`);
|
|
444
|
-
if (excluded.length > 0) {
|
|
445
|
-
lines.push(`[Fly.io] excluded by --exclude: ${excluded.join(", ")}`);
|
|
446
|
-
}
|
|
447
|
-
lines.push(`[Fly.io] image: ${targetImage}`);
|
|
448
|
-
lines.push(`[Fly.io] mode: ${args.dryRun ? "dry-run (no actual deploy)" : "live sequential, fail-fast"}`);
|
|
449
|
-
lines.push("");
|
|
450
|
-
|
|
451
|
-
let completed = true;
|
|
452
|
-
for (const app of candidates) {
|
|
453
|
-
if (args.dryRun) {
|
|
454
|
-
lines.push(`[Fly.io] [DRY-RUN] would update ${app} image to ${targetImage}`);
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const summary = provider.deploySeller({ app, image: targetImage });
|
|
459
|
-
lines.push(`[Fly.io] ${app}: ${summary}`);
|
|
460
|
-
} catch (err: any) {
|
|
461
|
-
lines.push(`[Fly.io] ${app}: FAILED — ${err.message || String(err)}`);
|
|
462
|
-
completed = false;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
lines.push("");
|
|
467
|
-
lines.push(completed ? "[Fly.io] roll completed." : "[Fly.io] roll stopped at failure.");
|
|
468
|
-
return lines.join("\n");
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// --json 路径: 同样顺序执行, 但输出结构化
|
|
472
|
-
const attempts: SellerRollAttempt[] = [];
|
|
473
|
-
let completed = true;
|
|
474
|
-
for (const app of candidates) {
|
|
475
|
-
if (args.dryRun) {
|
|
476
|
-
attempts.push({ app, ok: true, summary: `[dry-run] would update ${app} image to ${targetImage}` });
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const summary = provider.deploySeller({ app, image: targetImage });
|
|
481
|
-
attempts.push({ app, ok: true, summary: String(summary) });
|
|
482
|
-
} catch (err: any) {
|
|
483
|
-
attempts.push({ app, ok: false, error: err.message || String(err) });
|
|
484
|
-
completed = false;
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
ok: true,
|
|
490
|
-
provider: "fly",
|
|
491
|
-
action: "roll",
|
|
492
|
-
image: targetImage,
|
|
493
|
-
dryRun: Boolean(args.dryRun),
|
|
494
|
-
candidates,
|
|
495
|
-
excluded,
|
|
496
|
-
attempts,
|
|
497
|
-
completed
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// ===== helpers =====
|
|
503
|
-
|
|
504
|
-
/** 跟 FlyProvider 私有 flyEnv 行为一致; 复用 provider config token */
|
|
505
|
-
function flyEnv(config: SellerProviderConfig | undefined): NodeJS.ProcessEnv {
|
|
506
|
-
const configuredToken = config?.token;
|
|
507
|
-
const merged: NodeJS.ProcessEnv = { ...process.env };
|
|
508
|
-
if (configuredToken && !merged.FLY_API_TOKEN) {
|
|
509
|
-
merged.FLY_API_TOKEN = configuredToken;
|
|
510
|
-
}
|
|
511
|
-
return merged;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function providerCheck(flyctl: string): boolean {
|
|
515
|
-
try {
|
|
516
|
-
execSync(`which ${flyctl}`, { stdio: "ignore" });
|
|
517
|
-
return true;
|
|
518
|
-
} catch {
|
|
519
|
-
return false;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function buildCreateCommands(options: SellerCreateOptions): string[] {
|
|
524
|
-
const appName = options.app || `tb-seller-${options.name}`;
|
|
525
|
-
const region = options.region || "sin";
|
|
526
|
-
const lines: string[] = [];
|
|
527
|
-
if (options.flyConfig) {
|
|
528
|
-
lines.push(`fly apps create ${appName}`);
|
|
529
|
-
}
|
|
530
|
-
lines.push(`fly deploy --config ${options.flyConfig} --app ${appName} --region ${region}`);
|
|
531
|
-
if (options.image) {
|
|
532
|
-
lines.push(`fly machine update <machine-id> --app ${appName} --image ${options.image} --yes`);
|
|
533
|
-
}
|
|
534
|
-
return lines;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// re-exports 方便测试
|
|
538
|
-
export { parseFlyListJson, parseFlyListText, parseFlyStatusJson };
|