@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.
- package/dist/src/buyer-store.d.ts +23 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +31 -6
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/clawtip-bootstrap.d.ts +23 -0
- package/dist/src/clawtip-bootstrap.d.ts.map +1 -0
- package/dist/src/clawtip-bootstrap.js +47 -0
- package/dist/src/clawtip-bootstrap.js.map +1 -0
- package/dist/src/cli.d.ts +24 -33
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +157 -58
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +79 -1
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +984 -23
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/model-index.d.ts +1 -1
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js +4 -0
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +4 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +2 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +2 -0
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +4 -2
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js +10 -0
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +17 -0
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +15 -1
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-pool.d.ts +12 -1
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +61 -7
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +11 -1
- package/dist/src/seller-route-planner.d.ts.map +1 -1
- package/dist/src/seller-route-planner.js +21 -9
- package/dist/src/seller-route-planner.js.map +1 -1
- package/dist/src/seller-routing-config.d.ts +2 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -1
- package/dist/src/seller-routing-config.js +11 -1
- package/dist/src/seller-routing-config.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +70 -7
- package/src/clawtip-bootstrap.ts +64 -0
- package/src/cli.ts +201 -76
- package/src/daemon.ts +1132 -25
- package/src/model-index.ts +4 -1
- package/src/prewarm-cache.ts +6 -1
- package/src/prewarm-scheduler.ts +6 -2
- package/src/route-failover.ts +11 -0
- package/src/seller-catalog.ts +24 -1
- package/src/seller-pool.ts +69 -7
- package/src/seller-route-planner.ts +33 -11
- package/src/seller-routing-config.ts +14 -1
- package/static/clawtip/recharge.png +0 -0
- package/tests/control-plane-ui-endpoints.test.ts +559 -0
- package/tests/daemon-classify.test.ts +9 -0
- package/tests/model-index.test.ts +14 -0
- package/tests/route-failover.test.ts +16 -0
- package/tests/seller-catalog-utilities.test.ts +54 -0
- package/tests/seller-pool.test.ts +56 -0
- package/tests/seller-route-planner.test.ts +40 -0
- package/tests/seller-routing-config.test.ts +13 -0
- 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
|
-
|
|
477
|
-
|
|
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
|
}
|