chore: consolidate duplicated runtime dependencies into the root package.json - #2200
Conversation
…age.json Every runtime dependency core/ imports is now declared once, in the repo-root package.json, and nowhere else. Deleting the client-side copies does not merely align them: a package installs only into an install root that declares it, so with no second declaration a second copy cannot be created at all — the failure class #1896 was opened for and #1965/#2029 built verify:dep-lockstep to detect. - clients/{web,cli,tui,launcher}: dropped ajv, atomically, chokidar, commander, open, pino, zod, @napi-rs/keyring, hono, @hono/node-server and react, as each applied. clients/cli and clients/launcher now declare no runtime dependencies. - Root hono floor raised ^4.12.18 -> ^4.13.1, keeping the higher of the two ranges that were in the tree rather than silently lowering web's. - tsup external lists: added ajv and zod to all three bundles, plus atomically and open (cli) and atomically (tui). tsup externalizes only what the nearest manifest declares, so a package that moves to the root is inlined unless it is named explicitly. - vitest.shared.mts: repointed the pino, hono, @hono/node-server, atomically, chokidar and @napi-rs/keyring pins from <client>/node_modules to the repo root, joining express, yaml and proper-lockfile. Left behind they would resolve to a directory that no longer exists, or to a transitive copy an unrelated dependency dragged in. react and react-dom stay client-pinned so the pair stays version-matched within an install. - Docs: AGENTS.md dependency placement, the root README and the local-dev skill describe the consolidated layout. verify:dep-lockstep candidate set: 4 -> 2 (hono and pino fell out; only @standard-schema/spec and zod still reach one tsc program from two installs). npm run local:gate and npm run pack:verify both pass. Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
🟡 Changes recommended
The external lists remain incomplete against the documented invariant, and several new dependency-resolution explanations are inaccurate.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Consolidates shared runtime dependencies at the repository root to prevent version skew across client installations.
Changes:
- Removes duplicate client dependency declarations and refreshes lockfiles.
- Updates bundler externals and Vitest resolution aliases.
- Documents the consolidated dependency model.
File summaries
| File | Description |
|---|---|
package.json |
Updates the consolidated Hono range. |
package-lock.json |
Records the root Hono range. |
vitest.shared.mts |
Redirects selected dependency aliases to the root. |
README.md |
Documents root-owned runtime dependencies. |
AGENTS.md |
Expands dependency-placement rules. |
.claude/skills/local-dev/SKILL.md |
Explains the consolidation rationale. |
clients/web/package.json |
Removes shared runtime dependencies. |
clients/web/package-lock.json |
Regenerates the web dependency tree. |
clients/web/tsup.runner.config.ts |
Adds explicit runner externals. |
clients/tui/package.json |
Retains only TUI-specific dependencies. |
clients/tui/package-lock.json |
Regenerates the TUI dependency tree. |
clients/tui/tsup.config.ts |
Adds explicit TUI externals. |
clients/cli/package.json |
Removes all CLI runtime declarations. |
clients/cli/package-lock.json |
Regenerates the CLI dependency tree. |
clients/cli/tsup.config.ts |
Adds explicit CLI externals. |
clients/launcher/package.json |
Removes the launcher dependency section. |
clients/launcher/package-lock.json |
Removes the launcher’s local Commander copy. |
Review details
Files not reviewed (4)
- clients/cli/package-lock.json: Generated file
- clients/launcher/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
- Files reviewed: 12/17 changed files
- Comments generated: 6
- Review effort level: Balanced
- Complete the bundler external lists against the AGENTS.md invariant rather than against today's reachability: chokidar, hono and react to clients/cli, chokidar and hono to clients/tui, react to the web runner. Every root-declared package core/ imports at runtime is now named in all three, so a new import in core/ cannot silently inline one. - AGENTS.md: split the single over-broad bullet in two. commander, open, @hono/node-server, vite and @vitejs/plugin-react are root dependencies reached only from client code, so they are not part of the core/ set and need naming only in the external list of the client that imports them. Give the vitest.shared.mts pins their own rule, stating the react/react-dom exception instead of implying every root-owned package is aliased to the root. - vitest.shared.mts: the umbrella comment now says root-OWNED rather than root runtime dependency — express is test-only and lives in the root devDependencies — and notes that the list is deliberately not the complete root set, since a package that resolves unambiguously needs no pin. - local-dev skill: document why react/react-dom stay client-pinned, which the previous text contradicted. npm run local:gate and npm run verify:bundle-externals pass. Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 1 — all six addressedMirroring at PR level, since inline replies go hidden once the threads go outdated.
On 1–3Correct catch, and the fix is the one you named: the list has to satisfy the invariant, not today's reachability. I verified the On 4The single bullet was making three claims and only one of them held. It now reads as three rules:
On 5 and 6Both were my text describing the change inaccurately.
The alias comment now says root-owned rather than root runtime dependency (
|
There was a problem hiding this comment.
🟡 Changes recommended
Web TypeScript mappings and local-development documentation remain inconsistent with the consolidated ownership model.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (4)
- clients/cli/package-lock.json: Generated file
- clients/launcher/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.claude/skills/local-dev/SKILL.md:105
- This rationale still groups client-only imports (
commander,open, and@hono/node-server) withcore/dependencies and says #2195 movedundici, althoughAGENTS.md:89distinguishes those client imports and this PR explicitly recordsundicias already root-only. It also implies the whole list is represented by root aliases, whilevitest.shared.mts:58-60says several intentionally have no alias. Split the core-owned and client-owned cases so this procedure does not contradict the governing rule.
has no manifest of its own (`test-servers/src`, `core/`) — hence the repo-root
alias for those in `vitest.shared.mts`. That used to be `express` and `yaml`
alone; #2195 made it the general case, moving `ajv`, `atomically`, `chokidar`,
`commander`, `hono`, `@hono/node-server`, `@napi-rs/keyring`, `open`, `pino`,
`react`, `undici` and `zod` to the root as well.
- Files reviewed: 12/17 changed files
- Comments generated: 1
- Review effort level: Balanced
- clients/web/tsconfig.{app,node,test}.json: repoint the pino, hono,
hono/streaming, @hono/node-server, atomically, chokidar and @napi-rs/keyring
path mappings from ./node_modules to ../../node_modules. The stale mappings
were not failing the build — an unresolvable paths entry falls back to
ordinary walk-up resolution, which finds the root copy — but chokidar is the
case where the fallback never runs: Vite's transitive copy still sits under
clients/web, so tsc was type-checking against the client's copy while Vitest
and the shipped bundle both resolved the root's. Same version today, silent
skew tomorrow. Both path prefixes now carry a comment saying which ownership
each one asserts.
- local-dev skill: the consolidation rationale no longer groups commander, open
and @hono/node-server (client-imported) with the core/ set, no longer claims
#2195 moved undici (it was already root-only), and no longer implies every
root-declared package has an alias.
npm run local:gate passes with clients/web/node_modules/.tmp cleared, so the
tsc results are from a cold build rather than an incremental cache.
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 2 — both addressed1. Web tsconfig
|
There was a problem hiding this comment.
🔵 Needs a closer look
Resolution guidance remains inaccurate about root aliases and transitive web copies.
Review details
Files not reviewed (4)
- clients/cli/package-lock.json: Generated file
- clients/launcher/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
.claude/skills/local-dev/SKILL.md:117
zodandopendo not currently resolve unambiguously by walk-up in the web client:clients/web/package-lock.jsonstill installs top-level copies transitively (zodviaeslint-plugin-react-hooks,openvia Storybook), and web tests/server code import both directly. The PR also recordszodas a remaining cross-install TypeScript candidate. Either pin these specifiers to the root (includingzod/v4and the applicable TypeScript paths) or document the actual reason that resolving the client-transitive copies is intentional; the current guidance would lead maintainers to overlook a real two-install resolution path.
the `clients/web/tsconfig.*.json` `paths` cover the packages whose resolution
is genuinely ambiguous — a transitive copy sits in the client install, or the
importer is `core/`, which has no manifest to resolve from. A package that
resolves unambiguously by walk-up (`ajv`, `commander`, `open`, `undici`,
`zod`) is deliberately absent from both.
vitest.shared.mts:95
- Repointing these aliases to
repoRootleaves the consumer comments inaccurate:clients/web/vite.config.ts:145-158,276-280,332-335andclients/web/server/start-vite-dev-server.ts:34-40still say the shared aliases resolvepino/honoand other core dependencies fromclients/web/node_modules. Those comments now describe the opposite ownership model and could cause a future edit to restore client-local resolution. Update them to distinguish the client-pinned React pair from the root-owned aliases.
// The rest of `core/`'s runtime dependencies, consolidated into the root
// manifest by #2195. Each of these used to be declared by the clients that
// reached it and was pinned to `<client>/node_modules` accordingly; once
// the declarations went away those paths stopped existing, so the pin has
// to follow the package to the root. Left un-repointed they would resolve
- Files reviewed: 15/20 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Two suppressed comments, both accurate. - Pin zod, zod/v4 and open to the root in vitest.shared.mts, and add matching paths to the three clients/web tsconfigs. The claim they replace — that these resolve unambiguously by walk-up — was wrong: both still sit at the top level of clients/web/node_modules as transitive copies (zod under eslint-plugin-react-hooks, open under Storybook), so an unpinned bare import from a web test resolved the client copy while core/ and the SDK packages resolved the root's. For open that is only wasteful; for zod it is a schema built by one copy and an instanceof check made by the other across the whole @modelcontextprotocol/* surface. zod is deduped as well, and zod/v4 gets its own entry because first-party code imports both specifiers and pinning one would split the package rather than collapse it. - Update the consumer comments that #2195 falsified. clients/web/vite.config.ts (three places) and server/start-vite-dev-server.ts all still said the shared aliases resolve pino/hono and friends from clients/web/node_modules; they now state the split — react and react-dom from the client install, everything root-declared from the repo root. - local-dev skill: replace the wrong 'resolves unambiguously' list with the distinction that actually matters. Deleting a declaration is not the same as the package resolving from the root, because chokidar, zod, open and react all sit in a client install transitively; check the install, not the manifest. ajv, commander and undici carry no pin because no client install has a copy. npm run local:gate passes from a cold tsc build. Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 3 — both suppressed comments addressedNo new inline comments this round; both findings came through the suppressed block, and both were right. 1.
|
There was a problem hiding this comment.
🔵 Needs a closer look
The shared alias documentation contains two contradictory dependency classifications that should be corrected.
Review details
Files not reviewed (4)
- clients/cli/package-lock.json: Generated file
- clients/launcher/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
Suppressed comments (2)
vitest.shared.mts:64
openandzodare described here as unpinned, but this same array adds root aliases for both below because client-local transitive copies make their resolution ambiguous. Remove them from the no-pin examples so this guidance does not encourage deleting the required aliases.
// `devDependencies`. Nor is this the complete root runtime set; a package
// that resolves unambiguously without help (`ajv`, `commander`, `open`,
// `undici`, `zod`) needs no pin and has none.
vitest.shared.mts:95
- This group includes
@hono/node-server, which is imported only by web client code rather thancore/(as the newAGENTS.mdrule notes). Describe the group as root-owned aliases instead ofcore/runtime dependencies so the resolution rationale remains accurate.
// The rest of `core/`'s runtime dependencies, consolidated into the root
- Files reviewed: 17/22 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Both findings are my own comments contradicting each other, in the same file. - The no-pin example list still named `open` and `zod`, which the array right below it now pins. It names `ajv`, `commander` and `undici` instead, and says what the criterion actually is: absence is a statement about the installed tree, not about the manifests. - The consolidated group is described as root-OWNED aliases rather than `core/`'s runtime dependencies, since `@hono/node-server` is reached from web client code alone and is here for the resolution reason, not because `core/` imports it. Comments only. npm run local:gate passes. Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 4 — both addressedBoth findings were my own comments in 1. The no-pin example list still named 2. The consolidated group is not purely Comments only, no behavior change. |
There was a problem hiding this comment.
🟢 Approval recommended
Dependency declarations, lockfiles, bundler externals, resolution aliases, and documentation are consistent with the repository’s packaging model.
Review details
Files not reviewed (4)
- clients/cli/package-lock.json: Generated file
- clients/launcher/package-lock.json: Generated file
- clients/tui/package-lock.json: Generated file
- clients/web/package-lock.json: Generated file
- Files reviewed: 17/22 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Closes #2195
Step 1 of two on #2030.
Every runtime dependency
core/imports is now declared once, in the repo-rootpackage.json. Deleting the client-side copies rather than aligning them is the point: a package installs only into an install root that declares it, so with no second declaration a second copy cannot be created at all — the failure class #1896 was opened for and #1965 / #2029 builtverify:dep-lockstepto detect.What moved
Removed from
clients/{web,cli,tui,launcher}as each applied:ajv,atomically,chokidar,commander,open,pino,zod,@napi-rs/keyring,hono,@hono/node-server,reactAll eleven were already declared at the root, so this PR is purely the deletion of the duplicate side.
undiciwas in the scope list but was already root-only (#2067) — no change.clients/cliandclients/launchernow declare no runtime dependencies at all, which is the expected steady state, not an omission.Left where they are, as the issue specifies:
ink-form/ink-scroll-view(inlined into the TUI bundle, #1952) and web's UI stack (@mantine/*,@dnd-kit/*,@emotion/react,react-dom,react-icons,react-markdown,react-syntax-highlighter,remark-gfm,papaparse, plusace-builds/react-ace) — all bundled intodist.Root
honowent^4.12.18→^4.13.1. The two declarations disagreed, and consolidating onto the root's floor would have silently lowered web's; keeping the higher range is the no-op choice.reactstays^19.0.0at the root. The clients'^19.2.4is what went away, per the constraint — narrowing the root range would reopen #1952 for theinkrenderer itself.Bundler
externalliststsup externalizes only what the nearest manifest declares, so a package that moves to the root is inlined unless named explicitly. Added:
clients/cli/tsup.config.tsajv,atomically,open,zodclients/tui/tsup.config.tsajv,atomically,zodclients/web/tsup.runner.config.tsajv,zodnpm run verify:bundle-externalspasses, and a banner scan of the built output confirms the only npm packages still inlined anywhere are the TUI's deliberateink-form/ink-scroll-viewtree (chalk,figures,ink-select-input,ink-text-input,is-unicode-supported,to-rotated) — nothing new was dragged in.vitest.shared.mtsThe pins could not retire — the opposite.
pino,hono,@hono/node-server,atomically,chokidarand@napi-rs/keyringwere pinned to<client>/node_modules, which after this change is a path that no longer exists for most of them. Worse for the ones where it does still exist:chokidaris present in every client install transitively viavite, andreactarrives as a peer ofreact-domandink— so an unpinned or stale specifier resolves the transitive copy rather than the root's, which is the duplicate the pin list exists to prevent.Those six therefore move to
repoRoot, joiningexpress,yamlandproper-lockfile.react/react-domstay client-pinned so the pair stays version-matched within one install (web resolves 19.2.8 for both; pinningreactat the root would have paired 19.2.7 with web's 19.2.8react-dom).Verification
npm run local:gate— pass.npm run pack:verify— pass. The tarball installs clean into a fresh consumer and the real bin drives web (serveddist+ an MCP App through the shipped sandbox proxy), cli (stdiotools/list) and tui (help).npm run verify:dep-lockstepcandidate set: 4 → 2. Before:@standard-schema/spec,hono,pino,zod. After:@standard-schema/spec,zod. Both remaining candidates agree across installs, andTOLERATED_SKEWstays empty.scripts/install-clients.mjs) is unaffected by a client manifest with nodependencieskey — it gates on the presence ofpackage.json, not on its contents, and the rootnpm installcascaded into all four clients successfully.Docs
AGENTS.md's dependency-placement section, the root README's setup paragraph, and the
local-devskill (which owns the reasoning) all describe the consolidated layout, including the two things that read as bugs and are not: two clients with zero runtime dependencies, and root-owned package names still appearing in a client'snode_modulestransitively..github/copilot-instructions.mdmirror to be updated in the same PR. That file does not exist in this repo —.github/holds onlyISSUE_TEMPLATE/,workflows/,dependabot.ymlandpull_request_template.md— so there was no mirror to update. Flagging rather than creating one, since introducing that file is a separate decision.