@vellumai/cli 0.10.3-dev.202606290949.0cf8e08 → 0.10.3-dev.202606291342.c7a83ea
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
|
@@ -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/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
|
*
|