@vellumai/cli 0.11.8-staging.2 → 0.11.9-staging.1

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 (38) hide show
  1. package/README.md +6 -7
  2. package/knip.json +0 -1
  3. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +4 -1
  4. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  5. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  6. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  7. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  8. package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token-refresh.test.ts +2 -8
  9. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +2 -8
  10. package/node_modules/@vellumai/local-mode/src/__tests__/helpers/child-process-mock.ts +55 -0
  11. package/node_modules/@vellumai/local-mode/src/__tests__/retire.test.ts +2 -8
  12. package/node_modules/@vellumai/local-mode/src/__tests__/upgrade.test.ts +2 -8
  13. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +2 -8
  14. package/node_modules/@vellumai/local-mode/src/devices.test.ts +5 -8
  15. package/node_modules/@vellumai/local-mode/src/index.ts +1 -0
  16. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +6 -0
  17. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +165 -0
  18. package/node_modules/@vellumai/local-mode/src/lockfile.ts +63 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/package.json +1 -1
  25. package/src/__tests__/assistant-api-key-resolution.test.ts +102 -0
  26. package/src/__tests__/constants.test.ts +6 -2
  27. package/src/commands/hatch.ts +1 -367
  28. package/src/commands/login.ts +16 -29
  29. package/src/commands/teleport.ts +14 -26
  30. package/src/lib/assistant-api-key-resolution.ts +89 -0
  31. package/src/lib/aws.ts +0 -593
  32. package/src/lib/constants.ts +0 -2
  33. package/src/lib/gcp.ts +3 -671
  34. package/src/lib/platform-client.ts +58 -5
  35. package/src/__tests__/ssh-user-guard.test.ts +0 -28
  36. package/src/adapters/openclaw-http-server.ts +0 -209
  37. package/src/adapters/openclaw.ts +0 -131
  38. package/src/lib/openclaw-runtime-server.ts +0 -28
package/src/lib/gcp.ts CHANGED
@@ -1,214 +1,12 @@
1
- import { unlinkSync, writeFileSync } from "fs";
2
- import { tmpdir, userInfo } from "os";
3
- import { join } from "path";
4
-
5
- import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
6
- import type { AssistantEntry } from "./assistant-config";
7
- import { FIREWALL_TAG, GATEWAY_PORT } from "./constants";
8
- import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
9
- import type { Species } from "./constants";
10
- import { leaseGuardianToken } from "./guardian-token";
11
- import { getPlatformUrl } from "./platform-client";
12
- import { generateInstanceName } from "./random-name";
13
1
  import { exec, execOutput } from "./step-runner";
14
- import { emitProgress } from "./desktop-progress.js";
15
-
16
- export async function getActiveProject(): Promise<string> {
17
- const output = await execOutput("gcloud", ["config", "get-value", "project"]);
18
- const project = output.trim();
19
- if (!project || project === "(unset)") {
20
- throw new Error(
21
- "No active GCP project. Run `gcloud config set project <project>` first.",
22
- );
23
- }
24
- return project;
25
- }
26
-
27
- export interface FirewallRuleSpec {
28
- name: string;
29
- direction: "INGRESS" | "EGRESS";
30
- action: "ALLOW" | "DENY";
31
- rules: string;
32
- sourceRanges?: string;
33
- destinationRanges?: string;
34
- targetTags: string;
35
- description: string;
36
- }
37
-
38
- interface FirewallRuleState {
39
- name: string;
40
- direction: string;
41
- allowed: string;
42
- sourceRanges: string;
43
- destinationRanges: string;
44
- targetTags: string;
45
- description: string;
46
- }
47
-
48
- async function describeFirewallRule(
49
- ruleName: string,
50
- project: string,
51
- account?: string,
52
- ): Promise<FirewallRuleState | null> {
53
- try {
54
- const args = [
55
- "compute",
56
- "firewall-rules",
57
- "describe",
58
- ruleName,
59
- `--project=${project}`,
60
- "--format=json(name,direction,allowed,sourceRanges,destinationRanges,targetTags,description)",
61
- ];
62
- if (account) args.push(`--account=${account}`);
63
- const output = await execOutput("gcloud", args);
64
- const parsed = JSON.parse(output);
65
- const allowed = (parsed.allowed ?? [])
66
- .map((a: { IPProtocol: string; ports?: string[] }) => {
67
- const ports = a.ports ?? [];
68
- if (ports.length === 0) {
69
- return a.IPProtocol;
70
- }
71
- return ports.map((p: string) => `${a.IPProtocol}:${p}`).join(",");
72
- })
73
- .filter(Boolean)
74
- .join(",");
75
- return {
76
- name: parsed.name ?? "",
77
- direction: parsed.direction ?? "",
78
- allowed,
79
- sourceRanges: (parsed.sourceRanges ?? []).join(","),
80
- destinationRanges: (parsed.destinationRanges ?? []).join(","),
81
- targetTags: (parsed.targetTags ?? []).join(","),
82
- description: parsed.description ?? "",
83
- };
84
- } catch {
85
- return null;
86
- }
87
- }
88
2
 
89
- function ruleNeedsUpdate(
90
- spec: FirewallRuleSpec,
91
- state: FirewallRuleState,
92
- ): boolean {
93
- return (
94
- spec.direction !== state.direction ||
95
- spec.rules !== state.allowed ||
96
- (spec.sourceRanges ?? "") !== state.sourceRanges ||
97
- (spec.destinationRanges ?? "") !== state.destinationRanges ||
98
- spec.targetTags !== state.targetTags ||
99
- spec.description !== state.description
100
- );
101
- }
102
-
103
- async function createFirewallRule(
104
- spec: FirewallRuleSpec,
105
- project: string,
106
- account?: string,
107
- ): Promise<void> {
108
- const args = [
109
- "compute",
110
- "firewall-rules",
111
- "create",
112
- spec.name,
113
- `--project=${project}`,
114
- `--direction=${spec.direction}`,
115
- `--action=${spec.action}`,
116
- `--rules=${spec.rules}`,
117
- `--target-tags=${spec.targetTags}`,
118
- `--description=${spec.description}`,
119
- ];
120
- if (spec.sourceRanges) {
121
- args.push(`--source-ranges=${spec.sourceRanges}`);
122
- }
123
- if (spec.destinationRanges) {
124
- args.push(`--destination-ranges=${spec.destinationRanges}`);
125
- }
126
- if (account) args.push(`--account=${account}`);
127
- await exec("gcloud", args);
128
- }
129
-
130
- async function deleteFirewallRule(
131
- ruleName: string,
132
- project: string,
133
- account?: string,
134
- ): Promise<void> {
135
- const args = [
136
- "compute",
137
- "firewall-rules",
138
- "delete",
139
- ruleName,
140
- `--project=${project}`,
141
- "--quiet",
142
- ];
143
- if (account) args.push(`--account=${account}`);
144
- await exec("gcloud", args);
145
- }
146
-
147
- export async function syncFirewallRules(
148
- desiredRules: FirewallRuleSpec[],
149
- project: string,
150
- tag: string,
151
- account?: string,
152
- ): Promise<void> {
153
- let existingNames: string[];
154
- try {
155
- const listArgs = [
156
- "compute",
157
- "firewall-rules",
158
- "list",
159
- `--project=${project}`,
160
- "--format=json(name,targetTags)",
161
- ];
162
- if (account) listArgs.push(`--account=${account}`);
163
- const output = await execOutput("gcloud", listArgs);
164
- const allRules = JSON.parse(output) as Array<{
165
- name: string;
166
- targetTags?: string[];
167
- }>;
168
- existingNames = allRules
169
- .filter((r) => r.targetTags?.includes(tag))
170
- .map((r) => r.name);
171
- } catch {
172
- existingNames = [];
173
- }
174
-
175
- const desiredNames = new Set(desiredRules.map((r) => r.name));
176
-
177
- for (const existingName of existingNames) {
178
- if (!desiredNames.has(existingName)) {
179
- console.log(` 🗑️ Deleting stale firewall rule: ${existingName}`);
180
- await deleteFirewallRule(existingName, project, account);
181
- }
182
- }
183
-
184
- for (const spec of desiredRules) {
185
- const state = await describeFirewallRule(spec.name, project, account);
186
-
187
- if (!state) {
188
- console.log(` ➕ Creating firewall rule: ${spec.name}`);
189
- await createFirewallRule(spec, project, account);
190
- continue;
191
- }
192
-
193
- if (ruleNeedsUpdate(spec, state)) {
194
- console.log(` 🔄 Updating firewall rule: ${spec.name}`);
195
- await deleteFirewallRule(spec.name, project, account);
196
- await createFirewallRule(spec, project, account);
197
- continue;
198
- }
199
-
200
- console.log(` ✅ Firewall rule up to date: ${spec.name}`);
201
- }
202
- }
203
-
204
- export async function instanceExists(
3
+ async function instanceExists(
205
4
  instanceName: string,
206
5
  project: string,
207
6
  zone: string,
208
- account?: string,
209
7
  ): Promise<boolean> {
210
8
  try {
211
- const args = [
9
+ await execOutput("gcloud", [
212
10
  "compute",
213
11
  "instances",
214
12
  "describe",
@@ -216,9 +14,7 @@ export async function instanceExists(
216
14
  `--project=${project}`,
217
15
  `--zone=${zone}`,
218
16
  "--format=get(name)",
219
- ];
220
- if (account) args.push(`--account=${account}`);
221
- await execOutput("gcloud", args);
17
+ ]);
222
18
  return true;
223
19
  } catch (error) {
224
20
  const msg = error instanceof Error ? error.message.toLowerCase() : "";
@@ -232,45 +28,6 @@ export async function instanceExists(
232
28
  }
233
29
  }
234
30
 
235
- export async function fetchAndDisplayStartupLogs(
236
- instanceName: string,
237
- project: string,
238
- zone: string,
239
- account?: string,
240
- ): Promise<void> {
241
- try {
242
- const remoteCmd =
243
- 'echo "=== Last 50 lines of /var/log/startup-script.log ==="; ' +
244
- "tail -50 /var/log/startup-script.log 2>/dev/null || echo '(no startup log found)'; " +
245
- 'echo ""; ' +
246
- 'echo "=== /var/log/startup-error ==="; ' +
247
- "cat /var/log/startup-error 2>/dev/null || echo '(no error file found)'";
248
- const args = [
249
- "compute",
250
- "ssh",
251
- instanceName,
252
- `--project=${project}`,
253
- `--zone=${zone}`,
254
- "--quiet",
255
- "--ssh-flag=-o StrictHostKeyChecking=no",
256
- "--ssh-flag=-o UserKnownHostsFile=/dev/null",
257
- "--ssh-flag=-o ConnectTimeout=10",
258
- "--ssh-flag=-o LogLevel=ERROR",
259
- `--command=${remoteCmd}`,
260
- ];
261
- if (account) args.push(`--account=${account}`);
262
- const output = await execOutput("gcloud", args);
263
- console.log("📋 Startup logs from instance:");
264
- for (const line of output.split("\n")) {
265
- console.log(` ${line}`);
266
- }
267
- console.log("");
268
- } catch {
269
- console.log("⚠️ Could not retrieve startup logs from instance");
270
- console.log("");
271
- }
272
- }
273
-
274
31
  async function checkGcloudAvailable(): Promise<boolean> {
275
32
  try {
276
33
  await execOutput("gcloud", ["--version"]);
@@ -280,431 +37,6 @@ async function checkGcloudAvailable(): Promise<boolean> {
280
37
  }
281
38
  }
282
39
 
283
- export interface PollResult {
284
- lastLine: string | null;
285
- done: boolean;
286
- failed: boolean;
287
- errorContent: string;
288
- }
289
-
290
- export interface WatchHatchingResult {
291
- success: boolean;
292
- errorContent: string;
293
- }
294
-
295
- const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
296
- const MACHINE_TYPE = "e2-standard-4"; // 4 vCPUs, 16 GB memory
297
-
298
- const DESIRED_FIREWALL_RULES: FirewallRuleSpec[] = [
299
- {
300
- name: "allow-vellum-assistant-gateway",
301
- direction: "INGRESS",
302
- action: "ALLOW",
303
- rules: `tcp:${GATEWAY_PORT}`,
304
- sourceRanges: "0.0.0.0/0",
305
- targetTags: FIREWALL_TAG,
306
- description: `Allow gateway ingress on port ${GATEWAY_PORT} for vellum-assistant instances`,
307
- },
308
- {
309
- name: "allow-vellum-assistant-egress",
310
- direction: "EGRESS",
311
- action: "ALLOW",
312
- rules: "all",
313
- destinationRanges: "0.0.0.0/0",
314
- targetTags: FIREWALL_TAG,
315
- description: "Allow all egress traffic for vellum-assistant instances",
316
- },
317
- ];
318
-
319
- import INSTALL_SCRIPT_CONTENT from "../adapters/install.sh" with { type: "text" };
320
-
321
- function resolveInstallScriptPath(): string {
322
- const tmpPath = join(tmpdir(), `vellum-install-${process.pid}.sh`);
323
- writeFileSync(tmpPath, INSTALL_SCRIPT_CONTENT, { mode: 0o755 });
324
- return tmpPath;
325
- }
326
-
327
- async function pollInstance(
328
- instanceName: string,
329
- project: string,
330
- zone: string,
331
- account?: string,
332
- ): Promise<PollResult> {
333
- try {
334
- const remoteCmd =
335
- "L=$(tail -1 /var/log/startup-script.log 2>/dev/null || true); " +
336
- "S=$(systemctl is-active google-startup-scripts.service 2>/dev/null || true); " +
337
- "E=$(cat /var/log/startup-error 2>/dev/null || true); " +
338
- 'printf "%s\\n===HATCH_SEP===\\n%s\\n===HATCH_ERR===\\n%s" "$L" "$S" "$E"';
339
- const args = [
340
- "compute",
341
- "ssh",
342
- instanceName,
343
- `--project=${project}`,
344
- `--zone=${zone}`,
345
- "--quiet",
346
- "--ssh-flag=-o StrictHostKeyChecking=no",
347
- "--ssh-flag=-o UserKnownHostsFile=/dev/null",
348
- "--ssh-flag=-o ConnectTimeout=10",
349
- "--ssh-flag=-o LogLevel=ERROR",
350
- `--command=${remoteCmd}`,
351
- ];
352
- if (account) args.push(`--account=${account}`);
353
- const output = await execOutput("gcloud", args);
354
- const sepIdx = output.indexOf("===HATCH_SEP===");
355
- if (sepIdx === -1) {
356
- return {
357
- lastLine: output.trim() || null,
358
- done: false,
359
- failed: false,
360
- errorContent: "",
361
- };
362
- }
363
- const errIdx = output.indexOf("===HATCH_ERR===");
364
- const lastLine = output.substring(0, sepIdx).trim() || null;
365
- const statusEnd = errIdx === -1 ? undefined : errIdx;
366
- const status = output
367
- .substring(sepIdx + "===HATCH_SEP===".length, statusEnd)
368
- .trim();
369
- const errorContent =
370
- errIdx === -1
371
- ? ""
372
- : output.substring(errIdx + "===HATCH_ERR===".length).trim();
373
- const done =
374
- lastLine !== null && status !== "active" && status !== "activating";
375
- const failed = errorContent.length > 0 || status === "failed";
376
- return { lastLine, done, failed, errorContent };
377
- } catch {
378
- return { lastLine: null, done: false, failed: false, errorContent: "" };
379
- }
380
- }
381
-
382
- async function checkCurlFailure(
383
- instanceName: string,
384
- project: string,
385
- zone: string,
386
- account?: string,
387
- ): Promise<boolean> {
388
- try {
389
- const args = [
390
- "compute",
391
- "ssh",
392
- instanceName,
393
- `--project=${project}`,
394
- `--zone=${zone}`,
395
- "--quiet",
396
- "--ssh-flag=-o StrictHostKeyChecking=no",
397
- "--ssh-flag=-o UserKnownHostsFile=/dev/null",
398
- "--ssh-flag=-o ConnectTimeout=10",
399
- "--ssh-flag=-o LogLevel=ERROR",
400
- `--command=test -s ${INSTALL_SCRIPT_REMOTE_PATH} && echo EXISTS || echo MISSING`,
401
- ];
402
- if (account) args.push(`--account=${account}`);
403
- const output = await execOutput("gcloud", args);
404
- return output.trim() === "MISSING";
405
- } catch {
406
- return false;
407
- }
408
- }
409
-
410
- async function recoverFromCurlFailure(
411
- instanceName: string,
412
- project: string,
413
- zone: string,
414
- sshUser: string,
415
- account?: string,
416
- ): Promise<void> {
417
- const installScriptPath = resolveInstallScriptPath();
418
-
419
- const scpArgs = [
420
- "compute",
421
- "scp",
422
- installScriptPath,
423
- `${instanceName}:${INSTALL_SCRIPT_REMOTE_PATH}`,
424
- `--zone=${zone}`,
425
- `--project=${project}`,
426
- ];
427
- if (account) scpArgs.push(`--account=${account}`);
428
- console.log("\ud83d\udccb Uploading install script to instance...");
429
- await exec("gcloud", scpArgs);
430
-
431
- const sshArgs = [
432
- "compute",
433
- "ssh",
434
- `${sshUser}@${instanceName}`,
435
- `--zone=${zone}`,
436
- `--project=${project}`,
437
- `--command=source ${INSTALL_SCRIPT_REMOTE_PATH}`,
438
- ];
439
- if (account) sshArgs.push(`--account=${account}`);
440
- console.log("\ud83d\udd27 Running install script on instance...");
441
- await exec("gcloud", sshArgs);
442
- try {
443
- unlinkSync(installScriptPath);
444
- } catch {}
445
- }
446
-
447
- export async function hatchGcp(
448
- species: Species,
449
- detached: boolean,
450
- name: string | null,
451
- buildStartupScript: (
452
- species: Species,
453
- sshUser: string,
454
- providerApiKeys: Record<string, string>,
455
- instanceName: string,
456
- cloud: "gcp",
457
- configValues?: Record<string, string>,
458
- ) => Promise<{ script: string; laptopBootstrapSecret: string }>,
459
- watchHatching: (
460
- pollFn: () => Promise<PollResult>,
461
- instanceName: string,
462
- startTime: number,
463
- species: Species,
464
- ) => Promise<WatchHatchingResult>,
465
- configValues: Record<string, string> = {},
466
- ): Promise<void> {
467
- const startTime = Date.now();
468
- const account = process.env.GCP_ACCOUNT_EMAIL;
469
-
470
- try {
471
- const project = process.env.GCP_PROJECT ?? (await getActiveProject());
472
- let instanceName: string;
473
-
474
- instanceName = generateInstanceName(species, name);
475
-
476
- console.log(`\ud83e\udd5a Creating new assistant: ${instanceName}`);
477
- console.log(` Species: ${species}`);
478
- console.log(` Cloud: GCP`);
479
- console.log(` Project: ${project}`);
480
- const zone = process.env.GCP_DEFAULT_ZONE;
481
- if (!zone) {
482
- console.error("Error: GCP_DEFAULT_ZONE environment variable is not set.");
483
- process.exit(1);
484
- }
485
-
486
- console.log(` Zone: ${zone}`);
487
- console.log(` Machine type: ${MACHINE_TYPE}`);
488
- console.log("");
489
-
490
- if (name) {
491
- if (await instanceExists(name, project, zone, account)) {
492
- console.error(
493
- `Error: Instance name '${name}' is already taken. Please choose a different name.`,
494
- );
495
- process.exit(1);
496
- }
497
- } else {
498
- while (await instanceExists(instanceName, project, zone, account)) {
499
- console.log(
500
- `\u26a0\ufe0f Instance name ${instanceName} already exists, generating a new name...`,
501
- );
502
- instanceName = generateInstanceName(species);
503
- }
504
- }
505
-
506
- let sshUser: string;
507
- try {
508
- sshUser = userInfo().username;
509
- } catch {
510
- sshUser = process.env.USER ?? "";
511
- }
512
- if (!sshUser) {
513
- console.error(
514
- "Error: Could not determine SSH username. Set the USER environment variable and try again.",
515
- );
516
- process.exit(1);
517
- }
518
- const hatchedBy = process.env.VELLUM_HATCHED_BY;
519
- const providerApiKeys: Record<string, string> = {};
520
- for (const [, envVar] of Object.entries(PROVIDER_ENV_VAR_NAMES)) {
521
- const value = process.env[envVar];
522
- if (value) {
523
- providerApiKeys[envVar] = value;
524
- }
525
- }
526
- if (Object.keys(providerApiKeys).length === 0) {
527
- console.error(
528
- "Error: No provider API key environment variable is set. " +
529
- "Set at least one of: " +
530
- Object.values(PROVIDER_ENV_VAR_NAMES).join(", "),
531
- );
532
- process.exit(1);
533
- }
534
- emitProgress(1, 5, "Preparing startup script...");
535
- const { script: startupScript, laptopBootstrapSecret } =
536
- await buildStartupScript(
537
- species,
538
- sshUser,
539
- providerApiKeys,
540
- instanceName,
541
- "gcp",
542
- configValues,
543
- );
544
- const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
545
- writeFileSync(startupScriptPath, startupScript);
546
-
547
- emitProgress(2, 5, "Launching instance...");
548
- console.log("\ud83d\udd28 Creating instance with startup script...");
549
- try {
550
- const createArgs = [
551
- "compute",
552
- "instances",
553
- "create",
554
- instanceName,
555
- `--project=${project}`,
556
- `--zone=${zone}`,
557
- `--machine-type=${MACHINE_TYPE}`,
558
- "--image-family=debian-11",
559
- "--image-project=debian-cloud",
560
- "--boot-disk-size=50GB",
561
- "--boot-disk-type=pd-standard",
562
- `--metadata-from-file=startup-script=${startupScriptPath}`,
563
- `--labels=species=${species},vellum-assistant=true${hatchedBy ? `,hatched-by=${hatchedBy.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}` : ""}`,
564
- "--tags=vellum-assistant",
565
- "--no-service-account",
566
- "--no-scopes",
567
- ];
568
- if (account) createArgs.push(`--account=${account}`);
569
- await exec("gcloud", createArgs);
570
- } finally {
571
- try {
572
- unlinkSync(startupScriptPath);
573
- } catch {}
574
- }
575
-
576
- console.log("\ud83d\udd12 Syncing firewall rules...");
577
- await syncFirewallRules(
578
- DESIRED_FIREWALL_RULES,
579
- project,
580
- FIREWALL_TAG,
581
- account,
582
- );
583
-
584
- console.log(`\u2705 Instance ${instanceName} created successfully\n`);
585
-
586
- let externalIp: string | null = null;
587
- try {
588
- const describeArgs = [
589
- "compute",
590
- "instances",
591
- "describe",
592
- instanceName,
593
- `--project=${project}`,
594
- `--zone=${zone}`,
595
- "--format=get(networkInterfaces[0].accessConfigs[0].natIP)",
596
- ];
597
- if (account) describeArgs.push(`--account=${account}`);
598
- const ipOutput = await execOutput("gcloud", describeArgs);
599
- externalIp = ipOutput.trim() || null;
600
- } catch {
601
- console.log(
602
- "\u26a0\ufe0f Could not retrieve external IP yet (instance may still be starting)",
603
- );
604
- }
605
-
606
- const runtimeUrl = externalIp
607
- ? `http://${externalIp}:${GATEWAY_PORT}`
608
- : `http://${instanceName}:${GATEWAY_PORT}`;
609
- emitProgress(3, 5, "Saving configuration...");
610
- const gcpEntry: AssistantEntry = {
611
- assistantId: instanceName,
612
- runtimeUrl,
613
- cloud: "gcp",
614
- project,
615
- zone,
616
- species,
617
- sshUser,
618
- hatchedAt: new Date().toISOString(),
619
- };
620
- saveAssistantEntry(gcpEntry);
621
- setActiveAssistant(instanceName);
622
-
623
- if (detached) {
624
- console.log("\ud83d\ude80 Startup script is running on the instance...");
625
- console.log("");
626
- console.log("\u2705 Assistant is hatching!\n");
627
- console.log("Instance details:");
628
- console.log(` Name: ${instanceName}`);
629
- console.log(` Project: ${project}`);
630
- console.log(` Zone: ${zone}`);
631
- if (externalIp) {
632
- console.log(` External IP: ${externalIp}`);
633
- }
634
- console.log("");
635
- } else {
636
- console.log(" Press Ctrl+C to detach (instance will keep running)");
637
- console.log("");
638
-
639
- emitProgress(4, 5, "Installing software...");
640
- const result = await watchHatching(
641
- () => pollInstance(instanceName, project, zone, account),
642
- instanceName,
643
- startTime,
644
- species,
645
- );
646
-
647
- if (!result.success) {
648
- console.log("");
649
- if (result.errorContent) {
650
- console.log("\ud83d\udccb Startup error:");
651
- console.log(` ${result.errorContent}`);
652
- console.log("");
653
- }
654
-
655
- await fetchAndDisplayStartupLogs(instanceName, project, zone, account);
656
-
657
- if (
658
- species === "vellum" &&
659
- (await checkCurlFailure(instanceName, project, zone, account))
660
- ) {
661
- const installScriptUrl = `${getPlatformUrl()}/install.sh`;
662
- console.log(
663
- `\ud83d\udd04 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`,
664
- );
665
- await recoverFromCurlFailure(
666
- instanceName,
667
- project,
668
- zone,
669
- sshUser,
670
- account,
671
- );
672
- console.log("\u2705 Recovery successful!");
673
- } else {
674
- process.exit(1);
675
- }
676
- }
677
-
678
- emitProgress(5, 5, "Finalizing...");
679
- try {
680
- await leaseGuardianToken(
681
- runtimeUrl,
682
- instanceName,
683
- laptopBootstrapSecret,
684
- );
685
- } catch (err) {
686
- console.warn(
687
- `\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
688
- );
689
- }
690
-
691
- console.log("Instance details:");
692
- console.log(` Name: ${instanceName}`);
693
- console.log(` Project: ${project}`);
694
- console.log(` Zone: ${zone}`);
695
- if (externalIp) {
696
- console.log(` External IP: ${externalIp}`);
697
- }
698
- }
699
- } catch (error) {
700
- console.error(
701
- "\u274c Error:",
702
- error instanceof Error ? error.message : error,
703
- );
704
- process.exit(1);
705
- }
706
- }
707
-
708
40
  export async function retireInstance(
709
41
  name: string,
710
42
  project: string,