@vellumai/cli 0.4.36 → 0.4.40
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 +2 -2
- package/src/__tests__/multi-local.test.ts +275 -0
- package/src/__tests__/skills-uninstall.test.ts +203 -0
- package/src/commands/client.ts +23 -7
- package/src/commands/hatch.ts +38 -42
- package/src/commands/ps.ts +32 -12
- package/src/commands/retire.ts +48 -12
- package/src/commands/skills.ts +130 -5
- package/src/commands/sleep.ts +25 -6
- package/src/commands/use.ts +44 -0
- package/src/commands/wake.ts +25 -16
- package/src/index.ts +5 -49
- package/src/lib/assistant-config.ts +226 -3
- package/src/lib/constants.ts +6 -0
- package/src/lib/local.ts +189 -49
- package/src/lib/status-emoji.ts +3 -0
package/src/lib/local.ts
CHANGED
|
@@ -12,7 +12,11 @@ import { createConnection } from "net";
|
|
|
12
12
|
import { homedir, hostname, networkInterfaces, platform } from "os";
|
|
13
13
|
import { dirname, join } from "path";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
defaultLocalResources,
|
|
17
|
+
loadLatestAssistant,
|
|
18
|
+
type LocalInstanceResources,
|
|
19
|
+
} from "./assistant-config.js";
|
|
16
20
|
import { GATEWAY_PORT } from "./constants.js";
|
|
17
21
|
import { stopProcessByPidFile } from "./process.js";
|
|
18
22
|
import { openLogFile, pipeToLogFile } from "./xdg-log.js";
|
|
@@ -210,14 +214,20 @@ function resolveDaemonMainPath(assistantIndex: string): string {
|
|
|
210
214
|
return join(dirname(assistantIndex), "daemon", "main.ts");
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
async function startDaemonFromSource(
|
|
217
|
+
async function startDaemonFromSource(
|
|
218
|
+
assistantIndex: string,
|
|
219
|
+
resources?: LocalInstanceResources,
|
|
220
|
+
): Promise<void> {
|
|
214
221
|
const daemonMainPath = resolveDaemonMainPath(assistantIndex);
|
|
215
222
|
|
|
216
|
-
const
|
|
217
|
-
|
|
223
|
+
const defaults = defaultLocalResources();
|
|
224
|
+
const res = resources ?? defaults;
|
|
225
|
+
// Ensure the directory containing PID/socket files exists. For named
|
|
226
|
+
// instances this is instanceDir/.vellum/ (matching daemon's getRootDir()).
|
|
227
|
+
mkdirSync(dirname(res.pidFile), { recursive: true });
|
|
218
228
|
|
|
219
|
-
const pidFile =
|
|
220
|
-
const socketFile =
|
|
229
|
+
const pidFile = res.pidFile;
|
|
230
|
+
const socketFile = res.socketPath;
|
|
221
231
|
|
|
222
232
|
// --- Lifecycle guard: prevent split-brain daemon state ---
|
|
223
233
|
if (existsSync(pidFile)) {
|
|
@@ -263,6 +273,14 @@ async function startDaemonFromSource(assistantIndex: string): Promise<void> {
|
|
|
263
273
|
env.VELLUM_DAEMON_TCP_ENABLED =
|
|
264
274
|
process.env.VELLUM_DAEMON_TCP_ENABLED || "1";
|
|
265
275
|
}
|
|
276
|
+
if (resources) {
|
|
277
|
+
env.BASE_DATA_DIR = resources.instanceDir;
|
|
278
|
+
env.RUNTIME_HTTP_PORT = String(resources.daemonPort);
|
|
279
|
+
env.GATEWAY_PORT = String(resources.gatewayPort);
|
|
280
|
+
env.VELLUM_DAEMON_SOCKET = resources.socketPath;
|
|
281
|
+
env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
|
|
282
|
+
delete env.QDRANT_URL;
|
|
283
|
+
}
|
|
266
284
|
|
|
267
285
|
// Use fd inheritance instead of pipes so the daemon's stdout/stderr survive
|
|
268
286
|
// after the parent (hatch) exits. Bun does not ignore SIGPIPE, so piped
|
|
@@ -287,17 +305,19 @@ async function startDaemonFromSource(assistantIndex: string): Promise<void> {
|
|
|
287
305
|
// assistant-side equivalent.
|
|
288
306
|
async function startDaemonWatchFromSource(
|
|
289
307
|
assistantIndex: string,
|
|
308
|
+
resources?: LocalInstanceResources,
|
|
290
309
|
): Promise<void> {
|
|
291
310
|
const mainPath = resolveDaemonMainPath(assistantIndex);
|
|
292
311
|
if (!existsSync(mainPath)) {
|
|
293
312
|
throw new Error(`Daemon main.ts not found at ${mainPath}`);
|
|
294
313
|
}
|
|
295
314
|
|
|
296
|
-
const
|
|
297
|
-
|
|
315
|
+
const defaults = defaultLocalResources();
|
|
316
|
+
const res = resources ?? defaults;
|
|
317
|
+
mkdirSync(dirname(res.pidFile), { recursive: true });
|
|
298
318
|
|
|
299
|
-
const pidFile =
|
|
300
|
-
const socketFile =
|
|
319
|
+
const pidFile = res.pidFile;
|
|
320
|
+
const socketFile = res.socketPath;
|
|
301
321
|
|
|
302
322
|
// --- Lifecycle guard: prevent split-brain daemon state ---
|
|
303
323
|
// If a daemon is already running, skip spawning a new one.
|
|
@@ -342,7 +362,16 @@ async function startDaemonWatchFromSource(
|
|
|
342
362
|
const env: Record<string, string | undefined> = {
|
|
343
363
|
...process.env,
|
|
344
364
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
365
|
+
VELLUM_DEV: "1",
|
|
345
366
|
};
|
|
367
|
+
if (resources) {
|
|
368
|
+
env.BASE_DATA_DIR = resources.instanceDir;
|
|
369
|
+
env.RUNTIME_HTTP_PORT = String(resources.daemonPort);
|
|
370
|
+
env.GATEWAY_PORT = String(resources.gatewayPort);
|
|
371
|
+
env.VELLUM_DAEMON_SOCKET = resources.socketPath;
|
|
372
|
+
env.QDRANT_HTTP_PORT = String(resources.qdrantPort);
|
|
373
|
+
delete env.QDRANT_URL;
|
|
374
|
+
}
|
|
346
375
|
|
|
347
376
|
const daemonLogFd = openLogFile("hatch.log");
|
|
348
377
|
const child = spawn("bun", ["--watch", "run", mainPath], {
|
|
@@ -405,9 +434,12 @@ function normalizeIngressUrl(value: unknown): string | undefined {
|
|
|
405
434
|
return normalized || undefined;
|
|
406
435
|
}
|
|
407
436
|
|
|
408
|
-
function readWorkspaceIngressPublicBaseUrl(
|
|
437
|
+
function readWorkspaceIngressPublicBaseUrl(
|
|
438
|
+
instanceDir?: string,
|
|
439
|
+
): string | undefined {
|
|
409
440
|
const baseDataDir =
|
|
410
|
-
|
|
441
|
+
instanceDir ??
|
|
442
|
+
(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir()));
|
|
411
443
|
const workspaceConfigPath = join(
|
|
412
444
|
baseDataDir,
|
|
413
445
|
".vellum",
|
|
@@ -477,7 +509,8 @@ function isSocketResponsive(
|
|
|
477
509
|
});
|
|
478
510
|
}
|
|
479
511
|
|
|
480
|
-
async function discoverPublicUrl(): Promise<string | undefined> {
|
|
512
|
+
async function discoverPublicUrl(port?: number): Promise<string | undefined> {
|
|
513
|
+
const effectivePort = port ?? GATEWAY_PORT;
|
|
481
514
|
const cloud = process.env.VELLUM_CLOUD;
|
|
482
515
|
|
|
483
516
|
let externalIp: string | undefined;
|
|
@@ -515,7 +548,7 @@ async function discoverPublicUrl(): Promise<string | undefined> {
|
|
|
515
548
|
|
|
516
549
|
if (externalIp) {
|
|
517
550
|
console.log(` Discovered external IP: ${externalIp}`);
|
|
518
|
-
return `http://${externalIp}:${
|
|
551
|
+
return `http://${externalIp}:${effectivePort}`;
|
|
519
552
|
}
|
|
520
553
|
}
|
|
521
554
|
|
|
@@ -526,18 +559,18 @@ async function discoverPublicUrl(): Promise<string | undefined> {
|
|
|
526
559
|
const localHostname = getMacLocalHostname();
|
|
527
560
|
if (localHostname) {
|
|
528
561
|
console.log(` Discovered macOS local hostname: ${localHostname}`);
|
|
529
|
-
return `http://${localHostname}:${
|
|
562
|
+
return `http://${localHostname}:${effectivePort}`;
|
|
530
563
|
}
|
|
531
564
|
}
|
|
532
565
|
|
|
533
566
|
const lanIp = getLocalLanIPv4();
|
|
534
567
|
if (lanIp) {
|
|
535
568
|
console.log(` Discovered LAN IP: ${lanIp}`);
|
|
536
|
-
return `http://${lanIp}:${
|
|
569
|
+
return `http://${lanIp}:${effectivePort}`;
|
|
537
570
|
}
|
|
538
571
|
|
|
539
572
|
// Final fallback to localhost when no LAN address could be discovered.
|
|
540
|
-
return `http://localhost:${
|
|
573
|
+
return `http://localhost:${effectivePort}`;
|
|
541
574
|
}
|
|
542
575
|
|
|
543
576
|
/**
|
|
@@ -606,7 +639,10 @@ function getLocalLanIPv4(): string | undefined {
|
|
|
606
639
|
// It should eventually converge with
|
|
607
640
|
// assistant/src/daemon/daemon-control.ts::startDaemon which is the
|
|
608
641
|
// assistant-side equivalent.
|
|
609
|
-
export async function startLocalDaemon(
|
|
642
|
+
export async function startLocalDaemon(
|
|
643
|
+
watch: boolean = false,
|
|
644
|
+
resources?: LocalInstanceResources,
|
|
645
|
+
): Promise<void> {
|
|
610
646
|
if (process.env.VELLUM_DESKTOP_APP && !watch) {
|
|
611
647
|
// When running inside the desktop app, the CLI owns the daemon lifecycle.
|
|
612
648
|
// Find the vellum-daemon binary adjacent to the CLI binary.
|
|
@@ -620,9 +656,10 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
620
656
|
);
|
|
621
657
|
}
|
|
622
658
|
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
const
|
|
659
|
+
const defaults = defaultLocalResources();
|
|
660
|
+
const res = resources ?? defaults;
|
|
661
|
+
const pidFile = res.pidFile;
|
|
662
|
+
const socketFile = res.socketPath;
|
|
626
663
|
|
|
627
664
|
// If a daemon is already running, skip spawning a new one.
|
|
628
665
|
// This prevents cascading kill→restart cycles when multiple callers
|
|
@@ -678,8 +715,8 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
678
715
|
// Ensure bun is available for runtime features (browser, skills install)
|
|
679
716
|
ensureBunInstalled();
|
|
680
717
|
|
|
681
|
-
// Ensure
|
|
682
|
-
mkdirSync(
|
|
718
|
+
// Ensure the directory containing PID/socket files exists
|
|
719
|
+
mkdirSync(dirname(pidFile), { recursive: true });
|
|
683
720
|
|
|
684
721
|
// Build a minimal environment for the daemon. When launched from the
|
|
685
722
|
// macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
|
|
@@ -697,6 +734,8 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
697
734
|
for (const key of [
|
|
698
735
|
"ANTHROPIC_API_KEY",
|
|
699
736
|
"BASE_DATA_DIR",
|
|
737
|
+
"QDRANT_HTTP_PORT",
|
|
738
|
+
"QDRANT_URL",
|
|
700
739
|
"RUNTIME_HTTP_PORT",
|
|
701
740
|
"VELLUM_DAEMON_TCP_PORT",
|
|
702
741
|
"VELLUM_DAEMON_TCP_HOST",
|
|
@@ -712,6 +751,16 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
712
751
|
daemonEnv[key] = process.env[key]!;
|
|
713
752
|
}
|
|
714
753
|
}
|
|
754
|
+
// When running a named instance, override env so the daemon resolves
|
|
755
|
+
// all paths under the instance directory and listens on its own port.
|
|
756
|
+
if (resources) {
|
|
757
|
+
daemonEnv.BASE_DATA_DIR = resources.instanceDir;
|
|
758
|
+
daemonEnv.RUNTIME_HTTP_PORT = String(resources.daemonPort);
|
|
759
|
+
daemonEnv.GATEWAY_PORT = String(resources.gatewayPort);
|
|
760
|
+
daemonEnv.VELLUM_DAEMON_SOCKET = resources.socketPath;
|
|
761
|
+
daemonEnv.QDRANT_HTTP_PORT = String(resources.qdrantPort);
|
|
762
|
+
delete daemonEnv.QDRANT_URL;
|
|
763
|
+
}
|
|
715
764
|
|
|
716
765
|
// Use fd inheritance instead of pipes so the daemon's stdout/stderr
|
|
717
766
|
// survive after the parent (hatch) exits. Bun does not ignore SIGPIPE,
|
|
@@ -757,9 +806,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
757
806
|
// Kill the bundled daemon to avoid two processes competing for the same socket/port
|
|
758
807
|
await stopProcessByPidFile(pidFile, "bundled daemon", [socketFile]);
|
|
759
808
|
if (watch) {
|
|
760
|
-
await startDaemonWatchFromSource(assistantIndex);
|
|
809
|
+
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
761
810
|
} else {
|
|
762
|
-
await startDaemonFromSource(assistantIndex);
|
|
811
|
+
await startDaemonFromSource(assistantIndex, resources);
|
|
763
812
|
}
|
|
764
813
|
socketReady = await waitForSocketFile(socketFile, 60000);
|
|
765
814
|
}
|
|
@@ -782,12 +831,13 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
782
831
|
" Ensure the daemon binary is bundled alongside the CLI, or run from the source tree.",
|
|
783
832
|
);
|
|
784
833
|
}
|
|
834
|
+
const defaults = defaultLocalResources();
|
|
835
|
+
const res = resources ?? defaults;
|
|
836
|
+
|
|
785
837
|
if (watch) {
|
|
786
|
-
await startDaemonWatchFromSource(assistantIndex);
|
|
838
|
+
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
787
839
|
|
|
788
|
-
const
|
|
789
|
-
const socketFile = join(vellumDir, "vellum.sock");
|
|
790
|
-
const socketReady = await waitForSocketFile(socketFile, 60000);
|
|
840
|
+
const socketReady = await waitForSocketFile(res.socketPath, 60000);
|
|
791
841
|
if (socketReady) {
|
|
792
842
|
console.log(" Assistant socket ready\n");
|
|
793
843
|
} else {
|
|
@@ -796,11 +846,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
796
846
|
);
|
|
797
847
|
}
|
|
798
848
|
} else {
|
|
799
|
-
await startDaemonFromSource(assistantIndex);
|
|
849
|
+
await startDaemonFromSource(assistantIndex, resources);
|
|
800
850
|
|
|
801
|
-
const
|
|
802
|
-
const socketFile = join(vellumDir, "vellum.sock");
|
|
803
|
-
const socketReady = await waitForSocketFile(socketFile, 60000);
|
|
851
|
+
const socketReady = await waitForSocketFile(res.socketPath, 60000);
|
|
804
852
|
if (socketReady) {
|
|
805
853
|
console.log(" Assistant socket ready\n");
|
|
806
854
|
} else {
|
|
@@ -815,8 +863,11 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
815
863
|
export async function startGateway(
|
|
816
864
|
assistantId?: string,
|
|
817
865
|
watch: boolean = false,
|
|
866
|
+
resources?: LocalInstanceResources,
|
|
818
867
|
): Promise<string> {
|
|
819
|
-
const
|
|
868
|
+
const effectiveGatewayPort = resources?.gatewayPort ?? GATEWAY_PORT;
|
|
869
|
+
|
|
870
|
+
const publicUrl = await discoverPublicUrl(effectiveGatewayPort);
|
|
820
871
|
if (publicUrl) {
|
|
821
872
|
console.log(` Public URL: ${publicUrl}`);
|
|
822
873
|
}
|
|
@@ -830,16 +881,91 @@ export async function startGateway(
|
|
|
830
881
|
process.env.GATEWAY_DEFAULT_ASSISTANT_ID ||
|
|
831
882
|
loadLatestAssistant()?.assistantId;
|
|
832
883
|
|
|
884
|
+
// Read the bearer token so the gateway can authenticate proxied requests
|
|
885
|
+
// (e.g. from paired iOS devices). Respect VELLUM_HTTP_TOKEN_PATH and
|
|
886
|
+
// BASE_DATA_DIR for consistency with gateway/config.ts and the daemon.
|
|
887
|
+
// When resources are provided, the token lives under the instance directory.
|
|
888
|
+
const httpTokenPath =
|
|
889
|
+
process.env.VELLUM_HTTP_TOKEN_PATH ??
|
|
890
|
+
(resources
|
|
891
|
+
? join(resources.instanceDir, ".vellum", "http-token")
|
|
892
|
+
: join(
|
|
893
|
+
process.env.BASE_DATA_DIR?.trim() || homedir(),
|
|
894
|
+
".vellum",
|
|
895
|
+
"http-token",
|
|
896
|
+
));
|
|
897
|
+
let runtimeProxyBearerToken: string | undefined;
|
|
898
|
+
try {
|
|
899
|
+
const tok = readFileSync(httpTokenPath, "utf-8").trim();
|
|
900
|
+
if (tok) runtimeProxyBearerToken = tok;
|
|
901
|
+
} catch {
|
|
902
|
+
// Token file doesn't exist yet — daemon hasn't written it.
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// If no token is available (first startup — daemon hasn't written it yet),
|
|
906
|
+
// poll for the file to appear. On fresh installs the daemon may take 60s+
|
|
907
|
+
// for Qdrant download, migrations, and first-time init. Starting the
|
|
908
|
+
// gateway without auth is a security risk since the config is loaded once
|
|
909
|
+
// at startup and never reloads, so we fail rather than silently disabling auth.
|
|
910
|
+
if (!runtimeProxyBearerToken) {
|
|
911
|
+
console.log(" Waiting for bearer token file...");
|
|
912
|
+
const maxWait = 60000;
|
|
913
|
+
const pollInterval = 500;
|
|
914
|
+
const start = Date.now();
|
|
915
|
+
const pidFile =
|
|
916
|
+
resources?.pidFile ??
|
|
917
|
+
join(
|
|
918
|
+
process.env.BASE_DATA_DIR?.trim() || homedir(),
|
|
919
|
+
".vellum",
|
|
920
|
+
"vellum.pid",
|
|
921
|
+
);
|
|
922
|
+
while (Date.now() - start < maxWait) {
|
|
923
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
924
|
+
try {
|
|
925
|
+
const tok = readFileSync(httpTokenPath, "utf-8").trim();
|
|
926
|
+
if (tok) {
|
|
927
|
+
runtimeProxyBearerToken = tok;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
} catch {
|
|
931
|
+
// File still doesn't exist, keep polling.
|
|
932
|
+
}
|
|
933
|
+
// Check if the daemon process is still alive — no point waiting if it crashed
|
|
934
|
+
try {
|
|
935
|
+
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
936
|
+
if (pid) process.kill(pid, 0); // throws if process doesn't exist
|
|
937
|
+
} catch {
|
|
938
|
+
break; // daemon process is gone
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!runtimeProxyBearerToken) {
|
|
944
|
+
throw new Error(
|
|
945
|
+
`Bearer token file not found at ${httpTokenPath} after 60s.\n` +
|
|
946
|
+
" The gateway cannot start without authentication — this would leave the proxy permanently unauthenticated.\n" +
|
|
947
|
+
" Ensure the daemon is running and has written the token file, or set VELLUM_HTTP_TOKEN_PATH to the correct path.",
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const effectiveDaemonPort =
|
|
951
|
+
resources?.daemonPort ?? Number(process.env.RUNTIME_HTTP_PORT || "7821");
|
|
952
|
+
|
|
833
953
|
const gatewayEnv: Record<string, string> = {
|
|
834
954
|
...(process.env as Record<string, string>),
|
|
835
955
|
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
836
956
|
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "true",
|
|
837
|
-
|
|
957
|
+
RUNTIME_PROXY_BEARER_TOKEN: runtimeProxyBearerToken,
|
|
958
|
+
RUNTIME_HTTP_PORT: String(effectiveDaemonPort),
|
|
959
|
+
GATEWAY_PORT: String(effectiveGatewayPort),
|
|
838
960
|
// Skip the drain window for locally-launched gateways — there is no load
|
|
839
961
|
// balancer draining connections, so waiting serves no purpose and causes
|
|
840
962
|
// `vellum sleep` to SIGKILL the gateway when the CLI timeout is shorter
|
|
841
963
|
// than the drain window. Respect an explicit env override.
|
|
842
964
|
GATEWAY_SHUTDOWN_DRAIN_MS: process.env.GATEWAY_SHUTDOWN_DRAIN_MS || "0",
|
|
965
|
+
...(watch ? { VELLUM_DEV: "1" } : {}),
|
|
966
|
+
// Set BASE_DATA_DIR so the gateway loads the correct signing key and
|
|
967
|
+
// credentials for this instance (mirrors the daemon env setup).
|
|
968
|
+
...(resources ? { BASE_DATA_DIR: resources.instanceDir } : {}),
|
|
843
969
|
};
|
|
844
970
|
|
|
845
971
|
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
|
@@ -851,7 +977,9 @@ export async function startGateway(
|
|
|
851
977
|
if (resolvedAssistantId) {
|
|
852
978
|
gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID = resolvedAssistantId;
|
|
853
979
|
}
|
|
854
|
-
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(
|
|
980
|
+
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(
|
|
981
|
+
resources?.instanceDir,
|
|
982
|
+
);
|
|
855
983
|
const ingressPublicBaseUrl =
|
|
856
984
|
workspaceIngressPublicBaseUrl ??
|
|
857
985
|
normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL) ??
|
|
@@ -906,11 +1034,13 @@ export async function startGateway(
|
|
|
906
1034
|
gateway.unref();
|
|
907
1035
|
|
|
908
1036
|
if (gateway.pid) {
|
|
909
|
-
const
|
|
910
|
-
|
|
1037
|
+
const gwPidDir = resources
|
|
1038
|
+
? join(resources.instanceDir, ".vellum")
|
|
1039
|
+
: join(homedir(), ".vellum");
|
|
1040
|
+
writeFileSync(join(gwPidDir, "gateway.pid"), String(gateway.pid), "utf-8");
|
|
911
1041
|
}
|
|
912
1042
|
|
|
913
|
-
const gatewayUrl = publicUrl || `http://localhost:${
|
|
1043
|
+
const gatewayUrl = publicUrl || `http://localhost:${effectiveGatewayPort}`;
|
|
914
1044
|
|
|
915
1045
|
// Wait for the gateway to be responsive before returning. Without this,
|
|
916
1046
|
// callers (e.g. displayPairingQRCode) may try to connect before the HTTP
|
|
@@ -920,9 +1050,12 @@ export async function startGateway(
|
|
|
920
1050
|
let ready = false;
|
|
921
1051
|
while (Date.now() - start < timeoutMs) {
|
|
922
1052
|
try {
|
|
923
|
-
const res = await fetch(
|
|
924
|
-
|
|
925
|
-
|
|
1053
|
+
const res = await fetch(
|
|
1054
|
+
`http://localhost:${effectiveGatewayPort}/healthz`,
|
|
1055
|
+
{
|
|
1056
|
+
signal: AbortSignal.timeout(2000),
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
926
1059
|
if (res.ok) {
|
|
927
1060
|
ready = true;
|
|
928
1061
|
break;
|
|
@@ -944,14 +1077,21 @@ export async function startGateway(
|
|
|
944
1077
|
}
|
|
945
1078
|
|
|
946
1079
|
/**
|
|
947
|
-
* Stop any locally-running daemon and gateway processes
|
|
948
|
-
* PID/socket files. Called when hatch fails partway through
|
|
949
|
-
* leave orphaned processes with no lock file entry.
|
|
1080
|
+
* Stop any locally-running daemon and gateway processes
|
|
1081
|
+
* and clean up PID/socket files. Called when hatch fails partway through
|
|
1082
|
+
* so we don't leave orphaned processes with no lock file entry.
|
|
1083
|
+
*
|
|
1084
|
+
* When `resources` is provided, uses instance-specific paths instead of
|
|
1085
|
+
* the default ~/.vellum/ paths.
|
|
950
1086
|
*/
|
|
951
|
-
export async function stopLocalProcesses(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const
|
|
1087
|
+
export async function stopLocalProcesses(
|
|
1088
|
+
resources?: LocalInstanceResources,
|
|
1089
|
+
): Promise<void> {
|
|
1090
|
+
const vellumDir = resources
|
|
1091
|
+
? join(resources.instanceDir, ".vellum")
|
|
1092
|
+
: join(homedir(), ".vellum");
|
|
1093
|
+
const daemonPidFile = resources?.pidFile ?? join(vellumDir, "vellum.pid");
|
|
1094
|
+
const socketFile = resources?.socketPath ?? join(vellumDir, "vellum.sock");
|
|
955
1095
|
await stopProcessByPidFile(daemonPidFile, "daemon", [socketFile]);
|
|
956
1096
|
|
|
957
1097
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|