@vellumai/cli 0.10.5-dev.202607051436.bba51d6 → 0.10.5-dev.202607051627.ba77975
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 +1 -1
- package/src/__tests__/wake.test.ts +34 -0
- package/src/commands/hatch.ts +1 -1
- package/src/commands/wake.ts +14 -1
- package/src/index.ts +59 -0
package/package.json
CHANGED
|
@@ -90,11 +90,16 @@ const resolveProcessStateMock = mock<typeof processLib.resolveProcessState>(
|
|
|
90
90
|
const stopProcessByPidFileMock = mock<typeof processLib.stopProcessByPidFile>(
|
|
91
91
|
async () => true,
|
|
92
92
|
);
|
|
93
|
+
const isProcessAliveMock = mock<typeof processLib.isProcessAlive>(() => ({
|
|
94
|
+
alive: false,
|
|
95
|
+
pid: null,
|
|
96
|
+
}));
|
|
93
97
|
|
|
94
98
|
mock.module("../lib/process", () => ({
|
|
95
99
|
...realProcessLib,
|
|
96
100
|
resolveProcessState: resolveProcessStateMock,
|
|
97
101
|
stopProcessByPidFile: stopProcessByPidFileMock,
|
|
102
|
+
isProcessAlive: isProcessAliveMock,
|
|
98
103
|
}));
|
|
99
104
|
|
|
100
105
|
const generateLocalSigningKeyMock = mock<typeof local.generateLocalSigningKey>(
|
|
@@ -110,6 +115,7 @@ const startLocalDaemonMock = mock<typeof local.startLocalDaemon>(async () => {})
|
|
|
110
115
|
const startGatewayMock = mock<typeof local.startGateway>(
|
|
111
116
|
async () => "http://127.0.0.1:7830",
|
|
112
117
|
);
|
|
118
|
+
const startCesMock = mock<typeof local.startCes>(async () => {});
|
|
113
119
|
|
|
114
120
|
mock.module("../lib/local", () => ({
|
|
115
121
|
...realLocal,
|
|
@@ -118,6 +124,7 @@ mock.module("../lib/local", () => ({
|
|
|
118
124
|
isGatewayWatchModeAvailable: isGatewayWatchModeAvailableMock,
|
|
119
125
|
startLocalDaemon: startLocalDaemonMock,
|
|
120
126
|
startGateway: startGatewayMock,
|
|
127
|
+
startCes: startCesMock,
|
|
121
128
|
}));
|
|
122
129
|
|
|
123
130
|
const maybeStartNgrokTunnelMock = mock<typeof ngrok.maybeStartNgrokTunnel>(
|
|
@@ -184,6 +191,10 @@ beforeEach(() => {
|
|
|
184
191
|
startLocalDaemonMock.mockResolvedValue(undefined);
|
|
185
192
|
startGatewayMock.mockReset();
|
|
186
193
|
startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
|
|
194
|
+
startCesMock.mockReset();
|
|
195
|
+
startCesMock.mockResolvedValue(undefined);
|
|
196
|
+
isProcessAliveMock.mockReset();
|
|
197
|
+
isProcessAliveMock.mockReturnValue({ alive: false, pid: null });
|
|
187
198
|
seedGuardianTokenFromSiblingEnvMock.mockReset();
|
|
188
199
|
seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
|
|
189
200
|
loadGuardianTokenMock.mockReset();
|
|
@@ -291,4 +302,27 @@ describe("vellum wake", () => {
|
|
|
291
302
|
"generated-bootstrap-secret",
|
|
292
303
|
);
|
|
293
304
|
});
|
|
305
|
+
|
|
306
|
+
test("relaunches CES sibling when daemon is healthy but CES is dead", async () => {
|
|
307
|
+
// Daemon is healthy (default mock) but CES pid says dead.
|
|
308
|
+
// isProcessAliveMock defaults to { alive: false }.
|
|
309
|
+
await wake();
|
|
310
|
+
|
|
311
|
+
// startCes should be called to relaunch the dead sibling.
|
|
312
|
+
expect(startCesMock).toHaveBeenCalledTimes(1);
|
|
313
|
+
// startLocalDaemon should NOT be called (daemon already running).
|
|
314
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("does NOT relaunch CES sibling when both daemon and CES are healthy", async () => {
|
|
318
|
+
// Daemon is healthy (default mock). CES is also alive.
|
|
319
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 789 });
|
|
320
|
+
|
|
321
|
+
await wake();
|
|
322
|
+
|
|
323
|
+
// startCes should NOT be called (CES is alive).
|
|
324
|
+
expect(startCesMock).not.toHaveBeenCalled();
|
|
325
|
+
// startLocalDaemon should NOT be called (daemon already running).
|
|
326
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
294
328
|
});
|
package/src/commands/hatch.ts
CHANGED
|
@@ -146,7 +146,7 @@ ${userSetup}
|
|
|
146
146
|
${envSetLines}
|
|
147
147
|
VELLUM_ASSISTANT_NAME=${instanceName}
|
|
148
148
|
mkdir -p "\$HOME/.config/vellum"
|
|
149
|
-
cat > "\$HOME/.config/vellum
|
|
149
|
+
cat > "\$HOME/.config/vellum/.env" << DOTENV_EOF
|
|
150
150
|
${dotenvLines}
|
|
151
151
|
RUNTIME_HTTP_PORT=7821
|
|
152
152
|
DOTENV_EOF
|
package/src/commands/wake.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
resetGuardianBootstrap,
|
|
13
13
|
seedGuardianTokenFromSiblingEnv,
|
|
14
14
|
} from "../lib/guardian-token.js";
|
|
15
|
-
import { resolveProcessState, stopProcessByPidFile } from "../lib/process";
|
|
15
|
+
import { resolveProcessState, stopProcessByPidFile, isProcessAlive } from "../lib/process";
|
|
16
16
|
import {
|
|
17
17
|
generateLocalSigningKey,
|
|
18
18
|
isAssistantWatchModeAvailable,
|
|
@@ -189,6 +189,19 @@ export async function wake(): Promise<void> {
|
|
|
189
189
|
startCes(watch, resources),
|
|
190
190
|
startLocalDaemon(watch, resources, { foreground, signingKey }),
|
|
191
191
|
]);
|
|
192
|
+
} else {
|
|
193
|
+
// Self-heal: the daemon is already healthy, but the CES sibling may have
|
|
194
|
+
// died independently (crash, OOM kill). A dead ces.pid under a live daemon
|
|
195
|
+
// means credential operations will fail until the next wake. Relaunch the
|
|
196
|
+
// sibling so the daemon's lazy reconnect (secure-keys.ts) picks it up on
|
|
197
|
+
// the next credential read. startCes is a no-op unless CES_STANDALONE.
|
|
198
|
+
const vellumDir = join(resources.instanceDir, ".vellum");
|
|
199
|
+
const cesPidFile = join(vellumDir, "ces.pid");
|
|
200
|
+
const cesAlive = isProcessAlive(cesPidFile).alive;
|
|
201
|
+
if (!cesAlive) {
|
|
202
|
+
console.log("CES sibling not running — relaunching...");
|
|
203
|
+
await startCes(watch, resources);
|
|
204
|
+
}
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
// Start gateway
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
import { resolveConfigDir } from "@vellumai/local-mode";
|
|
7
|
+
|
|
3
8
|
import cliPkg from "../package.json";
|
|
4
9
|
import { backup } from "./commands/backup";
|
|
5
10
|
import { clean } from "./commands/clean";
|
|
@@ -156,6 +161,56 @@ function applyNoColorFlags(argv: string[]): void {
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Load env vars from the vellum config dotenv file into `process.env` so
|
|
166
|
+
* that `vellum hatch` forwards provider API keys to containers and other
|
|
167
|
+
* commands have access to them.
|
|
168
|
+
*
|
|
169
|
+
* Reads `$XDG_CONFIG_HOME/vellum{-env}/.env` — the same config directory
|
|
170
|
+
* the CLI uses for guardian tokens and environment state. The file is
|
|
171
|
+
* written by remote-hatch scripts and can be user-managed.
|
|
172
|
+
*
|
|
173
|
+
* Existing `process.env` values take precedence (standard dotenv convention).
|
|
174
|
+
* Only KEY=VALUE lines are parsed. Lines starting with # are comments.
|
|
175
|
+
* Values may be quoted with single or double quotes.
|
|
176
|
+
*/
|
|
177
|
+
function loadConfigDotenv(): void {
|
|
178
|
+
const configDir = resolveConfigDir(process.env);
|
|
179
|
+
const envPath = join(configDir, ".env");
|
|
180
|
+
|
|
181
|
+
let content: string;
|
|
182
|
+
try {
|
|
183
|
+
content = readFileSync(envPath, "utf-8");
|
|
184
|
+
} catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const line of content.split("\n")) {
|
|
189
|
+
const trimmed = line.trim();
|
|
190
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
191
|
+
|
|
192
|
+
const eqIndex = trimmed.indexOf("=");
|
|
193
|
+
if (eqIndex === -1) continue;
|
|
194
|
+
|
|
195
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
196
|
+
if (!key) continue;
|
|
197
|
+
|
|
198
|
+
// Existing env vars take precedence (dotenv convention).
|
|
199
|
+
if (process.env[key] !== undefined) continue;
|
|
200
|
+
|
|
201
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
202
|
+
// Strip surrounding quotes.
|
|
203
|
+
if (
|
|
204
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
205
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
206
|
+
) {
|
|
207
|
+
value = value.slice(1, -1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
process.env[key] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
159
214
|
/**
|
|
160
215
|
* If a running assistant is detected, launch the TUI client and return true.
|
|
161
216
|
* Otherwise return false so the caller can fall back to help text.
|
|
@@ -181,6 +236,10 @@ async function tryLaunchClient(): Promise<boolean> {
|
|
|
181
236
|
}
|
|
182
237
|
|
|
183
238
|
async function main() {
|
|
239
|
+
// Load $XDG_CONFIG_HOME/vellum/.env before any command runs so
|
|
240
|
+
// provider API keys and other config are available to hatch, exec, etc.
|
|
241
|
+
loadConfigDotenv();
|
|
242
|
+
|
|
184
243
|
const args = process.argv.slice(2);
|
|
185
244
|
|
|
186
245
|
// Must run before any command or terminal-capabilities usage
|