@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.
- package/AGENTS.md +3 -11
- package/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
|
@@ -99,6 +99,23 @@ function tokenAuthHeader(token: string): Record<string, string> {
|
|
|
99
99
|
const orgIdCache = new Map<string, { orgId: string; expiresAt: number }>();
|
|
100
100
|
const ORG_ID_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Drop the cached org ID for a given (token, platformUrl) pair. Used by the
|
|
104
|
+
* one-shot 401-retry path: a 401 on a session-token request frequently means
|
|
105
|
+
* the cached `Vellum-Organization-Id` header is stale (e.g. user switched
|
|
106
|
+
* orgs in another tab). Clearing the entry forces the next `authHeaders`
|
|
107
|
+
* call to refetch the org ID from the platform.
|
|
108
|
+
*
|
|
109
|
+
* Exported so other modules (e.g. local-runtime-client) can implement the
|
|
110
|
+
* same retry pattern without needing direct access to the cache map.
|
|
111
|
+
*/
|
|
112
|
+
export function invalidateOrgIdCache(
|
|
113
|
+
token: string,
|
|
114
|
+
platformUrl?: string,
|
|
115
|
+
): void {
|
|
116
|
+
orgIdCache.delete(`${token}::${platformUrl ?? ""}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Returns the full set of headers needed for an authenticated platform
|
|
104
121
|
* API request:
|
|
@@ -468,6 +485,7 @@ export async function hatchAssistant(
|
|
|
468
485
|
method: "POST",
|
|
469
486
|
headers: await authHeaders(token, platformUrl),
|
|
470
487
|
body: JSON.stringify({}),
|
|
488
|
+
signal: AbortSignal.timeout(300_000),
|
|
471
489
|
});
|
|
472
490
|
|
|
473
491
|
if (response.ok) {
|
|
@@ -665,182 +683,10 @@ export async function rollbackPlatformAssistant(
|
|
|
665
683
|
throw new Error(`Rollback failed: ${response.status} ${response.statusText}`);
|
|
666
684
|
}
|
|
667
685
|
|
|
668
|
-
// ---------------------------------------------------------------------------
|
|
669
|
-
// Migration export
|
|
670
|
-
// ---------------------------------------------------------------------------
|
|
671
|
-
|
|
672
|
-
export async function platformInitiateExport(
|
|
673
|
-
token: string,
|
|
674
|
-
description?: string,
|
|
675
|
-
platformUrl?: string,
|
|
676
|
-
): Promise<{ jobId: string; status: string }> {
|
|
677
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
678
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/export/`, {
|
|
679
|
-
method: "POST",
|
|
680
|
-
headers: await authHeaders(token, platformUrl),
|
|
681
|
-
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
if (response.status !== 201) {
|
|
685
|
-
const body = (await response.json().catch(() => ({}))) as {
|
|
686
|
-
detail?: string;
|
|
687
|
-
};
|
|
688
|
-
throw new Error(
|
|
689
|
-
body.detail ??
|
|
690
|
-
`Export initiation failed: ${response.status} ${response.statusText}`,
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const body = (await response.json()) as {
|
|
695
|
-
job_id: string;
|
|
696
|
-
status: string;
|
|
697
|
-
};
|
|
698
|
-
return { jobId: body.job_id, status: body.status };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
export async function platformPollExportStatus(
|
|
702
|
-
jobId: string,
|
|
703
|
-
token: string,
|
|
704
|
-
platformUrl?: string,
|
|
705
|
-
): Promise<{ status: string; downloadUrl?: string; error?: string }> {
|
|
706
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
707
|
-
const response = await fetch(
|
|
708
|
-
`${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
|
|
709
|
-
{
|
|
710
|
-
headers: await authHeaders(token, platformUrl),
|
|
711
|
-
},
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
if (response.status === 404) {
|
|
715
|
-
throw new Error("Export job not found");
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (!response.ok) {
|
|
719
|
-
throw new Error(
|
|
720
|
-
`Export status check failed: ${response.status} ${response.statusText}`,
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const body = (await response.json()) as {
|
|
725
|
-
status: string;
|
|
726
|
-
download_url?: string;
|
|
727
|
-
error?: string;
|
|
728
|
-
};
|
|
729
|
-
return {
|
|
730
|
-
status: body.status,
|
|
731
|
-
downloadUrl: body.download_url,
|
|
732
|
-
error: body.error,
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
export async function platformDownloadExport(
|
|
737
|
-
downloadUrl: string,
|
|
738
|
-
): Promise<Response> {
|
|
739
|
-
const response = await fetch(downloadUrl);
|
|
740
|
-
if (!response.ok) {
|
|
741
|
-
throw new Error(
|
|
742
|
-
`Download failed: ${response.status} ${response.statusText}`,
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
return response;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// ---------------------------------------------------------------------------
|
|
749
|
-
// Migration import
|
|
750
|
-
// ---------------------------------------------------------------------------
|
|
751
|
-
|
|
752
|
-
export async function platformImportPreflight(
|
|
753
|
-
bundleData: Uint8Array<ArrayBuffer>,
|
|
754
|
-
token: string,
|
|
755
|
-
platformUrl?: string,
|
|
756
|
-
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
757
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
758
|
-
const response = await fetch(
|
|
759
|
-
`${resolvedUrl}/v1/migrations/import-preflight/`,
|
|
760
|
-
{
|
|
761
|
-
method: "POST",
|
|
762
|
-
headers: {
|
|
763
|
-
...(await authHeaders(token, platformUrl)),
|
|
764
|
-
"Content-Type": "application/octet-stream",
|
|
765
|
-
},
|
|
766
|
-
body: new Blob([bundleData]),
|
|
767
|
-
signal: AbortSignal.timeout(120_000),
|
|
768
|
-
},
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
const body = (await response.json().catch(() => ({}))) as Record<
|
|
772
|
-
string,
|
|
773
|
-
unknown
|
|
774
|
-
>;
|
|
775
|
-
return { statusCode: response.status, body };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
export async function platformImportBundle(
|
|
779
|
-
bundleData: Uint8Array<ArrayBuffer>,
|
|
780
|
-
token: string,
|
|
781
|
-
platformUrl?: string,
|
|
782
|
-
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
783
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
784
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/import/`, {
|
|
785
|
-
method: "POST",
|
|
786
|
-
headers: {
|
|
787
|
-
...(await authHeaders(token, platformUrl)),
|
|
788
|
-
"Content-Type": "application/octet-stream",
|
|
789
|
-
},
|
|
790
|
-
body: new Blob([bundleData]),
|
|
791
|
-
signal: AbortSignal.timeout(300_000),
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
const body = (await response.json().catch(() => ({}))) as Record<
|
|
795
|
-
string,
|
|
796
|
-
unknown
|
|
797
|
-
>;
|
|
798
|
-
return { statusCode: response.status, body };
|
|
799
|
-
}
|
|
800
|
-
|
|
801
686
|
// ---------------------------------------------------------------------------
|
|
802
687
|
// Signed-URL upload flow
|
|
803
688
|
// ---------------------------------------------------------------------------
|
|
804
689
|
|
|
805
|
-
export async function platformRequestUploadUrl(
|
|
806
|
-
token: string,
|
|
807
|
-
platformUrl?: string,
|
|
808
|
-
): Promise<{ uploadUrl: string; bundleKey: string; expiresAt: string }> {
|
|
809
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
810
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/upload-url/`, {
|
|
811
|
-
method: "POST",
|
|
812
|
-
headers: await authHeaders(token, platformUrl),
|
|
813
|
-
body: JSON.stringify({ content_type: "application/octet-stream" }),
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
if (response.status === 201) {
|
|
817
|
-
const body = (await response.json()) as {
|
|
818
|
-
upload_url: string;
|
|
819
|
-
bundle_key: string;
|
|
820
|
-
expires_at: string;
|
|
821
|
-
};
|
|
822
|
-
return {
|
|
823
|
-
uploadUrl: body.upload_url,
|
|
824
|
-
bundleKey: body.bundle_key,
|
|
825
|
-
expiresAt: body.expires_at,
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (response.status === 404 || response.status === 503) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
"Signed uploads are not available on this platform instance",
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
836
|
-
detail?: string;
|
|
837
|
-
};
|
|
838
|
-
throw new Error(
|
|
839
|
-
errorBody.detail ??
|
|
840
|
-
`Failed to request upload URL: ${response.status} ${response.statusText}`,
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
690
|
export async function platformUploadToSignedUrl(
|
|
845
691
|
uploadUrl: string,
|
|
846
692
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
@@ -911,46 +757,6 @@ export async function platformImportBundleFromGcs(
|
|
|
911
757
|
return { statusCode: response.status, body };
|
|
912
758
|
}
|
|
913
759
|
|
|
914
|
-
export async function platformPollImportStatus(
|
|
915
|
-
jobId: string,
|
|
916
|
-
token: string,
|
|
917
|
-
platformUrl?: string,
|
|
918
|
-
): Promise<{
|
|
919
|
-
status: string;
|
|
920
|
-
result?: Record<string, unknown>;
|
|
921
|
-
error?: string;
|
|
922
|
-
}> {
|
|
923
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
924
|
-
const response = await fetch(
|
|
925
|
-
`${resolvedUrl}/v1/migrations/import/${jobId}/status/`,
|
|
926
|
-
{
|
|
927
|
-
headers: await authHeaders(token, platformUrl),
|
|
928
|
-
},
|
|
929
|
-
);
|
|
930
|
-
|
|
931
|
-
if (response.status === 404) {
|
|
932
|
-
throw new Error("Import job not found");
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (!response.ok) {
|
|
936
|
-
throw new Error(
|
|
937
|
-
`Import status check failed: ${response.status} ${response.statusText}`,
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const body = (await response.json()) as {
|
|
942
|
-
status: string;
|
|
943
|
-
job_id?: string;
|
|
944
|
-
result?: Record<string, unknown>;
|
|
945
|
-
error?: string;
|
|
946
|
-
};
|
|
947
|
-
return {
|
|
948
|
-
status: body.status,
|
|
949
|
-
result: body.result,
|
|
950
|
-
error: body.error,
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
|
|
954
760
|
// ---------------------------------------------------------------------------
|
|
955
761
|
// Unified signed-url + job-status endpoints (teleport-gcs-unify)
|
|
956
762
|
// ---------------------------------------------------------------------------
|
|
@@ -1017,6 +823,45 @@ export function parseUnifiedJobStatus(
|
|
|
1017
823
|
};
|
|
1018
824
|
}
|
|
1019
825
|
|
|
826
|
+
export interface BundleCompatibility {
|
|
827
|
+
min_runtime_version: string;
|
|
828
|
+
max_runtime_version: string | null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Thrown by platformRequestSignedUrl when the platform rejects a download
|
|
833
|
+
* signed-URL request because the target runtime version is outside the
|
|
834
|
+
* ExportJob's [min_runtime_version, max_runtime_version] band. Terminal
|
|
835
|
+
* — callers must NOT retry; surface to the user and abort the
|
|
836
|
+
* teleport/restore wizard.
|
|
837
|
+
*/
|
|
838
|
+
export class VersionMismatchError extends Error {
|
|
839
|
+
readonly bundleCompat: BundleCompatibility;
|
|
840
|
+
readonly targetRuntimeVersion: string;
|
|
841
|
+
|
|
842
|
+
constructor(bundleCompat: BundleCompatibility, targetRuntimeVersion: string) {
|
|
843
|
+
super(
|
|
844
|
+
VersionMismatchError.formatMessage(bundleCompat, targetRuntimeVersion),
|
|
845
|
+
);
|
|
846
|
+
this.name = "VersionMismatchError";
|
|
847
|
+
this.bundleCompat = bundleCompat;
|
|
848
|
+
this.targetRuntimeVersion = targetRuntimeVersion;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
static formatMessage(
|
|
852
|
+
compat: BundleCompatibility,
|
|
853
|
+
targetRuntimeVersion: string,
|
|
854
|
+
): string {
|
|
855
|
+
const range = compat.max_runtime_version
|
|
856
|
+
? `${compat.min_runtime_version}–${compat.max_runtime_version}`
|
|
857
|
+
: `${compat.min_runtime_version}+`;
|
|
858
|
+
return (
|
|
859
|
+
`Cannot import: bundle requires runtime ${range}, but this runtime is ${targetRuntimeVersion}. ` +
|
|
860
|
+
`Update your runtime before importing.`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
1020
865
|
/**
|
|
1021
866
|
* Request a signed URL from the platform for either uploading a new bundle
|
|
1022
867
|
* or downloading an existing one. Calls `POST /v1/migrations/signed-url/`.
|
|
@@ -1027,8 +872,10 @@ export function parseUnifiedJobStatus(
|
|
|
1027
872
|
* runtime can GET the bundle from during an import-from-GCS flow.
|
|
1028
873
|
*
|
|
1029
874
|
* Retries once with a fresh org-ID cache on 401 to match the retry pattern
|
|
1030
|
-
* used by other authenticated platform helpers.
|
|
1031
|
-
*
|
|
875
|
+
* used by other authenticated platform helpers.
|
|
876
|
+
*
|
|
877
|
+
* Throws {@link VersionMismatchError} on a 422 `version_mismatch` response,
|
|
878
|
+
* which is terminal — callers must NOT retry.
|
|
1032
879
|
*/
|
|
1033
880
|
export async function platformRequestSignedUrl(
|
|
1034
881
|
params: {
|
|
@@ -1036,6 +883,11 @@ export async function platformRequestSignedUrl(
|
|
|
1036
883
|
bundleKey?: string;
|
|
1037
884
|
contentType?: string;
|
|
1038
885
|
contentLength?: number;
|
|
886
|
+
// Source-side, upload only: runtime version that produced the bundle.
|
|
887
|
+
minRuntimeVersion?: string;
|
|
888
|
+
maxRuntimeVersion?: string | null;
|
|
889
|
+
// Target-side, download only: runtime version that will import.
|
|
890
|
+
targetRuntimeVersion?: string;
|
|
1039
891
|
},
|
|
1040
892
|
token: string,
|
|
1041
893
|
platformUrl?: string,
|
|
@@ -1052,6 +904,17 @@ export async function platformRequestSignedUrl(
|
|
|
1052
904
|
if (params.contentLength !== undefined) {
|
|
1053
905
|
body.content_length = params.contentLength;
|
|
1054
906
|
}
|
|
907
|
+
if (params.minRuntimeVersion !== undefined) {
|
|
908
|
+
body.min_runtime_version = params.minRuntimeVersion;
|
|
909
|
+
}
|
|
910
|
+
if (params.maxRuntimeVersion !== undefined) {
|
|
911
|
+
// Explicit null is the documented "no upper bound" sentinel; keep it
|
|
912
|
+
// in the payload rather than stripping to undefined.
|
|
913
|
+
body.max_runtime_version = params.maxRuntimeVersion;
|
|
914
|
+
}
|
|
915
|
+
if (params.targetRuntimeVersion !== undefined) {
|
|
916
|
+
body.target_runtime_version = params.targetRuntimeVersion;
|
|
917
|
+
}
|
|
1055
918
|
|
|
1056
919
|
const doRequest = async (): Promise<Response> =>
|
|
1057
920
|
fetch(`${resolvedUrl}/v1/migrations/signed-url/`, {
|
|
@@ -1067,7 +930,7 @@ export async function platformRequestSignedUrl(
|
|
|
1067
930
|
// lookup. For session-token callers, a 401 frequently means the
|
|
1068
931
|
// cached org ID is stale — calling doRequest() again without clearing
|
|
1069
932
|
// the cache would just send the same stale header and fail again.
|
|
1070
|
-
|
|
933
|
+
invalidateOrgIdCache(token, platformUrl);
|
|
1071
934
|
response = await doRequest();
|
|
1072
935
|
}
|
|
1073
936
|
|
|
@@ -1086,15 +949,28 @@ export async function platformRequestSignedUrl(
|
|
|
1086
949
|
};
|
|
1087
950
|
}
|
|
1088
951
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
952
|
+
// Non-success body. Read once and reuse for both the 422 version-mismatch
|
|
953
|
+
// branch and the generic-error fallthrough — `response.json()` consumes
|
|
954
|
+
// the body, so a second read would always return undefined.
|
|
1095
955
|
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
1096
956
|
detail?: string;
|
|
957
|
+
reason?: string;
|
|
958
|
+
bundle_compat?: BundleCompatibility;
|
|
959
|
+
target_runtime_version?: string;
|
|
1097
960
|
};
|
|
961
|
+
|
|
962
|
+
if (
|
|
963
|
+
response.status === 422 &&
|
|
964
|
+
errorBody.reason === "version_mismatch" &&
|
|
965
|
+
errorBody.bundle_compat &&
|
|
966
|
+
typeof errorBody.target_runtime_version === "string"
|
|
967
|
+
) {
|
|
968
|
+
throw new VersionMismatchError(
|
|
969
|
+
errorBody.bundle_compat,
|
|
970
|
+
errorBody.target_runtime_version,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
1098
974
|
throw new Error(
|
|
1099
975
|
errorBody.detail ??
|
|
1100
976
|
`Failed to request signed URL: ${response.status} ${response.statusText}`,
|
|
@@ -7,6 +7,29 @@ export interface ResolvedImageRefs {
|
|
|
7
7
|
source: "platform" | "dockerhub";
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the latest stable release version from the platform API.
|
|
12
|
+
* Returns the version string (e.g. "0.7.0") or null if unavailable.
|
|
13
|
+
* The releases endpoint returns entries ordered newest-first.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchLatestStableVersion(): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const platformUrl = getPlatformUrl();
|
|
18
|
+
const response = await fetch(`${platformUrl}/v1/releases/?stable=true`, {
|
|
19
|
+
signal: AbortSignal.timeout(10_000),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return null;
|
|
22
|
+
|
|
23
|
+
const releases = (await response.json()) as Array<{
|
|
24
|
+
version?: string;
|
|
25
|
+
}>;
|
|
26
|
+
const first = releases[0];
|
|
27
|
+
return first?.version ?? null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
/**
|
|
11
34
|
* Resolve image references for a given version.
|
|
12
35
|
*
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import { homedir } from "os";
|
|
2
3
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
4
|
import { basename, dirname, join } from "path";
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
|
-
getBaseDir,
|
|
7
7
|
getDaemonPidPath,
|
|
8
8
|
loadAllAssistants,
|
|
9
9
|
} from "./assistant-config.js";
|
|
@@ -77,7 +77,7 @@ export async function retireLocal(
|
|
|
77
77
|
// For named instances (instanceDir differs from the base directory),
|
|
78
78
|
// archive and remove the entire instance directory. For the default
|
|
79
79
|
// instance, archive only the .vellum subdirectory.
|
|
80
|
-
const isNamedInstance = resources.instanceDir !==
|
|
80
|
+
const isNamedInstance = resources.instanceDir !== homedir();
|
|
81
81
|
const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
|
|
82
82
|
|
|
83
83
|
// Move the data directory out of the way so the path is immediately available
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AssistantEntry } from "./assistant-config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the URL for a runtime migration endpoint, taking the assistant's
|
|
5
|
+
* topology into account.
|
|
6
|
+
*
|
|
7
|
+
* - For local/docker assistants, `runtimeUrl` is the loopback gateway and
|
|
8
|
+
* the runtime serves `/v1/migrations/<subpath>` directly. The CLI hits
|
|
9
|
+
* that path with guardian-token bearer auth.
|
|
10
|
+
* - For platform-managed (cloud="vellum") assistants, `runtimeUrl` is the
|
|
11
|
+
* platform host (e.g. `https://platform.vellum.ai`). The platform's
|
|
12
|
+
* `MigrationViewSet` does NOT expose `export-to-gcs` or arbitrary runtime
|
|
13
|
+
* migration paths under `/v1/migrations/...`. The wildcard runtime proxy
|
|
14
|
+
* at `/v1/assistants/<id>/<path:rest>` is what forwards arbitrary runtime
|
|
15
|
+
* paths to the managed runtime — vembda's unified proxy bootstraps the
|
|
16
|
+
* guardian token internally for the runtime call. From the CLI side it's
|
|
17
|
+
* user-session auth.
|
|
18
|
+
*
|
|
19
|
+
* The `subpath` is appended to the migrations namespace verbatim
|
|
20
|
+
* (e.g. `"export-to-gcs"`, `"import-from-gcs"`, `\`jobs/${jobId}\``).
|
|
21
|
+
*/
|
|
22
|
+
export function resolveRuntimeMigrationUrl(
|
|
23
|
+
entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
|
|
24
|
+
subpath: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (entry.cloud === "vellum") {
|
|
27
|
+
return `${entry.runtimeUrl}/v1/assistants/${entry.assistantId}/migrations/${subpath}`;
|
|
28
|
+
}
|
|
29
|
+
return `${entry.runtimeUrl}/v1/migrations/${subpath}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the URL for a generic runtime endpoint under `/v1/<subpath>`,
|
|
34
|
+
* taking the assistant's topology into account.
|
|
35
|
+
*
|
|
36
|
+
* - For local/docker assistants, `runtimeUrl` is the loopback gateway and
|
|
37
|
+
* the runtime serves `/v1/<subpath>` directly.
|
|
38
|
+
* - For platform-managed (cloud="vellum") assistants the path is rewritten
|
|
39
|
+
* to the wildcard runtime proxy:
|
|
40
|
+
* `{platformUrl}/v1/assistants/<assistantId>/<subpath>`.
|
|
41
|
+
*
|
|
42
|
+
* The `subpath` is appended verbatim (e.g. `"identity"`).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveRuntimeUrl(
|
|
45
|
+
entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
|
|
46
|
+
subpath: string,
|
|
47
|
+
): string {
|
|
48
|
+
if (entry.cloud === "vellum") {
|
|
49
|
+
return `${entry.runtimeUrl}/v1/assistants/${entry.assistantId}/${subpath}`;
|
|
50
|
+
}
|
|
51
|
+
return `${entry.runtimeUrl}/v1/${subpath}`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync cloud-managed (platform) assistants into the local lockfile.
|
|
3
|
+
*
|
|
4
|
+
* - Adds new platform assistants that aren't in the lockfile yet.
|
|
5
|
+
* - Removes lockfile entries whose IDs are no longer returned by the platform
|
|
6
|
+
* (e.g. retired assistants).
|
|
7
|
+
*
|
|
8
|
+
* Used by both `vellum login` and `vellum ps` to keep the lockfile fresh.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
loadAllAssistants,
|
|
13
|
+
removeAssistantEntry,
|
|
14
|
+
saveAssistantEntry,
|
|
15
|
+
} from "./assistant-config.js";
|
|
16
|
+
import {
|
|
17
|
+
fetchCurrentUser,
|
|
18
|
+
fetchPlatformAssistants,
|
|
19
|
+
getPlatformUrl,
|
|
20
|
+
readPlatformToken,
|
|
21
|
+
} from "./platform-client.js";
|
|
22
|
+
|
|
23
|
+
export type SyncLogger = (message: string) => void;
|
|
24
|
+
|
|
25
|
+
export interface SyncResult {
|
|
26
|
+
added: number;
|
|
27
|
+
removed: number;
|
|
28
|
+
email?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SyncOptions {
|
|
32
|
+
log?: SyncLogger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch platform assistants and reconcile against the lockfile.
|
|
37
|
+
* Returns the number of entries added/removed, or `null` if the user
|
|
38
|
+
* is not logged in or the fetch fails.
|
|
39
|
+
*/
|
|
40
|
+
export async function syncCloudAssistants(
|
|
41
|
+
options?: SyncOptions,
|
|
42
|
+
): Promise<SyncResult | null> {
|
|
43
|
+
const log = options?.log;
|
|
44
|
+
const platformUrl = getPlatformUrl();
|
|
45
|
+
log?.(`Platform URL: ${platformUrl}`);
|
|
46
|
+
|
|
47
|
+
const token = readPlatformToken();
|
|
48
|
+
if (!token) {
|
|
49
|
+
log?.("No platform token found — skipping cloud sync");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
log?.(
|
|
53
|
+
`Token found (${token.length} chars, prefix: ${token.slice(0, 6)}…)`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Fetch user info for the login status line
|
|
57
|
+
let email: string | undefined;
|
|
58
|
+
try {
|
|
59
|
+
log?.("Fetching current user…");
|
|
60
|
+
const user = await fetchCurrentUser(token);
|
|
61
|
+
email = user.email;
|
|
62
|
+
log?.(`Authenticated as ${user.email} (${user.id})`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
log?.(`Failed to fetch current user: ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let platformAssistants: { id: string; name: string; status: string }[];
|
|
69
|
+
try {
|
|
70
|
+
log?.("Fetching platform assistants…");
|
|
71
|
+
platformAssistants = await fetchPlatformAssistants(token);
|
|
72
|
+
log?.(
|
|
73
|
+
`Platform returned ${platformAssistants.length} assistant(s): ${platformAssistants.map((a) => a.name || a.id).join(", ") || "(none)"}`,
|
|
74
|
+
);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
log?.(`fetchPlatformAssistants failed: ${msg}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (platformAssistants.length === 0) {
|
|
82
|
+
log?.(
|
|
83
|
+
"Platform returned 0 assistants — this may mean the API returned a non-ok status (check token validity)",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const platformIds = new Set(platformAssistants.map((a) => a.id));
|
|
88
|
+
|
|
89
|
+
// Add new platform assistants not yet in the lockfile
|
|
90
|
+
const existingCloudIds = new Set(
|
|
91
|
+
loadAllAssistants()
|
|
92
|
+
.filter((a) => a.cloud === "vellum")
|
|
93
|
+
.map((a) => a.assistantId),
|
|
94
|
+
);
|
|
95
|
+
log?.(
|
|
96
|
+
`Lockfile has ${existingCloudIds.size} cloud assistant(s): ${[...existingCloudIds].join(", ") || "(none)"}`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
let added = 0;
|
|
100
|
+
for (const pa of platformAssistants) {
|
|
101
|
+
if (!existingCloudIds.has(pa.id)) {
|
|
102
|
+
log?.(`Adding ${pa.name || pa.id} to lockfile`);
|
|
103
|
+
saveAssistantEntry({
|
|
104
|
+
assistantId: pa.id,
|
|
105
|
+
runtimeUrl: getPlatformUrl(),
|
|
106
|
+
cloud: "vellum",
|
|
107
|
+
species: "vellum",
|
|
108
|
+
hatchedAt: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
added++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove stale lockfile entries that the platform no longer knows about
|
|
115
|
+
let removed = 0;
|
|
116
|
+
for (const id of existingCloudIds) {
|
|
117
|
+
if (!platformIds.has(id)) {
|
|
118
|
+
log?.(`Removing stale entry ${id} from lockfile`);
|
|
119
|
+
removeAssistantEntry(id);
|
|
120
|
+
removed++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log?.(`Sync complete: ${added} added, ${removed} removed`);
|
|
125
|
+
return { added, removed, email };
|
|
126
|
+
}
|
|
@@ -18,14 +18,19 @@ export async function createTerminalSession(
|
|
|
18
18
|
cols: number,
|
|
19
19
|
rows: number,
|
|
20
20
|
platformUrl?: string,
|
|
21
|
+
service?: string,
|
|
21
22
|
): Promise<{ session_id: string }> {
|
|
22
23
|
const baseUrl = platformUrl || getPlatformUrl();
|
|
24
|
+
const body: Record<string, unknown> = { cols, rows };
|
|
25
|
+
if (service) {
|
|
26
|
+
body.service = service;
|
|
27
|
+
}
|
|
23
28
|
const response = await fetch(
|
|
24
29
|
`${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/`,
|
|
25
30
|
{
|
|
26
31
|
method: "POST",
|
|
27
32
|
headers: await authHeaders(token, platformUrl),
|
|
28
|
-
body: JSON.stringify(
|
|
33
|
+
body: JSON.stringify(body),
|
|
29
34
|
},
|
|
30
35
|
);
|
|
31
36
|
if (!response.ok) {
|