diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index a378a2c245..8a3e521f2b 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -361,14 +361,19 @@ jobs: with: node-version: 24 + # Pre-install lockfile structural audit. Has to fire BEFORE + # `npm ci` so any tarball's `prepare` / `postinstall` cannot run + # ahead of the scan. Pure-Python read-only; safe everywhere. + - name: Lockfile supply-chain audit (pre-install scan) + shell: bash + run: python3 scripts/lockfile_supply_chain_audit.py + - name: Install pinned Tauri CLI - # Lifecycle scripts (esbuild native-binary postinstall, etc.) are - # required for `vite build`. The pre-install lockfile structural - # audit (lockfile_supply_chain_audit.py) is the practical defence - # against the npm postinstall-dropper class -- it fires BEFORE any - # tarball runs, on the injection pattern itself rather than an - # advisory-DB lookup. - run: npm install --save-dev --prefix studio @tauri-apps/cli@2.10.1 --no-fund --no-audit + # `npm ci` resolves @tauri-apps/cli and its 11 platform-specific + # optional native binaries from studio/package-lock.json -- + # transitive versions are fully pinned, integrity hashes are + # verified, and the install is reproducible across re-runs. + run: npm ci --prefix studio --no-fund --no-audit - name: Verify pinned Tauri CLI shell: bash diff --git a/.gitignore b/.gitignore index 505fb9562b..a839633790 100644 --- a/.gitignore +++ b/.gitignore @@ -233,4 +233,5 @@ server.pid package-lock.json !studio/frontend/package-lock.json !studio/backend/core/data_recipe/oxc-validator/package-lock.json +!studio/package-lock.json llama.cpp/ diff --git a/build.sh b/build.sh index de69f13d7c..2d54d7a36c 100644 --- a/build.sh +++ b/build.sh @@ -33,23 +33,14 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -# Use bun if a bun.lock is committed (lockfile-strict, fast), else npm ci. -# bun install --frozen-lockfile cannot migrate from package-lock.json, so if -# only the npm lockfile is present we skip bun and go straight to npm ci. -_install_ok=false -if [ -f bun.lock ] && command -v bun &>/dev/null; then - if bun install --frozen-lockfile; then - _install_ok=true - else - echo "⚠ bun install --frozen-lockfile failed, falling back to npm ci" - rm -rf node_modules - fi -fi -if [ "$_install_ok" != "true" ]; then - if ! npm ci; then - echo "❌ ERROR: package install failed" >&2 - exit 1 - fi +# Frontend installs always use npm ci against the committed lockfile. +# There is no bun.lock anywhere in the repo, so a bun-first branch +# would always miss and silently regenerate (or fail under +# --frozen-lockfile). Keep this single path until/unless a real +# bun.lock lands. +if ! npm ci; then + echo "❌ ERROR: npm ci failed" >&2 + exit 1 fi npm run build # outputs to studio/frontend/dist/ diff --git a/scripts/lockfile_supply_chain_audit.py b/scripts/lockfile_supply_chain_audit.py index 478be4a27e..effab17fc5 100644 --- a/scripts/lockfile_supply_chain_audit.py +++ b/scripts/lockfile_supply_chain_audit.py @@ -655,6 +655,7 @@ def audit_cargo_lockfile(path: Path) -> list[Finding]: DEFAULT_NPM_LOCKFILES = ( "studio/frontend/package-lock.json", "studio/backend/core/data_recipe/oxc-validator/package-lock.json", + "studio/package-lock.json", ) DEFAULT_CARGO_LOCKFILES = ("studio/src-tauri/Cargo.lock",) @@ -674,8 +675,9 @@ def main(argv: list[str] | None = None) -> int: default = None, help = ( "Path to a package-lock.json (repeatable). " - "Default: studio/frontend/package-lock.json plus " - "studio/backend/core/data_recipe/oxc-validator/package-lock.json." + "Default: studio/frontend/package-lock.json, " + "studio/backend/core/data_recipe/oxc-validator/package-lock.json, " + "and studio/package-lock.json (Tauri CLI for desktop release)." ), ) parser.add_argument( diff --git a/studio/package-lock.json b/studio/package-lock.json new file mode 100644 index 0000000000..b5ed4ea2f3 --- /dev/null +++ b/studio/package-lock.json @@ -0,0 +1,233 @@ +{ + "name": "unsloth-studio-tauri-cli", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unsloth-studio-tauri-cli", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "devDependencies": { + "@tauri-apps/cli": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/studio/package.json b/studio/package.json new file mode 100644 index 0000000000..ac4789e874 --- /dev/null +++ b/studio/package.json @@ -0,0 +1,10 @@ +{ + "name": "unsloth-studio-tauri-cli", + "version": "0.0.0", + "private": true, + "description": "Lockfile holder for @tauri-apps/cli used by the desktop release workflow. Not a real npm package; `npm ci --prefix studio` resolves the pinned Tauri CLI from this directory's package-lock.json.", + "license": "AGPL-3.0-only", + "devDependencies": { + "@tauri-apps/cli": "2.10.1" + } +} diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 5aefcea216..1d5eb2814f 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1146,24 +1146,6 @@ if ($IsPipInstall) { } step "node" "$(node -v) | npm $(npm -v)" - - # ── bun (optional, faster package installs) ── - # Installed via npm — Node is already guaranteed above. Works on all platforms. - if (-not (Get-Command bun -ErrorAction SilentlyContinue)) { - substep "installing bun (faster frontend package installs)..." - $prevEAP_bun = $ErrorActionPreference - $ErrorActionPreference = "Continue" - Invoke-SetupCommand { npm install -g bun } | Out-Null - $ErrorActionPreference = $prevEAP_bun - Refresh-Environment - if (Get-Command bun -ErrorAction SilentlyContinue) { - substep "bun installed ($(bun --version))" - } else { - substep "bun install skipped (npm will be used instead)" - } - } else { - substep "bun already installed ($(bun --version))" - } } # 1g. Python (>= 3.11 and < 3.14). Prefer py.exe so a 3.14 ahead of 3.13 on PATH does not trip the gate. @@ -1286,7 +1268,7 @@ if ($IsPipInstall) { # Also check all top-level files (package.json, vite.config.ts, index.html, etc.) if (-not $NewerFile) { $NewerFile = Get-ChildItem -Path $FrontendDir -File -ErrorAction SilentlyContinue | - Where-Object { $_.Name -ne "bun.lock" -and $_.LastWriteTime -gt $DistTime } | + Where-Object { $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 } if (-not $NewerFile) { @@ -1321,69 +1303,25 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $WalkDir = Split-Path $WalkDir -Parent } - # Use bun if available (faster install), fall back to npm. - # Bun is used only as package manager; Node runs the actual build (Vite 8). + # npm ci: install exactly what package-lock.json pins, fail on drift. + # There is no committed bun.lock so we don't dispatch to bun; a bun + # branch here would always miss and silently regenerate (or fail + # under --frozen-lockfile). Keep this single path until/unless a + # real bun.lock lands. $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir - # Only use bun when a committed bun.lock is present. bun install - # --frozen-lockfile cannot migrate from package-lock.json, so without - # bun.lock the bun path would always fail. - $UseBun = ($null -ne (Get-Command bun -ErrorAction SilentlyContinue)) -and (Test-Path "bun.lock") - - # bun's package cache can become corrupt -- packages get stored with only - # metadata but no actual content (bin/, lib/). When this happens bun install - # exits 0 but leaves binaries missing. We validate after install and clear - # the cache + retry once before falling back to npm. - if ($UseBun) { - Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile } - # On Windows, .bin/ entries vary by package manager: - # npm → tsc, tsc.cmd, tsc.ps1 - # bun → tsc.exe, tsc.bunx - $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx") - $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx") - if ($bunExit -eq 0 -and $hasTsc -and $hasVite) { - # bun install succeeded and critical binaries are present - } elseif ($bunExit -eq 0) { - Write-Host " bun install exited 0 but critical binaries are missing, clearing cache and retrying..." -ForegroundColor Yellow - if (Test-Path "node_modules") { - Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue - } - Invoke-SetupCommand { bun pm cache rm } | Out-Null - $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile } - $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx") - $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx") - if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { - Write-Host " bun retry failed, falling back to npm" -ForegroundColor Yellow - if (Test-Path "node_modules") { - Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue - } - $UseBun = $false - } - } else { - substep "bun install failed (exit $bunExit), falling back to npm" "Yellow" - if (Test-Path "node_modules") { - Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue - } - $UseBun = $false - } - } - if (-not $UseBun) { - # npm ci: install exactly what the lockfile pins, fail on drift. - $npmExit = Invoke-SetupCommand { npm ci } - if ($npmExit -ne 0) { - Pop-Location - $ErrorActionPreference = $prevEAP_npm - foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } - Write-Host "[ERROR] npm ci failed (exit code $npmExit)" -ForegroundColor Red - Write-Host " Try running 'npm ci' manually in frontend/ to see errors" -ForegroundColor Yellow - exit 1 - } + $npmExit = Invoke-SetupCommand { npm ci } + if ($npmExit -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } + Write-Host "[ERROR] npm ci failed (exit code $npmExit)" -ForegroundColor Red + Write-Host " Try running 'npm ci' manually in frontend/ to see errors" -ForegroundColor Yellow + exit 1 } - # Always use npm to run the build (Node runtime — avoids bun Windows runtime issues) $buildExit = Invoke-SetupCommand { npm run build } if ($buildExit -ne 0) { Pop-Location diff --git a/studio/setup.sh b/studio/setup.sh index f4b5bea076..a5a0fbebf9 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -211,7 +211,6 @@ else _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \ - ! -name 'bun.lock' \ -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) if [ -z "$_changed" ]; then _changed=$(find "$SCRIPT_DIR/frontend/src" "$SCRIPT_DIR/frontend/public" \ @@ -295,20 +294,6 @@ fi step "node" "$(node -v) | npm $(npm -v)" verbose_substep "node check: NEED_NODE=$NEED_NODE NODE_OK=${NODE_OK:-unknown} NPM_MAJOR=${NPM_MAJOR:-unknown}" -# ── Install bun (optional, faster package installs) ── -# Uses npm to install bun globally -- Node is already guaranteed above, -# avoids platform-specific installers, PATH issues, and admin requirements. -if ! command -v bun &>/dev/null; then - substep "installing bun..." - if run_maybe_quiet npm install -g bun && command -v bun &>/dev/null; then - substep "bun installed ($(bun --version))" - else - substep "bun install skipped (npm will be used instead)" - fi -else - substep "bun already installed ($(bun --version))" -fi - # ── Build frontend ── substep "building frontend..." cd "$SCRIPT_DIR/frontend" @@ -329,69 +314,15 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -# Use bun for install if available (faster), fall back to npm. -# Build always uses npm (Node runtime -- avoids bun runtime issues on some platforms). -# NOTE: We intentionally avoid run_quiet for the bun install attempt because -# run_quiet calls exit on failure, which would kill the script before the npm -# fallback can run. Instead we capture output manually and only show it on failure. -# -# IMPORTANT: bun's package cache can become corrupt -- packages get stored -# with only metadata (package.json, README) but no actual content (bin/, -# lib/). When this happens bun install exits 0 but leaves binaries missing. -# We verify critical binaries after install. If missing, we clear the cache -# and retry once before falling back to npm. -_try_bun_install() { - local _log _exit_code=0 - _log=$(mktemp) - # --frozen-lockfile so a fresh caret-range patch can't land via npm registry. - bun install --frozen-lockfile >"$_log" 2>&1 || _exit_code=$? - - # bun may create .exe shims on Windows (Git Bash / MSYS2) instead of plain scripts - if [ "$_exit_code" -eq 0 ] \ - && { [ -x node_modules/.bin/tsc ] || [ -f node_modules/.bin/tsc.exe ] || [ -f node_modules/.bin/tsc.bunx ]; } \ - && { [ -x node_modules/.bin/vite ] || [ -f node_modules/.bin/vite.exe ] || [ -f node_modules/.bin/vite.bunx ]; }; then - rm -f "$_log" - return 0 - fi - - # Either bun install failed or it exited 0 but left packages missing - if [ "$_exit_code" -ne 0 ]; then - echo " bun install failed (exit code $_exit_code):" - else - echo " bun install exited 0 but critical binaries are missing:" - fi - sed 's/^/ | /' "$_log" >&2 - rm -f "$_log" - rm -rf node_modules - return 1 -} - -_bun_install_ok=false -# bun install --frozen-lockfile cannot migrate from package-lock.json, so we -# only enter the bun path when a committed bun.lock exists. Without it, -# bun would fail every time and the corrupt-cache retry would clear the -# user's bun cache for nothing. -if [ -f bun.lock ] && command -v bun &>/dev/null; then - substep "using bun for package install (faster)" - if _try_bun_install; then - _bun_install_ok=true - else - # First attempt failed, likely due to corrupt cache entries. - # Clear the cache and retry once. - echo " Clearing bun cache and retrying..." - run_maybe_quiet bun pm cache rm || true - if _try_bun_install; then - _bun_install_ok=true - fi - fi -fi -if [ "$_bun_install_ok" = false ]; then - # npm ci: install exactly what the lockfile pins, fail on drift. - run_quiet_no_exit "npm ci" npm ci --no-fund --no-audit --loglevel=error - _npm_install_rc=$? - if [ "$_npm_install_rc" -ne 0 ]; then - exit "$_npm_install_rc" - fi +# npm ci: install exactly what package-lock.json pins, fail on drift. +# There is no committed bun.lock so we don't dispatch to bun; a bun +# branch here would always miss and silently regenerate (or fail under +# --frozen-lockfile). Keep this single path until/unless a real +# bun.lock lands. +run_quiet_no_exit "npm ci" npm ci --no-fund --no-audit --loglevel=error +_npm_install_rc=$? +if [ "$_npm_install_rc" -ne 0 ]; then + exit "$_npm_install_rc" fi run_quiet "npm run build" npm run build