@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.
- package/dist/accessibility-auditor/index.d.ts.map +1 -1
- package/dist/accessibility-auditor.js +19 -2
- package/dist/agent-handoff/index.d.ts.map +1 -1
- package/dist/agent-handoff.js +30 -1
- package/dist/auto-doc/index.d.ts.map +1 -1
- package/dist/auto-doc.js +3 -1
- package/dist/auto-i18n-extractor.js +5 -0
- package/dist/branch-guard/index.d.ts.map +1 -1
- package/dist/branch-guard.js +103 -1
- package/dist/changelog-writer/index.d.ts.map +1 -1
- package/dist/changelog-writer.js +137 -3
- package/dist/checkpoint/index.d.ts +2 -0
- package/dist/checkpoint/index.d.ts.map +1 -1
- package/dist/checkpoint.js +34 -8
- package/dist/code-metrics.js +5 -0
- package/dist/commit-validator/index.d.ts.map +1 -1
- package/dist/commit-validator.js +17 -7
- package/dist/context-pins/index.d.ts.map +1 -1
- package/dist/context-pins.js +6 -5
- package/dist/cost-tracker/index.d.ts.map +1 -1
- package/dist/cost-tracker.js +101 -1
- package/dist/cron/index.d.ts.map +1 -1
- package/dist/cron.js +1 -0
- package/dist/dead-code-detector.js +5 -0
- package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
- package/dist/dependency-vulnerability-gate.js +43 -8
- package/dist/diff-summary/index.d.ts.map +1 -1
- package/dist/diff-summary.js +100 -1
- package/dist/doc-sync-guard.js +5 -0
- package/dist/duplicate-code-detector/index.d.ts.map +1 -1
- package/dist/duplicate-code-detector.js +102 -2
- package/dist/feature-flag-tracker/index.d.ts.map +1 -1
- package/dist/feature-flag-tracker.js +30 -4
- package/dist/file-watcher/index.d.ts.map +1 -1
- package/dist/file-watcher.js +6 -7
- package/dist/format-on-save/index.d.ts +2 -0
- package/dist/format-on-save/index.d.ts.map +1 -1
- package/dist/format-on-save.js +288 -59
- package/dist/import-organizer/index.d.ts.map +1 -1
- package/dist/import-organizer.js +140 -20
- package/dist/index.js +1265 -544
- package/dist/interface-contract-guard/index.d.ts.map +1 -1
- package/dist/interface-contract-guard.js +60 -26
- package/dist/knowledge-graph/index.d.ts.map +1 -1
- package/dist/knowledge-graph.js +7 -6
- package/dist/lint-gate/index.d.ts.map +1 -1
- package/dist/lint-gate.js +103 -1
- package/dist/loop-breaker/index.d.ts.map +1 -1
- package/dist/loop-breaker.js +15 -5
- package/dist/migration-planner.js +3 -0
- package/dist/notify-hub/index.d.ts.map +1 -1
- package/dist/notify-hub.js +41 -4
- package/dist/prompt-firewall/index.d.ts.map +1 -1
- package/dist/prompt-firewall.js +140 -11
- package/dist/refactor-suggester/index.d.ts.map +1 -1
- package/dist/refactor-suggester.js +119 -4
- package/dist/runtime/bounded-map.d.ts +86 -0
- package/dist/runtime/bounded-map.d.ts.map +1 -0
- package/dist/runtime/credential-patterns.d.ts +42 -0
- package/dist/runtime/credential-patterns.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/local-bin.d.ts +120 -0
- package/dist/runtime/local-bin.d.ts.map +1 -0
- package/dist/runtime/safe-json.d.ts +25 -0
- package/dist/runtime/safe-json.d.ts.map +1 -0
- package/dist/runtime.js +291 -22
- package/dist/schema-evolution-guard.js +5 -0
- package/dist/secret-scanner/index.d.ts.map +1 -1
- package/dist/secret-scanner.js +175 -48
- package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
- package/dist/security-hotspot-scanner.js +132 -1
- package/dist/semantic-search-indexer/index.d.ts.map +1 -1
- package/dist/semantic-search-indexer.js +7 -1
- package/dist/semver-bump/index.d.ts.map +1 -1
- package/dist/semver-bump.js +21 -2
- package/dist/template-engine/index.d.ts.map +1 -1
- package/dist/template-engine.js +30 -2
- package/dist/test-coverage-gate.js +5 -0
- package/dist/test-runner-gate/index.d.ts.map +1 -1
- package/dist/test-runner-gate.js +130 -8
- package/dist/todo-listener/index.d.ts.map +1 -1
- package/dist/todo-listener.js +30 -1
- package/dist/todo-tracker/index.d.ts.map +1 -1
- package/dist/todo-tracker.js +3 -4
- package/dist/token-throttle/index.d.ts.map +1 -1
- package/dist/token-throttle.js +17 -2
- package/dist/type-gate/index.d.ts.map +1 -1
- package/dist/type-gate.js +96 -16
- package/package.json +3 -3
package/dist/format-on-save.js
CHANGED
|
@@ -1,9 +1,200 @@
|
|
|
1
1
|
// src/format-on-save/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import {
|
|
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 =
|
|
15
|
-
const rel =
|
|
16
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
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) ||
|
|
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 =
|
|
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((
|
|
345
|
+
await new Promise((resolve3, reject) => {
|
|
97
346
|
execFile(
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 || !
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/import-organizer.js
CHANGED
|
@@ -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
|
|
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 =
|
|
16
|
-
const rel =
|
|
17
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
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) ||
|
|
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 (
|
|
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 (!
|
|
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((
|
|
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
|
-
|
|
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
|
|
207
|
+
signal,
|
|
208
|
+
windowsHide: true,
|
|
209
|
+
...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
|
|
95
210
|
});
|
|
96
211
|
} catch {
|
|
97
|
-
|
|
212
|
+
signal.removeEventListener("abort", onAbort);
|
|
213
|
+
settle({ code: 127, stdout: "", stderr: "", timedOut: false });
|
|
98
214
|
return;
|
|
99
215
|
}
|
|
100
|
-
child.stdout?.on("data", (c) =>
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|