@wrongstack/plugins 0.293.0 → 0.295.1

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.
Files changed (211) hide show
  1. package/README.md +7 -71
  2. package/dist/accessibility-auditor/index.d.ts +1 -1
  3. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  4. package/dist/accessibility-auditor.js +50 -33
  5. package/dist/agent-handoff/index.d.ts +1 -1
  6. package/dist/agent-handoff/index.d.ts.map +1 -1
  7. package/dist/agent-handoff.js +30 -1
  8. package/dist/api-compatibility-gate/index.d.ts +1 -1
  9. package/dist/api-compatibility-gate/index.d.ts.map +1 -1
  10. package/dist/api-compatibility-gate.js +21 -16
  11. package/dist/audit/index.d.ts +386 -0
  12. package/dist/audit/index.d.ts.map +1 -0
  13. package/dist/audit.d.ts +1 -0
  14. package/dist/audit.js +454 -0
  15. package/dist/auto-doc/index.d.ts +1 -1
  16. package/dist/auto-doc/index.d.ts.map +1 -1
  17. package/dist/auto-doc.js +3 -1
  18. package/dist/auto-escalate/index.d.ts +1 -1
  19. package/dist/auto-escalate/index.d.ts.map +1 -1
  20. package/dist/auto-i18n-extractor/index.d.ts +1 -1
  21. package/dist/auto-i18n-extractor/index.d.ts.map +1 -1
  22. package/dist/auto-i18n-extractor.js +7 -1
  23. package/dist/branch-guard/index.d.ts +1 -1
  24. package/dist/branch-guard/index.d.ts.map +1 -1
  25. package/dist/branch-guard.js +111 -1
  26. package/dist/catalog.d.ts +5 -30
  27. package/dist/catalog.d.ts.map +1 -1
  28. package/dist/changelog-writer/index.d.ts +1 -1
  29. package/dist/changelog-writer/index.d.ts.map +1 -1
  30. package/dist/changelog-writer.js +137 -3
  31. package/dist/checkpoint/index.d.ts +3 -1
  32. package/dist/checkpoint/index.d.ts.map +1 -1
  33. package/dist/checkpoint.js +41 -16
  34. package/dist/code-metrics/index.d.ts +1 -1
  35. package/dist/code-metrics/index.d.ts.map +1 -1
  36. package/dist/code-metrics.js +34 -29
  37. package/dist/commit-validator/index.d.ts +1 -1
  38. package/dist/commit-validator/index.d.ts.map +1 -1
  39. package/dist/commit-validator.js +17 -7
  40. package/dist/config-validator/index.d.ts +1 -1
  41. package/dist/config-validator/index.d.ts.map +1 -1
  42. package/dist/context-pins/index.d.ts +1 -1
  43. package/dist/context-pins/index.d.ts.map +1 -1
  44. package/dist/context-pins.js +6 -5
  45. package/dist/cost-tracker/index.d.ts +1 -1
  46. package/dist/cost-tracker/index.d.ts.map +1 -1
  47. package/dist/cost-tracker.js +109 -2
  48. package/dist/cron/index.d.ts +1 -1
  49. package/dist/cron/index.d.ts.map +1 -1
  50. package/dist/cron.js +1 -0
  51. package/dist/dead-code-detector/index.d.ts +1 -1
  52. package/dist/dead-code-detector/index.d.ts.map +1 -1
  53. package/dist/dead-code-detector.js +31 -18
  54. package/dist/dep-guard/index.d.ts +1 -1
  55. package/dist/dep-guard/index.d.ts.map +1 -1
  56. package/dist/dependency-vulnerability-gate/index.d.ts +1 -1
  57. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  58. package/dist/dependency-vulnerability-gate.js +67 -20
  59. package/dist/diff-summary/index.d.ts +1 -1
  60. package/dist/diff-summary/index.d.ts.map +1 -1
  61. package/dist/diff-summary.js +128 -30
  62. package/dist/doc-sync-guard/index.d.ts +1 -1
  63. package/dist/doc-sync-guard/index.d.ts.map +1 -1
  64. package/dist/doc-sync-guard.js +7 -1
  65. package/dist/duplicate-code-detector/index.d.ts +26 -1
  66. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  67. package/dist/duplicate-code-detector.js +280 -69
  68. package/dist/error-lens/index.d.ts +1 -1
  69. package/dist/error-lens/index.d.ts.map +1 -1
  70. package/dist/factories/index.d.ts +4 -0
  71. package/dist/factories/index.d.ts.map +1 -0
  72. package/dist/factories.d.ts +1 -0
  73. package/dist/factories.js +74 -0
  74. package/dist/feature-flag-tracker/index.d.ts +1 -1
  75. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  76. package/dist/feature-flag-tracker.js +59 -33
  77. package/dist/file-watcher/index.d.ts +1 -1
  78. package/dist/file-watcher/index.d.ts.map +1 -1
  79. package/dist/file-watcher.js +6 -7
  80. package/dist/format-on-save/index.d.ts +3 -1
  81. package/dist/format-on-save/index.d.ts.map +1 -1
  82. package/dist/format-on-save.js +289 -59
  83. package/dist/generated-plugin-exports.d.ts +65 -0
  84. package/dist/generated-plugin-exports.d.ts.map +1 -0
  85. package/dist/git-autocommit/index.d.ts +1 -1
  86. package/dist/git-autocommit/index.d.ts.map +1 -1
  87. package/dist/import-organizer/index.d.ts +1 -1
  88. package/dist/import-organizer/index.d.ts.map +1 -1
  89. package/dist/import-organizer.js +141 -20
  90. package/dist/index.d.ts +4 -183
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +18298 -17279
  93. package/dist/injection-shield/index.d.ts +1 -1
  94. package/dist/injection-shield/index.d.ts.map +1 -1
  95. package/dist/interface-contract-guard/index.d.ts +1 -1
  96. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  97. package/dist/interface-contract-guard.js +87 -53
  98. package/dist/knowledge-graph/index.d.ts +1 -1
  99. package/dist/knowledge-graph/index.d.ts.map +1 -1
  100. package/dist/knowledge-graph.js +7 -6
  101. package/dist/license-audit-gate/index.d.ts +1 -1
  102. package/dist/license-audit-gate/index.d.ts.map +1 -1
  103. package/dist/lint-gate/index.d.ts +1 -1
  104. package/dist/lint-gate/index.d.ts.map +1 -1
  105. package/dist/lint-gate.js +112 -1
  106. package/dist/llm-cache/index.d.ts +5 -1
  107. package/dist/llm-cache/index.d.ts.map +1 -1
  108. package/dist/llm-cache.js +16 -8
  109. package/dist/loop-breaker/index.d.ts +1 -1
  110. package/dist/loop-breaker/index.d.ts.map +1 -1
  111. package/dist/loop-breaker.js +25 -9
  112. package/dist/manifest/index.d.ts +16 -0
  113. package/dist/manifest/index.d.ts.map +1 -0
  114. package/dist/manifest.d.ts +1 -0
  115. package/dist/manifest.js +152 -0
  116. package/dist/migration-planner/index.d.ts +1 -1
  117. package/dist/migration-planner/index.d.ts.map +1 -1
  118. package/dist/migration-planner.js +5 -1
  119. package/dist/model-router/index.d.ts +1 -1
  120. package/dist/model-router/index.d.ts.map +1 -1
  121. package/dist/notify-hub/index.d.ts +1 -1
  122. package/dist/notify-hub/index.d.ts.map +1 -1
  123. package/dist/notify-hub/webhook-channel.d.ts +1 -1
  124. package/dist/notify-hub/webhook-channel.d.ts.map +1 -1
  125. package/dist/notify-hub.js +41 -4
  126. package/dist/path-guard/index.d.ts +1 -1
  127. package/dist/path-guard/index.d.ts.map +1 -1
  128. package/dist/performance-regression-gate/index.d.ts +1 -1
  129. package/dist/performance-regression-gate/index.d.ts.map +1 -1
  130. package/dist/plugin-stack-observer/index.d.ts +1 -1
  131. package/dist/plugin-stack-observer/index.d.ts.map +1 -1
  132. package/dist/pr-drafter/index.d.ts +1 -1
  133. package/dist/pr-drafter/index.d.ts.map +1 -1
  134. package/dist/pr-drafter.js +28 -29
  135. package/dist/process-guard/index.d.ts +1 -1
  136. package/dist/process-guard/index.d.ts.map +1 -1
  137. package/dist/prompt-firewall/index.d.ts +1 -1
  138. package/dist/prompt-firewall/index.d.ts.map +1 -1
  139. package/dist/prompt-firewall.js +140 -11
  140. package/dist/refactor-suggester/index.d.ts +1 -1
  141. package/dist/refactor-suggester/index.d.ts.map +1 -1
  142. package/dist/refactor-suggester.js +148 -33
  143. package/dist/release-notes-generator/index.d.ts +1 -1
  144. package/dist/release-notes-generator/index.d.ts.map +1 -1
  145. package/dist/release-notes-generator.js +29 -22
  146. package/dist/runtime/bounded-map.d.ts +86 -0
  147. package/dist/runtime/bounded-map.d.ts.map +1 -0
  148. package/dist/runtime/credential-patterns.d.ts +42 -0
  149. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  150. package/dist/runtime/index.d.ts +18 -0
  151. package/dist/runtime/index.d.ts.map +1 -1
  152. package/dist/runtime/llm.d.ts +1 -1
  153. package/dist/runtime/llm.d.ts.map +1 -1
  154. package/dist/runtime/local-bin.d.ts +120 -0
  155. package/dist/runtime/local-bin.d.ts.map +1 -0
  156. package/dist/runtime/safe-json.d.ts +25 -0
  157. package/dist/runtime/safe-json.d.ts.map +1 -0
  158. package/dist/runtime.js +332 -22
  159. package/dist/schema-evolution-guard/index.d.ts +1 -1
  160. package/dist/schema-evolution-guard/index.d.ts.map +1 -1
  161. package/dist/schema-evolution-guard.js +7 -1
  162. package/dist/secret-scanner/index.d.ts +1 -1
  163. package/dist/secret-scanner/index.d.ts.map +1 -1
  164. package/dist/secret-scanner.js +181 -48
  165. package/dist/security-hotspot-scanner/index.d.ts +1 -1
  166. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  167. package/dist/security-hotspot-scanner.js +158 -20
  168. package/dist/semantic-search-indexer/index.d.ts +1 -1
  169. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  170. package/dist/semantic-search-indexer.js +7 -1
  171. package/dist/semver-bump/index.d.ts +1 -1
  172. package/dist/semver-bump/index.d.ts.map +1 -1
  173. package/dist/semver-bump.js +97 -43
  174. package/dist/session-recap/index.d.ts +1 -1
  175. package/dist/session-recap/index.d.ts.map +1 -1
  176. package/dist/shell-check/index.d.ts +1 -1
  177. package/dist/shell-check/index.d.ts.map +1 -1
  178. package/dist/smart-rename/index.d.ts +1 -1
  179. package/dist/smart-rename/index.d.ts.map +1 -1
  180. package/dist/spec-linker/index.d.ts +1 -1
  181. package/dist/spec-linker/index.d.ts.map +1 -1
  182. package/dist/spec-linker.js +222 -20772
  183. package/dist/template-engine/index.d.ts +1 -1
  184. package/dist/template-engine/index.d.ts.map +1 -1
  185. package/dist/template-engine.js +34 -8
  186. package/dist/test-coverage-gate/index.d.ts +1 -1
  187. package/dist/test-coverage-gate/index.d.ts.map +1 -1
  188. package/dist/test-coverage-gate.js +34 -14
  189. package/dist/test-flake-detector/index.d.ts +1 -1
  190. package/dist/test-flake-detector/index.d.ts.map +1 -1
  191. package/dist/test-flake-detector.js +31 -20
  192. package/dist/test-generator/index.d.ts +1 -1
  193. package/dist/test-generator/index.d.ts.map +1 -1
  194. package/dist/test-runner-gate/index.d.ts +1 -1
  195. package/dist/test-runner-gate/index.d.ts.map +1 -1
  196. package/dist/test-runner-gate.js +132 -9
  197. package/dist/todo-listener/index.d.ts +1 -32
  198. package/dist/todo-listener/index.d.ts.map +1 -1
  199. package/dist/todo-listener.js +30 -1
  200. package/dist/todo-tracker/index.d.ts +1 -1
  201. package/dist/todo-tracker/index.d.ts.map +1 -1
  202. package/dist/todo-tracker.js +3 -4
  203. package/dist/token-budget/index.d.ts +1 -1
  204. package/dist/token-budget/index.d.ts.map +1 -1
  205. package/dist/token-throttle/index.d.ts +1 -1
  206. package/dist/token-throttle/index.d.ts.map +1 -1
  207. package/dist/token-throttle.js +17 -2
  208. package/dist/type-gate/index.d.ts +1 -1
  209. package/dist/type-gate/index.d.ts.map +1 -1
  210. package/dist/type-gate.js +106 -19
  211. package/package.json +80 -68
@@ -1,10 +1,14 @@
1
1
  // src/feature-flag-tracker/index.ts
2
- import { readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
4
4
 
5
5
  // src/runtime/index.ts
6
- import { existsSync, readdirSync, statSync } from "node:fs";
7
6
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
+
8
+ // src/runtime/local-bin.ts
9
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
10
+
11
+ // src/runtime/index.ts
8
12
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
9
13
  function hasLeadingDash(arg) {
10
14
  return arg.length > 0 && arg.startsWith("-");
@@ -17,44 +21,45 @@ function withinProjectPath(projectRoot, candidate) {
17
21
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
18
22
  }
19
23
  function withinProject(p) {
20
- return withinProjectPath(process.cwd(), p) || relative(process.cwd(), p) === ".";
24
+ const cwd = process.cwd();
25
+ return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
21
26
  }
22
27
  var DEFAULT_EXCLUDE_DIRS = ["node_modules", "dist", ".git", "coverage"];
23
- function collectSourceFiles(root, opts) {
28
+ async function collectSourceFilesAsync(root, opts) {
29
+ const { readdir, stat } = await import("node:fs/promises");
24
30
  const files = [];
25
- if (!existsSync(root)) return files;
26
- const s = statSync(root);
27
- if (s.isFile()) {
28
- if (matchesExtension(root, opts.extensions)) files.push(root);
31
+ try {
32
+ const s = await stat(root);
33
+ if (s.isFile()) {
34
+ if (matchesExtension(root, opts.extensions)) files.push(root);
35
+ return files;
36
+ }
37
+ if (!s.isDirectory()) return files;
38
+ } catch {
29
39
  return files;
30
40
  }
31
- if (!s.isDirectory()) return files;
32
41
  const exclude = opts.excludeDirs ?? DEFAULT_EXCLUDE_DIRS;
33
- function walk(dir, depth) {
42
+ const excludeSet = new Set(exclude);
43
+ async function walk(dir, depth) {
34
44
  if (opts.maxDepth !== void 0 && depth > opts.maxDepth) return;
35
45
  let entries;
36
46
  try {
37
- entries = readdirSync(dir);
47
+ entries = await readdir(dir, { withFileTypes: true });
38
48
  } catch {
39
49
  return;
40
50
  }
51
+ entries.sort((a, b) => a.name.localeCompare(b.name));
41
52
  for (const entry of entries) {
42
- if (exclude.includes(entry)) continue;
43
- const full = resolve(dir, entry);
44
- let st;
45
- try {
46
- st = statSync(full);
47
- } catch {
48
- continue;
49
- }
50
- if (st.isDirectory()) {
51
- walk(full, depth + 1);
52
- } else if (st.isFile() && matchesExtension(full, opts.extensions)) {
53
+ if (excludeSet.has(entry.name)) continue;
54
+ const full = resolve(dir, entry.name);
55
+ if (entry.isDirectory()) {
56
+ await walk(full, depth + 1);
57
+ } else if (entry.isFile() && matchesExtension(full, opts.extensions)) {
53
58
  files.push(full);
54
59
  }
55
60
  }
56
61
  }
57
- walk(root, 0);
62
+ await walk(root, 0);
58
63
  return files;
59
64
  }
60
65
  function matchesExtension(p, exts) {
@@ -103,8 +108,14 @@ function relativePath(p) {
103
108
  return toPosix(relative2(process.cwd(), p));
104
109
  }
105
110
  function compilePatterns(patterns) {
106
- const sources = [...DEFAULT_PATTERNS, ...patterns];
107
- return sources.map((src) => new RegExp(src, "g"));
111
+ const out = [];
112
+ for (const src of [...DEFAULT_PATTERNS, ...patterns]) {
113
+ try {
114
+ out.push(new RegExp(src, "g"));
115
+ } catch {
116
+ }
117
+ }
118
+ return out;
108
119
  }
109
120
  function scanFile(filePath, content, patterns, maxFindings) {
110
121
  const usages = [];
@@ -128,22 +139,33 @@ function scanFile(filePath, content, patterns, maxFindings) {
128
139
  }
129
140
  return usages.slice(0, maxFindings);
130
141
  }
131
- function scanPath(rawPath, cfg) {
142
+ async function scanPath(rawPath, cfg) {
132
143
  const root = process.cwd();
133
144
  const resolved = isAbsolute2(rawPath) ? resolve2(rawPath) : resolve2(root, rawPath);
134
145
  const exts = normalizeExtensions(cfg.extensions);
135
- const files = collectSourceFiles(resolved, { extensions: exts });
146
+ const files = await collectSourceFilesAsync(resolved, { extensions: exts });
136
147
  const patterns = compilePatterns(cfg.patterns);
137
148
  const usages = [];
149
+ let scannedFiles = 0;
150
+ let truncated = false;
138
151
  for (const p of files) {
139
152
  try {
140
- const content = readFileSync(p, "utf-8");
153
+ const content = await readFile(p, "utf-8");
154
+ scannedFiles += 1;
141
155
  usages.push(...scanFile(p, content, patterns, cfg.maxFindings));
142
- if (usages.length >= cfg.maxFindings) break;
156
+ if (usages.length >= cfg.maxFindings) {
157
+ truncated = scannedFiles < files.length;
158
+ break;
159
+ }
143
160
  } catch {
144
161
  }
145
162
  }
146
- return { usages: usages.slice(0, cfg.maxFindings), scannedFiles: files.length };
163
+ return {
164
+ usages: usages.slice(0, cfg.maxFindings),
165
+ scannedFiles,
166
+ discoveredFiles: files.length,
167
+ truncated
168
+ };
147
169
  }
148
170
  var plugin = {
149
171
  name: "feature-flag-tracker",
@@ -191,7 +213,7 @@ var plugin = {
191
213
  state.hookUnregister = null;
192
214
  }
193
215
  const cfg = readConfig(api.config.extensions?.["feature-flag-tracker"]);
194
- const hook = (input) => {
216
+ const hook = async (input) => {
195
217
  if (!cfg.enabled) return;
196
218
  if (input.toolResult?.isError) return;
197
219
  const inp = input.toolInput ?? {};
@@ -204,7 +226,7 @@ var plugin = {
204
226
  const resolved = resolve2(process.cwd(), sourcePath);
205
227
  let content;
206
228
  try {
207
- content = readFileSync(resolved, "utf-8");
229
+ content = await readFile(resolved, "utf-8");
208
230
  } catch {
209
231
  state.errorCount += 1;
210
232
  return;
@@ -242,7 +264,7 @@ Make sure flag behavior is intentional and consider updating flag inventory/docs
242
264
  state.scanCount += 1;
243
265
  let result;
244
266
  try {
245
- result = scanPath(rawPath, cfg);
267
+ result = await scanPath(rawPath, cfg);
246
268
  } catch (err) {
247
269
  state.errorCount += 1;
248
270
  return { ok: false, error: String(err) };
@@ -252,6 +274,10 @@ Make sure flag behavior is intentional and consider updating flag inventory/docs
252
274
  ok: true,
253
275
  path: relativePath(resolve2(process.cwd(), rawPath)),
254
276
  scannedFiles: result.scannedFiles,
277
+ discoveredFiles: result.discoveredFiles,
278
+ // Say so when the cap stopped the walk early: a partial scan
279
+ // that reports few findings must not read as a clean result.
280
+ truncated: result.truncated,
255
281
  usages: result.usages
256
282
  };
257
283
  }
@@ -6,7 +6,7 @@
6
6
  * - watch_stop: Stop a watch by ID
7
7
  * - watch_list: List all active watches
8
8
  */
9
- import type { Plugin } from '@wrongstack/core';
9
+ import type { Plugin } from '@wrongstack/core/types';
10
10
  declare const plugin: Plugin;
11
11
  export default plugin;
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file-watcher/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA2D/C,QAAA,MAAM,MAAM,EAAE,MAsYb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file-watcher/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAqErD,QAAA,MAAM,MAAM,EAAE,MA+Yb,CAAC;eAEa,MAAM"}
@@ -80,13 +80,12 @@ var plugin = {
80
80
  function debounceEvent(key, fn, ms) {
81
81
  const existing = debounceTimers.get(key);
82
82
  if (existing) clearTimeout(existing);
83
- debounceTimers.set(
84
- key,
85
- setTimeout(() => {
86
- debounceTimers.delete(key);
87
- fn();
88
- }, ms)
89
- );
83
+ const timer = setTimeout(() => {
84
+ debounceTimers.delete(key);
85
+ fn();
86
+ }, ms);
87
+ timer.unref?.();
88
+ debounceTimers.set(key, timer);
90
89
  }
91
90
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
92
91
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
@@ -27,7 +27,9 @@
27
27
  *
28
28
  * @public
29
29
  */
30
- import type { Plugin } from '@wrongstack/core';
30
+ import type { Plugin } from '@wrongstack/core/types';
31
+ /** Which formatter is currently in use — surfaced by health()/status. */
32
+ export declare function activeFormatterLabel(): string | null;
31
33
  declare const plugin: Plugin;
32
34
  export default plugin;
33
35
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/format-on-save/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA0P/C,QAAA,MAAM,MAAM,EAAE,MAuPb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/format-on-save/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAmPrD,yEAAyE;AACzE,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAEpD;AAsGD,QAAA,MAAM,MAAM,EAAE,MAsPb,CAAC;eAEa,MAAM"}
@@ -1,9 +1,200 @@
1
1
  // src/format-on-save/index.ts
2
2
  import { execFile } from "node:child_process";
3
- import { access, stat } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import { access, readFile, stat } from "node:fs/promises";
5
+
6
+ // src/runtime/index.ts
7
+ import { basename, extname as extname2, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
8
+
9
+ // src/runtime/local-bin.ts
10
+ import { createRequire } from "node:module";
11
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { accessSync, constants, readFileSync } from "node:fs";
13
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
14
+ function resolveExecInvocation(command, args = []) {
15
+ const resolved = resolveWin32Command(command);
16
+ const normalizedResolved = resolved.toLowerCase();
17
+ const needsShell = process.platform === "win32" && (normalizedResolved.endsWith(".cmd") || normalizedResolved.endsWith(".bat"));
18
+ if (needsShell) {
19
+ const shim = buildWin32CmdShimInvocation(resolved, args);
20
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
21
+ }
22
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
23
+ }
24
+ function findOnPath(cmd) {
25
+ if (!cmd) return null;
26
+ const exists = (p) => {
27
+ try {
28
+ accessSync(p, constants.X_OK);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+ if (cmd.includes("/") || cmd.includes("\\")) {
35
+ return exists(cmd) ? resolve(cmd) : null;
36
+ }
37
+ const suffixes = process.platform === "win32" && extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
38
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
39
+ if (!dir) continue;
40
+ const base = join(dir, cmd);
41
+ for (const suffix of suffixes) {
42
+ const candidate = `${base}${suffix}`;
43
+ if (exists(candidate)) return candidate;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function isInside(parent, candidate) {
49
+ const rel = relative(parent, candidate);
50
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
51
+ }
52
+ var binCache = /* @__PURE__ */ new Map();
53
+ var BIN_CACHE_MAX = 64;
54
+ var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
55
+ function cachePut(key, value) {
56
+ while (binCache.size >= BIN_CACHE_MAX) {
57
+ const oldest = binCache.keys().next().value;
58
+ if (oldest === void 0) break;
59
+ binCache.delete(oldest);
60
+ }
61
+ binCache.set(key, { value, cachedAt: Date.now() });
62
+ return value;
63
+ }
64
+ function clearLocalBinCache() {
65
+ binCache.clear();
66
+ }
67
+ function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
68
+ const key = `${packageName}|${binName}|${cwd}`;
69
+ const cached = binCache.get(key);
70
+ if (cached !== void 0) {
71
+ if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
72
+ return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
73
+ }
74
+ binCache.delete(key);
75
+ }
76
+ let resolved = null;
77
+ try {
78
+ const requireFromProject = createRequire(resolve(cwd, "package.json"));
79
+ const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
80
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
81
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
82
+ if (relativeBin && !isAbsolute(relativeBin)) {
83
+ const packageDir = dirname(packagePath);
84
+ const entry = resolve(packageDir, relativeBin);
85
+ if (isInside(packageDir, entry)) {
86
+ resolved = { cmd: process.execPath, args: [entry], entry };
87
+ }
88
+ }
89
+ } catch {
90
+ resolved = null;
91
+ }
92
+ cachePut(key, resolved);
93
+ return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
94
+ }
95
+ function resolveFirstNodeBin(candidates, cwd) {
96
+ for (const c of candidates) {
97
+ const hit = resolveNodeBin(c.packageName, c.binName, cwd, c.args ?? []);
98
+ if (hit) return { ...hit, packageName: c.packageName, binName: c.binName };
99
+ }
100
+ return null;
101
+ }
102
+
103
+ // src/runtime/bounded-map.ts
104
+ var BoundedMap = class {
105
+ map = /* @__PURE__ */ new Map();
106
+ max;
107
+ ttlMs;
108
+ now;
109
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
110
+ evictions = 0;
111
+ constructor(options) {
112
+ const normalizedMax = Math.floor(options.max);
113
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
114
+ this.ttlMs = options.ttlMs;
115
+ this.now = options.now ?? Date.now;
116
+ }
117
+ expired(entry) {
118
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
119
+ }
120
+ get(key) {
121
+ const entry = this.map.get(key);
122
+ if (entry === void 0) return void 0;
123
+ if (this.expired(entry)) {
124
+ this.map.delete(key);
125
+ return void 0;
126
+ }
127
+ this.map.delete(key);
128
+ this.map.set(key, entry);
129
+ return entry.value;
130
+ }
131
+ /**
132
+ * Read without promoting the key to most-recently-used. Use for
133
+ * diagnostics that must not perturb the eviction order.
134
+ */
135
+ peek(key) {
136
+ const entry = this.map.get(key);
137
+ if (entry === void 0 || this.expired(entry)) return void 0;
138
+ return entry.value;
139
+ }
140
+ has(key) {
141
+ const entry = this.map.get(key);
142
+ if (entry === void 0) return false;
143
+ if (this.expired(entry)) {
144
+ this.map.delete(key);
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+ set(key, value) {
150
+ this.map.delete(key);
151
+ this.map.set(key, { value, storedAt: this.now() });
152
+ while (this.map.size > this.max) {
153
+ const coldest = this.map.keys().next().value;
154
+ if (coldest === void 0) break;
155
+ this.map.delete(coldest);
156
+ this.evictions += 1;
157
+ }
158
+ return this;
159
+ }
160
+ delete(key) {
161
+ return this.map.delete(key);
162
+ }
163
+ clear() {
164
+ this.map.clear();
165
+ this.evictions = 0;
166
+ }
167
+ get size() {
168
+ return this.map.size;
169
+ }
170
+ /** How many entries have been dropped to respect `max`, since the last clear. */
171
+ get evictionCount() {
172
+ return this.evictions;
173
+ }
174
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
175
+ prune() {
176
+ if (this.ttlMs === void 0) return 0;
177
+ let removed = 0;
178
+ for (const [key, entry] of this.map) {
179
+ if (this.expired(entry)) {
180
+ this.map.delete(key);
181
+ removed += 1;
182
+ }
183
+ }
184
+ return removed;
185
+ }
186
+ /** Live (non-expired) entries, coldest first. */
187
+ *entries() {
188
+ for (const [key, entry] of this.map) {
189
+ if (!this.expired(entry)) yield [key, entry.value];
190
+ }
191
+ }
192
+ [Symbol.iterator]() {
193
+ return this.entries();
194
+ }
195
+ };
4
196
 
5
197
  // src/runtime/index.ts
6
- import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
198
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
8
199
  function hasLeadingDash(arg) {
9
200
  return arg.length > 0 && arg.startsWith("-");
@@ -11,12 +202,13 @@ function hasLeadingDash(arg) {
11
202
  function withinProjectPath(projectRoot, candidate) {
12
203
  if (candidate.length === 0 || candidate.length > 4096) return false;
13
204
  if (hasLeadingDash(candidate)) return false;
14
- const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
15
- const rel = relative(projectRoot, resolved);
16
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
205
+ const resolved = isAbsolute2(candidate) ? resolve2(candidate) : resolve2(projectRoot, candidate);
206
+ const rel = relative2(projectRoot, resolved);
207
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
17
208
  }
18
209
  function withinProject(p) {
19
- return withinProjectPath(process.cwd(), p) || relative(process.cwd(), p) === ".";
210
+ const cwd = process.cwd();
211
+ return withinProjectPath(cwd, p) || relative2(cwd, p) === ".";
20
212
  }
21
213
 
22
214
  // src/format-on-save/index.ts
@@ -55,7 +247,7 @@ function readConfig(raw) {
55
247
  skipTtlMs: typeof r["skipTtlMs"] === "number" && r["skipTtlMs"] >= 0 ? r["skipTtlMs"] : DEFAULTS.skipTtlMs
56
248
  };
57
249
  }
58
- var recentlyCovered = /* @__PURE__ */ new Map();
250
+ var recentlyCovered = new BoundedMap({ max: 256 });
59
251
  function clearRegistrations() {
60
252
  if (state.hookUnregister) {
61
253
  try {
@@ -78,6 +270,55 @@ function evictExpired(ttlMs) {
78
270
  if (ts < cutoff) recentlyCovered.delete(path);
79
271
  }
80
272
  }
273
+ var FORMATTERS = [
274
+ {
275
+ packageName: "@biomejs/biome",
276
+ binName: "biome",
277
+ writeArgs: ["format", "--write"],
278
+ label: "biome"
279
+ },
280
+ { packageName: "prettier", binName: "prettier", writeArgs: ["--write"], label: "prettier" },
281
+ { packageName: "dprint", binName: "dprint", writeArgs: ["fmt"], label: "dprint" }
282
+ ];
283
+ var activeFormatter;
284
+ function resetFormatter() {
285
+ activeFormatter = void 0;
286
+ clearLocalBinCache();
287
+ }
288
+ function resolveFormatter() {
289
+ if (activeFormatter !== void 0) return activeFormatter;
290
+ const cwd = process.cwd();
291
+ for (const f of FORMATTERS) {
292
+ const local = resolveNodeBinFor(f, cwd);
293
+ if (local) {
294
+ activeFormatter = local;
295
+ return local;
296
+ }
297
+ }
298
+ for (const f of FORMATTERS) {
299
+ const onPath = findOnPath(f.binName);
300
+ if (!onPath) continue;
301
+ activeFormatter = { cmd: onPath, args: [...f.writeArgs], label: f.label };
302
+ return activeFormatter;
303
+ }
304
+ activeFormatter = null;
305
+ return null;
306
+ }
307
+ function resolveNodeBinFor(f, cwd) {
308
+ const hit = resolveFirstNodeBin([{ packageName: f.packageName, binName: f.binName }], cwd);
309
+ if (!hit) return null;
310
+ return { cmd: hit.cmd, args: [...hit.args, ...f.writeArgs], label: f.label };
311
+ }
312
+ function activeFormatterLabel() {
313
+ return activeFormatter?.label ?? null;
314
+ }
315
+ async function sha256File(filePath) {
316
+ try {
317
+ return createHash("sha256").update(await readFile(filePath)).digest("hex");
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
81
322
  async function formatFile(filePath, timeoutMs) {
82
323
  if (!withinProject(filePath)) return null;
83
324
  try {
@@ -85,30 +326,40 @@ async function formatFile(filePath, timeoutMs) {
85
326
  } catch {
86
327
  return null;
87
328
  }
329
+ const formatter = resolveFormatter();
330
+ if (!formatter) return null;
88
331
  let bytesBefore;
89
332
  try {
90
333
  bytesBefore = (await stat(filePath)).size;
91
334
  } catch {
92
335
  return null;
93
336
  }
337
+ const hashBefore = await sha256File(filePath);
338
+ let invocation;
339
+ try {
340
+ invocation = resolveExecInvocation(formatter.cmd, [...formatter.args, filePath]);
341
+ } catch {
342
+ return null;
343
+ }
94
344
  try {
95
- await new Promise((resolve2, reject) => {
345
+ await new Promise((resolve3, reject) => {
96
346
  execFile(
97
- "npx",
98
- ["biome", "format", "--write", filePath],
347
+ invocation.cmd,
348
+ invocation.args,
99
349
  {
100
350
  encoding: "utf-8",
101
351
  timeout: timeoutMs,
102
352
  cwd: process.cwd(),
103
353
  windowsHide: true,
104
- maxBuffer: 16 * 1024 * 1024
354
+ maxBuffer: 16 * 1024 * 1024,
355
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
105
356
  },
106
357
  (err) => {
107
358
  if (err) {
108
359
  const e = err;
109
360
  if (e.killed) return reject(err);
110
361
  }
111
- resolve2();
362
+ resolve3();
112
363
  }
113
364
  );
114
365
  });
@@ -124,28 +375,11 @@ async function formatFile(filePath, timeoutMs) {
124
375
  if (bytesAfter !== bytesBefore) {
125
376
  return { changed: true, bytesBefore, bytesAfter };
126
377
  }
127
- try {
128
- await new Promise((resolve2, reject) => {
129
- execFile(
130
- "npx",
131
- ["biome", "format", filePath],
132
- {
133
- encoding: "utf-8",
134
- timeout: timeoutMs,
135
- cwd: process.cwd(),
136
- windowsHide: true,
137
- maxBuffer: 16 * 1024 * 1024
138
- },
139
- (err) => {
140
- if (err) return reject(err);
141
- resolve2();
142
- }
143
- );
144
- });
378
+ const hashAfter = await sha256File(filePath);
379
+ if (hashBefore === null || hashAfter === null) {
145
380
  return { changed: false, bytesBefore, bytesAfter };
146
- } catch {
147
- return { changed: true, bytesBefore, bytesAfter };
148
381
  }
382
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
149
383
  }
150
384
  var plugin = {
151
385
  name: "format-on-save",
@@ -191,32 +425,18 @@ var plugin = {
191
425
  state.lastResult = null;
192
426
  recentlyCovered.clear();
193
427
  const cfg = readConfig(api.config.extensions?.["format-on-save"]);
194
- let biomeAvailable = false;
195
- try {
196
- await new Promise((resolve2, reject) => {
197
- execFile(
198
- "npx",
199
- ["biome", "--version"],
200
- {
201
- encoding: "utf-8",
202
- timeout: 5e3,
203
- cwd: process.cwd(),
204
- windowsHide: true
205
- },
206
- (err) => {
207
- if (err) reject(err);
208
- else resolve2();
209
- }
210
- );
211
- });
212
- biomeAvailable = true;
213
- api.log.info("format-on-save: biome detected");
214
- } catch {
215
- biomeAvailable = false;
216
- api.log.warn("format-on-save: biome not found \u2014 hook will be a no-op");
428
+ resetFormatter();
429
+ const formatter = resolveFormatter();
430
+ const formatterAvailable = formatter !== null;
431
+ if (formatter) {
432
+ api.log.info(`format-on-save: ${formatter.label} detected`);
433
+ } else {
434
+ api.log.warn(
435
+ "format-on-save: no formatter found (biome, prettier, dprint) \u2014 hook will be a no-op"
436
+ );
217
437
  }
218
438
  const hook = async (input) => {
219
- if (!cfg.enabled || !biomeAvailable) return;
439
+ if (!cfg.enabled || !formatterAvailable) return;
220
440
  if (input.toolResult?.isError) return;
221
441
  const toolName = input.toolName ?? "";
222
442
  const inp = input.toolInput ?? {};
@@ -277,7 +497,7 @@ var plugin = {
277
497
  );
278
498
  api.tools.register({
279
499
  name: "format_on_save_status",
280
- description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
500
+ description: "Reports format-on-save state: which formatter is active, and per-session formatted/clean/error/skipped counters.",
281
501
  inputSchema: { type: "object", properties: {} },
282
502
  permission: "auto",
283
503
  category: "Code Quality",
@@ -286,7 +506,14 @@ var plugin = {
286
506
  return {
287
507
  ok: true,
288
508
  enabled: cfg.enabled,
289
- biomeAvailable,
509
+ formatterAvailable,
510
+ formatter: activeFormatterLabel(),
511
+ /**
512
+ * Back-compat alias for the pre-multi-formatter status shape.
513
+ * Now accurate rather than assumed: true only when the resolved
514
+ * formatter really is biome.
515
+ */
516
+ biomeAvailable: activeFormatterLabel() === "biome",
290
517
  timeoutMs: cfg.timeoutMs,
291
518
  skipWhenCoveredBy: cfg.skipWhenCoveredBy,
292
519
  skipTtlMs: cfg.skipTtlMs,
@@ -304,11 +531,13 @@ var plugin = {
304
531
  api.log.info("format-on-save plugin loaded", {
305
532
  version: "0.1.0",
306
533
  enabled: cfg.enabled,
307
- biomeAvailable
534
+ formatterAvailable,
535
+ formatter: activeFormatterLabel()
308
536
  });
309
537
  },
310
538
  teardown(api) {
311
539
  clearRegistrations();
540
+ resetFormatter();
312
541
  const final = {
313
542
  invocations: state.invocationCount,
314
543
  formatted: state.formattedCount,
@@ -342,5 +571,6 @@ var plugin = {
342
571
  };
343
572
  var format_on_save_default = plugin;
344
573
  export {
574
+ activeFormatterLabel,
345
575
  format_on_save_default as default
346
576
  };