@vellumai/cli 0.11.7 → 0.11.8-staging.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/AGENTS.md +1 -1
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +17 -2
- package/node_modules/@vellumai/local-mode/src/index.ts +5 -0
- package/node_modules/@vellumai/local-mode/src/lifecycle-budgets.ts +47 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +9 -3
- package/node_modules/@vellumai/local-mode/src/sleep.ts +5 -5
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/package.json +4 -3
- package/src/__tests__/assistant-client-platform-auth.test.ts +219 -0
- package/src/__tests__/clean.test.ts +1 -0
- package/src/__tests__/nginx-ingress.test.ts +123 -8
- package/src/__tests__/retire-local.test.ts +32 -1
- package/src/__tests__/sleep.test.ts +5 -3
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/__tests__/upgrade-local.test.ts +116 -0
- package/src/__tests__/windows-lifecycle.test.ts +77 -5
- package/src/adapters/openclaw-http-server.ts +5 -0
- package/src/commands/client.ts +35 -7
- package/src/commands/exec.ts +8 -2
- package/src/commands/logs.ts +11 -2
- package/src/commands/sleep.ts +6 -9
- package/src/commands/ssh.ts +6 -3
- package/src/commands/teleport.ts +6 -1
- package/src/commands/upgrade.ts +22 -12
- package/src/commands/voice.test.ts +81 -0
- package/src/commands/voice.ts +83 -6
- package/src/commands/wake.ts +7 -1
- package/src/lib/assistant-client.ts +145 -31
- package/src/lib/cloudflare-tunnel.ts +2 -1
- package/src/lib/detached-process.ts +1 -0
- package/src/lib/guardian-token.ts +4 -2
- package/src/lib/hatch-local.ts +5 -1
- package/src/lib/live-voice/client.ts +23 -2
- package/src/lib/live-voice/connection.ts +87 -10
- package/src/lib/live-voice/pcm-player.ts +16 -3
- package/src/lib/live-voice/velay.test.ts +99 -0
- package/src/lib/live-voice/velay.ts +199 -0
- package/src/lib/local-hatch-lock.ts +16 -3
- package/src/lib/local.ts +48 -13
- package/src/lib/nginx-ingress.ts +70 -10
- package/src/lib/ngrok.ts +2 -0
- package/src/lib/open-browser.ts +5 -1
- package/src/lib/process.ts +72 -10
- package/src/lib/provider-secrets.ts +5 -0
- package/src/lib/retire-local.ts +12 -5
- package/src/lib/step-runner.ts +3 -0
- package/src/lib/tailscale-tunnel.ts +6 -1
- package/src/lib/upgrade-lifecycle.ts +1 -0
|
@@ -26,6 +26,7 @@ const isProcessAliveMock = mock((): { alive: boolean; pid: number | null } => ({
|
|
|
26
26
|
const realProcess = { ...(await import("../lib/process.js")) };
|
|
27
27
|
|
|
28
28
|
mock.module("../lib/process.js", () => ({
|
|
29
|
+
...realProcess,
|
|
29
30
|
isProcessAlive: isProcessAliveMock,
|
|
30
31
|
stopProcessByPidFile: stopProcessByPidFileMock,
|
|
31
32
|
}));
|
|
@@ -222,14 +223,15 @@ describe("sleep command", () => {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(3);
|
|
225
|
-
// The assistant stop passes
|
|
226
|
-
// checkpoint completes before any SIGKILL (default 2s
|
|
226
|
+
// The assistant stop passes the daemon's own shutdown budget plus a margin,
|
|
227
|
+
// so its WAL checkpoint completes before any SIGKILL (the default 2s grace
|
|
228
|
+
// would truncate it).
|
|
227
229
|
expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
|
|
228
230
|
1,
|
|
229
231
|
join(assistantRootDir, "workspace", "vellum.pid"),
|
|
230
232
|
"assistant",
|
|
231
233
|
undefined,
|
|
232
|
-
|
|
234
|
+
45_000,
|
|
233
235
|
);
|
|
234
236
|
expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
|
|
235
237
|
2,
|
|
@@ -33,6 +33,8 @@ const realUpgradeLifecycle = { ...upgradeLifecycle };
|
|
|
33
33
|
|
|
34
34
|
const findAssistantByNameMock =
|
|
35
35
|
mock<typeof assistantConfig.findAssistantByName>();
|
|
36
|
+
const lookupAssistantByIdentifierMock =
|
|
37
|
+
mock<typeof assistantConfig.lookupAssistantByIdentifier>();
|
|
36
38
|
const getActiveAssistantMock = mock<typeof assistantConfig.getActiveAssistant>(
|
|
37
39
|
() => "local-assistant",
|
|
38
40
|
);
|
|
@@ -46,6 +48,7 @@ const saveAssistantEntryMock = mock<typeof assistantConfig.saveAssistantEntry>(
|
|
|
46
48
|
mock.module("../lib/assistant-config", () => ({
|
|
47
49
|
...realAssistantConfig,
|
|
48
50
|
findAssistantByName: findAssistantByNameMock,
|
|
51
|
+
lookupAssistantByIdentifier: lookupAssistantByIdentifierMock,
|
|
49
52
|
getActiveAssistant: getActiveAssistantMock,
|
|
50
53
|
loadAllAssistants: loadAllAssistantsMock,
|
|
51
54
|
saveAssistantEntry: saveAssistantEntryMock,
|
|
@@ -228,6 +231,8 @@ beforeEach(() => {
|
|
|
228
231
|
const entry = makeLocalEntry();
|
|
229
232
|
findAssistantByNameMock.mockReset();
|
|
230
233
|
findAssistantByNameMock.mockReturnValue(entry);
|
|
234
|
+
lookupAssistantByIdentifierMock.mockReset();
|
|
235
|
+
lookupAssistantByIdentifierMock.mockReturnValue({ status: "found", entry });
|
|
231
236
|
getActiveAssistantMock.mockReset();
|
|
232
237
|
getActiveAssistantMock.mockReturnValue("local-assistant");
|
|
233
238
|
loadAllAssistantsMock.mockReset();
|
|
@@ -446,3 +451,114 @@ describe("vellum upgrade local", () => {
|
|
|
446
451
|
expect(consoleLogSpy.mock.calls.flat().join("\n")).toContain("Already on");
|
|
447
452
|
});
|
|
448
453
|
});
|
|
454
|
+
|
|
455
|
+
// `cli/AGENTS.md` § "Assistant targeting convention": exact assistant ID
|
|
456
|
+
// matches win over display-name matches; a unique display-name match
|
|
457
|
+
// resolves to its assistant; an ambiguous display name is an error listing
|
|
458
|
+
// the matching IDs. That precedence lives in, and is tested directly against,
|
|
459
|
+
// `lookupAssistantByIdentifier` (see assistant-config.test.ts). These tests
|
|
460
|
+
// only check `vellum upgrade`'s wiring: each one stubs the exact result
|
|
461
|
+
// (found/ambiguous) the shared helper would return for a given argument, and
|
|
462
|
+
// asserts the command acts on it correctly, without reimplementing the
|
|
463
|
+
// lookup rules themselves.
|
|
464
|
+
describe("vellum upgrade assistant targeting", () => {
|
|
465
|
+
function makeQuillEntry(
|
|
466
|
+
overrides: Partial<AssistantEntry> = {},
|
|
467
|
+
): AssistantEntry {
|
|
468
|
+
return {
|
|
469
|
+
...makeLocalEntry(),
|
|
470
|
+
assistantId: "quill-uuid",
|
|
471
|
+
name: "Quill",
|
|
472
|
+
...overrides,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function argvFor(...positional: string[]): string[] {
|
|
477
|
+
return [
|
|
478
|
+
"bun",
|
|
479
|
+
"vellum",
|
|
480
|
+
"upgrade",
|
|
481
|
+
...positional,
|
|
482
|
+
"--version",
|
|
483
|
+
cliPkg.version ? `v${cliPkg.version}` : "v0.8.12",
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
test("resolves a unique display-name match to its assistant", async () => {
|
|
488
|
+
const quill = makeQuillEntry();
|
|
489
|
+
process.argv = argvFor("Quill");
|
|
490
|
+
lookupAssistantByIdentifierMock.mockReturnValue({
|
|
491
|
+
status: "found",
|
|
492
|
+
entry: quill,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await upgrade();
|
|
496
|
+
|
|
497
|
+
expect(lookupAssistantByIdentifierMock).toHaveBeenCalledWith("Quill");
|
|
498
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
499
|
+
expect(stopLocalProcessesMock).toHaveBeenCalledWith(
|
|
500
|
+
expect.objectContaining({ instanceDir: quill.resources!.instanceDir }),
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("resolves a multi-word display-name argument, unsplit by --version", async () => {
|
|
505
|
+
const multiWord = makeQuillEntry({ name: "My Assistant" });
|
|
506
|
+
process.argv = argvFor("My", "Assistant");
|
|
507
|
+
lookupAssistantByIdentifierMock.mockReturnValue({
|
|
508
|
+
status: "found",
|
|
509
|
+
entry: multiWord,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await upgrade();
|
|
513
|
+
|
|
514
|
+
expect(lookupAssistantByIdentifierMock).toHaveBeenCalledWith(
|
|
515
|
+
"My Assistant",
|
|
516
|
+
);
|
|
517
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
518
|
+
expect(stopLocalProcessesMock).toHaveBeenCalledWith(
|
|
519
|
+
expect.objectContaining({
|
|
520
|
+
instanceDir: multiWord.resources!.instanceDir,
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("an exact assistant-ID argument resolves via the shared helper's found result", async () => {
|
|
526
|
+
// `lookupAssistantByIdentifier` owns ID-wins-over-name precedence
|
|
527
|
+
// (covered in assistant-config.test.ts); this only checks that
|
|
528
|
+
// `vellum upgrade` passes the raw argument through unmodified and acts
|
|
529
|
+
// on whatever entry the shared helper resolves.
|
|
530
|
+
const target = makeQuillEntry({ assistantId: "quill-uuid" });
|
|
531
|
+
process.argv = argvFor("quill-uuid");
|
|
532
|
+
lookupAssistantByIdentifierMock.mockReturnValue({
|
|
533
|
+
status: "found",
|
|
534
|
+
entry: target,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await upgrade();
|
|
538
|
+
|
|
539
|
+
expect(lookupAssistantByIdentifierMock).toHaveBeenCalledWith("quill-uuid");
|
|
540
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
541
|
+
expect(stopLocalProcessesMock).toHaveBeenCalledWith(
|
|
542
|
+
expect.objectContaining({ instanceDir: target.resources!.instanceDir }),
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("an ambiguous display name fails and lists the matching IDs", async () => {
|
|
547
|
+
process.argv = argvFor("Quill");
|
|
548
|
+
lookupAssistantByIdentifierMock.mockReturnValue({
|
|
549
|
+
status: "ambiguous",
|
|
550
|
+
matches: [
|
|
551
|
+
makeQuillEntry({ assistantId: "quill-uuid-1" }),
|
|
552
|
+
makeQuillEntry({ assistantId: "quill-uuid-2" }),
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await expect(upgrade()).rejects.toThrow("process.exit(1)");
|
|
557
|
+
|
|
558
|
+
expect(stopLocalProcessesMock).not.toHaveBeenCalled();
|
|
559
|
+
const errText = consoleErrorSpy.mock.calls.flat().join("\n");
|
|
560
|
+
expect(errText).toContain("Multiple assistants match 'Quill'");
|
|
561
|
+
expect(errText).toContain("quill-uuid-1");
|
|
562
|
+
expect(errText).toContain("quill-uuid-2");
|
|
563
|
+
});
|
|
564
|
+
});
|
|
@@ -119,10 +119,10 @@ test("reports a failed forced Windows process-tree termination", async () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
test("treats a Unix exit before SIGKILL as a successful stop", async () => {
|
|
122
|
-
const
|
|
122
|
+
const calls: Array<[number, NodeJS.Signals | number | undefined]> = [];
|
|
123
123
|
const originalKill = process.kill.bind(process);
|
|
124
|
-
process.kill = ((
|
|
125
|
-
|
|
124
|
+
process.kill = ((pid: number, signal?: NodeJS.Signals | number) => {
|
|
125
|
+
calls.push([pid, signal]);
|
|
126
126
|
if (signal === "SIGKILL") {
|
|
127
127
|
throw Object.assign(new Error("no such process"), { code: "ESRCH" });
|
|
128
128
|
}
|
|
@@ -130,8 +130,80 @@ test("treats a Unix exit before SIGKILL as a successful stop", async () => {
|
|
|
130
130
|
}) as typeof process.kill;
|
|
131
131
|
|
|
132
132
|
try {
|
|
133
|
-
expect(
|
|
134
|
-
|
|
133
|
+
expect(
|
|
134
|
+
await stopProcess(
|
|
135
|
+
4812,
|
|
136
|
+
"test process",
|
|
137
|
+
0,
|
|
138
|
+
"darwin",
|
|
139
|
+
undefined,
|
|
140
|
+
() => "bun run /app/daemon/main.ts",
|
|
141
|
+
),
|
|
142
|
+
).toBeTrue();
|
|
143
|
+
// Escalation targets the process group first (the negative PID), so a
|
|
144
|
+
// daemon's workers die with it, then falls back to the single PID for
|
|
145
|
+
// processes that lead no group. ESRCH on both means it exited on its
|
|
146
|
+
// own, which is a successful stop.
|
|
147
|
+
expect(calls).toEqual([
|
|
148
|
+
[4812, 0],
|
|
149
|
+
[4812, "SIGTERM"],
|
|
150
|
+
[4812, 0],
|
|
151
|
+
[-4812, "SIGKILL"],
|
|
152
|
+
[4812, "SIGKILL"],
|
|
153
|
+
]);
|
|
154
|
+
} finally {
|
|
155
|
+
process.kill = originalKill;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Widening a signal to a process group requires proving the PID still names
|
|
160
|
+
// the same process after the grace wait. A PID recycled mid-wait must never
|
|
161
|
+
// receive a group-wide SIGKILL, only the single-PID kill this has always sent.
|
|
162
|
+
test("a PID recycled during the wait never gets a group kill", async () => {
|
|
163
|
+
const calls: Array<[number, NodeJS.Signals | number | undefined]> = [];
|
|
164
|
+
const originalKill = process.kill.bind(process);
|
|
165
|
+
process.kill = ((pid: number, signal?: NodeJS.Signals | number) => {
|
|
166
|
+
calls.push([pid, signal]);
|
|
167
|
+
return true;
|
|
168
|
+
}) as typeof process.kill;
|
|
169
|
+
|
|
170
|
+
let read = 0;
|
|
171
|
+
const identities = [
|
|
172
|
+
"bun run /app/daemon/main.ts",
|
|
173
|
+
"/usr/bin/postgres -D /data",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await stopProcess(
|
|
178
|
+
4812,
|
|
179
|
+
"test process",
|
|
180
|
+
0,
|
|
181
|
+
"darwin",
|
|
182
|
+
undefined,
|
|
183
|
+
() => identities[Math.min(read++, identities.length - 1)] ?? null,
|
|
184
|
+
);
|
|
185
|
+
expect(calls).not.toContainEqual([-4812, "SIGKILL"]);
|
|
186
|
+
expect(calls).toContainEqual([4812, "SIGKILL"]);
|
|
187
|
+
} finally {
|
|
188
|
+
process.kill = originalKill;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("an unreadable identity after the wait never gets a group kill", async () => {
|
|
193
|
+
const calls: Array<[number, NodeJS.Signals | number | undefined]> = [];
|
|
194
|
+
const originalKill = process.kill.bind(process);
|
|
195
|
+
process.kill = ((pid: number, signal?: NodeJS.Signals | number) => {
|
|
196
|
+
calls.push([pid, signal]);
|
|
197
|
+
return true;
|
|
198
|
+
}) as typeof process.kill;
|
|
199
|
+
|
|
200
|
+
let read = 0;
|
|
201
|
+
try {
|
|
202
|
+
await stopProcess(4812, "test process", 0, "darwin", undefined, () =>
|
|
203
|
+
read++ === 0 ? "bun run /app/daemon/main.ts" : null,
|
|
204
|
+
);
|
|
205
|
+
expect(calls).not.toContainEqual([-4812, "SIGKILL"]);
|
|
206
|
+
expect(calls).toContainEqual([4812, "SIGKILL"]);
|
|
135
207
|
} finally {
|
|
136
208
|
process.kill = originalKill;
|
|
137
209
|
}
|
|
@@ -51,11 +51,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
51
51
|
encoding: "utf-8",
|
|
52
52
|
timeout: 120000,
|
|
53
53
|
env: npmEnv,
|
|
54
|
+
windowsHide: true,
|
|
54
55
|
});
|
|
55
56
|
const child = spawn("vellum-openclaw-adapter", [], {
|
|
56
57
|
detached: true,
|
|
57
58
|
stdio: "ignore",
|
|
58
59
|
env: npmEnv,
|
|
60
|
+
windowsHide: true,
|
|
59
61
|
});
|
|
60
62
|
child.unref();
|
|
61
63
|
const responseBody = JSON.stringify({
|
|
@@ -91,6 +93,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
91
93
|
encoding: "utf-8",
|
|
92
94
|
timeout: 10000,
|
|
93
95
|
env: execEnv,
|
|
96
|
+
windowsHide: true,
|
|
94
97
|
});
|
|
95
98
|
const health = JSON.parse(output.trim()) as Record<string, unknown>;
|
|
96
99
|
const result: Record<string, unknown> = {
|
|
@@ -108,6 +111,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
108
111
|
encoding: "utf-8",
|
|
109
112
|
timeout: 10000,
|
|
110
113
|
env: execEnv,
|
|
114
|
+
windowsHide: true,
|
|
111
115
|
});
|
|
112
116
|
result.message = `${result.message}\n\nGateway Status:\n${gatewayOutput.trim()}`;
|
|
113
117
|
} catch (gatewayErr) {
|
|
@@ -138,6 +142,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
138
142
|
encoding: "utf-8",
|
|
139
143
|
timeout: 10000,
|
|
140
144
|
env: execEnv,
|
|
145
|
+
windowsHide: true,
|
|
141
146
|
});
|
|
142
147
|
result.message = `${result.message}\n\nGateway Status:\n${gatewayOutput.trim()}`;
|
|
143
148
|
} catch (gatewayErr) {
|
package/src/commands/client.ts
CHANGED
|
@@ -408,6 +408,20 @@ async function maybeHydratePlatformAssistantName(
|
|
|
408
408
|
|
|
409
409
|
const SPA_BASE = "/assistant/";
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Cache policy for hashed build output under `dist/assets/`: the content hash
|
|
413
|
+
* is part of the filename, so a new build emits new names and a stored copy can
|
|
414
|
+
* never be stale.
|
|
415
|
+
*/
|
|
416
|
+
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Cache policy for everything whose name is stable across builds: the HTML
|
|
420
|
+
* shell and unhashed `public/` files. Storing is allowed, serving without
|
|
421
|
+
* revalidating is not, so an assistant update is never masked by a cached copy.
|
|
422
|
+
*/
|
|
423
|
+
const REVALIDATE_CACHE_CONTROL = "no-cache";
|
|
424
|
+
|
|
411
425
|
/**
|
|
412
426
|
* Locate the clients/web source directory for running the Vite dev server.
|
|
413
427
|
* Only works from a source checkout (not npm-installed).
|
|
@@ -1303,6 +1317,17 @@ async function runWebInterface(
|
|
|
1303
1317
|
`<script>window.__VELLUM_CONFIG__=${configJson}${flagOverridesSnippet}</script></head>`,
|
|
1304
1318
|
);
|
|
1305
1319
|
|
|
1320
|
+
// The shell names this build's hashed chunks and carries the host's injected
|
|
1321
|
+
// config snippet, so every route that falls back to it serves the same body
|
|
1322
|
+
// under the same revalidating policy.
|
|
1323
|
+
const spaShellResponse = () =>
|
|
1324
|
+
new Response(indexHtml, {
|
|
1325
|
+
headers: {
|
|
1326
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1327
|
+
"Cache-Control": REVALIDATE_CACHE_CONTROL,
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1306
1331
|
const fetchHandler: WebFetchHandler = async (req, server) => {
|
|
1307
1332
|
const url = new URL(req.url);
|
|
1308
1333
|
const { pathname } = url;
|
|
@@ -1399,19 +1424,21 @@ async function runWebInterface(
|
|
|
1399
1424
|
const filePath = path.join(distDir, relPath);
|
|
1400
1425
|
const file = Bun.file(filePath);
|
|
1401
1426
|
if (await file.exists()) {
|
|
1402
|
-
return new Response(file
|
|
1427
|
+
return new Response(file, {
|
|
1428
|
+
headers: {
|
|
1429
|
+
"Cache-Control": relPath.startsWith("assets/")
|
|
1430
|
+
? IMMUTABLE_CACHE_CONTROL
|
|
1431
|
+
: REVALIDATE_CACHE_CONTROL,
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1403
1434
|
}
|
|
1404
1435
|
}
|
|
1405
|
-
return
|
|
1406
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1407
|
-
});
|
|
1436
|
+
return spaShellResponse();
|
|
1408
1437
|
}
|
|
1409
1438
|
|
|
1410
1439
|
// SPA fallback for /account/* routes (login, callback, etc.)
|
|
1411
1440
|
if (pathname.startsWith("/account/")) {
|
|
1412
|
-
return
|
|
1413
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1414
|
-
});
|
|
1441
|
+
return spaShellResponse();
|
|
1415
1442
|
}
|
|
1416
1443
|
|
|
1417
1444
|
return new Response("Not Found", { status: 404 });
|
|
@@ -1479,6 +1506,7 @@ async function runViteDevServer(
|
|
|
1479
1506
|
const child = spawn("bun", ["run", "dev"], {
|
|
1480
1507
|
cwd: webSourceDir,
|
|
1481
1508
|
stdio: "inherit",
|
|
1509
|
+
windowsHide: true,
|
|
1482
1510
|
env: {
|
|
1483
1511
|
...process.env,
|
|
1484
1512
|
...flagEnvVars,
|
package/src/commands/exec.ts
CHANGED
|
@@ -156,7 +156,10 @@ export async function exec(): Promise<void> {
|
|
|
156
156
|
const cloud = entry.cloud;
|
|
157
157
|
|
|
158
158
|
if (cloud === "local") {
|
|
159
|
-
const child = spawn(command[0], command.slice(1), {
|
|
159
|
+
const child = spawn(command[0], command.slice(1), {
|
|
160
|
+
stdio: "inherit",
|
|
161
|
+
windowsHide: true,
|
|
162
|
+
});
|
|
160
163
|
await new Promise<void>((resolve) => {
|
|
161
164
|
child.on("close", (code) => {
|
|
162
165
|
process.exitCode = code ?? 0;
|
|
@@ -187,7 +190,10 @@ export async function exec(): Promise<void> {
|
|
|
187
190
|
? ["exec", "-it", container, ...command]
|
|
188
191
|
: ["exec", container, ...command];
|
|
189
192
|
|
|
190
|
-
const child = spawn("docker", dockerArgs, {
|
|
193
|
+
const child = spawn("docker", dockerArgs, {
|
|
194
|
+
stdio: "inherit",
|
|
195
|
+
windowsHide: true,
|
|
196
|
+
});
|
|
191
197
|
await new Promise<void>((resolve, reject) => {
|
|
192
198
|
child.on("close", (code) => {
|
|
193
199
|
if (code === 0) resolve();
|
package/src/commands/logs.ts
CHANGED
|
@@ -329,7 +329,10 @@ async function showDockerLogs(
|
|
|
329
329
|
// Single container — stream directly to stdout/stderr
|
|
330
330
|
const target = targets[0];
|
|
331
331
|
const args = buildDockerArgs(target.containerName);
|
|
332
|
-
const child = spawn("docker", args, {
|
|
332
|
+
const child = spawn("docker", args, {
|
|
333
|
+
stdio: "inherit",
|
|
334
|
+
windowsHide: true,
|
|
335
|
+
});
|
|
333
336
|
|
|
334
337
|
await new Promise<void>((resolve, reject) => {
|
|
335
338
|
child.on("close", (code) => {
|
|
@@ -356,6 +359,7 @@ async function showDockerLogs(
|
|
|
356
359
|
const args = buildDockerArgs(target.containerName);
|
|
357
360
|
const child = spawn("docker", args, {
|
|
358
361
|
stdio: ["ignore", "pipe", "pipe"],
|
|
362
|
+
windowsHide: true,
|
|
359
363
|
});
|
|
360
364
|
|
|
361
365
|
const prefix = `[${target.name}] `;
|
|
@@ -466,7 +470,10 @@ async function showGcpLogs(
|
|
|
466
470
|
|
|
467
471
|
if (opts.follow) {
|
|
468
472
|
// For follow mode, stream output directly to terminal
|
|
469
|
-
const child = spawn("gcloud", args, {
|
|
473
|
+
const child = spawn("gcloud", args, {
|
|
474
|
+
stdio: "inherit",
|
|
475
|
+
windowsHide: true,
|
|
476
|
+
});
|
|
470
477
|
await new Promise<void>((resolve, reject) => {
|
|
471
478
|
child.on("close", (code) => {
|
|
472
479
|
if (code !== 0 && code !== null) {
|
|
@@ -503,6 +510,7 @@ async function showCustomLogs(
|
|
|
503
510
|
if (opts.follow) {
|
|
504
511
|
const child = spawn("ssh", [...SSH_OPTS, sshTarget, remoteCmd], {
|
|
505
512
|
stdio: "inherit",
|
|
513
|
+
windowsHide: true,
|
|
506
514
|
});
|
|
507
515
|
await new Promise<void>((resolve, reject) => {
|
|
508
516
|
child.on("close", (code) => {
|
|
@@ -544,6 +552,7 @@ async function showAwsLogs(
|
|
|
544
552
|
if (opts.follow) {
|
|
545
553
|
const child = spawn("ssh", [...SSH_OPTS, sshTarget, remoteCmd], {
|
|
546
554
|
stdio: "inherit",
|
|
555
|
+
windowsHide: true,
|
|
547
556
|
});
|
|
548
557
|
await new Promise<void>((resolve, reject) => {
|
|
549
558
|
child.on("close", (code) => {
|
package/src/commands/sleep.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
stopContainerTunnelEdge,
|
|
18
18
|
stopIngressNginx,
|
|
19
19
|
} from "../lib/nginx-ingress.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
22
|
+
isProcessAlive,
|
|
23
|
+
stopProcessByPidFile,
|
|
24
|
+
} from "../lib/process";
|
|
21
25
|
import { resolveFreshBearerToken } from "./client.js";
|
|
22
26
|
|
|
23
27
|
const ACTIVE_CALL_LEASES_FILE = "active-call-leases.json";
|
|
@@ -208,18 +212,11 @@ export async function sleep(): Promise<void> {
|
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
// Stop assistant — use a generous timeout. On SIGTERM the daemon runs a
|
|
212
|
-
// WAL checkpoint before exiting, which can take several seconds on a
|
|
213
|
-
// multi-GB database. The default 2s grace in stopProcess() would SIGKILL a
|
|
214
|
-
// healthy daemon mid-checkpoint, forcing a costly multi-minute WAL recovery
|
|
215
|
-
// on the next start. The timeout is only a SIGKILL ceiling — stopProcess
|
|
216
|
-
// returns as soon as the process exits, so this adds no delay in the common
|
|
217
|
-
// case and only applies when the daemon is genuinely wedged.
|
|
218
215
|
const assistantStopped = await stopProcessByPidFile(
|
|
219
216
|
assistantPidFile,
|
|
220
217
|
"assistant",
|
|
221
218
|
undefined,
|
|
222
|
-
|
|
219
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
223
220
|
);
|
|
224
221
|
if (!assistantStopped) {
|
|
225
222
|
console.log("Assistant is not running.");
|
package/src/commands/ssh.ts
CHANGED
|
@@ -73,7 +73,7 @@ export async function ssh(): Promise<void> {
|
|
|
73
73
|
child = spawn(
|
|
74
74
|
"docker",
|
|
75
75
|
["exec", "-it", res.assistantContainer, "/bin/sh"],
|
|
76
|
-
{ stdio: "inherit" },
|
|
76
|
+
{ stdio: "inherit", windowsHide: true },
|
|
77
77
|
);
|
|
78
78
|
} else if (cloud === "gcp") {
|
|
79
79
|
const project = entry.project;
|
|
@@ -94,7 +94,7 @@ export async function ssh(): Promise<void> {
|
|
|
94
94
|
child = spawn(
|
|
95
95
|
"gcloud",
|
|
96
96
|
["compute", "ssh", sshTarget, `--project=${project}`, `--zone=${zone}`],
|
|
97
|
-
{ stdio: "inherit" },
|
|
97
|
+
{ stdio: "inherit", windowsHide: true },
|
|
98
98
|
);
|
|
99
99
|
} else if (cloud === "vellum") {
|
|
100
100
|
const token = readPlatformToken();
|
|
@@ -117,7 +117,10 @@ export async function ssh(): Promise<void> {
|
|
|
117
117
|
|
|
118
118
|
console.log(`🔗 Connecting to ${entry.assistantId} via ssh...\n`);
|
|
119
119
|
|
|
120
|
-
child = spawn("ssh", [...SSH_OPTS, sshTarget], {
|
|
120
|
+
child = spawn("ssh", [...SSH_OPTS, sshTarget], {
|
|
121
|
+
stdio: "inherit",
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
121
124
|
} else {
|
|
122
125
|
console.error(`Error: Unknown cloud type '${cloud}'.`);
|
|
123
126
|
process.exit(1);
|
package/src/commands/teleport.ts
CHANGED
|
@@ -47,7 +47,10 @@ import {
|
|
|
47
47
|
import { hatchLocal } from "../lib/hatch-local.js";
|
|
48
48
|
import { retireLocal } from "../lib/retire-local.js";
|
|
49
49
|
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
52
|
+
stopProcessByPidFile,
|
|
53
|
+
} from "../lib/process.js";
|
|
51
54
|
import {
|
|
52
55
|
fetchAssistantIngressUrl,
|
|
53
56
|
fetchCurrentVersion,
|
|
@@ -1497,6 +1500,8 @@ export async function teleport(): Promise<void> {
|
|
|
1497
1500
|
await stopProcessByPidFile(
|
|
1498
1501
|
getDaemonPidPath(fromEntry.resources),
|
|
1499
1502
|
"assistant",
|
|
1503
|
+
undefined,
|
|
1504
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
1500
1505
|
);
|
|
1501
1506
|
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
1502
1507
|
}
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -6,8 +6,10 @@ import cliPkg from "../../package.json";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
findAssistantByName,
|
|
9
|
+
formatAssistantLookupError,
|
|
9
10
|
getActiveAssistant,
|
|
10
11
|
loadAllAssistants,
|
|
12
|
+
lookupAssistantByIdentifier,
|
|
11
13
|
saveAssistantEntry,
|
|
12
14
|
type AssistantEntry,
|
|
13
15
|
} from "../lib/assistant-config";
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
restoreBackup,
|
|
43
45
|
} from "../lib/backup-ops.js";
|
|
44
46
|
import { emitCliError, categorizeUpgradeError } from "../lib/cli-error.js";
|
|
47
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
45
48
|
import { exec } from "../lib/step-runner.js";
|
|
46
49
|
import {
|
|
47
50
|
broadcastUpgradeEvent,
|
|
@@ -89,9 +92,14 @@ interface UpgradeArgs {
|
|
|
89
92
|
force: boolean;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// Flags that consume the following argv token as a value, rather than a
|
|
96
|
+
// boolean switch. Passed to `parseAssistantTargetArg` so an unquoted
|
|
97
|
+
// multi-word display name is not truncated at a flag's value.
|
|
98
|
+
const UPGRADE_FLAGS_WITH_VALUES = ["--version"];
|
|
99
|
+
|
|
92
100
|
function parseArgs(): UpgradeArgs {
|
|
93
101
|
const args = process.argv.slice(3);
|
|
94
|
-
|
|
102
|
+
const name = parseAssistantTargetArg(args, UPGRADE_FLAGS_WITH_VALUES) ?? null;
|
|
95
103
|
let version: string | null = null;
|
|
96
104
|
let latest = false;
|
|
97
105
|
let prepare = false;
|
|
@@ -166,7 +174,8 @@ function parseArgs(): UpgradeArgs {
|
|
|
166
174
|
} else if (arg === "--force") {
|
|
167
175
|
force = true;
|
|
168
176
|
} else if (!arg.startsWith("-")) {
|
|
169
|
-
|
|
177
|
+
// Positional token, part of a possibly multi-word display name.
|
|
178
|
+
// Collected up front via `parseAssistantTargetArg` above.
|
|
170
179
|
} else {
|
|
171
180
|
console.error(`Error: Unknown option '${arg}'.`);
|
|
172
181
|
emitCliError("UNKNOWN", `Unknown option '${arg}'`);
|
|
@@ -202,22 +211,22 @@ function parseArgs(): UpgradeArgs {
|
|
|
202
211
|
|
|
203
212
|
/**
|
|
204
213
|
* Resolve which assistant to target for the upgrade command. Priority:
|
|
205
|
-
* 1. Explicit name argument
|
|
214
|
+
* 1. Explicit name argument (exact assistant ID wins over a display-name
|
|
215
|
+
* match; a unique display-name match resolves; an ambiguous display
|
|
216
|
+
* name is an error listing the matching IDs)
|
|
206
217
|
* 2. Active assistant set via `vellum use`
|
|
207
218
|
* 3. Sole assistant (when exactly one exists)
|
|
208
219
|
*/
|
|
209
220
|
function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
|
|
210
221
|
if (nameArg) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`No assistant found with name '${nameArg}'.`,
|
|
217
|
-
);
|
|
222
|
+
const result = lookupAssistantByIdentifier(nameArg);
|
|
223
|
+
if (result.status !== "found") {
|
|
224
|
+
const msg = formatAssistantLookupError(nameArg, result);
|
|
225
|
+
console.error(msg);
|
|
226
|
+
emitCliError("ASSISTANT_NOT_FOUND", msg);
|
|
218
227
|
process.exit(1);
|
|
219
228
|
}
|
|
220
|
-
return entry;
|
|
229
|
+
return result.entry;
|
|
221
230
|
}
|
|
222
231
|
|
|
223
232
|
const active = getActiveAssistant();
|
|
@@ -1518,7 +1527,7 @@ async function resolveLatestAndMaybeSelfUpdate(
|
|
|
1518
1527
|
const installResult = spawnSync(
|
|
1519
1528
|
"bun",
|
|
1520
1529
|
["install", "-g", `vellum@${stripVersionPrefix(latestTag)}`],
|
|
1521
|
-
{ stdio: "inherit" },
|
|
1530
|
+
{ stdio: "inherit", windowsHide: true },
|
|
1522
1531
|
);
|
|
1523
1532
|
if (installResult.error || installResult.status !== 0) {
|
|
1524
1533
|
const detail =
|
|
@@ -1542,6 +1551,7 @@ async function resolveLatestAndMaybeSelfUpdate(
|
|
|
1542
1551
|
console.log(`🚀 Re-running upgrade with updated CLI...\n`);
|
|
1543
1552
|
const reexecResult = spawnSync("vellum", reexecArgs, {
|
|
1544
1553
|
stdio: "inherit",
|
|
1554
|
+
windowsHide: true,
|
|
1545
1555
|
});
|
|
1546
1556
|
process.exit(reexecResult.status ?? 1);
|
|
1547
1557
|
}
|