@tokenbuddy/tokenbuddy 1.0.13 → 1.0.15

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 (70) hide show
  1. package/dist/src/buyer-store.d.ts +23 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +31 -6
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/clawtip-bootstrap.d.ts +23 -0
  6. package/dist/src/clawtip-bootstrap.d.ts.map +1 -0
  7. package/dist/src/clawtip-bootstrap.js +47 -0
  8. package/dist/src/clawtip-bootstrap.js.map +1 -0
  9. package/dist/src/cli.d.ts +24 -33
  10. package/dist/src/cli.d.ts.map +1 -1
  11. package/dist/src/cli.js +157 -58
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/daemon.d.ts +79 -1
  14. package/dist/src/daemon.d.ts.map +1 -1
  15. package/dist/src/daemon.js +984 -23
  16. package/dist/src/daemon.js.map +1 -1
  17. package/dist/src/model-index.d.ts +1 -1
  18. package/dist/src/model-index.d.ts.map +1 -1
  19. package/dist/src/model-index.js +4 -0
  20. package/dist/src/model-index.js.map +1 -1
  21. package/dist/src/prewarm-cache.d.ts +4 -0
  22. package/dist/src/prewarm-cache.d.ts.map +1 -1
  23. package/dist/src/prewarm-cache.js +2 -1
  24. package/dist/src/prewarm-cache.js.map +1 -1
  25. package/dist/src/prewarm-scheduler.d.ts +2 -0
  26. package/dist/src/prewarm-scheduler.d.ts.map +1 -1
  27. package/dist/src/prewarm-scheduler.js +4 -2
  28. package/dist/src/prewarm-scheduler.js.map +1 -1
  29. package/dist/src/route-failover.d.ts.map +1 -1
  30. package/dist/src/route-failover.js +10 -0
  31. package/dist/src/route-failover.js.map +1 -1
  32. package/dist/src/seller-catalog.d.ts +17 -0
  33. package/dist/src/seller-catalog.d.ts.map +1 -1
  34. package/dist/src/seller-catalog.js +15 -1
  35. package/dist/src/seller-catalog.js.map +1 -1
  36. package/dist/src/seller-pool.d.ts +12 -1
  37. package/dist/src/seller-pool.d.ts.map +1 -1
  38. package/dist/src/seller-pool.js +61 -7
  39. package/dist/src/seller-pool.js.map +1 -1
  40. package/dist/src/seller-route-planner.d.ts +11 -1
  41. package/dist/src/seller-route-planner.d.ts.map +1 -1
  42. package/dist/src/seller-route-planner.js +21 -9
  43. package/dist/src/seller-route-planner.js.map +1 -1
  44. package/dist/src/seller-routing-config.d.ts +2 -0
  45. package/dist/src/seller-routing-config.d.ts.map +1 -1
  46. package/dist/src/seller-routing-config.js +11 -1
  47. package/dist/src/seller-routing-config.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/buyer-store.ts +70 -7
  50. package/src/clawtip-bootstrap.ts +64 -0
  51. package/src/cli.ts +201 -76
  52. package/src/daemon.ts +1132 -25
  53. package/src/model-index.ts +4 -1
  54. package/src/prewarm-cache.ts +6 -1
  55. package/src/prewarm-scheduler.ts +6 -2
  56. package/src/route-failover.ts +11 -0
  57. package/src/seller-catalog.ts +24 -1
  58. package/src/seller-pool.ts +69 -7
  59. package/src/seller-route-planner.ts +33 -11
  60. package/src/seller-routing-config.ts +14 -1
  61. package/static/clawtip/recharge.png +0 -0
  62. package/tests/control-plane-ui-endpoints.test.ts +559 -0
  63. package/tests/daemon-classify.test.ts +9 -0
  64. package/tests/model-index.test.ts +14 -0
  65. package/tests/route-failover.test.ts +16 -0
  66. package/tests/seller-catalog-utilities.test.ts +54 -0
  67. package/tests/seller-pool.test.ts +56 -0
  68. package/tests/seller-route-planner.test.ts +40 -0
  69. package/tests/seller-routing-config.test.ts +13 -0
  70. package/tests/tokenbuddy.test.ts +200 -7
package/src/cli.ts CHANGED
@@ -61,6 +61,10 @@ import {
61
61
  startClawtipWalletBootstrap,
62
62
  waitForClawtipActivationConfirmation,
63
63
  } from "./init-clawtip-activation.js";
64
+ import {
65
+ fetchClawtipBootstrap,
66
+ normalizeClawtipBootstrapResourceUrl,
67
+ } from "./clawtip-bootstrap.js";
64
68
  import { displayTerminalImage } from "./terminal-image.js";
65
69
 
66
70
  // @ts-ignore
@@ -90,26 +94,22 @@ interface DaemonRepairResult {
90
94
  error?: string;
91
95
  }
92
96
 
97
+ interface DaemonRestartResult {
98
+ attempted: boolean;
99
+ restarted: boolean;
100
+ method: "launchd";
101
+ plistPath: string;
102
+ target?: string;
103
+ before?: DaemonProbeResult;
104
+ after?: DaemonProbeResult;
105
+ error?: string;
106
+ }
107
+
93
108
  interface CommandFailure extends Error {
94
109
  code?: string;
95
110
  exitCode?: number;
96
111
  }
97
112
 
98
- interface ClawtipBootstrapResponse {
99
- activationFeeFen?: number;
100
- payment?: {
101
- orderNo?: string;
102
- amountFen?: number;
103
- payTo?: string;
104
- encryptedData?: string;
105
- indicator?: string;
106
- slug?: string;
107
- skillId?: string;
108
- description?: string;
109
- resourceUrl?: string;
110
- };
111
- }
112
-
113
113
  interface SelectOption {
114
114
  value: string;
115
115
  label: string;
@@ -196,8 +196,6 @@ interface NormalizedClawtipPaymentPayload {
196
196
  resourceUrl: string;
197
197
  }
198
198
 
199
- const CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO = "bootstrap-pay-to";
200
-
201
199
  async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult> {
202
200
  const deadline = Date.now() + timeoutMs;
203
201
  let latest: DaemonProbeResult = { running: false, error: "not checked" };
@@ -211,6 +209,20 @@ async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Prom
211
209
  return latest;
212
210
  }
213
211
 
212
+ function launchControlUi(controlPort: number): string {
213
+ const url = `http://127.0.0.1:${controlPort}/`;
214
+ const platform = process.platform;
215
+ const args: string[] = platform === "darwin" ? [url] : platform === "win32" ? ["/c", "start", "", url] : [url];
216
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
217
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
218
+ child.on("error", (err) => {
219
+ console.error(`Failed to launch browser: ${err.message}`);
220
+ process.exitCode = 1;
221
+ });
222
+ child.unref();
223
+ return url;
224
+ }
225
+
214
226
  function defaultProxydLogPath(kind: "stdout" | "stderr"): string {
215
227
  const logDir = path.join(os.homedir(), ".tokenbuddy-store");
216
228
  fs.mkdirSync(logDir, { recursive: true });
@@ -302,6 +314,14 @@ function launchdUserDomain(): string {
302
314
  return "gui/501";
303
315
  }
304
316
 
317
+ function launchAgentPlistPath(label: string): string {
318
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${label}.plist`);
319
+ }
320
+
321
+ function launchdServiceTarget(label: string): string {
322
+ return `${launchdUserDomain()}/${label}`;
323
+ }
324
+
305
325
  function runLaunchctl(args: string[], ignoreFailure = false): void {
306
326
  try {
307
327
  execFileSync("launchctl", args, { stdio: "ignore" });
@@ -363,6 +383,72 @@ async function repairDaemon(controlPort: number): Promise<{ repair: DaemonRepair
363
383
  };
364
384
  }
365
385
 
386
+ interface RestartLaunchAgentDeps {
387
+ platform: NodeJS.Platform;
388
+ plistPath: string;
389
+ existsSync(filePath: string): boolean;
390
+ runLaunchctl(args: string[]): void;
391
+ probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult>;
392
+ waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult>;
393
+ }
394
+
395
+ export async function restartLaunchAgent(
396
+ controlPort: number,
397
+ deps: RestartLaunchAgentDeps = {
398
+ platform: process.platform,
399
+ plistPath: launchAgentPlistPath(TOKENBUDDY_LAUNCHD_LABEL),
400
+ existsSync: fs.existsSync,
401
+ runLaunchctl: (args) => runLaunchctl(args),
402
+ probeDaemonStatus,
403
+ waitForDaemonStatus,
404
+ },
405
+ ): Promise<DaemonRestartResult> {
406
+ const before = await deps.probeDaemonStatus(controlPort);
407
+ const baseResult = {
408
+ attempted: false,
409
+ restarted: false,
410
+ method: "launchd" as const,
411
+ plistPath: deps.plistPath,
412
+ before,
413
+ };
414
+
415
+ if (deps.platform !== "darwin") {
416
+ return {
417
+ ...baseResult,
418
+ error: "tb daemon restart is only supported for the macOS LaunchAgent service. Run `tb doctor --fix` to start tb-proxyd in the background."
419
+ };
420
+ }
421
+
422
+ if (!deps.existsSync(deps.plistPath)) {
423
+ return {
424
+ ...baseResult,
425
+ error: "LaunchAgent plist is missing. Run `tb init` to install tb-proxyd as a service first."
426
+ };
427
+ }
428
+
429
+ const target = launchdServiceTarget(TOKENBUDDY_LAUNCHD_LABEL);
430
+ try {
431
+ deps.runLaunchctl(["kickstart", "-k", target]);
432
+ } catch (error: unknown) {
433
+ return {
434
+ ...baseResult,
435
+ attempted: true,
436
+ target,
437
+ error: error instanceof Error ? error.message : String(error)
438
+ };
439
+ }
440
+
441
+ const after = await deps.waitForDaemonStatus(controlPort, 8000);
442
+ return {
443
+ ...baseResult,
444
+ attempted: true,
445
+ restarted: after.running,
446
+ target,
447
+ after,
448
+ error: after.running ? undefined : after.error || "tb-proxyd did not become ready after restart"
449
+ };
450
+ }
451
+
366
452
  function commandPath(command: Command): string {
367
453
  const names: string[] = [];
368
454
  let current: Command | null = command;
@@ -386,7 +472,7 @@ function rootActionName(command: Command): string {
386
472
 
387
473
  function commandRequiresDaemon(command: Command): boolean {
388
474
  const rootName = rootActionName(command);
389
- return rootName !== "doctor" && rootName !== "init" && rootName !== "routing";
475
+ return rootName !== "doctor" && rootName !== "init" && rootName !== "routing" && rootName !== "daemon" && rootName !== "ui";
390
476
  }
391
477
 
392
478
  async function enforceDaemonGate(command: Command): Promise<void> {
@@ -472,64 +558,10 @@ function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
472
558
  console.log(table.toString());
473
559
  }
474
560
 
475
- /**
476
- * 调 wallet-bootstrap 的 `/payments/clawtip/bootstrap` 端点,拿到激活支付参数。
477
- * 校验:HTTP 200、订单字段齐全、`payTo` 不是占位符。
478
- *
479
- * @param bootstrapUrl wallet-bootstrap 服务 base URL
480
- * @returns bootstrap 响应(含 `payment` 字段)
481
- * @throws Error 任何校验失败
482
- */
483
- export async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
484
- const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
485
- method: "POST",
486
- headers: { "Content-Type": "application/json" },
487
- body: JSON.stringify({ clientTag: "tb-payment-add" })
488
- });
489
- const body = await response.json() as ClawtipBootstrapResponse & { error?: string };
490
- if (!response.ok) {
491
- throw new Error(body.error || `ClawTip bootstrap failed with HTTP ${response.status}`);
492
- }
493
- if (!body.payment?.orderNo || !body.payment.indicator || !body.payment.resourceUrl) {
494
- throw new Error("ClawTip bootstrap response missing payment order fields");
495
- }
496
- if ((body.payment.payTo || "").trim() === CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO) {
497
- throw new Error(
498
- [
499
- `ClawTip bootstrap service is misconfigured: payTo is still the placeholder \`${CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO}\`.`,
500
- `Bootstrap URL: ${bootstrapUrl}`,
501
- "Configure the bootstrap service with the real ClawTip merchant pay_to before retrying `tb init`.",
502
- ].join(" ")
503
- );
504
- }
505
- body.payment.resourceUrl = normalizeClawtipBootstrapResourceUrl(bootstrapUrl, body.payment.resourceUrl);
506
- return body;
507
- }
508
-
509
- /**
510
- * 修正 Clawtip bootstrap 返回的 `resourceUrl`。
511
- * 早期 bootstrap 返回 `/registry/sellers` 这种占位 URL,把它替换成当前 bootstrap URL 的 path,
512
- * 让 buyer 正确指向 wallet-bootstrap。
513
- *
514
- * @param bootstrapUrl wallet-bootstrap base URL
515
- * @param resourceUrl bootstrap 响应中的 `resourceUrl` 字段
516
- * @returns 修正后的 resource URL(无法解析时返回原值)
517
- */
518
- export function normalizeClawtipBootstrapResourceUrl(bootstrapUrl: string, resourceUrl: string): string {
519
- try {
520
- const bootstrap = new URL(bootstrapUrl);
521
- const resource = new URL(resourceUrl);
522
- if (resource.origin === bootstrap.origin && resource.pathname === "/registry/sellers") {
523
- resource.pathname = bootstrap.pathname.replace(/\/+$/, "") || "/";
524
- resource.search = "";
525
- resource.hash = "";
526
- return resource.toString().replace(/\/$/, "");
527
- }
528
- } catch {
529
- // Leave the server-provided value unchanged when URL parsing fails.
530
- }
531
- return resourceUrl;
532
- }
561
+ export {
562
+ fetchClawtipBootstrap,
563
+ normalizeClawtipBootstrapResourceUrl,
564
+ } from "./clawtip-bootstrap.js";
533
565
 
534
566
  function readProof(options: { proofFile?: string; requireProof?: boolean }): string | undefined {
535
567
  const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
@@ -1036,6 +1068,79 @@ export function buildCli(): Command {
1036
1068
  }
1037
1069
  });
1038
1070
 
1071
+ // tb ui — 探测 daemon + 用系统默认浏览器打开控制台
1072
+ program
1073
+ .command("ui")
1074
+ .description("Open the local TokenBuddy control console in your default browser")
1075
+ .action(async () => {
1076
+ const controlPort = configuredControlPort();
1077
+ const url = `http://127.0.0.1:${controlPort}/`;
1078
+ const probe = await probeDaemonStatus(controlPort);
1079
+ if (!probe.running) {
1080
+ console.error(`tb-proxyd is not running.`);
1081
+ console.error(`Checked: ${url}status`);
1082
+ console.error(`Run \`tb init\` to complete first-time setup, then \`tb-proxyd\` to start the daemon.`);
1083
+ process.exitCode = 1;
1084
+ const err = new Error("tb-proxyd is not running") as CommandFailure;
1085
+ err.code = "tokenbuddy.daemon_not_running";
1086
+ err.exitCode = 1;
1087
+ throw err;
1088
+ }
1089
+ console.log(`Opening ${launchControlUi(controlPort)} in your default browser…`);
1090
+ });
1091
+
1092
+ const daemon = program
1093
+ .command("daemon")
1094
+ .description("Manage the local tb-proxyd service");
1095
+
1096
+ daemon
1097
+ .command("restart")
1098
+ .description("Restart tb-proxyd through the installed macOS LaunchAgent")
1099
+ .option("--json", "Output restart result as JSON")
1100
+ .action(async (options: { json?: boolean }) => {
1101
+ const controlPort = configuredControlPort();
1102
+ const proxyPort = configuredProxyPort();
1103
+ if (!options.json) {
1104
+ console.log("Restarting tb-proxyd LaunchAgent...");
1105
+ }
1106
+ const result = await restartLaunchAgent(controlPort);
1107
+ if (!result.restarted) {
1108
+ process.exitCode = 1;
1109
+ }
1110
+
1111
+ if (options.json) {
1112
+ const daemonProbe = result.after || result.before;
1113
+ console.log(JSON.stringify({
1114
+ restart: result,
1115
+ daemon: {
1116
+ running: Boolean(daemonProbe?.running),
1117
+ controlPort,
1118
+ proxyPort,
1119
+ controlUrl: `http://127.0.0.1:${controlPort}`,
1120
+ proxyUrl: `http://127.0.0.1:${proxyPort}`,
1121
+ status: daemonProbe?.status,
1122
+ error: daemonProbe?.error || result.error
1123
+ }
1124
+ }, null, 2));
1125
+ return;
1126
+ }
1127
+
1128
+ if (!result.restarted) {
1129
+ console.error(`Failed to restart tb-proxyd: ${result.error || "unknown error"}`);
1130
+ if (!result.after?.running) {
1131
+ console.error(`Checked: http://127.0.0.1:${controlPort}/status`);
1132
+ }
1133
+ return;
1134
+ }
1135
+
1136
+ const status = result.after?.status && typeof result.after.status === "object"
1137
+ ? result.after.status as { pid?: number; controlPort?: number; proxyPort?: number }
1138
+ : undefined;
1139
+ console.log(`✅ tb-proxyd restarted${status?.pid ? ` (PID: ${status.pid})` : ""}.`);
1140
+ console.log(` Control Plane URL: http://127.0.0.1:${status?.controlPort || controlPort}`);
1141
+ console.log(` Proxy Plane URL: http://127.0.0.1:${status?.proxyPort || proxyPort}`);
1142
+ });
1143
+
1039
1144
  // v1.2 §18.11 helpers for `tb doctor` / `tb doctor --json`.
1040
1145
  async function fetchV12Snapshot(controlUrl: string): Promise<unknown | null> {
1041
1146
  try {
@@ -1679,6 +1784,26 @@ export function buildCli(): Command {
1679
1784
  installLaunchAgent(plistPath, TOKENBUDDY_LAUNCHD_LABEL);
1680
1785
  spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
1681
1786
  setupSummaryLines.push("Background tb-proxyd launchd service installed.");
1787
+
1788
+ spinner.start("Checking tb-proxyd control plane...");
1789
+ const daemonProbe = await waitForDaemonStatus(controlPort, 8000);
1790
+ spinner.stop(daemonProbe.running ? "tb-proxyd control plane is ready." : "tb-proxyd control plane is still starting.");
1791
+
1792
+ if (daemonProbe.running) {
1793
+ const nextStep = await p.select({
1794
+ message: "tb-proxyd is running. What would you like to do next?",
1795
+ options: [
1796
+ { value: "continue", label: "Continue in terminal", hint: "Finish the CLI setup summary" },
1797
+ { value: "ui", label: "Open tb ui", hint: "Launch the local graphical console" }
1798
+ ]
1799
+ }) as string;
1800
+ if (nextStep === "ui") {
1801
+ const uiUrl = launchControlUi(controlPort);
1802
+ setupSummaryLines.push(`Opened TokenBuddy UI at ${uiUrl}.`);
1803
+ p.outro(buildInitSuccessMessage(setupSummaryLines));
1804
+ return;
1805
+ }
1806
+ }
1682
1807
  } catch (err: any) {
1683
1808
  spinner.stop(`Failed to write launchd plist: ${err.message}`);
1684
1809
  }