From 41cfab5762223a82e3be3a5b33c96e3cc4607fc4 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Thu, 30 Jul 2026 13:36:17 -0700 Subject: [PATCH] Resolve latest cli-preview release for website CLI download links (#924) The website hardcoded the CLI download links to the cli-preview-0.10.0 release tag in the download dropdown and the models page, so users were sent to an outdated CLI once newer preview releases shipped. Resolve the newest cli-preview-* release at build time via a prerendered server layout load, and point the per-platform CLI buttons at that release's page - where users choose their architecture/variant, matching the arch-safe winget/brew install path in platform.ts. A single static link cannot be architecture-correct (e.g. an arm64 user must not be handed an x64 build), so linking to the release page fixes the staleness without that regression. Discovery degrades gracefully to the general releases page on any failure, and an optional build-time GITHUB_TOKEN raises the GitHub API rate limit. Adds vitest (the site had no test runner) with coverage for the resolver: selection, filtering, null-date determinism, token header, and every fallback path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- www/package-lock.json | 406 +++++++++++++++++- www/package.json | 6 +- www/src/app.d.ts | 4 +- www/src/lib/cli-release.test.ts | 101 +++++ www/src/lib/cli-release.ts | 73 ++++ .../lib/components/download-dropdown.svelte | 22 +- www/src/routes/+layout.server.ts | 13 + www/src/routes/models/+page.svelte | 25 +- www/vitest.config.ts | 10 + 9 files changed, 611 insertions(+), 49 deletions(-) create mode 100644 www/src/lib/cli-release.test.ts create mode 100644 www/src/lib/cli-release.ts create mode 100644 www/src/routes/+layout.server.ts create mode 100644 www/vitest.config.ts diff --git a/www/package-lock.json b/www/package-lock.json index 3cab52375..2f4cbda1a 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -35,7 +35,8 @@ "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.10" }, "engines": { "node": ">=22.0.0 <23.0.0", @@ -59,6 +60,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -70,6 +72,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -80,6 +83,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -213,6 +217,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -251,6 +256,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -267,6 +273,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -283,6 +290,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -299,6 +307,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -315,6 +324,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -331,6 +341,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -347,6 +358,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -363,6 +375,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -379,6 +392,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -395,6 +409,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -411,6 +426,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -427,6 +443,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -443,6 +460,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -461,6 +479,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -477,6 +496,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -517,9 +537,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha1-p5tV26+GBIEvUtFAssmrQbwVC7g=", "devOptional": true, "license": "MIT" }, @@ -1602,9 +1622,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1622,9 +1639,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1642,9 +1656,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1662,9 +1673,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1815,12 +1823,24 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1828,6 +1848,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1911,6 +1938,129 @@ "node": ">=20" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha1-eZwG/ES7DPfieEE3tifFzBcyhdQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha1-JBOYerTNf6HCthS0BMQHv2rR6tE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha1-dVQucnOgjMEP1Nja1OPrHxbNlYw=", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha1-/r8KIakWhCFCLRlVNw5gb+q2A1U=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha1-fj6f7H1NRyMuSTz9y9IXDeQ3HAQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha1-XAv6l7Vrup43QDyXbbd2/2q1b2U=", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha1-/8cQVfGL/Msf0FhjZevCgkiS5AM=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -1964,6 +2114,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-sema": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", @@ -2144,6 +2304,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-6.2.2.tgz", + "integrity": "sha1-rkG1LJrKh3NFBTYnF/MlX6zaNg4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2239,6 +2409,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha1-S1YPZJ/E6RjdCrdc9JYei8iC2Co=", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2338,6 +2515,13 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha1-W/LfBpmdu+XwBqX0ahH7n1t7ORs=", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2380,6 +2564,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2433,6 +2627,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2503,7 +2698,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -2556,6 +2751,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2576,6 +2772,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2596,6 +2793,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2616,6 +2814,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2636,6 +2835,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2656,6 +2856,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2676,6 +2877,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2696,6 +2898,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2716,6 +2919,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2736,6 +2940,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2756,6 +2961,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3055,6 +3261,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3366,6 +3579,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha1-MudscLeXJOO7Vny51UPrhYzPrzA=", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3391,6 +3611,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha1-jr4OxgSFZoq0ciezEvQlTN+AydM=", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-object": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", @@ -3590,6 +3824,23 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha1-EDyfi6bXI3pHq23R3P93JRhjQms=", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha1-rkW7Lt69qUxw9OqJfg8SQ+Rw23E=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -3607,6 +3858,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha1-HYpiOJP5XPCi3bnl0RFQ4ZFAlCE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3628,14 +3889,14 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3781,6 +4042,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha1-fpKF7+JksRZwULejp/80eI4bevw=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3799,6 +4150,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha1-o/aalxB/SUs83Dvd3Yg6fWXOvwQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/www/package.json b/www/package.json index 953e8eeab..3f6886a16 100644 --- a/www/package.json +++ b/www/package.json @@ -22,7 +22,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", - "lint": "prettier --check ." + "lint": "prettier --check .", + "test": "vitest run" }, "devDependencies": { "@sveltejs/adapter-vercel": "^6.3.3", @@ -48,7 +49,8 @@ "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.10" }, "dependencies": { "@vercel/analytics": "^2.0.1" diff --git a/www/src/app.d.ts b/www/src/app.d.ts index 9d100de4a..74e1e6f17 100644 --- a/www/src/app.d.ts +++ b/www/src/app.d.ts @@ -4,7 +4,9 @@ declare global { namespace App { // interface Error {} // interface Locals {} - // interface PageData {} + interface PageData { + cliLinks: import('$lib/cli-release').CliLinks; + } // interface PageState {} interface Platform { env: { diff --git a/www/src/lib/cli-release.test.ts b/www/src/lib/cli-release.test.ts new file mode 100644 index 000000000..1ce9569ef --- /dev/null +++ b/www/src/lib/cli-release.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { resolveCliLinks } from './cli-release'; + +const RELEASES_PAGE = 'https://github.com/microsoft/Foundry-Local/releases'; + +function makeRelease(tag: string, publishedAt: string | null, opts: { draft?: boolean } = {}) { + return { + tag_name: tag, + draft: opts.draft ?? false, + prerelease: true, + published_at: publishedAt, + html_url: `https://github.com/microsoft/Foundry-Local/releases/tag/${tag}` + }; +} + +// Minimal fetch stubs. Types are loosened because resolveCliLinks only reads `ok` and `json()`. +const okFetch = (body: unknown) => + (async () => ({ ok: true, json: async () => body })) as unknown as typeof fetch; +const statusFetch = (ok: boolean) => + (async () => ({ ok, json: async () => [] })) as unknown as typeof fetch; +const throwingFetch = () => + (async () => { + throw new Error('network down'); + }) as unknown as typeof fetch; + +describe('resolveCliLinks', () => { + it('resolves the newest cli-preview release by published date, not array order', async () => { + const releases = [ + makeRelease('cli-preview-0.10.0', '2026-06-04T20:35:47Z'), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z'), + makeRelease('cli-preview-0.10.1', '2026-06-22T23:45:41Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + expect(links.version).toBe('0.10.2'); + expect(links.releasePage).toBe( + 'https://github.com/microsoft/Foundry-Local/releases/tag/cli-preview-0.10.2' + ); + }); + + it('ignores non cli-preview tags and draft releases', async () => { + const releases = [ + makeRelease('v-unrelated', '2027-01-01T00:00:00Z'), + makeRelease('cli-preview-0.11.0', '2026-08-01T00:00:00Z', { draft: true }), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + }); + + it('treats a null published_at as oldest, deterministically, regardless of array order', async () => { + const releases = [ + makeRelease('cli-preview-0.10.3', null), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + }); + + it('sends a bearer Authorization header only when a token is provided', async () => { + const seen: Array | undefined> = []; + const spyFetch = ((_url: string, init?: { headers?: Record }) => { + seen.push(init?.headers); + return Promise.resolve({ + ok: true, + json: async () => [makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z')] + }); + }) as unknown as typeof fetch; + + await resolveCliLinks(spyFetch, 'secret-token'); + await resolveCliLinks(spyFetch); + + expect(seen[0]?.Authorization).toBe('Bearer secret-token'); + expect(seen[1]?.Authorization).toBeUndefined(); + }); + + it('falls back to the releases page when the API responds non-OK', async () => { + const links = await resolveCliLinks(statusFetch(false)); + expect(links.tag).toBeNull(); + expect(links.version).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back to the releases page when no cli-preview release exists', async () => { + const links = await resolveCliLinks(okFetch([makeRelease('v1.0.0', '2027-01-01T00:00:00Z')])); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back to the releases page when fetch throws', async () => { + const links = await resolveCliLinks(throwingFetch()); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back when the API returns a non-array error object (e.g. rate limited)', async () => { + const links = await resolveCliLinks(okFetch({ message: 'API rate limit exceeded' })); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); +}); diff --git a/www/src/lib/cli-release.ts b/www/src/lib/cli-release.ts new file mode 100644 index 000000000..0afd938e0 --- /dev/null +++ b/www/src/lib/cli-release.ts @@ -0,0 +1,73 @@ +// Resolves the latest `cli-preview-*` GitHub release at build time so the site never hardcodes a +// specific preview tag (issue #924). Returns the release's page URL; the platform download buttons +// link there and let the user pick their architecture/variant (mirroring the arch-safe `winget`/ +// `brew` install path in platform.ts). If release discovery fails, it falls back to the general +// releases page. + +const OWNER_REPO = 'microsoft/Foundry-Local'; +const RELEASES_PAGE = `https://github.com/${OWNER_REPO}/releases`; +const RELEASES_API = `https://api.github.com/repos/${OWNER_REPO}/releases?per_page=30`; +const CLI_TAG_PREFIX = 'cli-preview-'; + +export interface CliLinks { + /** Release tag (e.g. "cli-preview-0.10.2"), or null when discovery failed. */ + tag: string | null; + /** Version portion of the tag (e.g. "0.10.2"), or null when discovery failed. */ + version: string | null; + /** The resolved release's page, or the general releases page on fallback. */ + releasePage: string; +} + +interface GitHubRelease { + tag_name: string; + draft: boolean; + published_at: string | null; + html_url: string; +} + +function fallbackLinks(): CliLinks { + return { tag: null, version: null, releasePage: RELEASES_PAGE }; +} + +/** + * Fetch the repo's releases and resolve the newest non-draft `cli-preview-*` release. + * Pass the SvelteKit-provided `fetch` from a server `load` so this runs at build/prerender time. + * An optional `token` (from the build environment) is sent as a bearer credential to raise the + * GitHub API rate limit; it is never required and never exposed to the client. + */ +export async function resolveCliLinks( + fetchFn: typeof fetch = fetch, + token?: string +): Promise { + try { + const headers: Record = { Accept: 'application/vnd.github+json' }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const response = await fetchFn(RELEASES_API, { headers }); + if (!response.ok) { + return fallbackLinks(); + } + + const releases = (await response.json()) as GitHubRelease[]; + const latest = releases + .filter((release) => release.tag_name?.startsWith(CLI_TAG_PREFIX) && !release.draft) + // Coerce invalid/missing published_at to 0 so a null date sorts oldest deterministically. + .sort( + (a, b) => (Date.parse(b.published_at ?? '') || 0) - (Date.parse(a.published_at ?? '') || 0) + )[0]; + + if (!latest) { + return fallbackLinks(); + } + + return { + tag: latest.tag_name, + version: latest.tag_name.slice(CLI_TAG_PREFIX.length) || null, + releasePage: latest.html_url ?? RELEASES_PAGE + }; + } catch { + return fallbackLinks(); + } +} diff --git a/www/src/lib/components/download-dropdown.svelte b/www/src/lib/components/download-dropdown.svelte index ce7a37ef9..591549643 100644 --- a/www/src/lib/components/download-dropdown.svelte +++ b/www/src/lib/components/download-dropdown.svelte @@ -4,9 +4,7 @@ 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'; interface Props { variant?: 'default' | 'ghost' | 'outline'; @@ -24,6 +22,10 @@ const isMobile = new IsMobile(); + // CLI download links resolve to the latest cli-preview-* release at build time (issue #924), + // falling back to the releases page when discovery fails. Data comes from the root layout load. + const cliLinks = $derived($page.data.cliLinks); + // Platform SVG icons const AppleIcon = ``; @@ -54,26 +56,26 @@ crossPlatformCommand: string; }; - const cliInstallOptions: CliInstallOption[] = [ + const cliInstallOptions: CliInstallOption[] = $derived([ { id: 'windows', label: 'Windows CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: WindowsIcon }, { id: 'macos', label: 'macOS CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: AppleIcon }, { id: 'linux', label: 'Linux CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: LinuxIcon } - ]; + ]); const sdkInstallOptions: SdkInstallOption[] = [ { @@ -195,7 +197,9 @@
{item.label} - cli-preview-0.10.0 on GitHub + {cliLinks.tag ? `${cliLinks.tag} on GitHub` : 'Download from GitHub'}