Skip to content

The CLI

Two commands, no dependencies, nothing to configure:

bash
npx vitest-auto-spy doctor   # read-only. Exits 1 when it finds something
npx vitest-auto-spy init     # writes the agent instructions pointer

They are one binary because they answer one question from two directions — is anything in this test suite quietly not doing what it looks like it is doing? doctor asks it of the repository, init asks it of the agent about to write the next spec.

doctor — defects that never fail

Every check here shares one property: nothing consumes the result. The suite is green, tsc --noEmit reports zero errors, and the only reader of the stale thing is a person who happens to open the file. That is what makes them survive for years, and it is also why a linter cannot find most of them — the evidence is spread across files.

$ npx vitest-auto-spy doctor
vitest-auto-spy doctor — /work/app
1 284 files, runner: vitest, entry: vitest-auto-spy/angular

error  tsconfig-glob-matches-nothing libs/users/tsconfig.spec.json
       The "include" pattern "src*.spec.ts" matches no file.
       → A pattern that matches nothing type-checks nothing, and `tsc --noEmit` still reports
         zero errors. Fix the glob or delete the entry.

3 errors, 4 warnings, 1 note
CheckWhat it findsWhy nothing catches it
tsconfig-glob-matches-nothingAn include pattern that matches no fileA glob that matches nothing type-checks nothing, and tsc reports success
tsconfig-file-missingA files entry naming a file that is goneSame — the config is only read by editors once the runner stopped using it
spec-imported-by-non-specA production module importing a *.spec.tsUnder a shared environment the import is a cycle, and the spec loses its own suite
spec-exports-fixtureA spec importing another specThe imported file's suites are collected twice and its hooks run in a foreign file's context
foreign-runner-pragma@jest-environment and friends left in a specVitest never reads them; the environment comes from the config, so the comment looks operative
dead-runner-configjest.config.*, karma.conf.* for a runner that is not installedIt is the first file a newcomer — or an agent — reads to learn how tests run
orphan-runner-fileA setup file only that dead config referencedOne found this way had been empty since before the migration: a year as a setting that configured nothing
angular-build-splitting-off@angular/build in [22.1.5, 22.1.7)The unit-test bundle is built with code splitting off. --coverage then grows by hundreds of megabytes with no plateau, and the builder emits no warning — see Angular
no-agent-instructionsNo AGENTS.md / CLAUDE.md / GEMINI.md names the packageA note, not an error. It is the one moment where saying so costs nothing

The check that motivated the tool: a spec showing Cannot find name 'vi' in the editor while tsc --noEmit reported zero errors. A migration codemod editing include had eaten a /**, turning src/**/*.spec.ts into src*.spec.ts — a syntactically valid glob that matches nothing. Nine of 152 spec tsconfigs still covered their specs.

It never writes. doctor reads the repository and prints; there is no --fix. Exit code 1 when anything above a note was found, 0 otherwise, so it drops into CI as one line.

Two shapes of pattern are deliberately exempt, because for them "matches nothing" is not evidence of anything: a declaration-only glob (src/**/*.d.ts, routinely a placeholder for ambient types that do not exist yet) and a pattern rooted in a directory the scan never enters (dist, out-tsc, coverage).

init — the pointer an agent actually reads

There is no zero-setup path into any coding agent's instruction context. Every tool discovers rules from a fixed set of repository-root or dot-directory paths, and none of them scans dependencies — the skill and the AGENTS.md shipped inside this package's tarball are never auto-discovered. Something in the repository has to point at them, once.

bash
npx vitest-auto-spy init

Tier 1, always written — three root files cover the whole field:

FileRead by
AGENTS.mdCodex, Cursor, Copilot, Cline, Windsurf, Zed, OpenCode, Qwen, Roo, Junie, Aider
CLAUDE.mdClaude Code, and GLM / Kimi running inside it
GEMINI.mdGemini CLI, which does not read AGENTS.md by default
.claude/skills/vitest-auto-spy/SKILL.mdClaude Code — a stub whose frontmatter is copied verbatim from the shipped skill, over a body that only points at node_modules, so it cannot go stale

Tier 2, only when the tool's own directory already exists, and glob-scoped so it costs no context on a task that is not about tests: .cursor/rules/, .github/instructions/, .windsurf/rules/, .devin/rules/, .clinerules/, .roo/rules/.

Never created: .rules, .cursorrules, .windsurfrules, or .clinerules as a file. Zed resolves instructions first-match-wins over an ordered list that ends in AGENTS.md, so creating any of them silently shadows the entire project's instructions. If one already exists, init appends to it rather than leaving it stale.

Why a command rather than the snippet

The paste-able snippet has to state every runner and every adapter, and half of it is false for any given repository. init reads the consuming package.json and the test config and writes only the true half — which subpath matches this runner, which adapter matches the framework, the actual path of the setup file that needs import 'vitest-auto-spy/rxjs', and it omits the rxjs bullet entirely when rxjs is not installed.

Idempotent by construction

Everything is written between markers:

md
<!-- vitest-auto-spy:begin v=3.7.0 sha=90452bea -->



<!-- vitest-auto-spy:end -->

The text between them is regenerated in full on every run; text outside them is never read and never reformatted. Running init after an upgrade is a no-op or a one-hunk diff. A CLAUDE.md that is a symlink to AGENTS.md, or that already carries an @AGENTS.md import line, is left alone — both are ways of keeping one instruction file, and writing through either would duplicate the block.

The block is kept under 1.6 kB on purpose. Codex caps the whole root→cwd AGENTS.md chain at project_doc_max_bytes (32 768 bytes by default) and silently truncates past it, so init warns when the file it appended to crosses that line.

Flags

FlagCommandEffect
--cwd <dir>bothRun against another directory
--checkinitWrite nothing; exit 1 if the block is missing or out of date. The CI form
--dry-runinitPrint what would change and write nothing
--uninstallinitRemove the managed blocks and delete the files init created
-h, --helpbothThe usage screen
-v, --versionbothThe installed version

init --check in CI is the same shape as llms:check in this repository: it fails when the block on disk is not the block the installed version would write, which is exactly when an upgrade changed the advice.

In CI

yaml
- run: npx vitest-auto-spy doctor
- run: npx vitest-auto-spy init --check

Neither needs a network, a config file, or a token. The CLI ships with the package, has no runtime dependencies of its own, and is the only part of it allowed to touch node:fs — an invariant this repository checks on every build.

Released under the MIT License.