@vellumai/cli 0.6.6 → 0.7.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.
Files changed (61) hide show
  1. package/AGENTS.md +8 -2
  2. package/README.md +49 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/assistant-config.test.ts +1 -7
  5. package/src/__tests__/backup.test.ts +475 -0
  6. package/src/__tests__/config-utils.test.ts +146 -0
  7. package/src/__tests__/env-drift.test.ts +10 -32
  8. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  9. package/src/__tests__/multi-local.test.ts +0 -5
  10. package/src/__tests__/sleep.test.ts +1 -2
  11. package/src/__tests__/teleport.test.ts +988 -1266
  12. package/src/commands/backup.ts +117 -71
  13. package/src/commands/client.ts +10 -9
  14. package/src/commands/env.ts +93 -0
  15. package/src/commands/events.ts +2 -0
  16. package/src/commands/exec.ts +58 -13
  17. package/src/commands/login.ts +77 -12
  18. package/src/commands/logs.ts +2 -7
  19. package/src/commands/ps.ts +144 -25
  20. package/src/commands/restore.ts +26 -47
  21. package/src/commands/sleep.ts +5 -2
  22. package/src/commands/ssh.ts +17 -7
  23. package/src/commands/teleport.ts +462 -584
  24. package/src/commands/terminal.ts +9 -221
  25. package/src/commands/tunnel.ts +2 -7
  26. package/src/commands/upgrade.ts +108 -7
  27. package/src/commands/wake.ts +2 -1
  28. package/src/components/DefaultMainScreen.tsx +328 -154
  29. package/src/index.ts +5 -7
  30. package/src/lib/__tests__/docker.test.ts +50 -74
  31. package/src/lib/__tests__/job-polling.test.ts +278 -0
  32. package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
  33. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  34. package/src/lib/__tests__/runtime-url.test.ts +87 -0
  35. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  36. package/src/lib/assistant-client.ts +5 -21
  37. package/src/lib/assistant-config.ts +46 -24
  38. package/src/lib/cli-error.ts +1 -0
  39. package/src/lib/client-identity.ts +67 -0
  40. package/src/lib/docker.ts +75 -77
  41. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  42. package/src/lib/environments/resolve.ts +89 -7
  43. package/src/lib/environments/seeds.ts +8 -5
  44. package/src/lib/environments/types.ts +10 -0
  45. package/src/lib/hatch-local.ts +15 -120
  46. package/src/lib/health-check.ts +98 -0
  47. package/src/lib/job-polling.ts +195 -0
  48. package/src/lib/local-runtime-client.ts +231 -0
  49. package/src/lib/local.ts +165 -72
  50. package/src/lib/orphan-detection.ts +2 -35
  51. package/src/lib/platform-client.ts +190 -194
  52. package/src/lib/platform-releases.ts +23 -0
  53. package/src/lib/retire-local.ts +6 -2
  54. package/src/lib/runtime-url.ts +30 -0
  55. package/src/lib/sync-cloud-assistants.ts +126 -0
  56. package/src/lib/terminal-client.ts +6 -1
  57. package/src/lib/terminal-session.ts +536 -0
  58. package/src/lib/tui-log.ts +60 -0
  59. package/src/lib/xdg-log.ts +10 -4
  60. package/src/shared/provider-env-vars.ts +2 -3
  61. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -1,214 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterAll, mock } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- // Redirect lockfile reads/writes to a scratch directory so the test never
7
- // touches the real `~/.vellum.lock.json`.
8
- const testDir = mkdtempSync(join(tmpdir(), "cli-orphan-detection-test-"));
9
- process.env.VELLUM_LOCKFILE_DIR = testDir;
10
-
11
- // Mock homedir() so `join(homedir(), ".vellum")` inside orphan-detection
12
- // resolves under the scratch directory — the legacy fallback scan is part
13
- // of the behavior under test and we need to keep it off the real filesystem.
14
- const realOs = await import("node:os");
15
- const fakeHome = mkdtempSync(join(tmpdir(), "cli-orphan-detection-home-"));
16
- mock.module("node:os", () => ({
17
- ...realOs,
18
- homedir: () => fakeHome,
19
- }));
20
- mock.module("os", () => ({
21
- ...realOs,
22
- homedir: () => fakeHome,
23
- }));
24
-
25
- // Stub execOutput so the process-table scan never shells out to `ps`.
26
- // The PID-file scan is the surface we want to exercise here.
27
- mock.module("../lib/step-runner", () => ({
28
- execOutput: () => Promise.resolve(""),
29
- exec: () => Promise.resolve(undefined),
30
- }));
31
-
32
- // Every detected PID claims to be a live process so the scan surfaces it.
33
- const originalKill = process.kill;
34
- process.kill = ((pid: number, signal?: string | number) => {
35
- if (signal === 0) return true;
36
- return originalKill.call(process, pid, signal);
37
- }) as typeof process.kill;
38
-
39
- import { detectOrphanedProcesses } from "../lib/orphan-detection.js";
40
- import {
41
- saveAssistantEntry,
42
- type AssistantEntry,
43
- type LocalInstanceResources,
44
- } from "../lib/assistant-config.js";
45
- import {
46
- DEFAULT_CES_PORT,
47
- DEFAULT_DAEMON_PORT,
48
- DEFAULT_GATEWAY_PORT,
49
- DEFAULT_QDRANT_PORT,
50
- } from "../lib/constants.js";
51
-
52
- afterAll(() => {
53
- process.kill = originalKill;
54
- rmSync(testDir, { recursive: true, force: true });
55
- rmSync(fakeHome, { recursive: true, force: true });
56
- delete process.env.VELLUM_LOCKFILE_DIR;
57
- });
58
-
59
- function resetLockfile(): void {
60
- for (const name of [".vellum.lock.json", ".vellum.lockfile.json"]) {
61
- try {
62
- rmSync(join(testDir, name));
63
- } catch {
64
- // file may not exist
65
- }
66
- }
67
- }
68
-
69
- function resetFakeHome(): void {
70
- rmSync(fakeHome, { recursive: true, force: true });
71
- mkdirSync(fakeHome, { recursive: true });
72
- }
73
-
74
- function makeResources(
75
- instanceDir: string,
76
- ports: Partial<LocalInstanceResources> = {},
77
- ): LocalInstanceResources {
78
- return {
79
- instanceDir,
80
- daemonPort: ports.daemonPort ?? DEFAULT_DAEMON_PORT,
81
- gatewayPort: ports.gatewayPort ?? DEFAULT_GATEWAY_PORT,
82
- qdrantPort: ports.qdrantPort ?? DEFAULT_QDRANT_PORT,
83
- cesPort: ports.cesPort ?? DEFAULT_CES_PORT,
84
- pidFile: join(instanceDir, ".vellum", "vellum.pid"),
85
- };
86
- }
87
-
88
- function makeEntry(
89
- id: string,
90
- instanceDir: string,
91
- extra?: Partial<AssistantEntry>,
92
- ): AssistantEntry {
93
- return {
94
- assistantId: id,
95
- runtimeUrl: `http://localhost:${DEFAULT_GATEWAY_PORT}`,
96
- cloud: "local",
97
- resources: makeResources(instanceDir),
98
- ...extra,
99
- };
100
- }
101
-
102
- function writePidFile(
103
- instanceDir: string,
104
- name: "vellum" | "gateway" | "qdrant",
105
- pid: number,
106
- ): void {
107
- const dir = join(instanceDir, ".vellum");
108
- mkdirSync(dir, { recursive: true });
109
- writeFileSync(join(dir, `${name}.pid`), String(pid));
110
- }
111
-
112
- describe("detectOrphanedProcesses", () => {
113
- beforeEach(() => {
114
- resetLockfile();
115
- resetFakeHome();
116
- });
117
-
118
- test("scans every local entry's instanceDir/.vellum and reports each PID", async () => {
119
- // GIVEN two local entries in the lockfile, each pointing at its own
120
- // instance directory with a stale PID file
121
- const instanceA = mkdtempSync(join(tmpdir(), "orphan-instance-a-"));
122
- const instanceB = mkdtempSync(join(tmpdir(), "orphan-instance-b-"));
123
- try {
124
- saveAssistantEntry(makeEntry("alpha", instanceA));
125
- saveAssistantEntry(
126
- makeEntry("beta", instanceB, {
127
- runtimeUrl: "http://localhost:8821",
128
- }),
129
- );
130
- writePidFile(instanceA, "vellum", 111111);
131
- writePidFile(instanceB, "gateway", 222222);
132
-
133
- // WHEN we run orphan detection
134
- const orphans = await detectOrphanedProcesses();
135
-
136
- // THEN both containers are scanned and each PID is surfaced
137
- const pidFileOrphans = orphans.filter((o) => o.source === "pid file");
138
- const pids = pidFileOrphans.map((o) => o.pid);
139
- expect(pids).toContain("111111");
140
- expect(pids).toContain("222222");
141
-
142
- const byName = new Map(pidFileOrphans.map((o) => [o.pid, o.name]));
143
- expect(byName.get("111111")).toBe("assistant");
144
- expect(byName.get("222222")).toBe("gateway");
145
- } finally {
146
- rmSync(instanceA, { recursive: true, force: true });
147
- rmSync(instanceB, { recursive: true, force: true });
148
- }
149
- });
150
-
151
- test("still scans legacy ~/.vellum/ when no lockfile entry covers it", async () => {
152
- // GIVEN no local entries in the lockfile but a stale PID file in the
153
- // legacy `~/.vellum/` root (pre-upgrade install)
154
- resetLockfile();
155
- writePidFile(fakeHome, "vellum", 333333);
156
-
157
- // WHEN we run orphan detection
158
- const orphans = await detectOrphanedProcesses();
159
-
160
- // THEN the legacy root is still scanned and the PID surfaces
161
- const pids = orphans
162
- .filter((o) => o.source === "pid file")
163
- .map((o) => o.pid);
164
- expect(pids).toContain("333333");
165
- });
166
-
167
- test("legacy ~/.vellum/ is scanned alongside multi-instance entries", async () => {
168
- // GIVEN a local entry AND a legacy `~/.vellum/` PID file at the same time
169
- const instanceA = mkdtempSync(join(tmpdir(), "orphan-instance-coexist-"));
170
- try {
171
- saveAssistantEntry(makeEntry("alpha", instanceA));
172
- writePidFile(instanceA, "vellum", 444444);
173
- writePidFile(fakeHome, "gateway", 555555);
174
-
175
- // WHEN we run orphan detection
176
- const orphans = await detectOrphanedProcesses();
177
-
178
- // THEN both the entry's instance dir and the legacy root are scanned
179
- const pids = orphans
180
- .filter((o) => o.source === "pid file")
181
- .map((o) => o.pid);
182
- expect(pids).toContain("444444");
183
- expect(pids).toContain("555555");
184
- } finally {
185
- rmSync(instanceA, { recursive: true, force: true });
186
- }
187
- });
188
-
189
- test("ignores remote entries — only local entries with resources are scanned", async () => {
190
- // GIVEN a remote entry (no resources) alongside a local entry
191
- const instanceA = mkdtempSync(join(tmpdir(), "orphan-instance-local-"));
192
- try {
193
- saveAssistantEntry({
194
- assistantId: "cloud-box",
195
- runtimeUrl: "http://10.0.0.1:7821",
196
- cloud: "gcp",
197
- });
198
- saveAssistantEntry(makeEntry("alpha", instanceA));
199
- writePidFile(instanceA, "qdrant", 666666);
200
-
201
- // WHEN we run orphan detection
202
- const orphans = await detectOrphanedProcesses();
203
-
204
- // THEN the local entry's PID still surfaces (the remote entry is
205
- // silently skipped)
206
- const pids = orphans
207
- .filter((o) => o.source === "pid file")
208
- .map((o) => o.pid);
209
- expect(pids).toContain("666666");
210
- } finally {
211
- rmSync(instanceA, { recursive: true, force: true });
212
- }
213
- });
214
- });