@vellumai/cli 0.5.6 → 0.5.8
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/knip.json +3 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +152 -13
- package/src/commands/hatch.ts +120 -65
- package/src/commands/restore.ts +359 -16
- package/src/commands/retire.ts +5 -5
- package/src/commands/rollback.ts +436 -142
- package/src/commands/upgrade.ts +575 -205
- package/src/index.ts +4 -4
- package/src/lib/assistant-config.ts +33 -6
- package/src/lib/aws.ts +15 -8
- package/src/lib/backup-ops.ts +213 -0
- package/src/lib/cli-error.ts +93 -0
- package/src/lib/config-utils.ts +59 -0
- package/src/lib/docker.ts +99 -50
- package/src/lib/doctor-client.ts +11 -1
- package/src/lib/gcp.ts +19 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +30 -9
- package/src/lib/platform-client.ts +205 -3
- package/src/lib/platform-releases.ts +112 -0
- package/src/lib/upgrade-lifecycle.ts +844 -0
package/knip.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"src/**/*.test.ts",
|
|
4
4
|
"src/**/__tests__/**/*.ts",
|
|
5
5
|
"src/adapters/openclaw-http-server.ts",
|
|
6
|
-
"src/lib/version-compat.ts"
|
|
6
|
+
"src/lib/version-compat.ts",
|
|
7
|
+
"src/lib/platform-releases.ts",
|
|
8
|
+
"src/lib/cli-error.ts"
|
|
7
9
|
],
|
|
8
10
|
"project": ["src/**/*.ts", "src/**/*.tsx"]
|
|
9
11
|
}
|
package/package.json
CHANGED
package/src/commands/backup.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { dirname, join } from "path";
|
|
4
3
|
|
|
5
4
|
import { findAssistantByName } from "../lib/assistant-config";
|
|
5
|
+
import { getBackupsDir, formatSize } from "../lib/backup-ops.js";
|
|
6
6
|
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function formatSize(bytes: number): string {
|
|
15
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
17
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
readPlatformToken,
|
|
9
|
+
fetchOrganizationId,
|
|
10
|
+
platformInitiateExport,
|
|
11
|
+
platformPollExportStatus,
|
|
12
|
+
platformDownloadExport,
|
|
13
|
+
} from "../lib/platform-client.js";
|
|
19
14
|
|
|
20
15
|
export async function backup(): Promise<void> {
|
|
21
16
|
const args = process.argv.slice(3);
|
|
@@ -67,6 +62,14 @@ export async function backup(): Promise<void> {
|
|
|
67
62
|
process.exit(1);
|
|
68
63
|
}
|
|
69
64
|
|
|
65
|
+
// Detect topology and route platform assistants through Django export
|
|
66
|
+
const cloud =
|
|
67
|
+
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
|
|
68
|
+
if (cloud === "vellum") {
|
|
69
|
+
await backupPlatform(name, outputArg);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
70
73
|
// Obtain an auth token
|
|
71
74
|
let accessToken: string;
|
|
72
75
|
const tokenData = loadGuardianToken(entry.assistantId);
|
|
@@ -104,6 +107,33 @@ export async function backup(): Promise<void> {
|
|
|
104
107
|
body: JSON.stringify({ description: "CLI backup" }),
|
|
105
108
|
signal: AbortSignal.timeout(120_000),
|
|
106
109
|
});
|
|
110
|
+
|
|
111
|
+
// Retry once with a fresh token on 401 — the cached token may be stale
|
|
112
|
+
// after a container restart that generated a new gateway signing key.
|
|
113
|
+
if (response.status === 401) {
|
|
114
|
+
let refreshedToken: string | null = null;
|
|
115
|
+
try {
|
|
116
|
+
const freshToken = await leaseGuardianToken(
|
|
117
|
+
entry.runtimeUrl,
|
|
118
|
+
entry.assistantId,
|
|
119
|
+
);
|
|
120
|
+
refreshedToken = freshToken.accessToken;
|
|
121
|
+
} catch {
|
|
122
|
+
// If token refresh fails, fall through to the !response.ok handler below
|
|
123
|
+
}
|
|
124
|
+
if (refreshedToken) {
|
|
125
|
+
accessToken = refreshedToken;
|
|
126
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Bearer ${accessToken}`,
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ description: "CLI backup" }),
|
|
133
|
+
signal: AbortSignal.timeout(120_000),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
107
137
|
} catch (err) {
|
|
108
138
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
109
139
|
console.error("Error: Export request timed out after 2 minutes.");
|
|
@@ -149,3 +179,112 @@ export async function backup(): Promise<void> {
|
|
|
149
179
|
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
150
180
|
}
|
|
151
181
|
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Platform (Vellum-hosted) backup via Django async migration export
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
async function backupPlatform(name: string, outputArg?: string): Promise<void> {
|
|
188
|
+
// Step 1 — Authenticate
|
|
189
|
+
const token = readPlatformToken();
|
|
190
|
+
if (!token) {
|
|
191
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let orgId: string;
|
|
196
|
+
try {
|
|
197
|
+
orgId = await fetchOrganizationId(token);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
201
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Step 2 — Initiate export job
|
|
208
|
+
let jobId: string;
|
|
209
|
+
try {
|
|
210
|
+
const result = await platformInitiateExport(token, orgId, "CLI backup");
|
|
211
|
+
jobId = result.jobId;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
215
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
if (msg.includes("429")) {
|
|
219
|
+
console.error(
|
|
220
|
+
"Too many export requests. Please wait before trying again.",
|
|
221
|
+
);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`Export started (job ${jobId})...`);
|
|
228
|
+
|
|
229
|
+
// Step 3 — Poll for completion
|
|
230
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
231
|
+
const TIMEOUT_MS = 5 * 60 * 1_000; // 5 minutes
|
|
232
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
233
|
+
let downloadUrl: string | undefined;
|
|
234
|
+
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
let status: { status: string; downloadUrl?: string; error?: string };
|
|
237
|
+
try {
|
|
238
|
+
status = await platformPollExportStatus(jobId, token, orgId);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
// Let non-transient errors (e.g. 404 "job not found") propagate immediately
|
|
242
|
+
if (msg.includes("not found")) {
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
console.warn(`Polling failed, retrying... (${msg})`);
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (status.status === "complete") {
|
|
251
|
+
downloadUrl = status.downloadUrl;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (status.status === "failed") {
|
|
256
|
+
console.error(`Export failed: ${status.error ?? "unknown error"}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Still in progress — wait and retry
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!downloadUrl) {
|
|
265
|
+
console.error("Export timed out after 5 minutes.");
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Step 4 — Download bundle
|
|
270
|
+
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
271
|
+
const outputPath =
|
|
272
|
+
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
273
|
+
|
|
274
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
275
|
+
|
|
276
|
+
const response = await platformDownloadExport(downloadUrl);
|
|
277
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
278
|
+
const data = new Uint8Array(arrayBuffer);
|
|
279
|
+
|
|
280
|
+
writeFileSync(outputPath, data);
|
|
281
|
+
|
|
282
|
+
// Step 5 — Print success
|
|
283
|
+
console.log(`Backup saved to ${outputPath}`);
|
|
284
|
+
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
285
|
+
|
|
286
|
+
const manifestSha = response.headers.get("X-Vbundle-Manifest-Sha256");
|
|
287
|
+
if (manifestSha) {
|
|
288
|
+
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/commands/hatch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
1
2
|
import {
|
|
2
3
|
appendFileSync,
|
|
3
4
|
existsSync,
|
|
@@ -39,12 +40,14 @@ import type { RemoteHost, Species } from "../lib/constants";
|
|
|
39
40
|
import { hatchDocker } from "../lib/docker";
|
|
40
41
|
import { hatchGcp } from "../lib/gcp";
|
|
41
42
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
43
|
+
import { buildNestedConfig, writeInitialConfig } from "../lib/config-utils";
|
|
42
44
|
import {
|
|
43
45
|
startLocalDaemon,
|
|
44
46
|
startGateway,
|
|
45
47
|
stopLocalProcesses,
|
|
46
48
|
} from "../lib/local";
|
|
47
49
|
import { maybeStartNgrokTunnel } from "../lib/ngrok";
|
|
50
|
+
import { getPlatformUrl } from "../lib/platform-client";
|
|
48
51
|
import { httpHealthCheck } from "../lib/http-client";
|
|
49
52
|
import { detectOrphanedProcesses } from "../lib/orphan-detection";
|
|
50
53
|
import { isProcessAlive, stopProcess } from "../lib/process";
|
|
@@ -99,8 +102,9 @@ export async function buildStartupScript(
|
|
|
99
102
|
providerApiKeys: Record<string, string>,
|
|
100
103
|
instanceName: string,
|
|
101
104
|
cloud: RemoteHost,
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
configValues: Record<string, string> = {},
|
|
106
|
+
): Promise<{ script: string; laptopBootstrapSecret: string }> {
|
|
107
|
+
const platformUrl = getPlatformUrl();
|
|
104
108
|
const logPath =
|
|
105
109
|
cloud === "custom"
|
|
106
110
|
? "/tmp/vellum-startup.log"
|
|
@@ -112,25 +116,57 @@ export async function buildStartupScript(
|
|
|
112
116
|
const ownershipFixup = buildOwnershipFixup();
|
|
113
117
|
|
|
114
118
|
if (species === "openclaw") {
|
|
115
|
-
|
|
119
|
+
const script = await buildOpenclawStartupScript(
|
|
116
120
|
sshUser,
|
|
117
121
|
providerApiKeys,
|
|
118
122
|
timestampRedirect,
|
|
119
123
|
userSetup,
|
|
120
124
|
ownershipFixup,
|
|
121
125
|
);
|
|
126
|
+
return { script, laptopBootstrapSecret: "" };
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
// Generate a bootstrap secret for the laptop that initiated this remote
|
|
130
|
+
// hatch. The startup script exports it as GUARDIAN_BOOTSTRAP_SECRET so
|
|
131
|
+
// that when `vellum hatch --remote docker` runs on the VM, the docker
|
|
132
|
+
// hatch detects the pre-set env var and appends its own secret.
|
|
133
|
+
const laptopBootstrapSecret = randomBytes(32).toString("hex");
|
|
134
|
+
|
|
124
135
|
// Build bash lines that set each provider API key as a shell variable
|
|
125
136
|
// and corresponding dotenv lines for the env file.
|
|
126
|
-
|
|
137
|
+
// Include the laptop bootstrap secret so that when the remote runs
|
|
138
|
+
// `vellum hatch --remote docker`, the docker hatch detects the pre-set
|
|
139
|
+
// env var and appends its own secret for multi-secret guardian init.
|
|
140
|
+
const allEnvEntries: Record<string, string> = {
|
|
141
|
+
...providerApiKeys,
|
|
142
|
+
GUARDIAN_BOOTSTRAP_SECRET: laptopBootstrapSecret,
|
|
143
|
+
};
|
|
144
|
+
const envSetLines = Object.entries(allEnvEntries)
|
|
127
145
|
.map(([envVar, value]) => `${envVar}=${value}`)
|
|
128
146
|
.join("\n");
|
|
129
147
|
const dotenvLines = Object.keys(providerApiKeys)
|
|
130
148
|
.map((envVar) => `${envVar}=\$${envVar}`)
|
|
131
149
|
.join("\n");
|
|
132
150
|
|
|
133
|
-
|
|
151
|
+
// Write --config key=value pairs to a temp JSON file on the remote host
|
|
152
|
+
// and export the env var so the daemon reads it on first boot.
|
|
153
|
+
let configWriteBlock = "";
|
|
154
|
+
if (Object.keys(configValues).length > 0) {
|
|
155
|
+
const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
|
|
156
|
+
configWriteBlock = `
|
|
157
|
+
echo "Writing default workspace config..."
|
|
158
|
+
VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
|
|
159
|
+
cat > "\$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH" << 'VELLUM_CONFIG_EOF'
|
|
160
|
+
${configJson}
|
|
161
|
+
VELLUM_CONFIG_EOF
|
|
162
|
+
export VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH
|
|
163
|
+
echo "Default workspace config written to \$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH"
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
laptopBootstrapSecret,
|
|
169
|
+
script: `#!/bin/bash
|
|
134
170
|
set -e
|
|
135
171
|
|
|
136
172
|
${timestampRedirect}
|
|
@@ -146,16 +182,19 @@ RUNTIME_HTTP_PORT=7821
|
|
|
146
182
|
DOTENV_EOF
|
|
147
183
|
|
|
148
184
|
${ownershipFixup}
|
|
149
|
-
|
|
185
|
+
${configWriteBlock}
|
|
186
|
+
export GUARDIAN_BOOTSTRAP_SECRET
|
|
150
187
|
export VELLUM_SSH_USER="\$SSH_USER"
|
|
151
188
|
export VELLUM_ASSISTANT_NAME="\$VELLUM_ASSISTANT_NAME"
|
|
189
|
+
export VELLUM_CLOUD="${cloud}"
|
|
152
190
|
echo "Downloading install script from ${platformUrl}/install.sh..."
|
|
153
191
|
curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
154
192
|
echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
|
|
155
193
|
chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
156
194
|
echo "Running install script..."
|
|
157
195
|
source ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
158
|
-
|
|
196
|
+
`,
|
|
197
|
+
};
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
const DEFAULT_REMOTE: RemoteHost = "local";
|
|
@@ -166,9 +205,9 @@ interface HatchArgs {
|
|
|
166
205
|
keepAlive: boolean;
|
|
167
206
|
name: string | null;
|
|
168
207
|
remote: RemoteHost;
|
|
169
|
-
daemonOnly: boolean;
|
|
170
208
|
restart: boolean;
|
|
171
209
|
watch: boolean;
|
|
210
|
+
configValues: Record<string, string>;
|
|
172
211
|
}
|
|
173
212
|
|
|
174
213
|
function parseArgs(): HatchArgs {
|
|
@@ -178,9 +217,9 @@ function parseArgs(): HatchArgs {
|
|
|
178
217
|
let keepAlive = false;
|
|
179
218
|
let name: string | null = null;
|
|
180
219
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
181
|
-
let daemonOnly = false;
|
|
182
220
|
let restart = false;
|
|
183
221
|
let watch = false;
|
|
222
|
+
const configValues: Record<string, string> = {};
|
|
184
223
|
|
|
185
224
|
for (let i = 0; i < args.length; i++) {
|
|
186
225
|
const arg = args[i];
|
|
@@ -199,9 +238,6 @@ function parseArgs(): HatchArgs {
|
|
|
199
238
|
console.log(
|
|
200
239
|
" --remote <host> Remote host (local, gcp, aws, docker, custom)",
|
|
201
240
|
);
|
|
202
|
-
console.log(
|
|
203
|
-
" --daemon-only Start assistant only, skip gateway",
|
|
204
|
-
);
|
|
205
241
|
console.log(
|
|
206
242
|
" --restart Restart processes without onboarding side effects",
|
|
207
243
|
);
|
|
@@ -211,11 +247,12 @@ function parseArgs(): HatchArgs {
|
|
|
211
247
|
console.log(
|
|
212
248
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
213
249
|
);
|
|
250
|
+
console.log(
|
|
251
|
+
" --config <key=value> Set a workspace config value (repeatable)",
|
|
252
|
+
);
|
|
214
253
|
process.exit(0);
|
|
215
254
|
} else if (arg === "-d") {
|
|
216
255
|
detached = true;
|
|
217
|
-
} else if (arg === "--daemon-only") {
|
|
218
|
-
daemonOnly = true;
|
|
219
256
|
} else if (arg === "--restart") {
|
|
220
257
|
restart = true;
|
|
221
258
|
} else if (arg === "--watch") {
|
|
@@ -248,11 +285,28 @@ function parseArgs(): HatchArgs {
|
|
|
248
285
|
}
|
|
249
286
|
remote = next as RemoteHost;
|
|
250
287
|
i++;
|
|
288
|
+
} else if (arg === "--config") {
|
|
289
|
+
const next = args[i + 1];
|
|
290
|
+
if (!next || next.startsWith("-")) {
|
|
291
|
+
console.error("Error: --config requires a key=value argument");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const eqIndex = next.indexOf("=");
|
|
295
|
+
if (eqIndex <= 0) {
|
|
296
|
+
console.error(
|
|
297
|
+
`Error: --config value must be in key=value format, got '${next}'`,
|
|
298
|
+
);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
const key = next.slice(0, eqIndex);
|
|
302
|
+
const value = next.slice(eqIndex + 1);
|
|
303
|
+
configValues[key] = value;
|
|
304
|
+
i++;
|
|
251
305
|
} else if (VALID_SPECIES.includes(arg as Species)) {
|
|
252
306
|
species = arg as Species;
|
|
253
307
|
} else {
|
|
254
308
|
console.error(
|
|
255
|
-
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --
|
|
309
|
+
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --restart, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
|
|
256
310
|
);
|
|
257
311
|
process.exit(1);
|
|
258
312
|
}
|
|
@@ -264,9 +318,9 @@ function parseArgs(): HatchArgs {
|
|
|
264
318
|
keepAlive,
|
|
265
319
|
name,
|
|
266
320
|
remote,
|
|
267
|
-
daemonOnly,
|
|
268
321
|
restart,
|
|
269
322
|
watch,
|
|
323
|
+
configValues,
|
|
270
324
|
};
|
|
271
325
|
}
|
|
272
326
|
|
|
@@ -574,10 +628,10 @@ function installCLISymlink(): void {
|
|
|
574
628
|
async function hatchLocal(
|
|
575
629
|
species: Species,
|
|
576
630
|
name: string | null,
|
|
577
|
-
daemonOnly: boolean = false,
|
|
578
631
|
restart: boolean = false,
|
|
579
632
|
watch: boolean = false,
|
|
580
633
|
keepAlive: boolean = false,
|
|
634
|
+
configValues: Record<string, string> = {},
|
|
581
635
|
): Promise<void> {
|
|
582
636
|
if (restart && !name && !process.env.VELLUM_ASSISTANT_NAME) {
|
|
583
637
|
console.error(
|
|
@@ -709,46 +763,44 @@ async function hatchLocal(
|
|
|
709
763
|
process.env.APP_VERSION = cliPkg.version;
|
|
710
764
|
}
|
|
711
765
|
|
|
712
|
-
|
|
766
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
|
|
767
|
+
|
|
768
|
+
await startLocalDaemon(watch, resources, { defaultWorkspaceConfigPath });
|
|
713
769
|
|
|
714
|
-
// When daemonOnly is set, skip gateway and ngrok — the caller only wants
|
|
715
|
-
// the daemon restarted (e.g. macOS app bootstrap retry).
|
|
716
770
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
771
|
+
try {
|
|
772
|
+
runtimeUrl = await startGateway(watch, resources);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
// Gateway failed — stop the daemon we just started so we don't leave
|
|
775
|
+
// orphaned processes with no lock file entry.
|
|
776
|
+
console.error(
|
|
777
|
+
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
778
|
+
);
|
|
779
|
+
await stopLocalProcesses(resources);
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
729
782
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
783
|
+
// Lease a guardian token so the desktop app can import it on first launch
|
|
784
|
+
// instead of hitting /v1/guardian/init itself.
|
|
785
|
+
try {
|
|
786
|
+
await leaseGuardianToken(runtimeUrl, instanceName);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error(`⚠️ Guardian token lease failed: ${err}`);
|
|
789
|
+
}
|
|
737
790
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
791
|
+
// Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
|
|
792
|
+
// Set BASE_DATA_DIR so ngrok reads the correct instance config.
|
|
793
|
+
const prevBaseDataDir = process.env.BASE_DATA_DIR;
|
|
794
|
+
process.env.BASE_DATA_DIR = resources.instanceDir;
|
|
795
|
+
const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
|
|
796
|
+
if (ngrokChild?.pid) {
|
|
797
|
+
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
798
|
+
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
799
|
+
}
|
|
800
|
+
if (prevBaseDataDir !== undefined) {
|
|
801
|
+
process.env.BASE_DATA_DIR = prevBaseDataDir;
|
|
802
|
+
} else {
|
|
803
|
+
delete process.env.BASE_DATA_DIR;
|
|
752
804
|
}
|
|
753
805
|
|
|
754
806
|
const localEntry: AssistantEntry = {
|
|
@@ -761,7 +813,7 @@ async function hatchLocal(
|
|
|
761
813
|
serviceGroupVersion: cliPkg.version ? `v${cliPkg.version}` : undefined,
|
|
762
814
|
resources,
|
|
763
815
|
};
|
|
764
|
-
if (!
|
|
816
|
+
if (!restart) {
|
|
765
817
|
saveAssistantEntry(localEntry);
|
|
766
818
|
setActiveAssistant(instanceName);
|
|
767
819
|
syncConfigToLockfile();
|
|
@@ -780,12 +832,8 @@ async function hatchLocal(
|
|
|
780
832
|
}
|
|
781
833
|
|
|
782
834
|
if (keepAlive) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const healthUrl = daemonOnly
|
|
786
|
-
? `http://127.0.0.1:${resources.daemonPort}/healthz`
|
|
787
|
-
: `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
788
|
-
const healthTarget = daemonOnly ? "Assistant" : "Gateway";
|
|
835
|
+
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
836
|
+
const healthTarget = "Gateway";
|
|
789
837
|
const POLL_INTERVAL_MS = 5000;
|
|
790
838
|
const MAX_FAILURES = 3;
|
|
791
839
|
let consecutiveFailures = 0;
|
|
@@ -839,9 +887,9 @@ export async function hatch(): Promise<void> {
|
|
|
839
887
|
keepAlive,
|
|
840
888
|
name,
|
|
841
889
|
remote,
|
|
842
|
-
daemonOnly,
|
|
843
890
|
restart,
|
|
844
891
|
watch,
|
|
892
|
+
configValues,
|
|
845
893
|
} = parseArgs();
|
|
846
894
|
|
|
847
895
|
if (restart && remote !== "local") {
|
|
@@ -859,22 +907,29 @@ export async function hatch(): Promise<void> {
|
|
|
859
907
|
}
|
|
860
908
|
|
|
861
909
|
if (remote === "local") {
|
|
862
|
-
await hatchLocal(species, name,
|
|
910
|
+
await hatchLocal(species, name, restart, watch, keepAlive, configValues);
|
|
863
911
|
return;
|
|
864
912
|
}
|
|
865
913
|
|
|
866
914
|
if (remote === "gcp") {
|
|
867
|
-
await hatchGcp(
|
|
915
|
+
await hatchGcp(
|
|
916
|
+
species,
|
|
917
|
+
detached,
|
|
918
|
+
name,
|
|
919
|
+
buildStartupScript,
|
|
920
|
+
watchHatching,
|
|
921
|
+
configValues,
|
|
922
|
+
);
|
|
868
923
|
return;
|
|
869
924
|
}
|
|
870
925
|
|
|
871
926
|
if (remote === "aws") {
|
|
872
|
-
await hatchAws(species, detached, name);
|
|
927
|
+
await hatchAws(species, detached, name, configValues);
|
|
873
928
|
return;
|
|
874
929
|
}
|
|
875
930
|
|
|
876
931
|
if (remote === "docker") {
|
|
877
|
-
await hatchDocker(species, detached, name, watch);
|
|
932
|
+
await hatchDocker(species, detached, name, watch, configValues);
|
|
878
933
|
return;
|
|
879
934
|
}
|
|
880
935
|
|