@vellumai/cli 0.10.4-staging.1 → 0.10.4
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
|
@@ -83,10 +83,10 @@ describe("vellum flags --assistant routing", () => {
|
|
|
83
83
|
fetchCalls.push({ url, method });
|
|
84
84
|
if (method === "PATCH") {
|
|
85
85
|
return jsonResponse({
|
|
86
|
-
key: "
|
|
86
|
+
key: "voice-mode",
|
|
87
87
|
enabled: true,
|
|
88
88
|
defaultEnabled: false,
|
|
89
|
-
label: "
|
|
89
|
+
label: "Voice Mode",
|
|
90
90
|
description: "test",
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -133,7 +133,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
133
133
|
"vellum",
|
|
134
134
|
"flags",
|
|
135
135
|
"set",
|
|
136
|
-
"
|
|
136
|
+
"voice-mode",
|
|
137
137
|
"true",
|
|
138
138
|
"--assistant",
|
|
139
139
|
"Bob",
|
|
@@ -146,7 +146,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
146
146
|
// bob-2 has assistantId.length === 5, so port = 7800 + 5 = 7805.
|
|
147
147
|
expect(fetchCalls[0].url).toContain("http://127.0.0.1:7805");
|
|
148
148
|
expect(fetchCalls[0].url).toContain(
|
|
149
|
-
"/v1/assistants/bob-2/feature-flags/
|
|
149
|
+
"/v1/assistants/bob-2/feature-flags/voice-mode",
|
|
150
150
|
);
|
|
151
151
|
});
|
|
152
152
|
|
|
@@ -166,7 +166,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
166
166
|
"--assistant",
|
|
167
167
|
"Bob",
|
|
168
168
|
"set",
|
|
169
|
-
"
|
|
169
|
+
"voice-mode",
|
|
170
170
|
"true",
|
|
171
171
|
];
|
|
172
172
|
|
|
@@ -174,7 +174,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
174
174
|
|
|
175
175
|
expect(fetchCalls.length).toBe(1);
|
|
176
176
|
expect(fetchCalls[0].url).toContain(
|
|
177
|
-
"/v1/assistants/bob-2/feature-flags/
|
|
177
|
+
"/v1/assistants/bob-2/feature-flags/voice-mode",
|
|
178
178
|
);
|
|
179
179
|
});
|
|
180
180
|
|
|
@@ -193,7 +193,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
193
193
|
"vellum",
|
|
194
194
|
"flags",
|
|
195
195
|
"set",
|
|
196
|
-
"
|
|
196
|
+
"voice-mode",
|
|
197
197
|
"true",
|
|
198
198
|
];
|
|
199
199
|
|
|
@@ -203,7 +203,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
203
203
|
// alice-1 has assistantId.length === 7, so port = 7800 + 7 = 7807.
|
|
204
204
|
expect(fetchCalls[0].url).toContain("http://127.0.0.1:7807");
|
|
205
205
|
expect(fetchCalls[0].url).toContain(
|
|
206
|
-
"/v1/assistants/alice-1/feature-flags/
|
|
206
|
+
"/v1/assistants/alice-1/feature-flags/voice-mode",
|
|
207
207
|
);
|
|
208
208
|
});
|
|
209
209
|
|
|
@@ -214,7 +214,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
214
214
|
"vellum",
|
|
215
215
|
"flags",
|
|
216
216
|
"set",
|
|
217
|
-
"
|
|
217
|
+
"voice-mode",
|
|
218
218
|
"true",
|
|
219
219
|
"--assistant",
|
|
220
220
|
"Ghost",
|
|
@@ -234,7 +234,7 @@ describe("vellum flags --assistant routing", () => {
|
|
|
234
234
|
"vellum",
|
|
235
235
|
"flags",
|
|
236
236
|
"set",
|
|
237
|
-
"
|
|
237
|
+
"voice-mode",
|
|
238
238
|
"true",
|
|
239
239
|
"--assistant",
|
|
240
240
|
];
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
existsSync,
|
|
14
14
|
mkdirSync,
|
|
15
15
|
mkdtempSync,
|
|
16
|
+
renameSync,
|
|
16
17
|
rmSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
@@ -220,37 +221,43 @@ describe("recover error cases", () => {
|
|
|
220
221
|
describe("recover extraction path — default instance (instanceDir === homedir())", () => {
|
|
221
222
|
test("extracts to retiredDir and renames staging dir to instanceDir/.vellum", async () => {
|
|
222
223
|
const name = "default-instance";
|
|
223
|
-
// Default instance: instanceDir is the real home directory
|
|
224
224
|
const entry = makeEntry(name, homedir());
|
|
225
225
|
const { archivePath, extractedPath } = writeArchiveFixtures(name, entry);
|
|
226
226
|
|
|
227
227
|
const expectedTargetDir = join(homedir(), ".vellum");
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
228
|
+
// If a real ~/.vellum exists (e.g. the machine runs a live assistant),
|
|
229
|
+
// temporarily move it aside so the collision guard doesn't fire.
|
|
230
|
+
const backupDir = join(homedir(), ".vellum-recover-test-bak");
|
|
231
|
+
const hadExisting = existsSync(expectedTargetDir);
|
|
232
|
+
if (hadExisting) renameSync(expectedTargetDir, backupDir);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
process.argv = ["bun", "vellum", "recover", name];
|
|
236
|
+
await recover();
|
|
237
|
+
|
|
238
|
+
// exec must have been called with -C retiredDir, NOT -C homedir()
|
|
239
|
+
expect(execMock).toHaveBeenCalledTimes(1);
|
|
240
|
+
const [cmd, args] = execMock.mock.calls[0] as [string, string[]];
|
|
241
|
+
expect(cmd).toBe("tar");
|
|
242
|
+
expect(args).toContain("-C");
|
|
243
|
+
const cIndex = args.indexOf("-C");
|
|
244
|
+
expect(args[cIndex + 1]).toBe(retiredDir);
|
|
245
|
+
expect(args[cIndex + 1]).not.toBe(homedir());
|
|
246
|
+
|
|
247
|
+
// Staging dir was renamed to the correct target
|
|
248
|
+
expect(existsSync(extractedPath)).toBe(false);
|
|
249
|
+
expect(existsSync(expectedTargetDir)).toBe(true);
|
|
250
|
+
|
|
251
|
+
// Archive and metadata were cleaned up
|
|
252
|
+
expect(existsSync(archivePath)).toBe(false);
|
|
253
|
+
|
|
254
|
+
// Daemon and gateway were started
|
|
255
|
+
expect(startLocalDaemonMock).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(startGatewayMock).toHaveBeenCalledTimes(1);
|
|
257
|
+
} finally {
|
|
258
|
+
rmSync(expectedTargetDir, { recursive: true, force: true });
|
|
259
|
+
if (hadExisting) renameSync(backupDir, expectedTargetDir);
|
|
260
|
+
}
|
|
254
261
|
});
|
|
255
262
|
});
|
|
256
263
|
|
|
@@ -259,12 +259,27 @@ const localRuntimePollJobStatusMock = mock<
|
|
|
259
259
|
},
|
|
260
260
|
}));
|
|
261
261
|
|
|
262
|
+
const localRuntimePreflightFromGcsMock = mock<
|
|
263
|
+
typeof localRuntimeClient.localRuntimePreflightFromGcs
|
|
264
|
+
>(async () => ({
|
|
265
|
+
can_import: true,
|
|
266
|
+
summary: {
|
|
267
|
+
files_to_create: 2,
|
|
268
|
+
files_to_overwrite: 1,
|
|
269
|
+
files_unchanged: 0,
|
|
270
|
+
total_files: 3,
|
|
271
|
+
},
|
|
272
|
+
files: [],
|
|
273
|
+
conflicts: [],
|
|
274
|
+
}));
|
|
275
|
+
|
|
262
276
|
mock.module("../lib/local-runtime-client.js", () => ({
|
|
263
277
|
...realLocalRuntimeClient,
|
|
264
278
|
localRuntimeExportToGcs: localRuntimeExportToGcsMock,
|
|
265
279
|
localRuntimeImportFromGcs: localRuntimeImportFromGcsMock,
|
|
266
280
|
localRuntimeIdentity: localRuntimeIdentityMock,
|
|
267
281
|
localRuntimePollJobStatus: localRuntimePollJobStatusMock,
|
|
282
|
+
localRuntimePreflightFromGcs: localRuntimePreflightFromGcsMock,
|
|
268
283
|
}));
|
|
269
284
|
|
|
270
285
|
const hatchLocalMock = mock(async () => {});
|
|
@@ -483,6 +498,18 @@ beforeEach(() => {
|
|
|
483
498
|
localRuntimeImportFromGcsMock.mockResolvedValue({
|
|
484
499
|
jobId: "local-import-job-1",
|
|
485
500
|
});
|
|
501
|
+
localRuntimePreflightFromGcsMock.mockReset();
|
|
502
|
+
localRuntimePreflightFromGcsMock.mockResolvedValue({
|
|
503
|
+
can_import: true,
|
|
504
|
+
summary: {
|
|
505
|
+
files_to_create: 2,
|
|
506
|
+
files_to_overwrite: 1,
|
|
507
|
+
files_unchanged: 0,
|
|
508
|
+
total_files: 3,
|
|
509
|
+
},
|
|
510
|
+
files: [],
|
|
511
|
+
conflicts: [],
|
|
512
|
+
});
|
|
486
513
|
localRuntimeIdentityMock.mockReset();
|
|
487
514
|
localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
|
|
488
515
|
localRuntimePollJobStatusMock.mockReset();
|
|
@@ -1932,7 +1959,7 @@ describe("dry-run", () => {
|
|
|
1932
1959
|
}
|
|
1933
1960
|
});
|
|
1934
1961
|
|
|
1935
|
-
test("dry-run
|
|
1962
|
+
test("dry-run with existing local target calls preflight-from-gcs on runtime", async () => {
|
|
1936
1963
|
setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
|
|
1937
1964
|
|
|
1938
1965
|
const platformEntry = makeEntry("my-platform", {
|
|
@@ -1947,26 +1974,24 @@ describe("dry-run", () => {
|
|
|
1947
1974
|
return null;
|
|
1948
1975
|
});
|
|
1949
1976
|
|
|
1950
|
-
platformPollJobStatusMock.mockResolvedValue({
|
|
1951
|
-
jobId: "platform-export-job-1",
|
|
1952
|
-
type: "export",
|
|
1953
|
-
status: "complete",
|
|
1954
|
-
bundleKey: "bundle-key-from-platform",
|
|
1955
|
-
});
|
|
1956
|
-
|
|
1957
1977
|
const restoreFetch = installTrackingFetch();
|
|
1958
1978
|
try {
|
|
1959
|
-
await
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1979
|
+
await teleport();
|
|
1980
|
+
|
|
1981
|
+
// Preflight was run against the local runtime
|
|
1982
|
+
expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledTimes(1);
|
|
1983
|
+
expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledWith(
|
|
1984
|
+
expect.objectContaining({ assistantId: "my-local" }),
|
|
1985
|
+
expect.any(String),
|
|
1986
|
+
{ bundleUrl: "https://storage.googleapis.com/bucket/signed-download" },
|
|
1964
1987
|
);
|
|
1965
1988
|
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1989
|
+
// No actual import happened — dry-run only
|
|
1990
|
+
expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
|
|
1991
|
+
|
|
1992
|
+
// Source was not retired
|
|
1993
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
1994
|
+
expect(retireDockerMock).not.toHaveBeenCalled();
|
|
1970
1995
|
} finally {
|
|
1971
1996
|
restoreFetch();
|
|
1972
1997
|
}
|
package/src/commands/flags.ts
CHANGED
|
@@ -98,7 +98,7 @@ function printHelp(): void {
|
|
|
98
98
|
" $ vellum flags set voice-mode true # enable a flag",
|
|
99
99
|
);
|
|
100
100
|
console.log(
|
|
101
|
-
" $ vellum flags set
|
|
101
|
+
" $ vellum flags set voice-mode true --assistant eval-1 # target by name/id",
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
|
package/src/commands/teleport.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
localRuntimeExportToGcs,
|
|
34
34
|
localRuntimeIdentity,
|
|
35
35
|
localRuntimeImportFromGcs,
|
|
36
|
+
localRuntimePreflightFromGcs,
|
|
36
37
|
localRuntimePollJobStatus,
|
|
37
38
|
MigrationInProgressError,
|
|
38
39
|
} from "../lib/local-runtime-client.js";
|
|
@@ -707,11 +708,46 @@ async function importToAssistant(
|
|
|
707
708
|
|
|
708
709
|
if (cloud === "local" || cloud === "docker") {
|
|
709
710
|
if (dryRun) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
console.log("Running preflight analysis...\n");
|
|
712
|
+
|
|
713
|
+
// Query the target runtime's version before requesting a signed
|
|
714
|
+
// download URL — the platform uses it for the bundle compatibility check.
|
|
715
|
+
let targetRuntimeVersion: string;
|
|
716
|
+
try {
|
|
717
|
+
const identity = await callRuntimeWithAuthRetry(entry, (token) =>
|
|
718
|
+
localRuntimeIdentity(entry, token),
|
|
719
|
+
);
|
|
720
|
+
targetRuntimeVersion = identity.version;
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
723
|
+
console.error(
|
|
724
|
+
`Error: Could not read target runtime version from '${entry.assistantId}': ${msg}`,
|
|
725
|
+
);
|
|
726
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let bundleUrl: string;
|
|
731
|
+
try {
|
|
732
|
+
const result = await platformRequestSignedUrl(
|
|
733
|
+
{ operation: "download", bundleKey, targetRuntimeVersion },
|
|
734
|
+
platformToken,
|
|
735
|
+
bundlePlatformUrl,
|
|
736
|
+
);
|
|
737
|
+
bundleUrl = result.url;
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (err instanceof VersionMismatchError) {
|
|
740
|
+
console.error(`Error: ${err.message}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const preflightData = await callRuntimeWithAuthRetry(entry, (token) =>
|
|
747
|
+
localRuntimePreflightFromGcs(entry, token, { bundleUrl }),
|
|
713
748
|
);
|
|
714
|
-
|
|
749
|
+
printPreflightSummary(preflightData as unknown as PreflightResponse);
|
|
750
|
+
return;
|
|
715
751
|
}
|
|
716
752
|
|
|
717
753
|
// Ask the platform for a signed download URL and hand it to the local
|
|
@@ -1231,18 +1267,8 @@ export async function teleport(): Promise<void> {
|
|
|
1231
1267
|
process.exit(1);
|
|
1232
1268
|
}
|
|
1233
1269
|
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
// so we can't actually run a dry-run against it; burning a GCS upload
|
|
1237
|
-
// just to fail afterwards would be wasteful.
|
|
1238
|
-
// TODO(cli): support dry-run against local targets (needs a
|
|
1239
|
-
// preflight-from-gcs endpoint on the runtime).
|
|
1240
|
-
if (toCloud === "local" || toCloud === "docker") {
|
|
1241
|
-
console.error(
|
|
1242
|
-
"Error: --dry-run is not yet supported for local or docker targets (no preflight-from-gcs endpoint on the runtime).",
|
|
1243
|
-
);
|
|
1244
|
-
process.exit(1);
|
|
1245
|
-
}
|
|
1270
|
+
// Nothing to reject — preflight-from-gcs is now implemented for all
|
|
1271
|
+
// target topologies including local and docker.
|
|
1246
1272
|
|
|
1247
1273
|
// Version guard: block platform→non-platform when target is behind
|
|
1248
1274
|
if (fromCloud === "vellum" && toCloud !== "vellum") {
|
|
@@ -195,6 +195,44 @@ export async function localRuntimeImportFromGcs(
|
|
|
195
195
|
return { jobId: json.job_id };
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Run a dry-run preflight analysis on a .vbundle archive stored at a signed
|
|
200
|
+
* GCS download URL.
|
|
201
|
+
*
|
|
202
|
+
* For local/docker assistants this POSTs to
|
|
203
|
+
* `{runtimeUrl}/v1/migrations/preflight-from-gcs` with guardian-token bearer
|
|
204
|
+
* auth. The daemon fetches the bundle from GCS, validates it, and returns a
|
|
205
|
+
* report of what would change on import — without writing anything to disk.
|
|
206
|
+
*
|
|
207
|
+
* Returns the raw preflight response body. Callers should cast to their
|
|
208
|
+
* local `PreflightResponse` shape (mirrors the `/import-preflight` shape).
|
|
209
|
+
*/
|
|
210
|
+
export async function localRuntimePreflightFromGcs(
|
|
211
|
+
entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
|
|
212
|
+
token: string,
|
|
213
|
+
params: { bundleUrl: string },
|
|
214
|
+
): Promise<Record<string, unknown>> {
|
|
215
|
+
const response = await fetch(
|
|
216
|
+
resolveRuntimeMigrationUrl(entry, "preflight-from-gcs"),
|
|
217
|
+
{
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: await migrationRequestHeaders(entry, token),
|
|
220
|
+
body: JSON.stringify({ bundle_url: params.bundleUrl }),
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (response.status !== 200) {
|
|
225
|
+
const errText = await response.text().catch(() => "");
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Local runtime preflight-from-gcs failed (${response.status}): ${
|
|
228
|
+
errText || response.statusText
|
|
229
|
+
}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (await response.json()) as Record<string, unknown>;
|
|
234
|
+
}
|
|
235
|
+
|
|
198
236
|
/**
|
|
199
237
|
* Poll the runtime's unified job-status endpoint.
|
|
200
238
|
*
|