@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.
Files changed (78) hide show
  1. package/README.md +29 -3
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-i18n-extractor.d.ts +36 -0
  9. package/dist/auto-i18n-extractor.js +335 -0
  10. package/dist/checkpoint.js +18 -0
  11. package/dist/code-metrics.d.ts +31 -0
  12. package/dist/code-metrics.js +338 -0
  13. package/dist/commit-validator.js +67 -12
  14. package/dist/cost-tracker.js +58 -16
  15. package/dist/dead-code-detector.d.ts +34 -0
  16. package/dist/dead-code-detector.js +354 -0
  17. package/dist/dep-guard.js +47 -6
  18. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  19. package/dist/dependency-vulnerability-gate.js +308 -0
  20. package/dist/diff-summary.js +97 -10
  21. package/dist/doc-sync-guard.d.ts +33 -0
  22. package/dist/doc-sync-guard.js +223 -0
  23. package/dist/duplicate-code-detector.d.ts +33 -0
  24. package/dist/duplicate-code-detector.js +384 -0
  25. package/dist/feature-flag-tracker.d.ts +38 -0
  26. package/dist/feature-flag-tracker.js +316 -0
  27. package/dist/file-watcher.js +85 -36
  28. package/dist/format-on-save.js +76 -10
  29. package/dist/import-organizer.js +73 -14
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.js +14067 -5054
  32. package/dist/interface-contract-guard.d.ts +37 -0
  33. package/dist/interface-contract-guard.js +302 -0
  34. package/dist/knowledge-graph.d.ts +45 -0
  35. package/dist/knowledge-graph.js +325 -0
  36. package/dist/license-audit-gate.d.ts +34 -0
  37. package/dist/license-audit-gate.js +260 -0
  38. package/dist/llm-cache.js +5 -0
  39. package/dist/loop-breaker.d.ts +0 -38
  40. package/dist/loop-breaker.js +209 -8
  41. package/dist/migration-planner.d.ts +30 -0
  42. package/dist/migration-planner.js +349 -0
  43. package/dist/model-router.js +5 -0
  44. package/dist/performance-regression-gate.d.ts +33 -0
  45. package/dist/performance-regression-gate.js +315 -0
  46. package/dist/plugin-stack-observer.d.ts +35 -0
  47. package/dist/plugin-stack-observer.js +125 -0
  48. package/dist/pr-drafter.d.ts +35 -0
  49. package/dist/pr-drafter.js +334 -0
  50. package/dist/prompt-firewall.js +5 -0
  51. package/dist/refactor-suggester.d.ts +38 -0
  52. package/dist/refactor-suggester.js +382 -0
  53. package/dist/release-notes-generator.d.ts +27 -0
  54. package/dist/release-notes-generator.js +209 -0
  55. package/dist/schema-evolution-guard.d.ts +42 -0
  56. package/dist/schema-evolution-guard.js +319 -0
  57. package/dist/security-hotspot-scanner.d.ts +30 -0
  58. package/dist/security-hotspot-scanner.js +402 -0
  59. package/dist/semantic-search-indexer.d.ts +38 -0
  60. package/dist/semantic-search-indexer.js +436 -0
  61. package/dist/shell-check.js +38 -3
  62. package/dist/smart-rename.d.ts +26 -0
  63. package/dist/smart-rename.js +170 -0
  64. package/dist/spec-linker.js +273 -133
  65. package/dist/test-coverage-gate.d.ts +37 -0
  66. package/dist/test-coverage-gate.js +263 -0
  67. package/dist/test-flake-detector.d.ts +27 -0
  68. package/dist/test-flake-detector.js +274 -0
  69. package/dist/test-generator.d.ts +32 -0
  70. package/dist/test-generator.js +243 -0
  71. package/dist/test-runner-gate.js +154 -22
  72. package/dist/todo-listener.d.ts +2 -2
  73. package/dist/todo-listener.js +5 -5
  74. package/dist/token-throttle.js +5 -0
  75. package/dist/type-gate.d.ts +37 -0
  76. package/dist/type-gate.js +311 -0
  77. package/package.json +112 -4
  78. 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 };
@@ -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 };