@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,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/branch-guard.d.ts
CHANGED
|
@@ -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
|
|
13
|
-
* the tool call itself (git_autocommit). If the current
|
|
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
|
package/dist/branch-guard.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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();
|
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 };
|