@vellumai/cli 0.4.37 → 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 +1 -1
- package/src/__tests__/multi-local.test.ts +275 -0
- package/src/__tests__/skills-uninstall.test.ts +3 -1
- 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/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 +187 -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.
|
|
@@ -344,6 +364,14 @@ async function startDaemonWatchFromSource(
|
|
|
344
364
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
345
365
|
VELLUM_DEV: "1",
|
|
346
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
|
+
}
|
|
347
375
|
|
|
348
376
|
const daemonLogFd = openLogFile("hatch.log");
|
|
349
377
|
const child = spawn("bun", ["--watch", "run", mainPath], {
|
|
@@ -406,9 +434,12 @@ function normalizeIngressUrl(value: unknown): string | undefined {
|
|
|
406
434
|
return normalized || undefined;
|
|
407
435
|
}
|
|
408
436
|
|
|
409
|
-
function readWorkspaceIngressPublicBaseUrl(
|
|
437
|
+
function readWorkspaceIngressPublicBaseUrl(
|
|
438
|
+
instanceDir?: string,
|
|
439
|
+
): string | undefined {
|
|
410
440
|
const baseDataDir =
|
|
411
|
-
|
|
441
|
+
instanceDir ??
|
|
442
|
+
(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir()));
|
|
412
443
|
const workspaceConfigPath = join(
|
|
413
444
|
baseDataDir,
|
|
414
445
|
".vellum",
|
|
@@ -478,7 +509,8 @@ function isSocketResponsive(
|
|
|
478
509
|
});
|
|
479
510
|
}
|
|
480
511
|
|
|
481
|
-
async function discoverPublicUrl(): Promise<string | undefined> {
|
|
512
|
+
async function discoverPublicUrl(port?: number): Promise<string | undefined> {
|
|
513
|
+
const effectivePort = port ?? GATEWAY_PORT;
|
|
482
514
|
const cloud = process.env.VELLUM_CLOUD;
|
|
483
515
|
|
|
484
516
|
let externalIp: string | undefined;
|
|
@@ -516,7 +548,7 @@ async function discoverPublicUrl(): Promise<string | undefined> {
|
|
|
516
548
|
|
|
517
549
|
if (externalIp) {
|
|
518
550
|
console.log(` Discovered external IP: ${externalIp}`);
|
|
519
|
-
return `http://${externalIp}:${
|
|
551
|
+
return `http://${externalIp}:${effectivePort}`;
|
|
520
552
|
}
|
|
521
553
|
}
|
|
522
554
|
|
|
@@ -527,18 +559,18 @@ async function discoverPublicUrl(): Promise<string | undefined> {
|
|
|
527
559
|
const localHostname = getMacLocalHostname();
|
|
528
560
|
if (localHostname) {
|
|
529
561
|
console.log(` Discovered macOS local hostname: ${localHostname}`);
|
|
530
|
-
return `http://${localHostname}:${
|
|
562
|
+
return `http://${localHostname}:${effectivePort}`;
|
|
531
563
|
}
|
|
532
564
|
}
|
|
533
565
|
|
|
534
566
|
const lanIp = getLocalLanIPv4();
|
|
535
567
|
if (lanIp) {
|
|
536
568
|
console.log(` Discovered LAN IP: ${lanIp}`);
|
|
537
|
-
return `http://${lanIp}:${
|
|
569
|
+
return `http://${lanIp}:${effectivePort}`;
|
|
538
570
|
}
|
|
539
571
|
|
|
540
572
|
// Final fallback to localhost when no LAN address could be discovered.
|
|
541
|
-
return `http://localhost:${
|
|
573
|
+
return `http://localhost:${effectivePort}`;
|
|
542
574
|
}
|
|
543
575
|
|
|
544
576
|
/**
|
|
@@ -607,7 +639,10 @@ function getLocalLanIPv4(): string | undefined {
|
|
|
607
639
|
// It should eventually converge with
|
|
608
640
|
// assistant/src/daemon/daemon-control.ts::startDaemon which is the
|
|
609
641
|
// assistant-side equivalent.
|
|
610
|
-
export async function startLocalDaemon(
|
|
642
|
+
export async function startLocalDaemon(
|
|
643
|
+
watch: boolean = false,
|
|
644
|
+
resources?: LocalInstanceResources,
|
|
645
|
+
): Promise<void> {
|
|
611
646
|
if (process.env.VELLUM_DESKTOP_APP && !watch) {
|
|
612
647
|
// When running inside the desktop app, the CLI owns the daemon lifecycle.
|
|
613
648
|
// Find the vellum-daemon binary adjacent to the CLI binary.
|
|
@@ -621,9 +656,10 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
621
656
|
);
|
|
622
657
|
}
|
|
623
658
|
|
|
624
|
-
const
|
|
625
|
-
const
|
|
626
|
-
const
|
|
659
|
+
const defaults = defaultLocalResources();
|
|
660
|
+
const res = resources ?? defaults;
|
|
661
|
+
const pidFile = res.pidFile;
|
|
662
|
+
const socketFile = res.socketPath;
|
|
627
663
|
|
|
628
664
|
// If a daemon is already running, skip spawning a new one.
|
|
629
665
|
// This prevents cascading kill→restart cycles when multiple callers
|
|
@@ -679,8 +715,8 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
679
715
|
// Ensure bun is available for runtime features (browser, skills install)
|
|
680
716
|
ensureBunInstalled();
|
|
681
717
|
|
|
682
|
-
// Ensure
|
|
683
|
-
mkdirSync(
|
|
718
|
+
// Ensure the directory containing PID/socket files exists
|
|
719
|
+
mkdirSync(dirname(pidFile), { recursive: true });
|
|
684
720
|
|
|
685
721
|
// Build a minimal environment for the daemon. When launched from the
|
|
686
722
|
// macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
|
|
@@ -698,6 +734,8 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
698
734
|
for (const key of [
|
|
699
735
|
"ANTHROPIC_API_KEY",
|
|
700
736
|
"BASE_DATA_DIR",
|
|
737
|
+
"QDRANT_HTTP_PORT",
|
|
738
|
+
"QDRANT_URL",
|
|
701
739
|
"RUNTIME_HTTP_PORT",
|
|
702
740
|
"VELLUM_DAEMON_TCP_PORT",
|
|
703
741
|
"VELLUM_DAEMON_TCP_HOST",
|
|
@@ -713,6 +751,16 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
713
751
|
daemonEnv[key] = process.env[key]!;
|
|
714
752
|
}
|
|
715
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
|
+
}
|
|
716
764
|
|
|
717
765
|
// Use fd inheritance instead of pipes so the daemon's stdout/stderr
|
|
718
766
|
// survive after the parent (hatch) exits. Bun does not ignore SIGPIPE,
|
|
@@ -758,9 +806,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
758
806
|
// Kill the bundled daemon to avoid two processes competing for the same socket/port
|
|
759
807
|
await stopProcessByPidFile(pidFile, "bundled daemon", [socketFile]);
|
|
760
808
|
if (watch) {
|
|
761
|
-
await startDaemonWatchFromSource(assistantIndex);
|
|
809
|
+
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
762
810
|
} else {
|
|
763
|
-
await startDaemonFromSource(assistantIndex);
|
|
811
|
+
await startDaemonFromSource(assistantIndex, resources);
|
|
764
812
|
}
|
|
765
813
|
socketReady = await waitForSocketFile(socketFile, 60000);
|
|
766
814
|
}
|
|
@@ -783,12 +831,13 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
783
831
|
" Ensure the daemon binary is bundled alongside the CLI, or run from the source tree.",
|
|
784
832
|
);
|
|
785
833
|
}
|
|
834
|
+
const defaults = defaultLocalResources();
|
|
835
|
+
const res = resources ?? defaults;
|
|
836
|
+
|
|
786
837
|
if (watch) {
|
|
787
|
-
await startDaemonWatchFromSource(assistantIndex);
|
|
838
|
+
await startDaemonWatchFromSource(assistantIndex, resources);
|
|
788
839
|
|
|
789
|
-
const
|
|
790
|
-
const socketFile = join(vellumDir, "vellum.sock");
|
|
791
|
-
const socketReady = await waitForSocketFile(socketFile, 60000);
|
|
840
|
+
const socketReady = await waitForSocketFile(res.socketPath, 60000);
|
|
792
841
|
if (socketReady) {
|
|
793
842
|
console.log(" Assistant socket ready\n");
|
|
794
843
|
} else {
|
|
@@ -797,11 +846,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
797
846
|
);
|
|
798
847
|
}
|
|
799
848
|
} else {
|
|
800
|
-
await startDaemonFromSource(assistantIndex);
|
|
849
|
+
await startDaemonFromSource(assistantIndex, resources);
|
|
801
850
|
|
|
802
|
-
const
|
|
803
|
-
const socketFile = join(vellumDir, "vellum.sock");
|
|
804
|
-
const socketReady = await waitForSocketFile(socketFile, 60000);
|
|
851
|
+
const socketReady = await waitForSocketFile(res.socketPath, 60000);
|
|
805
852
|
if (socketReady) {
|
|
806
853
|
console.log(" Assistant socket ready\n");
|
|
807
854
|
} else {
|
|
@@ -816,8 +863,11 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
816
863
|
export async function startGateway(
|
|
817
864
|
assistantId?: string,
|
|
818
865
|
watch: boolean = false,
|
|
866
|
+
resources?: LocalInstanceResources,
|
|
819
867
|
): Promise<string> {
|
|
820
|
-
const
|
|
868
|
+
const effectiveGatewayPort = resources?.gatewayPort ?? GATEWAY_PORT;
|
|
869
|
+
|
|
870
|
+
const publicUrl = await discoverPublicUrl(effectiveGatewayPort);
|
|
821
871
|
if (publicUrl) {
|
|
822
872
|
console.log(` Public URL: ${publicUrl}`);
|
|
823
873
|
}
|
|
@@ -831,17 +881,91 @@ export async function startGateway(
|
|
|
831
881
|
process.env.GATEWAY_DEFAULT_ASSISTANT_ID ||
|
|
832
882
|
loadLatestAssistant()?.assistantId;
|
|
833
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
|
+
|
|
834
953
|
const gatewayEnv: Record<string, string> = {
|
|
835
954
|
...(process.env as Record<string, string>),
|
|
836
955
|
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
837
956
|
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "true",
|
|
838
|
-
|
|
957
|
+
RUNTIME_PROXY_BEARER_TOKEN: runtimeProxyBearerToken,
|
|
958
|
+
RUNTIME_HTTP_PORT: String(effectiveDaemonPort),
|
|
959
|
+
GATEWAY_PORT: String(effectiveGatewayPort),
|
|
839
960
|
// Skip the drain window for locally-launched gateways — there is no load
|
|
840
961
|
// balancer draining connections, so waiting serves no purpose and causes
|
|
841
962
|
// `vellum sleep` to SIGKILL the gateway when the CLI timeout is shorter
|
|
842
963
|
// than the drain window. Respect an explicit env override.
|
|
843
964
|
GATEWAY_SHUTDOWN_DRAIN_MS: process.env.GATEWAY_SHUTDOWN_DRAIN_MS || "0",
|
|
844
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 } : {}),
|
|
845
969
|
};
|
|
846
970
|
|
|
847
971
|
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
|
@@ -853,7 +977,9 @@ export async function startGateway(
|
|
|
853
977
|
if (resolvedAssistantId) {
|
|
854
978
|
gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID = resolvedAssistantId;
|
|
855
979
|
}
|
|
856
|
-
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(
|
|
980
|
+
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl(
|
|
981
|
+
resources?.instanceDir,
|
|
982
|
+
);
|
|
857
983
|
const ingressPublicBaseUrl =
|
|
858
984
|
workspaceIngressPublicBaseUrl ??
|
|
859
985
|
normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL) ??
|
|
@@ -908,11 +1034,13 @@ export async function startGateway(
|
|
|
908
1034
|
gateway.unref();
|
|
909
1035
|
|
|
910
1036
|
if (gateway.pid) {
|
|
911
|
-
const
|
|
912
|
-
|
|
1037
|
+
const gwPidDir = resources
|
|
1038
|
+
? join(resources.instanceDir, ".vellum")
|
|
1039
|
+
: join(homedir(), ".vellum");
|
|
1040
|
+
writeFileSync(join(gwPidDir, "gateway.pid"), String(gateway.pid), "utf-8");
|
|
913
1041
|
}
|
|
914
1042
|
|
|
915
|
-
const gatewayUrl = publicUrl || `http://localhost:${
|
|
1043
|
+
const gatewayUrl = publicUrl || `http://localhost:${effectiveGatewayPort}`;
|
|
916
1044
|
|
|
917
1045
|
// Wait for the gateway to be responsive before returning. Without this,
|
|
918
1046
|
// callers (e.g. displayPairingQRCode) may try to connect before the HTTP
|
|
@@ -922,9 +1050,12 @@ export async function startGateway(
|
|
|
922
1050
|
let ready = false;
|
|
923
1051
|
while (Date.now() - start < timeoutMs) {
|
|
924
1052
|
try {
|
|
925
|
-
const res = await fetch(
|
|
926
|
-
|
|
927
|
-
|
|
1053
|
+
const res = await fetch(
|
|
1054
|
+
`http://localhost:${effectiveGatewayPort}/healthz`,
|
|
1055
|
+
{
|
|
1056
|
+
signal: AbortSignal.timeout(2000),
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
928
1059
|
if (res.ok) {
|
|
929
1060
|
ready = true;
|
|
930
1061
|
break;
|
|
@@ -946,14 +1077,21 @@ export async function startGateway(
|
|
|
946
1077
|
}
|
|
947
1078
|
|
|
948
1079
|
/**
|
|
949
|
-
* Stop any locally-running daemon and gateway processes
|
|
950
|
-
* PID/socket files. Called when hatch fails partway through
|
|
951
|
-
* 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.
|
|
952
1086
|
*/
|
|
953
|
-
export async function stopLocalProcesses(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
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");
|
|
957
1095
|
await stopProcessByPidFile(daemonPidFile, "daemon", [socketFile]);
|
|
958
1096
|
|
|
959
1097
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|