@vellumai/cli 0.1.5 → 0.1.7
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/commands/hatch.ts +58 -16
- package/src/lib/aws.ts +10 -5
- package/src/lib/gcp.ts +39 -0
package/package.json
CHANGED
package/src/commands/hatch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
|
-
import { existsSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { tmpdir, userInfo } from "os";
|
|
6
6
|
import { dirname, join } from "path";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "../lib/constants";
|
|
19
19
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
20
20
|
import type { FirewallRuleSpec } from "../lib/gcp";
|
|
21
|
-
import { getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
|
|
21
|
+
import { fetchAndDisplayStartupLogs, getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
|
|
22
22
|
import { buildInterfacesSeed } from "../lib/interfaces-seed";
|
|
23
23
|
import { generateRandomSuffix } from "../lib/random-name";
|
|
24
24
|
import { exec, execOutput } from "../lib/step-runner";
|
|
@@ -108,7 +108,7 @@ set -e
|
|
|
108
108
|
|
|
109
109
|
${timestampRedirect}
|
|
110
110
|
|
|
111
|
-
trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE" > /var/log/startup-error; fi' EXIT
|
|
111
|
+
trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > /var/log/startup-error; echo "Last 20 log lines:" >> /var/log/startup-error; tail -20 /var/log/startup-script.log >> /var/log/startup-error 2>/dev/null || true; fi' EXIT
|
|
112
112
|
${userSetup}
|
|
113
113
|
ANTHROPIC_API_KEY=${anthropicApiKey}
|
|
114
114
|
GATEWAY_RUNTIME_PROXY_ENABLED=true
|
|
@@ -135,8 +135,11 @@ CONFIG_EOF
|
|
|
135
135
|
${ownershipFixup}
|
|
136
136
|
|
|
137
137
|
export VELLUM_SSH_USER="\$SSH_USER"
|
|
138
|
+
echo "Downloading install script from ${platformUrl}/install.sh..."
|
|
138
139
|
curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
140
|
+
echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
|
|
139
141
|
chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
142
|
+
echo "Running install script..."
|
|
140
143
|
source ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
141
144
|
`;
|
|
142
145
|
}
|
|
@@ -196,6 +199,7 @@ export interface PollResult {
|
|
|
196
199
|
lastLine: string | null;
|
|
197
200
|
done: boolean;
|
|
198
201
|
failed: boolean;
|
|
202
|
+
errorContent: string;
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
async function pollInstance(
|
|
@@ -227,7 +231,7 @@ async function pollInstance(
|
|
|
227
231
|
const output = await execOutput("gcloud", args);
|
|
228
232
|
const sepIdx = output.indexOf("===HATCH_SEP===");
|
|
229
233
|
if (sepIdx === -1) {
|
|
230
|
-
return { lastLine: output.trim() || null, done: false, failed: false };
|
|
234
|
+
return { lastLine: output.trim() || null, done: false, failed: false, errorContent: "" };
|
|
231
235
|
}
|
|
232
236
|
const errIdx = output.indexOf("===HATCH_ERR===");
|
|
233
237
|
const lastLine = output.substring(0, sepIdx).trim() || null;
|
|
@@ -237,9 +241,9 @@ async function pollInstance(
|
|
|
237
241
|
errIdx === -1 ? "" : output.substring(errIdx + "===HATCH_ERR===".length).trim();
|
|
238
242
|
const done = lastLine !== null && status !== "active" && status !== "activating";
|
|
239
243
|
const failed = errorContent.length > 0 || status === "failed";
|
|
240
|
-
return { lastLine, done, failed };
|
|
244
|
+
return { lastLine, done, failed, errorContent };
|
|
241
245
|
} catch {
|
|
242
|
-
return { lastLine: null, done: false, failed: false };
|
|
246
|
+
return { lastLine: null, done: false, failed: false, errorContent: "" };
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
|
|
@@ -326,17 +330,23 @@ async function recoverFromCurlFailure(
|
|
|
326
330
|
await exec("gcloud", sshArgs);
|
|
327
331
|
}
|
|
328
332
|
|
|
333
|
+
export interface WatchHatchingResult {
|
|
334
|
+
success: boolean;
|
|
335
|
+
errorContent: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
329
338
|
export async function watchHatching(
|
|
330
339
|
pollFn: () => Promise<PollResult>,
|
|
331
340
|
instanceName: string,
|
|
332
341
|
startTime: number,
|
|
333
342
|
species: Species,
|
|
334
|
-
): Promise<
|
|
343
|
+
): Promise<WatchHatchingResult> {
|
|
335
344
|
let spinnerIdx = 0;
|
|
336
345
|
let lastLogLine: string | null = null;
|
|
337
346
|
let linesDrawn = 0;
|
|
338
347
|
let finished = false;
|
|
339
348
|
let failed = false;
|
|
349
|
+
let lastErrorContent = "";
|
|
340
350
|
let pollInFlight = false;
|
|
341
351
|
let nextPollAt = Date.now() + 15000;
|
|
342
352
|
|
|
@@ -386,6 +396,9 @@ export async function watchHatching(
|
|
|
386
396
|
if (result.lastLine) {
|
|
387
397
|
lastLogLine = result.lastLine;
|
|
388
398
|
}
|
|
399
|
+
if (result.errorContent) {
|
|
400
|
+
lastErrorContent = result.errorContent;
|
|
401
|
+
}
|
|
389
402
|
if (result.done) {
|
|
390
403
|
finished = true;
|
|
391
404
|
failed = result.failed;
|
|
@@ -396,12 +409,12 @@ export async function watchHatching(
|
|
|
396
409
|
}
|
|
397
410
|
}
|
|
398
411
|
|
|
399
|
-
return new Promise<
|
|
412
|
+
return new Promise<WatchHatchingResult>((resolve) => {
|
|
400
413
|
const interval = setInterval(() => {
|
|
401
414
|
if (finished) {
|
|
402
415
|
draw();
|
|
403
416
|
clearInterval(interval);
|
|
404
|
-
resolve(!failed);
|
|
417
|
+
resolve({ success: !failed, errorContent: lastErrorContent });
|
|
405
418
|
return;
|
|
406
419
|
}
|
|
407
420
|
|
|
@@ -412,7 +425,7 @@ export async function watchHatching(
|
|
|
412
425
|
console.log(` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
|
|
413
426
|
console.log(` Monitor with: vel logs ${instanceName}`);
|
|
414
427
|
console.log("");
|
|
415
|
-
resolve(true);
|
|
428
|
+
resolve({ success: true, errorContent: lastErrorContent });
|
|
416
429
|
return;
|
|
417
430
|
}
|
|
418
431
|
|
|
@@ -574,25 +587,32 @@ async function hatchGcp(
|
|
|
574
587
|
console.log(" Press Ctrl+C to detach (instance will keep running)");
|
|
575
588
|
console.log("");
|
|
576
589
|
|
|
577
|
-
const
|
|
590
|
+
const result = await watchHatching(
|
|
578
591
|
() => pollInstance(instanceName, project, zone, account),
|
|
579
592
|
instanceName,
|
|
580
593
|
startTime,
|
|
581
594
|
species,
|
|
582
595
|
);
|
|
583
596
|
|
|
584
|
-
if (!success) {
|
|
597
|
+
if (!result.success) {
|
|
598
|
+
console.log("");
|
|
599
|
+
if (result.errorContent) {
|
|
600
|
+
console.log("📋 Startup error:");
|
|
601
|
+
console.log(` ${result.errorContent}`);
|
|
602
|
+
console.log("");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
await fetchAndDisplayStartupLogs(instanceName, project, zone, account);
|
|
606
|
+
|
|
585
607
|
if (
|
|
586
608
|
species === "vellum" &&
|
|
587
609
|
(await checkCurlFailure(instanceName, project, zone, account))
|
|
588
610
|
) {
|
|
589
|
-
console.log("");
|
|
590
611
|
const installScriptUrl = `${process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai"}/install.sh`;
|
|
591
612
|
console.log(`🔄 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`);
|
|
592
613
|
await recoverFromCurlFailure(instanceName, project, zone, sshUser, account);
|
|
593
614
|
console.log("✅ Recovery successful!");
|
|
594
615
|
} else {
|
|
595
|
-
console.log("");
|
|
596
616
|
process.exit(1);
|
|
597
617
|
}
|
|
598
618
|
}
|
|
@@ -769,8 +789,17 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
|
|
|
769
789
|
console.warn("⚠️ Daemon socket did not appear within 10s — continuing anyway");
|
|
770
790
|
}
|
|
771
791
|
} else {
|
|
772
|
-
const
|
|
773
|
-
|
|
792
|
+
const sourceTreeIndex = join(import.meta.dir, "..", "..", "..", "assistant", "src", "index.ts");
|
|
793
|
+
let assistantIndex = sourceTreeIndex;
|
|
794
|
+
|
|
795
|
+
if (!existsSync(assistantIndex)) {
|
|
796
|
+
try {
|
|
797
|
+
const vellumPkgPath = _require.resolve("vellum/package.json");
|
|
798
|
+
assistantIndex = join(dirname(vellumPkgPath), "src", "index.ts");
|
|
799
|
+
} catch {
|
|
800
|
+
// resolve failed, will fall through to existsSync check below
|
|
801
|
+
}
|
|
802
|
+
}
|
|
774
803
|
|
|
775
804
|
if (!existsSync(assistantIndex)) {
|
|
776
805
|
throw new Error(
|
|
@@ -830,7 +859,20 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
|
|
|
830
859
|
console.log("");
|
|
831
860
|
}
|
|
832
861
|
|
|
862
|
+
function getCliVersion(): string {
|
|
863
|
+
try {
|
|
864
|
+
const pkgPath = join(import.meta.dir, "..", "..", "package.json");
|
|
865
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
866
|
+
return pkg.version ?? "unknown";
|
|
867
|
+
} catch {
|
|
868
|
+
return "unknown";
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
833
872
|
export async function hatch(): Promise<void> {
|
|
873
|
+
const cliVersion = getCliVersion();
|
|
874
|
+
console.log(`@vellumai/cli v${cliVersion}`);
|
|
875
|
+
|
|
834
876
|
const { species, detached, name, remote } = parseArgs();
|
|
835
877
|
|
|
836
878
|
if (remote === "local") {
|
package/src/lib/aws.ts
CHANGED
|
@@ -338,7 +338,7 @@ async function pollAwsInstance(
|
|
|
338
338
|
const output = await awsSshExec(ip, keyPath, remoteCmd);
|
|
339
339
|
const sepIdx = output.indexOf("===HATCH_SEP===");
|
|
340
340
|
if (sepIdx === -1) {
|
|
341
|
-
return { lastLine: output.trim() || null, done: false, failed: false };
|
|
341
|
+
return { lastLine: output.trim() || null, done: false, failed: false, errorContent: "" };
|
|
342
342
|
}
|
|
343
343
|
const errIdx = output.indexOf("===HATCH_ERR===");
|
|
344
344
|
const lastLine = output.substring(0, sepIdx).trim() || null;
|
|
@@ -348,9 +348,9 @@ async function pollAwsInstance(
|
|
|
348
348
|
errIdx === -1 ? "" : output.substring(errIdx + "===HATCH_ERR===".length).trim();
|
|
349
349
|
const done = lastLine !== null && status !== "running" && status !== "pending";
|
|
350
350
|
const failed = errorContent.length > 0 || status === "error";
|
|
351
|
-
return { lastLine, done, failed };
|
|
351
|
+
return { lastLine, done, failed, errorContent };
|
|
352
352
|
} catch {
|
|
353
|
-
return { lastLine: null, done: false, failed: false };
|
|
353
|
+
return { lastLine: null, done: false, failed: false, errorContent: "" };
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
@@ -488,15 +488,20 @@ export async function hatchAws(
|
|
|
488
488
|
|
|
489
489
|
if (externalIp) {
|
|
490
490
|
const ip = externalIp;
|
|
491
|
-
const
|
|
491
|
+
const result = await watchHatching(
|
|
492
492
|
() => pollAwsInstance(ip, keyPath),
|
|
493
493
|
instanceName,
|
|
494
494
|
startTime,
|
|
495
495
|
species,
|
|
496
496
|
);
|
|
497
497
|
|
|
498
|
-
if (!success) {
|
|
498
|
+
if (!result.success) {
|
|
499
499
|
console.log("");
|
|
500
|
+
if (result.errorContent) {
|
|
501
|
+
console.log("📋 Startup error:");
|
|
502
|
+
console.log(` ${result.errorContent}`);
|
|
503
|
+
console.log("");
|
|
504
|
+
}
|
|
500
505
|
process.exit(1);
|
|
501
506
|
}
|
|
502
507
|
} else {
|
package/src/lib/gcp.ts
CHANGED
|
@@ -274,6 +274,45 @@ export async function sshCommand(
|
|
|
274
274
|
]);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
export async function fetchAndDisplayStartupLogs(
|
|
278
|
+
instanceName: string,
|
|
279
|
+
project: string,
|
|
280
|
+
zone: string,
|
|
281
|
+
account?: string,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
try {
|
|
284
|
+
const remoteCmd =
|
|
285
|
+
'echo "=== Last 50 lines of /var/log/startup-script.log ==="; ' +
|
|
286
|
+
"tail -50 /var/log/startup-script.log 2>/dev/null || echo '(no startup log found)'; " +
|
|
287
|
+
'echo ""; ' +
|
|
288
|
+
'echo "=== /var/log/startup-error ==="; ' +
|
|
289
|
+
"cat /var/log/startup-error 2>/dev/null || echo '(no error file found)'";
|
|
290
|
+
const args = [
|
|
291
|
+
"compute",
|
|
292
|
+
"ssh",
|
|
293
|
+
instanceName,
|
|
294
|
+
`--project=${project}`,
|
|
295
|
+
`--zone=${zone}`,
|
|
296
|
+
"--quiet",
|
|
297
|
+
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
298
|
+
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
299
|
+
"--ssh-flag=-o ConnectTimeout=10",
|
|
300
|
+
"--ssh-flag=-o LogLevel=ERROR",
|
|
301
|
+
`--command=${remoteCmd}`,
|
|
302
|
+
];
|
|
303
|
+
if (account) args.push(`--account=${account}`);
|
|
304
|
+
const output = await execOutput("gcloud", args);
|
|
305
|
+
console.log("📋 Startup logs from instance:");
|
|
306
|
+
for (const line of output.split("\n")) {
|
|
307
|
+
console.log(` ${line}`);
|
|
308
|
+
}
|
|
309
|
+
console.log("");
|
|
310
|
+
} catch {
|
|
311
|
+
console.log("⚠️ Could not retrieve startup logs from instance");
|
|
312
|
+
console.log("");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
277
316
|
export async function retireInstance(
|
|
278
317
|
name: string,
|
|
279
318
|
project: string,
|