@wrongstack/plugins 0.307.1 → 0.308.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.
@@ -27,6 +27,7 @@
27
27
  * @public
28
28
  */
29
29
  import type { Plugin } from '@wrongstack/core/types';
30
+ export declare function parsePackageNames(command: string): string[];
30
31
  declare const plugin: Plugin;
31
32
  export default plugin;
32
33
  //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,383 @@
1
1
  // src/license-audit-gate/index.ts
2
2
  import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- var API_VERSION = "^0.1.10";
4
+
5
+ // src/dep-guard/index.ts
5
6
  var state = {
7
+ invocations: 0,
8
+ installsSeen: 0,
9
+ blocks: 0,
10
+ warns: 0,
11
+ llmConfirmCount: 0,
12
+ llmConfirmErrors: 0,
13
+ lastBlock: null,
14
+ hookUnregister: null
15
+ };
16
+ var DEFAULTS = {
17
+ enabled: true,
18
+ mode: "block",
19
+ deny: [],
20
+ allow: [],
21
+ warnOnUnpinned: false,
22
+ typosquatCheck: true,
23
+ confirmTyposquatsWithLlm: false
24
+ };
25
+ function readConfig(raw) {
26
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
27
+ const r = raw;
28
+ const strings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === "string" && s.length > 0) : [];
29
+ return {
30
+ enabled: r["enabled"] !== false,
31
+ mode: r["mode"] === "warn" ? "warn" : "block",
32
+ deny: strings(r["deny"]),
33
+ allow: strings(r["allow"]),
34
+ warnOnUnpinned: r["warnOnUnpinned"] === true,
35
+ typosquatCheck: r["typosquatCheck"] !== false,
36
+ confirmTyposquatsWithLlm: r["confirmTyposquatsWithLlm"] === true
37
+ };
38
+ }
39
+ var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)|(?:^|[;&|]\s*)(pip3?|uv)\s+(?:pip\s+)?install\s+([^;&|]+)|(?:^|[;&|]\s*)(cargo)\s+add\s+([^;&|]+)/gi;
40
+ function parseInstallCommands(command) {
41
+ const out = [];
42
+ INSTALL_RE.lastIndex = 0;
43
+ let m = INSTALL_RE.exec(command);
44
+ while (m !== null) {
45
+ const manager = (m[1] ?? m[3] ?? m[5] ?? "").toLowerCase();
46
+ const argString = m[2] ?? m[4] ?? m[6] ?? "";
47
+ const packages = [];
48
+ for (const token of argString.split(/\s+/)) {
49
+ if (!token || token.startsWith("-")) continue;
50
+ if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
51
+ const cleaned = token.replace(/^['"]|['"]$/g, "");
52
+ if (!cleaned) continue;
53
+ let name = cleaned;
54
+ let version = null;
55
+ const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
56
+ if (pipMatch?.[1]) {
57
+ name = pipMatch[1];
58
+ const op = pipMatch[2] ?? "";
59
+ const ver = (pipMatch[3] ?? "").trim();
60
+ version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
61
+ } else {
62
+ const at = cleaned.lastIndexOf("@");
63
+ if (at > 0) {
64
+ name = cleaned.slice(0, at);
65
+ version = cleaned.slice(at + 1) || null;
66
+ }
67
+ }
68
+ if (name) packages.push({ name, version });
69
+ }
70
+ out.push({ manager, packages });
71
+ m = INSTALL_RE.exec(command);
72
+ }
73
+ return out;
74
+ }
75
+ function matchesPattern(name, pattern) {
76
+ if (pattern.endsWith("*"))
77
+ return name.toLowerCase().startsWith(pattern.slice(0, -1).toLowerCase());
78
+ return name.toLowerCase() === pattern.toLowerCase();
79
+ }
80
+ var POPULAR_PACKAGES = [
81
+ "react",
82
+ "react-dom",
83
+ "express",
84
+ "lodash",
85
+ "axios",
86
+ "typescript",
87
+ "vite",
88
+ "vitest",
89
+ "next",
90
+ "vue",
91
+ "svelte",
92
+ "zod",
93
+ "prettier",
94
+ "eslint",
95
+ "jest",
96
+ "webpack",
97
+ "commander",
98
+ "chalk",
99
+ "dotenv",
100
+ "requests",
101
+ "numpy",
102
+ "pandas",
103
+ "flask",
104
+ "django",
105
+ "serde",
106
+ "tokio"
107
+ ];
108
+ function editDistance(a, b) {
109
+ if (a === b) return 0;
110
+ if (Math.abs(a.length - b.length) > 2) return 3;
111
+ const d = Array.from({ length: a.length + 1 }, (_, i) => {
112
+ const row = new Array(b.length + 1).fill(0);
113
+ row[0] = i;
114
+ return row;
115
+ });
116
+ for (let j = 0; j <= b.length; j++) d[0][j] = j;
117
+ for (let i = 1; i <= a.length; i++) {
118
+ for (let j = 1; j <= b.length; j++) {
119
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
120
+ const row = d[i];
121
+ const prevRow = d[i - 1];
122
+ row[j] = Math.min(
123
+ prevRow[j] + 1,
124
+ row[j - 1] + 1,
125
+ prevRow[j - 1] + cost
126
+ );
127
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
128
+ row[j] = Math.min(row[j], d[i - 2][j - 2] + 1);
129
+ }
130
+ }
131
+ }
132
+ return d[a.length][b.length];
133
+ }
134
+ function typosquatOf(name) {
135
+ const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
136
+ if (POPULAR_PACKAGES.includes(lower)) return null;
137
+ for (const popular of POPULAR_PACKAGES) {
138
+ if (editDistance(lower, popular) === 1) return popular;
139
+ }
140
+ return null;
141
+ }
142
+ var plugin = {
143
+ name: "dep-guard",
144
+ version: "0.1.0",
145
+ description: "Supervises dependency installs: blocks deny-listed packages, flags typosquat lookalikes, and optionally warns on unpinned versions",
146
+ apiVersion: "^0.1.10",
147
+ capabilities: { tools: true, hooks: true, llm: true },
148
+ defaultConfig: { ...DEFAULTS },
149
+ configSchema: {
150
+ type: "object",
151
+ properties: {
152
+ enabled: { type: "boolean", default: true, description: "Master switch." },
153
+ mode: {
154
+ type: "string",
155
+ enum: ["block", "warn"],
156
+ default: "block",
157
+ description: "How deny-list hits are handled."
158
+ },
159
+ deny: {
160
+ type: "array",
161
+ items: { type: "string" },
162
+ default: [],
163
+ description: 'Package names (exact) or prefix globs ("@evil/*") that must not be installed.'
164
+ },
165
+ allow: {
166
+ type: "array",
167
+ items: { type: "string" },
168
+ default: [],
169
+ description: "Exemptions that override deny."
170
+ },
171
+ warnOnUnpinned: {
172
+ type: "boolean",
173
+ default: false,
174
+ description: "Warn when a package is installed without an explicit version."
175
+ },
176
+ typosquatCheck: {
177
+ type: "boolean",
178
+ default: true,
179
+ description: "Warn when a package name is one edit away from a well-known package."
180
+ },
181
+ confirmTyposquatsWithLlm: {
182
+ type: "boolean",
183
+ default: false,
184
+ description: "Ask the risk-review Council to assess a flagged typosquat, with One Shot fallback. Appended to the warn context, never escalates to a block. Off by default."
185
+ }
186
+ }
187
+ },
188
+ setup(api) {
189
+ state.invocations = 0;
190
+ state.installsSeen = 0;
191
+ state.blocks = 0;
192
+ state.warns = 0;
193
+ state.llmConfirmCount = 0;
194
+ state.llmConfirmErrors = 0;
195
+ state.lastBlock = null;
196
+ if (state.hookUnregister) {
197
+ try {
198
+ state.hookUnregister();
199
+ } catch {
200
+ }
201
+ state.hookUnregister = null;
202
+ }
203
+ const cfg = readConfig(api.config.extensions?.["dep-guard"]);
204
+ const hook = async (input) => {
205
+ if (!cfg.enabled) return;
206
+ state.invocations += 1;
207
+ const ti = input.toolInput ?? {};
208
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
209
+ if (!command) return;
210
+ const installs = parseInstallCommands(command);
211
+ const packages = installs.flatMap((i) => i.packages);
212
+ if (packages.length === 0) return;
213
+ state.installsSeen += 1;
214
+ api.metrics.counter("installs_seen");
215
+ const notes = [];
216
+ for (const pkg of packages) {
217
+ const allowed = cfg.allow.some((p) => matchesPattern(pkg.name, p));
218
+ const denied = !allowed && cfg.deny.some((p) => matchesPattern(pkg.name, p));
219
+ if (denied) {
220
+ if (cfg.mode === "block") {
221
+ state.blocks += 1;
222
+ state.lastBlock = {
223
+ pkg: pkg.name,
224
+ command: command.slice(0, 200),
225
+ when: (/* @__PURE__ */ new Date()).toISOString()
226
+ };
227
+ api.metrics.counter("blocks");
228
+ return {
229
+ decision: "block",
230
+ reason: `dep-guard: package "${pkg.name}" is on the deny list (config.extensions["dep-guard"].deny) \u2014 install refused. Ask the user before adding this dependency, or add an \`allow\` entry.`
231
+ };
232
+ }
233
+ notes.push(
234
+ `"${pkg.name}" is DENY-LISTED \u2014 do not add it without explicit user approval.`
235
+ );
236
+ }
237
+ if (cfg.typosquatCheck) {
238
+ const lookalike = typosquatOf(pkg.name);
239
+ if (lookalike) {
240
+ const baseNote = `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`;
241
+ if (cfg.confirmTyposquatsWithLlm && api.llm) {
242
+ try {
243
+ const question = `Classify whether npm package "${pkg.name}" is likely a typo or typosquat of "${lookalike}".`;
244
+ const council = api.llm.council ? await api.llm.council(question, {
245
+ context: "The only supplied evidence is that the names have edit distance 1. Do not invent registry, download, ownership, or provenance facts.",
246
+ profile: "risk-review",
247
+ options: [
248
+ { id: "typo", label: "Likely typo or typosquat" },
249
+ { id: "real", label: "Likely distinct real package" },
250
+ { id: "uncertain", label: "Insufficient evidence" }
251
+ ]
252
+ }) : null;
253
+ const councilVerdict = council?.status === "decided" && council.optionId ? `${council.optionId.toUpperCase()}: ${council.reason ?? council.answer ?? "no rationale"}` : null;
254
+ const t = councilVerdict ? councilVerdict : (await api.llm.complete(
255
+ `${question} Reply with ONE sentence starting with "TYPO:", "REAL:", or "UNCERTAIN:".`,
256
+ {
257
+ system: "You are a supply-chain security assistant. Use only supplied evidence and preserve uncertainty.",
258
+ role: "security-reviewer",
259
+ maxTokens: 100
260
+ }
261
+ )).text.trim();
262
+ if (t) {
263
+ state.llmConfirmCount += 1;
264
+ api.metrics.counter("llm_confirm");
265
+ notes.push(`${baseNote} LLM verdict: ${t.slice(0, 300)}`);
266
+ } else {
267
+ notes.push(baseNote);
268
+ }
269
+ } catch {
270
+ state.llmConfirmErrors += 1;
271
+ notes.push(baseNote);
272
+ }
273
+ } else {
274
+ notes.push(baseNote);
275
+ }
276
+ }
277
+ }
278
+ if (cfg.warnOnUnpinned && pkg.version === null) {
279
+ notes.push(`"${pkg.name}" has no pinned version \u2014 consider "${pkg.name}@<version>".`);
280
+ }
281
+ }
282
+ if (notes.length > 0) {
283
+ state.warns += 1;
284
+ api.metrics.counter("warns");
285
+ return {
286
+ decision: "allow",
287
+ additionalContext: `dep-guard:
288
+ ${notes.map((n) => ` - ${n}`).join("\n")}`
289
+ };
290
+ }
291
+ return {
292
+ decision: "allow",
293
+ additionalContext: `dep-guard: this command adds ${packages.length} dependenc${packages.length === 1 ? "y" : "ies"}: ${packages.map((p) => p.name).join(", ")}. Confirm each is intentional.`
294
+ };
295
+ };
296
+ state.hookUnregister = api.registerHook("PreToolUse", "bash|exec", hook, {
297
+ name: "dep-guard",
298
+ stage: "validate",
299
+ failurePolicy: "closed",
300
+ policy: true
301
+ });
302
+ api.tools.register({
303
+ name: "dep_guard_status",
304
+ description: "Reports dep-guard state: deny/allow lists, mode, and counters (installs seen, blocks, warns).",
305
+ inputSchema: { type: "object", properties: {} },
306
+ permission: "auto",
307
+ category: "Diagnostics",
308
+ mutating: false,
309
+ async execute() {
310
+ return {
311
+ ok: true,
312
+ enabled: cfg.enabled,
313
+ mode: cfg.mode,
314
+ deny: cfg.deny,
315
+ allow: cfg.allow,
316
+ warnOnUnpinned: cfg.warnOnUnpinned,
317
+ typosquatCheck: cfg.typosquatCheck,
318
+ counters: {
319
+ invocations: state.invocations,
320
+ installsSeen: state.installsSeen,
321
+ llmConfirmCount: state.llmConfirmCount,
322
+ llmConfirmErrors: state.llmConfirmErrors,
323
+ blocks: state.blocks,
324
+ warns: state.warns
325
+ },
326
+ lastBlock: state.lastBlock
327
+ };
328
+ }
329
+ });
330
+ api.log.info("dep-guard plugin loaded", {
331
+ version: "0.1.0",
332
+ enabled: cfg.enabled,
333
+ mode: cfg.mode,
334
+ denyCount: cfg.deny.length
335
+ });
336
+ },
337
+ teardown(api) {
338
+ if (state.hookUnregister) {
339
+ try {
340
+ state.hookUnregister();
341
+ } catch {
342
+ }
343
+ state.hookUnregister = null;
344
+ }
345
+ const final = {
346
+ invocations: state.invocations,
347
+ installsSeen: state.installsSeen,
348
+ llmConfirmCount: state.llmConfirmCount,
349
+ llmConfirmErrors: state.llmConfirmErrors,
350
+ blocks: state.blocks,
351
+ warns: state.warns
352
+ };
353
+ state.invocations = 0;
354
+ state.installsSeen = 0;
355
+ state.blocks = 0;
356
+ state.warns = 0;
357
+ state.llmConfirmCount = 0;
358
+ state.llmConfirmErrors = 0;
359
+ state.lastBlock = null;
360
+ api.log.info("dep-guard: teardown complete", { final });
361
+ },
362
+ async health() {
363
+ return {
364
+ ok: true,
365
+ message: state.lastBlock === null ? `dep-guard: ${state.installsSeen} install command(s) seen, ${state.blocks} block(s), ${state.warns} warn(s)` : `dep-guard: last block on "${state.lastBlock.pkg}" at ${state.lastBlock.when}`,
366
+ counters: {
367
+ invocations: state.invocations,
368
+ installsSeen: state.installsSeen,
369
+ llmConfirmCount: state.llmConfirmCount,
370
+ llmConfirmErrors: state.llmConfirmErrors,
371
+ blocks: state.blocks,
372
+ warns: state.warns
373
+ }
374
+ };
375
+ }
376
+ };
377
+
378
+ // src/license-audit-gate/index.ts
379
+ var API_VERSION = "^0.1.10";
380
+ var state2 = {
6
381
  invocations: 0,
7
382
  installsSeen: 0,
8
383
  packagesAudited: 0,
@@ -13,7 +388,7 @@ var state = {
13
388
  lastResult: null,
14
389
  hookUnregister: null
15
390
  };
16
- var DEFAULTS = {
391
+ var DEFAULTS2 = {
17
392
  enabled: true,
18
393
  allowedLicenses: ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "0BSD", "Unlicense"],
19
394
  block: true
@@ -22,13 +397,13 @@ function normalizeStrings(v) {
22
397
  if (!Array.isArray(v)) return [];
23
398
  return v.filter((s) => typeof s === "string" && s.length > 0).map((s) => s.trim());
24
399
  }
25
- function readConfig(raw) {
26
- if (!raw || typeof raw !== "object") return { ...DEFAULTS };
400
+ function readConfig2(raw) {
401
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS2 };
27
402
  const r = raw;
28
403
  const allowed = normalizeStrings(r["allowedLicenses"]);
29
404
  return {
30
405
  enabled: r["enabled"] !== false,
31
- allowedLicenses: allowed.length > 0 ? allowed : [...DEFAULTS.allowedLicenses],
406
+ allowedLicenses: allowed.length > 0 ? allowed : [...DEFAULTS2.allowedLicenses],
32
407
  block: r["block"] !== false
33
408
  };
34
409
  }
@@ -50,24 +425,10 @@ function extractLicenseStrings(pkg) {
50
425
  }
51
426
  return [...new Set(out)];
52
427
  }
53
- var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/gi;
54
428
  function parsePackageNames(command) {
55
- const names = [];
56
- INSTALL_RE.lastIndex = 0;
57
- for (const m of command.matchAll(INSTALL_RE)) {
58
- const argString = m[2] ?? "";
59
- for (const token of argString.split(/\s+/)) {
60
- if (!token || token.startsWith("-")) continue;
61
- if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
62
- const cleaned = token.replace(/^['"]|['"]$/g, "");
63
- if (!cleaned) continue;
64
- let name = cleaned;
65
- const at = cleaned.lastIndexOf("@");
66
- if (at > 0) name = cleaned.slice(0, at);
67
- if (name) names.push(name);
68
- }
69
- }
70
- return [...new Set(names)];
429
+ return [
430
+ ...new Set(parseInstallCommands(command).flatMap((entry) => entry.packages.map((pkg) => pkg.name)))
431
+ ];
71
432
  }
72
433
  function auditPackages(names, allowedLicenses) {
73
434
  const results = [];
@@ -88,13 +449,13 @@ function auditPackages(names, allowedLicenses) {
88
449
  const ok = errors.length === 0 && results.every((r) => r.allowed);
89
450
  return { ok, results, errors };
90
451
  }
91
- var plugin = {
452
+ var plugin2 = {
92
453
  name: "license-audit-gate",
93
454
  version: "0.1.0",
94
455
  description: "PostToolUse hook that audits dependency licenses after package-manager install/add commands and blocks disallowed licenses",
95
456
  apiVersion: API_VERSION,
96
457
  capabilities: { tools: true, hooks: true },
97
- defaultConfig: { ...DEFAULTS },
458
+ defaultConfig: { ...DEFAULTS2 },
98
459
  configSchema: {
99
460
  type: "object",
100
461
  properties: {
@@ -106,7 +467,7 @@ var plugin = {
106
467
  allowedLicenses: {
107
468
  type: "array",
108
469
  items: { type: "string" },
109
- default: DEFAULTS.allowedLicenses,
470
+ default: DEFAULTS2.allowedLicenses,
110
471
  description: "List of allowed SPDX/license identifiers."
111
472
  },
112
473
  block: {
@@ -117,39 +478,39 @@ var plugin = {
117
478
  }
118
479
  },
119
480
  setup(api) {
120
- state.invocations = 0;
121
- state.installsSeen = 0;
122
- state.packagesAudited = 0;
123
- state.allowedCount = 0;
124
- state.deniedCount = 0;
125
- state.blockedCount = 0;
126
- state.errorCount = 0;
127
- state.lastResult = null;
128
- if (state.hookUnregister) {
481
+ state2.invocations = 0;
482
+ state2.installsSeen = 0;
483
+ state2.packagesAudited = 0;
484
+ state2.allowedCount = 0;
485
+ state2.deniedCount = 0;
486
+ state2.blockedCount = 0;
487
+ state2.errorCount = 0;
488
+ state2.lastResult = null;
489
+ if (state2.hookUnregister) {
129
490
  try {
130
- state.hookUnregister();
491
+ state2.hookUnregister();
131
492
  } catch {
132
493
  }
133
- state.hookUnregister = null;
494
+ state2.hookUnregister = null;
134
495
  }
135
- const cfg = readConfig(api.config.extensions?.["license-audit-gate"]);
496
+ const cfg = readConfig2(api.config.extensions?.["license-audit-gate"]);
136
497
  const hook = (input) => {
137
498
  if (!cfg.enabled) return;
138
499
  if (input.toolResult?.isError) return;
139
500
  const ti = input.toolInput ?? {};
140
501
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
141
502
  if (!command) return;
142
- state.invocations += 1;
503
+ state2.invocations += 1;
143
504
  const names = parsePackageNames(command);
144
505
  if (names.length === 0) return;
145
- state.installsSeen += 1;
506
+ state2.installsSeen += 1;
146
507
  const audit = auditPackages(names, cfg.allowedLicenses);
147
- state.packagesAudited += names.length;
508
+ state2.packagesAudited += names.length;
148
509
  const denied = audit.results.filter((r) => !r.allowed).map((r) => r.name);
149
- state.allowedCount += audit.results.filter((r) => r.allowed).length;
150
- state.deniedCount += denied.length;
151
- state.errorCount += audit.errors.length;
152
- state.lastResult = {
510
+ state2.allowedCount += audit.results.filter((r) => r.allowed).length;
511
+ state2.deniedCount += denied.length;
512
+ state2.errorCount += audit.errors.length;
513
+ state2.lastResult = {
153
514
  passed: audit.ok,
154
515
  denied,
155
516
  when: (/* @__PURE__ */ new Date()).toISOString()
@@ -167,7 +528,7 @@ var plugin = {
167
528
  Allowed licenses: ${cfg.allowedLicenses.join(", ")}
168
529
  ` + lines.join("\n");
169
530
  if (cfg.block) {
170
- state.blockedCount += 1;
531
+ state2.blockedCount += 1;
171
532
  return {
172
533
  decision: "block",
173
534
  reason: message,
@@ -176,7 +537,7 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
176
537
  }
177
538
  return { additionalContext: message };
178
539
  };
179
- state.hookUnregister = api.registerHook("PostToolUse", "bash|exec", hook);
540
+ state2.hookUnregister = api.registerHook("PostToolUse", "bash|exec", hook);
180
541
  api.tools.register({
181
542
  name: "license_audit_status",
182
543
  description: "Reports license-audit-gate state: allowlist, block mode, and per-session audit counters.",
@@ -191,15 +552,15 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
191
552
  allowedLicenses: cfg.allowedLicenses,
192
553
  block: cfg.block,
193
554
  counters: {
194
- invocations: state.invocations,
195
- installsSeen: state.installsSeen,
196
- packagesAudited: state.packagesAudited,
197
- allowed: state.allowedCount,
198
- denied: state.deniedCount,
199
- blocked: state.blockedCount,
200
- errors: state.errorCount
555
+ invocations: state2.invocations,
556
+ installsSeen: state2.installsSeen,
557
+ packagesAudited: state2.packagesAudited,
558
+ allowed: state2.allowedCount,
559
+ denied: state2.deniedCount,
560
+ blocked: state2.blockedCount,
561
+ errors: state2.errorCount
201
562
  },
202
- lastResult: state.lastResult
563
+ lastResult: state2.lastResult
203
564
  };
204
565
  }
205
566
  });
@@ -210,50 +571,51 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
210
571
  });
211
572
  },
212
573
  teardown(api) {
213
- if (state.hookUnregister) {
574
+ if (state2.hookUnregister) {
214
575
  try {
215
- state.hookUnregister();
576
+ state2.hookUnregister();
216
577
  } catch {
217
578
  }
218
- state.hookUnregister = null;
579
+ state2.hookUnregister = null;
219
580
  }
220
581
  const final = {
221
- invocations: state.invocations,
222
- installsSeen: state.installsSeen,
223
- packagesAudited: state.packagesAudited,
224
- allowed: state.allowedCount,
225
- denied: state.deniedCount,
226
- blocked: state.blockedCount,
227
- errors: state.errorCount
582
+ invocations: state2.invocations,
583
+ installsSeen: state2.installsSeen,
584
+ packagesAudited: state2.packagesAudited,
585
+ allowed: state2.allowedCount,
586
+ denied: state2.deniedCount,
587
+ blocked: state2.blockedCount,
588
+ errors: state2.errorCount
228
589
  };
229
- state.invocations = 0;
230
- state.installsSeen = 0;
231
- state.packagesAudited = 0;
232
- state.allowedCount = 0;
233
- state.deniedCount = 0;
234
- state.blockedCount = 0;
235
- state.errorCount = 0;
236
- state.lastResult = null;
590
+ state2.invocations = 0;
591
+ state2.installsSeen = 0;
592
+ state2.packagesAudited = 0;
593
+ state2.allowedCount = 0;
594
+ state2.deniedCount = 0;
595
+ state2.blockedCount = 0;
596
+ state2.errorCount = 0;
597
+ state2.lastResult = null;
237
598
  api.log.info("license-audit-gate: teardown complete", { final });
238
599
  },
239
600
  async health() {
240
601
  return {
241
602
  ok: true,
242
- message: state.lastResult ? `license-audit-gate: ${state.installsSeen} install command(s) seen, ${state.deniedCount} denied, ${state.blockedCount} blocked` : `license-audit-gate: ${state.invocations} invocation(s), ${state.packagesAudited} package(s) audited`,
603
+ message: state2.lastResult ? `license-audit-gate: ${state2.installsSeen} install command(s) seen, ${state2.deniedCount} denied, ${state2.blockedCount} blocked` : `license-audit-gate: ${state2.invocations} invocation(s), ${state2.packagesAudited} package(s) audited`,
243
604
  counters: {
244
- invocations: state.invocations,
245
- installsSeen: state.installsSeen,
246
- packagesAudited: state.packagesAudited,
247
- allowed: state.allowedCount,
248
- denied: state.deniedCount,
249
- blocked: state.blockedCount,
250
- errors: state.errorCount
605
+ invocations: state2.invocations,
606
+ installsSeen: state2.installsSeen,
607
+ packagesAudited: state2.packagesAudited,
608
+ allowed: state2.allowedCount,
609
+ denied: state2.deniedCount,
610
+ blocked: state2.blockedCount,
611
+ errors: state2.errorCount
251
612
  },
252
- lastResult: state.lastResult
613
+ lastResult: state2.lastResult
253
614
  };
254
615
  }
255
616
  };
256
- var license_audit_gate_default = plugin;
617
+ var license_audit_gate_default = plugin2;
257
618
  export {
258
- license_audit_gate_default as default
619
+ license_audit_gate_default as default,
620
+ parsePackageNames
259
621
  };
@@ -26,6 +26,19 @@
26
26
  * @public
27
27
  */
28
28
  import type { Plugin } from '@wrongstack/core/types';
29
+ interface ResolvedLinter {
30
+ /** Always the current Node executable; never a shell or package-manager shim. */
31
+ cmd: string;
32
+ /** Local package bin entry followed by linter-specific arguments. */
33
+ args: string[];
34
+ name: 'biome' | 'eslint';
35
+ }
36
+ /**
37
+ * Resolve a linter's project-local JavaScript bin entry from package metadata.
38
+ * Using `node <entry>` avoids npx, shell configuration, and Windows `.cmd`
39
+ * shims while still respecting Node's normal project-local package lookup.
40
+ */
41
+ export declare function resolveLocalLinter(name: 'biome' | 'eslint', cwd: string): ResolvedLinter | null;
29
42
  declare const plugin: Plugin;
30
43
  export default plugin;
31
44
  //# sourceMappingURL=index.d.ts.map