@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.
Files changed (84) hide show
  1. package/README.md +30 -4
  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-escalate.d.ts +1 -1
  9. package/dist/auto-i18n-extractor.d.ts +36 -0
  10. package/dist/auto-i18n-extractor.js +335 -0
  11. package/dist/branch-guard.d.ts +6 -5
  12. package/dist/branch-guard.js +54 -4
  13. package/dist/checkpoint.js +18 -0
  14. package/dist/code-metrics.d.ts +31 -0
  15. package/dist/code-metrics.js +338 -0
  16. package/dist/commit-validator.js +67 -12
  17. package/dist/context-pins.js +4 -4
  18. package/dist/cost-tracker.d.ts +1 -1
  19. package/dist/cost-tracker.js +58 -19
  20. package/dist/cron.js +18 -8
  21. package/dist/dead-code-detector.d.ts +34 -0
  22. package/dist/dead-code-detector.js +354 -0
  23. package/dist/dep-guard.js +47 -6
  24. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  25. package/dist/dependency-vulnerability-gate.js +308 -0
  26. package/dist/diff-summary.js +97 -10
  27. package/dist/doc-sync-guard.d.ts +33 -0
  28. package/dist/doc-sync-guard.js +223 -0
  29. package/dist/duplicate-code-detector.d.ts +33 -0
  30. package/dist/duplicate-code-detector.js +384 -0
  31. package/dist/feature-flag-tracker.d.ts +38 -0
  32. package/dist/feature-flag-tracker.js +316 -0
  33. package/dist/file-watcher.js +85 -36
  34. package/dist/format-on-save.js +99 -18
  35. package/dist/import-organizer.js +73 -14
  36. package/dist/index.d.ts +27 -0
  37. package/dist/index.js +11205 -2106
  38. package/dist/interface-contract-guard.d.ts +37 -0
  39. package/dist/interface-contract-guard.js +302 -0
  40. package/dist/knowledge-graph.d.ts +45 -0
  41. package/dist/knowledge-graph.js +325 -0
  42. package/dist/license-audit-gate.d.ts +34 -0
  43. package/dist/license-audit-gate.js +260 -0
  44. package/dist/llm-cache.js +5 -0
  45. package/dist/loop-breaker.d.ts +0 -38
  46. package/dist/loop-breaker.js +209 -8
  47. package/dist/migration-planner.d.ts +30 -0
  48. package/dist/migration-planner.js +349 -0
  49. package/dist/model-router.js +5 -0
  50. package/dist/performance-regression-gate.d.ts +33 -0
  51. package/dist/performance-regression-gate.js +315 -0
  52. package/dist/plugin-stack-observer.d.ts +35 -0
  53. package/dist/plugin-stack-observer.js +138 -0
  54. package/dist/pr-drafter.d.ts +35 -0
  55. package/dist/pr-drafter.js +334 -0
  56. package/dist/prompt-firewall.js +5 -0
  57. package/dist/refactor-suggester.d.ts +38 -0
  58. package/dist/refactor-suggester.js +382 -0
  59. package/dist/release-notes-generator.d.ts +27 -0
  60. package/dist/release-notes-generator.js +209 -0
  61. package/dist/schema-evolution-guard.d.ts +42 -0
  62. package/dist/schema-evolution-guard.js +319 -0
  63. package/dist/security-hotspot-scanner.d.ts +30 -0
  64. package/dist/security-hotspot-scanner.js +402 -0
  65. package/dist/semantic-search-indexer.d.ts +38 -0
  66. package/dist/semantic-search-indexer.js +436 -0
  67. package/dist/shell-check.js +38 -3
  68. package/dist/smart-rename.d.ts +26 -0
  69. package/dist/smart-rename.js +170 -0
  70. package/dist/spec-linker.js +273 -133
  71. package/dist/test-coverage-gate.d.ts +37 -0
  72. package/dist/test-coverage-gate.js +263 -0
  73. package/dist/test-flake-detector.d.ts +27 -0
  74. package/dist/test-flake-detector.js +274 -0
  75. package/dist/test-generator.d.ts +32 -0
  76. package/dist/test-generator.js +243 -0
  77. package/dist/test-runner-gate.js +154 -22
  78. package/dist/todo-listener.d.ts +2 -2
  79. package/dist/todo-listener.js +5 -5
  80. package/dist/token-throttle.js +5 -0
  81. package/dist/type-gate.d.ts +37 -0
  82. package/dist/type-gate.js +311 -0
  83. package/package.json +112 -4
  84. 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 };
@@ -8,17 +8,18 @@ import { Plugin } from '@wrongstack/core';
8
8
  * - branch_guard_status : Show protected branches, mode, and counters.
9
9
  *
10
10
  * Hooks registered:
11
- * - PreToolUse with matcher `bash|git_autocommit`. Inspects the tool
12
- * input for git commit / push / merge / rebase commands (bash) or
13
- * the tool call itself (git_autocommit). If the current branch is
14
- * protected, the call is blocked with a clear reason.
11
+ * - PreToolUse with matcher `bash|git|git_autocommit`. Inspects the tool
12
+ * input for git commit / push / merge commands (bash), structured git
13
+ * operations (git), or the tool call itself (git_autocommit). If the current
14
+ * branch is protected, the call is blocked with a clear reason.
15
15
  *
16
16
  * Config (`config.extensions['branch-guard']`):
17
17
  *
18
18
  * ```jsonc
19
19
  * {
20
+ * "enabled": true, // set false to make the hook a no-op
20
21
  * "branches": ["main", "master"], // protected branch names
21
- * "mode": "block", // "block" | "warn"
22
+ * "mode": "block", // "block" | "warn" | "off"
22
23
  * "blockMerge": true, // also block merges into protected
23
24
  * "blockPush": true, // also block pushes from protected
24
25
  * "blockCommit": true // also block commits on protected
@@ -7,9 +7,11 @@ var state = {
7
7
  blockCount: 0,
8
8
  warnCount: 0,
9
9
  hookUnregister: null,
10
+ configUnregister: null,
10
11
  lastBlock: null
11
12
  };
12
13
  var DEFAULTS = {
14
+ enabled: true,
13
15
  branches: ["main", "master"],
14
16
  mode: "block",
15
17
  blockCommit: true,
@@ -20,14 +22,36 @@ function readConfig(raw) {
20
22
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
21
23
  const r = raw;
22
24
  const branches = Array.isArray(r["branches"]) ? r["branches"].filter((b) => typeof b === "string") : DEFAULTS.branches;
25
+ const mode = r["mode"] === "warn" ? "warn" : r["mode"] === "off" ? "off" : "block";
23
26
  return {
27
+ enabled: r["enabled"] !== false && mode !== "off",
24
28
  branches: branches.length > 0 ? branches : DEFAULTS.branches,
25
- mode: r["mode"] === "warn" ? "warn" : "block",
29
+ mode,
26
30
  blockCommit: r["blockCommit"] !== false,
27
31
  blockPush: r["blockPush"] !== false,
28
32
  blockMerge: r["blockMerge"] !== false
29
33
  };
30
34
  }
35
+ function readHostConfig(raw) {
36
+ const host = raw && typeof raw === "object" ? raw : {};
37
+ const extensions = host["extensions"];
38
+ const branchGuardOptions = extensions && typeof extensions === "object" ? extensions["branch-guard"] : void 0;
39
+ const cfg = readConfig(branchGuardOptions);
40
+ if (hasDisabledPluginEntry(host["plugins"])) {
41
+ return { ...cfg, enabled: false, mode: "off" };
42
+ }
43
+ return cfg;
44
+ }
45
+ function hasDisabledPluginEntry(raw) {
46
+ if (!Array.isArray(raw)) return false;
47
+ return raw.some((entry) => {
48
+ if (!entry || typeof entry !== "object") return false;
49
+ const r = entry;
50
+ if (r["enabled"] !== false) return false;
51
+ const name = typeof r["name"] === "string" ? r["name"] : "";
52
+ return name === "branch-guard" || name === "@wrongstack/plugins/branch-guard";
53
+ });
54
+ }
31
55
  function getCurrentBranch(cwd) {
32
56
  try {
33
57
  const branch = execSync("git branch --show-current", {
@@ -67,6 +91,16 @@ function detectGitCommand(command) {
67
91
  }
68
92
  return null;
69
93
  }
94
+ function detectStructuredGitCommand(input) {
95
+ const command = input["command"];
96
+ if (command === "commit") {
97
+ if (input["dry_run"] === true) return null;
98
+ return { type: "commit", snippet: "git commit" };
99
+ }
100
+ if (command === "push") return { type: "push", snippet: "git push" };
101
+ if (command === "merge") return { type: "merge", snippet: "git merge" };
102
+ return null;
103
+ }
70
104
  function shouldBlock(op, cfg) {
71
105
  if (op === "commit") return cfg.blockCommit;
72
106
  if (op === "push") return cfg.blockPush;
@@ -117,14 +151,18 @@ var plugin = {
117
151
  state.blockCount = 0;
118
152
  state.warnCount = 0;
119
153
  state.hookUnregister = null;
154
+ state.configUnregister = null;
120
155
  state.lastBlock = null;
121
- const cfg = readConfig(api.config.extensions?.["branch-guard"]);
156
+ let cfg = readHostConfig(api.config);
157
+ state.configUnregister = api.onConfigChange((next) => {
158
+ cfg = readHostConfig(next);
159
+ });
122
160
  const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
123
- const protectedSet = new Set(cfg.branches);
124
161
  const hook = (input) => {
125
162
  const toolName = input.toolName ?? "";
126
163
  const inp = input.toolInput ?? {};
127
164
  state.invocationCount += 1;
165
+ if (!cfg.enabled || cfg.mode === "off") return;
128
166
  let gitOp = null;
129
167
  if (toolName === "git_autocommit") {
130
168
  if (inp["dry_run"] === true) return;
@@ -133,11 +171,14 @@ var plugin = {
133
171
  const command = inp["command"];
134
172
  if (typeof command !== "string") return;
135
173
  gitOp = detectGitCommand(command);
174
+ } else if (toolName === "git") {
175
+ gitOp = detectStructuredGitCommand(inp);
136
176
  }
137
177
  if (!gitOp) return;
138
178
  if (!shouldBlock(gitOp.type, cfg)) return;
139
179
  const branch = getCurrentBranch(cwd);
140
180
  if (!branch) return;
181
+ const protectedSet = new Set(cfg.branches);
141
182
  if (!protectedSet.has(branch)) return;
142
183
  const when = (/* @__PURE__ */ new Date()).toISOString();
143
184
  const opVerb = gitOp.type === "commit" ? "committing to" : gitOp.type === "push" ? "pushing from" : "merging into";
@@ -174,7 +215,7 @@ var plugin = {
174
215
  \u26A0\uFE0F branch-guard: you are ${opVerb} protected branch '${branch}'. ` + (hasUncommitted ? `You have uncommitted changes \u2014 consider \`git stash\` before switching branches. ` : "") + `Use a feature branch instead. Protected: ${cfg.branches.join(", ")}.`
175
216
  };
176
217
  };
177
- state.hookUnregister = api.registerHook("PreToolUse", "bash|git_autocommit", hook);
218
+ state.hookUnregister = api.registerHook("PreToolUse", "bash|git|git_autocommit", hook);
178
219
  api.tools.register({
179
220
  name: "branch_guard_status",
180
221
  description: "Reports branch-guard state: protected branches, mode, and per-session invocation/block/warn counters.",
@@ -185,6 +226,7 @@ var plugin = {
185
226
  async execute() {
186
227
  return {
187
228
  ok: true,
229
+ enabled: cfg.enabled,
188
230
  branches: cfg.branches,
189
231
  mode: cfg.mode,
190
232
  blockCommit: cfg.blockCommit,
@@ -201,11 +243,19 @@ var plugin = {
201
243
  });
202
244
  api.log.info("branch-guard plugin loaded", {
203
245
  version: "0.1.0",
246
+ enabled: cfg.enabled,
204
247
  branches: cfg.branches,
205
248
  mode: cfg.mode
206
249
  });
207
250
  },
208
251
  teardown(api) {
252
+ if (state.configUnregister) {
253
+ try {
254
+ state.configUnregister();
255
+ } catch {
256
+ }
257
+ state.configUnregister = null;
258
+ }
209
259
  if (state.hookUnregister) {
210
260
  try {
211
261
  state.hookUnregister();
@@ -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 };