vitest-auto-spy 5.27.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -4421,7 +4421,9 @@ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
4421
4421
  - **The scan does not descend into a nested repository or a git worktree.** A tree carrying
4422
4422
  worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
4423
4423
  `codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
4424
- it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
4424
+ it is a directory (a nested clone) or a file (a worktree). Directories git ignores are skipped too
4425
+ — every `.gitignore` from the scan root down, `.git/info/exclude` and the per-user
4426
+ `core.excludesFile`, directories only, so an ignored file is still listed. Past 50 000 files the scan still
4425
4427
  truncates, and `doctor` reports that as a `scan-cap-reached` warning (exit 1) rather than a clean
4426
4428
  result; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4427
4429
  - **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
package/CHANGELOG.md CHANGED
@@ -10,6 +10,38 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.28.0] - 2026-09-23
14
+
15
+ ### Changed
16
+
17
+ - **The `doctor` and `codemod` repository scan honours git's exclude rules below the root, not only
18
+ the root `.gitignore`.** A nested `.gitignore` is read relative to its own directory and overrides a
19
+ shallower one; below every `.gitignore` sit the root's `.git/info/exclude` (followed through a
20
+ worktree's `.git` file) and the per-user `core.excludesFile` (else `$XDG_CONFIG_HOME/git/ignore`),
21
+ as in git. A monorepo keeping `/generated` in `libs/app/.gitignore`, or a checkout excluding local
22
+ directories through `info/exclude` or a global ignore, had them counted, walked and offered to
23
+ `codemod --write` — so a local run reported more files than CI did. Still no `git` process and no
24
+ dependency, and still directories only: an ignored file is listed. A `.gitignore` above the scan
25
+ root is not read, and `[include]` in git config is not followed.
26
+ - **`doctor --json` and `doctor --markdown` name the flag they meant.** Both are still refused, exit 2,
27
+ like any unknown flag; the error now ends with ``Did you mean `--format json`?``.
28
+
29
+ ### Fixed
30
+
31
+ - **`init --check` and `init --dry-run` no longer seem to disagree over a version-only change.** A
32
+ block whose marker differs only in `v=` is `unchanged` under `--check` (by design since 5.19.0: CI
33
+ must not go red on a release that changed no advice) and `updated` under `--dry-run`, which read as
34
+ a contradiction. Both now say _only the version stamp differs_ in the file's note.
35
+ - **The `tsconfig` glob check exempts a pattern into an ignored directory that does not exist yet.**
36
+ `src/generated/**/*.ts` in `include`, or a `files` entry there, was reported as matching nothing or
37
+ missing whenever `doctor` ran before the code generator — in CI, typically — although
38
+ `src/generated/` is in `.gitignore`: the exemption only knew directories the scan had pruned.
39
+ - **`prefer-create-spy-from-class` at `{ minRunnerFns: 1 }` leaves a one-member literal passed to a
40
+ typed parameter of a helper the same file declares alone.** `createDefaultOptions({ changeOptionsCallback:
41
+ callback })` over `const createDefaultOptions = (overrides?: Partial<InfoboxOptions>) => …` is
42
+ checked against that type already. An untyped parameter, a rest parameter, an inline object type
43
+ of mocks and an imported helper still report — the last is out of reach of a syntax-only rule.
44
+
13
45
  ## [5.27.0] - 2026-09-23
14
46
 
15
47
  ### Changed
@@ -7796,7 +7828,8 @@ by hand there, in more than one place, by more than one person.
7796
7828
  `mockAccessorsProp`.
7797
7829
  - Dual ESM + CJS build with type declarations; 100% test coverage.
7798
7830
 
7799
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.27.0...HEAD
7831
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.28.0...HEAD
7832
+ [5.28.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.27.0...v5.28.0
7800
7833
  [5.27.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.26.0...v5.27.0
7801
7834
  [5.26.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.25.0...v5.26.0
7802
7835
  [5.25.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.24.0...v5.25.0
package/README.md CHANGED
@@ -139,7 +139,7 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
139
139
  - [Which file your agent reads](#which-file-your-agent-reads)
140
140
  - [Install it in your agent](#install-it-in-your-agent)
141
141
  - [OpenAI Codex](#openai-codex)
142
- - [GLM (z.ai), Kimi K2 and other Claude-compatible models](#glm-zai-kimi-k2-and-other-claude-compatible-models)
142
+ - [GLM (z.ai), Kimi K3 and other Claude-compatible models](#glm-zai-kimi-k3-and-other-claude-compatible-models)
143
143
  - [Gemini CLI](#gemini-cli)
144
144
  - [Claude Code plugin](#claude-code-plugin)
145
145
  - [Availability](#availability)
@@ -372,8 +372,10 @@ that matches no file is the same kind of error, because _Nothing left to migrate
372
372
  read is not a result. A red suite under `perf --gate` lands there too: a failing test is measured
373
373
  until its timeout, and 30 s of timeout looks exactly like 30 s of slow code.
374
374
 
375
- The repository scan behind `doctor` and `codemod` stops at a nested repository or a git worktree — a
376
- `.git` entry, whether it is a directory or a file. A tree carrying worktrees under it used to be
375
+ The repository scan behind `doctor` and `codemod` skips the directories git ignores — every
376
+ `.gitignore` from the root down, `.git/info/exclude` and the per-user `core.excludesFile`, read as
377
+ files with no `git` process — and stops at a nested repository or a git worktree — a `.git` entry,
378
+ whether it is a directory or a file. A tree carrying worktrees under it used to be
377
379
  listed twice, so `doctor` reported every import graph in duplicate and `codemod --write` could have
378
380
  rewritten specs on someone else's branch.
379
381
 
@@ -653,7 +655,7 @@ served — including the ones your teammates use and you do not.
653
655
  | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
654
656
  | **Claude Code** | `CLAUDE.md` — project, `.claude/CLAUDE.md` and `~/.claude/CLAUDE.md`, all concatenated | **No.** Bridge with an `@AGENTS.md` import line, or a symlink |
655
657
  | **OpenAI Codex** — the `codex` CLI, the IDE extension, Codex cloud | `AGENTS.md`, one per directory from the git root down to the cwd ([below](#openai-codex)) | native |
656
- | **GLM (z.ai coding plan)**, **Kimi K2** | whatever their client reads — inside Claude Code that is `CLAUDE.md` ([below](#glm-zai-kimi-k2-and-other-claude-compatible-models)) | through the client |
658
+ | **GLM (z.ai coding plan)**, **Kimi K3** | whatever their client reads — inside Claude Code that is `CLAUDE.md` ([below](#glm-zai-kimi-k3-and-other-claude-compatible-models)) | through the client |
657
659
  | **Cursor** | root `AGENTS.md`; `.cursor/rules/*.mdc` for glob-scoped rules | native — and it applies a root `CLAUDE.md` the same always-on way |
658
660
  | **GitHub Copilot** | root `AGENTS.md`; `.github/copilot-instructions.md` | native, coding agent included |
659
661
  | **OpenCode** | `AGENTS.md`, then `CLAUDE.md`, per directory upwards | native |
@@ -708,7 +710,7 @@ Then, per tool — everything in the right-hand column is optional on top of tho
708
710
  | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
709
711
  | **Claude Code** | `/plugin marketplace add ASDAlexey/vitest-auto-spy`, then `/plugin install vitest-auto-spy@vitest-auto-spy` — [the skill](#claude-code-plugin), no project files touched |
710
712
  | **OpenAI Codex** | nothing more; optionally `~/.codex/config.toml` from [below](#openai-codex) |
711
- | **GLM (z.ai)**, **Kimi K2** | identical to Claude Code — same client, same plugin command |
713
+ | **GLM (z.ai)**, **Kimi K3** | identical to Claude Code — same client, same plugin command |
712
714
  | **Cursor** | `.cursor/rules/vitest-auto-spy.mdc` to load it only for spec files (see below) |
713
715
  | **GitHub Copilot** | `.github/instructions/vitest-auto-spy.instructions.md` (see below) |
714
716
  | **Cline** | `.clinerules/vitest-auto-spy.md` — the same three lines, plus `paths: ["**/*.spec.ts","**/*.test.ts"]` |
@@ -788,14 +790,14 @@ which is exactly why this reference ships inside the tarball rather than only on
788
790
  `node_modules/vitest-auto-spy/AGENTS.md` is on disk the moment the setup script has installed
789
791
  dependencies, so nothing has to be fetched.
790
792
 
791
- ### GLM (z.ai), Kimi K2 and other Claude-compatible models
793
+ ### GLM (z.ai), Kimi K3 and other Claude-compatible models
792
794
 
793
795
  GLM is a **model**, not an agent — the thing that reads files is the client you run it in.
794
796
 
795
797
  The z.ai coding plan runs GLM **inside Claude Code**, by pointing `ANTHROPIC_BASE_URL` (with
796
798
  `ANTHROPIC_AUTH_TOKEN`) at z.ai's Anthropic-compatible endpoint. File discovery is untouched by
797
799
  that: `CLAUDE.md`, `.claude/skills/` and the [plugin](#claude-code-plugin) below behave exactly as
798
- they do on Claude, because it is the same client. Kimi K2 driven through Claude Code is the same
800
+ they do on Claude, because it is the same client. Kimi K3 driven through Claude Code is the same
799
801
  story — and there the skill and the plugin are worth more than a pasted snippet, because they load
800
802
  only when a spec actually mentions the library and cost no context the rest of the time.
801
803
 
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve, join, dirname, posix, isAbsolute, relative, sep } from 'node:path';
3
3
  import { readFileSync, statSync, mkdirSync, writeFileSync, existsSync, lstatSync, rmSync, readdirSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
4
5
  import { createHash } from 'node:crypto';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { createRequire } from 'node:module';
@@ -117,15 +118,40 @@ function parseGitignore(text) {
117
118
  }
118
119
  return rules;
119
120
  }
120
- function isIgnoredDirectory(rules, path) {
121
+ function isIgnoredBy(sources, path) {
121
122
  let ignored = false;
122
- for (const rule of rules) {
123
- if (rule.pattern.test(path)) {
124
- ignored = !rule.negated;
123
+ for (const { base, rules } of sources) {
124
+ if (base === "" || path.startsWith(`${base}/`)) {
125
+ const local = base === "" ? path : path.slice(base.length + 1);
126
+ for (const rule of rules) {
127
+ if (rule.pattern.test(local)) {
128
+ ignored = !rule.negated;
129
+ }
130
+ }
125
131
  }
126
132
  }
127
133
  return ignored;
128
134
  }
135
+ function excludesFileSetting(config) {
136
+ let section2 = "";
137
+ let found;
138
+ for (const line of config.split("\n")) {
139
+ const header2 = /^\s*\[\s*([\w.-]+)/.exec(line)?.[1];
140
+ if (header2 !== void 0) {
141
+ section2 = header2.toLowerCase();
142
+ continue;
143
+ }
144
+ const value = /^\s*excludesfile\s*=\s*(.*)$/i.exec(line)?.[1];
145
+ if (section2 === "core" && value !== void 0) {
146
+ found = configValue(value);
147
+ }
148
+ }
149
+ return found;
150
+ }
151
+ function configValue(raw) {
152
+ const quoted = /^"((?:[^"\\]|\\.)*)"/.exec(raw)?.[1];
153
+ return quoted === void 0 ? raw.replace(/\s*[#;].*$/, "").trim() : quoted.replace(/\\(.)/g, "$1");
154
+ }
129
155
  function toRegExp(line) {
130
156
  const body = line.replace(/\/+$/, "");
131
157
  if (body === "" || line.includes("\\")) {
@@ -252,20 +278,76 @@ function removeFile(path) {
252
278
  rmSync(path, { force: true });
253
279
  }
254
280
  function listRepositoryFiles(root, limit = scanCap()) {
255
- return scan(root, limit, []).files;
281
+ return scan(root, limit, () => false).files;
256
282
  }
257
- function scanRepository(root, limit = scanCap()) {
258
- return scan(root, limit, readGitignoreRules(root));
283
+ function scanRepository(root, limit = scanCap(), filter = gitignoreFilter(root)) {
284
+ return scan(root, limit, filter);
259
285
  }
260
- function scan(root, limit, rules) {
261
- const walker = { root, rules, found: [], ignored: [], limit };
286
+ function scan(root, limit, filter) {
287
+ const walker = { root, filter, found: [], ignored: [], limit };
262
288
  const truncated = walk(walker, root);
263
289
  return { files: walker.found.sort(), ignored: walker.ignored.sort(), truncated };
264
290
  }
265
- function readGitignoreRules(root) {
266
- const text = readTextFile(join(root, ".gitignore"));
291
+ function gitignoreFilter(root) {
292
+ const gitDirectory = commonGitDirectory(root);
293
+ const excludes = [
294
+ { base: "", rules: rulesOf(readTextFile(resolve(root, globalExcludesFile(gitDirectory)))) },
295
+ { base: "", rules: rulesOf(gitDirectory === void 0 ? void 0 : readTextFile(join(gitDirectory, "info", "exclude"))) }
296
+ ];
297
+ const files = /* @__PURE__ */ new Map();
298
+ const verdicts = /* @__PURE__ */ new Map();
299
+ const sourceIn = (directory) => {
300
+ const cached = files.get(directory) ?? { base: directory, rules: rulesOf(readTextFile(join(root, directory, ".gitignore"))) };
301
+ files.set(directory, cached);
302
+ return cached;
303
+ };
304
+ const isIgnored = (path) => {
305
+ if (path === "" || path === ".." || path.startsWith("../")) {
306
+ return false;
307
+ }
308
+ const cached = verdicts.get(path);
309
+ if (cached !== void 0) {
310
+ return cached;
311
+ }
312
+ const segments = path.split("/");
313
+ const parents = segments.slice(0, -1).map((_, index) => segments.slice(0, index + 1).join("/"));
314
+ const verdict = isIgnored(parents.at(-1) ?? "") || isIgnoredBy([...excludes, sourceIn(""), ...parents.map(sourceIn)], path);
315
+ verdicts.set(path, verdict);
316
+ return verdict;
317
+ };
318
+ return isIgnored;
319
+ }
320
+ function rulesOf(text) {
267
321
  return (text === void 0 ? void 0 : parseGitignore(text)) ?? [];
268
322
  }
323
+ function commonGitDirectory(root) {
324
+ const dotGit = join(root, ".git");
325
+ const gitDirectory = isDirectory(dotGit) ? dotGit : linkedGitDirectory(root, dotGit);
326
+ if (gitDirectory === void 0) {
327
+ return void 0;
328
+ }
329
+ const common = readTextFile(join(gitDirectory, "commondir"))?.trim();
330
+ return common ? resolve(gitDirectory, common) : gitDirectory;
331
+ }
332
+ function linkedGitDirectory(root, dotGit) {
333
+ const [target] = captures(readTextFile(dotGit) ?? "", /^gitdir:\s*(.+?)\s*$/m);
334
+ return target === void 0 ? void 0 : resolve(root, target);
335
+ }
336
+ function setting(name) {
337
+ const value = process.env[name];
338
+ return value === "" ? void 0 : value;
339
+ }
340
+ function globalExcludesFile(gitDirectory) {
341
+ const home = setting("HOME") ?? homedir();
342
+ const xdg = setting("XDG_CONFIG_HOME") ?? join(home, ".config");
343
+ const override = process.env["GIT_CONFIG_GLOBAL"];
344
+ const configs = [
345
+ ...override === void 0 ? [join(xdg, "git", "config"), join(home, ".gitconfig")] : [override],
346
+ ...gitDirectory === void 0 ? [] : [join(gitDirectory, "config")]
347
+ ];
348
+ const configured = configs.flatMap((config) => excludesFileSetting(readTextFile(config) ?? "") ?? []).at(-1);
349
+ return configured === void 0 ? join(xdg, "git", "ignore") : configured.replace(/^~(?=\/|$)/, home);
350
+ }
269
351
  function isRepositoryRoot(directory) {
270
352
  return existsSync(join(directory, ".git"));
271
353
  }
@@ -298,7 +380,7 @@ function walk(walker, directory) {
298
380
  }
299
381
  function isGitignored(walker, directory) {
300
382
  const path = toPosix(relative(walker.root, directory));
301
- if (!isIgnoredDirectory(walker.rules, path)) {
383
+ if (!walker.filter(path)) {
302
384
  return false;
303
385
  }
304
386
  walker.ignored.push(path);
@@ -733,7 +815,8 @@ function readProfile(cwd) {
733
815
  const scripts = stringMap(packageJson["scripts"]);
734
816
  const runner = detectRunner(dependencies, scripts);
735
817
  const framework = detectFramework(dependencies);
736
- const scan3 = scanRepository(cwd);
818
+ const isIgnoredDirectory = gitignoreFilter(cwd);
819
+ const scan3 = scanRepository(cwd, scanCap(), isIgnoredDirectory);
737
820
  return {
738
821
  cwd,
739
822
  runner,
@@ -746,7 +829,7 @@ function readProfile(cwd) {
746
829
  scripts,
747
830
  files: scan3.files,
748
831
  filesTruncated: scan3.truncated,
749
- ignoredDirectories: scan3.ignored
832
+ isIgnoredDirectory
750
833
  };
751
834
  }
752
835
  var PACKAGE_NAME = "vitest-auto-spy";
@@ -3005,7 +3088,7 @@ function checkForeignPragma(graph) {
3005
3088
  }
3006
3089
 
3007
3090
  // src/cli/checks/export-map.generated.ts
3008
- var EXPORT_MAP_VERSION = "5.26.0";
3091
+ var EXPORT_MAP_VERSION = "5.27.0";
3009
3092
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin vitest-auto-spy/perf-reporter";
3010
3093
  var EXPORTED_BY = {
3011
3094
  AccessorImplementations: "0 1 2 3 4 12 20 21 22",
@@ -3881,14 +3964,14 @@ function expandInclude(pattern) {
3881
3964
  const base = last === "*" ? trimmed2 : `${trimmed2}${last === "**" ? "/*" : "/**/*"}`;
3882
3965
  return IMPLIED_EXTENSIONS.map((extension) => `${base}${extension}`);
3883
3966
  }
3884
- function isExemptPattern(pattern, ignoredDirectories = []) {
3885
- return /\.d\.[cm]?ts$/.test(pattern) || isOutsideScan(pattern, ignoredDirectories);
3967
+ function isExemptPattern(pattern, isIgnored = () => false) {
3968
+ return /\.d\.[cm]?ts$/.test(pattern) || isOutsideScan(pattern, isIgnored);
3886
3969
  }
3887
- function isOutsideScan(path, ignoredDirectories) {
3970
+ function isOutsideScan(path, isIgnored) {
3888
3971
  const segments = path.split("/");
3889
3972
  const wildcard = segments.findIndex((segment) => /[*?[]/.test(segment));
3890
3973
  const literal = wildcard === -1 ? segments : segments.slice(0, wildcard);
3891
- return literal.some(isSkippedDirectory) || ignoredDirectories.some((directory) => `${path}/`.startsWith(`${directory}/`));
3974
+ return literal.some(isSkippedDirectory) || literal.some((_, index) => isIgnored(literal.slice(0, index + 1).join("/")));
3892
3975
  }
3893
3976
  function literalTail(pattern) {
3894
3977
  const last = pattern.slice(pattern.lastIndexOf("/") + 1);
@@ -3920,7 +4003,7 @@ function checkInclude(configPath, entry, profile) {
3920
4003
  return void 0;
3921
4004
  }
3922
4005
  const resolved = resolveFromConfig(configPath, entry);
3923
- if (isExemptPattern(resolved, profile.ignoredDirectories) || matchesAnyFile(resolved, files)) {
4006
+ if (isExemptPattern(resolved, profile.isIgnoredDirectory) || matchesAnyFile(resolved, files)) {
3924
4007
  return void 0;
3925
4008
  }
3926
4009
  const tail = literalTail(resolved);
@@ -3950,7 +4033,7 @@ function checkOne(profile, configPath) {
3950
4033
  const findings = stringList(parsed["include"]).flatMap((entry) => checkInclude(configPath, entry, profile) ?? []);
3951
4034
  for (const entry of stringList(parsed["files"])) {
3952
4035
  const resolved = resolveFromConfig(configPath, entry);
3953
- if (!profile.files.includes(resolved) && !isOutsideScan(resolved, profile.ignoredDirectories)) {
4036
+ if (!profile.files.includes(resolved) && !isOutsideScan(resolved, profile.isIgnoredDirectory)) {
3954
4037
  findings.push({
3955
4038
  check: "tsconfig-file-missing",
3956
4039
  severity: "error",
@@ -4474,6 +4557,10 @@ function planFor(target, content, profile, version, facts) {
4474
4557
  }
4475
4558
  return { target, existing, desired: applyManaged(existing ?? "", managedBlock(profile, version, facts)), note: note2 };
4476
4559
  }
4560
+ function isStampOnly(plan) {
4561
+ return plan.existing !== void 0 && plan.desired !== void 0 && plan.existing !== plan.desired && withoutVersion(plan.existing) === withoutVersion(plan.desired);
4562
+ }
4563
+ var STAMP_ONLY_NOTE = "only the version stamp differs, which `--check` does not count; a plain `init` refreshes it";
4477
4564
  function statusOf(plan, check) {
4478
4565
  if (plan.desired === void 0) {
4479
4566
  return "skipped";
@@ -4484,7 +4571,7 @@ function statusOf(plan, check) {
4484
4571
  if (plan.existing === plan.desired) {
4485
4572
  return "unchanged";
4486
4573
  }
4487
- return check && withoutVersion(plan.existing) === withoutVersion(plan.desired) ? "unchanged" : "updated";
4574
+ return check && isStampOnly(plan) ? "unchanged" : "updated";
4488
4575
  }
4489
4576
  function isSelected(path, only) {
4490
4577
  return only === void 0 || only.some((entry) => {
@@ -4536,15 +4623,16 @@ function uninstallPlan(plan) {
4536
4623
  function applyPlan(cwd, plan, options) {
4537
4624
  const status = options.uninstall ? uninstallStatus(plan) : statusOf(plan, options.check);
4538
4625
  const path = plan.target.path;
4626
+ const note2 = !options.uninstall && isStampOnly(plan) ? `${plan.note} \u2014 ${STAMP_ONLY_NOTE}` : plan.note;
4539
4627
  if (options.check || options.dryRun || plan.desired === void 0 || status === "unchanged") {
4540
- return { path, status, note: plan.note };
4628
+ return { path, status, note: note2 };
4541
4629
  }
4542
4630
  if (plan.desired === "") {
4543
4631
  removeFile(join(cwd, path));
4544
4632
  } else {
4545
4633
  writeTextFile(join(cwd, path), plan.desired);
4546
4634
  }
4547
- return { path, status, note: plan.note };
4635
+ return { path, status, note: note2 };
4548
4636
  }
4549
4637
  function uninstallStatus(plan) {
4550
4638
  if (plan.desired === void 0 || plan.desired === plan.existing) {
@@ -6186,8 +6274,8 @@ function configCode(graph) {
6186
6274
  }
6187
6275
  return configs;
6188
6276
  }
6189
- function configDeclares(graph, setting) {
6190
- return configCode(graph).some((code) => setting.test(code));
6277
+ function configDeclares(graph, setting2) {
6278
+ return configCode(graph).some((code) => setting2.test(code));
6191
6279
  }
6192
6280
  function declaresNoIsolation(graph) {
6193
6281
  return configDeclares(graph, NO_ISOLATION);
@@ -7047,6 +7135,10 @@ function rejectFlags(args, command, io) {
7047
7135
  io.err(
7048
7136
  `\`${command}\` accepts ${[...accepted, ...COMMON_FLAGS].sort((a, b) => a.localeCompare(b)).map((name) => `--${name}`).join(", ")}.`
7049
7137
  );
7138
+ const formats = unknown.filter((name) => accepted.includes("format") && (name === "json" || name === "markdown"));
7139
+ if (formats.length > 0) {
7140
+ io.err(`Did you mean ${formats.map((name) => `\`--format ${name}\``).join(", ")}?`);
7141
+ }
7050
7142
  return true;
7051
7143
  }
7052
7144
  function guardBrokenPipe(stream, quit) {
@@ -1406,6 +1406,30 @@ function declaredTypeOf(context, node) {
1406
1406
  const declared = definition && isVariableDeclarator(definition) ? definition.id : void 0;
1407
1407
  return declared && isIdentifier(declared) ? declared.typeAnnotation?.typeAnnotation : void 0;
1408
1408
  }
1409
+ function localFunction2(context, callee) {
1410
+ const scope = context.sourceCode.getScope(callee);
1411
+ const declared = findBinding(scope, callee.name)?.defs.map((definition) => definition.node).find(isFunctionNode);
1412
+ const bound = declared ?? boundValueOf(scope, callee);
1413
+ return bound && isFunctionNode(bound) ? bound : void 0;
1414
+ }
1415
+ function isAssignmentPattern(node) {
1416
+ return node.type === "AssignmentPattern";
1417
+ }
1418
+ function isAnnotatedParameter(node) {
1419
+ return node.type === "Identifier" || node.type === "ObjectPattern";
1420
+ }
1421
+ function parameterType(parameter) {
1422
+ const target = parameter && isAssignmentPattern(parameter) ? parameter.left : parameter;
1423
+ return target && isAnnotatedParameter(target) ? target.typeAnnotation?.typeAnnotation : void 0;
1424
+ }
1425
+ function declaredParameterTypeOf(context, node) {
1426
+ const call = node.parent;
1427
+ if (!isCallExpression(call) || !isIdentifier(call.callee)) {
1428
+ return void 0;
1429
+ }
1430
+ const helper = localFunction2(context, call.callee);
1431
+ return parameterType(helper?.params[call.arguments.indexOf(node)]);
1432
+ }
1409
1433
  function outermostData(node) {
1410
1434
  let current = node;
1411
1435
  for (; ; ) {
@@ -1426,7 +1450,7 @@ function isMemberStubValue(node) {
1426
1450
  }
1427
1451
  function isCheckedByDeclaration(context, node) {
1428
1452
  const data = outermostData(node);
1429
- const declared = declaredTypeOf(context, data);
1453
+ const declared = declaredTypeOf(context, data) ?? declaredParameterTypeOf(context, data);
1430
1454
  return isMemberStubValue(data) || declared !== void 0 && declared.type !== "TSTypeLiteral";
1431
1455
  }
1432
1456
  var SIGNAL_WRITERS = /* @__PURE__ */ new Set(["set", "update"]);
@@ -19,8 +19,8 @@ function isRecord(value) {
19
19
  var PERF_PROFILE_ENV = "VITEST_AUTO_SPY_PERF_PROFILE";
20
20
  var SAMPLING_MICROSECONDS = 500;
21
21
  function send(session, method, params = {}) {
22
- return new Promise((resolve, reject) => {
23
- session.post(method, params, (error, result) => error === null ? resolve(result) : reject(error));
22
+ return new Promise((resolve2, reject) => {
23
+ session.post(method, params, (error, result) => error === null ? resolve2(result) : reject(error));
24
24
  });
25
25
  }
26
26
  var written = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.27.0",
3
+ "version": "5.28.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",