@vellumai/cli 0.8.7 → 0.8.8
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/node_modules/@vellumai/local-mode/package.json +2 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +15 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +15 -8
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +42 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +22 -4
- package/node_modules/@vellumai/local-mode/src/index.ts +26 -4
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +9 -7
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +115 -26
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +16 -0
- package/src/commands/retire.ts +20 -47
- package/src/commands/sleep.ts +7 -0
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/wake.ts +7 -0
- package/src/components/DefaultMainScreen.tsx +84 -13
- package/src/index.ts +16 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +12 -0
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +25 -1
- package/src/lib/environments/resolve.ts +9 -30
- package/src/lib/guardian-token.ts +120 -4
- package/src/lib/local.ts +20 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interactive confirmation for destructive CLI commands (retire, unpair,
|
|
3
|
+
* …). Per cli/AGENTS.md, a command that removes assistant state must print the
|
|
4
|
+
* resolved identity and require confirmation, with a `--yes` bypass for
|
|
5
|
+
* automation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** True only when we can run an interactive raw-mode confirmation prompt. */
|
|
9
|
+
export function canPromptForConfirmation(): boolean {
|
|
10
|
+
return (
|
|
11
|
+
process.stdin.isTTY === true &&
|
|
12
|
+
process.stdout.isTTY === true &&
|
|
13
|
+
typeof process.stdin.setRawMode === "function"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show `prompt` and resolve true on Enter, false on Esc/q/Ctrl-C. Restores the
|
|
19
|
+
* prior stdin raw/paused state on exit. Caller must gate on
|
|
20
|
+
* {@link canPromptForConfirmation} first.
|
|
21
|
+
*/
|
|
22
|
+
export async function confirmAction(prompt: string): Promise<boolean> {
|
|
23
|
+
const stdin = process.stdin;
|
|
24
|
+
const stdout = process.stdout;
|
|
25
|
+
const wasRaw = stdin.isRaw === true;
|
|
26
|
+
const wasPaused = stdin.isPaused();
|
|
27
|
+
|
|
28
|
+
stdout.write(prompt);
|
|
29
|
+
stdin.setRawMode(true);
|
|
30
|
+
stdin.resume();
|
|
31
|
+
|
|
32
|
+
return await new Promise<boolean>((resolve) => {
|
|
33
|
+
const cleanup = () => {
|
|
34
|
+
stdin.off("data", onData);
|
|
35
|
+
stdin.setRawMode(wasRaw);
|
|
36
|
+
if (wasPaused) {
|
|
37
|
+
stdin.pause();
|
|
38
|
+
}
|
|
39
|
+
stdout.write("\n");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const onData = (chunk: Buffer) => {
|
|
43
|
+
const byte = chunk[0];
|
|
44
|
+
if (byte === 13 || byte === 10) {
|
|
45
|
+
cleanup();
|
|
46
|
+
resolve(true);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
|
|
50
|
+
cleanup();
|
|
51
|
+
resolve(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
stdin.on("data", onData);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/src/lib/docker.ts
CHANGED
|
@@ -478,6 +478,17 @@ export async function retireDocker(name: string): Promise<void> {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
// Future: consider stopping Colima VM when no Docker instances remain.
|
|
482
|
+
// Considerations:
|
|
483
|
+
// - Use loadAllAssistantsAcrossEnvs() instead of loadAllAssistants() to
|
|
484
|
+
// avoid stopping Colima while another VELLUM_ENVIRONMENT still has a
|
|
485
|
+
// running Docker instance.
|
|
486
|
+
// - Track whether Vellum started Colima (vs. the user already had it
|
|
487
|
+
// running for non-Vellum workloads) \u2014 e.g. via a dedicated Colima
|
|
488
|
+
// profile (`colima start --profile vellum`) or a sentinel file.
|
|
489
|
+
// - Only stop if both conditions are met: no cross-env Docker instances
|
|
490
|
+
// AND Vellum owns the Colima lifecycle.
|
|
491
|
+
|
|
481
492
|
console.log(`\u2705 Docker instance retired.`);
|
|
482
493
|
}
|
|
483
494
|
|
|
@@ -1137,7 +1148,20 @@ export async function hatchDocker(
|
|
|
1137
1148
|
await loadImageViaHost(HOST_IMAGE_LOADER_URL, ref, log);
|
|
1138
1149
|
} else {
|
|
1139
1150
|
log(` ↪ pulling ${ref}`);
|
|
1140
|
-
|
|
1151
|
+
const MAX_PULL_RETRIES = 3;
|
|
1152
|
+
for (let attempt = 1; attempt <= MAX_PULL_RETRIES; attempt++) {
|
|
1153
|
+
try {
|
|
1154
|
+
await exec("docker", ["pull", ref]);
|
|
1155
|
+
break;
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
if (attempt === MAX_PULL_RETRIES) throw err;
|
|
1158
|
+
const delaySec = 2 ** attempt;
|
|
1159
|
+
log(
|
|
1160
|
+
` ⚠ pull failed (attempt ${attempt}/${MAX_PULL_RETRIES}), retrying in ${delaySec}s...`,
|
|
1161
|
+
);
|
|
1162
|
+
await new Promise((r) => setTimeout(r, delaySec * 1000));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1141
1165
|
}
|
|
1142
1166
|
}
|
|
1143
1167
|
log("✅ Docker images acquired");
|
|
@@ -1,48 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
mkdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
unlinkSync,
|
|
6
|
-
writeFileSync,
|
|
7
|
-
} from "fs";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { dirname, join } from "path";
|
|
1
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
10
3
|
|
|
11
4
|
import { SEEDS, type EnvironmentDefinition } from "@vellumai/environments";
|
|
5
|
+
import {
|
|
6
|
+
defaultEnvironmentFilePath,
|
|
7
|
+
readDefaultEnvironment as readPersistedDefaultEnvironment,
|
|
8
|
+
} from "@vellumai/local-mode";
|
|
12
9
|
|
|
13
10
|
const DEFAULT_ENVIRONMENT_NAME = "production";
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Path to the user's persisted default environment file.
|
|
17
|
-
* Lives at `~/.config/vellum/environment` — a fixed, environment-agnostic
|
|
18
|
-
* location so it can be read before the environment is resolved.
|
|
19
|
-
*/
|
|
20
|
-
function getDefaultEnvironmentPath(): string {
|
|
21
|
-
const xdgConfig =
|
|
22
|
-
process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
23
|
-
return join(xdgConfig, "vellum", "environment");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
/**
|
|
27
13
|
* Read the persisted default environment name, if any.
|
|
28
14
|
* Returns `undefined` if no file exists or the file is empty.
|
|
29
15
|
*/
|
|
30
16
|
export function readDefaultEnvironment(): string | undefined {
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
if (!existsSync(filePath)) return undefined;
|
|
34
|
-
const content = readFileSync(filePath, "utf-8").trim();
|
|
35
|
-
return content.length > 0 ? content : undefined;
|
|
36
|
-
} catch {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
17
|
+
return readPersistedDefaultEnvironment(process.env);
|
|
39
18
|
}
|
|
40
19
|
|
|
41
20
|
/**
|
|
42
21
|
* Persist a default environment name to the user config file.
|
|
43
22
|
*/
|
|
44
23
|
export function writeDefaultEnvironment(name: string): void {
|
|
45
|
-
const filePath =
|
|
24
|
+
const filePath = defaultEnvironmentFilePath(process.env);
|
|
46
25
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
47
26
|
writeFileSync(filePath, name + "\n", "utf-8");
|
|
48
27
|
}
|
|
@@ -51,7 +30,7 @@ export function writeDefaultEnvironment(name: string): void {
|
|
|
51
30
|
* Remove the persisted default environment file, falling back to production.
|
|
52
31
|
*/
|
|
53
32
|
export function clearDefaultEnvironment(): void {
|
|
54
|
-
const filePath =
|
|
33
|
+
const filePath = defaultEnvironmentFilePath(process.env);
|
|
55
34
|
try {
|
|
56
35
|
unlinkSync(filePath);
|
|
57
36
|
} catch {
|
|
@@ -2,11 +2,16 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import {
|
|
4
4
|
chmodSync,
|
|
5
|
+
closeSync,
|
|
5
6
|
existsSync,
|
|
6
7
|
mkdirSync,
|
|
8
|
+
openSync,
|
|
7
9
|
readFileSync,
|
|
10
|
+
rmdirSync,
|
|
8
11
|
statSync,
|
|
12
|
+
unlinkSync,
|
|
9
13
|
writeFileSync,
|
|
14
|
+
writeSync,
|
|
10
15
|
} from "fs";
|
|
11
16
|
import { platform } from "os";
|
|
12
17
|
import { dirname, join } from "path";
|
|
@@ -41,6 +46,27 @@ function getGuardianTokenPath(assistantId: string): string {
|
|
|
41
46
|
);
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Best-effort removal of an assistant's stored guardian token (used by
|
|
51
|
+
* `vellum unpair` to forget a paired connection). Never throws if the token
|
|
52
|
+
* file or its per-assistant directory is already absent.
|
|
53
|
+
*/
|
|
54
|
+
export function deleteGuardianToken(assistantId: string): void {
|
|
55
|
+
const tokenPath = getGuardianTokenPath(assistantId);
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(tokenPath);
|
|
58
|
+
} catch {
|
|
59
|
+
/* already gone */
|
|
60
|
+
}
|
|
61
|
+
// Clean up the now-empty per-assistant directory; rmdir throws if it still
|
|
62
|
+
// holds other files, in which case we leave it.
|
|
63
|
+
try {
|
|
64
|
+
rmdirSync(dirname(tokenPath));
|
|
65
|
+
} catch {
|
|
66
|
+
/* not empty or absent */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
function getPersistedDeviceIdPath(): string {
|
|
45
71
|
return join(getConfigDir(getCurrentEnvironment()), "device-id");
|
|
46
72
|
}
|
|
@@ -161,33 +187,121 @@ export function saveGuardianToken(
|
|
|
161
187
|
chmodSync(tokenPath, 0o600);
|
|
162
188
|
}
|
|
163
189
|
|
|
190
|
+
/** Abort the refresh POST if the gateway is slow/unreachable (it's now on the
|
|
191
|
+
* hot request path, so it must never hang indefinitely). */
|
|
192
|
+
const REFRESH_FETCH_TIMEOUT_MS = 15_000;
|
|
193
|
+
/** Max time to wait for the per-assistant refresh lock before proceeding. */
|
|
194
|
+
const REFRESH_LOCK_WAIT_MS = 10_000;
|
|
195
|
+
/** A lock older than this is treated as stale (holder crashed) and stolen. */
|
|
196
|
+
const REFRESH_LOCK_STALE_MS = 30_000;
|
|
197
|
+
const REFRESH_LOCK_POLL_MS = 100;
|
|
198
|
+
|
|
199
|
+
function getRefreshLockPath(assistantId: string): string {
|
|
200
|
+
return join(dirname(getGuardianTokenPath(assistantId)), "refresh.lock");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Best-effort exclusive cross-process lock for a per-assistant token refresh.
|
|
207
|
+
* Created atomically with `wx`; a stale lock (crashed holder) is stolen.
|
|
208
|
+
* Returns true if acquired, false if it timed out (caller proceeds degraded).
|
|
209
|
+
*/
|
|
210
|
+
async function acquireRefreshLock(lockPath: string): Promise<boolean> {
|
|
211
|
+
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
212
|
+
const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
|
|
213
|
+
for (;;) {
|
|
214
|
+
try {
|
|
215
|
+
const fd = openSync(lockPath, "wx", 0o600);
|
|
216
|
+
writeSync(fd, String(process.pid));
|
|
217
|
+
closeSync(fd);
|
|
218
|
+
return true;
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") return false;
|
|
221
|
+
try {
|
|
222
|
+
if (Date.now() - statSync(lockPath).mtimeMs > REFRESH_LOCK_STALE_MS) {
|
|
223
|
+
unlinkSync(lockPath); // steal a stale lock, then retry
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
continue; // lock vanished between open and stat — retry
|
|
228
|
+
}
|
|
229
|
+
if (Date.now() >= deadline) return false;
|
|
230
|
+
await delay(REFRESH_LOCK_POLL_MS);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function releaseRefreshLock(lockPath: string): void {
|
|
236
|
+
try {
|
|
237
|
+
unlinkSync(lockPath);
|
|
238
|
+
} catch {
|
|
239
|
+
/* already released/stolen */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
164
243
|
/**
|
|
165
244
|
* Call POST /v1/guardian/refresh on the remote gateway to obtain a new
|
|
166
245
|
* access token using an existing (possibly expired) access token for auth.
|
|
167
246
|
* Returns the refreshed token data (persisted locally), or null if the
|
|
168
247
|
* refresh fails (e.g. no stored token, or refresh token itself is expired).
|
|
248
|
+
*
|
|
249
|
+
* Concurrency-safe: the gateway rotates refresh tokens and treats reuse of an
|
|
250
|
+
* already-rotated token as replay (revoking the whole token family), so two
|
|
251
|
+
* processes (e.g. `vellum message` + `vellum events`) refreshing the same
|
|
252
|
+
* stored token at once would self-revoke and force re-pairing. We serialize on
|
|
253
|
+
* a per-assistant lock and, once held, re-read the stored token: if another
|
|
254
|
+
* process already rotated it while we waited, we return that fresh token
|
|
255
|
+
* instead of replaying our now-stale refresh token.
|
|
169
256
|
*/
|
|
170
257
|
export async function refreshGuardianToken(
|
|
171
258
|
gatewayUrl: string,
|
|
172
259
|
assistantId: string,
|
|
173
260
|
): Promise<GuardianTokenData | null> {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
261
|
+
const before = loadGuardianToken(assistantId);
|
|
262
|
+
if (!before) return null;
|
|
176
263
|
|
|
177
264
|
// Gateway persists expiresAt as epoch-ms numbers; Date.parse("1234567890000")
|
|
178
265
|
// returns NaN. new Date() accepts both ISO strings and epoch-ms numbers.
|
|
179
|
-
const refreshExpiry = new Date(
|
|
266
|
+
const refreshExpiry = new Date(before.refreshTokenExpiresAt).getTime();
|
|
180
267
|
if (!Number.isFinite(refreshExpiry) || refreshExpiry <= Date.now())
|
|
181
268
|
return null;
|
|
182
269
|
|
|
270
|
+
const lockPath = getRefreshLockPath(assistantId);
|
|
271
|
+
const locked = await acquireRefreshLock(lockPath);
|
|
183
272
|
try {
|
|
273
|
+
// Re-read under the lock: a concurrent process may have rotated the token
|
|
274
|
+
// while we waited. If the stored refresh token changed, ours is now stale
|
|
275
|
+
// (replaying it would trip reuse-detection) — use the fresh token instead.
|
|
276
|
+
const current = loadGuardianToken(assistantId);
|
|
277
|
+
if (current && current.refreshToken !== before.refreshToken) {
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// We did NOT acquire the lock (another process is likely mid-refresh) and
|
|
282
|
+
// the stored token hasn't been rotated yet. Do NOT call the gateway: our
|
|
283
|
+
// refresh token may be the one the winner is rotating right now, and
|
|
284
|
+
// replaying a rotated token revokes the whole family (forcing re-pair).
|
|
285
|
+
// Give up — the caller surfaces the original 401, and the next attempt
|
|
286
|
+
// picks up the winner's persisted token.
|
|
287
|
+
if (!locked) return null;
|
|
288
|
+
|
|
289
|
+
const tokenData = current ?? before;
|
|
290
|
+
|
|
184
291
|
const response = await fetch(`${gatewayUrl}/v1/guardian/refresh`, {
|
|
185
292
|
method: "POST",
|
|
186
293
|
headers: {
|
|
187
294
|
"Content-Type": "application/json",
|
|
188
295
|
Authorization: `Bearer ${tokenData.accessToken}`,
|
|
189
296
|
},
|
|
190
|
-
body: JSON.stringify({
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
refreshToken: tokenData.refreshToken,
|
|
299
|
+
// The refresh token is device-bound; send the device id used at init
|
|
300
|
+
// (falling back to a fresh computation for tokens persisted before the
|
|
301
|
+
// field was stored) so the gateway can verify the binding.
|
|
302
|
+
deviceId: tokenData.deviceId || computeDeviceId(),
|
|
303
|
+
}),
|
|
304
|
+
signal: AbortSignal.timeout(REFRESH_FETCH_TIMEOUT_MS),
|
|
191
305
|
});
|
|
192
306
|
if (!response.ok) return null;
|
|
193
307
|
|
|
@@ -212,6 +326,8 @@ export async function refreshGuardianToken(
|
|
|
212
326
|
return refreshed;
|
|
213
327
|
} catch {
|
|
214
328
|
return null;
|
|
329
|
+
} finally {
|
|
330
|
+
if (locked) releaseRefreshLock(lockPath);
|
|
215
331
|
}
|
|
216
332
|
}
|
|
217
333
|
|
package/src/lib/local.ts
CHANGED
|
@@ -230,8 +230,10 @@ function resolveAssistantIndexPath(): string | undefined {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
try {
|
|
233
|
-
const
|
|
234
|
-
|
|
233
|
+
const assistantPkgPath = _require.resolve(
|
|
234
|
+
"@vellumai/assistant/package.json",
|
|
235
|
+
);
|
|
236
|
+
const resolved = join(dirname(assistantPkgPath), "src", "index.ts");
|
|
235
237
|
if (existsSync(resolved)) {
|
|
236
238
|
return resolved;
|
|
237
239
|
}
|
|
@@ -416,13 +418,13 @@ async function startDaemonFromSource(
|
|
|
416
418
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
417
419
|
|
|
418
420
|
const child = foreground
|
|
419
|
-
? spawn(
|
|
421
|
+
? spawn(process.execPath, ["run", daemonMainPath], {
|
|
420
422
|
stdio: "inherit",
|
|
421
423
|
env,
|
|
422
424
|
})
|
|
423
425
|
: (() => {
|
|
424
426
|
const daemonLogFd = openLogFile("hatch.log");
|
|
425
|
-
const c = spawn(
|
|
427
|
+
const c = spawn(process.execPath, ["run", daemonMainPath], {
|
|
426
428
|
detached: true,
|
|
427
429
|
stdio: ["ignore", "pipe", "pipe"],
|
|
428
430
|
env,
|
|
@@ -486,7 +488,7 @@ async function startDaemonWatchFromSource(
|
|
|
486
488
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
487
489
|
|
|
488
490
|
const daemonLogFd = openLogFile("hatch.log");
|
|
489
|
-
const child = spawn(
|
|
491
|
+
const child = spawn(process.execPath, ["--watch", "run", mainPath], {
|
|
490
492
|
detached: true,
|
|
491
493
|
stdio: ["ignore", "pipe", "pipe"],
|
|
492
494
|
env,
|
|
@@ -514,6 +516,18 @@ function resolveGatewayDir(): string {
|
|
|
514
516
|
return sourceDir;
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
// npm-installed: @vellumai/cli and @vellumai/vellum-gateway are siblings
|
|
520
|
+
const npmGatewayDir = join(
|
|
521
|
+
import.meta.dir,
|
|
522
|
+
"..",
|
|
523
|
+
"..",
|
|
524
|
+
"..",
|
|
525
|
+
"vellum-gateway",
|
|
526
|
+
);
|
|
527
|
+
if (isGatewaySourceDir(npmGatewayDir)) {
|
|
528
|
+
return npmGatewayDir;
|
|
529
|
+
}
|
|
530
|
+
|
|
517
531
|
// Compiled binary: gateway/ bundled adjacent to the CLI executable.
|
|
518
532
|
const binGateway = join(dirname(process.execPath), "gateway");
|
|
519
533
|
if (isGatewaySourceDir(binGateway)) {
|
|
@@ -1135,7 +1149,7 @@ export async function startGateway(
|
|
|
1135
1149
|
? ["--watch", "run", "src/index.ts", "--vellum-gateway"]
|
|
1136
1150
|
: ["run", "src/index.ts", "--vellum-gateway"];
|
|
1137
1151
|
const gwLogFd = openLogFile("hatch.log");
|
|
1138
|
-
gateway = spawn(
|
|
1152
|
+
gateway = spawn(process.execPath, bunArgs, {
|
|
1139
1153
|
cwd: gatewayDir,
|
|
1140
1154
|
detached: true,
|
|
1141
1155
|
stdio: ["ignore", "pipe", "pipe"],
|