@vellumai/cli 0.10.4-dev.202607022227.14c7770 → 0.10.4-staging.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.
@@ -76,6 +76,13 @@ describe("buildAuthorizeUrl", () => {
76
76
  expect(url.searchParams.has("prompt")).toBe(false);
77
77
  });
78
78
 
79
+ test("provider hint replaces authkit", () => {
80
+ const url = new URL(
81
+ buildAuthorizeUrl({ ...base, providerHint: "GoogleOAuth" }),
82
+ );
83
+ expect(url.searchParams.get("provider")).toBe("GoogleOAuth");
84
+ });
85
+
79
86
  test("login hint is forwarded", () => {
80
87
  const url = new URL(
81
88
  buildAuthorizeUrl({ ...base, loginHint: "user@example.com" }),
@@ -178,7 +178,6 @@ interface HatchArgs {
178
178
  remote: RemoteHost;
179
179
  watch: boolean;
180
180
  sourcePath: string | null;
181
- preview: boolean;
182
181
  configValues: Record<string, string>;
183
182
  flagEnvVars: Record<string, string>;
184
183
  analyze: boolean;
@@ -204,7 +203,6 @@ function parseArgs(): HatchArgs {
204
203
  let remote: RemoteHost = DEFAULT_REMOTE;
205
204
  let watch = false;
206
205
  let sourcePath: string | null = null;
207
- let preview = false;
208
206
  const configValues: Record<string, string> = {};
209
207
  let analyze = false;
210
208
  let netnsContainer: string | null = null;
@@ -233,7 +231,6 @@ function parseArgs(): HatchArgs {
233
231
  );
234
232
  console.log(
235
233
  " --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.",
237
234
  );
238
235
  console.log(
239
236
  " --keep-alive Stay alive after hatch, exit when gateway stops",
@@ -266,8 +263,6 @@ function parseArgs(): HatchArgs {
266
263
  watch = true;
267
264
  } else if (arg === "--analyze") {
268
265
  analyze = true;
269
- } else if (arg === "--preview") {
270
- preview = true;
271
266
  } else if (arg === "--source") {
272
267
  const next = args[i + 1];
273
268
  if (!next || next.startsWith("-")) {
@@ -354,7 +349,7 @@ function parseArgs(): HatchArgs {
354
349
  species = arg as Species;
355
350
  } else {
356
351
  console.error(
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>`,
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>`,
358
353
  );
359
354
  process.exit(1);
360
355
  }
@@ -368,7 +363,6 @@ function parseArgs(): HatchArgs {
368
363
  remote,
369
364
  watch,
370
365
  sourcePath,
371
- preview,
372
366
  configValues,
373
367
  flagEnvVars,
374
368
  analyze,
@@ -608,7 +602,6 @@ export async function hatch(): Promise<void> {
608
602
  remote,
609
603
  watch,
610
604
  sourcePath,
611
- preview,
612
605
  configValues,
613
606
  flagEnvVars,
614
607
  analyze,
@@ -688,7 +681,6 @@ export async function hatch(): Promise<void> {
688
681
  flagEnvVars,
689
682
  sourcePath,
690
683
  analyze,
691
- channel: preview ? "preview" : undefined,
692
684
  netnsContainer: netnsContainer ?? undefined,
693
685
  gatewayPort: gatewayPort ?? undefined,
694
686
  assistantCaCertPath: assistantCaCert ?? undefined,
@@ -7,7 +7,6 @@ import {
7
7
  loadAllAssistants,
8
8
  removeAssistantEntry,
9
9
  resolveAssistant,
10
- saveAssistantEntry,
11
10
  setActiveAssistant,
12
11
  } from "../lib/assistant-config";
13
12
  import { computeDeviceId } from "../lib/guardian-token";
@@ -384,7 +383,6 @@ export async function login(): Promise<void> {
384
383
  fetchCurrentVersion(entry.runtimeUrl),
385
384
  fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
386
385
  ]);
387
- const platformUrl = getPlatformUrl();
388
386
  const registration = await ensureSelfHostedLocalRegistration(
389
387
  token,
390
388
  orgId,
@@ -392,15 +390,9 @@ export async function login(): Promise<void> {
392
390
  entry.assistantId,
393
391
  "cli",
394
392
  assistantVersion,
395
- platformUrl,
393
+ getPlatformUrl(),
396
394
  ingressUrl,
397
395
  );
398
- saveAssistantEntry({
399
- ...entry,
400
- platformAssistantId: registration.assistant.id,
401
- platformBaseUrl: platformUrl,
402
- platformOrganizationId: orgId,
403
- });
404
396
  console.log(
405
397
  `Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
406
398
  );
@@ -441,7 +433,7 @@ export async function login(): Promise<void> {
441
433
  bearerToken: entry.bearerToken,
442
434
  assistantApiKey,
443
435
  platformAssistantId: registration.assistant.id,
444
- platformBaseUrl: platformUrl,
436
+ platformBaseUrl: getPlatformUrl(),
445
437
  organizationId: orgId,
446
438
  userId: user.id,
447
439
  webhookSecret: registration.webhook_secret,
@@ -20,9 +20,7 @@ import { getConfigDir } from "../lib/environments/paths.js";
20
20
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
21
21
  import {
22
22
  authHeaders,
23
- authHeadersForKnownOrganization,
24
23
  getPlatformUrl,
25
- readGatewayCredential,
26
24
  readPlatformToken,
27
25
  } from "../lib/platform-client.js";
28
26
  import { retireInstance as retireAwsInstance } from "../lib/aws.js";
@@ -47,180 +45,6 @@ interface RetireArgs {
47
45
  yes: boolean;
48
46
  }
49
47
 
50
- interface SelfHostedPlatformRegistration {
51
- platformAssistantId: string;
52
- platformBaseUrl: string | null;
53
- organizationId: string | null;
54
- }
55
-
56
- const PLATFORM_TOKEN_ENV = "VELLUM_PLATFORM_TOKEN";
57
-
58
- function readPlatformRetireToken(): string | null {
59
- const envToken = process.env[PLATFORM_TOKEN_ENV]?.trim();
60
- if (envToken) return envToken;
61
- return readPlatformToken();
62
- }
63
-
64
- function platformOrigin(platformUrl: string): string | null {
65
- try {
66
- return new URL(platformUrl).origin;
67
- } catch {
68
- return null;
69
- }
70
- }
71
-
72
- function resolveTrustedSelfHostedPlatformUrl(
73
- storedPlatformUrl: string | null,
74
- ): string | null {
75
- const configuredPlatformUrl = getPlatformUrl();
76
- if (!storedPlatformUrl) return configuredPlatformUrl;
77
-
78
- const storedOrigin = platformOrigin(storedPlatformUrl);
79
- const configuredOrigin = platformOrigin(configuredPlatformUrl);
80
- if (storedOrigin && configuredOrigin && storedOrigin === configuredOrigin) {
81
- return configuredPlatformUrl;
82
- }
83
-
84
- console.warn(
85
- "⚠️ Stored platform URL does not match the configured platform URL; skipping platform unregister.",
86
- );
87
- return null;
88
- }
89
-
90
- function nonEmptyString(value: string | null | undefined): string | null {
91
- const trimmed = value?.trim();
92
- return trimmed ? trimmed : null;
93
- }
94
-
95
- function lockfileSelfHostedPlatformRegistration(
96
- entry: AssistantEntry,
97
- ): SelfHostedPlatformRegistration | null {
98
- const platformAssistantId = nonEmptyString(entry.platformAssistantId);
99
- if (!platformAssistantId) return null;
100
-
101
- return {
102
- platformAssistantId,
103
- platformBaseUrl: nonEmptyString(entry.platformBaseUrl),
104
- organizationId: nonEmptyString(entry.platformOrganizationId),
105
- };
106
- }
107
-
108
- async function deletePlatformAssistant(
109
- assistantId: string,
110
- options: {
111
- token: string;
112
- platformUrl?: string;
113
- organizationId?: string;
114
- label: string;
115
- successMessage: string;
116
- alreadyGoneMessage: string;
117
- },
118
- ): Promise<void> {
119
- const platformUrl = options.platformUrl || getPlatformUrl();
120
- const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
121
- const headers = options.organizationId
122
- ? authHeadersForKnownOrganization(options.token, options.organizationId)
123
- : await authHeaders(options.token, platformUrl);
124
-
125
- const response = await loopbackSafeFetch(url, {
126
- method: "DELETE",
127
- headers,
128
- });
129
-
130
- if (!response.ok && response.status !== 404) {
131
- const body = await response.text();
132
- throw new Error(`${options.label} failed (${response.status}): ${body}`);
133
- }
134
-
135
- if (response.status === 404) {
136
- console.log(options.alreadyGoneMessage);
137
- } else {
138
- console.log(options.successMessage);
139
- }
140
- }
141
-
142
- async function readSelfHostedPlatformRegistration(
143
- entry: AssistantEntry,
144
- ): Promise<SelfHostedPlatformRegistration | null> {
145
- const fallback = lockfileSelfHostedPlatformRegistration(entry);
146
- const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
147
- const platformAssistant = await readGatewayCredential(
148
- gatewayUrl,
149
- "vellum:platform_assistant_id",
150
- entry.bearerToken,
151
- );
152
- if (platformAssistant.unreachable) {
153
- if (fallback) {
154
- console.warn(
155
- "\u26a0\ufe0f Could not read platform registration from the local assistant; using saved platform registration.",
156
- );
157
- return fallback;
158
- }
159
- console.warn(
160
- "\u26a0\ufe0f Could not read platform registration from the local assistant and no saved platform registration is available; skipping platform unregister.",
161
- );
162
- return null;
163
- }
164
-
165
- const platformAssistantId =
166
- nonEmptyString(platformAssistant.value) ?? fallback?.platformAssistantId;
167
- if (!platformAssistantId) return null;
168
-
169
- const [platformBaseUrl, organizationId] = await Promise.all([
170
- readGatewayCredential(
171
- gatewayUrl,
172
- "vellum:platform_base_url",
173
- entry.bearerToken,
174
- ),
175
- readGatewayCredential(
176
- gatewayUrl,
177
- "vellum:platform_organization_id",
178
- entry.bearerToken,
179
- ),
180
- ]);
181
-
182
- return {
183
- platformAssistantId,
184
- platformBaseUrl:
185
- nonEmptyString(platformBaseUrl.value) ??
186
- fallback?.platformBaseUrl ??
187
- null,
188
- organizationId:
189
- nonEmptyString(organizationId.value) ?? fallback?.organizationId ?? null,
190
- };
191
- }
192
-
193
- async function unregisterSelfHostedLocalPlatformRecord(
194
- entry: AssistantEntry,
195
- ): Promise<void> {
196
- const token = readPlatformRetireToken();
197
- if (!token) return;
198
-
199
- const registration = await readSelfHostedPlatformRegistration(entry);
200
- if (!registration) return;
201
-
202
- const platformUrl = resolveTrustedSelfHostedPlatformUrl(
203
- registration.platformBaseUrl,
204
- );
205
- if (!platformUrl) return;
206
- if (!registration.organizationId) {
207
- console.warn(
208
- "⚠️ Could not read platform organization ID from the local assistant; skipping platform unregister.",
209
- );
210
- return;
211
- }
212
-
213
- await deletePlatformAssistant(registration.platformAssistantId, {
214
- token,
215
- platformUrl,
216
- organizationId: registration.organizationId,
217
- label: "Platform self-hosted unregister",
218
- successMessage: "\u2705 Platform self-hosted registration unregistered.",
219
- alreadyGoneMessage:
220
- "\u2705 Platform self-hosted registration already unregistered (404).",
221
- });
222
- }
223
-
224
48
  async function retireCustom(entry: AssistantEntry): Promise<void> {
225
49
  const host = extractHostFromUrl(entry.runtimeUrl);
226
50
  const sshUser = entry.sshUser ?? "root";
@@ -262,7 +86,7 @@ async function retireVellum(
262
86
  ): Promise<void> {
263
87
  console.log("\u{1F5D1}\ufe0f Retiring platform-hosted instance...\n");
264
88
 
265
- const token = readPlatformRetireToken();
89
+ const token = readPlatformToken();
266
90
  if (!token) {
267
91
  console.error(
268
92
  "Error: Not logged in. Run `vellum login --token <token>` first.",
@@ -270,21 +94,33 @@ async function retireVellum(
270
94
  process.exit(1);
271
95
  }
272
96
 
273
- try {
274
- await deletePlatformAssistant(assistantId, {
275
- token,
276
- platformUrl: runtimeUrl,
277
- label: "Platform retire",
278
- successMessage: "\u2705 Platform-hosted instance retired.",
279
- alreadyGoneMessage:
280
- "\u2705 Platform-hosted instance already retired (404) \u2014 cleaning up local state.",
281
- });
282
- } catch (error) {
97
+ const platformUrl = runtimeUrl || getPlatformUrl();
98
+ const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
99
+ const response = await loopbackSafeFetch(url, {
100
+ method: "DELETE",
101
+ headers: await authHeaders(token, runtimeUrl),
102
+ });
103
+
104
+ // Treat 404 as success: the assistant is already gone from the platform
105
+ // (previously retired, deleted from the web UI, or retired from another
106
+ // device) so the caller's job is done. Falling through to the lockfile
107
+ // cleanup avoids leaving a stale entry that would otherwise wedge the
108
+ // macOS app in a permanent health-check loop.
109
+ if (!response.ok && response.status !== 404) {
110
+ const body = await response.text();
283
111
  console.error(
284
- `Error: ${error instanceof Error ? error.message : String(error)}`,
112
+ `Error: Platform retire failed (${response.status}): ${body}`,
285
113
  );
286
114
  process.exit(1);
287
115
  }
116
+
117
+ if (response.status === 404) {
118
+ console.log(
119
+ "\u2705 Platform-hosted instance already retired (404) — cleaning up local state.",
120
+ );
121
+ } else {
122
+ console.log("\u2705 Platform-hosted instance retired.");
123
+ }
288
124
  }
289
125
 
290
126
  function parseRetireArgs(args: string[]): RetireArgs {
@@ -467,15 +303,6 @@ async function retireInner(): Promise<void> {
467
303
  } else if (cloud === "docker") {
468
304
  await retireDocker(assistantId);
469
305
  } else if (cloud === "local") {
470
- try {
471
- await unregisterSelfHostedLocalPlatformRecord(entry);
472
- } catch (error) {
473
- console.warn(
474
- `⚠️ Platform self-hosted unregister failed; continuing local retire: ${
475
- error instanceof Error ? error.message : String(error)
476
- }`,
477
- );
478
- }
479
306
  await retireLocal(assistantId, entry);
480
307
  } else if (cloud === "custom") {
481
308
  await retireCustom(entry);
@@ -1123,7 +1123,6 @@ async function tryInjectPlatformCredentials(
1123
1123
  fetchCurrentVersion(entry.runtimeUrl),
1124
1124
  fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
1125
1125
  ]);
1126
- const platformUrl = getPlatformUrl();
1127
1126
  const registration = await ensureSelfHostedLocalRegistration(
1128
1127
  token,
1129
1128
  orgId,
@@ -1131,15 +1130,9 @@ async function tryInjectPlatformCredentials(
1131
1130
  entry.assistantId,
1132
1131
  "cli",
1133
1132
  assistantVersion,
1134
- platformUrl,
1133
+ getPlatformUrl(),
1135
1134
  ingressUrl,
1136
1135
  );
1137
- saveAssistantEntry({
1138
- ...entry,
1139
- platformAssistantId: registration.assistant.id,
1140
- platformBaseUrl: platformUrl,
1141
- platformOrganizationId: orgId,
1142
- });
1143
1136
 
1144
1137
  // Resolve the API key: 1) fresh from registration, 2) existing from
1145
1138
  // daemon credential store, 3) reprovision as last resort (revokes old key).
@@ -1171,7 +1164,7 @@ async function tryInjectPlatformCredentials(
1171
1164
  bearerToken: entry.bearerToken,
1172
1165
  assistantApiKey,
1173
1166
  platformAssistantId: registration.assistant.id,
1174
- platformBaseUrl: platformUrl,
1167
+ platformBaseUrl: getPlatformUrl(),
1175
1168
  organizationId: orgId,
1176
1169
  userId: user.id,
1177
1170
  webhookSecret: registration.webhook_secret,
package/src/lib/docker.ts CHANGED
@@ -35,9 +35,8 @@ import {
35
35
  loadImageViaHost,
36
36
  } from "./host-image-loader.js";
37
37
  import {
38
- fetchLatestVersion,
38
+ fetchLatestStableVersion,
39
39
  resolveImageRefs,
40
- type ReleaseChannel,
41
40
  } from "./platform-releases.js";
42
41
  import {
43
42
  configureHatchProviderApiKey,
@@ -322,15 +321,6 @@ export interface HatchDockerParams {
322
321
  * connection.
323
322
  */
324
323
  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;
334
324
  }
335
325
 
336
326
  export type DockerProviderCredentialSetupAction =
@@ -1103,14 +1093,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1103
1093
  flagEnvVars = {},
1104
1094
  } = params;
1105
1095
  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");
1114
1096
 
1115
1097
  resetLogFile("hatch.log");
1116
1098
  const provider =
@@ -1268,8 +1250,8 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1268
1250
  // Resolve image refs from a remote source that may have dev/local
1269
1251
  // builds. If resolution is unavailable, fall back to the CLI's own
1270
1252
  // version so a default tag can still be resolved.
1271
- log(`🔍 Fetching latest ${channel} release...`);
1272
- const latestVersion = await fetchLatestVersion(channel);
1253
+ log("🔍 Fetching latest stable release...");
1254
+ const latestVersion = await fetchLatestStableVersion();
1273
1255
  let versionTag: string;
1274
1256
  if (latestVersion) {
1275
1257
  versionTag = latestVersion.startsWith("v")
@@ -1283,7 +1265,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1283
1265
  );
1284
1266
  }
1285
1267
  log("🔍 Resolving image references...");
1286
- const resolved = await resolveImageRefs(versionTag, log, channel);
1268
+ const resolved = await resolveImageRefs(versionTag, log);
1287
1269
  imageTags.assistant = resolved.imageTags.assistant;
1288
1270
  imageTags.gateway = resolved.imageTags.gateway;
1289
1271
  imageTags["credential-executor"] =
@@ -96,20 +96,6 @@ function tokenAuthHeader(token: string): Record<string, string> {
96
96
  return { "X-Session-Token": token };
97
97
  }
98
98
 
99
- export function authHeadersForKnownOrganization(
100
- token: string,
101
- organizationId: string,
102
- ): Record<string, string> {
103
- const headers: Record<string, string> = {
104
- "Content-Type": "application/json",
105
- ...tokenAuthHeader(token),
106
- };
107
- if (!token.startsWith(VAK_PREFIX)) {
108
- headers["Vellum-Organization-Id"] = organizationId;
109
- }
110
- return headers;
111
- }
112
-
113
99
  /** Module-level cache for org IDs to avoid redundant fetches in polling loops. */
114
100
  const orgIdCache = new Map<string, { orgId: string; expiresAt: number }>();
115
101
  const ORG_ID_CACHE_TTL_MS = 60_000; // 60 seconds
@@ -9,25 +9,6 @@ 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
-
31
12
  export interface ReleaseListItem {
32
13
  version: string;
33
14
  is_stable?: boolean;
@@ -70,24 +51,13 @@ export async function fetchReleases(opts?: {
70
51
  }
71
52
 
72
53
  /**
73
- * Fetch the latest release version for a channel from the platform API.
54
+ * Fetch the latest stable release version from the platform API.
74
55
  * Returns the version string (e.g. "0.7.0") or null if unavailable.
75
- * The releases endpoint returns entries ordered newest-first, so the first
76
- * entry is the latest for the requested channel.
77
- */
78
- export async function fetchLatestVersion(
79
- channel: ReleaseChannel = "stable",
80
- ): Promise<string | null> {
81
- const releases = await fetchReleases(releasesOptsFor(channel));
82
- return releases?.[0]?.version ?? null;
83
- }
84
-
85
- /**
86
- * Fetch the latest stable release version. Thin back-compat wrapper around
87
- * {@link fetchLatestVersion} for existing callers.
56
+ * The releases endpoint returns entries ordered newest-first.
88
57
  */
89
58
  export async function fetchLatestStableVersion(): Promise<string | null> {
90
- return fetchLatestVersion("stable");
59
+ const releases = await fetchReleases();
60
+ return releases?.[0]?.version ?? null;
91
61
  }
92
62
 
93
63
  export type ImageRefResolution =
@@ -117,11 +87,10 @@ function dockerhubImageTags(version: string): Record<ServiceName, string> {
117
87
  export async function resolveImageRefsDetailed(
118
88
  version: string,
119
89
  log?: (msg: string) => void,
120
- channel: ReleaseChannel = "stable",
121
90
  ): Promise<ImageRefResolution> {
122
91
  log?.("Resolving image references...");
123
92
 
124
- const releases = await fetchReleases(releasesOptsFor(channel));
93
+ const releases = await fetchReleases();
125
94
  if (releases === null) {
126
95
  log?.("Platform unreachable — falling back to DockerHub tags");
127
96
  return {
@@ -182,9 +151,8 @@ export async function resolveImageRefsDetailed(
182
151
  export async function resolveImageRefs(
183
152
  version: string,
184
153
  log?: (msg: string) => void,
185
- channel: ReleaseChannel = "stable",
186
154
  ): Promise<ResolvedImageRefs> {
187
- const resolution = await resolveImageRefsDetailed(version, log, channel);
155
+ const resolution = await resolveImageRefsDetailed(version, log);
188
156
  if (resolution.status === "platform") {
189
157
  log?.("Resolved image refs from platform API");
190
158
  return { imageTags: resolution.imageTags, source: "platform" };
@@ -33,6 +33,7 @@ export interface AuthorizeUrlOptions {
33
33
  challenge: string;
34
34
  state: string;
35
35
  loginHint?: string;
36
+ providerHint?: string;
36
37
  }
37
38
 
38
39
  export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
@@ -45,7 +46,7 @@ export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
45
46
  url.searchParams.set("code_challenge_method", "S256");
46
47
  url.searchParams.set("state", options.state);
47
48
  // No `prompt`: lets the browser's existing IdP session be reused.
48
- url.searchParams.set("provider", "authkit");
49
+ url.searchParams.set("provider", options.providerHint || "authkit");
49
50
  if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
50
51
  return url.toString();
51
52
  }
@@ -1,79 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, mock, test } from "bun:test";
2
- import { EventEmitter } from "node:events";
3
-
4
- import type { CliInvocation } from "../util";
5
-
6
- class FakeChild extends EventEmitter {
7
- stdout = new EventEmitter();
8
- stderr = new EventEmitter();
9
- kill = mock(() => true);
10
- }
11
-
12
- let lastChild: FakeChild;
13
- const spawnArgs: Array<
14
- [string, string[], { env?: NodeJS.ProcessEnv; stdio?: unknown }]
15
- > = [];
16
- const spawnMock = mock(
17
- (
18
- command: string,
19
- args: string[],
20
- options: { env?: NodeJS.ProcessEnv; stdio?: unknown },
21
- ) => {
22
- spawnArgs.push([command, args, options]);
23
- lastChild = new FakeChild();
24
- return lastChild;
25
- },
26
- );
27
-
28
- mock.module("node:child_process", () => ({ spawn: spawnMock }));
29
-
30
- let runRetire: typeof import("../retire").runRetire;
31
-
32
- beforeAll(async () => {
33
- ({ runRetire } = await import("../retire"));
34
- });
35
-
36
- afterEach(() => {
37
- spawnArgs.length = 0;
38
- spawnMock.mockClear();
39
- delete process.env.VELLUM_PLATFORM_TOKEN;
40
- });
41
-
42
- const invocation: CliInvocation = { command: "bun", baseArgs: ["run", "cli"] };
43
-
44
- describe("runRetire", () => {
45
- test("spawns the CLI retire command", async () => {
46
- const pending = runRetire(invocation, "asst-42");
47
- lastChild.emit("close", 0);
48
-
49
- expect(await pending).toEqual({ ok: true });
50
- expect(spawnArgs[0]).toEqual([
51
- "bun",
52
- ["run", "cli", "retire", "asst-42", "--yes"],
53
- { stdio: ["ignore", "pipe", "pipe"] },
54
- ]);
55
- });
56
-
57
- test("passes a host platform token to the CLI subprocess", async () => {
58
- const pending = runRetire(invocation, "asst-42", {
59
- platformToken: "session-token",
60
- });
61
- lastChild.emit("close", 0);
62
-
63
- expect(await pending).toEqual({ ok: true });
64
- expect(spawnArgs[0]?.[2].env?.VELLUM_PLATFORM_TOKEN).toBe("session-token");
65
- expect(process.env.VELLUM_PLATFORM_TOKEN).toBeUndefined();
66
- });
67
-
68
- test("a non-zero exit resolves to a failure carrying the CLI output", async () => {
69
- const pending = runRetire(invocation, "asst-42");
70
- lastChild.stderr.emit("data", Buffer.from("retire failed"));
71
- lastChild.emit("close", 1);
72
-
73
- expect(await pending).toEqual({
74
- ok: false,
75
- status: 500,
76
- error: "retire failed",
77
- });
78
- });
79
- });