@tokenbuddy/tokenbuddy 1.0.26 → 1.0.27

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 (84) hide show
  1. package/bin/tb-clawtip-proof.js +2 -0
  2. package/dist/src/clawtip-bootstrap.d.ts +1 -0
  3. package/dist/src/clawtip-bootstrap.d.ts.map +1 -1
  4. package/dist/src/clawtip-bootstrap.js +1 -0
  5. package/dist/src/clawtip-bootstrap.js.map +1 -1
  6. package/dist/src/cli.d.ts +1 -0
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +172 -51
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/daemon.d.ts +6 -0
  11. package/dist/src/daemon.d.ts.map +1 -1
  12. package/dist/src/daemon.js +562 -292
  13. package/dist/src/daemon.js.map +1 -1
  14. package/dist/src/init-clawtip-activation.d.ts +5 -0
  15. package/dist/src/init-clawtip-activation.d.ts.map +1 -1
  16. package/dist/src/init-clawtip-activation.js +61 -1
  17. package/dist/src/init-clawtip-activation.js.map +1 -1
  18. package/dist/src/package-update.d.ts +60 -0
  19. package/dist/src/package-update.d.ts.map +1 -0
  20. package/dist/src/package-update.js +220 -0
  21. package/dist/src/package-update.js.map +1 -0
  22. package/dist/src/registry-trust.d.ts +7 -0
  23. package/dist/src/registry-trust.d.ts.map +1 -0
  24. package/dist/src/registry-trust.js +37 -0
  25. package/dist/src/registry-trust.js.map +1 -0
  26. package/dist/src/route-failover.d.ts +2 -2
  27. package/dist/src/route-failover.d.ts.map +1 -1
  28. package/dist/src/route-failover.js +11 -0
  29. package/dist/src/route-failover.js.map +1 -1
  30. package/dist/src/seller-catalog.d.ts +20 -0
  31. package/dist/src/seller-catalog.d.ts.map +1 -1
  32. package/dist/src/seller-catalog.js +41 -4
  33. package/dist/src/seller-catalog.js.map +1 -1
  34. package/dist/src/seller-concurrency-limiter.d.ts +36 -0
  35. package/dist/src/seller-concurrency-limiter.d.ts.map +1 -0
  36. package/dist/src/seller-concurrency-limiter.js +126 -0
  37. package/dist/src/seller-concurrency-limiter.js.map +1 -0
  38. package/dist/src/seller-pool.d.ts +7 -1
  39. package/dist/src/seller-pool.d.ts.map +1 -1
  40. package/dist/src/seller-pool.js +18 -0
  41. package/dist/src/seller-pool.js.map +1 -1
  42. package/dist/src/seller-route-planner.d.ts +21 -0
  43. package/dist/src/seller-route-planner.d.ts.map +1 -1
  44. package/dist/src/seller-route-planner.js +98 -20
  45. package/dist/src/seller-route-planner.js.map +1 -1
  46. package/dist/src/tb-clawtip-proof.d.ts +3 -0
  47. package/dist/src/tb-clawtip-proof.d.ts.map +1 -0
  48. package/dist/src/tb-clawtip-proof.js +24 -0
  49. package/dist/src/tb-clawtip-proof.js.map +1 -0
  50. package/dist/src/tb-proxyd.js +45 -3
  51. package/dist/src/tb-proxyd.js.map +1 -1
  52. package/package.json +3 -2
  53. package/src/clawtip-bootstrap.ts +1 -0
  54. package/src/cli.ts +200 -47
  55. package/src/daemon.ts +347 -50
  56. package/src/init-clawtip-activation.ts +77 -1
  57. package/src/package-update.ts +313 -0
  58. package/src/registry-trust.ts +51 -0
  59. package/src/route-failover.ts +14 -2
  60. package/src/seller-catalog.ts +67 -4
  61. package/src/seller-concurrency-limiter.ts +161 -0
  62. package/src/seller-pool.ts +20 -0
  63. package/src/seller-route-planner.ts +142 -20
  64. package/src/tb-clawtip-proof.ts +28 -0
  65. package/src/tb-proxyd.ts +48 -3
  66. package/static/ui/assets/index-Bzbrp7Qe.css +1 -0
  67. package/static/ui/assets/index-UAfOhbwC.js +236 -0
  68. package/static/ui/assets/index-UAfOhbwC.js.map +1 -0
  69. package/static/ui/index.html +2 -2
  70. package/tests/cli-routing.test.ts +37 -4
  71. package/tests/control-plane-ui-endpoints.test.ts +7 -7
  72. package/tests/daemon-trusted-registry-cache.test.ts +132 -0
  73. package/tests/e2e.test.ts +14 -1
  74. package/tests/package-update.test.ts +132 -0
  75. package/tests/registry-trust.test.ts +28 -0
  76. package/tests/route-failover.test.ts +13 -0
  77. package/tests/seller-catalog-413.test.ts +60 -1
  78. package/tests/seller-concurrency-limiter.test.ts +83 -0
  79. package/tests/seller-pool.test.ts +23 -0
  80. package/tests/seller-route-planner.test.ts +78 -0
  81. package/tests/tokenbuddy.test.ts +316 -34
  82. package/static/ui/assets/index-1uuyCCzj.css +0 -1
  83. package/static/ui/assets/index-cm_EgQZ-.js +0 -236
  84. package/static/ui/assets/index-cm_EgQZ-.js.map +0 -1
package/src/cli.ts CHANGED
@@ -62,10 +62,20 @@ import {
62
62
  waitForClawtipActivationConfirmation,
63
63
  } from "./init-clawtip-activation.js";
64
64
  import {
65
+ DEFAULT_CLAWTIP_BOOTSTRAP_URL,
65
66
  fetchClawtipBootstrap,
66
67
  normalizeClawtipBootstrapResourceUrl,
67
68
  } from "./clawtip-bootstrap.js";
68
69
  import { displayTerminalImage } from "./terminal-image.js";
70
+ import {
71
+ checkPackageUpdate,
72
+ readInstalledPackageManifest,
73
+ runPackageUpdate,
74
+ type PackageRestartResult,
75
+ type PackageUpdateCheck,
76
+ type PackageUpdateResult,
77
+ } from "./package-update.js";
78
+ import { DEFAULT_SELLER_REGISTRY_URL } from "./registry-trust.js";
69
79
 
70
80
  // @ts-ignore
71
81
  import qrcode from "qrcode-terminal";
@@ -80,34 +90,7 @@ const STALE_TOKENBUDDY_LAUNCHD_LABELS = [
80
90
  const logger = createModuleLogger("tokenbuddy-cli");
81
91
 
82
92
  function readCliPackageVersion(): string {
83
- const candidateRoots = [
84
- process.argv[1],
85
- path.join(process.cwd(), "packages", "tokenbuddy-cli"),
86
- process.cwd(),
87
- ];
88
- const seen = new Set<string>();
89
- for (const candidateRoot of candidateRoots) {
90
- if (!candidateRoot) continue;
91
- let current = fs.existsSync(candidateRoot) ? fs.realpathSync(candidateRoot) : candidateRoot;
92
- if (!fs.existsSync(current)) continue;
93
- if (!fs.statSync(current).isDirectory()) {
94
- current = path.dirname(current);
95
- }
96
- while (!seen.has(current)) {
97
- seen.add(current);
98
- const packageJsonPath = path.join(current, "package.json");
99
- if (fs.existsSync(packageJsonPath)) {
100
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { name?: unknown; version?: unknown };
101
- if (packageJson.name === "@tokenbuddy/tokenbuddy" && typeof packageJson.version === "string" && packageJson.version.length > 0) {
102
- return packageJson.version;
103
- }
104
- }
105
- const parent = path.dirname(current);
106
- if (parent === current) break;
107
- current = parent;
108
- }
109
- }
110
- return "0.0.0";
93
+ return readInstalledPackageManifest().version;
111
94
  }
112
95
  const SUPPORTED_PAYMENT_METHODS = ["mock", "clawtip"] as const;
113
96
  type SupportedPaymentMethod = typeof SUPPORTED_PAYMENT_METHODS[number];
@@ -265,6 +248,14 @@ function tbProxydScriptPath(): string {
265
248
  return path.resolve(currentModuleDir(), "./tb-proxyd.js");
266
249
  }
267
250
 
251
+ function tbClawtipProofScriptPath(): string {
252
+ return path.resolve(currentModuleDir(), "../../bin/tb-clawtip-proof.js");
253
+ }
254
+
255
+ function defaultClawtipProofCommand(): string {
256
+ return process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND || tbClawtipProofScriptPath();
257
+ }
258
+
268
259
  /**
269
260
  * `buildLaunchdPlistContent` 的输入。
270
261
  * 用于生成 macOS launchd plist,把 `tb-proxyd` 注册成 LaunchAgent。
@@ -376,27 +367,59 @@ function runLaunchctl(args: string[], ignoreFailure = false): void {
376
367
  }
377
368
  }
378
369
 
379
- function launchAgentLoaded(label: string): boolean {
370
+ function sleepSync(ms: number): void {
371
+ const shared = new Int32Array(new SharedArrayBuffer(4));
372
+ Atomics.wait(shared, 0, 0, ms);
373
+ }
374
+
375
+ function runLaunchctlWithRetry(
376
+ args: string[],
377
+ run: (args: string[], ignoreFailure?: boolean) => void,
378
+ attempts = 3
379
+ ): void {
380
+ let latestError: unknown;
381
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
382
+ try {
383
+ run(args);
384
+ return;
385
+ } catch (error: unknown) {
386
+ latestError = error;
387
+ if (attempt < attempts - 1) {
388
+ sleepSync(250 * (attempt + 1));
389
+ }
390
+ }
391
+ }
392
+ throw latestError;
393
+ }
394
+
395
+ function launchAgentLoaded(label: string, run: (args: string[], ignoreFailure?: boolean) => void = runLaunchctl): boolean {
380
396
  try {
381
- runLaunchctl(["print", launchdServiceTarget(label)]);
397
+ run(["print", launchdServiceTarget(label)]);
382
398
  return true;
383
399
  } catch {
384
400
  return false;
385
401
  }
386
402
  }
387
403
 
388
- function installLaunchAgent(plistPath: string, label: string): void {
404
+ export function installLaunchAgentWithRunner(
405
+ plistPath: string,
406
+ label: string,
407
+ run: (args: string[], ignoreFailure?: boolean) => void
408
+ ): void {
389
409
  const domain = launchdUserDomain();
390
410
  for (const staleLabel of STALE_TOKENBUDDY_LAUNCHD_LABELS) {
391
- runLaunchctl(["bootout", `${domain}/${staleLabel}`], true);
411
+ run(["bootout", `${domain}/${staleLabel}`], true);
392
412
  }
393
413
  const target = launchdServiceTarget(label);
394
- if (launchAgentLoaded(label)) {
395
- runLaunchctl(["kickstart", target]);
396
- return;
414
+ if (launchAgentLoaded(label, run)) {
415
+ run(["bootout", target], true);
397
416
  }
398
- runLaunchctl(["bootstrap", domain, plistPath]);
399
- runLaunchctl(["kickstart", "-k", target]);
417
+ runLaunchctlWithRetry(["bootstrap", domain, plistPath], run);
418
+ run(["kickstart", "-k", target]);
419
+ }
420
+
421
+ function installLaunchAgent(plistPath: string, label: string): void {
422
+ installLaunchAgentWithRunner(plistPath, label, runLaunchctl);
400
423
  }
401
424
 
402
425
  async function repairDaemon(controlPort: number): Promise<{ repair: DaemonRepairResult; probe: DaemonProbeResult }> {
@@ -604,7 +627,7 @@ export async function runWebInitLauncher(deps: WebInitLauncherDeps = {}): Promis
604
627
  controlPort,
605
628
  proxyPort,
606
629
  sellerRegistryUrl,
607
- clawtipProofCommand: deps.clawtipProofCommand ?? process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND,
630
+ clawtipProofCommand: deps.clawtipProofCommand ?? defaultClawtipProofCommand(),
608
631
  clawtipProofTimeoutMs: deps.clawtipProofTimeoutMs ?? (
609
632
  process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS
610
633
  ? Number(process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS)
@@ -710,7 +733,7 @@ function rootActionName(command: Command): string {
710
733
 
711
734
  function commandRequiresDaemon(command: Command): boolean {
712
735
  const rootName = rootActionName(command);
713
- return rootName !== "doctor" && rootName !== "init" && rootName !== "routing" && rootName !== "daemon" && rootName !== "ui";
736
+ return rootName !== "doctor" && rootName !== "init" && rootName !== "routing" && rootName !== "daemon" && rootName !== "ui" && rootName !== "update";
714
737
  }
715
738
 
716
739
  async function enforceDaemonGate(command: Command): Promise<void> {
@@ -796,6 +819,91 @@ function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
796
819
  console.log(table.toString());
797
820
  }
798
821
 
822
+ async function safeCheckPackageUpdate(): Promise<{ check?: PackageUpdateCheck; error?: string }> {
823
+ if (process.env.NODE_ENV === "test" && process.env.TOKENBUDDY_TEST_UPDATE_CHECK !== "1") {
824
+ return { error: "version check skipped in tests" };
825
+ }
826
+ try {
827
+ return { check: await checkPackageUpdate() };
828
+ } catch (error: unknown) {
829
+ return { error: error instanceof Error ? error.message : String(error) };
830
+ }
831
+ }
832
+
833
+ async function runDoctorPackageUpdate(
834
+ fix: boolean,
835
+ controlPort: number,
836
+ ): Promise<{ check?: PackageUpdateCheck; error?: string } | PackageUpdateResult> {
837
+ if (process.env.NODE_ENV === "test" && process.env.TOKENBUDDY_TEST_UPDATE_CHECK !== "1") {
838
+ return { error: "version check skipped in tests" };
839
+ }
840
+ if (!fix) {
841
+ return safeCheckPackageUpdate();
842
+ }
843
+ return await runPackageUpdate({ apply: true, controlPort }, { restartProxyd: restartProxydForPackageUpdate });
844
+ }
845
+
846
+ function printPackageUpdateCheck(result: { check?: PackageUpdateCheck; error?: string }): void {
847
+ if (result.error) {
848
+ console.log(`⚠️ Version check unavailable: ${result.error}`);
849
+ return;
850
+ }
851
+ const check = result.check!;
852
+ if (check.updateAvailable) {
853
+ console.log(`⬆️ TokenBuddy update available: ${check.currentVersion} -> ${check.latestVersion}`);
854
+ console.log(` Run \`tb update\` to install and restart tb-proxyd.`);
855
+ } else {
856
+ console.log(`✅ TokenBuddy package is current (${check.currentVersion}).`);
857
+ }
858
+ }
859
+
860
+ function packageUpdateExitCode(result: PackageUpdateResult): number {
861
+ if (!result.check.updateAvailable) {
862
+ return 0;
863
+ }
864
+ if (!result.install.succeeded || !result.restart.restarted) {
865
+ return 1;
866
+ }
867
+ return 0;
868
+ }
869
+
870
+ function printPackageUpdateResult(result: PackageUpdateResult): void {
871
+ const { check, install, restart } = result;
872
+ if (!check.updateAvailable) {
873
+ console.log(`TokenBuddy is already current (${check.currentVersion}).`);
874
+ return;
875
+ }
876
+
877
+ console.log(`TokenBuddy update available: ${check.currentVersion} -> ${check.latestVersion}`);
878
+ if (!install.attempted) {
879
+ console.log(`Run \`${check.installCommand}\` to install it.`);
880
+ return;
881
+ }
882
+ if (!install.succeeded) {
883
+ console.error(`Failed to install ${check.packageName}@${check.latestVersion}: ${install.error || "unknown error"}`);
884
+ return;
885
+ }
886
+
887
+ console.log(`Installed ${check.packageName}@${check.latestVersion}.`);
888
+ if (!restart.restarted) {
889
+ console.error(`Failed to restart tb-proxyd: ${restart.error || "unknown error"}`);
890
+ return;
891
+ }
892
+ console.log("tb-proxyd restarted.");
893
+ }
894
+
895
+ async function restartProxydForPackageUpdate(controlPort: number): Promise<PackageRestartResult> {
896
+ const result = await restartLaunchAgent(controlPort);
897
+ return {
898
+ attempted: result.attempted,
899
+ restarted: result.restarted,
900
+ method: result.method,
901
+ plistPath: result.plistPath,
902
+ target: result.target,
903
+ error: result.error,
904
+ };
905
+ }
906
+
799
907
  export {
800
908
  fetchClawtipBootstrap,
801
909
  normalizeClawtipBootstrapResourceUrl,
@@ -820,7 +928,7 @@ function readProof(options: { proofFile?: string; requireProof?: boolean }): str
820
928
  }
821
929
 
822
930
  function sellerRegistryUrlForInit(): string {
823
- return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
931
+ return process.env.TB_PROXYD_SELLER_REGISTRY_URL || DEFAULT_SELLER_REGISTRY_URL;
824
932
  }
825
933
 
826
934
  function stableModelChoices(models: ModelCatalogEntry[]): SelectOption[] {
@@ -1176,12 +1284,45 @@ export function buildCli(): Command {
1176
1284
  await enforceDaemonGate(actionCommand);
1177
1285
  });
1178
1286
 
1287
+ program
1288
+ .command("update")
1289
+ .description("Check for a TokenBuddy package update, install it, and restart tb-proxyd")
1290
+ .option("--check", "Only check for an available update")
1291
+ .option("--json", "Output update state as JSON")
1292
+ .action(async (options: { check?: boolean; json?: boolean }) => {
1293
+ const controlPort = configuredControlPort();
1294
+ try {
1295
+ const result = await runPackageUpdate(
1296
+ { apply: !options.check, controlPort },
1297
+ { restartProxyd: restartProxydForPackageUpdate },
1298
+ );
1299
+ if (options.json) {
1300
+ console.log(JSON.stringify(result, null, 2));
1301
+ } else if (options.check) {
1302
+ printPackageUpdateCheck({ check: result.check });
1303
+ } else {
1304
+ printPackageUpdateResult(result);
1305
+ }
1306
+ if (!options.check) {
1307
+ process.exitCode = packageUpdateExitCode(result);
1308
+ }
1309
+ } catch (error: unknown) {
1310
+ const errorMessage = error instanceof Error ? error.message : String(error);
1311
+ process.exitCode = 1;
1312
+ if (options.json) {
1313
+ console.log(JSON.stringify({ error: { code: "package_update_failed", message: errorMessage } }, null, 2));
1314
+ } else {
1315
+ console.error(`TokenBuddy update failed: ${errorMessage}`);
1316
+ }
1317
+ }
1318
+ });
1319
+
1179
1320
  // 1. tb doctor
1180
1321
  program
1181
1322
  .command("doctor")
1182
1323
  .description("Check running status, system agents, and network diagnostics")
1183
1324
  .option("--json", "Output diagnostics as JSON")
1184
- .option("--fix", "Start tb-proxyd in the background when it is not running")
1325
+ .option("--fix", "Start tb-proxyd when needed, install available package updates, and restart the service")
1185
1326
  .action(async (options: { json?: boolean; fix?: boolean }) => {
1186
1327
  const controlPort = configuredControlPort();
1187
1328
  const proxyPort = configuredProxyPort();
@@ -1203,9 +1344,18 @@ export function buildCli(): Command {
1203
1344
  ? daemonInfo as { selectionMode?: string; sellerRoutingMode?: string; selectedSellerId?: string; sellerRegistryUrl?: string }
1204
1345
  : undefined;
1205
1346
  const providers = readDoctorProviders();
1347
+ const updateState = await runDoctorPackageUpdate(Boolean(options.fix), controlPort).catch((error: unknown) => ({
1348
+ error: error instanceof Error ? error.message : String(error)
1349
+ }));
1206
1350
  if (options.fix && repair.attempted && !repair.fixed) {
1207
1351
  process.exitCode = 1;
1208
1352
  }
1353
+ if (options.fix && "check" in updateState && updateState.check?.updateAvailable) {
1354
+ process.exitCode = packageUpdateExitCode(updateState as PackageUpdateResult);
1355
+ }
1356
+ if (options.fix && "error" in updateState && updateState.error) {
1357
+ process.exitCode = 1;
1358
+ }
1209
1359
 
1210
1360
  if (options.json) {
1211
1361
  const diagnostics = await collectDoctorDiagnostics({
@@ -1233,6 +1383,7 @@ export function buildCli(): Command {
1233
1383
  requested: Boolean(options.fix),
1234
1384
  ...repair
1235
1385
  },
1386
+ packageUpdate: updateState,
1236
1387
  service: {
1237
1388
  platform: process.platform,
1238
1389
  plistPath,
@@ -1245,6 +1396,11 @@ export function buildCli(): Command {
1245
1396
  }
1246
1397
 
1247
1398
  console.log("=== TokenBuddy System Diagnostics ===");
1399
+ printPackageUpdateCheck("check" in updateState ? { check: updateState.check } : updateState);
1400
+ if (options.fix && "install" in updateState) {
1401
+ printPackageUpdateResult(updateState);
1402
+ }
1403
+ console.log("");
1248
1404
 
1249
1405
  // 1. Detect if daemon is listening
1250
1406
  if (daemonRunning) {
@@ -1475,10 +1631,7 @@ export function buildCli(): Command {
1475
1631
  return;
1476
1632
  }
1477
1633
 
1478
- const bootstrapUrl = options.bootstrapUrl || process.env.TOKENBUDDY_BOOTSTRAP_URL;
1479
- if (!bootstrapUrl) {
1480
- throw new Error("ClawTip bootstrap URL is required; pass --bootstrap-url or TOKENBUDDY_BOOTSTRAP_URL");
1481
- }
1634
+ const bootstrapUrl = options.bootstrapUrl || process.env.TOKENBUDDY_BOOTSTRAP_URL || DEFAULT_CLAWTIP_BOOTSTRAP_URL;
1482
1635
  const proof = readProof({
1483
1636
  proofFile: options.proofFile,
1484
1637
  requireProof: options.requireProof
@@ -1815,7 +1968,7 @@ export function buildCli(): Command {
1815
1968
  }
1816
1969
 
1817
1970
  spinner.start("Requesting payment activation payload from public bootstrap registry...");
1818
- const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
1971
+ const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || DEFAULT_CLAWTIP_BOOTSTRAP_URL;
1819
1972
  const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
1820
1973
  spinner.stop("Bootstrap payload received.");
1821
1974
 
@@ -2043,7 +2196,7 @@ export function buildCli(): Command {
2043
2196
  proxyPort,
2044
2197
  sellerRegistryUrl,
2045
2198
  pathEnv: process.env.PATH,
2046
- clawtipProofCommand: process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND,
2199
+ clawtipProofCommand: defaultClawtipProofCommand(),
2047
2200
  clawtipProofTimeoutMs: process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS
2048
2201
  ? Number(process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS)
2049
2202
  : undefined,