@tokenbuddy/tb-admin 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/src/cli.js +92 -19
  2. package/dist/src/config.d.ts +7 -1
  3. package/dist/src/config.js +16 -4
  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 +8 -2
  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
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 configPath = opts.config;
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 configPath = opts.config;
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 configPath = opts.config;
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 configPath = opts.config;
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 configPath = opts.config;
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 configPath = opts.config || process.env.TOKENBUDDY_ADMIN_CONFIG;
1121
- const resolved = configPath || `${process.env.HOME || ""}/.config/tokenbuddy/admin.toml`;
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 opts = program.opts();
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 opts = program.opts();
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
- .requiredOption("--fly-config <path>", "Fly.io config file path, for example deploy/fly.io/fly.tb-seller.toml")
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)")
@@ -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
- constructor(customPath?: string);
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;
@@ -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
- const home = os.homedir();
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
- constructor(customPath) {
22
- this.configPath = customPath || getDefaultConfigPath();
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
- if (ratio === 0)
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 formatSignificantDiscount(value) {
84
- const rounded = Math.round(value * 10) / 10;
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
- if (ratio === 0) return "免费";
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 formatSignificantDiscount(value) {
282
- const rounded = Math.round(value * 10) / 10;
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