@vellumai/cli 0.6.6 → 0.7.1
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 +8 -2
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
|
@@ -40,6 +40,19 @@ export function getPlatformUrl(): string {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the web app (Next.js) base URL for browser-facing pages like
|
|
45
|
+
* `/account/login`. Mirrors `VellumEnvironment.resolvedWebURL` on the
|
|
46
|
+
* Swift side.
|
|
47
|
+
*
|
|
48
|
+
* Resolution order:
|
|
49
|
+
* 1. `VELLUM_WEB_URL` env var (explicit override)
|
|
50
|
+
* 2. The current environment's seed web URL
|
|
51
|
+
*/
|
|
52
|
+
export function getWebUrl(): string {
|
|
53
|
+
return process.env.VELLUM_WEB_URL?.trim() || getCurrentEnvironment().webUrl;
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export function readPlatformToken(): string | null {
|
|
44
57
|
try {
|
|
45
58
|
return readFileSync(getPlatformTokenPath(), "utf-8").trim();
|
|
@@ -516,6 +529,30 @@ export async function checkExistingPlatformAssistant(
|
|
|
516
529
|
return active ?? null;
|
|
517
530
|
}
|
|
518
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Fetch all active assistants for the authenticated user from the platform.
|
|
534
|
+
* Returns an empty array on failure (non-fatal).
|
|
535
|
+
*/
|
|
536
|
+
export async function fetchPlatformAssistants(
|
|
537
|
+
token: string,
|
|
538
|
+
platformUrl?: string,
|
|
539
|
+
): Promise<HatchedAssistant[]> {
|
|
540
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
541
|
+
const url = `${resolvedUrl}/v1/assistants/`;
|
|
542
|
+
|
|
543
|
+
const response = await fetch(url, {
|
|
544
|
+
headers: await authHeaders(token, platformUrl),
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (!response.ok) return [];
|
|
548
|
+
|
|
549
|
+
const body = (await response.json()) as {
|
|
550
|
+
results?: HatchedAssistant[];
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return (body.results ?? []).filter((a) => a.status === "active");
|
|
554
|
+
}
|
|
555
|
+
|
|
519
556
|
export interface PlatformUser {
|
|
520
557
|
id: string;
|
|
521
558
|
email: string;
|
|
@@ -628,182 +665,10 @@ export async function rollbackPlatformAssistant(
|
|
|
628
665
|
throw new Error(`Rollback failed: ${response.status} ${response.statusText}`);
|
|
629
666
|
}
|
|
630
667
|
|
|
631
|
-
// ---------------------------------------------------------------------------
|
|
632
|
-
// Migration export
|
|
633
|
-
// ---------------------------------------------------------------------------
|
|
634
|
-
|
|
635
|
-
export async function platformInitiateExport(
|
|
636
|
-
token: string,
|
|
637
|
-
description?: string,
|
|
638
|
-
platformUrl?: string,
|
|
639
|
-
): Promise<{ jobId: string; status: string }> {
|
|
640
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
641
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/export/`, {
|
|
642
|
-
method: "POST",
|
|
643
|
-
headers: await authHeaders(token, platformUrl),
|
|
644
|
-
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
if (response.status !== 201) {
|
|
648
|
-
const body = (await response.json().catch(() => ({}))) as {
|
|
649
|
-
detail?: string;
|
|
650
|
-
};
|
|
651
|
-
throw new Error(
|
|
652
|
-
body.detail ??
|
|
653
|
-
`Export initiation failed: ${response.status} ${response.statusText}`,
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const body = (await response.json()) as {
|
|
658
|
-
job_id: string;
|
|
659
|
-
status: string;
|
|
660
|
-
};
|
|
661
|
-
return { jobId: body.job_id, status: body.status };
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export async function platformPollExportStatus(
|
|
665
|
-
jobId: string,
|
|
666
|
-
token: string,
|
|
667
|
-
platformUrl?: string,
|
|
668
|
-
): Promise<{ status: string; downloadUrl?: string; error?: string }> {
|
|
669
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
670
|
-
const response = await fetch(
|
|
671
|
-
`${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
|
|
672
|
-
{
|
|
673
|
-
headers: await authHeaders(token, platformUrl),
|
|
674
|
-
},
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
if (response.status === 404) {
|
|
678
|
-
throw new Error("Export job not found");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (!response.ok) {
|
|
682
|
-
throw new Error(
|
|
683
|
-
`Export status check failed: ${response.status} ${response.statusText}`,
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const body = (await response.json()) as {
|
|
688
|
-
status: string;
|
|
689
|
-
download_url?: string;
|
|
690
|
-
error?: string;
|
|
691
|
-
};
|
|
692
|
-
return {
|
|
693
|
-
status: body.status,
|
|
694
|
-
downloadUrl: body.download_url,
|
|
695
|
-
error: body.error,
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export async function platformDownloadExport(
|
|
700
|
-
downloadUrl: string,
|
|
701
|
-
): Promise<Response> {
|
|
702
|
-
const response = await fetch(downloadUrl);
|
|
703
|
-
if (!response.ok) {
|
|
704
|
-
throw new Error(
|
|
705
|
-
`Download failed: ${response.status} ${response.statusText}`,
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
return response;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
// Migration import
|
|
713
|
-
// ---------------------------------------------------------------------------
|
|
714
|
-
|
|
715
|
-
export async function platformImportPreflight(
|
|
716
|
-
bundleData: Uint8Array<ArrayBuffer>,
|
|
717
|
-
token: string,
|
|
718
|
-
platformUrl?: string,
|
|
719
|
-
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
720
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
721
|
-
const response = await fetch(
|
|
722
|
-
`${resolvedUrl}/v1/migrations/import-preflight/`,
|
|
723
|
-
{
|
|
724
|
-
method: "POST",
|
|
725
|
-
headers: {
|
|
726
|
-
...(await authHeaders(token, platformUrl)),
|
|
727
|
-
"Content-Type": "application/octet-stream",
|
|
728
|
-
},
|
|
729
|
-
body: new Blob([bundleData]),
|
|
730
|
-
signal: AbortSignal.timeout(120_000),
|
|
731
|
-
},
|
|
732
|
-
);
|
|
733
|
-
|
|
734
|
-
const body = (await response.json().catch(() => ({}))) as Record<
|
|
735
|
-
string,
|
|
736
|
-
unknown
|
|
737
|
-
>;
|
|
738
|
-
return { statusCode: response.status, body };
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
export async function platformImportBundle(
|
|
742
|
-
bundleData: Uint8Array<ArrayBuffer>,
|
|
743
|
-
token: string,
|
|
744
|
-
platformUrl?: string,
|
|
745
|
-
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
746
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
747
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/import/`, {
|
|
748
|
-
method: "POST",
|
|
749
|
-
headers: {
|
|
750
|
-
...(await authHeaders(token, platformUrl)),
|
|
751
|
-
"Content-Type": "application/octet-stream",
|
|
752
|
-
},
|
|
753
|
-
body: new Blob([bundleData]),
|
|
754
|
-
signal: AbortSignal.timeout(300_000),
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
const body = (await response.json().catch(() => ({}))) as Record<
|
|
758
|
-
string,
|
|
759
|
-
unknown
|
|
760
|
-
>;
|
|
761
|
-
return { statusCode: response.status, body };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
668
|
// ---------------------------------------------------------------------------
|
|
765
669
|
// Signed-URL upload flow
|
|
766
670
|
// ---------------------------------------------------------------------------
|
|
767
671
|
|
|
768
|
-
export async function platformRequestUploadUrl(
|
|
769
|
-
token: string,
|
|
770
|
-
platformUrl?: string,
|
|
771
|
-
): Promise<{ uploadUrl: string; bundleKey: string; expiresAt: string }> {
|
|
772
|
-
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
773
|
-
const response = await fetch(`${resolvedUrl}/v1/migrations/upload-url/`, {
|
|
774
|
-
method: "POST",
|
|
775
|
-
headers: await authHeaders(token, platformUrl),
|
|
776
|
-
body: JSON.stringify({ content_type: "application/octet-stream" }),
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
if (response.status === 201) {
|
|
780
|
-
const body = (await response.json()) as {
|
|
781
|
-
upload_url: string;
|
|
782
|
-
bundle_key: string;
|
|
783
|
-
expires_at: string;
|
|
784
|
-
};
|
|
785
|
-
return {
|
|
786
|
-
uploadUrl: body.upload_url,
|
|
787
|
-
bundleKey: body.bundle_key,
|
|
788
|
-
expiresAt: body.expires_at,
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (response.status === 404 || response.status === 503) {
|
|
793
|
-
throw new Error(
|
|
794
|
-
"Signed uploads are not available on this platform instance",
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
799
|
-
detail?: string;
|
|
800
|
-
};
|
|
801
|
-
throw new Error(
|
|
802
|
-
errorBody.detail ??
|
|
803
|
-
`Failed to request upload URL: ${response.status} ${response.statusText}`,
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
672
|
export async function platformUploadToSignedUrl(
|
|
808
673
|
uploadUrl: string,
|
|
809
674
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
@@ -874,42 +739,173 @@ export async function platformImportBundleFromGcs(
|
|
|
874
739
|
return { statusCode: response.status, body };
|
|
875
740
|
}
|
|
876
741
|
|
|
877
|
-
|
|
878
|
-
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
// Unified signed-url + job-status endpoints (teleport-gcs-unify)
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Discriminated union representing the unified migration job status shape
|
|
748
|
+
* returned by `GET /v1/migrations/jobs/{job_id}/` on both the platform and
|
|
749
|
+
* the local runtime.
|
|
750
|
+
*/
|
|
751
|
+
export type UnifiedJobStatus =
|
|
752
|
+
| {
|
|
753
|
+
jobId: string;
|
|
754
|
+
type: "export" | "import";
|
|
755
|
+
status: "processing";
|
|
756
|
+
}
|
|
757
|
+
| {
|
|
758
|
+
jobId: string;
|
|
759
|
+
type: "export" | "import";
|
|
760
|
+
status: "complete";
|
|
761
|
+
bundleKey?: string;
|
|
762
|
+
result?: unknown;
|
|
763
|
+
}
|
|
764
|
+
| {
|
|
765
|
+
jobId: string;
|
|
766
|
+
type: "export" | "import";
|
|
767
|
+
status: "failed";
|
|
768
|
+
error: string;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
interface RawUnifiedJobStatus {
|
|
772
|
+
job_id: string;
|
|
773
|
+
type: "export" | "import";
|
|
774
|
+
status: "processing" | "complete" | "failed";
|
|
775
|
+
bundle_key?: string;
|
|
776
|
+
result?: unknown;
|
|
777
|
+
error?: string;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Normalise the wire-format job-status payload into the TypeScript
|
|
782
|
+
* discriminated union. Shared between platform and local-runtime helpers
|
|
783
|
+
* since both endpoints return the same shape.
|
|
784
|
+
*/
|
|
785
|
+
export function parseUnifiedJobStatus(
|
|
786
|
+
raw: RawUnifiedJobStatus,
|
|
787
|
+
): UnifiedJobStatus {
|
|
788
|
+
if (raw.status === "processing") {
|
|
789
|
+
return { jobId: raw.job_id, type: raw.type, status: "processing" };
|
|
790
|
+
}
|
|
791
|
+
if (raw.status === "complete") {
|
|
792
|
+
return {
|
|
793
|
+
jobId: raw.job_id,
|
|
794
|
+
type: raw.type,
|
|
795
|
+
status: "complete",
|
|
796
|
+
bundleKey: raw.bundle_key,
|
|
797
|
+
result: raw.result,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
jobId: raw.job_id,
|
|
802
|
+
type: raw.type,
|
|
803
|
+
status: "failed",
|
|
804
|
+
error: raw.error ?? "Job failed without an error message",
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Request a signed URL from the platform for either uploading a new bundle
|
|
810
|
+
* or downloading an existing one. Calls `POST /v1/migrations/signed-url/`.
|
|
811
|
+
*
|
|
812
|
+
* - `operation: "upload"` (optionally with `contentType` / `contentLength`)
|
|
813
|
+
* returns a URL the CLI can PUT a bundle to.
|
|
814
|
+
* - `operation: "download"` with a `bundleKey` returns a URL the local
|
|
815
|
+
* runtime can GET the bundle from during an import-from-GCS flow.
|
|
816
|
+
*
|
|
817
|
+
* Retries once with a fresh org-ID cache on 401 to match the retry pattern
|
|
818
|
+
* used by other authenticated platform helpers.
|
|
819
|
+
*/
|
|
820
|
+
export async function platformRequestSignedUrl(
|
|
821
|
+
params: {
|
|
822
|
+
operation: "upload" | "download";
|
|
823
|
+
bundleKey?: string;
|
|
824
|
+
contentType?: string;
|
|
825
|
+
contentLength?: number;
|
|
826
|
+
},
|
|
879
827
|
token: string,
|
|
880
828
|
platformUrl?: string,
|
|
881
829
|
): Promise<{
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
830
|
+
url: string;
|
|
831
|
+
bundleKey: string;
|
|
832
|
+
expiresAt: string;
|
|
833
|
+
maxContentLength?: number;
|
|
885
834
|
}> {
|
|
886
835
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
836
|
+
const body: Record<string, unknown> = { operation: params.operation };
|
|
837
|
+
if (params.bundleKey !== undefined) body.bundle_key = params.bundleKey;
|
|
838
|
+
if (params.contentType !== undefined) body.content_type = params.contentType;
|
|
839
|
+
if (params.contentLength !== undefined) {
|
|
840
|
+
body.content_length = params.contentLength;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const doRequest = async (): Promise<Response> =>
|
|
844
|
+
fetch(`${resolvedUrl}/v1/migrations/signed-url/`, {
|
|
845
|
+
method: "POST",
|
|
890
846
|
headers: await authHeaders(token, platformUrl),
|
|
891
|
-
|
|
847
|
+
body: JSON.stringify(body),
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
let response = await doRequest();
|
|
851
|
+
|
|
852
|
+
if (response.status === 401) {
|
|
853
|
+
// Invalidate the cached org-ID (if any) and retry once with a fresh
|
|
854
|
+
// lookup. For session-token callers, a 401 frequently means the
|
|
855
|
+
// cached org ID is stale — calling doRequest() again without clearing
|
|
856
|
+
// the cache would just send the same stale header and fail again.
|
|
857
|
+
orgIdCache.delete(`${token}::${platformUrl ?? ""}`);
|
|
858
|
+
response = await doRequest();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (response.status === 201 || response.status === 200) {
|
|
862
|
+
const json = (await response.json()) as {
|
|
863
|
+
url: string;
|
|
864
|
+
bundle_key: string;
|
|
865
|
+
expires_at: string;
|
|
866
|
+
max_content_length?: number;
|
|
867
|
+
};
|
|
868
|
+
return {
|
|
869
|
+
url: json.url,
|
|
870
|
+
bundleKey: json.bundle_key,
|
|
871
|
+
expiresAt: json.expires_at,
|
|
872
|
+
maxContentLength: json.max_content_length,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const errorBody = (await response.json().catch(() => ({}))) as {
|
|
877
|
+
detail?: string;
|
|
878
|
+
};
|
|
879
|
+
throw new Error(
|
|
880
|
+
errorBody.detail ??
|
|
881
|
+
`Failed to request signed URL: ${response.status} ${response.statusText}`,
|
|
892
882
|
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Poll the unified job-status endpoint on the platform. Calls
|
|
887
|
+
* `GET /v1/migrations/jobs/{jobId}/` and parses into {@link UnifiedJobStatus}.
|
|
888
|
+
*/
|
|
889
|
+
export async function platformPollJobStatus(
|
|
890
|
+
jobId: string,
|
|
891
|
+
token: string,
|
|
892
|
+
platformUrl?: string,
|
|
893
|
+
): Promise<UnifiedJobStatus> {
|
|
894
|
+
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
895
|
+
const response = await fetch(`${resolvedUrl}/v1/migrations/jobs/${jobId}/`, {
|
|
896
|
+
headers: await authHeaders(token, platformUrl),
|
|
897
|
+
});
|
|
893
898
|
|
|
894
899
|
if (response.status === 404) {
|
|
895
|
-
throw new Error("
|
|
900
|
+
throw new Error("Migration job not found");
|
|
896
901
|
}
|
|
897
902
|
|
|
898
903
|
if (!response.ok) {
|
|
899
904
|
throw new Error(
|
|
900
|
-
`
|
|
905
|
+
`Job status check failed: ${response.status} ${response.statusText}`,
|
|
901
906
|
);
|
|
902
907
|
}
|
|
903
908
|
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
job_id?: string;
|
|
907
|
-
result?: Record<string, unknown>;
|
|
908
|
-
error?: string;
|
|
909
|
-
};
|
|
910
|
-
return {
|
|
911
|
-
status: body.status,
|
|
912
|
-
result: body.result,
|
|
913
|
-
error: body.error,
|
|
914
|
-
};
|
|
909
|
+
const raw = (await response.json()) as RawUnifiedJobStatus;
|
|
910
|
+
return parseUnifiedJobStatus(raw);
|
|
915
911
|
}
|
|
@@ -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
|
@@ -2,7 +2,11 @@ import { spawn } from "child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, dirname, join } from "path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getBaseDir,
|
|
7
|
+
getDaemonPidPath,
|
|
8
|
+
loadAllAssistants,
|
|
9
|
+
} from "./assistant-config.js";
|
|
6
10
|
import type { AssistantEntry } from "./assistant-config.js";
|
|
7
11
|
import {
|
|
8
12
|
stopOrphanedDaemonProcesses,
|
|
@@ -41,7 +45,7 @@ export async function retireLocal(
|
|
|
41
45
|
return;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
const daemonPidFile = resources
|
|
48
|
+
const daemonPidFile = getDaemonPidPath(resources);
|
|
45
49
|
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon");
|
|
46
50
|
|
|
47
51
|
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -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) {
|