@wrongstack/plugins 0.281.3 → 0.282.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/README.md +30 -4
- 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-escalate.d.ts +1 -1
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/branch-guard.d.ts +6 -5
- package/dist/branch-guard.js +54 -4
- 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/context-pins.js +4 -4
- package/dist/cost-tracker.d.ts +1 -1
- package/dist/cost-tracker.js +58 -19
- package/dist/cron.js +18 -8
- 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 +99 -18
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +11205 -2106
- 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 +138 -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,
|