@vellumai/cli 0.10.4-dev.202607012136.5ef7865 → 0.10.4-dev.202607012234.a9be056

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.4-dev.202607012136.5ef7865",
3
+ "version": "0.10.4-dev.202607012234.a9be056",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import {
4
+ fetchLatestVersion,
4
5
  fetchReleases,
5
6
  resolveImageRefsDetailed,
6
7
  } from "../lib/platform-releases.js";
@@ -114,4 +115,45 @@ describe("resolveImageRefsDetailed", () => {
114
115
  const result = await resolveImageRefsDetailed("0.7.0");
115
116
  expect(result.status).toBe("dockerhub-fallback");
116
117
  });
118
+
119
+ test("requests the preview channel when channel=preview", async () => {
120
+ const fetchMock = mockFetchJson([RELEASE]);
121
+ await resolveImageRefsDetailed("v0.7.0", undefined, "preview");
122
+ const url = String(fetchMock.mock.calls[0][0]);
123
+ expect(url).toContain("channel=preview");
124
+ expect(url).not.toContain("stable=true");
125
+ });
126
+
127
+ test("keeps the stable filter when channel=stable (default)", async () => {
128
+ const fetchMock = mockFetchJson([RELEASE]);
129
+ await resolveImageRefsDetailed("v0.7.0", undefined, "stable");
130
+ const url = String(fetchMock.mock.calls[0][0]);
131
+ expect(url).toContain("stable=true");
132
+ expect(url).not.toContain("channel=");
133
+ });
134
+ });
135
+
136
+ describe("fetchLatestVersion", () => {
137
+ test("defaults to the stable channel", async () => {
138
+ const fetchMock = mockFetchJson([RELEASE]);
139
+ const version = await fetchLatestVersion();
140
+ expect(version).toBe("0.7.0");
141
+ const url = String(fetchMock.mock.calls[0][0]);
142
+ expect(url).toContain("stable=true");
143
+ expect(url).not.toContain("channel=");
144
+ });
145
+
146
+ test("requests the preview channel when asked", async () => {
147
+ const preview = { ...RELEASE, version: "0.8.0-preview.3" };
148
+ const fetchMock = mockFetchJson([preview]);
149
+ const version = await fetchLatestVersion("preview");
150
+ expect(version).toBe("0.8.0-preview.3");
151
+ const url = String(fetchMock.mock.calls[0][0]);
152
+ expect(url).toContain("channel=preview");
153
+ });
154
+
155
+ test("returns null when the channel has no releases", async () => {
156
+ mockFetchJson([]);
157
+ expect(await fetchLatestVersion("preview")).toBeNull();
158
+ });
117
159
  });
@@ -178,6 +178,7 @@ interface HatchArgs {
178
178
  remote: RemoteHost;
179
179
  watch: boolean;
180
180
  sourcePath: string | null;
181
+ preview: boolean;
181
182
  configValues: Record<string, string>;
182
183
  flagEnvVars: Record<string, string>;
183
184
  analyze: boolean;
@@ -203,6 +204,7 @@ function parseArgs(): HatchArgs {
203
204
  let remote: RemoteHost = DEFAULT_REMOTE;
204
205
  let watch = false;
205
206
  let sourcePath: string | null = null;
207
+ let preview = false;
206
208
  const configValues: Record<string, string> = {};
207
209
  let analyze = false;
208
210
  let netnsContainer: string | null = null;
@@ -231,6 +233,7 @@ function parseArgs(): HatchArgs {
231
233
  );
232
234
  console.log(
233
235
  " --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
236
+ " --preview When pulling published images (no local source), resolve from the preview channel (latest preview release) instead of latest-stable. Also settable via VELLUM_HATCH_CHANNEL=preview.",
234
237
  );
235
238
  console.log(
236
239
  " --keep-alive Stay alive after hatch, exit when gateway stops",
@@ -263,6 +266,8 @@ function parseArgs(): HatchArgs {
263
266
  watch = true;
264
267
  } else if (arg === "--analyze") {
265
268
  analyze = true;
269
+ } else if (arg === "--preview") {
270
+ preview = true;
266
271
  } else if (arg === "--source") {
267
272
  const next = args[i + 1];
268
273
  if (!next || next.startsWith("-")) {
@@ -349,7 +354,7 @@ function parseArgs(): HatchArgs {
349
354
  species = arg as Species;
350
355
  } else {
351
356
  console.error(
352
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
357
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --preview, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
353
358
  );
354
359
  process.exit(1);
355
360
  }
@@ -363,6 +368,7 @@ function parseArgs(): HatchArgs {
363
368
  remote,
364
369
  watch,
365
370
  sourcePath,
371
+ preview,
366
372
  configValues,
367
373
  flagEnvVars,
368
374
  analyze,
@@ -602,6 +608,7 @@ export async function hatch(): Promise<void> {
602
608
  remote,
603
609
  watch,
604
610
  sourcePath,
611
+ preview,
605
612
  configValues,
606
613
  flagEnvVars,
607
614
  analyze,
@@ -681,6 +688,7 @@ export async function hatch(): Promise<void> {
681
688
  flagEnvVars,
682
689
  sourcePath,
683
690
  analyze,
691
+ channel: preview ? "preview" : undefined,
684
692
  netnsContainer: netnsContainer ?? undefined,
685
693
  gatewayPort: gatewayPort ?? undefined,
686
694
  assistantCaCertPath: assistantCaCert ?? undefined,
package/src/lib/docker.ts CHANGED
@@ -35,8 +35,9 @@ import {
35
35
  loadImageViaHost,
36
36
  } from "./host-image-loader.js";
37
37
  import {
38
- fetchLatestStableVersion,
38
+ fetchLatestVersion,
39
39
  resolveImageRefs,
40
+ type ReleaseChannel,
40
41
  } from "./platform-releases.js";
41
42
  import {
42
43
  configureHatchProviderApiKey,
@@ -321,6 +322,15 @@ export interface HatchDockerParams {
321
322
  * connection.
322
323
  */
323
324
  assistantCaCertPath?: string;
325
+ /**
326
+ * Release channel to resolve published images from when hatching without a
327
+ * local source tree (the image-pull fallback). `stable` (default) keeps the
328
+ * latest-stable behavior; `preview` pulls the latest preview release. Only
329
+ * affects the pull path — a local source build ignores it. Falls back to
330
+ * the `VELLUM_HATCH_CHANNEL` env var when unset, so callers that hatch via
331
+ * env (e.g. evals) can opt in without changing the invocation.
332
+ */
333
+ channel?: ReleaseChannel;
324
334
  }
325
335
 
326
336
  export type DockerProviderCredentialSetupAction =
@@ -1093,6 +1103,14 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1093
1103
  flagEnvVars = {},
1094
1104
  } = params;
1095
1105
  let watch = params.watch ?? false;
1106
+ // Resolve the release channel for the image-pull fallback: explicit param
1107
+ // wins, then the VELLUM_HATCH_CHANNEL env var, else stable. Any value other
1108
+ // than "preview" (case-insensitive) is treated as stable.
1109
+ const channel: ReleaseChannel =
1110
+ params.channel ??
1111
+ (process.env.VELLUM_HATCH_CHANNEL?.trim().toLowerCase() === "preview"
1112
+ ? "preview"
1113
+ : "stable");
1096
1114
 
1097
1115
  resetLogFile("hatch.log");
1098
1116
  const provider =
@@ -1250,8 +1268,8 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1250
1268
  // Resolve image refs from a remote source that may have dev/local
1251
1269
  // builds. If resolution is unavailable, fall back to the CLI's own
1252
1270
  // version so a default tag can still be resolved.
1253
- log("🔍 Fetching latest stable release...");
1254
- const latestVersion = await fetchLatestStableVersion();
1271
+ log(`🔍 Fetching latest ${channel} release...`);
1272
+ const latestVersion = await fetchLatestVersion(channel);
1255
1273
  let versionTag: string;
1256
1274
  if (latestVersion) {
1257
1275
  versionTag = latestVersion.startsWith("v")
@@ -1265,7 +1283,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1265
1283
  );
1266
1284
  }
1267
1285
  log("🔍 Resolving image references...");
1268
- const resolved = await resolveImageRefs(versionTag, log);
1286
+ const resolved = await resolveImageRefs(versionTag, log, channel);
1269
1287
  imageTags.assistant = resolved.imageTags.assistant;
1270
1288
  imageTags.gateway = resolved.imageTags.gateway;
1271
1289
  imageTags["credential-executor"] =
@@ -9,6 +9,25 @@ export interface ResolvedImageRefs {
9
9
  source: "platform" | "dockerhub";
10
10
  }
11
11
 
12
+ /**
13
+ * Release channel to resolve images from. `stable` is the default and maps
14
+ * to the server's `stable=true` filter (unchanged behavior). `preview` opts
15
+ * into the platform's preview channel — the newest release regardless of
16
+ * stability — used by callers (e.g. evals) that want to always hatch the
17
+ * latest preview image instead of latest-stable.
18
+ */
19
+ export type ReleaseChannel = "stable" | "preview";
20
+
21
+ /**
22
+ * Map a channel to `fetchReleases` options. `stable` passes no `channel`
23
+ * param so the server keeps its `stable=true` default (we don't rely on an
24
+ * unverified `channel=stable` filter); `preview` requests the preview
25
+ * channel explicitly.
26
+ */
27
+ function releasesOptsFor(channel: ReleaseChannel): { channel?: "preview" } {
28
+ return channel === "preview" ? { channel: "preview" } : {};
29
+ }
30
+
12
31
  export interface ReleaseListItem {
13
32
  version: string;
14
33
  is_stable?: boolean;
@@ -51,15 +70,26 @@ export async function fetchReleases(opts?: {
51
70
  }
52
71
 
53
72
  /**
54
- * Fetch the latest stable release version from the platform API.
73
+ * Fetch the latest release version for a channel from the platform API.
55
74
  * Returns the version string (e.g. "0.7.0") or null if unavailable.
56
- * The releases endpoint returns entries ordered newest-first.
75
+ * The releases endpoint returns entries ordered newest-first, so the first
76
+ * entry is the latest for the requested channel.
57
77
  */
58
- export async function fetchLatestStableVersion(): Promise<string | null> {
59
- const releases = await fetchReleases();
78
+ export async function fetchLatestVersion(
79
+ channel: ReleaseChannel = "stable",
80
+ ): Promise<string | null> {
81
+ const releases = await fetchReleases(releasesOptsFor(channel));
60
82
  return releases?.[0]?.version ?? null;
61
83
  }
62
84
 
85
+ /**
86
+ * Fetch the latest stable release version. Thin back-compat wrapper around
87
+ * {@link fetchLatestVersion} for existing callers.
88
+ */
89
+ export async function fetchLatestStableVersion(): Promise<string | null> {
90
+ return fetchLatestVersion("stable");
91
+ }
92
+
63
93
  export type ImageRefResolution =
64
94
  | { status: "platform"; imageTags: Record<ServiceName, string> }
65
95
  | { status: "dockerhub-fallback"; imageTags: Record<ServiceName, string> }
@@ -87,10 +117,11 @@ function dockerhubImageTags(version: string): Record<ServiceName, string> {
87
117
  export async function resolveImageRefsDetailed(
88
118
  version: string,
89
119
  log?: (msg: string) => void,
120
+ channel: ReleaseChannel = "stable",
90
121
  ): Promise<ImageRefResolution> {
91
122
  log?.("Resolving image references...");
92
123
 
93
- const releases = await fetchReleases();
124
+ const releases = await fetchReleases(releasesOptsFor(channel));
94
125
  if (releases === null) {
95
126
  log?.("Platform unreachable — falling back to DockerHub tags");
96
127
  return {
@@ -151,8 +182,9 @@ export async function resolveImageRefsDetailed(
151
182
  export async function resolveImageRefs(
152
183
  version: string,
153
184
  log?: (msg: string) => void,
185
+ channel: ReleaseChannel = "stable",
154
186
  ): Promise<ResolvedImageRefs> {
155
- const resolution = await resolveImageRefsDetailed(version, log);
187
+ const resolution = await resolveImageRefsDetailed(version, log, channel);
156
188
  if (resolution.status === "platform") {
157
189
  log?.("Resolved image refs from platform API");
158
190
  return { imageTags: resolution.imageTags, source: "platform" };