@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.
- package/dist/src/cli.js +98 -25
- package/dist/src/config.d.ts +8 -2
- package/dist/src/config.js +17 -5
- 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 +9 -3
- 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,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,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
|