@wrongstack/plugins 0.281.1 → 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,298 @@
|
|
|
1
|
+
// src/agent-handoff/index.ts
|
|
2
|
+
var API_VERSION = "^0.1.10";
|
|
3
|
+
var state = {
|
|
4
|
+
invocationCount: 0,
|
|
5
|
+
sentCount: 0,
|
|
6
|
+
skippedCount: 0,
|
|
7
|
+
errorCount: 0,
|
|
8
|
+
lastMessageId: null,
|
|
9
|
+
lastPayloadHash: "",
|
|
10
|
+
eventUnsubscribers: []
|
|
11
|
+
};
|
|
12
|
+
var DEFAULTS = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
subjectPrefix: "handoff: ",
|
|
15
|
+
includeResult: true,
|
|
16
|
+
includeTodos: true,
|
|
17
|
+
maxBodyChars: 4e3,
|
|
18
|
+
to: "leader"
|
|
19
|
+
};
|
|
20
|
+
function readConfig(raw) {
|
|
21
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
22
|
+
const r = raw;
|
|
23
|
+
return {
|
|
24
|
+
enabled: r["enabled"] !== false,
|
|
25
|
+
subjectPrefix: typeof r["subjectPrefix"] === "string" ? r["subjectPrefix"] : DEFAULTS.subjectPrefix,
|
|
26
|
+
includeResult: r["includeResult"] !== false,
|
|
27
|
+
includeTodos: r["includeTodos"] !== false,
|
|
28
|
+
maxBodyChars: typeof r["maxBodyChars"] === "number" && r["maxBodyChars"] >= 500 ? r["maxBodyChars"] : DEFAULTS.maxBodyChars,
|
|
29
|
+
to: typeof r["to"] === "string" ? r["to"] : DEFAULTS.to
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function truncate(s, max) {
|
|
33
|
+
return s.length > max ? s.slice(0, max) + `
|
|
34
|
+
|
|
35
|
+
[truncated ${s.length - max} chars]` : s;
|
|
36
|
+
}
|
|
37
|
+
function hashString(s) {
|
|
38
|
+
let h = 2166136261;
|
|
39
|
+
for (let i = 0; i < s.length; i++) {
|
|
40
|
+
h ^= s.charCodeAt(i);
|
|
41
|
+
h = h * 16777619 >>> 0;
|
|
42
|
+
}
|
|
43
|
+
return h.toString(16);
|
|
44
|
+
}
|
|
45
|
+
function buildBody(payload, cfg) {
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push("# Handoff note");
|
|
48
|
+
if (payload.agentName || payload.agentId) {
|
|
49
|
+
lines.push(`**From:** ${payload.agentName ?? payload.agentId ?? "unknown"}`);
|
|
50
|
+
}
|
|
51
|
+
if (payload.task) lines.push(`**Task:** ${payload.task}`);
|
|
52
|
+
if (payload.status) lines.push(`**Status:** ${payload.status}`);
|
|
53
|
+
lines.push(`**Time:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
if (cfg.includeResult && payload.result !== void 0) {
|
|
56
|
+
lines.push("## Result");
|
|
57
|
+
lines.push("```json");
|
|
58
|
+
lines.push(JSON.stringify(payload.result, null, 2));
|
|
59
|
+
lines.push("```");
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
if (payload.summary) {
|
|
63
|
+
lines.push("## Summary");
|
|
64
|
+
lines.push(payload.summary);
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
if (cfg.includeTodos && Array.isArray(payload.todos) && payload.todos.length > 0) {
|
|
68
|
+
lines.push("## Todos");
|
|
69
|
+
for (const t of payload.todos) {
|
|
70
|
+
const status = t.status ?? "unknown";
|
|
71
|
+
const content = t.content ?? t.id ?? "untitled";
|
|
72
|
+
lines.push(`- [${status}] ${content}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
}
|
|
76
|
+
return truncate(lines.join("\n"), cfg.maxBodyChars);
|
|
77
|
+
}
|
|
78
|
+
async function sendHandoff(cfg, mailbox, payload) {
|
|
79
|
+
const body = buildBody(payload, cfg);
|
|
80
|
+
const hash = hashString(body);
|
|
81
|
+
if (hash === state.lastPayloadHash) {
|
|
82
|
+
state.skippedCount += 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const subject = `${cfg.subjectPrefix}${payload.agentName ?? payload.agentId ?? "subagent"} \u2014 ${payload.status ?? "done"}`.slice(
|
|
86
|
+
0,
|
|
87
|
+
200
|
|
88
|
+
);
|
|
89
|
+
const sendInput = {
|
|
90
|
+
from: "plugin:agent-handoff",
|
|
91
|
+
to: cfg.to,
|
|
92
|
+
type: "status",
|
|
93
|
+
subject,
|
|
94
|
+
body,
|
|
95
|
+
priority: "normal"
|
|
96
|
+
};
|
|
97
|
+
const result = await mailbox.send(sendInput);
|
|
98
|
+
state.sentCount += 1;
|
|
99
|
+
state.lastMessageId = result.id ?? null;
|
|
100
|
+
state.lastPayloadHash = hash;
|
|
101
|
+
}
|
|
102
|
+
var plugin = {
|
|
103
|
+
name: "agent-handoff",
|
|
104
|
+
version: "0.1.0",
|
|
105
|
+
description: "Listens for subagent.done events and posts structured handoff notes to the project mailbox",
|
|
106
|
+
apiVersion: API_VERSION,
|
|
107
|
+
capabilities: { tools: true, hooks: true },
|
|
108
|
+
defaultConfig: { ...DEFAULTS },
|
|
109
|
+
configSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
113
|
+
subjectPrefix: {
|
|
114
|
+
type: "string",
|
|
115
|
+
default: DEFAULTS.subjectPrefix,
|
|
116
|
+
description: "Prepended to handoff message subjects."
|
|
117
|
+
},
|
|
118
|
+
includeResult: {
|
|
119
|
+
type: "boolean",
|
|
120
|
+
default: true,
|
|
121
|
+
description: "Include the subagent result payload in the handoff body."
|
|
122
|
+
},
|
|
123
|
+
includeTodos: {
|
|
124
|
+
type: "boolean",
|
|
125
|
+
default: true,
|
|
126
|
+
description: "Include remaining/completed todos from the subagent."
|
|
127
|
+
},
|
|
128
|
+
maxBodyChars: {
|
|
129
|
+
type: "number",
|
|
130
|
+
minimum: 500,
|
|
131
|
+
default: 4e3,
|
|
132
|
+
description: "Hard cap on handoff body size."
|
|
133
|
+
},
|
|
134
|
+
to: {
|
|
135
|
+
type: "string",
|
|
136
|
+
default: "leader",
|
|
137
|
+
description: "Default mailbox recipient for handoff notes."
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
setup(api) {
|
|
142
|
+
state.invocationCount = 0;
|
|
143
|
+
state.sentCount = 0;
|
|
144
|
+
state.skippedCount = 0;
|
|
145
|
+
state.errorCount = 0;
|
|
146
|
+
state.lastMessageId = null;
|
|
147
|
+
state.lastPayloadHash = "";
|
|
148
|
+
for (const off of state.eventUnsubscribers) {
|
|
149
|
+
try {
|
|
150
|
+
off();
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
state.eventUnsubscribers = [];
|
|
155
|
+
const cfg = readConfig(api.config.extensions?.["agent-handoff"]);
|
|
156
|
+
const mailbox = api.mailbox;
|
|
157
|
+
if (cfg.enabled && api.onEvent) {
|
|
158
|
+
const offDone = api.onEvent("subagent.done", (payload) => {
|
|
159
|
+
state.invocationCount += 1;
|
|
160
|
+
if (!mailbox) {
|
|
161
|
+
state.skippedCount += 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const p = payload ?? {};
|
|
165
|
+
sendHandoff(cfg, mailbox, p).catch((err) => {
|
|
166
|
+
state.errorCount += 1;
|
|
167
|
+
api.log.warn("agent-handoff: mailbox.send failed", {
|
|
168
|
+
error: err instanceof Error ? err.message : String(err)
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
state.eventUnsubscribers.push(offDone);
|
|
173
|
+
}
|
|
174
|
+
api.tools.register({
|
|
175
|
+
name: "handoff_note",
|
|
176
|
+
description: "Manually compose and send a handoff note to another agent via the project mailbox.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
to: { type: "string", description: "Recipient (default: leader)." },
|
|
181
|
+
task: { type: "string", description: "Short task description." },
|
|
182
|
+
summary: { type: "string", description: "What the next agent needs to know." },
|
|
183
|
+
todos: {
|
|
184
|
+
type: "array",
|
|
185
|
+
items: { type: "object" },
|
|
186
|
+
description: "Optional todo list to carry forward."
|
|
187
|
+
},
|
|
188
|
+
result: { description: "Optional structured result payload." }
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
permission: "auto",
|
|
192
|
+
category: "Coordination",
|
|
193
|
+
mutating: false,
|
|
194
|
+
async execute(input) {
|
|
195
|
+
if (!cfg.enabled) return { ok: false, error: "agent-handoff is disabled" };
|
|
196
|
+
if (!mailbox) return { ok: false, error: "mailbox not available" };
|
|
197
|
+
const payload = {
|
|
198
|
+
agentName: "manual",
|
|
199
|
+
task: input.task,
|
|
200
|
+
summary: input.summary,
|
|
201
|
+
result: input.result,
|
|
202
|
+
todos: Array.isArray(input.todos) ? input.todos : void 0
|
|
203
|
+
};
|
|
204
|
+
const recipient = input.to ?? cfg.to;
|
|
205
|
+
try {
|
|
206
|
+
const body = buildBody(payload, cfg);
|
|
207
|
+
const result = await mailbox.send({
|
|
208
|
+
from: "plugin:agent-handoff",
|
|
209
|
+
to: recipient,
|
|
210
|
+
type: "status",
|
|
211
|
+
subject: `${cfg.subjectPrefix}manual note \u2014 ${payload.task ?? "handoff"}`.slice(0, 200),
|
|
212
|
+
body,
|
|
213
|
+
priority: "normal"
|
|
214
|
+
});
|
|
215
|
+
state.sentCount += 1;
|
|
216
|
+
state.lastMessageId = result.id ?? null;
|
|
217
|
+
return { ok: true, messageId: result.id, to: recipient };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
state.errorCount += 1;
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: err instanceof Error ? err.message : String(err)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
api.tools.register({
|
|
228
|
+
name: "handoff_status",
|
|
229
|
+
description: "Reports agent-handoff counters and the last handoff message id.",
|
|
230
|
+
inputSchema: { type: "object", properties: {} },
|
|
231
|
+
permission: "auto",
|
|
232
|
+
category: "Diagnostics",
|
|
233
|
+
mutating: false,
|
|
234
|
+
async execute() {
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
enabled: cfg.enabled,
|
|
238
|
+
subjectPrefix: cfg.subjectPrefix,
|
|
239
|
+
includeResult: cfg.includeResult,
|
|
240
|
+
includeTodos: cfg.includeTodos,
|
|
241
|
+
maxBodyChars: cfg.maxBodyChars,
|
|
242
|
+
defaultRecipient: cfg.to,
|
|
243
|
+
mailboxAvailable: Boolean(mailbox),
|
|
244
|
+
counters: {
|
|
245
|
+
invocations: state.invocationCount,
|
|
246
|
+
sent: state.sentCount,
|
|
247
|
+
skipped: state.skippedCount,
|
|
248
|
+
errors: state.errorCount
|
|
249
|
+
},
|
|
250
|
+
lastMessageId: state.lastMessageId
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
api.log.info("agent-handoff plugin loaded", {
|
|
255
|
+
version: "0.1.0",
|
|
256
|
+
enabled: cfg.enabled,
|
|
257
|
+
mailboxAvailable: Boolean(mailbox)
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
teardown(api) {
|
|
261
|
+
for (const off of state.eventUnsubscribers) {
|
|
262
|
+
try {
|
|
263
|
+
off();
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
state.eventUnsubscribers = [];
|
|
268
|
+
const final = {
|
|
269
|
+
invocations: state.invocationCount,
|
|
270
|
+
sent: state.sentCount,
|
|
271
|
+
skipped: state.skippedCount,
|
|
272
|
+
errors: state.errorCount
|
|
273
|
+
};
|
|
274
|
+
state.invocationCount = 0;
|
|
275
|
+
state.sentCount = 0;
|
|
276
|
+
state.skippedCount = 0;
|
|
277
|
+
state.errorCount = 0;
|
|
278
|
+
state.lastMessageId = null;
|
|
279
|
+
state.lastPayloadHash = "";
|
|
280
|
+
api.log.info("agent-handoff: teardown complete", { final });
|
|
281
|
+
},
|
|
282
|
+
async health() {
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
message: `agent-handoff: ${state.invocationCount} event(s), ${state.sentCount} sent, ${state.skippedCount} skipped, ${state.errorCount} error(s)`,
|
|
286
|
+
counters: {
|
|
287
|
+
invocations: state.invocationCount,
|
|
288
|
+
sent: state.sentCount,
|
|
289
|
+
skipped: state.skippedCount,
|
|
290
|
+
errors: state.errorCount
|
|
291
|
+
},
|
|
292
|
+
lastMessageId: state.lastMessageId
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var agent_handoff_default = plugin;
|
|
297
|
+
|
|
298
|
+
export { agent_handoff_default as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* api-compatibility-gate plugin — guards public API surface changes.
|
|
5
|
+
*
|
|
6
|
+
* Registers a PostToolUse hook on `write|edit` to entry-point files
|
|
7
|
+
* (`index.ts`, `src/index.ts`, or package `main`/`module`/`exports`
|
|
8
|
+
* targets). For each such edit it reads the previous version from git
|
|
9
|
+
* (`git show HEAD:<path>`), parses exported identifiers with simple
|
|
10
|
+
* regex heuristics, and reports any removed exports as breaking changes.
|
|
11
|
+
*
|
|
12
|
+
* Tools registered:
|
|
13
|
+
* - api_compat_status : Show config + per-session counters.
|
|
14
|
+
*
|
|
15
|
+
* Hooks registered:
|
|
16
|
+
* - PostToolUse with matcher `write|edit`.
|
|
17
|
+
*
|
|
18
|
+
* Config (`config.extensions['api-compatibility-gate']`):
|
|
19
|
+
*
|
|
20
|
+
* ```jsonc
|
|
21
|
+
* {
|
|
22
|
+
* "enabled": true,
|
|
23
|
+
* "severity": "warn", // "warn" | "block"
|
|
24
|
+
* "entryPointPatterns": [ // glob-style patterns
|
|
25
|
+
* "**/index.ts",
|
|
26
|
+
* "**/index.js",
|
|
27
|
+
* "**/src/index.ts"
|
|
28
|
+
* ],
|
|
29
|
+
* "trackGitHistory": true // read pre-edit source from git
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
declare const plugin: Plugin;
|
|
37
|
+
|
|
38
|
+
export { plugin as default };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { relative, isAbsolute, dirname, resolve, basename, extname } from 'path';
|
|
4
|
+
|
|
5
|
+
// src/api-compatibility-gate/index.ts
|
|
6
|
+
var API_VERSION = "^0.1.10";
|
|
7
|
+
var state = {
|
|
8
|
+
invocationCount: 0,
|
|
9
|
+
skippedCount: 0,
|
|
10
|
+
checkedCount: 0,
|
|
11
|
+
cleanCount: 0,
|
|
12
|
+
breakingCount: 0,
|
|
13
|
+
errorCount: 0,
|
|
14
|
+
lastResult: null,
|
|
15
|
+
hookUnregister: null
|
|
16
|
+
};
|
|
17
|
+
var DEFAULTS = {
|
|
18
|
+
enabled: false,
|
|
19
|
+
severity: "warn",
|
|
20
|
+
entryPointPatterns: ["**/index.ts", "**/index.js", "**/src/index.ts", "src/index.ts"],
|
|
21
|
+
trackGitHistory: true
|
|
22
|
+
};
|
|
23
|
+
function readConfig(raw) {
|
|
24
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
25
|
+
const r = raw;
|
|
26
|
+
return {
|
|
27
|
+
enabled: r["enabled"] !== false,
|
|
28
|
+
severity: r["severity"] === "block" ? "block" : DEFAULTS.severity,
|
|
29
|
+
entryPointPatterns: Array.isArray(r["entryPointPatterns"]) ? r["entryPointPatterns"].filter((x) => typeof x === "string") : DEFAULTS.entryPointPatterns,
|
|
30
|
+
trackGitHistory: r["trackGitHistory"] !== false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function normalizePath(p) {
|
|
34
|
+
return p.replace(/\\/g, "/");
|
|
35
|
+
}
|
|
36
|
+
function patternToRegex(pattern) {
|
|
37
|
+
const normalized = normalizePath(pattern);
|
|
38
|
+
let regex = "^";
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < normalized.length) {
|
|
41
|
+
const c = normalized[i];
|
|
42
|
+
if (c === "*" && normalized[i + 1] === "*") {
|
|
43
|
+
if (normalized[i + 2] === "/") {
|
|
44
|
+
regex += "(?:.*/)?";
|
|
45
|
+
i += 3;
|
|
46
|
+
} else {
|
|
47
|
+
regex += ".*";
|
|
48
|
+
i += 2;
|
|
49
|
+
}
|
|
50
|
+
} else if (c === "*") {
|
|
51
|
+
regex += "[^/]*";
|
|
52
|
+
i += 1;
|
|
53
|
+
} else if (c === "?") {
|
|
54
|
+
regex += "[^/]";
|
|
55
|
+
i += 1;
|
|
56
|
+
} else {
|
|
57
|
+
regex += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
regex += "$";
|
|
62
|
+
return new RegExp(regex);
|
|
63
|
+
}
|
|
64
|
+
function matchesAnyPattern(filePath, patterns) {
|
|
65
|
+
const normalized = normalizePath(filePath);
|
|
66
|
+
return patterns.some((p) => patternToRegex(p).test(normalized));
|
|
67
|
+
}
|
|
68
|
+
function resolveToProjectRoot(filePath) {
|
|
69
|
+
if (isAbsolute(filePath)) return resolve(filePath);
|
|
70
|
+
return resolve(process.cwd(), filePath);
|
|
71
|
+
}
|
|
72
|
+
function withinProject(filePath) {
|
|
73
|
+
if (typeof filePath !== "string" || filePath.length === 0 || filePath.length > 4096) return false;
|
|
74
|
+
const resolved = resolveToProjectRoot(filePath);
|
|
75
|
+
const rel = relative(process.cwd(), resolved);
|
|
76
|
+
if (rel === "" || rel === ".") return true;
|
|
77
|
+
if (rel.startsWith("..")) return false;
|
|
78
|
+
if (isAbsolute(rel)) return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function isPackageEntryPoint(filePath) {
|
|
82
|
+
const resolved = resolveToProjectRoot(filePath);
|
|
83
|
+
const dir = dirname(resolved);
|
|
84
|
+
const pkgPath = resolve(dir, "package.json");
|
|
85
|
+
if (!existsSync(pkgPath)) return false;
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
88
|
+
const entries = [];
|
|
89
|
+
if (pkg.main) entries.push(pkg.main);
|
|
90
|
+
if (pkg.module) entries.push(pkg.module);
|
|
91
|
+
if (pkg.exports) {
|
|
92
|
+
if (typeof pkg.exports === "string") {
|
|
93
|
+
entries.push(pkg.exports);
|
|
94
|
+
} else if (typeof pkg.exports === "object" && pkg.exports !== null) {
|
|
95
|
+
const dot = pkg.exports["."];
|
|
96
|
+
if (typeof dot === "string") entries.push(dot);
|
|
97
|
+
else if (dot && typeof dot === "object") {
|
|
98
|
+
for (const v of Object.values(dot)) {
|
|
99
|
+
if (typeof v === "string") entries.push(v);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const entryResolved = resolve(dir, entry);
|
|
106
|
+
if (entryResolved === resolved) return true;
|
|
107
|
+
if (basename(dirname(entryResolved)) === basename(dirname(resolved)) && basename(entryResolved, extname(entryResolved)) === basename(resolved, extname(resolved))) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function isEntryPoint(filePath, patterns) {
|
|
116
|
+
if (!withinProject(filePath)) return false;
|
|
117
|
+
if (matchesAnyPattern(filePath, patterns)) return true;
|
|
118
|
+
return isPackageEntryPoint(filePath);
|
|
119
|
+
}
|
|
120
|
+
function extractExports(source) {
|
|
121
|
+
const exports = /* @__PURE__ */ new Set();
|
|
122
|
+
const declRegex = /export\s+(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
123
|
+
for (const match of source.matchAll(declRegex)) {
|
|
124
|
+
exports.add(match[1]);
|
|
125
|
+
}
|
|
126
|
+
const namedRegex = /export\s*\{([^}]*)\}/g;
|
|
127
|
+
for (const match of source.matchAll(namedRegex)) {
|
|
128
|
+
const clauses = match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
129
|
+
for (const clause of clauses) {
|
|
130
|
+
const parts = clause.split(/\s+as\s+/);
|
|
131
|
+
const exportedName = parts.length > 1 ? parts[parts.length - 1].trim() : parts[0].trim();
|
|
132
|
+
if (exportedName) exports.add(exportedName);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const starRegex = /export\s*\*\s*from\s*['"]([^'"]+)['"]/g;
|
|
136
|
+
for (const match of source.matchAll(starRegex)) {
|
|
137
|
+
exports.add(`* from '${match[1]}'`);
|
|
138
|
+
}
|
|
139
|
+
if (/\bexport\s+default\b/.test(source)) {
|
|
140
|
+
exports.add("default");
|
|
141
|
+
}
|
|
142
|
+
const cjsRegex = /module\.exports\s*=\s*\{([^}]*)\}/g;
|
|
143
|
+
for (const match of source.matchAll(cjsRegex)) {
|
|
144
|
+
const clauses = match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
145
|
+
for (const clause of clauses) {
|
|
146
|
+
const key = clause.split(":")[0].trim().replace(/['"]/g, "");
|
|
147
|
+
if (key) exports.add(key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const cjsPropRegex = /module\.exports\.([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g;
|
|
151
|
+
for (const match of source.matchAll(cjsPropRegex)) {
|
|
152
|
+
exports.add(match[1]);
|
|
153
|
+
}
|
|
154
|
+
return exports;
|
|
155
|
+
}
|
|
156
|
+
function readGitVersion(filePath) {
|
|
157
|
+
const resolved = resolveToProjectRoot(filePath);
|
|
158
|
+
const relPath = normalizePath(relative(process.cwd(), resolved));
|
|
159
|
+
try {
|
|
160
|
+
const out = execFileSync("git", ["show", `HEAD:${relPath}`], {
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
cwd: process.cwd(),
|
|
163
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
164
|
+
});
|
|
165
|
+
return out;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function readCurrentFile(filePath) {
|
|
171
|
+
try {
|
|
172
|
+
const resolved = resolveToProjectRoot(filePath);
|
|
173
|
+
return readFileSync(resolved, "utf-8");
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function compareExports(before, after) {
|
|
179
|
+
const beforeExports = extractExports(before);
|
|
180
|
+
const afterExports = extractExports(after);
|
|
181
|
+
const removed = [...beforeExports].filter((e) => !afterExports.has(e)).sort();
|
|
182
|
+
const added = [...afterExports].filter((e) => !beforeExports.has(e)).sort();
|
|
183
|
+
return { removed, added };
|
|
184
|
+
}
|
|
185
|
+
var plugin = {
|
|
186
|
+
name: "api-compatibility-gate",
|
|
187
|
+
version: "0.1.0",
|
|
188
|
+
description: "PostToolUse hook that detects breaking API changes (removed exports) in entry-point files after writes or edits",
|
|
189
|
+
apiVersion: API_VERSION,
|
|
190
|
+
capabilities: { tools: true, hooks: true },
|
|
191
|
+
defaultConfig: { ...DEFAULTS },
|
|
192
|
+
configSchema: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
196
|
+
severity: {
|
|
197
|
+
type: "string",
|
|
198
|
+
enum: ["warn", "block"],
|
|
199
|
+
default: "warn",
|
|
200
|
+
description: "warn = inject context; block = refuse the mutating tool."
|
|
201
|
+
},
|
|
202
|
+
entryPointPatterns: {
|
|
203
|
+
type: "array",
|
|
204
|
+
items: { type: "string" },
|
|
205
|
+
default: DEFAULTS.entryPointPatterns,
|
|
206
|
+
description: "Glob-style patterns for entry-point files to guard."
|
|
207
|
+
},
|
|
208
|
+
trackGitHistory: {
|
|
209
|
+
type: "boolean",
|
|
210
|
+
default: true,
|
|
211
|
+
description: "Read the pre-edit version from git HEAD to compute removed exports."
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
setup(api) {
|
|
216
|
+
state.invocationCount = 0;
|
|
217
|
+
state.skippedCount = 0;
|
|
218
|
+
state.checkedCount = 0;
|
|
219
|
+
state.cleanCount = 0;
|
|
220
|
+
state.breakingCount = 0;
|
|
221
|
+
state.errorCount = 0;
|
|
222
|
+
state.lastResult = null;
|
|
223
|
+
if (state.hookUnregister) {
|
|
224
|
+
try {
|
|
225
|
+
state.hookUnregister();
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
state.hookUnregister = null;
|
|
229
|
+
}
|
|
230
|
+
const cfg = readConfig(api.config.extensions?.["api-compatibility-gate"]);
|
|
231
|
+
const hook = (input) => {
|
|
232
|
+
if (!cfg.enabled) return;
|
|
233
|
+
if (input.toolResult?.isError) return;
|
|
234
|
+
const inp = input.toolInput ?? {};
|
|
235
|
+
const filePath = inp["path"];
|
|
236
|
+
if (!filePath || typeof filePath !== "string") return;
|
|
237
|
+
state.invocationCount += 1;
|
|
238
|
+
if (!isEntryPoint(filePath, cfg.entryPointPatterns)) {
|
|
239
|
+
state.skippedCount += 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!cfg.trackGitHistory) {
|
|
243
|
+
state.skippedCount += 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const before = readGitVersion(filePath);
|
|
247
|
+
if (before === null) {
|
|
248
|
+
state.skippedCount += 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const after = readCurrentFile(filePath);
|
|
252
|
+
if (after === null) {
|
|
253
|
+
state.errorCount += 1;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
state.checkedCount += 1;
|
|
257
|
+
const { removed, added } = compareExports(before, after);
|
|
258
|
+
state.lastResult = {
|
|
259
|
+
filePath,
|
|
260
|
+
removed,
|
|
261
|
+
added,
|
|
262
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
263
|
+
};
|
|
264
|
+
if (removed.length === 0) {
|
|
265
|
+
state.cleanCount += 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
state.breakingCount += 1;
|
|
269
|
+
const addedLine = added.length > 0 ? `Added exports: ${added.join(", ")}
|
|
270
|
+
` : "";
|
|
271
|
+
const message = `
|
|
272
|
+
\u26A0\uFE0F api-compatibility-gate: ${filePath} removed exported identifiers: ${removed.join(", ")}
|
|
273
|
+
` + addedLine + `These removals are breaking changes. Restore them or bump the major version.`;
|
|
274
|
+
if (cfg.severity === "block") {
|
|
275
|
+
return {
|
|
276
|
+
decision: "block",
|
|
277
|
+
reason: message,
|
|
278
|
+
additionalContext: message
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { additionalContext: message };
|
|
282
|
+
};
|
|
283
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
284
|
+
api.tools.register({
|
|
285
|
+
name: "api_compat_status",
|
|
286
|
+
description: "Reports api-compatibility-gate state: severity, entry-point patterns, and per-session counters.",
|
|
287
|
+
inputSchema: { type: "object", properties: {} },
|
|
288
|
+
permission: "auto",
|
|
289
|
+
category: "Diagnostics",
|
|
290
|
+
mutating: false,
|
|
291
|
+
async execute() {
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
enabled: cfg.enabled,
|
|
295
|
+
severity: cfg.severity,
|
|
296
|
+
entryPointPatterns: cfg.entryPointPatterns,
|
|
297
|
+
trackGitHistory: cfg.trackGitHistory,
|
|
298
|
+
counters: {
|
|
299
|
+
invocations: state.invocationCount,
|
|
300
|
+
skipped: state.skippedCount,
|
|
301
|
+
checked: state.checkedCount,
|
|
302
|
+
clean: state.cleanCount,
|
|
303
|
+
breaking: state.breakingCount,
|
|
304
|
+
errors: state.errorCount
|
|
305
|
+
},
|
|
306
|
+
lastResult: state.lastResult
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
api.log.info("api-compatibility-gate plugin loaded", {
|
|
311
|
+
version: "0.1.0",
|
|
312
|
+
severity: cfg.severity,
|
|
313
|
+
entryPointPatterns: cfg.entryPointPatterns
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
teardown(api) {
|
|
317
|
+
if (state.hookUnregister) {
|
|
318
|
+
try {
|
|
319
|
+
state.hookUnregister();
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
state.hookUnregister = null;
|
|
323
|
+
}
|
|
324
|
+
const final = {
|
|
325
|
+
invocations: state.invocationCount,
|
|
326
|
+
checked: state.checkedCount,
|
|
327
|
+
clean: state.cleanCount,
|
|
328
|
+
breaking: state.breakingCount,
|
|
329
|
+
errors: state.errorCount
|
|
330
|
+
};
|
|
331
|
+
state.invocationCount = 0;
|
|
332
|
+
state.skippedCount = 0;
|
|
333
|
+
state.checkedCount = 0;
|
|
334
|
+
state.cleanCount = 0;
|
|
335
|
+
state.breakingCount = 0;
|
|
336
|
+
state.errorCount = 0;
|
|
337
|
+
state.lastResult = null;
|
|
338
|
+
api.log.info("api-compatibility-gate: teardown complete", { final });
|
|
339
|
+
},
|
|
340
|
+
async health() {
|
|
341
|
+
return {
|
|
342
|
+
ok: state.errorCount === 0,
|
|
343
|
+
message: state.lastResult ? `api-compatibility-gate: ${state.checkedCount} checked, last ${state.lastResult.removed.length > 0 ? "BREAKING" : "clean"} (${state.lastResult.removed.length} removed)` : `api-compatibility-gate: ${state.invocationCount} invocation(s), ${state.checkedCount} checked`,
|
|
344
|
+
counters: {
|
|
345
|
+
invocations: state.invocationCount,
|
|
346
|
+
skipped: state.skippedCount,
|
|
347
|
+
checked: state.checkedCount,
|
|
348
|
+
clean: state.cleanCount,
|
|
349
|
+
breaking: state.breakingCount,
|
|
350
|
+
errors: state.errorCount
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var api_compatibility_gate_default = plugin;
|
|
356
|
+
|
|
357
|
+
export { api_compatibility_gate_default as default };
|