diff --git a/www/src/lib/cli-downloads.ts b/www/src/lib/cli-downloads.ts new file mode 100644 index 000000000..e3289d646 --- /dev/null +++ b/www/src/lib/cli-downloads.ts @@ -0,0 +1,29 @@ +export const FOUNDRY_LOCAL_RELEASES_URL = 'https://github.com/microsoft/Foundry-Local/releases'; + +export type CliDownloadLink = { + id: 'windows-cli' | 'macos-cli' | 'linux-cli'; + label: string; + href: string; + releaseLabel: string; +}; + +export const fallbackCliDownloadLinks: CliDownloadLink[] = [ + { + id: 'windows-cli', + label: 'Windows', + href: FOUNDRY_LOCAL_RELEASES_URL, + releaseLabel: 'GitHub releases' + }, + { + id: 'macos-cli', + label: 'macOS', + href: FOUNDRY_LOCAL_RELEASES_URL, + releaseLabel: 'GitHub releases' + }, + { + id: 'linux-cli', + label: 'Linux', + href: FOUNDRY_LOCAL_RELEASES_URL, + releaseLabel: 'GitHub releases' + } +]; \ No newline at end of file diff --git a/www/src/lib/components/download-dropdown.svelte b/www/src/lib/components/download-dropdown.svelte index ce7a37ef9..fe21ca29d 100644 --- a/www/src/lib/components/download-dropdown.svelte +++ b/www/src/lib/components/download-dropdown.svelte @@ -4,9 +4,8 @@ import { Download, Copy, Check, ExternalLink } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { IsMobile } from '$lib/hooks/is-mobile.svelte'; - - const CLI_RELEASE_URL = - 'https://github.com/microsoft/Foundry-Local/releases/tag/cli-preview-0.10.0'; + import { page } from '$app/stores'; + import { fallbackCliDownloadLinks, type CliDownloadLink } from '$lib/cli-downloads'; interface Props { variant?: 'default' | 'ghost' | 'outline'; @@ -43,6 +42,7 @@ id: string; label: string; href: string; + releaseLabel: string; icon: string; }; @@ -54,26 +54,20 @@ crossPlatformCommand: string; }; - const cliInstallOptions: CliInstallOption[] = [ - { - id: 'windows', - label: 'Windows CLI', - href: CLI_RELEASE_URL, - icon: WindowsIcon - }, - { - id: 'macos', - label: 'macOS CLI', - href: CLI_RELEASE_URL, - icon: AppleIcon - }, - { - id: 'linux', - label: 'Linux CLI', - href: CLI_RELEASE_URL, - icon: LinuxIcon - } - ]; + const cliIcons = { + 'windows-cli': WindowsIcon, + 'macos-cli': AppleIcon, + 'linux-cli': LinuxIcon + }; + + function getCliInstallOptions(): CliInstallOption[] { + const links = ($page.data.cliDownloadLinks as CliDownloadLink[] | undefined) ?? fallbackCliDownloadLinks; + return links.map((link) => ({ + ...link, + label: `${link.label} CLI`, + icon: cliIcons[link.id] + })); + } const sdkInstallOptions: SdkInstallOption[] = [ { @@ -181,7 +175,7 @@ - {#each cliInstallOptions as item} + {#each getCliInstallOptions() as item} {#snippet child({ props })} diff --git a/www/src/lib/server/cli-release.ts b/www/src/lib/server/cli-release.ts new file mode 100644 index 000000000..c463dbdf2 --- /dev/null +++ b/www/src/lib/server/cli-release.ts @@ -0,0 +1,86 @@ +import { FOUNDRY_LOCAL_RELEASES_URL, fallbackCliDownloadLinks, type CliDownloadLink } from '$lib/cli-downloads'; + +type GitHubReleaseAsset = { + name: string; + browser_download_url: string; +}; + +type GitHubRelease = { + tag_name: string; + html_url: string; + assets?: GitHubReleaseAsset[]; +}; + +type FetchLike = typeof fetch; + +const RELEASES_API_URL = 'https://api.github.com/repos/microsoft/Foundry-Local/releases'; +const CLI_PREVIEW_TAG = /^cli-preview-(\d+)\.(\d+)\.(\d+)$/i; + +export async function getCliDownloadLinks(fetcher: FetchLike): Promise { + try { + const response = await fetcher(RELEASES_API_URL, { + headers: { Accept: 'application/vnd.github+json' } + }); + + if (!response.ok) { + return fallbackCliDownloadLinks; + } + + const releases = (await response.json()) as GitHubRelease[]; + const latestCliRelease = releases + .filter((release) => CLI_PREVIEW_TAG.test(release.tag_name)) + .sort((left, right) => compareCliPreviewTags(right.tag_name, left.tag_name))[0]; + + if (!latestCliRelease?.assets?.length) { + return fallbackCliDownloadLinks; + } + + return buildCliDownloadLinks(latestCliRelease); + } catch { + return fallbackCliDownloadLinks; + } +} + +function compareCliPreviewTags(left: string, right: string): number { + const leftVersion = parseCliPreviewTag(left); + const rightVersion = parseCliPreviewTag(right); + + for (let i = 0; i < leftVersion.length; i++) { + const delta = leftVersion[i] - rightVersion[i]; + if (delta !== 0) return delta; + } + + return 0; +} + +function parseCliPreviewTag(tag: string): [number, number, number] { + const match = CLI_PREVIEW_TAG.exec(tag); + if (!match) return [0, 0, 0]; + return [Number(match[1]), Number(match[2]), Number(match[3])]; +} + +function buildCliDownloadLinks(release: GitHubRelease): CliDownloadLink[] { + const fallbackById = new Map(fallbackCliDownloadLinks.map((link) => [link.id, link])); + const releaseLabel = release.tag_name; + + return [ + buildLink(fallbackById.get('windows-cli')!, release, releaseLabel, /win-x64-winml\.msix$/i), + buildLink(fallbackById.get('macos-cli')!, release, releaseLabel, /osx-arm64\.pkg$/i), + buildLink(fallbackById.get('linux-cli')!, release, releaseLabel, /linux-x64\.tar\.gz$/i) + ]; +} + +function buildLink( + fallback: CliDownloadLink, + release: GitHubRelease, + releaseLabel: string, + assetPattern: RegExp +): CliDownloadLink { + const asset = release.assets?.find((candidate) => assetPattern.test(candidate.name)); + + return { + ...fallback, + href: asset?.browser_download_url ?? release.html_url ?? FOUNDRY_LOCAL_RELEASES_URL, + releaseLabel: asset ? releaseLabel : 'GitHub releases' + }; +} \ No newline at end of file diff --git a/www/src/routes/+layout.server.ts b/www/src/routes/+layout.server.ts new file mode 100644 index 000000000..87db85234 --- /dev/null +++ b/www/src/routes/+layout.server.ts @@ -0,0 +1,7 @@ +import { getCliDownloadLinks } from '$lib/server/cli-release'; + +export async function load({ fetch }) { + return { + cliDownloadLinks: await getCliDownloadLinks(fetch) + }; +} \ No newline at end of file diff --git a/www/src/routes/models/+page.svelte b/www/src/routes/models/+page.svelte index a515081a7..04e2240d8 100644 --- a/www/src/routes/models/+page.svelte +++ b/www/src/routes/models/+page.svelte @@ -14,30 +14,15 @@ import { ModelFilters, ModelGrid, ModelDetailsModal } from './components'; import { Terminal, Copy, Check, ExternalLink } from 'lucide-svelte'; import { detectModelFamily } from '$lib/utils/model-helpers'; + import { fallbackCliDownloadLinks, type CliDownloadLink } from '$lib/cli-downloads'; // Known device names used as shorthand URL params (e.g. /models?cpu) const KNOWN_DEVICES = ['cpu', 'gpu', 'npu']; const MODEL_QUERY_PARAM = 'model'; const CLI_RUN_COMMAND = 'foundry run qwen2.5-0.5b'; - const CLI_RELEASE_URL = - 'https://github.com/microsoft/Foundry-Local/releases/tag/cli-preview-0.10.0'; - const CLI_INSTALL_LINKS = [ - { - id: 'windows-cli', - label: 'Windows', - href: CLI_RELEASE_URL - }, - { - id: 'macos-cli', - label: 'macOS', - href: CLI_RELEASE_URL - }, - { - id: 'linux-cli', - label: 'Linux', - href: CLI_RELEASE_URL - } - ]; + function getCliInstallLinks(): CliDownloadLink[] { + return ($page.data.cliDownloadLinks as CliDownloadLink[] | undefined) ?? fallbackCliDownloadLinks; + } // Debounce timer for search let searchDebounceTimer: ReturnType | null = null; @@ -535,7 +520,7 @@
- {#each CLI_INSTALL_LINKS as item} + {#each getCliInstallLinks() as item}