@tokenbuddy/tb-admin 1.0.36 → 1.0.38

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 +98 -25
  2. package/dist/src/config.d.ts +8 -2
  3. package/dist/src/config.js +17 -5
  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 +9 -3
  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
@@ -0,0 +1,347 @@
1
+ import { spawnSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { resolveAdminWorkdir, resolveWorkdirPath } from "./workdir.js";
6
+ import { flyIoProviderDefinition } from "./providers/fly-io.js";
7
+ const providerDefinitions = {
8
+ "fly.io": flyIoProviderDefinition
9
+ };
10
+ const requiredWorkdirFiles = [
11
+ "tokenbuddy-admin-workdir.json",
12
+ "admin.toml",
13
+ "providers/fly.io.toml",
14
+ "env/deploy.env",
15
+ "fly/fly.tb-seller.toml",
16
+ "fly/fly.tb-registry.toml",
17
+ "deploy-secrets/seller-configs/seller.example.yaml",
18
+ "deploy-secrets/bootstrap/tb-registry.example.yaml"
19
+ ];
20
+ function currentModuleDir() {
21
+ if (typeof __dirname !== "undefined") {
22
+ return __dirname;
23
+ }
24
+ const stack = new Error().stack || "";
25
+ const fileUrlMatch = stack.match(/(file:\/\/\/[^)\n]+\/init-command\.js):\d+:\d+/);
26
+ if (fileUrlMatch) {
27
+ return path.dirname(fileURLToPath(fileUrlMatch[1]));
28
+ }
29
+ const filePathMatch = stack.match(/(\/[^)\n]+\/init-command\.(?:js|ts)):\d+:\d+/);
30
+ if (filePathMatch) {
31
+ return path.dirname(filePathMatch[1]);
32
+ }
33
+ return process.cwd();
34
+ }
35
+ function readPackageJson(packageRoot) {
36
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
37
+ }
38
+ function findAdminPackageRoot(startDir = currentModuleDir()) {
39
+ let current = path.resolve(startDir);
40
+ const seen = new Set();
41
+ while (!seen.has(current)) {
42
+ seen.add(current);
43
+ const packageJsonPath = path.join(current, "package.json");
44
+ if (fs.existsSync(packageJsonPath)) {
45
+ const packageJson = readPackageJson(current);
46
+ if (packageJson.name === "@tokenbuddy/tb-admin") {
47
+ return current;
48
+ }
49
+ }
50
+ const parent = path.dirname(current);
51
+ if (parent === current) {
52
+ break;
53
+ }
54
+ current = parent;
55
+ }
56
+ throw new Error("Could not locate @tokenbuddy/tb-admin package root for templates");
57
+ }
58
+ function getProviderDefinition(provider) {
59
+ const id = (provider || "fly.io");
60
+ const definition = providerDefinitions[id];
61
+ if (!definition) {
62
+ throw new Error(`Unknown provider: ${provider}. Supported providers: ${Object.keys(providerDefinitions).join(", ")}`);
63
+ }
64
+ return definition;
65
+ }
66
+ function packageTemplateContext(definition, options) {
67
+ const packageRoot = options.packageRoot || findAdminPackageRoot();
68
+ const packageJson = readPackageJson(packageRoot);
69
+ const templateRoot = options.templateRoot || path.join(packageRoot, definition.templateRelativeRoot);
70
+ if (!fs.existsSync(templateRoot)) {
71
+ throw new Error(`Provider template directory not found: ${templateRoot}`);
72
+ }
73
+ return {
74
+ packageRoot,
75
+ packageVersion: options.packageVersion || String(packageJson.version || "0.0.0"),
76
+ templateRoot
77
+ };
78
+ }
79
+ function listTemplateFiles(root) {
80
+ const entries = fs.readdirSync(root, { withFileTypes: true });
81
+ const files = [];
82
+ for (const entry of entries) {
83
+ const fullPath = path.join(root, entry.name);
84
+ if (entry.isDirectory()) {
85
+ for (const child of listTemplateFiles(fullPath)) {
86
+ files.push(path.join(entry.name, child));
87
+ }
88
+ continue;
89
+ }
90
+ if (entry.isFile()) {
91
+ files.push(entry.name);
92
+ }
93
+ }
94
+ return files.sort();
95
+ }
96
+ function targetForTemplate(relativePath) {
97
+ if (relativePath === "admin.toml.example") {
98
+ return "admin.toml";
99
+ }
100
+ if (relativePath === "provider.toml.example") {
101
+ return "providers/fly.io.toml";
102
+ }
103
+ if (relativePath === "env/deploy.env.example") {
104
+ return "env/deploy.env";
105
+ }
106
+ return relativePath;
107
+ }
108
+ function isForceOverwritable(relativeTarget) {
109
+ return (relativeTarget.startsWith("fly/") ||
110
+ relativeTarget.endsWith(".example.yaml") ||
111
+ relativeTarget.endsWith(".example.json") ||
112
+ relativeTarget.endsWith(".example.env") ||
113
+ path.basename(relativeTarget) === "README.md");
114
+ }
115
+ function isPrivateTarget(relativeTarget) {
116
+ return (relativeTarget === "admin.toml" ||
117
+ relativeTarget === "env/deploy.env" ||
118
+ relativeTarget.startsWith("deploy-secrets/"));
119
+ }
120
+ function templateCopyPlans(templateRoot, workdir) {
121
+ return listTemplateFiles(templateRoot).map((relativeSource) => {
122
+ const relativeTarget = targetForTemplate(relativeSource);
123
+ return {
124
+ source: path.join(templateRoot, relativeSource),
125
+ target: resolveWorkdirPath(workdir, relativeTarget),
126
+ overwriteWithForce: isForceOverwritable(relativeTarget),
127
+ chmodPrivate: isPrivateTarget(relativeTarget)
128
+ };
129
+ });
130
+ }
131
+ function chmodPrivate(filePath) {
132
+ try {
133
+ fs.chmodSync(filePath, 0o600);
134
+ }
135
+ catch {
136
+ // Best effort only; chmod is not available on every platform.
137
+ }
138
+ }
139
+ function copyTemplates(templateRoot, workdir, force) {
140
+ const results = [];
141
+ for (const plan of templateCopyPlans(templateRoot, workdir)) {
142
+ const exists = fs.existsSync(plan.target);
143
+ if (exists && !(force && plan.overwriteWithForce)) {
144
+ results.push({ path: plan.target, status: "skipped" });
145
+ continue;
146
+ }
147
+ fs.mkdirSync(path.dirname(plan.target), { recursive: true });
148
+ fs.copyFileSync(plan.source, plan.target);
149
+ if (plan.chmodPrivate) {
150
+ chmodPrivate(plan.target);
151
+ }
152
+ results.push({ path: plan.target, status: exists ? "updated" : "created" });
153
+ }
154
+ return results;
155
+ }
156
+ function writeManifest(workdir, provider, templateVersion, now) {
157
+ const manifestPath = path.join(workdir, "tokenbuddy-admin-workdir.json");
158
+ const existed = fs.existsSync(manifestPath);
159
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
160
+ fs.writeFileSync(manifestPath, `${JSON.stringify({
161
+ schemaVersion: 1,
162
+ provider,
163
+ templateVersion,
164
+ createdBy: "@tokenbuddy/tb-admin",
165
+ createdAt: now.toISOString()
166
+ }, null, 2)}\n`, "utf8");
167
+ return {
168
+ path: manifestPath,
169
+ status: existed ? "updated" : "created"
170
+ };
171
+ }
172
+ function ensureWorkdirDirectories(workdir) {
173
+ for (const relative of ["providers", "env", "fly", "deploy-secrets/seller-configs", "deploy-secrets/bootstrap", "artifacts"]) {
174
+ fs.mkdirSync(path.join(workdir, relative), { recursive: true });
175
+ }
176
+ }
177
+ function checkContext(options) {
178
+ return {
179
+ env: options.env || process.env,
180
+ spawnSync: options.spawnSync || spawnSync
181
+ };
182
+ }
183
+ export function runAdminInitCommand(options = {}) {
184
+ const definition = getProviderDefinition(options.provider);
185
+ const workdir = resolveAdminWorkdir({
186
+ cliWorkdir: options.cliWorkdir,
187
+ env: options.env || process.env
188
+ });
189
+ const template = packageTemplateContext(definition, options);
190
+ const checks = definition.check(checkContext(options));
191
+ const files = [];
192
+ let install;
193
+ const flyMissing = checks.some((check) => check.id === "fly" && check.status === "missing");
194
+ if (options.installTools && flyMissing && definition.install) {
195
+ const installResult = definition.install({
196
+ ...checkContext(options),
197
+ dryRun: false
198
+ });
199
+ install = {
200
+ attempted: true,
201
+ ok: installResult.ok,
202
+ message: installResult.message
203
+ };
204
+ }
205
+ if (!options.checkOnly) {
206
+ ensureWorkdirDirectories(workdir);
207
+ files.push(...copyTemplates(template.templateRoot, workdir, Boolean(options.force)));
208
+ files.push(writeManifest(workdir, definition.id, template.packageVersion, options.now || new Date()));
209
+ }
210
+ return {
211
+ provider: definition.id,
212
+ workdir,
213
+ templateVersion: template.packageVersion,
214
+ checkOnly: Boolean(options.checkOnly),
215
+ checks,
216
+ files,
217
+ install,
218
+ next: [
219
+ `export TB_ADMIN_WORKDIR=${workdir}`,
220
+ `edit ${path.join(workdir, definition.defaultPaths.deployEnv)}`,
221
+ `edit ${path.join(workdir, "admin.toml")}`
222
+ ]
223
+ };
224
+ }
225
+ function fileCheck(id, label, filePath) {
226
+ return {
227
+ id,
228
+ label,
229
+ status: fs.existsSync(filePath) ? "ok" : "missing",
230
+ message: fs.existsSync(filePath) ? `${label} exists` : `${label} missing: ${filePath}`
231
+ };
232
+ }
233
+ function deploySecretsGitCheck(workdir, runner) {
234
+ const deploySecretsPath = path.join(workdir, "deploy-secrets");
235
+ if (!fs.existsSync(deploySecretsPath)) {
236
+ return {
237
+ id: "deploy-secrets-git",
238
+ label: "deploy-secrets git",
239
+ status: "missing",
240
+ message: `deploy-secrets directory missing: ${deploySecretsPath}`
241
+ };
242
+ }
243
+ const rootResult = runner("git", ["-C", workdir, "rev-parse", "--show-toplevel"], {
244
+ encoding: "utf8",
245
+ stdio: ["ignore", "pipe", "pipe"]
246
+ });
247
+ if (rootResult.error || rootResult.status !== 0 || !rootResult.stdout) {
248
+ return {
249
+ id: "deploy-secrets-git",
250
+ label: "deploy-secrets git",
251
+ status: "ok",
252
+ message: "deploy-secrets is outside a git worktree"
253
+ };
254
+ }
255
+ const gitRoot = String(rootResult.stdout).trim();
256
+ const relative = path.relative(gitRoot, deploySecretsPath);
257
+ if (relative.startsWith("..")) {
258
+ return {
259
+ id: "deploy-secrets-git",
260
+ label: "deploy-secrets git",
261
+ status: "ok",
262
+ message: "deploy-secrets is outside the current git root"
263
+ };
264
+ }
265
+ const trackedResult = runner("git", ["-C", gitRoot, "ls-files", "--error-unmatch", relative], {
266
+ encoding: "utf8",
267
+ stdio: ["ignore", "pipe", "pipe"]
268
+ });
269
+ if (!trackedResult.error && trackedResult.status === 0) {
270
+ return {
271
+ id: "deploy-secrets-git",
272
+ label: "deploy-secrets git",
273
+ status: "warning",
274
+ message: `deploy-secrets appears to be tracked by git: ${deploySecretsPath}`
275
+ };
276
+ }
277
+ return {
278
+ id: "deploy-secrets-git",
279
+ label: "deploy-secrets git",
280
+ status: "ok",
281
+ message: "deploy-secrets is not tracked by git"
282
+ };
283
+ }
284
+ export function runWorkdirDoctor(options = {}) {
285
+ const definition = getProviderDefinition(options.provider);
286
+ const workdir = resolveAdminWorkdir({
287
+ cliWorkdir: options.cliWorkdir,
288
+ env: options.env || process.env
289
+ });
290
+ const runner = options.spawnSync || spawnSync;
291
+ const checks = [
292
+ ...requiredWorkdirFiles.map((relative) => fileCheck(`file:${relative}`, relative, path.join(workdir, relative))),
293
+ deploySecretsGitCheck(workdir, runner),
294
+ ...definition.check({
295
+ env: options.env || process.env,
296
+ spawnSync: runner
297
+ })
298
+ ];
299
+ return {
300
+ provider: definition.id,
301
+ workdir,
302
+ checks
303
+ };
304
+ }
305
+ export function formatAdminInitResult(result) {
306
+ const lines = [
307
+ result.checkOnly ? "TokenBuddy admin workdir check complete" : "TokenBuddy admin workdir initialized",
308
+ ` provider : ${result.provider}`,
309
+ ` workdir : ${result.workdir}`,
310
+ ` templates: @tokenbuddy/tb-admin@${result.templateVersion}`,
311
+ "",
312
+ "Checks"
313
+ ];
314
+ for (const check of result.checks) {
315
+ const suffix = check.command ? ` (run: ${check.command})` : "";
316
+ lines.push(` ${check.label.padEnd(10)} ${check.status}${suffix}`);
317
+ }
318
+ if (result.install) {
319
+ lines.push("", `Install: ${result.install.ok ? "ok" : "failed"} - ${result.install.message}`);
320
+ }
321
+ if (result.files.length > 0) {
322
+ lines.push("", "Files");
323
+ for (const file of result.files) {
324
+ lines.push(` ${file.status.padEnd(7)} ${file.path}`);
325
+ }
326
+ }
327
+ lines.push("", "Next");
328
+ for (const next of result.next) {
329
+ lines.push(` ${next}`);
330
+ }
331
+ return lines.join("\n");
332
+ }
333
+ export function formatWorkdirDoctorResult(result) {
334
+ const lines = [
335
+ "TokenBuddy admin workdir doctor",
336
+ ` provider : ${result.provider}`,
337
+ ` workdir : ${result.workdir}`,
338
+ "",
339
+ "Checks"
340
+ ];
341
+ for (const check of result.checks) {
342
+ const suffix = check.command ? ` (run: ${check.command})` : "";
343
+ lines.push(` ${check.label.padEnd(24)} ${check.status}${suffix}`);
344
+ }
345
+ return lines.join("\n");
346
+ }
347
+ //# sourceMappingURL=init-command.js.map
@@ -0,0 +1,3 @@
1
+ import type { AdminProviderDefinition } from "./provider-definition.js";
2
+ export declare const flyIoProviderDefinition: AdminProviderDefinition;
3
+ //# sourceMappingURL=fly-io.d.ts.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider-definition.js.map
@@ -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;
@@ -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(options);
232
+ return provider.createSeller(resolvedOptions);
212
233
  }
213
234
  // --json 路径: dry-run 模式直接列命令, 不实际调 flyctl
214
- if (options.dryRun) {
235
+ if (resolvedOptions.dryRun) {
215
236
  return {
216
237
  ok: true,
217
238
  provider: "fly",
218
239
  action: "create",
219
- app: options.app || `tb-seller-${options.name}`,
240
+ app: resolvedOptions.app || `tb-seller-${resolvedOptions.name}`,
220
241
  dryRun: true,
221
- commands: buildCreateCommands(options)
242
+ commands: buildCreateCommands(resolvedOptions)
222
243
  };
223
244
  }
224
245
  // 非 dry-run: 调 FlyProvider (它已经会跑 flyctl), 包装 stdout
225
- const summary = provider.createSeller(options);
246
+ const summary = provider.createSeller(resolvedOptions);
226
247
  return {
227
248
  ok: true,
228
249
  provider: "fly",
229
250
  action: "create",
230
- app: options.app || `tb-seller-${options.name}`,
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
- if (options.flyConfig) {
403
- lines.push(`fly apps create ${appName}`);
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 方便测试
@@ -40,6 +40,7 @@ export interface SellerCreateOptions {
40
40
  volumeId?: string;
41
41
  volumeSnapshotRetentionDays?: number;
42
42
  initialConfigPath?: string;
43
+ resolvedWorkdir?: string;
43
44
  dryRun?: boolean;
44
45
  }
45
46
  /**
@@ -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
- throw new Error("seller create requires --fly-config deploy/fly.io/fly.tb-seller.toml");
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
- requireReadableFile(flyConfig, "Fly config");
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
  }
@@ -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