@vellumai/cli 0.4.57 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.57",
3
+ "version": "0.5.1",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -5,6 +5,11 @@ import { resolve } from "node:path";
5
5
  import { expect, test } from "bun:test";
6
6
 
7
7
  const EXCLUDE_PATTERNS = [".test.ts", ".d.ts"];
8
+ const EXCLUDE_DIRS = [
9
+ // Ink components import yoga-layout whose WASM binary crashes
10
+ // intermittently during headless import (null reference in za()).
11
+ "components/",
12
+ ];
8
13
  const EXCLUDE_FILES = [
9
14
  // index.ts calls main() at module level, causing side effects on import
10
15
  "index.ts",
@@ -15,6 +20,7 @@ async function importAllModules(dir: string): Promise<string[]> {
15
20
  const files = [...glob.scanSync(dir)].filter(
16
21
  (f) =>
17
22
  !EXCLUDE_PATTERNS.some((pattern) => f.endsWith(pattern)) &&
23
+ !EXCLUDE_DIRS.some((dir) => f.startsWith(dir)) &&
18
24
  !EXCLUDE_FILES.some((excluded) => f === excluded) &&
19
25
  !f.includes("__tests__"),
20
26
  );
@@ -701,32 +701,36 @@ async function hatchLocal(
701
701
 
702
702
  await startLocalDaemon(watch, resources);
703
703
 
704
- let runtimeUrl: string;
705
- try {
706
- runtimeUrl = await startGateway(watch, resources);
707
- } catch (error) {
708
- // Gateway failed — stop the daemon we just started so we don't leave
709
- // orphaned processes with no lock file entry.
710
- console.error(
711
- `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
712
- );
713
- await stopLocalProcesses(resources);
714
- throw error;
715
- }
704
+ // When daemonOnly is set, skip gateway and ngrok — the caller only wants
705
+ // the daemon restarted (e.g. macOS app bootstrap retry).
706
+ let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
707
+ if (!daemonOnly) {
708
+ try {
709
+ runtimeUrl = await startGateway(watch, resources);
710
+ } catch (error) {
711
+ // Gateway failed — stop the daemon we just started so we don't leave
712
+ // orphaned processes with no lock file entry.
713
+ console.error(
714
+ `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
715
+ );
716
+ await stopLocalProcesses(resources);
717
+ throw error;
718
+ }
716
719
 
717
- // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
718
- // Set BASE_DATA_DIR so ngrok reads the correct instance config.
719
- const prevBaseDataDir = process.env.BASE_DATA_DIR;
720
- process.env.BASE_DATA_DIR = resources.instanceDir;
721
- const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
722
- if (ngrokChild?.pid) {
723
- const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
724
- writeFileSync(ngrokPidFile, String(ngrokChild.pid));
725
- }
726
- if (prevBaseDataDir !== undefined) {
727
- process.env.BASE_DATA_DIR = prevBaseDataDir;
728
- } else {
729
- delete process.env.BASE_DATA_DIR;
720
+ // Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
721
+ // Set BASE_DATA_DIR so ngrok reads the correct instance config.
722
+ const prevBaseDataDir = process.env.BASE_DATA_DIR;
723
+ process.env.BASE_DATA_DIR = resources.instanceDir;
724
+ const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
725
+ if (ngrokChild?.pid) {
726
+ const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
727
+ writeFileSync(ngrokPidFile, String(ngrokChild.pid));
728
+ }
729
+ if (prevBaseDataDir !== undefined) {
730
+ process.env.BASE_DATA_DIR = prevBaseDataDir;
731
+ } else {
732
+ delete process.env.BASE_DATA_DIR;
733
+ }
730
734
  }
731
735
 
732
736
  const localEntry: AssistantEntry = {
@@ -757,7 +761,12 @@ async function hatchLocal(
757
761
  }
758
762
 
759
763
  if (keepAlive) {
760
- const gatewayHealthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
764
+ // When --daemon-only is set, no gateway is running — poll the daemon
765
+ // health endpoint instead of the gateway to avoid self-termination.
766
+ const healthUrl = daemonOnly
767
+ ? `http://127.0.0.1:${resources.daemonPort}/healthz`
768
+ : `http://127.0.0.1:${resources.gatewayPort}/healthz`;
769
+ const healthTarget = daemonOnly ? "Assistant" : "Gateway";
761
770
  const POLL_INTERVAL_MS = 5000;
762
771
  const MAX_FAILURES = 3;
763
772
  let consecutiveFailures = 0;
@@ -771,11 +780,11 @@ async function hatchLocal(
771
780
  process.on("SIGTERM", () => void shutdown());
772
781
  process.on("SIGINT", () => void shutdown());
773
782
 
774
- // Poll the gateway health endpoint until it stops responding.
783
+ // Poll the health endpoint until it stops responding.
775
784
  while (true) {
776
785
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
777
786
  try {
778
- const res = await fetch(gatewayHealthUrl, {
787
+ const res = await fetch(healthUrl, {
779
788
  signal: AbortSignal.timeout(3000),
780
789
  });
781
790
  if (res.ok) {
@@ -787,7 +796,9 @@ async function hatchLocal(
787
796
  consecutiveFailures++;
788
797
  }
789
798
  if (consecutiveFailures >= MAX_FAILURES) {
790
- console.log("\n⚠️ Gateway stopped responding — shutting down.");
799
+ console.log(
800
+ `\n⚠️ ${healthTarget} stopped responding — shutting down.`,
801
+ );
791
802
  await stopLocalProcesses(resources);
792
803
  process.exit(1);
793
804
  }
package/src/lib/docker.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { existsSync, watch as fsWatch } from "fs";
1
+ import { chmodSync, existsSync, mkdirSync, watch as fsWatch } from "fs";
2
+ import { arch, platform } from "os";
2
3
  import { dirname, join } from "path";
3
4
 
4
5
  // Direct import — bun embeds this at compile time so it works in compiled binaries.
@@ -39,60 +40,201 @@ export const GATEWAY_INTERNAL_PORT = 7830;
39
40
  /** Max time to wait for the assistant container to emit the readiness sentinel. */
40
41
  export const DOCKER_READY_TIMEOUT_MS = 3 * 60 * 1000;
41
42
 
43
+ /** Directory for user-local binary installs (no sudo required). */
44
+ const LOCAL_BIN_DIR = join(
45
+ process.env.HOME || process.env.USERPROFILE || ".",
46
+ ".local",
47
+ "bin",
48
+ );
49
+
42
50
  /**
43
- * Checks whether the `docker` CLI and daemon are available on the system.
44
- * Installs Colima and Docker via Homebrew if the CLI is missing, and starts
45
- * Colima if the Docker daemon is not reachable.
51
+ * Returns the macOS architecture suffix used by GitHub release artifacts.
52
+ * Maps Node's `arch()` values to the names used in release URLs.
46
53
  */
47
- async function ensureDockerInstalled(): Promise<void> {
48
- let installed = false;
54
+ function releaseArch(): string {
55
+ const a = arch();
56
+ if (a === "arm64") return "aarch64";
57
+ if (a === "x64") return "x86_64";
58
+ return a;
59
+ }
60
+
61
+ /**
62
+ * Downloads a file from `url` to `destPath`, makes it executable, and returns
63
+ * the destination path. Throws on failure.
64
+ */
65
+ async function downloadBinary(
66
+ url: string,
67
+ destPath: string,
68
+ label: string,
69
+ ): Promise<void> {
70
+ console.log(` ⬇ Downloading ${label}...`);
71
+ await exec("bash", [
72
+ "-c",
73
+ `curl -fsSL -o "${destPath}" "${url}" && chmod +x "${destPath}"`,
74
+ ]);
75
+ }
76
+
77
+ /**
78
+ * Downloads and extracts a `.tar.gz` archive into `destDir`.
79
+ */
80
+ async function downloadAndExtract(
81
+ url: string,
82
+ destDir: string,
83
+ label: string,
84
+ ): Promise<void> {
85
+ console.log(` ⬇ Downloading ${label}...`);
86
+ await exec("bash", ["-c", `curl -fsSL "${url}" | tar xz -C "${destDir}"`]);
87
+ }
88
+
89
+ /**
90
+ * Installs Docker CLI, Colima, and Lima by downloading pre-built binaries
91
+ * directly into ~/.vellum/bin/. No Homebrew or sudo required.
92
+ *
93
+ * Falls back to Homebrew if available (e.g. admin users who prefer it).
94
+ */
95
+ async function installDockerToolchain(): Promise<void> {
96
+ // Try Homebrew first if available — it handles updates and dependencies.
97
+ let hasBrew = false;
49
98
  try {
50
- await execOutput("docker", ["--version"]);
51
- installed = true;
99
+ await execOutput("brew", ["--version"]);
100
+ hasBrew = true;
52
101
  } catch {
53
- // docker CLI not found — install it
102
+ // brew not found
54
103
  }
55
104
 
56
- if (!installed) {
57
- // Check whether Homebrew is available before attempting to use it.
58
- let hasBrew = false;
105
+ if (hasBrew) {
106
+ console.log("🐳 Docker not found. Installing via Homebrew...");
59
107
  try {
60
- await execOutput("brew", ["--version"]);
61
- hasBrew = true;
108
+ await exec("brew", ["install", "colima", "docker"]);
109
+ return;
62
110
  } catch {
63
- // brew not found
111
+ console.log(
112
+ " ⚠ Homebrew install failed, falling back to direct binary download...",
113
+ );
64
114
  }
115
+ }
65
116
 
66
- if (!hasBrew) {
67
- console.log("🍺 Homebrew not found. Installing Homebrew...");
68
- try {
69
- await exec("bash", [
70
- "-c",
71
- 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
72
- ]);
73
- } catch (err) {
74
- const message = err instanceof Error ? err.message : String(err);
75
- throw new Error(
76
- `Failed to install Homebrew. Please install Docker manually from https://www.docker.com/products/docker-desktop/\n${message}`,
77
- );
78
- }
117
+ // Direct binary install — no sudo required.
118
+ console.log(
119
+ "🐳 Docker not found. Installing Docker, Colima, and Lima to ~/.local/bin/...",
120
+ );
79
121
 
80
- // Homebrew on Apple Silicon installs to /opt/homebrew; add it to PATH
81
- // so subsequent brew/colima/docker invocations work in this session.
82
- if (!process.env.PATH?.includes("/opt/homebrew")) {
83
- process.env.PATH = `/opt/homebrew/bin:/opt/homebrew/sbin:${process.env.PATH}`;
84
- }
85
- }
122
+ mkdirSync(LOCAL_BIN_DIR, { recursive: true });
86
123
 
87
- console.log("🐳 Docker not found. Installing via Homebrew...");
88
- try {
89
- await exec("brew", ["install", "colima", "docker"]);
90
- } catch (err) {
91
- const message = err instanceof Error ? err.message : String(err);
124
+ const cpuArch = releaseArch();
125
+ const isMac = platform() === "darwin";
126
+
127
+ if (!isMac) {
128
+ throw new Error(
129
+ "Automatic Docker installation is only supported on macOS. " +
130
+ "Please install Docker manually: https://docs.docker.com/engine/install/",
131
+ );
132
+ }
133
+
134
+ // --- Docker CLI ---
135
+ // Docker publishes static binaries at download.docker.com.
136
+ const dockerArch = cpuArch === "aarch64" ? "aarch64" : "x86_64";
137
+ const dockerTarUrl = `https://download.docker.com/mac/static/stable/${dockerArch}/docker-27.5.1.tgz`;
138
+ const dockerTmpDir = join(LOCAL_BIN_DIR, ".docker-tmp");
139
+ mkdirSync(dockerTmpDir, { recursive: true });
140
+ try {
141
+ await downloadAndExtract(dockerTarUrl, dockerTmpDir, "Docker CLI");
142
+ // The archive extracts to docker/docker — move it to our bin dir.
143
+ await exec("mv", [
144
+ join(dockerTmpDir, "docker", "docker"),
145
+ join(LOCAL_BIN_DIR, "docker"),
146
+ ]);
147
+ chmodSync(join(LOCAL_BIN_DIR, "docker"), 0o755);
148
+ } finally {
149
+ await exec("rm", ["-rf", dockerTmpDir]).catch(() => {});
150
+ }
151
+
152
+ // --- Colima ---
153
+ const colimaArch = cpuArch === "aarch64" ? "arm64" : "x86_64";
154
+ const colimaUrl = `https://github.com/abiosoft/colima/releases/latest/download/colima-Darwin-${colimaArch}`;
155
+ await downloadBinary(colimaUrl, join(LOCAL_BIN_DIR, "colima"), "Colima");
156
+
157
+ // --- Lima ---
158
+ // Lima publishes tar.gz archives with bin/limactl and other tools.
159
+ const limaArch = cpuArch === "aarch64" ? "arm64" : "x86_64";
160
+ const limaVersionUrl =
161
+ "https://api.github.com/repos/lima-vm/lima/releases/latest";
162
+ let limaVersion: string;
163
+ try {
164
+ const resp = await fetch(limaVersionUrl);
165
+ if (!resp.ok) {
92
166
  throw new Error(
93
- `Failed to install Docker via Homebrew. Please install Docker manually.\n${message}`,
167
+ `GitHub API returned ${resp.status}` +
168
+ (resp.status === 403
169
+ ? " (rate-limited) — try again later."
170
+ : `. Check your network connection.`),
94
171
  );
95
172
  }
173
+ const data = (await resp.json()) as { tag_name?: string };
174
+ if (!data.tag_name) {
175
+ throw new Error("GitHub API response missing tag_name.");
176
+ }
177
+ limaVersion = data.tag_name; // e.g. "v1.0.3"
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : String(err);
180
+ throw new Error(`Failed to fetch latest Lima version: ${message}`);
181
+ }
182
+ const limaVersionNum = limaVersion.replace(/^v/, ""); // "1.0.3"
183
+ const limaTarUrl = `https://github.com/lima-vm/lima/releases/download/${limaVersion}/lima-${limaVersionNum}-Darwin-${limaArch}.tar.gz`;
184
+ // Lima archives contain bin/limactl, bin/lima, share/lima/..., so extract
185
+ // into the parent (~/.local/) so that limactl lands in ~/.local/bin/.
186
+ const localDir = dirname(LOCAL_BIN_DIR);
187
+ await downloadAndExtract(limaTarUrl, localDir, "Lima");
188
+
189
+ // Verify all binaries are in place.
190
+ for (const bin of ["docker", "colima", "limactl"]) {
191
+ if (!existsSync(join(LOCAL_BIN_DIR, bin))) {
192
+ throw new Error(
193
+ `${bin} binary not found after installation. Please install Docker manually.`,
194
+ );
195
+ }
196
+ }
197
+
198
+ console.log(" ✅ Docker toolchain installed to ~/.local/bin/");
199
+ }
200
+
201
+ /**
202
+ * Ensures ~/.local/bin/ is on PATH for this process so that docker, colima,
203
+ * and limactl are discoverable.
204
+ */
205
+ function ensureLocalBinOnPath(): void {
206
+ const currentPath = process.env.PATH || "";
207
+ if (!currentPath.includes(LOCAL_BIN_DIR)) {
208
+ process.env.PATH = `${LOCAL_BIN_DIR}:${currentPath}`;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Checks whether the `docker` CLI and daemon are available on the system.
214
+ * Installs Colima and Docker via direct binary download if missing (no sudo
215
+ * required), and starts Colima if the Docker daemon is not reachable.
216
+ */
217
+ async function ensureDockerInstalled(): Promise<void> {
218
+ // Always add ~/.local/bin to PATH so previously installed binaries are found.
219
+ ensureLocalBinOnPath();
220
+
221
+ // Check that docker, colima, and limactl are all available. If any is
222
+ // missing (e.g. partial install from a previous failure), re-run install.
223
+ const toolchainComplete = await (async () => {
224
+ try {
225
+ await execOutput("docker", ["--version"]);
226
+ await execOutput("colima", ["version"]);
227
+ await execOutput("limactl", ["--version"]);
228
+ return true;
229
+ } catch {
230
+ return false;
231
+ }
232
+ })();
233
+
234
+ if (!toolchainComplete) {
235
+ await installDockerToolchain();
236
+ // Re-check PATH after install.
237
+ ensureLocalBinOnPath();
96
238
 
97
239
  try {
98
240
  await execOutput("docker", ["--version"]);
@@ -104,7 +246,7 @@ async function ensureDockerInstalled(): Promise<void> {
104
246
  }
105
247
  }
106
248
 
107
- // Verify the Docker daemon is reachable; start Colima if it isn't
249
+ // Verify the Docker daemon is reachable; start Colima if it isn't.
108
250
  try {
109
251
  await exec("docker", ["info"]);
110
252
  } catch {
@@ -140,10 +282,10 @@ export function dockerResourceNames(instanceName: string) {
140
282
  return {
141
283
  assistantContainer: `${instanceName}-assistant`,
142
284
  cesContainer: `${instanceName}-credential-executor`,
143
- dataVolume: `vellum-data-${instanceName}`,
285
+ dataVolume: `${instanceName}-data`,
144
286
  gatewayContainer: `${instanceName}-gateway`,
145
- network: `vellum-net-${instanceName}`,
146
- socketVolume: `vellum-ces-bootstrap-${instanceName}`,
287
+ network: `${instanceName}-net`,
288
+ socketVolume: `${instanceName}-socket`,
147
289
  };
148
290
  }
149
291
 
@@ -187,7 +329,7 @@ export async function retireDocker(name: string): Promise<void> {
187
329
  // Also clean up a legacy single-container instance if it exists
188
330
  await removeContainer(name);
189
331
 
190
- // Remove shared network and volumes
332
+ // Remove network and volumes
191
333
  try {
192
334
  await exec("docker", ["network", "rm", res.network]);
193
335
  } catch {
@@ -378,7 +520,6 @@ export function serviceDockerRunArgs(opts: {
378
520
  "-d",
379
521
  "--name",
380
522
  res.cesContainer,
381
- `--network=${res.network}`,
382
523
  "-v",
383
524
  `${res.socketVolume}:/run/ces-bootstrap`,
384
525
  "-v",
@@ -654,7 +795,7 @@ export async function hatchDocker(
654
795
 
655
796
  const res = dockerResourceNames(instanceName);
656
797
 
657
- log("📁 Creating shared network and volumes...");
798
+ log("📁 Creating network and volumes...");
658
799
  await exec("docker", ["network", "create", res.network]);
659
800
  await exec("docker", ["volume", "create", res.dataVolume]);
660
801
  await exec("docker", ["volume", "create", res.socketVolume]);
package/src/lib/ngrok.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { execFileSync, spawn, type ChildProcess } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from "node:fs";
3
9
  import { homedir } from "node:os";
4
10
  import { dirname, join } from "node:path";
5
11
 
@@ -111,12 +117,29 @@ export async function findExistingTunnel(
111
117
 
112
118
  /**
113
119
  * Start an ngrok process tunneling HTTP traffic to the given local port.
120
+ *
121
+ * When `logFilePath` is provided, stdout/stderr are redirected to that file
122
+ * instead of being piped. This avoids keeping pipe handles open in the
123
+ * parent process — which would either prevent the CLI from exiting (if
124
+ * handles are left open) or send SIGPIPE to ngrok (if destroyed).
125
+ *
114
126
  * Returns the spawned child process.
115
127
  */
116
- export function startNgrokProcess(targetPort: number): ChildProcess {
128
+ export function startNgrokProcess(
129
+ targetPort: number,
130
+ logFilePath?: string,
131
+ ): ChildProcess {
132
+ let stdio: ("ignore" | "pipe" | number)[] = ["ignore", "pipe", "pipe"];
133
+ if (logFilePath) {
134
+ const dir = dirname(logFilePath);
135
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
136
+ const fd = openSync(logFilePath, "a");
137
+ stdio = ["ignore", fd, fd];
138
+ }
139
+
117
140
  const child = spawn("ngrok", ["http", String(targetPort), "--log=stdout"], {
118
141
  detached: true,
119
- stdio: ["ignore", "pipe", "pipe"],
142
+ stdio,
120
143
  });
121
144
  return child;
122
145
  }
@@ -170,14 +193,16 @@ function clearIngressUrl(): void {
170
193
  }
171
194
 
172
195
  /**
173
- * Check whether any webhook-based integrations (e.g. Telegram) are configured
174
- * that require a public ingress URL.
196
+ * Check whether any webhook-based integrations (e.g. Telegram, Twilio) are
197
+ * configured that require a public ingress URL.
175
198
  */
176
199
  function hasWebhookIntegrationsConfigured(): boolean {
177
200
  try {
178
201
  const config = loadRawConfig();
179
202
  const telegram = config.telegram as Record<string, unknown> | undefined;
180
203
  if (telegram?.botUsername) return true;
204
+ const twilio = config.twilio as Record<string, unknown> | undefined;
205
+ if (twilio?.accountSid || twilio?.phoneNumber) return true;
181
206
  return false;
182
207
  } catch {
183
208
  return false;
@@ -225,31 +250,23 @@ export async function maybeStartNgrokTunnel(
225
250
  }
226
251
 
227
252
  console.log(` Starting ngrok tunnel for webhook integrations...`);
228
- const ngrokProcess = startNgrokProcess(targetPort);
229
253
 
230
- // Pipe output for debugging but don't block on it
231
- ngrokProcess.stdout?.on("data", (data: Buffer) => {
232
- const line = data.toString().trim();
233
- if (line) console.log(`[ngrok] ${line}`);
234
- });
235
- ngrokProcess.stderr?.on("data", (data: Buffer) => {
236
- const line = data.toString().trim();
237
- if (line) console.error(`[ngrok] ${line}`);
238
- });
254
+ // Spawn ngrok with stdout/stderr redirected to a log file instead of pipes.
255
+ // This avoids two problems that occur with piped stdio:
256
+ // 1. If pipe handles are left open, the CLI process hangs after hatch/wake.
257
+ // 2. If pipe handles are destroyed, SIGPIPE kills ngrok on its next write.
258
+ // Writing to a log file sidesteps both issues — the file descriptor is
259
+ // inherited by the detached ngrok process and remains valid after CLI exit.
260
+ const root = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
261
+ const ngrokLogPath = join(root, "workspace", "data", "logs", "ngrok.log");
262
+ const ngrokProcess = startNgrokProcess(targetPort, ngrokLogPath);
263
+ ngrokProcess.unref();
239
264
 
240
265
  try {
241
266
  const publicUrl = await waitForNgrokUrl();
242
267
  saveIngressUrl(publicUrl);
243
268
  console.log(` Tunnel established: ${publicUrl}`);
244
269
 
245
- // Detach the ngrok process so the CLI (hatch/wake) can exit without
246
- // keeping it alive. Remove stdout/stderr listeners and unref all handles.
247
- ngrokProcess.stdout?.removeAllListeners("data");
248
- ngrokProcess.stderr?.removeAllListeners("data");
249
- ngrokProcess.stdout?.destroy();
250
- ngrokProcess.stderr?.destroy();
251
- ngrokProcess.unref();
252
-
253
270
  return ngrokProcess;
254
271
  } catch {
255
272
  console.warn(