Install, build and run the Inspector locally, and place a dependency correctly. Use when starting the web, cli, tui or launcher client; when asking whether a bu
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-local-dev-79afc211fe37 ,按照其中的说明把「local-dev」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
The rules a reviewer cites (TypeScript, Mantine/React, testing, the gate)
stay in AGENTS.md. This skill is how to get the thing
running and where a package goes.
Requires Node >=22.19.0.
npm install # at the REPO ROOT
v2 is not an npm workspace — each client under clients/* keeps its own
package.json and node_modules. A single root npm install is still all you
need: the root postinstall (scripts/install-clients.mjs) cascades
npm install into clients/web, clients/cli, clients/tui, and
clients/launcher.
npm install at the root.npm install at
the root to re-sync every client.The cascade is dev-only — it exits early when the package is installed under
node_modules, and a consumer never needs it because the tarball ships
prebuilt artifacts (each client's build/, plus clients/web/dist and
clients/web/static) rather than sources to compile. Set
INSPECTOR_SKIP_CLIENT_INSTALL=1 to skip it. The files allowlist itself is
docs/publishing.md.
⚠️ In a git worktree, do a real npm install. A symlinked node_modules
passes lint, tests and coverage, then fails every Storybook story file on Vite's
fs.allow.
For day-to-day web iteration, run Vite directly — fast HMR, no launcher build needed:
cd clients/web && npm run dev
The launcher-driven scripts run the built launcher, so npm run build
first:
npm run build # web → cli → tui → launcher
npm run web # prod web launcher against clients/web/dist
npm run web:dev # web launcher in --dev mode (Vite)
Individual builds: build:web, build:cli, build:tui, build:launcher. The
web build produces both the browser SPA (clients/web/dist, Vite) and the Node
prod-server runner (clients/web/build, tsup).
To run the CLI or TUI: node clients/launcher/build/index.js --cli … /
--tui ….
@inspector/core aliascore/ holds the logic shared by all three clients and intentionally has no
package.json — it is not published on its own. Each client bundles it via a
build-time alias:
esbuildOptions.alias in tsup.config.ts maps
@inspector/core → the repo core/ directory, with
noExternal: [/^@inspector\/core/] inlining it.clients/web/vite.config.ts for the browser app, and
clients/web/tsup.runner.config.ts for the Node backend runner, which
defines the alias itself. Dependency or debugging work on the runner belongs
in the tsup config, not the Vite one (Copilot).The rules are in AGENTS.md → Dependency placement, and
they are not restated here. Read them there and come back for the why — what
each rule is defending against, what it looked like when it was violated, and how
to tell you have hit one.
Work the rules in order: which manifest declares it, then dependencies vs
devDependencies, then whether the bundlers must also externalise it.
core/'s runtime dependencies are root-onlyA per-client declaration installs a second copy that drifts from the root's.
Not theoretical: it put two versions of ext-apps (1.7.4 / 1.7.5) and of the
transitive v1 @modelcontextprotocol/sdk (1.29.0 / 1.30.0) in the tree at once
(#1970). A second copy of client/core is exactly the failure the dedupe +
server.deps.inline workaround in vitest.shared.mts exists for.
The same reasoning extends to anything reached only through root-owned code that
has no manifest of its own (test-servers/src, core/). #2195 made that the
general case: ajv, atomically, chokidar, hono, @napi-rs/keyring,
pino, react and zod moved to the root, joining proper-lockfile and
undici, which were already there.
Keep two distinctions straight, because AGENTS.md's rules split on them:
core/-imported. commander, open and
@hono/node-server are root dependencies too, but they are reached only
from client code. Only the core/ set has to appear in all three bundler
external lists.vitest.shared.mts pins and
the clients/web/tsconfig.*.json paths cover the packages whose resolution
is genuinely ambiguous, which is two different situations: the importer is
core/, which has no manifest to resolve from, or a copy still sits in
the client install and would win. The second case is the one that is easy to
get wrong — chokidar (under vite), zod (under
eslint-plugin-react-hooks), open (under Storybook) and react (a peer of
react-dom and ink) are all present in a client's node_modules without
being declared there, so "we deleted the declaration" is not the same as "it
resolves from the root now". Check the install, don't reason from the
manifest. ajv, commander and undici carry no pin because no client
install has a copy of them at all.The point of deleting the client-side copies rather than merely keeping them in
step is that a package installs only into an install root that declares it.
Aligned duplicate declarations still drift the next time someone bumps one of
them; no declaration at all cannot. npm run verify:dep-lockstep is the detector
for the skew — in both its tiers since #2226 — and consolidation is what removes
the opportunity.
Two consequences that read as bugs and are not:
clients/cli and clients/launcher declare no runtime dependencies. Their
manifests carry devDependencies only. Everything they import at runtime is
root-declared and resolves by walking up from the client directory.node_modules still contains some of these names. They arrive
transitively (chokidar under vite, react as a peer of react-dom and
ink). That is why the vitest.shared.mts pins matter: an unpinned specifier
would resolve the nearest copy, which is the transitive one, not the root's.When a package moves to the root, its vitest.shared.mts pin has to move with
it — a path.resolve(dirname, …) pin left behind points at a directory that no
longer exists, or (for the transitive cases above) at the duplicate the pin was
supposed to rule out.
react and react-dom are the exception, and stay pinned per client. They
are a matched pair — react-dom reaches into React internals — and react-dom
is still web-declared, so npm resolves it and its React peer together inside
clients/web/node_modules. Pointing
react at the root while react-dom resolves from the client would pair a
renderer with a React it was not installed against, which is the same split the
pin exists to prevent, arrived at from the other side. dedupe still collapses
each install to one copy, which is what actually has to hold.
Every client's validate shells out to prettier, eslint, tsc and vitest,
which makes them look like they belong beside the scripts that call them. They
do not need to be: npm run prepends every ancestor node_modules/.bin to
PATH, and Node and TypeScript walk parent node_modules /
node_modules/@types the same way — so a client that declares no toolchain at
all still resolves the root's copy. clients/launcher declares no
devDependencies whatsoever and its validate is unchanged.
What a per-client declaration does buy is a second copy free to drift, and it
had (#2196): globals sat at ^17.7.0 at the root against ^17.4.0 in all four
clients, and typescript-eslint at ^8.65.0 against ^8.56.1. Nothing failed —
which is the point. A lint or format tool that differs per client makes the gate's
verdict a function of where you ran it, and the exact prettier pin (#1790)
only means something when there is one of it.
⚠️ The line is used by every client, not "used by one" and not "is it
toolchain". tsx, playwright, storybook, happy-dom, ink-testing-library,
vite-node and each client's own @types/* are toolchain too and stay where
they are — hoisting them would make every client install the union of all four.
So do the ones more than one client declares without all of them doing so:
tsup sits in web, cli and tui, and vite in web and tui on top of the root
runtime dependency that --web --dev needs. Neither is in scope here;
whether to consolidate them is a separate call with a separate rationale (vite
especially, since its root declaration is a dependency, not a
devDependency).
⚠️ Deleting a client's declaration does not always delete the copy — and where a copy survives, it is the one that wins. Two mechanisms put one back, neither of which the manifest mentions:
eslint-plugin-react-refresh / eslint-plugin-storybook
and the TUI's eslint-plugin-react-hooks each pull a client-local eslint;
web's Storybook/Vitest stack pulls a local typescript and vitest.@types/express brings @types/node into web and
cli's trees on its own.Those copies sit nearer than the root's, so clients/web/node_modules/.bin
precedes the root bin directory on PATH and TypeScript resolves the nearest
node_modules/@types. Verify with npm exec -- which eslint from the client
rather than assuming — the assumption is what made the first cut of #2196 claim
more than it delivered (Copilot).
So the consolidation buys one declaration and one place to bump, not one copy on disk. The two mechanisms are not equally safe, and neither is a guarantee:
@vitest/browser-playwright pins
vitest to a single version, which is why the trio below is pinned too. For a
wide range (eslint-plugin-react-refresh accepts ^9 || ^10) the copies agree
only because npm happens to resolve the same latest in both installs, which is
a coincidence that holds until it doesn't.@types/node was 24.13.1 against the root's
24.13.3, on v2/main too — a declared ^24.12.4 loses to a nearer
transitive. It is pinned with an overrides entry in clients/cli since
#2226.✅ verify:dep-lockstep gates both of those since #2226. Its second tier
compares every package any install declares — dependencies,
devDependencies and optionalDependencies, unioned across the root and all
four clients — against every top-level copy in every install, independent of
what a tsc program resolves. So a tool binary that no program loads
(eslint, typescript, vitest) and a transitive copy that no single program
meets (the cli @types/node above) are both in scope now, as is a skew between
two clients with no root copy involved (@types/react, web against tui).
⚠️ Two gaps remain, and they are why the by-hand check below is still worth running. The tier reads lockfiles, so a copy you installed by hand and never committed is invisible to it; and it compares only names some manifest declares, so a purely transitive package no manifest anywhere names stays the program tier's business. When you change what a client declares, check by hand from that client:
cd clients/web && npm exec -- which eslint prettier tsc vitest
@vitest/browser-playwright declares an exact peer on vitest ("vitest": "4.1.10", not a range), so that package — not the root's range — decides which
vitest lands in clients/web. Left floating, the root resolves the newest
patch while web's peer stays pinned to the older one, and web's tests then run on
one vitest while loading a @vitest/coverage-v8 provider built against
another. Both would still pass, which is the bad part.
So vitest and @vitest/coverage-v8 at the root and @vitest/browser-playwright
in clients/web are all pinned exactly, and a bump edits all three in one
change — the same discipline as the exact prettier pin (#1790).
dependenciesThe client builds externalise npm packages, so a published install resolves them from the root manifest — and devDependencies are not installed for consumers. A runtime import parked there therefore breaks the published package while passing every local check, which is the worst shape a mistake can take here.
Two live examples worth knowing:
express is test-only, so it is a devDependency.vite and @vitejs/plugin-react look like build tooling but are root
dependencies, because clients/web/server/start-vite-dev-server.ts imports
them at runtime for --web --dev. Moving them would break that for consumers
while every local check stayed green.tsup and Vite externalise what the client's package.json declares, and a
root-only dependency is in none of them — so it is bundled, silently. For a
CJS package inlined into an ESM bundle that is fatal: esbuild leaves a
Dynamic require of "path" is not supported shim that throws at import time,
so the binary dies before it parses a flag (proper-lockfile, #2082).
undici (#2067) is the worse variant, because it is import()ed lazily: the
binary boots fine and only the proxied path dies — with esbuild's rewritten
specifier (import("./undici-HXPKCIY3.js")) meaning no user-side install can
ever satisfy it. It was declared in clients/cli/package.json, so the CLI
looked correct while web and TUI silently inlined 1.05 MB of it.
How to tell. The ordinary tiers cannot: unit and integration tests run
against source, where the real package is on the resolution path, and the smokes
never take the lazy path. npm run verify:bundle-externals reads the built
output for exactly this reason — the config and the output disagreed for four
releases.
⚠️ Probing by hand with node -e falsely succeeds. node -e exposes a
global require that satisfies esbuild's typeof require !== "undefined"
guard, so the chunk looks loadable when it is not. Reproduce from a real .mjs
file.
An externalised package resolves its own react from wherever npm placed
it — beside a React satisfying that package's peer range, which is looser
than ours in every case here. ink-form and ink-scroll-view declare ">=18",
so a consumer's React 18 satisfies them and hoists them while our React 19 nests
underneath: the bundle renders through one React, those packages call hooks on
another, and the TUI crashes on the first hook (#1952).
ink is exempt on cost (~1.4 MB of react-reconciler + yoga-layout, plus
a createRequire banner). ⚠️ Never justify that exemption by a peer range —
it briefly read "its ">=19" peer keeps npm honest", which is false: a consumer
pinning React 19.0 satisfies ">=19" while a narrower range of ours nests
underneath. What makes it safe is the root react range staying open to the
whole major, so npm can dedupe. clients/tui/__tests__/tsupConfig.test.ts
enforces the whole split, the exemption included.
Because v2 is not a workspace, the same package can appear twice in one tsc
program — a client's own install plus the root's, reached through core/ or
test-servers/src. At the same version that is harmless; on a skew TypeScript
must relate two structurally-distinct declarations, which for a
recursive-generic surface is exponential. A zod 4.3.6 / 4.4.3 skew exhausted
the 4 GB tsc heap outright with TS2589 (#1896).
⚠️ Raising the heap hides the class rather than fixing it. Align the
versions; npm run verify:dep-lockstep is the guard, and it runs two tiers.
The first derives its candidate set from what actually enters each tsc
program, so a package whose declarations arrive only through another package's
.d.ts is still seen. The second (#2226) compares every declared package's
top-level copies across installs whatever any program loads, so a transitive or
peer-shadow drift no program can meet is caught too.
Aligning a stale install is npm update <pkg> there — it moves the lockfile
within the declared range without widening the range, which
npm install <pkg>@<version> would. A transitive copy that will not move takes
an overrides entry in that install (see the next section).
overrides beats npm audit fixtsup@8.5.1 declares esbuild: ^0.27.0, and the advisory covers
0.27.3 - 0.28.0 with 0.27.7 the last 0.27.x — so there is no upward escape
inside that range, and npm audit fix "resolves" it by silently downgrading
to 0.27.2 across three installs (~700 lines of lockfile churn for a low-severity
dev-only advisory; tried and reverted in #2058). The override forces one deduped
copy above the range instead.
Two costs: it puts tsup on an esbuild major it does not declare, so
npm run build — not npm audit — is the real gate on such a pin; and it is
invisible to the audit once clean, so drop it when tsup widens its range
rather than carrying it forever.