@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,36 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto-i18n-extractor plugin — detects hardcoded user-facing strings in UI
|
|
5
|
+
* source files and suggests translation keys.
|
|
6
|
+
*
|
|
7
|
+
* Tools registered:
|
|
8
|
+
* - i18n_extract : Scan a file/directory path for extractable strings.
|
|
9
|
+
* - i18n_status : Show config + per-session counters.
|
|
10
|
+
*
|
|
11
|
+
* Hooks registered:
|
|
12
|
+
* - PostToolUse with matcher `write|edit` for `.tsx`, `.jsx`, `.vue` files.
|
|
13
|
+
* Scans the written file and injects a short additionalContext with
|
|
14
|
+
* suggested i18n keys when user-facing string literals are found.
|
|
15
|
+
*
|
|
16
|
+
* Config (`config.extensions['auto-i18n-extractor']`):
|
|
17
|
+
*
|
|
18
|
+
* ```jsonc
|
|
19
|
+
* {
|
|
20
|
+
* "enabled": true,
|
|
21
|
+
* "fileExtensions": [".tsx", ".jsx", ".vue"],
|
|
22
|
+
* "minLength": 2,
|
|
23
|
+
* "maxContextStrings": 10,
|
|
24
|
+
* "excludeAttributes": [
|
|
25
|
+
* "className", "class", "testId", "data-testid",
|
|
26
|
+
* "aria-label", "aria-labelledby", "key", "id", "name", "htmlFor"
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
declare const plugin: Plugin;
|
|
35
|
+
|
|
36
|
+
export { plugin as default };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/auto-i18n-extractor/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
filesScanned: 0,
|
|
8
|
+
stringsFound: 0,
|
|
9
|
+
extractions: 0,
|
|
10
|
+
skipped: 0,
|
|
11
|
+
readErrors: 0,
|
|
12
|
+
hookUnregister: null,
|
|
13
|
+
lastResult: null
|
|
14
|
+
};
|
|
15
|
+
var DEFAULTS = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
fileExtensions: [".tsx", ".jsx", ".vue"],
|
|
18
|
+
minLength: 2,
|
|
19
|
+
maxContextStrings: 10,
|
|
20
|
+
excludeAttributes: [
|
|
21
|
+
"className",
|
|
22
|
+
"class",
|
|
23
|
+
"testId",
|
|
24
|
+
"data-testid",
|
|
25
|
+
"aria-label",
|
|
26
|
+
"aria-labelledby",
|
|
27
|
+
"key",
|
|
28
|
+
"id",
|
|
29
|
+
"name",
|
|
30
|
+
"htmlFor"
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
function readConfig(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
35
|
+
const r = raw;
|
|
36
|
+
const clamp = (n, min, max, fallback) => typeof n === "number" && Number.isFinite(n) && n >= min && n <= max ? Math.floor(n) : fallback;
|
|
37
|
+
return {
|
|
38
|
+
enabled: r["enabled"] !== false,
|
|
39
|
+
fileExtensions: Array.isArray(r["fileExtensions"]) ? r["fileExtensions"].filter((x) => typeof x === "string") : DEFAULTS.fileExtensions,
|
|
40
|
+
minLength: clamp(r["minLength"], 1, 500, DEFAULTS.minLength),
|
|
41
|
+
maxContextStrings: clamp(r["maxContextStrings"], 1, 100, DEFAULTS.maxContextStrings),
|
|
42
|
+
excludeAttributes: Array.isArray(r["excludeAttributes"]) ? r["excludeAttributes"].filter((x) => typeof x === "string") : DEFAULTS.excludeAttributes
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function withinProject(p) {
|
|
46
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
47
|
+
const root = process.cwd();
|
|
48
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
49
|
+
const rel = relative(root, resolved);
|
|
50
|
+
if (rel === "" || rel === ".") return true;
|
|
51
|
+
if (rel.startsWith("..")) return false;
|
|
52
|
+
if (isAbsolute(rel)) return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function fileExtension(p) {
|
|
56
|
+
const dot = p.lastIndexOf(".");
|
|
57
|
+
return dot > 0 ? p.slice(dot).toLowerCase() : "";
|
|
58
|
+
}
|
|
59
|
+
function generateKey(value) {
|
|
60
|
+
const base = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
61
|
+
return base ? `t.${base}` : "t.unknown";
|
|
62
|
+
}
|
|
63
|
+
function looksLikeUserText(value, minLength) {
|
|
64
|
+
if (value.length < minLength) return false;
|
|
65
|
+
if (/^\s*$/.test(value)) return false;
|
|
66
|
+
if (/^[0-9\s\W_]+$/.test(value)) return false;
|
|
67
|
+
if (/^(https?:|ftp:|www\.|\/|\.\/|@|#[0-9a-f]{3,8}$|\.?(jsx?|tsx?|vue|css|scss|json|png|svg|jpg)$)/i.test(value)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
function isExcludedContext(line, quoteIndex, excludeAttributes) {
|
|
73
|
+
const before = line.slice(0, quoteIndex);
|
|
74
|
+
for (const attr of excludeAttributes) {
|
|
75
|
+
const re = new RegExp(`\\b${attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*["'{]?$`);
|
|
76
|
+
if (re.test(before)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function extractStrings(content, cfg) {
|
|
81
|
+
const results = [];
|
|
82
|
+
const lines = content.split(/\r?\n/);
|
|
83
|
+
const stringRegex = /(["'`])((?:\\.|(?!\1)[^\\])*?)\1/g;
|
|
84
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
stringRegex.lastIndex = 0;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = stringRegex.exec(line)) !== null) {
|
|
89
|
+
const value = match[2] ?? "";
|
|
90
|
+
const quoteIndex = match.index + (match[1]?.length ?? 1);
|
|
91
|
+
if (!looksLikeUserText(value, cfg.minLength)) continue;
|
|
92
|
+
if (isExcludedContext(line, quoteIndex, cfg.excludeAttributes)) continue;
|
|
93
|
+
results.push({ value, line: i + 1, keySuggestion: generateKey(value) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
function readSourceFile(filePath) {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(filePath, "utf-8");
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var plugin = {
|
|
106
|
+
name: "auto-i18n-extractor",
|
|
107
|
+
version: "0.1.0",
|
|
108
|
+
description: "Detects hardcoded user-facing strings in UI source files and suggests translation keys",
|
|
109
|
+
apiVersion: API_VERSION,
|
|
110
|
+
capabilities: { tools: true, hooks: true },
|
|
111
|
+
defaultConfig: { ...DEFAULTS },
|
|
112
|
+
configSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
116
|
+
fileExtensions: {
|
|
117
|
+
type: "array",
|
|
118
|
+
items: { type: "string" },
|
|
119
|
+
default: [".tsx", ".jsx", ".vue"],
|
|
120
|
+
description: "Only scan files with these extensions."
|
|
121
|
+
},
|
|
122
|
+
minLength: {
|
|
123
|
+
type: "number",
|
|
124
|
+
minimum: 1,
|
|
125
|
+
maximum: 500,
|
|
126
|
+
default: 2,
|
|
127
|
+
description: "Minimum string length to consider user-facing."
|
|
128
|
+
},
|
|
129
|
+
maxContextStrings: {
|
|
130
|
+
type: "number",
|
|
131
|
+
minimum: 1,
|
|
132
|
+
maximum: 100,
|
|
133
|
+
default: 10,
|
|
134
|
+
description: "Maximum strings included in a hook additionalContext."
|
|
135
|
+
},
|
|
136
|
+
excludeAttributes: {
|
|
137
|
+
type: "array",
|
|
138
|
+
items: { type: "string" },
|
|
139
|
+
default: [
|
|
140
|
+
"className",
|
|
141
|
+
"class",
|
|
142
|
+
"testId",
|
|
143
|
+
"data-testid",
|
|
144
|
+
"aria-label",
|
|
145
|
+
"aria-labelledby",
|
|
146
|
+
"key",
|
|
147
|
+
"id",
|
|
148
|
+
"name",
|
|
149
|
+
"htmlFor"
|
|
150
|
+
],
|
|
151
|
+
description: "Attribute names whose string values are never user-facing."
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
setup(api) {
|
|
156
|
+
state.filesScanned = 0;
|
|
157
|
+
state.stringsFound = 0;
|
|
158
|
+
state.extractions = 0;
|
|
159
|
+
state.skipped = 0;
|
|
160
|
+
state.readErrors = 0;
|
|
161
|
+
state.lastResult = null;
|
|
162
|
+
if (state.hookUnregister) {
|
|
163
|
+
try {
|
|
164
|
+
state.hookUnregister();
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
state.hookUnregister = null;
|
|
168
|
+
}
|
|
169
|
+
const cfg = readConfig(api.config.extensions?.["auto-i18n-extractor"]);
|
|
170
|
+
const hook = (input) => {
|
|
171
|
+
if (!cfg.enabled) return;
|
|
172
|
+
if (input.toolResult?.isError) return;
|
|
173
|
+
const inp = input.toolInput ?? {};
|
|
174
|
+
const sourcePath = inp["path"];
|
|
175
|
+
if (!sourcePath || typeof sourcePath !== "string") return;
|
|
176
|
+
if (!withinProject(sourcePath)) return;
|
|
177
|
+
const ext = fileExtension(sourcePath);
|
|
178
|
+
if (!cfg.fileExtensions.includes(ext)) {
|
|
179
|
+
state.skipped += 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
state.filesScanned += 1;
|
|
183
|
+
const content = readSourceFile(sourcePath);
|
|
184
|
+
if (content === null) {
|
|
185
|
+
state.readErrors += 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const extracted = extractStrings(content, cfg);
|
|
189
|
+
if (extracted.length === 0) return;
|
|
190
|
+
state.stringsFound += extracted.length;
|
|
191
|
+
state.extractions += 1;
|
|
192
|
+
state.lastResult = {
|
|
193
|
+
path: sourcePath,
|
|
194
|
+
stringsFound: extracted.length,
|
|
195
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
196
|
+
};
|
|
197
|
+
const shown = extracted.slice(0, cfg.maxContextStrings);
|
|
198
|
+
const lines = shown.map((s) => ` - "${s.value}" \u2192 ${s.keySuggestion} (line ${s.line})`);
|
|
199
|
+
const more = extracted.length > shown.length ? `
|
|
200
|
+
\u2026 and ${extracted.length - shown.length} more` : "";
|
|
201
|
+
const context = `
|
|
202
|
+
\u{1F30D} auto-i18n-extractor: found ${extracted.length} user-facing string(s) in ${sourcePath}. Consider extracting to i18n keys:
|
|
203
|
+
${lines.join("\n")}${more}`;
|
|
204
|
+
return { additionalContext: context };
|
|
205
|
+
};
|
|
206
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
207
|
+
api.tools.register({
|
|
208
|
+
name: "i18n_extract",
|
|
209
|
+
description: "Scan a UI source file (.tsx/.jsx/.vue) for hardcoded user-facing string literals and return suggested i18n keys.",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
path: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Relative or absolute path to the source file to scan."
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
required: ["path"]
|
|
219
|
+
},
|
|
220
|
+
permission: "auto",
|
|
221
|
+
category: "Diagnostics",
|
|
222
|
+
mutating: false,
|
|
223
|
+
async execute(input) {
|
|
224
|
+
if (!cfg.enabled) return { ok: false, error: "auto-i18n-extractor is disabled" };
|
|
225
|
+
const filePath = input.path;
|
|
226
|
+
if (typeof filePath !== "string" || !filePath) {
|
|
227
|
+
return { ok: false, error: "path is required" };
|
|
228
|
+
}
|
|
229
|
+
if (!withinProject(filePath)) {
|
|
230
|
+
return { ok: false, error: "path must be inside the project" };
|
|
231
|
+
}
|
|
232
|
+
const ext = fileExtension(filePath);
|
|
233
|
+
if (!cfg.fileExtensions.includes(ext)) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: `unsupported extension "${ext}"; allowed: ${cfg.fileExtensions.join(", ")}`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
state.filesScanned += 1;
|
|
240
|
+
const content = readSourceFile(filePath);
|
|
241
|
+
if (content === null) {
|
|
242
|
+
state.readErrors += 1;
|
|
243
|
+
return { ok: false, error: `could not read ${filePath}` };
|
|
244
|
+
}
|
|
245
|
+
const extracted = extractStrings(content, cfg);
|
|
246
|
+
state.stringsFound += extracted.length;
|
|
247
|
+
if (extracted.length > 0) {
|
|
248
|
+
state.extractions += 1;
|
|
249
|
+
state.lastResult = {
|
|
250
|
+
path: filePath,
|
|
251
|
+
stringsFound: extracted.length,
|
|
252
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
path: filePath,
|
|
258
|
+
stringsFound: extracted.length,
|
|
259
|
+
strings: extracted
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
api.tools.register({
|
|
264
|
+
name: "i18n_status",
|
|
265
|
+
description: "Reports auto-i18n-extractor state: enabled, extensions, counters, and last extraction.",
|
|
266
|
+
inputSchema: { type: "object", properties: {} },
|
|
267
|
+
permission: "auto",
|
|
268
|
+
category: "Diagnostics",
|
|
269
|
+
mutating: false,
|
|
270
|
+
async execute() {
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
enabled: cfg.enabled,
|
|
274
|
+
fileExtensions: cfg.fileExtensions,
|
|
275
|
+
minLength: cfg.minLength,
|
|
276
|
+
maxContextStrings: cfg.maxContextStrings,
|
|
277
|
+
excludeAttributes: cfg.excludeAttributes,
|
|
278
|
+
counters: {
|
|
279
|
+
filesScanned: state.filesScanned,
|
|
280
|
+
stringsFound: state.stringsFound,
|
|
281
|
+
extractions: state.extractions,
|
|
282
|
+
skipped: state.skipped,
|
|
283
|
+
readErrors: state.readErrors
|
|
284
|
+
},
|
|
285
|
+
lastResult: state.lastResult
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
api.log.info("auto-i18n-extractor plugin loaded", {
|
|
290
|
+
version: "0.1.0",
|
|
291
|
+
fileExtensions: cfg.fileExtensions,
|
|
292
|
+
minLength: cfg.minLength
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
teardown(api) {
|
|
296
|
+
if (state.hookUnregister) {
|
|
297
|
+
try {
|
|
298
|
+
state.hookUnregister();
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
state.hookUnregister = null;
|
|
302
|
+
}
|
|
303
|
+
const final = {
|
|
304
|
+
filesScanned: state.filesScanned,
|
|
305
|
+
stringsFound: state.stringsFound,
|
|
306
|
+
extractions: state.extractions,
|
|
307
|
+
skipped: state.skipped,
|
|
308
|
+
readErrors: state.readErrors
|
|
309
|
+
};
|
|
310
|
+
state.filesScanned = 0;
|
|
311
|
+
state.stringsFound = 0;
|
|
312
|
+
state.extractions = 0;
|
|
313
|
+
state.skipped = 0;
|
|
314
|
+
state.readErrors = 0;
|
|
315
|
+
state.lastResult = null;
|
|
316
|
+
api.log.info("auto-i18n-extractor: teardown complete", { final });
|
|
317
|
+
},
|
|
318
|
+
async health() {
|
|
319
|
+
return {
|
|
320
|
+
ok: state.readErrors === 0,
|
|
321
|
+
message: state.lastResult ? `auto-i18n-extractor: ${state.filesScanned} file(s) scanned, ${state.stringsFound} string(s) found, last ${state.lastResult.path} (${state.lastResult.stringsFound})` : `auto-i18n-extractor: ${state.filesScanned} file(s) scanned, ${state.stringsFound} string(s) found`,
|
|
322
|
+
counters: {
|
|
323
|
+
filesScanned: state.filesScanned,
|
|
324
|
+
stringsFound: state.stringsFound,
|
|
325
|
+
extractions: state.extractions,
|
|
326
|
+
skipped: state.skipped,
|
|
327
|
+
readErrors: state.readErrors
|
|
328
|
+
},
|
|
329
|
+
lastResult: state.lastResult
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var auto_i18n_extractor_default = plugin;
|
|
334
|
+
|
|
335
|
+
export { auto_i18n_extractor_default as default };
|
package/dist/checkpoint.js
CHANGED
|
@@ -33,6 +33,14 @@ function resolveProjectPath(rawPath, cwd = process.cwd()) {
|
|
|
33
33
|
if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
+
function hashContent(s) {
|
|
37
|
+
const cap = Math.min(s.length, 65536);
|
|
38
|
+
let h = 5381;
|
|
39
|
+
for (let i = 0; i < cap; i++) {
|
|
40
|
+
h = (h << 5) + h + s.charCodeAt(i) | 0;
|
|
41
|
+
}
|
|
42
|
+
return h >>> 0;
|
|
43
|
+
}
|
|
36
44
|
function captureFile(path, maxBytes) {
|
|
37
45
|
try {
|
|
38
46
|
const st = statSync(path);
|
|
@@ -117,6 +125,16 @@ var plugin = {
|
|
|
117
125
|
);
|
|
118
126
|
state.captures += 1;
|
|
119
127
|
api.metrics.counter("captures");
|
|
128
|
+
api.emitCustom?.("checkpoint:captured", {
|
|
129
|
+
path: safePath,
|
|
130
|
+
bytes: captured.bytes,
|
|
131
|
+
hadContent: captured.content !== null,
|
|
132
|
+
// 32-bit unsigned hash of the captured bytes. Collisions
|
|
133
|
+
// are tolerable (consumers should compare hashes per-path,
|
|
134
|
+
// not across paths).
|
|
135
|
+
contentHash: captured.content !== null ? hashContent(captured.content) : 0,
|
|
136
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
137
|
+
});
|
|
120
138
|
};
|
|
121
139
|
state.hookUnregister = api.registerHook("PreToolUse", "write|edit", hook);
|
|
122
140
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* code-metrics plugin — lightweight per-file code metrics using regex-based
|
|
5
|
+
* line counting and complexity heuristics.
|
|
6
|
+
*
|
|
7
|
+
* Tools registered:
|
|
8
|
+
* - measure_code_metrics : Compute lines, comments, blanks, function count,
|
|
9
|
+
* and cyclomatic-complexity-like score for a file or directory.
|
|
10
|
+
* - metrics_status : Report config + counters.
|
|
11
|
+
*
|
|
12
|
+
* Hooks registered:
|
|
13
|
+
* - PostToolUse with matcher `write|edit` to source files, injecting a
|
|
14
|
+
* one-line metric summary of the changed file.
|
|
15
|
+
*
|
|
16
|
+
* Config (`config.extensions['code-metrics']`):
|
|
17
|
+
*
|
|
18
|
+
* ```jsonc
|
|
19
|
+
* {
|
|
20
|
+
* "enabled": true,
|
|
21
|
+
* "extensions": [".ts", ".tsx", ".js", ".jsx"],
|
|
22
|
+
* "maxFiles": 50
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
declare const plugin: Plugin;
|
|
30
|
+
|
|
31
|
+
export { plugin as default };
|