# Agent Note: Offline pnpm prefetch scripts Status: implemented English | [中文](2026-08-19-offline-pnpm-prefetch-scripts.zh.md) ## Problem A source checkout cannot boot `dsh web` until a supported Node runtime, `pnpm install`, and `pnpm run build` have materialized `node_modules`, package `lib/` output, and `apps/web/dist`. Install reaches the npm registry (and Corepack reaches it for `pnpm@11.7.0`); the Node distro comes from `nodejs.org/dist`. Node's fetch client cannot use `socks5h://` proxies, so a machine whose only ambient proxy is SOCKS fails `pnpm install` even while curl through the same proxy succeeds. An air-gapped follow-up install therefore needs a same-OS prefetch of the official Node archive, the lockfile store, and a Corepack home that does not call the registry. ## Decision [`scripts/offline/download-deps`](../../../../scripts/offline/download-deps.ps1) (POSIX twin [`.sh`](../../../../scripts/offline/download-deps.sh)) runs while `nodejs.org` and the registry are reachable. It downloads the official Node zip/tar for the current OS and CPU from `DSH_NODE_DIST_BASE` (default `https://nodejs.org/dist`), verifies `SHASUMS256.txt`, unpacks into ignored [`.offline-cache/node/runtime`](../../../../.gitignore), and writes `.offline-cache/node/runtime.json`. The Node version is `DSH_NODE_VERSION` when set, otherwise the running Node when it satisfies `engines.node` (`^22.19.0 || >=24.0.0`), otherwise the newest `v24.x` from `index.json` that publishes this platform's archive. It then sets `COREPACK_HOME` to `.offline-cache/corepack`, `corepack prepare`s the root `package.json` `packageManager` pin using that Node, and `pnpm fetch --frozen-lockfile --store-dir .offline-store`. [`scripts/offline/install-deps`](../../../../scripts/offline/install-deps.ps1) (POSIX twin [`.sh`](../../../../scripts/offline/install-deps.sh)) unpacks the cached Node if needed, prepends it to `PATH`, sets `COREPACK_ENABLE_NETWORK=0`, and runs `pnpm install --offline --frozen-lockfile` against that store. Optional `-Build` / `--build` runs `npm run build:lib` then `pnpm --dir apps/web run build`, avoiding the nested `pnpm --filter` invocation inside `npm run build:web` that can pick a different Corepack pnpm than `packageManager`. `DSH_NPM_HTTP_PROXY`, when set, is an HTTP overlay applied only around `corepack prepare` and `pnpm fetch`, then the process proxy variables are restored so a caller SOCKS session is unchanged. curl uses the inherited `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` (SOCKS included) and falls back to `DSH_NPM_HTTP_PROXY` only when none of those are set. A `socks5` or `socks5h` value for Node/pnpm (from `DSH_NPM_HTTP_PROXY`, or from the inherited proxies when no HTTP overlay is set) fails before Corepack runs, because that scheme is not a supported pnpm transport. The scripts prefetch Node and the lockfile for the host OS and CPU they run on. They do not download LLM weights, Playwright browsers, or a local inference server. Copy the checkout including `.offline-store` and `.offline-cache` to the offline machine. ## Alternatives considered **Tell contributors to copy a finished `node_modules` tree and skip a second install.** A copied isolated pnpm layout is host-specific and breaks when the store path or package content-address links do not match; `pnpm install --offline` against a fetched store is the documented pnpm air-gap path. **Check `supportedArchitectures` into the download script so one store serves win32 and linux.** Rejected for the default: optional native packages (esbuild, koffi, node-pty) must match the install host, and a mixed store still needs a same-OS install to unpack the right binaries. Cross-OS prefetch remains an explicit local `pnpm fetch` config, not the script default. **Add root `package.json` scripts that call these files.** Nested `pnpm` under `npm run` is what already mismatches Corepack versions on `build:web`; the wrappers stay invoked as files so they control `COREPACK_HOME` and `--store-dir`. **Overwrite the caller's `HTTP_PROXY` for the whole download script.** Rejected: `.\download-deps.ps1` runs in the current PowerShell session, so a session-wide overwrite would replace a working SOCKS proxy for later commands. curl can use SOCKS; only Node/pnpm need the HTTP overlay, and only for those child processes. **Ship the Node MSI or pkg installer.** Rejected: those installers need administrator rights and a global install; the official zip/tar is relocatable, and install only unpacks it and prepends `PATH`. **Hardcode one Node version in the script.** Rejected: a pinned patch goes stale against `engines.node` and CI (`22.19`, `24`, `26`). Matching the running Node keeps optional native packages aligned with the prefetch host; `index.json` is the fallback when Node is not on `PATH`. **Vendor the pnpm store in git.** The store is hundreds of megabytes of registry tarballs; `.gitignore` keeps `.offline-store/` and `.offline-cache/` untracked. ## Consequences Air-gap preparation is a two-command, same-OS pair and does not change the online `pnpm install` path. The offline machine does not need a preinstalled Node: install unpacks the cached official distro. `dsh plugin … add` of a registry or git spec still needs a network on the machine that runs it. Local chat still needs a separately installed OpenAI-compatible server; these scripts do not substitute for that.