@vellumai/cli 0.6.3 → 0.6.5

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 (56) hide show
  1. package/AGENTS.md +12 -2
  2. package/README.md +3 -3
  3. package/bun.lock +17 -17
  4. package/bunfig.toml +6 -0
  5. package/package.json +18 -18
  6. package/src/__tests__/assistant-config.test.ts +124 -0
  7. package/src/__tests__/env-drift.test.ts +87 -0
  8. package/src/__tests__/guardian-token.test.ts +225 -0
  9. package/src/__tests__/llm-provider-env-var-parity.test.ts +64 -0
  10. package/src/__tests__/multi-local.test.ts +90 -13
  11. package/src/__tests__/orphan-detection.test.ts +214 -0
  12. package/src/__tests__/platform-client.test.ts +204 -0
  13. package/src/__tests__/preload.ts +27 -0
  14. package/src/__tests__/ssh-user-guard.test.ts +28 -0
  15. package/src/__tests__/teleport.test.ts +1073 -56
  16. package/src/commands/backup.ts +8 -0
  17. package/src/commands/exec.ts +186 -0
  18. package/src/commands/hatch.ts +1 -1
  19. package/src/commands/login.ts +209 -9
  20. package/src/commands/logs.ts +652 -0
  21. package/src/commands/pair.ts +9 -1
  22. package/src/commands/ps.ts +37 -7
  23. package/src/commands/recover.ts +8 -4
  24. package/src/commands/restore.ts +8 -0
  25. package/src/commands/retire.ts +16 -9
  26. package/src/commands/rollback.ts +32 -33
  27. package/src/commands/ssh.ts +7 -0
  28. package/src/commands/teleport.ts +253 -1
  29. package/src/commands/upgrade.ts +43 -52
  30. package/src/commands/wake.ts +25 -10
  31. package/src/components/DefaultMainScreen.tsx +7 -1
  32. package/src/index.ts +6 -0
  33. package/src/lib/__tests__/docker.test.ts +168 -0
  34. package/src/lib/assistant-config.ts +82 -108
  35. package/src/lib/aws.ts +12 -1
  36. package/src/lib/config-utils.ts +4 -4
  37. package/src/lib/constants.ts +0 -10
  38. package/src/lib/docker.ts +158 -8
  39. package/src/lib/environments/__tests__/paths.test.ts +228 -0
  40. package/src/lib/environments/__tests__/resolve.test.ts +226 -0
  41. package/src/lib/environments/__tests__/seeds.test.ts +72 -0
  42. package/src/lib/environments/paths.ts +109 -0
  43. package/src/lib/environments/resolve.ts +96 -0
  44. package/src/lib/environments/seeds.ts +74 -0
  45. package/src/lib/environments/types.ts +60 -0
  46. package/src/lib/exec-apple-container.ts +122 -0
  47. package/src/lib/gcp.ts +12 -1
  48. package/src/lib/guardian-token.ts +71 -10
  49. package/src/lib/hatch-local.ts +44 -23
  50. package/src/lib/local.ts +47 -5
  51. package/src/lib/orphan-detection.ts +28 -12
  52. package/src/lib/platform-client.ts +354 -24
  53. package/src/lib/retire-apple-container.ts +102 -0
  54. package/src/lib/ssh-apple-container.ts +166 -0
  55. package/src/lib/upgrade-lifecycle.ts +101 -28
  56. package/src/shared/provider-env-vars.ts +30 -6
@@ -0,0 +1,226 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+
3
+ import { getCurrentEnvironment, getSeed } from "../resolve.js";
4
+
5
+ const ENV_VARS_TO_SAVE = [
6
+ "VELLUM_ENVIRONMENT",
7
+ "VELLUM_PLATFORM_URL",
8
+ "VELLUM_ASSISTANT_PLATFORM_URL",
9
+ "VELLUM_LOCKFILE_DIR",
10
+ ] as const;
11
+
12
+ describe("getCurrentEnvironment", () => {
13
+ let savedEnv: Record<string, string | undefined>;
14
+
15
+ beforeEach(() => {
16
+ savedEnv = {};
17
+ for (const key of ENV_VARS_TO_SAVE) {
18
+ savedEnv[key] = process.env[key];
19
+ delete process.env[key];
20
+ }
21
+ });
22
+
23
+ afterEach(() => {
24
+ for (const [key, value] of Object.entries(savedEnv)) {
25
+ if (value === undefined) {
26
+ delete process.env[key];
27
+ } else {
28
+ process.env[key] = value;
29
+ }
30
+ }
31
+ });
32
+
33
+ test("returns production seed when no override, no env var", () => {
34
+ const env = getCurrentEnvironment();
35
+ expect(env.name).toBe("production");
36
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
37
+ });
38
+
39
+ test("returns dev seed when VELLUM_ENVIRONMENT=dev", () => {
40
+ process.env.VELLUM_ENVIRONMENT = "dev";
41
+ const env = getCurrentEnvironment();
42
+ expect(env.name).toBe("dev");
43
+ expect(env.platformUrl).toBe("https://dev-platform.vellum.ai");
44
+ });
45
+
46
+ test("returns staging seed when VELLUM_ENVIRONMENT=staging", () => {
47
+ process.env.VELLUM_ENVIRONMENT = "staging";
48
+ expect(getCurrentEnvironment().platformUrl).toBe(
49
+ "https://staging-platform.vellum.ai",
50
+ );
51
+ });
52
+
53
+ test("returns local seed with localhost URL", () => {
54
+ process.env.VELLUM_ENVIRONMENT = "local";
55
+ expect(getCurrentEnvironment().platformUrl).toBe("http://localhost:8000");
56
+ });
57
+
58
+ test("override argument takes priority over VELLUM_ENVIRONMENT env var", () => {
59
+ process.env.VELLUM_ENVIRONMENT = "staging";
60
+ const env = getCurrentEnvironment("dev");
61
+ expect(env.name).toBe("dev");
62
+ expect(env.platformUrl).toBe("https://dev-platform.vellum.ai");
63
+ });
64
+
65
+ test("empty override argument falls through to env var", () => {
66
+ process.env.VELLUM_ENVIRONMENT = "dev";
67
+ const env = getCurrentEnvironment("");
68
+ expect(env.name).toBe("dev");
69
+ });
70
+
71
+ test("whitespace-only override argument falls through to env var", () => {
72
+ process.env.VELLUM_ENVIRONMENT = "dev";
73
+ const env = getCurrentEnvironment(" ");
74
+ expect(env.name).toBe("dev");
75
+ });
76
+
77
+ test("falls back to production seed for unknown env name via override, warns on stderr", () => {
78
+ const stderr = spyOn(process.stderr, "write").mockImplementation(
79
+ () => true,
80
+ );
81
+ try {
82
+ const env = getCurrentEnvironment("no-such-env");
83
+ expect(env.name).toBe("production");
84
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
85
+ expect(stderr).toHaveBeenCalledWith(
86
+ expect.stringContaining('unknown environment "no-such-env"'),
87
+ );
88
+ expect(stderr).toHaveBeenCalledWith(
89
+ expect.stringContaining('falling back to "production"'),
90
+ );
91
+ } finally {
92
+ stderr.mockRestore();
93
+ }
94
+ });
95
+
96
+ test("falls back to production seed for unknown env name via env var, warns on stderr", () => {
97
+ process.env.VELLUM_ENVIRONMENT = "nope";
98
+ const stderr = spyOn(process.stderr, "write").mockImplementation(
99
+ () => true,
100
+ );
101
+ try {
102
+ const env = getCurrentEnvironment();
103
+ expect(env.name).toBe("production");
104
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
105
+ expect(stderr).toHaveBeenCalledWith(
106
+ expect.stringContaining('unknown environment "nope"'),
107
+ );
108
+ } finally {
109
+ stderr.mockRestore();
110
+ }
111
+ });
112
+
113
+ test("VELLUM_ENVIRONMENT=production does not emit a warning", () => {
114
+ process.env.VELLUM_ENVIRONMENT = "production";
115
+ const stderr = spyOn(process.stderr, "write").mockImplementation(
116
+ () => true,
117
+ );
118
+ try {
119
+ const env = getCurrentEnvironment();
120
+ expect(env.name).toBe("production");
121
+ expect(stderr).not.toHaveBeenCalled();
122
+ } finally {
123
+ stderr.mockRestore();
124
+ }
125
+ });
126
+
127
+ test("VELLUM_PLATFORM_URL overrides platformUrl on the resolved definition", () => {
128
+ process.env.VELLUM_ENVIRONMENT = "dev";
129
+ process.env.VELLUM_PLATFORM_URL = "https://custom.example.com";
130
+ const env = getCurrentEnvironment();
131
+ expect(env.name).toBe("dev");
132
+ expect(env.platformUrl).toBe("https://custom.example.com");
133
+ });
134
+
135
+ test("VELLUM_PLATFORM_URL override does not affect the seed table", () => {
136
+ process.env.VELLUM_PLATFORM_URL = "https://custom.example.com";
137
+ getCurrentEnvironment();
138
+ delete process.env.VELLUM_PLATFORM_URL;
139
+ const env = getCurrentEnvironment();
140
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
141
+ });
142
+
143
+ test("VELLUM_ASSISTANT_PLATFORM_URL overrides assistantPlatformUrl", () => {
144
+ process.env.VELLUM_ASSISTANT_PLATFORM_URL =
145
+ "http://host.docker.internal:8000";
146
+ const env = getCurrentEnvironment();
147
+ expect(env.assistantPlatformUrl).toBe("http://host.docker.internal:8000");
148
+ });
149
+
150
+ test("VELLUM_ASSISTANT_PLATFORM_URL does not shadow platformUrl", () => {
151
+ process.env.VELLUM_ASSISTANT_PLATFORM_URL = "http://override";
152
+ const env = getCurrentEnvironment();
153
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
154
+ });
155
+
156
+ test("does not auto-materialize a new environment from VELLUM_PLATFORM_URL alone", () => {
157
+ // Unknown env names fall back to production (parity with daemon + Swift).
158
+ // The per-field VELLUM_PLATFORM_URL override is intentionally dropped on
159
+ // the fallback path — fallback returns a pristine production seed so a
160
+ // typo'd env var can't accidentally stitch together a new environment.
161
+ process.env.VELLUM_ENVIRONMENT = "my-custom";
162
+ process.env.VELLUM_PLATFORM_URL = "https://my-custom.example.com";
163
+ const stderr = spyOn(process.stderr, "write").mockImplementation(
164
+ () => true,
165
+ );
166
+ try {
167
+ const env = getCurrentEnvironment();
168
+ expect(env.name).toBe("production");
169
+ expect(env.platformUrl).toBe("https://platform.vellum.ai");
170
+ expect(stderr).toHaveBeenCalledWith(
171
+ expect.stringContaining('unknown environment "my-custom"'),
172
+ );
173
+ } finally {
174
+ stderr.mockRestore();
175
+ }
176
+ });
177
+
178
+ test("VELLUM_LOCKFILE_DIR populates lockfileDirOverride on the resolved definition", () => {
179
+ process.env.VELLUM_LOCKFILE_DIR = "/tmp/test-lockfile-dir";
180
+ const env = getCurrentEnvironment();
181
+ expect(env.lockfileDirOverride).toBe("/tmp/test-lockfile-dir");
182
+ });
183
+
184
+ test("lockfileDirOverride is undefined when VELLUM_LOCKFILE_DIR is unset", () => {
185
+ const env = getCurrentEnvironment();
186
+ expect(env.lockfileDirOverride).toBeUndefined();
187
+ });
188
+
189
+ test("lockfileDirOverride applies to non-prod envs too", () => {
190
+ process.env.VELLUM_ENVIRONMENT = "dev";
191
+ process.env.VELLUM_LOCKFILE_DIR = "/tmp/test-lockfile-dir";
192
+ const env = getCurrentEnvironment();
193
+ expect(env.name).toBe("dev");
194
+ expect(env.lockfileDirOverride).toBe("/tmp/test-lockfile-dir");
195
+ });
196
+ });
197
+
198
+ describe("getSeed", () => {
199
+ test("returns a definition for a known seed", () => {
200
+ const seed = getSeed("dev");
201
+ expect(seed).toBeDefined();
202
+ expect(seed?.name).toBe("dev");
203
+ expect(seed?.platformUrl).toBe("https://dev-platform.vellum.ai");
204
+ });
205
+
206
+ test("returns undefined for an unknown name", () => {
207
+ expect(getSeed("no-such-env")).toBeUndefined();
208
+ });
209
+
210
+ test("returned seed is a copy — mutations do not affect the table", () => {
211
+ const seed = getSeed("dev");
212
+ if (seed) {
213
+ seed.platformUrl = "mutated";
214
+ }
215
+ const second = getSeed("dev");
216
+ expect(second?.platformUrl).toBe("https://dev-platform.vellum.ai");
217
+ });
218
+
219
+ test("all five canonical seeds exist", () => {
220
+ expect(getSeed("production")).toBeDefined();
221
+ expect(getSeed("staging")).toBeDefined();
222
+ expect(getSeed("test")).toBeDefined();
223
+ expect(getSeed("dev")).toBeDefined();
224
+ expect(getSeed("local")).toBeDefined();
225
+ });
226
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { getDefaultPorts } from "../paths.js";
4
+ import { SEEDS } from "../seeds.js";
5
+
6
+ describe("SEEDS port blocks", () => {
7
+ test("production uses the legacy (pre-MVP) port layout", () => {
8
+ const ports = getDefaultPorts(SEEDS.production!);
9
+ expect(ports).toEqual({
10
+ daemon: 7821,
11
+ gateway: 7830,
12
+ qdrant: 6333,
13
+ ces: 8090,
14
+ outboundProxy: 8080,
15
+ tcp: 8765,
16
+ });
17
+ });
18
+
19
+ test.each([
20
+ ["staging", 17000],
21
+ ["dev", 18000],
22
+ ["test", 19000],
23
+ ["local", 20000],
24
+ ] as const)("%s block starts at %i with 100-apart services", (name, base) => {
25
+ const ports = getDefaultPorts(SEEDS[name]!);
26
+ expect(ports).toEqual({
27
+ daemon: base,
28
+ gateway: base + 100,
29
+ qdrant: base + 200,
30
+ ces: base + 300,
31
+ outboundProxy: base + 400,
32
+ tcp: base + 500,
33
+ });
34
+ });
35
+
36
+ test("non-prod blocks are disjoint across environments", () => {
37
+ // 100 instances per service is the scan headroom in findAvailablePort,
38
+ // so a block "occupies" base…base+599 from daemon through tcp. Verify no
39
+ // two blocks overlap for any service.
40
+ const blocks = (["staging", "dev", "test", "local"] as const).map(
41
+ (name) => ({
42
+ name,
43
+ ports: getDefaultPorts(SEEDS[name]!),
44
+ }),
45
+ );
46
+ const allPorts = new Set<number>();
47
+ for (const { name, ports } of blocks) {
48
+ for (const port of Object.values(ports)) {
49
+ // Within each block, each service has 100 slots (base…base+99).
50
+ for (let offset = 0; offset < 100; offset++) {
51
+ const p = port + offset;
52
+ if (allPorts.has(p)) {
53
+ throw new Error(
54
+ `port ${p} (in ${name}'s block) overlaps another env's block`,
55
+ );
56
+ }
57
+ allPorts.add(p);
58
+ }
59
+ }
60
+ }
61
+ });
62
+
63
+ test("non-prod blocks sit below Linux's default ephemeral range (32768)", () => {
64
+ for (const name of ["staging", "dev", "test", "local"] as const) {
65
+ const ports = getDefaultPorts(SEEDS[name]!);
66
+ for (const port of Object.values(ports)) {
67
+ // Max port we'll ever scan to is base+99 for daemon/gateway/etc.
68
+ expect(port + 99).toBeLessThan(32768);
69
+ }
70
+ }
71
+ });
72
+ });
@@ -0,0 +1,109 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+
4
+ import type { EnvironmentDefinition, PortMap } from "./types.js";
5
+
6
+ const PRODUCTION_ENVIRONMENT_NAME = "production";
7
+
8
+ /**
9
+ * Production lockfile filenames in priority order. The current name is
10
+ * `.vellum.lock.json`; `.vellum.lockfile.json` is the legacy name kept for
11
+ * backward compatibility with installs that predate the rename.
12
+ */
13
+ const PRODUCTION_LOCKFILE_NAMES = [
14
+ ".vellum.lock.json",
15
+ ".vellum.lockfile.json",
16
+ ] as const;
17
+
18
+ const DEFAULT_PORTS: Readonly<PortMap> = {
19
+ daemon: 7821,
20
+ gateway: 7830,
21
+ qdrant: 6333,
22
+ ces: 8090,
23
+ outboundProxy: 8080,
24
+ tcp: 8765,
25
+ };
26
+
27
+ /**
28
+ * Config directory for an environment.
29
+ * Production preserves the existing `~/.config/vellum/` location;
30
+ * non-production environments use `$XDG_CONFIG_HOME/vellum-<env>/`.
31
+ */
32
+ export function getConfigDir(env: EnvironmentDefinition): string {
33
+ if (env.configDirOverride) return env.configDirOverride;
34
+ if (env.name === PRODUCTION_ENVIRONMENT_NAME) {
35
+ return join(xdgConfigHome(), "vellum");
36
+ }
37
+ return join(xdgConfigHome(), `vellum-${env.name}`);
38
+ }
39
+
40
+ /**
41
+ * Lockfile candidate paths for an environment, in priority order.
42
+ *
43
+ * For production, returns both the current `.vellum.lock.json` and the
44
+ * legacy `.vellum.lockfile.json` so read-side callers can fall back to the
45
+ * legacy filename on installs that predate the rename. Non-production
46
+ * environments are new and have a single canonical path under the env-scoped
47
+ * XDG config directory.
48
+ *
49
+ * Read-side callers should iterate this array and use the first existing
50
+ * file (matching `cli/src/lib/assistant-config.ts:readLockfile`). Write-side
51
+ * callers should use {@link getLockfilePath}, which returns the first
52
+ * (canonical) entry.
53
+ *
54
+ * `env.lockfileDirOverride` (populated by the resolver from
55
+ * `VELLUM_LOCKFILE_DIR`) overrides the directory the lockfile lives in for
56
+ * both production and non-production environments.
57
+ */
58
+ export function getLockfilePaths(env: EnvironmentDefinition): string[] {
59
+ if (env.name === PRODUCTION_ENVIRONMENT_NAME) {
60
+ const dir = env.lockfileDirOverride ?? homedir();
61
+ return PRODUCTION_LOCKFILE_NAMES.map((name) => join(dir, name));
62
+ }
63
+ const dir = env.lockfileDirOverride ?? getConfigDir(env);
64
+ return [join(dir, "lockfile.json")];
65
+ }
66
+
67
+ /**
68
+ * Canonical lockfile path for writes. For production this is the current
69
+ * `.vellum.lock.json` (legacy reads handled by {@link getLockfilePaths}).
70
+ */
71
+ export function getLockfilePath(env: EnvironmentDefinition): string {
72
+ return getLockfilePaths(env)[0]!;
73
+ }
74
+
75
+ /**
76
+ * Multi-instance root directory for an environment. Production uses
77
+ * `~/.local/share/vellum/assistants/` — the convention already in
78
+ * `cli/src/lib/assistant-config.ts`. Non-production environments use
79
+ * `~/.local/share/vellum-<env>/assistants/`.
80
+ */
81
+ export function getMultiInstanceDir(env: EnvironmentDefinition): string {
82
+ if (env.name === PRODUCTION_ENVIRONMENT_NAME) {
83
+ return join(xdgDataHome(), "vellum", "assistants");
84
+ }
85
+ return join(xdgDataHome(), `vellum-${env.name}`, "assistants");
86
+ }
87
+
88
+ /**
89
+ * Default port set for an environment.
90
+ * Seed entries for non-prod environments come with separate port ranges
91
+ * to avoid collisions in multi-env / multi-instance setups.
92
+ * Longer term, consider allocating ports dynamically at hatch/wake time.
93
+ */
94
+ export function getDefaultPorts(env: EnvironmentDefinition): PortMap {
95
+ return {
96
+ ...DEFAULT_PORTS,
97
+ ...(env.portsOverride ?? {}),
98
+ };
99
+ }
100
+
101
+ function xdgDataHome(): string {
102
+ return (
103
+ process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share")
104
+ );
105
+ }
106
+
107
+ function xdgConfigHome(): string {
108
+ return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
109
+ }
@@ -0,0 +1,96 @@
1
+ import { SEEDS } from "./seeds.js";
2
+ import type { EnvironmentDefinition } from "./types.js";
3
+
4
+ const DEFAULT_ENVIRONMENT_NAME = "production";
5
+
6
+ /**
7
+ * Look up a seed entry by name. Returns `undefined` if no seed matches.
8
+ * Callers that need the full resolution stack (env-var overrides, default
9
+ * fallback, error on unknown) should use {@link getCurrentEnvironment}
10
+ * instead. The returned definition is a shallow copy so mutations by the
11
+ * caller don't leak back into the seed table.
12
+ */
13
+ export function getSeed(name: string): EnvironmentDefinition | undefined {
14
+ const seed = SEEDS[name];
15
+ if (!seed) return undefined;
16
+ return { ...seed };
17
+ }
18
+
19
+ /**
20
+ * Resolve the current environment definition.
21
+ *
22
+ * Priority:
23
+ * 1. `override` argument (from a `--environment` CLI flag, when wired)
24
+ * 2. `VELLUM_ENVIRONMENT` env var
25
+ * 3. (future) user context file
26
+ * 4. Default: `production`
27
+ *
28
+ * Per-field env-var overrides are honored on the resolved definition as
29
+ * ad-hoc escape hatches (they do not materialize new environments):
30
+ * - `VELLUM_PLATFORM_URL` overrides `platformUrl`
31
+ * - `VELLUM_ASSISTANT_PLATFORM_URL` overrides `assistantPlatformUrl`
32
+ * - `VELLUM_LOCKFILE_DIR` overrides `lockfileDirOverride` (legacy e2e
33
+ * test hook)
34
+ *
35
+ * This function should be the single entrypoint for environment resolution.
36
+ * No other code should drive off `VELLUM_ENVIRONMENT` directly.
37
+ */
38
+ export function getCurrentEnvironment(
39
+ override?: string,
40
+ ): EnvironmentDefinition {
41
+ const name = resolveEnvironmentName(override);
42
+ const seed = SEEDS[name];
43
+ if (!seed) {
44
+ if (name !== DEFAULT_ENVIRONMENT_NAME) {
45
+ // Warn on stderr instead of throwing, to match the silent-fallback
46
+ // behavior in assistant/src/util/platform.ts:getXdgVellumConfigDirName
47
+ // and clients/shared/App/VellumEnvironment.swift:current. Those two
48
+ // silently fall back to production; the CLI should agree so all three
49
+ // writers don't end up in disjoint states on a typo.
50
+ process.stderr.write(
51
+ `warning: unknown environment "${name}"; falling back to "${DEFAULT_ENVIRONMENT_NAME}". ` +
52
+ `Add it to cli/src/lib/environments/seeds.ts and rebuild if this was intentional.\n`,
53
+ );
54
+ }
55
+ const fallback = SEEDS[DEFAULT_ENVIRONMENT_NAME];
56
+ if (!fallback) {
57
+ throw new Error(
58
+ `fatal: default environment "${DEFAULT_ENVIRONMENT_NAME}" missing from seed table — this is a build error`,
59
+ );
60
+ }
61
+ return { ...fallback };
62
+ }
63
+
64
+ const resolved: EnvironmentDefinition = { ...seed };
65
+
66
+ const platformUrlOverride = process.env.VELLUM_PLATFORM_URL?.trim();
67
+ if (platformUrlOverride) {
68
+ resolved.platformUrl = platformUrlOverride;
69
+ }
70
+
71
+ const assistantPlatformUrlOverride =
72
+ process.env.VELLUM_ASSISTANT_PLATFORM_URL?.trim();
73
+ if (assistantPlatformUrlOverride) {
74
+ resolved.assistantPlatformUrl = assistantPlatformUrlOverride;
75
+ }
76
+
77
+ const lockfileDirOverride = process.env.VELLUM_LOCKFILE_DIR?.trim();
78
+ if (lockfileDirOverride) {
79
+ resolved.lockfileDirOverride = lockfileDirOverride;
80
+ }
81
+
82
+ return resolved;
83
+ }
84
+
85
+ function resolveEnvironmentName(override: string | undefined): string {
86
+ const trimmedOverride = override?.trim();
87
+ if (trimmedOverride && trimmedOverride.length > 0) {
88
+ return trimmedOverride;
89
+ }
90
+ const envVar = process.env.VELLUM_ENVIRONMENT?.trim();
91
+ if (envVar && envVar.length > 0) {
92
+ return envVar;
93
+ }
94
+
95
+ return DEFAULT_ENVIRONMENT_NAME;
96
+ }
@@ -0,0 +1,74 @@
1
+ import type { EnvironmentDefinition, PortMap } from "./types.js";
2
+
3
+ /**
4
+ * Non-prod port blocks. Each environment gets a 1000-port window in the
5
+ * 17000–21000 band. Within a block, services are spaced 100 apart so up to
6
+ * 100 assistants can coexist without the scan (`findAvailablePort`) running
7
+ * one service's range into the next. Band chosen to sit below Linux's
8
+ * default ephemeral start (32768) and macOS's (49152), and away from the
9
+ * 3000/5000/8000/9000 dev-tool swamp. Production keeps its legacy,
10
+ * non-contiguous port set (7821/7830/6333/8090/8080/8765): cross-env
11
+ * collision is the only problem this change targets, prod is unaffected
12
+ * because only one env's assistants compete on a given machine, and
13
+ * churning it would leave existing hatches on 7821 while new ones
14
+ * allocated elsewhere.
15
+ */
16
+ function portBlock(base: number): PortMap {
17
+ return {
18
+ daemon: base,
19
+ gateway: base + 100,
20
+ qdrant: base + 200,
21
+ ces: base + 300,
22
+ outboundProxy: base + 400,
23
+ tcp: base + 500,
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Built-in environment definitions. Mirrors Swift's
29
+ * `clients/macos/vellum-assistant/App/VellumEnvironment.swift` enum and is
30
+ * the TS-side source of truth for the set of known environment names.
31
+ * Two other TS sites duplicate the name list:
32
+ * - `assistant/src/util/platform.ts` (`KNOWN_ENVIRONMENTS`)
33
+ * - `clients/chrome-extension/native-host/src/lockfile.ts`
34
+ * (`NON_PRODUCTION_ENVIRONMENTS`, excludes `production`)
35
+ * Drift between these three sites is caught at test time by
36
+ * `cli/src/__tests__/env-drift.test.ts`. Fast follow: hoist the shared
37
+ * list into a `packages/environments` package so all three sites import
38
+ * from one place.
39
+ *
40
+ * Custom environments via a user config file are a future phase — see the
41
+ * "Coexisting environments" design doc. Until then, a call site that needs a
42
+ * new environment must add it here and rebuild.
43
+ */
44
+ export const SEEDS: Record<string, EnvironmentDefinition> = {
45
+ production: {
46
+ name: "production",
47
+ platformUrl: "https://platform.vellum.ai",
48
+ },
49
+ staging: {
50
+ name: "staging",
51
+ platformUrl: "https://staging-platform.vellum.ai",
52
+ portsOverride: portBlock(17000),
53
+ },
54
+ test: {
55
+ name: "test",
56
+ // Non-functional URL — used only by unit tests for URL resolution, never
57
+ // hit in production.
58
+ platformUrl: "https://test-platform.vellum.ai",
59
+ portsOverride: portBlock(19000),
60
+ },
61
+ dev: {
62
+ name: "dev",
63
+ platformUrl: "https://dev-platform.vellum.ai",
64
+ portsOverride: portBlock(18000),
65
+ },
66
+ local: {
67
+ name: "local",
68
+ platformUrl: "http://localhost:8000",
69
+ // assistantPlatformUrl: "http://host.docker.internal:8000",
70
+ // ^ uncomment this once dockerized hatch path is live.
71
+ // The assistant runs in a different network namespace than the host.
72
+ portsOverride: portBlock(20000),
73
+ },
74
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Environment type definitions. Environments are deployment targets with
3
+ * their own platform backend and their own isolated on-host state. See the
4
+ * "Coexisting environments" design doc for the full model.
5
+ */
6
+
7
+ /**
8
+ * Per-service default port set. Phase 5 (per-environment port offsets) is
9
+ * deferred from MVP, so today every environment uses the same port set. The
10
+ * shape exists so the rest of the stack can call `getDefaultPorts(env)` and
11
+ * gain per-env offsets later without changing any call sites.
12
+ */
13
+ export interface PortMap {
14
+ daemon: number;
15
+ gateway: number;
16
+ qdrant: number;
17
+ ces: number;
18
+ outboundProxy: number;
19
+ tcp: number;
20
+ }
21
+
22
+ /**
23
+ * A resolved environment definition. Required fields are `name` and
24
+ * `platformUrl`. All other fields are optional and declared upfront — new
25
+ * fields are additive, never breaking. `name` is intentionally typed as
26
+ * `string` (not `keyof SEEDS`) so custom environments can be represented by
27
+ * future layers (user config file, ad-hoc env vars, etc.).
28
+ */
29
+ export interface EnvironmentDefinition {
30
+ name: string;
31
+ platformUrl: string;
32
+
33
+ /**
34
+ * Override for the platform URL the assistant process itself uses. Only
35
+ * differs from `platformUrl` when the assistant runs in a different network
36
+ * namespace than the host (e.g. Docker on macOS, where the host's localhost
37
+ * is reached via `host.docker.internal`). Falls back to `platformUrl` when
38
+ * unset.
39
+ */
40
+ assistantPlatformUrl?: string;
41
+
42
+ /** Human-readable label for UI surfaces. */
43
+ displayName?: string;
44
+
45
+ /** Hint for UI surfaces that want to tint or badge their display. */
46
+ tintColor?: string;
47
+
48
+ /** Per-service port overrides merged on top of defaults. */
49
+ portsOverride?: Partial<PortMap>;
50
+
51
+ /** Override for the XDG config directory. */
52
+ configDirOverride?: string;
53
+
54
+ /**
55
+ * Override for the directory containing the lockfile. Populated by the
56
+ * resolver from `VELLUM_LOCKFILE_DIR` (an existing e2e test escape hatch)
57
+ * so path helpers don't read env vars directly.
58
+ */
59
+ lockfileDirOverride?: string;
60
+ }