@vellumai/cli 0.4.43 → 0.4.45

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.
@@ -0,0 +1,5 @@
1
+ # CLI Package — Contributing Guidelines
2
+
3
+ ## Module Boundaries
4
+
5
+ - **Commands must not import from other commands.** Shared logic belongs in `src/lib/`. If two commands need the same function, extract it into an appropriate lib module rather than importing across `src/commands/` files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, beforeEach, afterAll } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
@@ -14,6 +14,7 @@ import {
14
14
  loadAllAssistants,
15
15
  saveAssistantEntry,
16
16
  getActiveAssistant,
17
+ migrateLegacyEntry,
17
18
  type AssistantEntry,
18
19
  } from "../lib/assistant-config.js";
19
20
 
@@ -180,3 +181,250 @@ describe("assistant-config", () => {
180
181
  expect(all[0].assistantId).toBe("valid");
181
182
  });
182
183
  });
184
+
185
+ describe("migrateLegacyEntry", () => {
186
+ test("rewrites baseDataDir as resources.instanceDir", () => {
187
+ /**
188
+ * Tests that a legacy entry with top-level baseDataDir gets migrated
189
+ * to the current resources.instanceDir format.
190
+ */
191
+
192
+ // GIVEN a legacy entry with baseDataDir set at the top level
193
+ const entry: Record<string, unknown> = {
194
+ assistantId: "my-assistant",
195
+ runtimeUrl: "http://localhost:7830",
196
+ cloud: "local",
197
+ baseDataDir: "/home/user/.local/share/vellum/assistants/my-assistant",
198
+ };
199
+
200
+ // WHEN we migrate the entry
201
+ const changed = migrateLegacyEntry(entry);
202
+
203
+ // THEN the entry should be mutated
204
+ expect(changed).toBe(true);
205
+
206
+ // AND baseDataDir should be removed
207
+ expect(entry.baseDataDir).toBeUndefined();
208
+
209
+ // AND resources.instanceDir should contain the old baseDataDir value
210
+ const resources = entry.resources as Record<string, unknown>;
211
+ expect(resources.instanceDir).toBe(
212
+ "/home/user/.local/share/vellum/assistants/my-assistant",
213
+ );
214
+ });
215
+
216
+ test("synthesises full resources when none exist", () => {
217
+ /**
218
+ * Tests that a legacy local entry with no resources object gets a
219
+ * complete resources object synthesised with default ports and pidFile.
220
+ */
221
+
222
+ // GIVEN a local entry with no resources
223
+ const entry: Record<string, unknown> = {
224
+ assistantId: "old-assistant",
225
+ runtimeUrl: "http://localhost:7830",
226
+ cloud: "local",
227
+ };
228
+
229
+ // WHEN we migrate the entry
230
+ const changed = migrateLegacyEntry(entry);
231
+
232
+ // THEN the entry should be mutated
233
+ expect(changed).toBe(true);
234
+
235
+ // AND resources should be fully populated
236
+ const resources = entry.resources as Record<string, unknown>;
237
+ expect(resources.instanceDir).toContain("old-assistant");
238
+ expect(resources.daemonPort).toBe(7821);
239
+ expect(resources.gatewayPort).toBe(7830);
240
+ expect(resources.qdrantPort).toBe(6333);
241
+ expect(resources.pidFile).toContain("vellum.pid");
242
+ });
243
+
244
+ test("infers gateway port from runtimeUrl", () => {
245
+ /**
246
+ * Tests that the gateway port is extracted from the runtimeUrl when
247
+ * synthesising resources for a legacy entry.
248
+ */
249
+
250
+ // GIVEN a local entry with a non-default gateway port in the runtimeUrl
251
+ const entry: Record<string, unknown> = {
252
+ assistantId: "custom-port",
253
+ runtimeUrl: "http://localhost:9999",
254
+ cloud: "local",
255
+ };
256
+
257
+ // WHEN we migrate the entry
258
+ migrateLegacyEntry(entry);
259
+
260
+ // THEN the gateway port should match the runtimeUrl port
261
+ const resources = entry.resources as Record<string, unknown>;
262
+ expect(resources.gatewayPort).toBe(9999);
263
+ });
264
+
265
+ test("skips non-local entries", () => {
266
+ /**
267
+ * Tests that remote (non-local) entries are left untouched.
268
+ */
269
+
270
+ // GIVEN a GCP entry without resources
271
+ const entry: Record<string, unknown> = {
272
+ assistantId: "gcp-assistant",
273
+ runtimeUrl: "https://example.com",
274
+ cloud: "gcp",
275
+ };
276
+
277
+ // WHEN we attempt to migrate it
278
+ const changed = migrateLegacyEntry(entry);
279
+
280
+ // THEN nothing should change
281
+ expect(changed).toBe(false);
282
+ expect(entry.resources).toBeUndefined();
283
+ });
284
+
285
+ test("backfills missing fields on partial resources", () => {
286
+ /**
287
+ * Tests that an entry with a partial resources object (e.g. only
288
+ * instanceDir) gets the remaining fields backfilled.
289
+ */
290
+
291
+ // GIVEN an entry with partial resources (only instanceDir)
292
+ const entry: Record<string, unknown> = {
293
+ assistantId: "partial",
294
+ runtimeUrl: "http://localhost:7830",
295
+ cloud: "local",
296
+ resources: {
297
+ instanceDir: "/custom/path",
298
+ },
299
+ };
300
+
301
+ // WHEN we migrate the entry
302
+ const changed = migrateLegacyEntry(entry);
303
+
304
+ // THEN the entry should be mutated
305
+ expect(changed).toBe(true);
306
+
307
+ // AND all missing resources fields should be filled in
308
+ const resources = entry.resources as Record<string, unknown>;
309
+ expect(resources.instanceDir).toBe("/custom/path");
310
+ expect(resources.daemonPort).toBe(7821);
311
+ expect(resources.gatewayPort).toBe(7830);
312
+ expect(resources.qdrantPort).toBe(6333);
313
+ expect(resources.pidFile).toBe("/custom/path/.vellum/vellum.pid");
314
+ });
315
+
316
+ test("does not overwrite existing resources fields", () => {
317
+ /**
318
+ * Tests that an entry with a complete resources object is left untouched.
319
+ */
320
+
321
+ // GIVEN an entry with a complete resources object
322
+ const entry: Record<string, unknown> = {
323
+ assistantId: "complete",
324
+ runtimeUrl: "http://localhost:7830",
325
+ cloud: "local",
326
+ resources: {
327
+ instanceDir: "/my/path",
328
+ daemonPort: 8000,
329
+ gatewayPort: 8001,
330
+ qdrantPort: 8002,
331
+ pidFile: "/my/path/.vellum/vellum.pid",
332
+ },
333
+ };
334
+
335
+ // WHEN we migrate the entry
336
+ const changed = migrateLegacyEntry(entry);
337
+
338
+ // THEN nothing should change
339
+ expect(changed).toBe(false);
340
+
341
+ // AND existing values should be preserved
342
+ const resources = entry.resources as Record<string, unknown>;
343
+ expect(resources.daemonPort).toBe(8000);
344
+ expect(resources.gatewayPort).toBe(8001);
345
+ expect(resources.qdrantPort).toBe(8002);
346
+ });
347
+
348
+ test("baseDataDir does not overwrite existing resources.instanceDir", () => {
349
+ /**
350
+ * Tests that when both baseDataDir and resources.instanceDir exist,
351
+ * the existing instanceDir is preserved and baseDataDir is removed.
352
+ */
353
+
354
+ // GIVEN an entry with both baseDataDir and resources.instanceDir
355
+ const entry: Record<string, unknown> = {
356
+ assistantId: "conflict",
357
+ runtimeUrl: "http://localhost:7830",
358
+ cloud: "local",
359
+ baseDataDir: "/old/path",
360
+ resources: {
361
+ instanceDir: "/new/path",
362
+ daemonPort: 7821,
363
+ gatewayPort: 7830,
364
+ qdrantPort: 6333,
365
+ pidFile: "/new/path/.vellum/vellum.pid",
366
+ },
367
+ };
368
+
369
+ // WHEN we migrate the entry
370
+ const changed = migrateLegacyEntry(entry);
371
+
372
+ // THEN baseDataDir should be removed
373
+ expect(changed).toBe(true);
374
+ expect(entry.baseDataDir).toBeUndefined();
375
+
376
+ // AND the existing instanceDir should be preserved
377
+ const resources = entry.resources as Record<string, unknown>;
378
+ expect(resources.instanceDir).toBe("/new/path");
379
+ });
380
+ });
381
+
382
+ describe("legacy migration via loadAllAssistants", () => {
383
+ beforeEach(() => {
384
+ try {
385
+ rmSync(join(testDir, ".vellum.lock.json"));
386
+ } catch {
387
+ // file may not exist
388
+ }
389
+ });
390
+
391
+ test("migrates legacy entries and persists to disk on read", () => {
392
+ /**
393
+ * Tests that reading assistants from a lockfile with legacy entries
394
+ * triggers migration and persists the updated format to disk.
395
+ */
396
+
397
+ // GIVEN a lockfile with a legacy entry containing baseDataDir
398
+ writeLockfile({
399
+ assistants: [
400
+ {
401
+ assistantId: "legacy-bot",
402
+ runtimeUrl: "http://localhost:7830",
403
+ cloud: "local",
404
+ baseDataDir: "/home/user/.local/share/vellum/assistants/legacy-bot",
405
+ },
406
+ ],
407
+ });
408
+
409
+ // WHEN we load assistants
410
+ const all = loadAllAssistants();
411
+
412
+ // THEN the entry should have resources populated
413
+ expect(all).toHaveLength(1);
414
+ expect(all[0].resources).toBeDefined();
415
+ expect(all[0].resources!.instanceDir).toBe(
416
+ "/home/user/.local/share/vellum/assistants/legacy-bot",
417
+ );
418
+ expect(all[0].resources!.gatewayPort).toBe(7830);
419
+
420
+ // AND the lockfile on disk should reflect the migration
421
+ const rawDisk = JSON.parse(
422
+ readFileSync(join(testDir, ".vellum.lock.json"), "utf-8"),
423
+ );
424
+ const diskEntry = rawDisk.assistants[0];
425
+ expect(diskEntry.baseDataDir).toBeUndefined();
426
+ expect(diskEntry.resources.instanceDir).toBe(
427
+ "/home/user/.local/share/vellum/assistants/legacy-bot",
428
+ );
429
+ });
430
+ });
@@ -94,21 +94,22 @@ describe("multi-local", () => {
94
94
  });
95
95
 
96
96
  describe("allocateLocalResources() produces non-conflicting ports", () => {
97
- test("first instance gets XDG path and default ports", async () => {
97
+ test("first instance gets home directory and default ports", async () => {
98
98
  // GIVEN no local assistants exist in the lockfile
99
99
 
100
100
  // WHEN we allocate resources for the first instance
101
101
  const res = await allocateLocalResources("instance-a");
102
102
 
103
- // THEN it gets an XDG instance directory under the home dir
104
- expect(res.instanceDir).toBe(
105
- join(testDir, ".local", "share", "vellum", "assistants", "instance-a"),
106
- );
103
+ // THEN it gets the home directory as its instance root
104
+ expect(res.instanceDir).toBe(testDir);
107
105
 
108
106
  // AND it gets the default ports since no other instances exist
109
107
  expect(res.daemonPort).toBe(DEFAULT_DAEMON_PORT);
110
108
  expect(res.gatewayPort).toBe(DEFAULT_GATEWAY_PORT);
111
109
  expect(res.qdrantPort).toBe(DEFAULT_QDRANT_PORT);
110
+
111
+ // AND the PID file is under ~/.vellum/
112
+ expect(res.pidFile).toBe(join(testDir, ".vellum", "vellum.pid"));
112
113
  });
113
114
 
114
115
  test("second instance gets distinct ports and dir when first instance is saved", async () => {
@@ -0,0 +1,34 @@
1
+ import { detectOrphanedProcesses } from "../lib/orphan-detection";
2
+ import { stopProcess } from "../lib/process";
3
+
4
+ export async function clean(): Promise<void> {
5
+ const args = process.argv.slice(3);
6
+ if (args.includes("--help") || args.includes("-h")) {
7
+ console.log("Usage: vellum clean");
8
+ console.log("");
9
+ console.log("Kill all orphaned vellum processes that are not tracked by any assistant.");
10
+ process.exit(0);
11
+ }
12
+
13
+ const orphans = await detectOrphanedProcesses();
14
+
15
+ if (orphans.length === 0) {
16
+ console.log("No orphaned processes found.");
17
+ return;
18
+ }
19
+
20
+ console.log(`Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"}.\n`);
21
+
22
+ let killed = 0;
23
+ for (const orphan of orphans) {
24
+ const pid = parseInt(orphan.pid, 10);
25
+ const stopped = await stopProcess(pid, `${orphan.name} (PID ${orphan.pid})`);
26
+ if (stopped) {
27
+ killed++;
28
+ }
29
+ }
30
+
31
+ console.log(
32
+ `\nCleaned up ${killed} process${killed === 1 ? "" : "es"}.`,
33
+ );
34
+ }
@@ -47,9 +47,11 @@ import {
47
47
  startGateway,
48
48
  stopLocalProcesses,
49
49
  } from "../lib/local";
50
+ import { maybeStartNgrokTunnel } from "../lib/ngrok";
50
51
  import { isProcessAlive } from "../lib/process";
51
52
  import { generateRandomSuffix } from "../lib/random-name";
52
53
  import { validateAssistantName } from "../lib/retire-archive";
54
+ import { archiveLogFile, resetLogFile } from "../lib/xdg-log";
53
55
 
54
56
  export type { PollResult, WatchHatchingResult } from "../lib/gcp";
55
57
 
@@ -265,7 +267,16 @@ function parseArgs(): HatchArgs {
265
267
  }
266
268
  }
267
269
 
268
- return { species, detached, keepAlive, name, remote, daemonOnly, restart, watch };
270
+ return {
271
+ species,
272
+ detached,
273
+ keepAlive,
274
+ name,
275
+ remote,
276
+ daemonOnly,
277
+ restart,
278
+ watch,
279
+ };
269
280
  }
270
281
 
271
282
  function formatElapsed(ms: number): string {
@@ -730,6 +741,16 @@ async function hatchLocal(
730
741
  resources = await allocateLocalResources(instanceName);
731
742
  }
732
743
 
744
+ const logsDir = join(
745
+ resources.instanceDir,
746
+ ".vellum",
747
+ "workspace",
748
+ "data",
749
+ "logs",
750
+ );
751
+ archiveLogFile("hatch.log", logsDir);
752
+ resetLogFile("hatch.log");
753
+
733
754
  console.log(`🥚 Hatching local assistant: ${instanceName}`);
734
755
  console.log(` Species: ${species}`);
735
756
  console.log("");
@@ -749,6 +770,21 @@ async function hatchLocal(
749
770
  throw error;
750
771
  }
751
772
 
773
+ // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
774
+ // Set BASE_DATA_DIR so ngrok reads the correct instance config.
775
+ const prevBaseDataDir = process.env.BASE_DATA_DIR;
776
+ process.env.BASE_DATA_DIR = resources.instanceDir;
777
+ const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
778
+ if (ngrokChild?.pid) {
779
+ const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
780
+ writeFileSync(ngrokPidFile, String(ngrokChild.pid));
781
+ }
782
+ if (prevBaseDataDir !== undefined) {
783
+ process.env.BASE_DATA_DIR = prevBaseDataDir;
784
+ } else {
785
+ delete process.env.BASE_DATA_DIR;
786
+ }
787
+
752
788
  // Read the bearer token (JWT) written by the daemon so the CLI can
753
789
  // with the gateway (which requires auth by default). The daemon writes under
754
790
  // getRootDir() which resolves to <instanceDir>/.vellum/.
@@ -826,9 +862,7 @@ async function hatchLocal(
826
862
  consecutiveFailures++;
827
863
  }
828
864
  if (consecutiveFailures >= MAX_FAILURES) {
829
- console.log(
830
- "\n⚠️ Gateway stopped responding — shutting down.",
831
- );
865
+ console.log("\n⚠️ Gateway stopped responding — shutting down.");
832
866
  await stopLocalProcesses(resources);
833
867
  process.exit(1);
834
868
  }
@@ -844,8 +878,16 @@ export async function hatch(): Promise<void> {
844
878
  const cliVersion = getCliVersion();
845
879
  console.log(`@vellumai/cli v${cliVersion}`);
846
880
 
847
- const { species, detached, keepAlive, name, remote, daemonOnly, restart, watch } =
848
- parseArgs();
881
+ const {
882
+ species,
883
+ detached,
884
+ keepAlive,
885
+ name,
886
+ remote,
887
+ daemonOnly,
888
+ restart,
889
+ watch,
890
+ } = parseArgs();
849
891
 
850
892
  if (restart && remote !== "local") {
851
893
  console.error(
@@ -1,5 +1,3 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { homedir } from "os";
3
1
  import { join } from "path";
4
2
 
5
3
  import {
@@ -9,6 +7,13 @@ import {
9
7
  type AssistantEntry,
10
8
  } from "../lib/assistant-config";
11
9
  import { checkHealth } from "../lib/health-check";
10
+ import {
11
+ classifyProcess,
12
+ detectOrphanedProcesses,
13
+ isProcessAlive,
14
+ parseRemotePs,
15
+ readPidFile,
16
+ } from "../lib/orphan-detection";
12
17
  import { pgrepExact } from "../lib/pgrep";
13
18
  import { probePort } from "../lib/port-probe";
14
19
  import { withStatusEmoji } from "../lib/status-emoji";
@@ -77,40 +82,6 @@ const REMOTE_PS_CMD = [
77
82
  "| grep -v grep",
78
83
  ].join(" ");
79
84
 
80
- interface RemoteProcess {
81
- pid: string;
82
- ppid: string;
83
- command: string;
84
- }
85
-
86
- function classifyProcess(command: string): string {
87
- if (/qdrant/.test(command)) return "qdrant";
88
- if (/vellum-gateway/.test(command)) return "gateway";
89
- if (/openclaw/.test(command)) return "openclaw-adapter";
90
- if (/vellum-daemon/.test(command)) return "assistant";
91
- if (/daemon\s+(start|restart)/.test(command)) return "assistant";
92
- // Exclude macOS desktop app processes — their path contains .app/Contents/MacOS/
93
- // but they are not background service processes.
94
- if (/\.app\/Contents\/MacOS\//.test(command)) return "unknown";
95
- if (/vellum/.test(command)) return "vellum";
96
- return "unknown";
97
- }
98
-
99
- function parseRemotePs(output: string): RemoteProcess[] {
100
- return output
101
- .trim()
102
- .split("\n")
103
- .filter((line) => line.trim().length > 0)
104
- .map((line) => {
105
- const trimmed = line.trim();
106
- const parts = trimmed.split(/\s+/);
107
- const pid = parts[0];
108
- const ppid = parts[1];
109
- const command = parts.slice(2).join(" ");
110
- return { pid, ppid, command };
111
- });
112
- }
113
-
114
85
  function extractHostFromUrl(url: string): string {
115
86
  try {
116
87
  const parsed = new URL(url);
@@ -157,21 +128,6 @@ interface ProcessSpec {
157
128
  pidFile: string;
158
129
  }
159
130
 
160
- function readPidFile(pidFile: string): string | null {
161
- if (!existsSync(pidFile)) return null;
162
- const pid = readFileSync(pidFile, "utf-8").trim();
163
- return pid || null;
164
- }
165
-
166
- function isProcessAlive(pid: string): boolean {
167
- try {
168
- process.kill(parseInt(pid, 10), 0);
169
- return true;
170
- } catch {
171
- return false;
172
- }
173
- }
174
-
175
131
  interface DetectedProcess {
176
132
  name: string;
177
133
  pid: string | null;
@@ -318,57 +274,6 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
318
274
  printTable(rows);
319
275
  }
320
276
 
321
- // ── Orphaned process detection ──────────────────────────────────
322
-
323
- interface OrphanedProcess {
324
- name: string;
325
- pid: string;
326
- source: string;
327
- }
328
-
329
- async function detectOrphanedProcesses(): Promise<OrphanedProcess[]> {
330
- const results: OrphanedProcess[] = [];
331
- const seenPids = new Set<string>();
332
- const vellumDir = join(homedir(), ".vellum");
333
-
334
- // Strategy 1: PID file scan
335
- const pidFiles: Array<{ file: string; name: string }> = [
336
- { file: join(vellumDir, "vellum.pid"), name: "assistant" },
337
- { file: join(vellumDir, "gateway.pid"), name: "gateway" },
338
- { file: join(vellumDir, "qdrant.pid"), name: "qdrant" },
339
- ];
340
-
341
- for (const { file, name } of pidFiles) {
342
- const pid = readPidFile(file);
343
- if (pid && isProcessAlive(pid)) {
344
- results.push({ name, pid, source: "pid file" });
345
- seenPids.add(pid);
346
- }
347
- }
348
-
349
- // Strategy 2: Process table scan
350
- try {
351
- const output = await execOutput("sh", [
352
- "-c",
353
- "ps ax -o pid=,ppid=,args= | grep -E 'vellum|vellum-gateway|qdrant|openclaw' | grep -v grep",
354
- ]);
355
- const procs = parseRemotePs(output);
356
- const ownPid = String(process.pid);
357
-
358
- for (const p of procs) {
359
- if (p.pid === ownPid || seenPids.has(p.pid)) continue;
360
- const type = classifyProcess(p.command);
361
- if (type === "unknown") continue;
362
- results.push({ name: type, pid: p.pid, source: "process table" });
363
- seenPids.add(p.pid);
364
- }
365
- } catch {
366
- // grep exits 1 when no matches found — ignore
367
- }
368
-
369
- return results;
370
- }
371
-
372
277
  // ── List all assistants (no arg) ────────────────────────────────
373
278
 
374
279
  async function listAllAssistants(): Promise<void> {
@@ -387,9 +292,8 @@ async function listAllAssistants(): Promise<void> {
387
292
  info: `PID ${o.pid} (from ${o.source})`,
388
293
  }));
389
294
  printTable(rows);
390
- const pids = orphans.map((o) => o.pid).join(" ");
391
295
  console.log(
392
- `\nHint: Run \`kill ${pids}\` to clean up orphaned processes.`,
296
+ `\nHint: Run \`vellum clean\` to clean up orphaned processes.`,
393
297
  );
394
298
  }
395
299
 
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  import { resolveTargetAssistant } from "../lib/assistant-config.js";
5
5
  import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
6
6
  import { startLocalDaemon, startGateway } from "../lib/local";
7
+ import { maybeStartNgrokTunnel } from "../lib/ngrok";
7
8
 
8
9
  export async function wake(): Promise<void> {
9
10
  const args = process.argv.slice(3);
@@ -95,5 +96,20 @@ export async function wake(): Promise<void> {
95
96
  }
96
97
  }
97
98
 
99
+ // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
100
+ // Set BASE_DATA_DIR so ngrok reads the correct instance config.
101
+ const prevBaseDataDir = process.env.BASE_DATA_DIR;
102
+ process.env.BASE_DATA_DIR = resources.instanceDir;
103
+ const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
104
+ if (ngrokChild?.pid) {
105
+ const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
106
+ writeFileSync(ngrokPidFile, String(ngrokChild.pid));
107
+ }
108
+ if (prevBaseDataDir !== undefined) {
109
+ process.env.BASE_DATA_DIR = prevBaseDataDir;
110
+ } else {
111
+ delete process.env.BASE_DATA_DIR;
112
+ }
113
+
98
114
  console.log("Wake complete.");
99
115
  }