@tokenbuddy/tb-admin 1.0.36 → 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/dist/src/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ import { AdminClient } from "./client.js";
|
|
|
4
4
|
import { FlyProvider } from "./server-cmd.js";
|
|
5
5
|
import { SellerCommandRunner } from "./seller.js";
|
|
6
6
|
import { bindAdminUiCommand } from "./ui-command.js";
|
|
7
|
+
import { formatAdminInitResult, formatWorkdirDoctorResult, runAdminInitCommand, runWorkdirDoctor } from "./init-command.js";
|
|
8
|
+
import { resolveAdminWorkdir } from "./workdir.js";
|
|
7
9
|
import { loadRegistryFile, validateRegistryDocument } from "./bootstrap-registry.js";
|
|
8
10
|
import Table from "cli-table3";
|
|
9
11
|
import * as fs from "fs";
|
|
@@ -200,13 +202,91 @@ export function buildAdminCli(configManager) {
|
|
|
200
202
|
.option("--url <url>", "Remote seller core API url")
|
|
201
203
|
.option("--token <token>", "Operator Bearer token")
|
|
202
204
|
.option("--profile <profile>", "Use custom profile instead of default")
|
|
203
|
-
.option("--config <path>", "Use custom config file path")
|
|
205
|
+
.option("--config <path>", "Use custom config file path")
|
|
206
|
+
.option("--workdir <path>", "Admin workdir path (default: $TB_ADMIN_WORKDIR or ~/.config/tokenbuddy)");
|
|
204
207
|
bindAdminUiCommand(program, configManager);
|
|
208
|
+
function getRootConfigManager() {
|
|
209
|
+
const opts = program.opts();
|
|
210
|
+
if (opts.config || opts.workdir) {
|
|
211
|
+
return new ConfigManager(opts.config, { cliWorkdir: opts.workdir });
|
|
212
|
+
}
|
|
213
|
+
return configManager;
|
|
214
|
+
}
|
|
215
|
+
program
|
|
216
|
+
.command("init")
|
|
217
|
+
.description("Initialize an admin workdir for provider-specific deployment assets")
|
|
218
|
+
.option("--provider <provider>", "Deployment provider (default: fly.io)", "fly.io")
|
|
219
|
+
.option("--workdir <path>", "Admin workdir path (default: root --workdir, $TB_ADMIN_WORKDIR, or ~/.config/tokenbuddy)")
|
|
220
|
+
.option("--check-only", "Only check provider requirements; do not write files")
|
|
221
|
+
.option("--force", "Refresh regenerable templates without overwriting runtime secret files")
|
|
222
|
+
.option("--install-tools", "Install missing provider CLI tools when supported")
|
|
223
|
+
.option("--json", "Output structured JSON")
|
|
224
|
+
.action((options) => {
|
|
225
|
+
try {
|
|
226
|
+
const rootOptions = program.opts();
|
|
227
|
+
const result = runAdminInitCommand({
|
|
228
|
+
provider: options.provider,
|
|
229
|
+
cliWorkdir: options.workdir || rootOptions.workdir,
|
|
230
|
+
checkOnly: Boolean(options.checkOnly),
|
|
231
|
+
force: Boolean(options.force),
|
|
232
|
+
installTools: Boolean(options.installTools)
|
|
233
|
+
});
|
|
234
|
+
if (options.json) {
|
|
235
|
+
console.log(JSON.stringify(result, null, 2));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log(formatAdminInitResult(result));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
console.error("Error:", err.message);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
const workdirCmd = program.command("workdir").description("Inspect the resolved admin workdir");
|
|
247
|
+
workdirCmd
|
|
248
|
+
.command("path")
|
|
249
|
+
.description("Print the resolved admin workdir path")
|
|
250
|
+
.option("--workdir <path>", "Admin workdir path")
|
|
251
|
+
.action((options) => {
|
|
252
|
+
try {
|
|
253
|
+
const rootOptions = program.opts();
|
|
254
|
+
console.log(resolveAdminWorkdir({ cliWorkdir: options.workdir || rootOptions.workdir }));
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error("Error:", err.message);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
workdirCmd
|
|
262
|
+
.command("doctor")
|
|
263
|
+
.description("Check admin workdir files and provider tooling")
|
|
264
|
+
.option("--provider <provider>", "Deployment provider (default: fly.io)", "fly.io")
|
|
265
|
+
.option("--workdir <path>", "Admin workdir path")
|
|
266
|
+
.option("--json", "Output structured JSON")
|
|
267
|
+
.action((options) => {
|
|
268
|
+
try {
|
|
269
|
+
const rootOptions = program.opts();
|
|
270
|
+
const result = runWorkdirDoctor({
|
|
271
|
+
provider: options.provider,
|
|
272
|
+
cliWorkdir: options.workdir || rootOptions.workdir
|
|
273
|
+
});
|
|
274
|
+
if (options.json) {
|
|
275
|
+
console.log(JSON.stringify(result, null, 2));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.log(formatWorkdirDoctorResult(result));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
console.error("Error:", err.message);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
205
286
|
// Helper to resolve client
|
|
206
287
|
function getClient() {
|
|
207
288
|
const opts = program.opts();
|
|
208
|
-
const
|
|
209
|
-
const mgr = configPath ? new ConfigManager(configPath) : configManager;
|
|
289
|
+
const mgr = getRootConfigManager();
|
|
210
290
|
const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
|
|
211
291
|
const token = opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
|
|
212
292
|
const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
|
|
@@ -221,8 +301,7 @@ export function buildAdminCli(configManager) {
|
|
|
221
301
|
}
|
|
222
302
|
function getBaseUrl() {
|
|
223
303
|
const opts = program.opts();
|
|
224
|
-
const
|
|
225
|
-
const mgr = configPath ? new ConfigManager(configPath) : configManager;
|
|
304
|
+
const mgr = getRootConfigManager();
|
|
226
305
|
const url = opts.url || process.env.TOKENBUDDY_ADMIN_URL;
|
|
227
306
|
const profileName = opts.profile || process.env.TOKENBUDDY_ADMIN_PROFILE;
|
|
228
307
|
if (url) {
|
|
@@ -1039,8 +1118,7 @@ export function buildAdminCli(configManager) {
|
|
|
1039
1118
|
.option("--token <token>", "Bearer Operator secret token")
|
|
1040
1119
|
.action((profileName, options) => {
|
|
1041
1120
|
const opts = program.opts();
|
|
1042
|
-
const
|
|
1043
|
-
const mgr = configPath ? new ConfigManager(configPath) : configManager;
|
|
1121
|
+
const mgr = getRootConfigManager();
|
|
1044
1122
|
const url = options.url || opts.url || process.env.TOKENBUDDY_ADMIN_URL;
|
|
1045
1123
|
const token = options.token || opts.token || process.env.TOKENBUDDY_ADMIN_TOKEN;
|
|
1046
1124
|
if (!url || !token) {
|
|
@@ -1055,8 +1133,7 @@ export function buildAdminCli(configManager) {
|
|
|
1055
1133
|
.description("Switch default profile to select")
|
|
1056
1134
|
.action((profileName) => {
|
|
1057
1135
|
const opts = program.opts();
|
|
1058
|
-
const
|
|
1059
|
-
const mgr = configPath ? new ConfigManager(configPath) : configManager;
|
|
1136
|
+
const mgr = getRootConfigManager();
|
|
1060
1137
|
try {
|
|
1061
1138
|
mgr.useProfile(profileName);
|
|
1062
1139
|
console.log(`Now using profile \`${profileName}\` by default.`);
|
|
@@ -1070,8 +1147,7 @@ export function buildAdminCli(configManager) {
|
|
|
1070
1147
|
.description("List all configured profiles in the active config file")
|
|
1071
1148
|
.action(() => {
|
|
1072
1149
|
const opts = program.opts();
|
|
1073
|
-
const
|
|
1074
|
-
const mgr = configPath ? new ConfigManager(configPath) : configManager;
|
|
1150
|
+
const mgr = getRootConfigManager();
|
|
1075
1151
|
const profiles = mgr.listProfiles();
|
|
1076
1152
|
const config = mgr.load();
|
|
1077
1153
|
console.log(`=== Configured Local Profiles (config: ${mgr.getConfigPath() || "default"}) ===`);
|
|
@@ -1117,20 +1193,17 @@ export function buildAdminCli(configManager) {
|
|
|
1117
1193
|
.description("Print the config file path in use (resolves --config, env, or default)")
|
|
1118
1194
|
.action(() => {
|
|
1119
1195
|
const opts = program.opts();
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
console.log(resolved);
|
|
1196
|
+
const mgr = getRootConfigManager();
|
|
1197
|
+
console.log(mgr.getConfigPath());
|
|
1123
1198
|
});
|
|
1124
1199
|
// 8. Seller Command (Fly.io)
|
|
1125
1200
|
const sellerCmd = program.command("seller").description("Deploy and manage seller containers on Fly.io");
|
|
1126
1201
|
function getFlyProvider() {
|
|
1127
|
-
const
|
|
1128
|
-
const mgr = opts.config ? new ConfigManager(opts.config) : configManager;
|
|
1202
|
+
const mgr = getRootConfigManager();
|
|
1129
1203
|
return new FlyProvider(mgr.getSellerProvider("fly"));
|
|
1130
1204
|
}
|
|
1131
1205
|
function getSellerRunner() {
|
|
1132
|
-
const
|
|
1133
|
-
const mgr = opts.config ? new ConfigManager(opts.config) : configManager;
|
|
1206
|
+
const mgr = getRootConfigManager();
|
|
1134
1207
|
return new SellerCommandRunner(mgr);
|
|
1135
1208
|
}
|
|
1136
1209
|
function printResult(res, json) {
|
|
@@ -1183,7 +1256,7 @@ export function buildAdminCli(configManager) {
|
|
|
1183
1256
|
.option("--app <app>", "Fly.io app name (bypasses tb-seller- prefix; use tbs-<random> format)")
|
|
1184
1257
|
.option("--region <region>", "Fly region (default: sin)", "sin")
|
|
1185
1258
|
.requiredOption("--image <image>", "Published Docker image, for example registry.fly.io/tb-seller:<v>")
|
|
1186
|
-
.
|
|
1259
|
+
.option("--fly-config <path>", "Fly.io config file path (default: $TB_ADMIN_WORKDIR/fly/fly.tb-seller.toml)")
|
|
1187
1260
|
.option("--volume-name <name>", "Persistent volume name")
|
|
1188
1261
|
.option("--volume-size-gb <gb>", "Persistent volume size in GB", (v) => parseInt(v, 10))
|
|
1189
1262
|
.option("--volume-id <id>", "Attach existing volume by ID (skips volume creation)")
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AdminWorkdirOptions } from "./workdir.js";
|
|
1
2
|
/**
|
|
2
3
|
* admin CLI 单个 profile:远程 wallet-bootstrap / seller 服务的 URL + 鉴权 token。
|
|
3
4
|
*/
|
|
@@ -55,14 +56,19 @@ export interface AdminConfig {
|
|
|
55
56
|
* @returns 配置文件绝对路径
|
|
56
57
|
*/
|
|
57
58
|
export declare function getDefaultConfigPath(): string;
|
|
59
|
+
export interface ConfigManagerOptions extends AdminWorkdirOptions {
|
|
60
|
+
workdir?: string;
|
|
61
|
+
}
|
|
58
62
|
/**
|
|
59
63
|
* admin CLI 配置的读写管理器(TOML 优先,legacy JSON 兼容)。
|
|
60
64
|
* 写盘时自动 chmod 0600(best effort)保护含 token 的配置文件。
|
|
61
65
|
*/
|
|
62
66
|
export declare class ConfigManager {
|
|
63
67
|
private configPath;
|
|
64
|
-
|
|
68
|
+
private workdir;
|
|
69
|
+
constructor(customPath?: string, options?: ConfigManagerOptions);
|
|
65
70
|
getConfigPath(): string;
|
|
71
|
+
getWorkdir(): string;
|
|
66
72
|
load(): AdminConfig;
|
|
67
73
|
save(config: AdminConfig): void;
|
|
68
74
|
getProfile(name?: string): AdminProfile | undefined;
|
package/dist/src/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import TOML from "@iarna/toml";
|
|
5
|
+
import { defaultAdminConfigPath, defaultAdminWorkdir, resolveAdminConfigPath, resolveAdminWorkdir } from "./workdir.js";
|
|
5
6
|
/**
|
|
6
7
|
* 解析 admin CLI 的默认配置路径:`~/.config/tokenbuddy/admin.toml`。
|
|
7
8
|
* 兼容旧版 `~/.tokenbuddy/admin.json`(legacy JSON),由 `ConfigManager` 自动识别。
|
|
@@ -9,8 +10,7 @@ import TOML from "@iarna/toml";
|
|
|
9
10
|
* @returns 配置文件绝对路径
|
|
10
11
|
*/
|
|
11
12
|
export function getDefaultConfigPath() {
|
|
12
|
-
|
|
13
|
-
return path.join(home, ".config", "tokenbuddy", "admin.toml");
|
|
13
|
+
return defaultAdminConfigPath(defaultAdminWorkdir(os.homedir()));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* admin CLI 配置的读写管理器(TOML 优先,legacy JSON 兼容)。
|
|
@@ -18,12 +18,24 @@ export function getDefaultConfigPath() {
|
|
|
18
18
|
*/
|
|
19
19
|
export class ConfigManager {
|
|
20
20
|
configPath;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
workdir;
|
|
22
|
+
constructor(customPath, options = {}) {
|
|
23
|
+
this.workdir = options.workdir
|
|
24
|
+
? path.resolve(options.workdir)
|
|
25
|
+
: resolveAdminWorkdir(options);
|
|
26
|
+
this.configPath = resolveAdminConfigPath({
|
|
27
|
+
workdir: this.workdir,
|
|
28
|
+
cliConfig: customPath,
|
|
29
|
+
env: options.env,
|
|
30
|
+
homeDir: options.homeDir
|
|
31
|
+
});
|
|
23
32
|
}
|
|
24
33
|
getConfigPath() {
|
|
25
34
|
return this.configPath;
|
|
26
35
|
}
|
|
36
|
+
getWorkdir() {
|
|
37
|
+
return this.workdir;
|
|
38
|
+
}
|
|
27
39
|
load() {
|
|
28
40
|
if (!fs.existsSync(this.configPath)) {
|
|
29
41
|
return { profiles: {} };
|
|
@@ -68,11 +68,7 @@ export function formatDiscountRatio(discountRatio) {
|
|
|
68
68
|
if (!Number.isFinite(discountRatio))
|
|
69
69
|
return UNKNOWN_VALUE;
|
|
70
70
|
const ratio = Math.max(0, discountRatio);
|
|
71
|
-
|
|
72
|
-
return "免费";
|
|
73
|
-
if (Math.abs(ratio - 1) < 0.0001)
|
|
74
|
-
return "原价";
|
|
75
|
-
return `${formatSignificantDiscount(ratio * 10)}折`;
|
|
71
|
+
return formatRatioValue(ratio);
|
|
76
72
|
}
|
|
77
73
|
export function formatPriceMicrosPerMillion(value) {
|
|
78
74
|
return formatMoney(value, { digits: 4 });
|
|
@@ -80,9 +76,8 @@ export function formatPriceMicrosPerMillion(value) {
|
|
|
80
76
|
export function formatPricePair(inputMicros, outputMicros) {
|
|
81
77
|
return formatMoneyPair(inputMicros, outputMicros, { digits: 4 });
|
|
82
78
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
79
|
+
function formatRatioValue(value) {
|
|
80
|
+
return String(value);
|
|
86
81
|
}
|
|
87
82
|
export function normalizeStatusLabel(status) {
|
|
88
83
|
if (!status)
|
|
@@ -272,15 +267,12 @@ function formatPercent(value) {
|
|
|
272
267
|
function formatDiscountRatio(discountRatio) {
|
|
273
268
|
if (!Number.isFinite(discountRatio)) return UNKNOWN_VALUE;
|
|
274
269
|
const ratio = Math.max(0, discountRatio);
|
|
275
|
-
|
|
276
|
-
if (Math.abs(ratio - 1) < 0.0001) return "原价";
|
|
277
|
-
return formatSignificantDiscount(ratio * 10) + "折";
|
|
270
|
+
return formatRatioValue(ratio);
|
|
278
271
|
}
|
|
279
272
|
function formatPriceMicrosPerMillion(value) { return formatMoney(value, { digits: 4 }); }
|
|
280
273
|
function formatPricePair(inputMicros, outputMicros) { return formatMoneyPair(inputMicros, outputMicros, { digits: 4 }); }
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
274
|
+
function formatRatioValue(value) {
|
|
275
|
+
return String(value);
|
|
284
276
|
}
|
|
285
277
|
function normalizeStatusLabel(status) {
|
|
286
278
|
if (!status) return UNKNOWN_VALUE;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AdminProviderId, ProviderCheckResult, SpawnSyncRunner } from "./providers/provider-definition.js";
|
|
2
|
+
export interface AdminInitOptions {
|
|
3
|
+
provider?: string;
|
|
4
|
+
cliWorkdir?: string;
|
|
5
|
+
checkOnly?: boolean;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
installTools?: boolean;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
now?: Date;
|
|
11
|
+
templateRoot?: string;
|
|
12
|
+
packageRoot?: string;
|
|
13
|
+
packageVersion?: string;
|
|
14
|
+
spawnSync?: SpawnSyncRunner;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkdirDoctorOptions {
|
|
17
|
+
provider?: string;
|
|
18
|
+
cliWorkdir?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
spawnSync?: SpawnSyncRunner;
|
|
22
|
+
}
|
|
23
|
+
export interface InitFileResult {
|
|
24
|
+
path: string;
|
|
25
|
+
status: "created" | "updated" | "skipped";
|
|
26
|
+
}
|
|
27
|
+
export interface AdminInitResult {
|
|
28
|
+
provider: AdminProviderId;
|
|
29
|
+
workdir: string;
|
|
30
|
+
templateVersion: string;
|
|
31
|
+
checkOnly: boolean;
|
|
32
|
+
checks: ProviderCheckResult[];
|
|
33
|
+
files: InitFileResult[];
|
|
34
|
+
install?: {
|
|
35
|
+
attempted: boolean;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
next: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface WorkdirDoctorResult {
|
|
42
|
+
provider: AdminProviderId;
|
|
43
|
+
workdir: string;
|
|
44
|
+
checks: ProviderCheckResult[];
|
|
45
|
+
}
|
|
46
|
+
export declare function runAdminInitCommand(options?: AdminInitOptions): AdminInitResult;
|
|
47
|
+
export declare function runWorkdirDoctor(options?: WorkdirDoctorOptions): WorkdirDoctorResult;
|
|
48
|
+
export declare function formatAdminInitResult(result: AdminInitResult): string;
|
|
49
|
+
export declare function formatWorkdirDoctorResult(result: WorkdirDoctorResult): string;
|
|
50
|
+
//# sourceMappingURL=init-command.d.ts.map
|
|
@@ -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
|