@wrongstack/plugins 0.281.3 → 0.282.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -3
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/cost-tracker.js +58 -16
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +125 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- package/LICENSE +0 -21
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
|
|
2
|
+
import { resolve, isAbsolute, relative, extname } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/feature-flag-tracker/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
scanCount: 0,
|
|
8
|
+
flagCount: 0,
|
|
9
|
+
hookInvocationCount: 0,
|
|
10
|
+
warningCount: 0,
|
|
11
|
+
errorCount: 0,
|
|
12
|
+
hookUnregister: null
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_PATTERNS = [
|
|
15
|
+
String.raw`isFeatureEnabled\(['"]([^'"]+)['"]\)`,
|
|
16
|
+
String.raw`featureFlags?\.([A-Za-z_$][A-Za-z0-9_$]*)`,
|
|
17
|
+
String.raw`useFeatureFlag\(['"]([^'"]+)['"]\)`,
|
|
18
|
+
String.raw`flags\.([A-Za-z_$][A-Za-z0-9_$]*)`
|
|
19
|
+
];
|
|
20
|
+
var DEFAULTS = {
|
|
21
|
+
enabled: false,
|
|
22
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
23
|
+
patterns: [],
|
|
24
|
+
maxFindings: 50
|
|
25
|
+
};
|
|
26
|
+
function readConfig(raw) {
|
|
27
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
28
|
+
const r = raw;
|
|
29
|
+
return {
|
|
30
|
+
enabled: r["enabled"] !== false,
|
|
31
|
+
extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
|
|
32
|
+
patterns: Array.isArray(r["patterns"]) ? r["patterns"].filter((x) => typeof x === "string") : DEFAULTS.patterns,
|
|
33
|
+
maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS.maxFindings
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function withinProject(p) {
|
|
37
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
38
|
+
const root = process.cwd();
|
|
39
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
40
|
+
const rel = relative(root, resolved);
|
|
41
|
+
if (rel === "" || rel === ".") return true;
|
|
42
|
+
if (rel.startsWith("..")) return false;
|
|
43
|
+
if (isAbsolute(rel)) return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
function normalizeExtensions(exts) {
|
|
47
|
+
return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
|
|
48
|
+
}
|
|
49
|
+
function matchesExtension(p, exts) {
|
|
50
|
+
return exts.includes(extname(p).toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
function collectSourceFiles(root, exts) {
|
|
53
|
+
const files = [];
|
|
54
|
+
if (!existsSync(root)) return files;
|
|
55
|
+
const s = statSync(root);
|
|
56
|
+
if (s.isFile()) {
|
|
57
|
+
if (matchesExtension(root, exts)) files.push(root);
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
if (!s.isDirectory()) return files;
|
|
61
|
+
function walk(dir) {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = readdirSync(dir);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (entry === "node_modules" || entry === "dist" || entry === ".git" || entry === "coverage") continue;
|
|
70
|
+
const full = resolve(dir, entry);
|
|
71
|
+
let st;
|
|
72
|
+
try {
|
|
73
|
+
st = statSync(full);
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (st.isDirectory()) {
|
|
78
|
+
walk(full);
|
|
79
|
+
} else if (st.isFile() && matchesExtension(full, exts)) {
|
|
80
|
+
files.push(full);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
walk(root);
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
function toPosix(p) {
|
|
88
|
+
return p.replace(/\\/g, "/");
|
|
89
|
+
}
|
|
90
|
+
function relativePath(p) {
|
|
91
|
+
return toPosix(relative(process.cwd(), p));
|
|
92
|
+
}
|
|
93
|
+
function compilePatterns(patterns) {
|
|
94
|
+
const sources = [...DEFAULT_PATTERNS, ...patterns];
|
|
95
|
+
return sources.map((src) => new RegExp(src, "g"));
|
|
96
|
+
}
|
|
97
|
+
function scanFile(filePath, content, patterns, maxFindings) {
|
|
98
|
+
const usages = [];
|
|
99
|
+
const lines = content.split(/\r?\n/);
|
|
100
|
+
for (const re of patterns) {
|
|
101
|
+
re.lastIndex = 0;
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = re.exec(content)) !== null) {
|
|
104
|
+
const flag = m[1] ?? m[0];
|
|
105
|
+
const lineNo = content.slice(0, m.index).split(/\r?\n/).length;
|
|
106
|
+
const context = (lines[lineNo - 1] ?? "").trim();
|
|
107
|
+
usages.push({
|
|
108
|
+
flag,
|
|
109
|
+
file: relativePath(filePath),
|
|
110
|
+
line: lineNo,
|
|
111
|
+
context,
|
|
112
|
+
pattern: re.source
|
|
113
|
+
});
|
|
114
|
+
if (usages.length >= maxFindings) break;
|
|
115
|
+
}
|
|
116
|
+
if (usages.length >= maxFindings) break;
|
|
117
|
+
}
|
|
118
|
+
return usages.slice(0, maxFindings);
|
|
119
|
+
}
|
|
120
|
+
function scanPath(rawPath, cfg) {
|
|
121
|
+
const root = process.cwd();
|
|
122
|
+
const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
|
|
123
|
+
const exts = normalizeExtensions(cfg.extensions);
|
|
124
|
+
const files = collectSourceFiles(resolved, exts);
|
|
125
|
+
const patterns = compilePatterns(cfg.patterns);
|
|
126
|
+
const usages = [];
|
|
127
|
+
for (const p of files) {
|
|
128
|
+
try {
|
|
129
|
+
const content = readFileSync(p, "utf-8");
|
|
130
|
+
usages.push(...scanFile(p, content, patterns, cfg.maxFindings));
|
|
131
|
+
if (usages.length >= cfg.maxFindings) break;
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { usages: usages.slice(0, cfg.maxFindings), scannedFiles: files.length };
|
|
136
|
+
}
|
|
137
|
+
var plugin = {
|
|
138
|
+
name: "feature-flag-tracker",
|
|
139
|
+
version: "0.1.0",
|
|
140
|
+
description: "Scans source files for feature-flag-like expressions and reports usages",
|
|
141
|
+
apiVersion: API_VERSION,
|
|
142
|
+
capabilities: { tools: true, hooks: true },
|
|
143
|
+
defaultConfig: { ...DEFAULTS },
|
|
144
|
+
configSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
148
|
+
extensions: {
|
|
149
|
+
type: "array",
|
|
150
|
+
items: { type: "string" },
|
|
151
|
+
default: [".ts", ".tsx", ".js", ".jsx"],
|
|
152
|
+
description: "File extensions to scan."
|
|
153
|
+
},
|
|
154
|
+
patterns: {
|
|
155
|
+
type: "array",
|
|
156
|
+
items: { type: "string" },
|
|
157
|
+
default: [],
|
|
158
|
+
description: "Extra regex patterns (merged with built-in defaults)."
|
|
159
|
+
},
|
|
160
|
+
maxFindings: {
|
|
161
|
+
type: "number",
|
|
162
|
+
minimum: 1,
|
|
163
|
+
maximum: 500,
|
|
164
|
+
default: 50,
|
|
165
|
+
description: "Maximum flag usages reported per scan."
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
setup(api) {
|
|
170
|
+
state.scanCount = 0;
|
|
171
|
+
state.flagCount = 0;
|
|
172
|
+
state.hookInvocationCount = 0;
|
|
173
|
+
state.warningCount = 0;
|
|
174
|
+
state.errorCount = 0;
|
|
175
|
+
if (state.hookUnregister) {
|
|
176
|
+
try {
|
|
177
|
+
state.hookUnregister();
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
state.hookUnregister = null;
|
|
181
|
+
}
|
|
182
|
+
const cfg = readConfig(api.config.extensions?.["feature-flag-tracker"]);
|
|
183
|
+
const hook = (input) => {
|
|
184
|
+
if (!cfg.enabled) return;
|
|
185
|
+
if (input.toolResult?.isError) return;
|
|
186
|
+
const inp = input.toolInput ?? {};
|
|
187
|
+
const sourcePath = inp["path"];
|
|
188
|
+
if (!sourcePath || typeof sourcePath !== "string") return;
|
|
189
|
+
if (!withinProject(sourcePath)) return;
|
|
190
|
+
const exts = normalizeExtensions(cfg.extensions);
|
|
191
|
+
if (!matchesExtension(sourcePath, exts)) return;
|
|
192
|
+
state.hookInvocationCount += 1;
|
|
193
|
+
const resolved = resolve(process.cwd(), sourcePath);
|
|
194
|
+
let content;
|
|
195
|
+
try {
|
|
196
|
+
content = readFileSync(resolved, "utf-8");
|
|
197
|
+
} catch {
|
|
198
|
+
state.errorCount += 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const patterns = compilePatterns(cfg.patterns);
|
|
202
|
+
const usages = scanFile(resolved, content, patterns, cfg.maxFindings);
|
|
203
|
+
if (usages.length === 0) return;
|
|
204
|
+
state.warningCount += usages.length;
|
|
205
|
+
const flags = [...new Set(usages.map((u) => u.flag))].join(", ");
|
|
206
|
+
return {
|
|
207
|
+
additionalContext: `
|
|
208
|
+
\u{1F6A9} feature-flag-tracker: ${sourcePath} references feature flag(s): ${flags}.
|
|
209
|
+
Make sure flag behavior is intentional and consider updating flag inventory/docs.`
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
213
|
+
api.tools.register({
|
|
214
|
+
name: "scan_feature_flags",
|
|
215
|
+
description: "Scan source files for feature-flag-like expressions (isFeatureEnabled, featureFlags.*, useFeatureFlag, flags.*, plus custom patterns).",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
path: { type: "string", default: ".", description: "File or directory path to scan." }
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
permission: "auto",
|
|
223
|
+
category: "Diagnostics",
|
|
224
|
+
mutating: false,
|
|
225
|
+
async execute(input) {
|
|
226
|
+
if (!cfg.enabled) return { ok: false, error: "feature-flag-tracker is disabled" };
|
|
227
|
+
const rawPath = typeof input.path === "string" ? input.path : ".";
|
|
228
|
+
if (!withinProject(rawPath)) {
|
|
229
|
+
return { ok: false, error: "path is outside the project root" };
|
|
230
|
+
}
|
|
231
|
+
state.scanCount += 1;
|
|
232
|
+
let result;
|
|
233
|
+
try {
|
|
234
|
+
result = scanPath(rawPath, cfg);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
state.errorCount += 1;
|
|
237
|
+
return { ok: false, error: String(err) };
|
|
238
|
+
}
|
|
239
|
+
state.flagCount += result.usages.length;
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
path: relativePath(resolve(process.cwd(), rawPath)),
|
|
243
|
+
scannedFiles: result.scannedFiles,
|
|
244
|
+
usages: result.usages
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
api.tools.register({
|
|
249
|
+
name: "feature_flag_status",
|
|
250
|
+
description: "Reports feature-flag-tracker state: config + counters.",
|
|
251
|
+
inputSchema: { type: "object", properties: {} },
|
|
252
|
+
permission: "auto",
|
|
253
|
+
category: "Diagnostics",
|
|
254
|
+
mutating: false,
|
|
255
|
+
async execute() {
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
enabled: cfg.enabled,
|
|
259
|
+
extensions: cfg.extensions,
|
|
260
|
+
patterns: cfg.patterns,
|
|
261
|
+
maxFindings: cfg.maxFindings,
|
|
262
|
+
counters: {
|
|
263
|
+
scans: state.scanCount,
|
|
264
|
+
flags: state.flagCount,
|
|
265
|
+
hookInvocations: state.hookInvocationCount,
|
|
266
|
+
warnings: state.warningCount,
|
|
267
|
+
errors: state.errorCount
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
api.log.info("feature-flag-tracker plugin loaded", {
|
|
273
|
+
version: "0.1.0",
|
|
274
|
+
extensions: cfg.extensions,
|
|
275
|
+
customPatterns: cfg.patterns.length
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
teardown(api) {
|
|
279
|
+
if (state.hookUnregister) {
|
|
280
|
+
try {
|
|
281
|
+
state.hookUnregister();
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
state.hookUnregister = null;
|
|
285
|
+
}
|
|
286
|
+
const final = {
|
|
287
|
+
scans: state.scanCount,
|
|
288
|
+
flags: state.flagCount,
|
|
289
|
+
hookInvocations: state.hookInvocationCount,
|
|
290
|
+
warnings: state.warningCount,
|
|
291
|
+
errors: state.errorCount
|
|
292
|
+
};
|
|
293
|
+
state.scanCount = 0;
|
|
294
|
+
state.flagCount = 0;
|
|
295
|
+
state.hookInvocationCount = 0;
|
|
296
|
+
state.warningCount = 0;
|
|
297
|
+
state.errorCount = 0;
|
|
298
|
+
api.log.info("feature-flag-tracker: teardown complete", { final });
|
|
299
|
+
},
|
|
300
|
+
async health() {
|
|
301
|
+
return {
|
|
302
|
+
ok: state.errorCount === 0,
|
|
303
|
+
message: state.errorCount ? `feature-flag-tracker: ${state.errorCount} error(s)` : `feature-flag-tracker: ${state.scanCount} scan(s), ${state.flagCount} flag usage(s)`,
|
|
304
|
+
counters: {
|
|
305
|
+
scans: state.scanCount,
|
|
306
|
+
flags: state.flagCount,
|
|
307
|
+
hookInvocations: state.hookInvocationCount,
|
|
308
|
+
warnings: state.warningCount,
|
|
309
|
+
errors: state.errorCount
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var feature_flag_tracker_default = plugin;
|
|
315
|
+
|
|
316
|
+
export { feature_flag_tracker_default as default };
|
package/dist/file-watcher.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { watch } from 'fs';
|
|
2
|
-
import
|
|
2
|
+
import { isAbsolute, resolve, relative } from 'path';
|
|
3
3
|
|
|
4
4
|
// src/file-watcher/index.ts
|
|
5
5
|
var API_VERSION = "^0.1.10";
|
|
6
|
+
function withinProject(p) {
|
|
7
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
10
|
+
const rel = relative(root, resolved);
|
|
11
|
+
if (rel === "" || rel === ".") return true;
|
|
12
|
+
if (rel.startsWith("..")) return false;
|
|
13
|
+
if (isAbsolute(rel)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
6
16
|
var watch_idCounter = 0;
|
|
7
17
|
function nextId() {
|
|
8
18
|
return `watch_${++watch_idCounter}_${Date.now().toString(36)}`;
|
|
@@ -71,16 +81,30 @@ var plugin = {
|
|
|
71
81
|
function debounceEvent(key, fn, ms) {
|
|
72
82
|
const existing = debounceTimers.get(key);
|
|
73
83
|
if (existing) clearTimeout(existing);
|
|
74
|
-
debounceTimers.set(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
debounceTimers.set(
|
|
85
|
+
key,
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
debounceTimers.delete(key);
|
|
88
|
+
fn();
|
|
89
|
+
}, ms)
|
|
90
|
+
);
|
|
78
91
|
}
|
|
79
92
|
const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
|
|
80
93
|
const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
|
|
94
|
+
const safeIndexRoot = indexProjectRoot !== "" && withinProject(indexProjectRoot) ? indexProjectRoot : "";
|
|
95
|
+
if (indexProjectRoot !== "" && safeIndexRoot === "") {
|
|
96
|
+
api.log.warn(
|
|
97
|
+
"file-watcher: indexProjectRoot is outside the project root \u2014 using watched dirPath instead",
|
|
98
|
+
{
|
|
99
|
+
indexProjectRoot
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
81
103
|
const INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
82
104
|
function isIndexableFile(filePath) {
|
|
83
|
-
|
|
105
|
+
const dot = filePath.lastIndexOf(".");
|
|
106
|
+
const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
|
|
107
|
+
return INDEXABLE_EXTENSIONS.has(ext);
|
|
84
108
|
}
|
|
85
109
|
function safeWatchDir(dirPath, recursive, handle) {
|
|
86
110
|
try {
|
|
@@ -88,34 +112,42 @@ var plugin = {
|
|
|
88
112
|
if (!filename) return;
|
|
89
113
|
const fullPath = `${dirPath}/${filename}`;
|
|
90
114
|
const key = `${handle.id}:${fullPath}:${eventType}`;
|
|
91
|
-
debounceEvent(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
debounceEvent(
|
|
116
|
+
key,
|
|
117
|
+
() => {
|
|
118
|
+
api.emitCustom("file-watcher:changed", {
|
|
119
|
+
watch_id: handle.id,
|
|
120
|
+
path: fullPath,
|
|
121
|
+
event: eventType,
|
|
122
|
+
filename,
|
|
123
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
124
|
+
});
|
|
125
|
+
api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
|
|
126
|
+
api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
|
|
127
|
+
if (autoIndex && isIndexableFile(fullPath)) {
|
|
128
|
+
debounceEvent(
|
|
129
|
+
`index:${fullPath}`,
|
|
130
|
+
async () => {
|
|
131
|
+
try {
|
|
132
|
+
const { enqueueReindex } = await import('@wrongstack/tools/codebase-index');
|
|
133
|
+
const root = safeIndexRoot || dirPath;
|
|
134
|
+
enqueueReindex({
|
|
135
|
+
projectRoot: root,
|
|
136
|
+
files: [fullPath],
|
|
137
|
+
onError: (err) => api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`)
|
|
138
|
+
});
|
|
139
|
+
api.metrics.counter("index_file", 1);
|
|
140
|
+
api.log.debug(`file-watcher: auto-index scheduled for ${fullPath}`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
api.log.warn(`file-watcher: auto-index failed for ${fullPath}: ${err}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
debounceMs
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
debounceMs
|
|
150
|
+
);
|
|
119
151
|
});
|
|
120
152
|
watcher.on("error", (err) => {
|
|
121
153
|
api.log.warn(`file-watcher: error on ${dirPath}: ${err}`);
|
|
@@ -156,14 +188,31 @@ var plugin = {
|
|
|
156
188
|
async execute(input) {
|
|
157
189
|
const rawPaths = input["paths"];
|
|
158
190
|
if (!rawPaths || typeof rawPaths !== "object" || !Array.isArray(rawPaths)) {
|
|
159
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: "paths must be an array of file/directory paths",
|
|
194
|
+
watch_id: null
|
|
195
|
+
};
|
|
160
196
|
}
|
|
161
197
|
const paths = rawPaths;
|
|
162
198
|
if (paths.length === 0) {
|
|
163
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
error: "paths array is empty \u2014 provide at least one path",
|
|
202
|
+
watch_id: null
|
|
203
|
+
};
|
|
164
204
|
}
|
|
165
205
|
const events = input["events"] ?? ["change", "add", "delete"];
|
|
166
206
|
const recursive = input["recursive"] ?? true;
|
|
207
|
+
const bad = paths.find((p) => !withinProject(p));
|
|
208
|
+
if (bad !== void 0) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: `path is outside the project root: ${bad}`,
|
|
212
|
+
watch_id: null,
|
|
213
|
+
rejectedOutsideProject: true
|
|
214
|
+
};
|
|
215
|
+
}
|
|
167
216
|
const id = nextId();
|
|
168
217
|
const handle = {
|
|
169
218
|
id,
|
package/dist/format-on-save.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execSync, execFileSync } from 'child_process';
|
|
2
2
|
import { existsSync, statSync } from 'fs';
|
|
3
|
+
import { isAbsolute, resolve, relative } from 'path';
|
|
3
4
|
|
|
4
5
|
// src/format-on-save/index.ts
|
|
5
6
|
var API_VERSION = "^0.1.10";
|
|
7
|
+
function withinProject(p) {
|
|
8
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
11
|
+
const rel = relative(root, resolved);
|
|
12
|
+
if (rel === "" || rel === ".") return true;
|
|
13
|
+
if (rel.startsWith("..")) return false;
|
|
14
|
+
if (isAbsolute(rel)) return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
6
17
|
var state = {
|
|
7
18
|
invocationCount: 0,
|
|
8
19
|
/** Times formatting was applied (file changed). */
|
|
@@ -11,6 +22,9 @@ var state = {
|
|
|
11
22
|
cleanCount: 0,
|
|
12
23
|
/** Times biome failed (not installed, timeout, parse error). */
|
|
13
24
|
errorCount: 0,
|
|
25
|
+
/** Times the format pass was skipped because import-organizer
|
|
26
|
+
* had already covered the path within the TTL window. */
|
|
27
|
+
coveredSkipCount: 0,
|
|
14
28
|
/** Hook handle for teardown. */
|
|
15
29
|
hookUnregister: null,
|
|
16
30
|
/** Last format result — surfaced by health() + status tool. */
|
|
@@ -18,17 +32,29 @@ var state = {
|
|
|
18
32
|
};
|
|
19
33
|
var DEFAULTS = {
|
|
20
34
|
enabled: true,
|
|
21
|
-
timeoutMs: 5e3
|
|
35
|
+
timeoutMs: 5e3,
|
|
36
|
+
skipWhenCoveredBy: true,
|
|
37
|
+
skipTtlMs: 3e4
|
|
22
38
|
};
|
|
23
39
|
function readConfig(raw) {
|
|
24
40
|
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
25
41
|
const r = raw;
|
|
26
42
|
return {
|
|
27
43
|
enabled: r["enabled"] !== false,
|
|
28
|
-
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs
|
|
44
|
+
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
|
|
45
|
+
skipWhenCoveredBy: r["skipWhenCoveredBy"] !== false,
|
|
46
|
+
skipTtlMs: typeof r["skipTtlMs"] === "number" && r["skipTtlMs"] >= 0 ? r["skipTtlMs"] : DEFAULTS.skipTtlMs
|
|
29
47
|
};
|
|
30
48
|
}
|
|
49
|
+
var recentlyCovered = /* @__PURE__ */ new Map();
|
|
50
|
+
function evictExpired(ttlMs) {
|
|
51
|
+
const cutoff = Date.now() - ttlMs;
|
|
52
|
+
for (const [path, ts] of recentlyCovered) {
|
|
53
|
+
if (ts < cutoff) recentlyCovered.delete(path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
function formatFile(filePath, timeoutMs) {
|
|
57
|
+
if (!withinProject(filePath)) return null;
|
|
32
58
|
if (!existsSync(filePath)) return null;
|
|
33
59
|
let bytesBefore;
|
|
34
60
|
try {
|
|
@@ -37,7 +63,7 @@ function formatFile(filePath, timeoutMs) {
|
|
|
37
63
|
return null;
|
|
38
64
|
}
|
|
39
65
|
try {
|
|
40
|
-
|
|
66
|
+
execFileSync("npx", ["biome", "format", "--write", filePath], {
|
|
41
67
|
encoding: "utf-8",
|
|
42
68
|
timeout: timeoutMs,
|
|
43
69
|
cwd: process.cwd(),
|
|
@@ -57,7 +83,7 @@ function formatFile(filePath, timeoutMs) {
|
|
|
57
83
|
return { changed: true, bytesBefore, bytesAfter };
|
|
58
84
|
}
|
|
59
85
|
try {
|
|
60
|
-
|
|
86
|
+
execFileSync("npx", ["biome", "format", filePath], {
|
|
61
87
|
encoding: "utf-8",
|
|
62
88
|
timeout: timeoutMs,
|
|
63
89
|
cwd: process.cwd(),
|
|
@@ -88,6 +114,17 @@ var plugin = {
|
|
|
88
114
|
minimum: 1e3,
|
|
89
115
|
default: 5e3,
|
|
90
116
|
description: "Biome format process timeout in milliseconds."
|
|
117
|
+
},
|
|
118
|
+
skipWhenCoveredBy: {
|
|
119
|
+
type: "boolean",
|
|
120
|
+
default: true,
|
|
121
|
+
description: "Skip the format pass when another plugin (e.g. import-organizer) just touched the same path. Saves one biome invocation per write/edit when both plugins are enabled."
|
|
122
|
+
},
|
|
123
|
+
skipTtlMs: {
|
|
124
|
+
type: "number",
|
|
125
|
+
minimum: 0,
|
|
126
|
+
default: 3e4,
|
|
127
|
+
description: "How long (ms) to remember a path covered by another plugin. 0 disables the memory."
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
},
|
|
@@ -96,8 +133,10 @@ var plugin = {
|
|
|
96
133
|
state.formattedCount = 0;
|
|
97
134
|
state.cleanCount = 0;
|
|
98
135
|
state.errorCount = 0;
|
|
136
|
+
state.coveredSkipCount = 0;
|
|
99
137
|
state.hookUnregister = null;
|
|
100
138
|
state.lastResult = null;
|
|
139
|
+
recentlyCovered.clear();
|
|
101
140
|
const cfg = readConfig(api.config.extensions?.["format-on-save"]);
|
|
102
141
|
let biomeAvailable = false;
|
|
103
142
|
try {
|
|
@@ -120,6 +159,20 @@ var plugin = {
|
|
|
120
159
|
const inp = input.toolInput ?? {};
|
|
121
160
|
const filePath = inp["path"];
|
|
122
161
|
if (!filePath || typeof filePath !== "string") return;
|
|
162
|
+
if (cfg.skipWhenCoveredBy && cfg.skipTtlMs > 0) {
|
|
163
|
+
evictExpired(cfg.skipTtlMs);
|
|
164
|
+
if (recentlyCovered.has(filePath)) {
|
|
165
|
+
state.coveredSkipCount = (state.coveredSkipCount ?? 0) + 1;
|
|
166
|
+
recentlyCovered.delete(filePath);
|
|
167
|
+
api.log.info(
|
|
168
|
+
`format-on-save: skipped ${filePath} \u2014 already formatted by import-organizer`,
|
|
169
|
+
{
|
|
170
|
+
tool: toolName
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
123
176
|
state.invocationCount += 1;
|
|
124
177
|
const result = formatFile(filePath, cfg.timeoutMs);
|
|
125
178
|
if (!result) {
|
|
@@ -150,9 +203,15 @@ var plugin = {
|
|
|
150
203
|
return;
|
|
151
204
|
};
|
|
152
205
|
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
206
|
+
api.onPattern("import-organizer:done", (_eventName, payload) => {
|
|
207
|
+
const p = payload ?? {};
|
|
208
|
+
if (typeof p.path !== "string" || p.path.length === 0) return;
|
|
209
|
+
recentlyCovered.set(p.path, Date.now());
|
|
210
|
+
api.metrics.counter("covered_notice");
|
|
211
|
+
});
|
|
153
212
|
api.tools.register({
|
|
154
213
|
name: "format_on_save_status",
|
|
155
|
-
description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error counters.",
|
|
214
|
+
description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error/skipped counters.",
|
|
156
215
|
inputSchema: { type: "object", properties: {} },
|
|
157
216
|
permission: "auto",
|
|
158
217
|
category: "Code Quality",
|
|
@@ -163,11 +222,14 @@ var plugin = {
|
|
|
163
222
|
enabled: cfg.enabled,
|
|
164
223
|
biomeAvailable,
|
|
165
224
|
timeoutMs: cfg.timeoutMs,
|
|
225
|
+
skipWhenCoveredBy: cfg.skipWhenCoveredBy,
|
|
226
|
+
skipTtlMs: cfg.skipTtlMs,
|
|
166
227
|
counters: {
|
|
167
228
|
invocations: state.invocationCount,
|
|
168
229
|
formatted: state.formattedCount,
|
|
169
230
|
clean: state.cleanCount,
|
|
170
|
-
errors: state.errorCount
|
|
231
|
+
errors: state.errorCount,
|
|
232
|
+
coveredSkips: state.coveredSkipCount
|
|
171
233
|
},
|
|
172
234
|
lastResult: state.lastResult
|
|
173
235
|
};
|
|
@@ -191,24 +253,28 @@ var plugin = {
|
|
|
191
253
|
invocations: state.invocationCount,
|
|
192
254
|
formatted: state.formattedCount,
|
|
193
255
|
clean: state.cleanCount,
|
|
194
|
-
errors: state.errorCount
|
|
256
|
+
errors: state.errorCount,
|
|
257
|
+
coveredSkips: state.coveredSkipCount
|
|
195
258
|
};
|
|
196
259
|
state.invocationCount = 0;
|
|
197
260
|
state.formattedCount = 0;
|
|
198
261
|
state.cleanCount = 0;
|
|
199
262
|
state.errorCount = 0;
|
|
263
|
+
state.coveredSkipCount = 0;
|
|
200
264
|
state.lastResult = null;
|
|
265
|
+
recentlyCovered.clear();
|
|
201
266
|
api.log.info("format-on-save: teardown complete", { final });
|
|
202
267
|
},
|
|
203
268
|
async health() {
|
|
204
269
|
return {
|
|
205
270
|
ok: true,
|
|
206
|
-
message: state.lastResult === null ? `format-on-save: ${state.invocationCount} invocation(s), ${state.formattedCount} formatted` : state.lastResult.changed ? `format-on-save: last formatted ${state.lastResult.path} (${state.lastResult.tool}) at ${state.lastResult.when}` : `format-on-save: last check on ${state.lastResult.path} was already clean`,
|
|
271
|
+
message: state.lastResult === null ? `format-on-save: ${state.invocationCount} invocation(s), ${state.formattedCount} formatted, ${state.coveredSkipCount} covered-skipped` : state.lastResult.changed ? `format-on-save: last formatted ${state.lastResult.path} (${state.lastResult.tool}) at ${state.lastResult.when}` : `format-on-save: last check on ${state.lastResult.path} was already clean`,
|
|
207
272
|
counters: {
|
|
208
273
|
invocations: state.invocationCount,
|
|
209
274
|
formatted: state.formattedCount,
|
|
210
275
|
clean: state.cleanCount,
|
|
211
|
-
errors: state.errorCount
|
|
276
|
+
errors: state.errorCount,
|
|
277
|
+
coveredSkips: state.coveredSkipCount
|
|
212
278
|
},
|
|
213
279
|
lastResult: state.lastResult
|
|
214
280
|
};
|