@vellumai/cli 0.10.6-dev.202607071937.200b337 → 0.10.6-dev.202607072040.fb971ce

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.6-dev.202607071937.200b337",
3
+ "version": "0.10.6-dev.202607072040.fb971ce",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,12 +30,14 @@ const realLocal = {
30
30
  generateLocalSigningKey: localModule.generateLocalSigningKey,
31
31
  startLocalDaemon: localModule.startLocalDaemon,
32
32
  startGateway: localModule.startGateway,
33
+ startCes: localModule.startCes,
33
34
  };
34
35
  const realExec = stepRunnerModule.exec;
35
36
 
36
- // Prevent real daemon / gateway from starting
37
+ // Prevent real daemon / gateway / CES from starting
37
38
  const startLocalDaemonMock = mock(async () => {});
38
39
  const startGatewayMock = mock(async () => {});
40
+ const startCesMock = mock(async () => {});
39
41
 
40
42
  // Capture exec calls without running real tar
41
43
  const execMock = mock(async (_cmd: string, _args: string[]) => {});
@@ -45,6 +47,7 @@ beforeAll(() => {
45
47
  generateLocalSigningKey: () => "deadbeefdeadbeefdeadbeefdeadbeef",
46
48
  startLocalDaemon: startLocalDaemonMock,
47
49
  startGateway: startGatewayMock,
50
+ startCes: startCesMock,
48
51
  }));
49
52
  mock.module("../lib/step-runner.js", () => ({ exec: execMock }));
50
53
  });
@@ -128,6 +131,7 @@ beforeEach(() => {
128
131
  execMock.mockClear();
129
132
  startLocalDaemonMock.mockClear();
130
133
  startGatewayMock.mockClear();
134
+ startCesMock.mockClear();
131
135
  });
132
136
 
133
137
  afterEach(() => {
@@ -119,6 +119,7 @@ const startGatewayMock = mock<typeof local.startGateway>(
119
119
  const stopLocalProcessesMock = mock<typeof local.stopLocalProcesses>(
120
120
  async () => {},
121
121
  );
122
+ const startCesMock = mock<typeof local.startCes>(async () => {});
122
123
 
123
124
  mock.module("../lib/local.js", () => ({
124
125
  ...realLocal,
@@ -127,6 +128,7 @@ mock.module("../lib/local.js", () => ({
127
128
  startLocalDaemon: startLocalDaemonMock,
128
129
  startGateway: startGatewayMock,
129
130
  stopLocalProcesses: stopLocalProcessesMock,
131
+ startCes: startCesMock,
130
132
  }));
131
133
 
132
134
  const loopbackSafeFetchMock = mock<typeof loopbackFetch.loopbackSafeFetch>(
@@ -256,6 +258,8 @@ beforeEach(() => {
256
258
  startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
257
259
  stopLocalProcessesMock.mockReset();
258
260
  stopLocalProcessesMock.mockResolvedValue(undefined);
261
+ startCesMock.mockReset();
262
+ startCesMock.mockResolvedValue(undefined);
259
263
  loopbackSafeFetchMock.mockReset();
260
264
  loopbackSafeFetchMock.mockResolvedValue({
261
265
  ok: true,
@@ -0,0 +1,104 @@
1
+ import { mkdtempSync, rmSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
5
+
6
+ import { startCes } from "../local.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Mocks
10
+ // ---------------------------------------------------------------------------
11
+
12
+ // Capture spawn calls so we can assert on cmd/env.
13
+ let lastSpawnCall: {
14
+ cmd: string[];
15
+ options: { detached?: boolean; env?: Record<string, string | undefined>; cwd?: string };
16
+ } | null = null;
17
+
18
+ mock.module("node:child_process", () => ({
19
+ spawn: mock((cmd: string, args: string[], options: object) => {
20
+ lastSpawnCall = { cmd: [cmd, ...args], options };
21
+ // Return a fake subprocess with a pid and no-op methods.
22
+ return {
23
+ pid: 42,
24
+ unref: () => {},
25
+ stdout: { on: () => {} },
26
+ stderr: { on: () => {} },
27
+ on: () => {},
28
+ };
29
+ }),
30
+ execSync: () => "",
31
+ execFileSync: () => "",
32
+ spawnSync: () => ({ status: 0, stdout: "", stderr: "" }),
33
+ }));
34
+
35
+ // Mock xdg-log so we don't open real log files.
36
+ mock.module("../xdg-log.js", () => ({
37
+ openLogFile: mock(() => 42),
38
+ pipeToLogFile: mock(() => {}),
39
+ }));
40
+
41
+ // Mock process helpers so stopProcessByPidFile is a no-op.
42
+ mock.module("../process.js", () => ({
43
+ stopProcessByPidFile: mock(async () => {}),
44
+ isProcessAlive: mock(() => false),
45
+ stopProcessGracefully: mock(async () => {}),
46
+ }));
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Tests
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe("startCes", () => {
53
+ let tempDir: string;
54
+
55
+ beforeAll(() => {
56
+ tempDir = mkdtempSync(join(tmpdir(), "ces-test-"));
57
+ });
58
+
59
+ afterAll(() => {
60
+ rmSync(tempDir, { recursive: true, force: true });
61
+ });
62
+
63
+ test("spawns CES with correct env vars and writes PID file", async () => {
64
+ // Create the socket path before calling startCes so the wait loop exits.
65
+ // startCes unlinks it first, then waits for it to reappear. We create it
66
+ // after a short delay to simulate CES binding the socket.
67
+ const resources = {
68
+ instanceDir: tempDir,
69
+ name: "test-assistant",
70
+ } as unknown as Parameters<typeof startCes>[1];
71
+
72
+ // Create the socket file asynchronously after startCes unlinks it.
73
+ // We use a small setTimeout to create it during the wait loop.
74
+ const vellumDir = join(tempDir, ".vellum");
75
+ mkdirSync(vellumDir, { recursive: true });
76
+
77
+ // Pre-create the socket so it exists when startCes checks after unlinking.
78
+ // startCes unlinks the stale socket, then polls for it. We create it with
79
+ // a slight delay so the poll catches it.
80
+ setTimeout(() => {
81
+ const socketDir = join(vellumDir, "workspace");
82
+ mkdirSync(socketDir, { recursive: true });
83
+ writeFileSync(join(socketDir, "ces.sock"), "");
84
+ }, 50);
85
+
86
+ lastSpawnCall = null;
87
+ await startCes(false, resources);
88
+
89
+ // Verify spawn was called
90
+ expect(lastSpawnCall).not.toBeNull();
91
+ expect(lastSpawnCall!.options.detached).toBe(true);
92
+
93
+ // Verify env vars
94
+ const env = lastSpawnCall!.options.env!;
95
+ expect(env["CES_STANDALONE"]).toBe("1");
96
+ expect(env["CES_LOCAL_SOCKET"]).toBeDefined();
97
+ expect(env["CREDENTIAL_SECURITY_DIR"]).toBeDefined();
98
+ expect(env["VELLUM_WORKSPACE_DIR"]).toBeDefined();
99
+
100
+ // Verify PID file was written
101
+ const cesPidFile = join(vellumDir, "ces.pid");
102
+ expect(existsSync(cesPidFile)).toBe(true);
103
+ }, 15_000);
104
+ });
package/src/lib/local.ts CHANGED
@@ -573,14 +573,12 @@ function applyDaemonEnvOverrides(
573
573
  env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
574
574
  options.defaultWorkspaceConfigPath;
575
575
  }
576
- // When the CLI launches CES as a sibling (CES_STANDALONE), pin the daemon to
577
- // the exact socket the sibling binds so the two agree regardless of any stale
578
- // CES_LOCAL_SOCKET inherited from the parent environment. The assistant then
579
- // connects to the sibling instead of spawning its own CES.
580
- if (isCesSiblingOptIn()) {
581
- env.CES_STANDALONE = "1";
582
- env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
583
- }
576
+ // Pin the daemon to the exact socket the sibling binds so the two agree
577
+ // regardless of any stale CES_LOCAL_SOCKET inherited from the parent
578
+ // environment. The assistant connects to the sibling instead of spawning
579
+ // its own CES.
580
+ env.CES_STANDALONE = "1";
581
+ env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
584
582
  applyIpcSocketDirOverride(env);
585
583
  }
586
584
 
@@ -850,19 +848,9 @@ function resolveCesSocketPath(resources?: LocalInstanceResources): string {
850
848
  }
851
849
 
852
850
  /**
853
- * Whether the CLI should launch CES as an independent sibling process instead
854
- * of leaving the assistant to spawn it as an stdio child. Temporary opt-in
855
- * (`CES_STANDALONE=1`) while local CES converges onto the sibling model that
856
- * containerized homes already use.
857
- */
858
- function isCesSiblingOptIn(): boolean {
859
- return process.env.CES_STANDALONE === "1";
860
- }
861
-
862
- /**
863
- * Launch the local CES sibling over a Unix socket (opted into via
864
- * `CES_STANDALONE=1`). No-op unless the opt-in is set, in which case the
865
- * assistant continues to spawn CES itself as today.
851
+ * Launch the local CES sibling over a Unix socket. The sibling model is now
852
+ * the default topology for local (non-containerized) instances, matching how
853
+ * containerized homes already run CES.
866
854
  *
867
855
  * The sibling runs with `CES_STANDALONE=1` so its lifecycle is anchored to
868
856
  * SIGTERM rather than stdin EOF, mirroring the gateway: a CLI-owned process
@@ -873,8 +861,6 @@ export async function startCes(
873
861
  watch: boolean = false,
874
862
  resources?: LocalInstanceResources,
875
863
  ): Promise<void> {
876
- if (!isCesSiblingOptIn()) return;
877
-
878
864
  const vellumDir = resources
879
865
  ? join(resources.instanceDir, ".vellum")
880
866
  : join(homedir(), ".vellum");