ADR 0003: Web UI distribution to npm consumers
Date: 2026-06-21 Status: Resolved (lazy-fetch on first use; see Resolution below)
Context
Section titled “Context”The web command (harn web up, the Next.js coordination dashboard) ships in
the published npm package (dist/commands/web.js), but the web/ app itself is
deliberately excluded from the tarball (ADR 0001: “web/ unpublished”; the
package.json#files allowlist omits it). harn web up resolves the app at
harnery/web/ relative to its own location, which does not exist in an npm
install, so it fails with web_missing: harnery/web/ not found.
Result: harn web up is shipped but cannot work for any npm consumer. This
is the same class of latent bug as the 0.2.0 startup crashes (a published
command that is guaranteed to fail for a real install). It was surfaced by
harnery-demo, a consumer repo built to dogfood + integration-test the package.
Why this is not a one-line fix
Section titled “Why this is not a one-line fix”The web app is not a self-contained, liftable artifact:
- It imports harnery’s
src/directly.web/next.configpinsoutputFileTracingRootto..and adds a webpackextensionAliasso it can resolve harnery’s.js-suffixed-but-actually-.tsimports. The dashboard reaches up into harnery’s source tree and is tied to that layout. - It is ~610 MB installed (Next, React, Shiki, react-virtual, …). Shipping the source or a naive build would balloon harnery’s core package (~570 KB) by ~100x, punishing every CLI-only user.
- It is webpack-specific (
next dev --webpack, custom resolution config).
Options
Section titled “Options”- Prebuilt standalone bundle in-package (
next buildwithoutput: "standalone", shipped in the tarball): +tens of MB to core harnery; finickyoutputFileTracingbecause the app reaches into../src/; prepublish build complexity. Bloats every install. - Separate
@harnery/webpackage: the “correct” long-term shape, but requires decoupling the web app from harnery’ssrc/imports (a refactor) and a second release pipeline (double the versioning / changeset / canary overhead for a solo maintainer). harn web uplazy-fetches the UI on first use (clone/install/run the dashboard from the harnery repo at the matching version, into a cache dir): keeps core lean; adds runtime complexity (network, on-machine build).
Decision
Section titled “Decision”Deferred. None of the options is quick, and the demo can be made presentable
without it. Recommended eventual direction: lazy-fetch or a separate
@harnery/web package, both of which keep the core CLI lean (the reason
web/ was excluded in the first place). The in-package prebuilt bundle is
rejected for the bloat it imposes on CLI-only users.
Interim: harnery-demo runs the web UI from harnery source, pointed at the
demo’s .harnery/ via HARNERY_COORD_ROOT (harnery-demo/scripts/web.sh), so
the demo is presentable today.
Resolution (lazy-fetch)
Section titled “Resolution (lazy-fetch)”Adopted the lazy-fetch option. harn web up / build / start detect a missing local web/ (the npm-install case) and fetch the dashboard on first use: clone the matching version tag into ~/.cache/harnery/web/<ref>/, install the web app’s deps (web/ only), and run it; later runs reuse the cache. Implemented in src/commands/web-fetch.ts and src/commands/web.ts.
What made this the light option, against the bloat feared above: web/ reaches into src/ for exactly one module (harnery/lib/scratch), which is dependency-free. So the install is web/-only: no root install, no Playwright browser download. The clone is a few MB; web/node_modules is hardlinked from the package manager’s global cache.
The fetched ref defaults to the installed version’s tag (v<version>), so the dashboard tracks the coord schema the CLI writes. HARNERY_WEB_REF overrides it; --no-fetch opts out and prints manual steps (which also satisfies the graceful-degradation follow-up below). Lazy-fetch and the Next 16.2.6 build fix ship in the same release, so any version that has lazy-fetch fetches a dashboard that builds.
A separate @harnery/web package is still the cleaner long-term shape (no runtime clone/build), but lazy-fetch delivers a working dashboard for npm consumers now, without a decoupling refactor or a second release pipeline.
Additionally discovered: web build breaks under Next 16.2.6
Section titled “Additionally discovered: web build breaks under Next 16.2.6”While wiring the harnery-demo hybrid (running the web UI from source), the dev
build failed with UnhandledSchemeError: Reading from "node:fs" is not handled.
Cause: components/HostInfoProvider.tsx is a client component ("use client")
that imports from lib/config.ts, and lib/config.ts imports node:fs at the
top level. A browser-safe constant (DEFAULT_BIN_NAME) and type (HostInfo)
live in the same module as server-only fs code, so importing the constant into
a client component drags node:fs into the browser bundle. Likely surfaced (or
caused) by the next 16.2.4 -> 16.2.6 bump done for a Dependabot security
alert. Fix: split the browser-safe constant + type into their own module with no
node:fs import. This must be fixed for the web UI to run at all, separate from
the distribution question above. (Done on the next branch, shipping in the
same release as lazy-fetch.)
Follow-up
Section titled “Follow-up”When this is picked up, also decide whether harn web up should degrade
gracefully in the meantime: detect the missing app and print an actionable
message (how to get the dashboard) instead of the bare web_missing error, so
the shipped command is honest about its current limitation.