@wrongstack/plugins 0.295.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 (90) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +19 -2
  3. package/dist/agent-handoff/index.d.ts.map +1 -1
  4. package/dist/agent-handoff.js +30 -1
  5. package/dist/auto-doc/index.d.ts.map +1 -1
  6. package/dist/auto-doc.js +3 -1
  7. package/dist/auto-i18n-extractor.js +5 -0
  8. package/dist/branch-guard/index.d.ts.map +1 -1
  9. package/dist/branch-guard.js +103 -1
  10. package/dist/changelog-writer/index.d.ts.map +1 -1
  11. package/dist/changelog-writer.js +137 -3
  12. package/dist/checkpoint/index.d.ts +2 -0
  13. package/dist/checkpoint/index.d.ts.map +1 -1
  14. package/dist/checkpoint.js +34 -8
  15. package/dist/code-metrics.js +5 -0
  16. package/dist/commit-validator/index.d.ts.map +1 -1
  17. package/dist/commit-validator.js +17 -7
  18. package/dist/context-pins/index.d.ts.map +1 -1
  19. package/dist/context-pins.js +6 -5
  20. package/dist/cost-tracker/index.d.ts.map +1 -1
  21. package/dist/cost-tracker.js +101 -1
  22. package/dist/cron/index.d.ts.map +1 -1
  23. package/dist/cron.js +1 -0
  24. package/dist/dead-code-detector.js +5 -0
  25. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  26. package/dist/dependency-vulnerability-gate.js +43 -8
  27. package/dist/diff-summary/index.d.ts.map +1 -1
  28. package/dist/diff-summary.js +100 -1
  29. package/dist/doc-sync-guard.js +5 -0
  30. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  31. package/dist/duplicate-code-detector.js +102 -2
  32. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  33. package/dist/feature-flag-tracker.js +30 -4
  34. package/dist/file-watcher/index.d.ts.map +1 -1
  35. package/dist/file-watcher.js +6 -7
  36. package/dist/format-on-save/index.d.ts +2 -0
  37. package/dist/format-on-save/index.d.ts.map +1 -1
  38. package/dist/format-on-save.js +288 -59
  39. package/dist/import-organizer/index.d.ts.map +1 -1
  40. package/dist/import-organizer.js +140 -20
  41. package/dist/index.js +1265 -544
  42. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  43. package/dist/interface-contract-guard.js +60 -26
  44. package/dist/knowledge-graph/index.d.ts.map +1 -1
  45. package/dist/knowledge-graph.js +7 -6
  46. package/dist/lint-gate/index.d.ts.map +1 -1
  47. package/dist/lint-gate.js +103 -1
  48. package/dist/loop-breaker/index.d.ts.map +1 -1
  49. package/dist/loop-breaker.js +15 -5
  50. package/dist/migration-planner.js +3 -0
  51. package/dist/notify-hub/index.d.ts.map +1 -1
  52. package/dist/notify-hub.js +41 -4
  53. package/dist/prompt-firewall/index.d.ts.map +1 -1
  54. package/dist/prompt-firewall.js +140 -11
  55. package/dist/refactor-suggester/index.d.ts.map +1 -1
  56. package/dist/refactor-suggester.js +119 -4
  57. package/dist/runtime/bounded-map.d.ts +86 -0
  58. package/dist/runtime/bounded-map.d.ts.map +1 -0
  59. package/dist/runtime/credential-patterns.d.ts +42 -0
  60. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  61. package/dist/runtime/index.d.ts +3 -0
  62. package/dist/runtime/index.d.ts.map +1 -1
  63. package/dist/runtime/local-bin.d.ts +120 -0
  64. package/dist/runtime/local-bin.d.ts.map +1 -0
  65. package/dist/runtime/safe-json.d.ts +25 -0
  66. package/dist/runtime/safe-json.d.ts.map +1 -0
  67. package/dist/runtime.js +291 -22
  68. package/dist/schema-evolution-guard.js +5 -0
  69. package/dist/secret-scanner/index.d.ts.map +1 -1
  70. package/dist/secret-scanner.js +175 -48
  71. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  72. package/dist/security-hotspot-scanner.js +132 -1
  73. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  74. package/dist/semantic-search-indexer.js +7 -1
  75. package/dist/semver-bump/index.d.ts.map +1 -1
  76. package/dist/semver-bump.js +21 -2
  77. package/dist/template-engine/index.d.ts.map +1 -1
  78. package/dist/template-engine.js +30 -2
  79. package/dist/test-coverage-gate.js +5 -0
  80. package/dist/test-runner-gate/index.d.ts.map +1 -1
  81. package/dist/test-runner-gate.js +130 -8
  82. package/dist/todo-listener/index.d.ts.map +1 -1
  83. package/dist/todo-listener.js +30 -1
  84. package/dist/todo-tracker/index.d.ts.map +1 -1
  85. package/dist/todo-tracker.js +3 -4
  86. package/dist/token-throttle/index.d.ts.map +1 -1
  87. package/dist/token-throttle.js +17 -2
  88. package/dist/type-gate/index.d.ts.map +1 -1
  89. package/dist/type-gate.js +96 -16
  90. package/package.json +3 -3
@@ -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,13 +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
210
  const cwd = process.cwd();
20
- return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
211
+ return withinProjectPath(cwd, p) || relative2(cwd, p) === ".";
21
212
  }
22
213
 
23
214
  // src/format-on-save/index.ts
@@ -56,7 +247,7 @@ function readConfig(raw) {
56
247
  skipTtlMs: typeof r["skipTtlMs"] === "number" && r["skipTtlMs"] >= 0 ? r["skipTtlMs"] : DEFAULTS.skipTtlMs
57
248
  };
58
249
  }
59
- var recentlyCovered = /* @__PURE__ */ new Map();
250
+ var recentlyCovered = new BoundedMap({ max: 256 });
60
251
  function clearRegistrations() {
61
252
  if (state.hookUnregister) {
62
253
  try {
@@ -79,6 +270,55 @@ function evictExpired(ttlMs) {
79
270
  if (ts < cutoff) recentlyCovered.delete(path);
80
271
  }
81
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
+ }
82
322
  async function formatFile(filePath, timeoutMs) {
83
323
  if (!withinProject(filePath)) return null;
84
324
  try {
@@ -86,30 +326,40 @@ async function formatFile(filePath, timeoutMs) {
86
326
  } catch {
87
327
  return null;
88
328
  }
329
+ const formatter = resolveFormatter();
330
+ if (!formatter) return null;
89
331
  let bytesBefore;
90
332
  try {
91
333
  bytesBefore = (await stat(filePath)).size;
92
334
  } catch {
93
335
  return null;
94
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
+ }
95
344
  try {
96
- await new Promise((resolve2, reject) => {
345
+ await new Promise((resolve3, reject) => {
97
346
  execFile(
98
- "npx",
99
- ["biome", "format", "--write", filePath],
347
+ invocation.cmd,
348
+ invocation.args,
100
349
  {
101
350
  encoding: "utf-8",
102
351
  timeout: timeoutMs,
103
352
  cwd: process.cwd(),
104
353
  windowsHide: true,
105
- maxBuffer: 16 * 1024 * 1024
354
+ maxBuffer: 16 * 1024 * 1024,
355
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
106
356
  },
107
357
  (err) => {
108
358
  if (err) {
109
359
  const e = err;
110
360
  if (e.killed) return reject(err);
111
361
  }
112
- resolve2();
362
+ resolve3();
113
363
  }
114
364
  );
115
365
  });
@@ -125,28 +375,11 @@ async function formatFile(filePath, timeoutMs) {
125
375
  if (bytesAfter !== bytesBefore) {
126
376
  return { changed: true, bytesBefore, bytesAfter };
127
377
  }
128
- try {
129
- await new Promise((resolve2, reject) => {
130
- execFile(
131
- "npx",
132
- ["biome", "format", filePath],
133
- {
134
- encoding: "utf-8",
135
- timeout: timeoutMs,
136
- cwd: process.cwd(),
137
- windowsHide: true,
138
- maxBuffer: 16 * 1024 * 1024
139
- },
140
- (err) => {
141
- if (err) return reject(err);
142
- resolve2();
143
- }
144
- );
145
- });
378
+ const hashAfter = await sha256File(filePath);
379
+ if (hashBefore === null || hashAfter === null) {
146
380
  return { changed: false, bytesBefore, bytesAfter };
147
- } catch {
148
- return { changed: true, bytesBefore, bytesAfter };
149
381
  }
382
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
150
383
  }
151
384
  var plugin = {
152
385
  name: "format-on-save",
@@ -192,32 +425,18 @@ var plugin = {
192
425
  state.lastResult = null;
193
426
  recentlyCovered.clear();
194
427
  const cfg = readConfig(api.config.extensions?.["format-on-save"]);
195
- let biomeAvailable = false;
196
- try {
197
- await new Promise((resolve2, reject) => {
198
- execFile(
199
- "npx",
200
- ["biome", "--version"],
201
- {
202
- encoding: "utf-8",
203
- timeout: 5e3,
204
- cwd: process.cwd(),
205
- windowsHide: true
206
- },
207
- (err) => {
208
- if (err) reject(err);
209
- else resolve2();
210
- }
211
- );
212
- });
213
- biomeAvailable = true;
214
- api.log.info("format-on-save: biome detected");
215
- } catch {
216
- biomeAvailable = false;
217
- 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
+ );
218
437
  }
219
438
  const hook = async (input) => {
220
- if (!cfg.enabled || !biomeAvailable) return;
439
+ if (!cfg.enabled || !formatterAvailable) return;
221
440
  if (input.toolResult?.isError) return;
222
441
  const toolName = input.toolName ?? "";
223
442
  const inp = input.toolInput ?? {};
@@ -278,7 +497,7 @@ var plugin = {
278
497
  );
279
498
  api.tools.register({
280
499
  name: "format_on_save_status",
281
- 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.",
282
501
  inputSchema: { type: "object", properties: {} },
283
502
  permission: "auto",
284
503
  category: "Code Quality",
@@ -287,7 +506,14 @@ var plugin = {
287
506
  return {
288
507
  ok: true,
289
508
  enabled: cfg.enabled,
290
- 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",
291
517
  timeoutMs: cfg.timeoutMs,
292
518
  skipWhenCoveredBy: cfg.skipWhenCoveredBy,
293
519
  skipTtlMs: cfg.skipTtlMs,
@@ -305,11 +531,13 @@ var plugin = {
305
531
  api.log.info("format-on-save plugin loaded", {
306
532
  version: "0.1.0",
307
533
  enabled: cfg.enabled,
308
- biomeAvailable
534
+ formatterAvailable,
535
+ formatter: activeFormatterLabel()
309
536
  });
310
537
  },
311
538
  teardown(api) {
312
539
  clearRegistrations();
540
+ resetFormatter();
313
541
  const final = {
314
542
  invocations: state.invocationCount,
315
543
  formatted: state.formattedCount,
@@ -343,5 +571,6 @@ var plugin = {
343
571
  };
344
572
  var format_on_save_default = plugin;
345
573
  export {
574
+ activeFormatterLabel,
346
575
  format_on_save_default as default
347
576
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import-organizer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAoRrD,QAAA,MAAM,MAAM,EAAE,MA8Nb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import-organizer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAiXrD,QAAA,MAAM,MAAM,EAAE,MA8Nb,CAAC;eAEa,MAAM"}
@@ -1,10 +1,72 @@
1
1
  // src/import-organizer/index.ts
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync, statSync } from "node:fs";
4
- import { basename as basename2, isAbsolute as isAbsolute2 } from "node:path";
4
+ import { basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
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 isInside(parent, candidate) {
25
+ const rel = relative(parent, candidate);
26
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
27
+ }
28
+ var binCache = /* @__PURE__ */ new Map();
29
+ var BIN_CACHE_MAX = 64;
30
+ var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
31
+ function cachePut(key, value) {
32
+ while (binCache.size >= BIN_CACHE_MAX) {
33
+ const oldest = binCache.keys().next().value;
34
+ if (oldest === void 0) break;
35
+ binCache.delete(oldest);
36
+ }
37
+ binCache.set(key, { value, cachedAt: Date.now() });
38
+ return value;
39
+ }
40
+ function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
41
+ const key = `${packageName}|${binName}|${cwd}`;
42
+ const cached = binCache.get(key);
43
+ if (cached !== void 0) {
44
+ if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
45
+ return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
46
+ }
47
+ binCache.delete(key);
48
+ }
49
+ let resolved = null;
50
+ try {
51
+ const requireFromProject = createRequire(resolve(cwd, "package.json"));
52
+ const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
53
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
54
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
55
+ if (relativeBin && !isAbsolute(relativeBin)) {
56
+ const packageDir = dirname(packagePath);
57
+ const entry = resolve(packageDir, relativeBin);
58
+ if (isInside(packageDir, entry)) {
59
+ resolved = { cmd: process.execPath, args: [entry], entry };
60
+ }
61
+ }
62
+ } catch {
63
+ resolved = null;
64
+ }
65
+ cachePut(key, resolved);
66
+ return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
67
+ }
5
68
 
6
69
  // src/runtime/index.ts
7
- import { basename, extname, isAbsolute, relative, resolve } from "node:path";
8
70
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
9
71
  function hasLeadingDash(arg) {
10
72
  return arg.length > 0 && arg.startsWith("-");
@@ -12,13 +74,13 @@ function hasLeadingDash(arg) {
12
74
  function withinProjectPath(projectRoot, candidate) {
13
75
  if (candidate.length === 0 || candidate.length > 4096) return false;
14
76
  if (hasLeadingDash(candidate)) return false;
15
- const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
16
- const rel = relative(projectRoot, resolved);
17
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
77
+ const resolved = isAbsolute2(candidate) ? resolve2(candidate) : resolve2(projectRoot, candidate);
78
+ const rel = relative2(projectRoot, resolved);
79
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
18
80
  }
19
81
  function withinProject(p) {
20
82
  const cwd = process.cwd();
21
- return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
83
+ return withinProjectPath(cwd, p) || relative2(cwd, p) === ".";
22
84
  }
23
85
 
24
86
  // src/import-organizer/index.ts
@@ -30,23 +92,51 @@ var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
30
92
  "biome",
31
93
  "@biomejs/biome",
32
94
  "eslint",
95
+ "oxlint",
33
96
  "node"
34
97
  // Recognise a fully-qualified project-local path like
35
98
  // "./node_modules/.bin/biome"; basename check kicks in below.
36
99
  ]);
100
+ var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "npm", "yarn"]);
101
+ var LOCAL_BIN_PACKAGES = {
102
+ biome: { packageName: "@biomejs/biome", binName: "biome" },
103
+ "@biomejs/biome": { packageName: "@biomejs/biome", binName: "biome" },
104
+ eslint: { packageName: "eslint", binName: "eslint" },
105
+ oxlint: { packageName: "oxlint", binName: "oxlint" }
106
+ };
107
+ function resolveLocalToolCommand(tokens) {
108
+ if (tokens.length === 0) return null;
109
+ let i = 0;
110
+ if (PACKAGE_RUNNERS.has(tokens[i])) {
111
+ i++;
112
+ while (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx" || tokens[i] === "run")) {
113
+ i++;
114
+ }
115
+ }
116
+ const toolToken = tokens[i];
117
+ if (!toolToken) return null;
118
+ const rest = tokens.slice(i + 1);
119
+ const pkg = LOCAL_BIN_PACKAGES[toolToken];
120
+ if (!pkg) return null;
121
+ const local = resolveNodeBin(pkg.packageName, pkg.binName, process.cwd(), rest);
122
+ if (!local) return null;
123
+ return { cmd: local.cmd, args: local.args };
124
+ }
37
125
  function resolveAllowedCommand(command) {
38
126
  const tokens = command.split(/\s+/).filter(Boolean);
39
127
  if (tokens.length === 0) return null;
40
128
  const head = tokens[0];
129
+ const local = ALLOWED_FIRST_TOKENS.has(head) ? resolveLocalToolCommand(tokens) : null;
130
+ if (local) return local;
41
131
  if (ALLOWED_FIRST_TOKENS.has(head)) {
42
132
  return { cmd: head, args: tokens.slice(1) };
43
133
  }
44
- if (isAbsolute2(head)) {
134
+ if (isAbsolute3(head)) {
45
135
  if (!withinProject(head)) return null;
46
136
  const base = basename2(head);
47
137
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
48
138
  }
49
- if (!isAbsolute2(head) && withinProject(head)) {
139
+ if (!isAbsolute3(head) && withinProject(head)) {
50
140
  const base = basename2(head);
51
141
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
52
142
  }
@@ -81,40 +171,70 @@ function readConfig(raw) {
81
171
  notifyFormatOnSave: r["notifyFormatOnSave"] !== false
82
172
  };
83
173
  }
174
+ var MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
84
175
  function runCommand(command, args, timeoutMs, cwd) {
85
- return new Promise((resolve2) => {
176
+ return new Promise((resolve3) => {
86
177
  let timedOut = false;
178
+ let settled = false;
179
+ const settle = (r) => {
180
+ if (settled) return;
181
+ settled = true;
182
+ resolve3(r);
183
+ };
87
184
  const stdoutChunks = [];
88
185
  const stderrChunks = [];
186
+ let stdoutBytes = 0;
187
+ let stderrBytes = 0;
188
+ const signal = AbortSignal.timeout(timeoutMs);
189
+ const onAbort = () => {
190
+ timedOut = true;
191
+ settle({ code: null, stdout: "", stderr: "", timedOut: true });
192
+ };
193
+ signal.addEventListener("abort", onAbort, { once: true });
89
194
  let child;
195
+ let invocation;
90
196
  try {
91
- child = spawn(command, args, {
197
+ invocation = resolveExecInvocation(command, args);
198
+ } catch {
199
+ signal.removeEventListener("abort", onAbort);
200
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
201
+ return;
202
+ }
203
+ try {
204
+ child = spawn(invocation.cmd, invocation.args, {
92
205
  cwd,
93
206
  stdio: ["ignore", "pipe", "pipe"],
94
- signal: AbortSignal.timeout(timeoutMs)
207
+ signal,
208
+ windowsHide: true,
209
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
95
210
  });
96
211
  } catch {
97
- resolve2({ code: 127, stdout: "", stderr: "", timedOut: false });
212
+ signal.removeEventListener("abort", onAbort);
213
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
98
214
  return;
99
215
  }
100
- child.stdout?.on("data", (c) => stdoutChunks.push(c));
101
- child.stderr?.on("data", (c) => stderrChunks.push(c));
216
+ child.stdout?.on("data", (c) => {
217
+ stdoutBytes += c.length;
218
+ if (stdoutBytes <= MAX_CAPTURE_BYTES) stdoutChunks.push(c);
219
+ });
220
+ child.stderr?.on("data", (c) => {
221
+ stderrBytes += c.length;
222
+ if (stderrBytes <= MAX_CAPTURE_BYTES) stderrChunks.push(c);
223
+ });
102
224
  child.on("error", () => {
103
- resolve2({ code: 127, stdout: "", stderr: "", timedOut: false });
225
+ signal.removeEventListener("abort", onAbort);
226
+ settle({ code: 127, stdout: "", stderr: "", timedOut: false });
104
227
  });
105
228
  child.on("close", (code) => {
229
+ signal.removeEventListener("abort", onAbort);
106
230
  if (timedOut) return;
107
- resolve2({
231
+ settle({
108
232
  code,
109
233
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
110
234
  stderr: Buffer.concat(stderrChunks).toString("utf-8"),
111
235
  timedOut: false
112
236
  });
113
237
  });
114
- child.on("abort", () => {
115
- timedOut = true;
116
- resolve2({ code: null, stdout: "", stderr: "", timedOut: true });
117
- });
118
238
  });
119
239
  }
120
240
  async function organizeImports(filePath, cfg, cwd) {