@vellumai/cli 0.7.0 → 0.7.2

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 (54) hide show
  1. package/AGENTS.md +3 -11
  2. package/README.md +49 -0
  3. package/bun.lock +0 -15
  4. package/package.json +1 -6
  5. package/src/__tests__/backup.test.ts +591 -0
  6. package/src/__tests__/config-utils.test.ts +35 -48
  7. package/src/__tests__/teleport.test.ts +597 -37
  8. package/src/commands/backup.ts +149 -70
  9. package/src/commands/client.ts +56 -14
  10. package/src/commands/events.ts +3 -0
  11. package/src/commands/exec.ts +34 -12
  12. package/src/commands/hatch.ts +3 -7
  13. package/src/commands/login.ts +15 -33
  14. package/src/commands/logs.ts +2 -7
  15. package/src/commands/ps.ts +41 -6
  16. package/src/commands/restore.ts +32 -47
  17. package/src/commands/setup.ts +38 -73
  18. package/src/commands/ssh.ts +2 -5
  19. package/src/commands/teleport.ts +148 -34
  20. package/src/commands/tunnel.ts +2 -7
  21. package/src/commands/upgrade.ts +114 -7
  22. package/src/commands/wake.ts +5 -16
  23. package/src/components/DefaultMainScreen.tsx +65 -129
  24. package/src/index.ts +2 -13
  25. package/src/lib/__tests__/docker.test.ts +50 -32
  26. package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
  27. package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
  28. package/src/lib/__tests__/runtime-url.test.ts +125 -0
  29. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  30. package/src/lib/assistant-client.ts +18 -26
  31. package/src/lib/assistant-config.ts +34 -41
  32. package/src/lib/backup-ops.ts +43 -17
  33. package/src/lib/cli-error.ts +1 -0
  34. package/src/lib/client-identity.ts +1 -1
  35. package/src/lib/config-utils.ts +1 -97
  36. package/src/lib/docker-statefulset.ts +381 -0
  37. package/src/lib/docker.ts +8 -247
  38. package/src/lib/guardian-token.ts +56 -6
  39. package/src/lib/hatch-local.ts +3 -26
  40. package/src/lib/job-polling.ts +1 -1
  41. package/src/lib/local-runtime-client.ts +162 -28
  42. package/src/lib/local.ts +35 -64
  43. package/src/lib/ngrok.ts +36 -26
  44. package/src/lib/platform-client.ts +97 -221
  45. package/src/lib/platform-releases.ts +23 -0
  46. package/src/lib/retire-local.ts +2 -2
  47. package/src/lib/runtime-url.ts +52 -0
  48. package/src/lib/sync-cloud-assistants.ts +126 -0
  49. package/src/lib/terminal-client.ts +6 -1
  50. package/src/lib/terminal-session.ts +127 -48
  51. package/src/lib/tui-log.ts +60 -0
  52. package/src/lib/upgrade-lifecycle.ts +65 -0
  53. package/src/lib/xdg-log.ts +10 -4
  54. package/src/commands/pair.ts +0 -212
@@ -1,7 +1,4 @@
1
- import {
2
- findAssistantByName,
3
- loadLatestAssistant,
4
- } from "../lib/assistant-config";
1
+ import { resolveAssistant } from "../lib/assistant-config";
5
2
  import { runNgrokTunnel } from "../lib/ngrok";
6
3
 
7
4
  const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
@@ -63,9 +60,7 @@ function parseArgs(): TunnelArgs {
63
60
  export async function tunnel(): Promise<void> {
64
61
  const { assistantName, provider } = parseArgs();
65
62
 
66
- const entry = assistantName
67
- ? findAssistantByName(assistantName)
68
- : loadLatestAssistant();
63
+ const entry = resolveAssistant(assistantName ?? undefined);
69
64
 
70
65
  if (!entry) {
71
66
  if (assistantName) {
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "crypto";
2
+ import { spawnSync } from "child_process";
2
3
 
3
4
  import cliPkg from "../../package.json";
4
5
 
@@ -16,7 +17,10 @@ import {
16
17
  startContainers,
17
18
  stopContainers,
18
19
  } from "../lib/docker";
19
- import { resolveImageRefs } from "../lib/platform-releases";
20
+ import {
21
+ fetchLatestStableVersion,
22
+ resolveImageRefs,
23
+ } from "../lib/platform-releases";
20
24
  import {
21
25
  authHeaders,
22
26
  getPlatformUrl,
@@ -36,6 +40,7 @@ import {
36
40
  buildStartingEvent,
37
41
  buildUpgradeCommitMessage,
38
42
  captureContainerEnv,
43
+ captureUpgradeFailureLogs,
39
44
  commitWorkspaceViaGateway,
40
45
  CONTAINER_ENV_EXCLUDE_KEYS,
41
46
  rollbackMigrations,
@@ -47,6 +52,7 @@ import { compareVersions } from "../lib/version-compat.js";
47
52
  interface UpgradeArgs {
48
53
  name: string | null;
49
54
  version: string | null;
55
+ latest: boolean;
50
56
  prepare: boolean;
51
57
  finalize: boolean;
52
58
  }
@@ -55,6 +61,7 @@ function parseArgs(): UpgradeArgs {
55
61
  const args = process.argv.slice(3);
56
62
  let name: string | null = null;
57
63
  let version: string | null = null;
64
+ let latest = false;
58
65
  let prepare = false;
59
66
  let finalize = false;
60
67
 
@@ -73,7 +80,10 @@ function parseArgs(): UpgradeArgs {
73
80
  console.log("");
74
81
  console.log("Options:");
75
82
  console.log(
76
- " --version <version> Target version to upgrade to (default: latest)",
83
+ " --version <version> Target version to upgrade to (default: CLI version)",
84
+ );
85
+ console.log(
86
+ " --latest Upgrade to the latest stable release, updating the CLI first if needed",
77
87
  );
78
88
  console.log(
79
89
  " --prepare Run pre-upgrade steps only (backup, notify) without swapping versions",
@@ -84,7 +94,10 @@ function parseArgs(): UpgradeArgs {
84
94
  console.log("");
85
95
  console.log("Examples:");
86
96
  console.log(
87
- " vellum upgrade # Upgrade the active assistant to the latest version",
97
+ " vellum upgrade # Upgrade the active assistant to the CLI's version",
98
+ );
99
+ console.log(
100
+ " vellum upgrade --latest # Upgrade CLI + assistant to the latest stable release",
88
101
  );
89
102
  console.log(
90
103
  " vellum upgrade my-assistant # Upgrade a specific assistant by name",
@@ -102,6 +115,8 @@ function parseArgs(): UpgradeArgs {
102
115
  }
103
116
  version = next;
104
117
  i++;
118
+ } else if (arg === "--latest") {
119
+ latest = true;
105
120
  } else if (arg === "--prepare") {
106
121
  prepare = true;
107
122
  } else if (arg === "--finalize") {
@@ -121,7 +136,13 @@ function parseArgs(): UpgradeArgs {
121
136
  process.exit(1);
122
137
  }
123
138
 
124
- return { name, version, prepare, finalize };
139
+ if (latest && version) {
140
+ console.error("Error: --latest and --version are mutually exclusive.");
141
+ emitCliError("UNKNOWN", "--latest and --version are mutually exclusive");
142
+ process.exit(1);
143
+ }
144
+
145
+ return { name, version, latest, prepare, finalize };
125
146
  }
126
147
 
127
148
  function resolveCloud(entry: AssistantEntry): string {
@@ -491,6 +512,11 @@ async function upgradeDocker(
491
512
  } else {
492
513
  console.error(`\nāŒ Containers failed to become ready within the timeout.`);
493
514
 
515
+ const logDir = await captureUpgradeFailureLogs(res, `${instanceName}-upgrade-failure`);
516
+ if (logDir) {
517
+ console.log(`šŸ“‹ Container logs saved to: ${logDir}`);
518
+ }
519
+
494
520
  if (previousImageRefs) {
495
521
  await broadcastUpgradeEvent(
496
522
  entry.runtimeUrl,
@@ -867,8 +893,80 @@ async function upgradeFinalize(
867
893
  );
868
894
  }
869
895
 
896
+ /**
897
+ * When `--latest` is passed, resolve the latest stable version from the
898
+ * platform API. If the running CLI is older than that version, self-update
899
+ * the CLI via `bun install -g` and re-exec so the new CLI's upgrade logic
900
+ * (and its cliPkg.version) drives the rest of the upgrade.
901
+ *
902
+ * Returns the resolved latest version string (e.g. "v0.7.0") for callers
903
+ * that need it. If the CLI was updated and re-exec'd, this function never
904
+ * returns — the process is replaced.
905
+ */
906
+ async function resolveLatestAndMaybeSelfUpdate(
907
+ name: string | null,
908
+ ): Promise<string> {
909
+ console.log("šŸ” Fetching latest stable release...");
910
+ const latestVersion = await fetchLatestStableVersion();
911
+ if (!latestVersion) {
912
+ console.error(
913
+ "Error: Could not determine the latest stable release from the platform API.",
914
+ );
915
+ emitCliError(
916
+ "UNKNOWN",
917
+ "Could not determine the latest stable release from the platform API",
918
+ );
919
+ process.exit(1);
920
+ }
921
+
922
+ const latestTag = latestVersion.startsWith("v")
923
+ ? latestVersion
924
+ : `v${latestVersion}`;
925
+ const currentTag = cliPkg.version ? `v${cliPkg.version}` : null;
926
+
927
+ console.log(` Latest stable: ${latestTag}`);
928
+ console.log(` CLI version: ${currentTag ?? "unknown"}\n`);
929
+
930
+ // Check if the CLI needs updating
931
+ const cmp = currentTag ? compareVersions(latestTag, currentTag) : null;
932
+ if (cmp !== null && cmp > 0) {
933
+ console.log(`šŸ”„ Updating CLI to ${latestTag}...`);
934
+ const installResult = spawnSync(
935
+ "bun",
936
+ ["install", "-g", `vellum@${latestVersion}`],
937
+ { stdio: "inherit" },
938
+ );
939
+ if (installResult.error || installResult.status !== 0) {
940
+ const detail =
941
+ installResult.error?.message ?? `exited with code ${installResult.status}`;
942
+ console.error(`\nāŒ CLI self-update failed: ${detail}`);
943
+ emitCliError("CLI_UPDATE_FAILED", "CLI self-update failed", detail);
944
+ process.exit(1);
945
+ }
946
+ console.log(`āœ… CLI updated to ${latestTag}\n`);
947
+
948
+ // Re-exec with the updated CLI. Pass --version instead of --latest
949
+ // to avoid re-fetching and to prevent infinite re-exec loops.
950
+ const reexecArgs = ["upgrade"];
951
+ if (name) reexecArgs.push(name);
952
+ reexecArgs.push("--version", latestTag);
953
+
954
+ console.log(`šŸš€ Re-running upgrade with updated CLI...\n`);
955
+ const reexecResult = spawnSync("vellum", reexecArgs, {
956
+ stdio: "inherit",
957
+ });
958
+ process.exit(reexecResult.status ?? 1);
959
+ }
960
+
961
+ if (cmp !== null && cmp === 0) {
962
+ console.log(`āœ… CLI is already on the latest version (${latestTag})\n`);
963
+ }
964
+
965
+ return latestTag;
966
+ }
967
+
870
968
  export async function upgrade(): Promise<void> {
871
- const { name, version, prepare, finalize } = parseArgs();
969
+ const { name, version, latest, prepare, finalize } = parseArgs();
872
970
  const entry = resolveTargetAssistant(name);
873
971
 
874
972
  if (prepare) {
@@ -881,16 +979,25 @@ export async function upgrade(): Promise<void> {
881
979
  return;
882
980
  }
883
981
 
982
+ // When --latest is passed, resolve the target from the platform API and
983
+ // self-update the CLI if it's behind. The resolved version is then used
984
+ // as the explicit target for the rest of the upgrade flow.
985
+ let effectiveVersion = version;
986
+ if (latest) {
987
+ const latestTag = await resolveLatestAndMaybeSelfUpdate(name);
988
+ effectiveVersion = latestTag;
989
+ }
990
+
884
991
  const cloud = resolveCloud(entry);
885
992
 
886
993
  try {
887
994
  if (cloud === "docker") {
888
- await upgradeDocker(entry, version);
995
+ await upgradeDocker(entry, effectiveVersion);
889
996
  return;
890
997
  }
891
998
 
892
999
  if (cloud === "vellum") {
893
- await upgradePlatform(entry, version);
1000
+ await upgradePlatform(entry, effectiveVersion);
894
1001
  return;
895
1002
  }
896
1003
  } catch (err) {
@@ -195,22 +195,11 @@ export async function wake(): Promise<void> {
195
195
  }
196
196
 
197
197
  // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
198
- // Scope BASE_DATA_DIR to the woken instance so ngrok reads the correct
199
- // instance config, then restore on any exit path.
200
- const prevBaseDataDir = process.env.BASE_DATA_DIR;
201
- process.env.BASE_DATA_DIR = resources.instanceDir;
202
- try {
203
- const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
204
- if (ngrokChild?.pid) {
205
- const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
206
- writeFileSync(ngrokPidFile, String(ngrokChild.pid));
207
- }
208
- } finally {
209
- if (prevBaseDataDir !== undefined) {
210
- process.env.BASE_DATA_DIR = prevBaseDataDir;
211
- } else {
212
- delete process.env.BASE_DATA_DIR;
213
- }
198
+ const workspaceDir = join(resources.instanceDir, ".vellum", "workspace");
199
+ const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort, workspaceDir);
200
+ if (ngrokChild?.pid) {
201
+ const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
202
+ writeFileSync(ngrokPidFile, String(ngrokChild.pid));
214
203
  }
215
204
 
216
205
  console.log("Wake complete.");