@wrongstack/plugins 0.307.0 → 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.
package/dist/index.js CHANGED
@@ -345,6 +345,83 @@ function releaseHandle(off) {
345
345
  return null;
346
346
  }
347
347
 
348
+ // src/runtime/redos-guard.ts
349
+ import { Worker } from "node:worker_threads";
350
+ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
351
+ const opts = { budgetMs, ...options };
352
+ const start = Date.now();
353
+ const workerSource = buildWorkerSource(re.source, input, re.flags);
354
+ const worker = new Worker(workerSource, {
355
+ eval: true,
356
+ name: `redos-guard:${re.source.slice(0, 32)}`
357
+ });
358
+ return new Promise((resolve29) => {
359
+ let settled = false;
360
+ const onMessage = (msg) => {
361
+ if (settled) return;
362
+ settled = true;
363
+ clearTimeout(timer);
364
+ worker.terminate().catch(() => {
365
+ });
366
+ if (!msg.ok) {
367
+ resolve29({ timedOut: true, match: null });
368
+ return;
369
+ }
370
+ resolve29({ timedOut: false, match: msg.match });
371
+ };
372
+ const onError = () => {
373
+ if (settled) return;
374
+ settled = true;
375
+ clearTimeout(timer);
376
+ worker.terminate().catch(() => {
377
+ });
378
+ resolve29({ timedOut: true, match: null });
379
+ };
380
+ const timer = setTimeout(() => {
381
+ if (settled) return;
382
+ settled = true;
383
+ const elapsedMs = Date.now() - start;
384
+ worker.terminate().catch(() => {
385
+ });
386
+ try {
387
+ opts.onTimeout?.({
388
+ regex: re,
389
+ input,
390
+ budgetMs: opts.budgetMs,
391
+ elapsedMs
392
+ });
393
+ } catch {
394
+ }
395
+ resolve29({ timedOut: true, match: null });
396
+ }, opts.budgetMs);
397
+ timer.unref?.();
398
+ worker.on("message", onMessage);
399
+ worker.on("error", onError);
400
+ });
401
+ }
402
+ function buildWorkerSource(source, input, flags) {
403
+ const S = JSON.stringify(source);
404
+ const I = JSON.stringify(input);
405
+ const F = JSON.stringify(flags);
406
+ return `
407
+ const { parentPort } = require('node:worker_threads');
408
+ const source = ${S};
409
+ const input = ${I};
410
+ const flags = ${F};
411
+ try {
412
+ const re = new RegExp(source, flags);
413
+ const match = re.exec(input);
414
+ // parentPort.postMessage, NOT bare postMessage: with eval:true
415
+ // workers this Node version does not expose the bare postMessage
416
+ // global \u2014 the worker throws ReferenceError at startup and the
417
+ // host misreads it as a timeout (positive-path regression).
418
+ parentPort.postMessage({ ok: true, match });
419
+ } catch (err) {
420
+ parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
421
+ }
422
+ `;
423
+ }
424
+
348
425
  // src/runtime/index.ts
349
426
  var META_CHARS = /["'`;&|<>\r\n]/;
350
427
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
@@ -446,6 +523,7 @@ function runRunnerCommand(argv, options) {
446
523
  }
447
524
  let timedOut = false;
448
525
  let spawnErrored = false;
526
+ const start = Date.now();
449
527
  const stdoutChunks = [];
450
528
  const stderrChunks = [];
451
529
  let stdoutBytes = 0;
@@ -475,6 +553,15 @@ function runRunnerCommand(argv, options) {
475
553
  timeout: options.timeoutMs,
476
554
  signal: options.signal,
477
555
  maxBuffer: MAX_BUFFER_BYTES,
556
+ // execFile defaults `encoding` to 'utf8', which makes the
557
+ // stdout/stderr `data` events emit *strings*. The chunk arrays
558
+ // below are typed Buffer[] and every consumer runs them through
559
+ // Buffer.concat(...).toString('utf8'), which throws
560
+ // ERR_INVALID_ARG_TYPE on any non-empty string output. Pin the
561
+ // streams to buffers so the declared contract holds (regression:
562
+ // runRunnerCommand crashed on any child that actually wrote
563
+ // output; only the maxBuffer fixture exercised this path).
564
+ encoding: "buffer",
478
565
  windowsHide: true,
479
566
  shell: false,
480
567
  ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
@@ -503,11 +590,31 @@ function runRunnerCommand(argv, options) {
503
590
  }
504
591
  if (err) {
505
592
  const anyErr = err;
593
+ if ((anyErr.killed === true || anyErr.signal === "SIGTERM") && // maxBuffer overflow must NOT be misreported as a timeout
594
+ // — it's a real failure (the child wrote too much) and
595
+ // downstream callers (type-gate/index.ts:227) return
596
+ // null on timedOut=true, which would silently swallow
597
+ // maxBuffer overflow into a confusing empty-output
598
+ // result. Skip the timeout resolve when the err shape
599
+ // names maxBuffer explicitly.
600
+ !/maxBuffer length exceeded/i.test(anyErr.message ?? "") && // And only count it as a timeout if the wall clock has
601
+ // actually elapsed past the budget. External SIGTERMs and
602
+ // races against the exit handler don't satisfy this.
603
+ Date.now() - start >= options.timeoutMs) {
604
+ resolvePromise({
605
+ code: null,
606
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
607
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
608
+ timedOut: true,
609
+ spawnError: false
610
+ });
611
+ return;
612
+ }
506
613
  const code = typeof anyErr.code === "number" ? anyErr.code : 1;
507
614
  resolvePromise({
508
615
  code,
509
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
510
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
616
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
617
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
511
618
  timedOut: false,
512
619
  spawnError: false
513
620
  });
@@ -523,7 +630,7 @@ function runRunnerCommand(argv, options) {
523
630
  }
524
631
  );
525
632
  child.on("exit", (_code, signal) => {
526
- if (signal !== null) {
633
+ if (signal !== null && Date.now() - start >= options.timeoutMs) {
527
634
  timedOut = true;
528
635
  }
529
636
  });
@@ -626,15 +733,40 @@ var INPUT_BUTTON = /<input\b[^>]*\btype\s*=\s*["'](submit|button|reset)["'][^>]*
626
733
  var ATTR_ID = /\bid\s*=\s*["']([^"']+)["']/gi;
627
734
  var ATTR_ALT = /\balt\s*=/i;
628
735
  var ATTR_ARIA_LABEL = /\b(?:aria-label|aria-labelledby)\s*=/i;
736
+ var ATTR_ARIA_DESCRIBEDBY = /\baria-describedby\s*=/i;
629
737
  var ATTR_TITLE = /\btitle\s*=/i;
630
738
  var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
631
739
  var ATTR_VALUE = /\bvalue\s*=/i;
740
+ var ATTR_ROLE_DECORATIVE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
741
+ var SINGLE_FILE_LABEL_NOTE = "Single-file heuristic: a label declared in a sibling component file is not visible to this scan.";
742
+ function escapeRegExp(value) {
743
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
744
+ }
632
745
  function hasMeaningfulAlt(tag) {
633
746
  const m = ATTR_ALT.exec(tag);
634
747
  ATTR_ALT.lastIndex = 0;
635
748
  if (!m) return false;
636
749
  const valMatch = tag.match(/\balt\s*=\s*["']?([^"'\s>]*)["']?/i);
637
- return valMatch ? valMatch[1].trim().length > 0 : false;
750
+ const alt = valMatch ? valMatch[1].trim() : "";
751
+ if (alt.length > 0) return true;
752
+ return ATTR_ROLE_DECORATIVE.test(tag);
753
+ }
754
+ function hasFieldsetLegendLabel(tag, content) {
755
+ const labelledBy = tag.match(/\baria-labelledby\s*=\s*["']([^"']+)["']/i);
756
+ if (labelledBy?.[1]) {
757
+ for (const id of labelledBy[1].split(/\s+/).filter(Boolean)) {
758
+ const idRe = new RegExp(`\\bid\\s*=\\s*["']${escapeRegExp(id)}["']`, "i");
759
+ if (idRe.test(content)) return true;
760
+ }
761
+ }
762
+ const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
763
+ const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
764
+ if (type === "checkbox" || type === "radio") {
765
+ return /<fieldset\b[\s\S]*?<legend\b[\s\S]*?<\/legend>[\s\S]*?<input\b[\s\S]*?<\/fieldset>/i.test(
766
+ content
767
+ );
768
+ }
769
+ return false;
638
770
  }
639
771
  async function auditFile(filePath, projectRoot) {
640
772
  let content;
@@ -646,13 +778,14 @@ async function auditFile(filePath, projectRoot) {
646
778
  const findings = [];
647
779
  const lines = content.split(/\r?\n/);
648
780
  const idsByValue = /* @__PURE__ */ new Map();
649
- function add(line, rule, severity, message) {
781
+ function add(line, rule, severity, message, note) {
650
782
  findings.push({
651
783
  file: relative3(projectRoot, filePath),
652
784
  line,
653
785
  rule,
654
786
  severity,
655
- message
787
+ message,
788
+ ...note ? { note } : {}
656
789
  });
657
790
  }
658
791
  for (let i = 0; i < lines.length; i++) {
@@ -680,17 +813,36 @@ async function auditFile(filePath, projectRoot) {
680
813
  continue;
681
814
  }
682
815
  const hasAriaLabel = ATTR_ARIA_LABEL.test(tag);
816
+ const hasDescribedBy = ATTR_ARIA_DESCRIBEDBY.test(tag);
683
817
  const hasTitle = ATTR_TITLE.test(tag);
684
818
  const idMatchLocal = tag.match(/\bid\s*=\s*["']([^"']+)["']/i);
685
819
  const id = idMatchLocal ? idMatchLocal[1] : null;
686
820
  let hasLabelFor = false;
687
821
  if (id) {
688
- const labelForRe = new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "i");
822
+ const labelForRe = new RegExp(
823
+ `<label\\b[^>]*\\bfor\\s*=\\s*["']${escapeRegExp(id)}["']`,
824
+ "i"
825
+ );
689
826
  hasLabelFor = labelForRe.test(content);
690
827
  }
691
828
  const wrappedInLabel = /<label\b[\s\S]*?<input\b[\s\S]*?<\/label>/i.test(content);
692
- if (!hasAriaLabel && !hasTitle && !hasLabelFor && !wrappedInLabel) {
693
- add(lineNo, "missing-input-label", "error", `<input type="${type}"> is missing an associated label`);
829
+ const hasLegend = hasFieldsetLegendLabel(tag, content);
830
+ const hasPrimaryLabel = hasAriaLabel || hasTitle || hasLabelFor || wrappedInLabel || hasLegend;
831
+ if (!hasPrimaryLabel && hasDescribedBy) {
832
+ add(
833
+ lineNo,
834
+ "low-contrast-placeholder",
835
+ "warning",
836
+ `<input type="${type}"> uses aria-describedby as a description (supplementary, not a primary label)`
837
+ );
838
+ } else if (!hasPrimaryLabel) {
839
+ add(
840
+ lineNo,
841
+ "missing-input-label",
842
+ "error",
843
+ `<input type="${type}"> is missing an associated label`,
844
+ SINGLE_FILE_LABEL_NOTE
845
+ );
694
846
  }
695
847
  if (ATTR_PLACEHOLDER.test(tag)) {
696
848
  add(lineNo, "low-contrast-placeholder", "warning", "<input> uses placeholder text (often low contrast and disappears on input)");
@@ -750,15 +902,24 @@ async function auditPath(rawPath, cfg) {
750
902
  truncated
751
903
  };
752
904
  }
905
+ function truncationWarning(result) {
906
+ if (!result.truncated) return "";
907
+ const unexamined = Math.max(0, result.fileCount - (result.scannedFiles ?? result.fileCount));
908
+ return `partial scan \u2014 ${unexamined} files not examined`;
909
+ }
753
910
  function formatSummary(result) {
911
+ const trunc = truncationWarning(result);
754
912
  if (result.findings.length === 0) {
755
- return `
913
+ const clean = `
756
914
  \u2705 accessibility-auditor: no issues found in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}).`;
915
+ return trunc ? `${clean}
916
+ \u26A0\uFE0F ${trunc}` : clean;
757
917
  }
758
918
  const lines = result.findings.map((f) => ` - ${f.file}:${f.line} \u2014 ${f.message} (${f.rule})`);
759
919
  return `
760
920
  \u26A0\uFE0F accessibility-auditor: ${result.findings.length} issue(s) in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}):
761
- ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids.";
921
+ ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids." + (trunc ? `
922
+ \u26A0\uFE0F ${trunc}` : "");
762
923
  }
763
924
  var plugin = {
764
925
  name: "accessibility-auditor",
@@ -829,9 +990,9 @@ var plugin = {
829
990
  findingCount: result.findings.length,
830
991
  when: (/* @__PURE__ */ new Date()).toISOString()
831
992
  };
832
- if (result.findings.length === 0) return;
993
+ if (result.findings.length === 0 && !result.truncated) return;
833
994
  const summary = formatSummary(result);
834
- if (cfg.severity === "block") {
995
+ if (cfg.severity === "block" && result.findings.length > 0) {
835
996
  return { decision: "block", reason: summary };
836
997
  }
837
998
  return { additionalContext: summary };
@@ -872,6 +1033,7 @@ var plugin = {
872
1033
  findingCount: result.findings.length,
873
1034
  when: (/* @__PURE__ */ new Date()).toISOString()
874
1035
  };
1036
+ const warning = truncationWarning(result);
875
1037
  return {
876
1038
  ok: true,
877
1039
  path: result.path,
@@ -881,7 +1043,8 @@ var plugin = {
881
1043
  // that reports few findings must not read as a clean result.
882
1044
  truncated: result.truncated,
883
1045
  findingCount: result.findings.length,
884
- findings: result.findings
1046
+ findings: result.findings,
1047
+ ...warning ? { additionalContext: `\u26A0\uFE0F ${warning}`, warning } : {}
885
1048
  };
886
1049
  }
887
1050
  });
@@ -3299,9 +3462,9 @@ var plugin9 = {
3299
3462
  const ti = input.toolInput ?? {};
3300
3463
  const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
3301
3464
  if (typeof raw !== "string" || raw.length === 0) return;
3302
- const safePath = resolveProjectPath3(raw);
3303
- if (!safePath) return;
3304
- const captured = await captureFileForHook(safePath, cfg.maxFileBytes, runtime.signal);
3465
+ const safePath2 = resolveProjectPath3(raw);
3466
+ if (!safePath2) return;
3467
+ const captured = await captureFileForHook(safePath2, cfg.maxFileBytes, runtime.signal);
3305
3468
  if (captured === "too-large") {
3306
3469
  state9.skippedLarge += 1;
3307
3470
  return;
@@ -3319,7 +3482,7 @@ var plugin9 = {
3319
3482
  state9.captures += 1;
3320
3483
  api.metrics.counter("captures");
3321
3484
  api.emitCustom?.("checkpoint:captured", {
3322
- path: safePath,
3485
+ path: safePath2,
3323
3486
  bytes: captured.bytes,
3324
3487
  hadContent: captured.content !== null,
3325
3488
  // 32-bit unsigned hash of the captured bytes. Collisions
@@ -3363,12 +3526,12 @@ var plugin9 = {
3363
3526
  const rejectedOutsideProject = [];
3364
3527
  let skipped = 0;
3365
3528
  for (const p of paths) {
3366
- const safePath = resolveProjectPath3(p);
3367
- if (!safePath) {
3529
+ const safePath2 = resolveProjectPath3(p);
3530
+ if (!safePath2) {
3368
3531
  rejectedOutsideProject.push(p);
3369
3532
  continue;
3370
3533
  }
3371
- const captured = await captureFile(safePath, cfg.maxFileBytes);
3534
+ const captured = await captureFile(safePath2, cfg.maxFileBytes);
3372
3535
  if (captured === "too-large") {
3373
3536
  skipped += 1;
3374
3537
  state9.skippedLarge += 1;
@@ -3601,36 +3764,65 @@ function countFunctions(content) {
3601
3764
  for (const _match of content.matchAll(arrowRe)) count++;
3602
3765
  return count;
3603
3766
  }
3767
+ var COMPLEXITY_FORMULA = "control(if|else if|for|while|switch|catch) + (&& || ?? ||= &&= ??=) + ternary ?; optional chaining ?. is not counted";
3604
3768
  function countComplexity(content) {
3605
3769
  const controlRe = /\b(if|else\s+if|for|while|switch|catch)\b/g;
3606
- const operatorRe = /[?&|]/g;
3607
3770
  let complexity = 0;
3608
3771
  controlRe.lastIndex = 0;
3609
3772
  for (const _match of content.matchAll(controlRe)) complexity++;
3610
- operatorRe.lastIndex = 0;
3611
- for (const match of content.matchAll(operatorRe)) {
3612
- const ch = match[0];
3613
- if (ch === "?") {
3773
+ for (let i = 0; i < content.length; i++) {
3774
+ const c = content[i];
3775
+ const n1 = content[i + 1];
3776
+ const n2 = content[i + 2];
3777
+ if (c === "?" && n1 === "?" && n2 === "=") {
3614
3778
  complexity++;
3615
- } else {
3616
- const next = content[match.index + 1];
3617
- if (next === ch) complexity++;
3779
+ i += 2;
3780
+ continue;
3781
+ }
3782
+ if (c === "|" && n1 === "|" && n2 === "=" || c === "&" && n1 === "&" && n2 === "=") {
3783
+ complexity++;
3784
+ i += 2;
3785
+ continue;
3618
3786
  }
3787
+ if (c === "?" && n1 === "?" || c === "|" && n1 === "|" || c === "&" && n1 === "&") {
3788
+ complexity++;
3789
+ i += 1;
3790
+ continue;
3791
+ }
3792
+ if (c === "?" && n1 === ".") {
3793
+ i += 1;
3794
+ continue;
3795
+ }
3796
+ if (c === "?") complexity++;
3619
3797
  }
3620
3798
  return complexity;
3621
3799
  }
3622
3800
  function analyzeFile(filePath, content) {
3801
+ if (content.length === 0) {
3802
+ return {
3803
+ file: relativePath(filePath),
3804
+ lines: 0,
3805
+ codeLines: 0,
3806
+ commentLines: 0,
3807
+ blankLines: 0,
3808
+ functionCount: 0,
3809
+ complexity: 0
3810
+ };
3811
+ }
3623
3812
  const lines = content.split(/\r?\n/);
3624
3813
  let codeLines = 0;
3625
3814
  let commentLines = 0;
3626
3815
  let blankLines = 0;
3627
3816
  let inBlockComment = false;
3628
- for (const rawLine of lines) {
3629
- const line = rawLine.trim();
3817
+ for (let i = 0; i < lines.length; i++) {
3818
+ const line = lines[i].trim();
3630
3819
  if (line.length === 0) {
3631
3820
  blankLines++;
3632
3821
  continue;
3633
3822
  }
3823
+ if (i === 0 && line.startsWith("#!")) {
3824
+ continue;
3825
+ }
3634
3826
  if (inBlockComment) {
3635
3827
  commentLines++;
3636
3828
  if (line.includes("*/")) inBlockComment = false;
@@ -3791,7 +3983,8 @@ var plugin10 = {
3791
3983
  files: state10.fileCount,
3792
3984
  hookInvocations: state10.hookInvocationCount,
3793
3985
  errors: state10.errorCount
3794
- }
3986
+ },
3987
+ complexityFormula: COMPLEXITY_FORMULA
3795
3988
  };
3796
3989
  }
3797
3990
  });
@@ -5860,10 +6053,12 @@ function parseInstallCommands(command) {
5860
6053
  if (!cleaned) continue;
5861
6054
  let name = cleaned;
5862
6055
  let version = null;
5863
- const pipMatch = /^([A-Za-z0-9_.-]+)\s*(?:==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
6056
+ const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
5864
6057
  if (pipMatch?.[1]) {
5865
6058
  name = pipMatch[1];
5866
- version = pipMatch[2] ?? null;
6059
+ const op = pipMatch[2] ?? "";
6060
+ const ver = (pipMatch[3] ?? "").trim();
6061
+ version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
5867
6062
  } else {
5868
6063
  const at = cleaned.lastIndexOf("@");
5869
6064
  if (at > 0) {
@@ -6288,13 +6483,12 @@ function exceedsThreshold(report, threshold) {
6288
6483
  if (thresholdRank == null || maxRank == null) return false;
6289
6484
  return maxRank >= thresholdRank;
6290
6485
  }
6291
- var INSTALL_RE2 = /\b(?:npm\s+(?:install|i)|pnpm\s+add|yarn\s+add)\b/i;
6292
6486
  function isInstallCommand(input) {
6293
6487
  if (input.toolName === "install") return true;
6294
6488
  if (input.toolName !== "bash" && input.toolName !== "exec") return false;
6295
6489
  const ti = input.toolInput ?? {};
6296
6490
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
6297
- return INSTALL_RE2.test(command);
6491
+ return parseInstallCommands(command).length > 0;
6298
6492
  }
6299
6493
  var LOCKFILE_MANAGERS = [
6300
6494
  { file: "pnpm-lock.yaml", manager: "pnpm" },
@@ -8067,18 +8261,8 @@ var feature_flag_tracker_default = plugin23;
8067
8261
 
8068
8262
  // src/file-watcher/index.ts
8069
8263
  import { watch as fsWatch } from "node:fs";
8070
- import { isAbsolute as isAbsolute12, join as join3, relative as relative13, resolve as resolve13 } from "node:path";
8264
+ import { join as join3 } from "node:path";
8071
8265
  var API_VERSION16 = "^0.1.10";
8072
- function withinProject3(p) {
8073
- if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
8074
- const root = process.cwd();
8075
- const resolved = isAbsolute12(p) ? resolve13(p) : resolve13(root, p);
8076
- const rel = relative13(root, resolved);
8077
- if (rel === "" || rel === ".") return true;
8078
- if (rel.startsWith("..")) return false;
8079
- if (isAbsolute12(rel)) return false;
8080
- return true;
8081
- }
8082
8266
  var watch_idCounter = 0;
8083
8267
  function nextId() {
8084
8268
  return `watch_${++watch_idCounter}_${Date.now().toString(36)}`;
@@ -8159,7 +8343,7 @@ var plugin24 = {
8159
8343
  }
8160
8344
  const autoIndex = api.config.extensions?.["file-watcher"]?.["autoIndex"] ?? false;
8161
8345
  const indexProjectRoot = api.config.extensions?.["file-watcher"]?.["indexProjectRoot"] ?? "";
8162
- const safeIndexRoot = indexProjectRoot !== "" && withinProject3(indexProjectRoot) ? indexProjectRoot : "";
8346
+ const safeIndexRoot = indexProjectRoot !== "" && withinProject(indexProjectRoot) ? indexProjectRoot : "";
8163
8347
  if (indexProjectRoot !== "" && safeIndexRoot === "") {
8164
8348
  api.log.warn(
8165
8349
  "file-watcher: indexProjectRoot is outside the project root \u2014 using watched dirPath instead",
@@ -8304,7 +8488,7 @@ var plugin24 = {
8304
8488
  }
8305
8489
  const events = input["events"] ?? ["change", "add", "delete"];
8306
8490
  const recursive = input["recursive"] ?? true;
8307
- const bad = paths.find((p) => !withinProject3(p));
8491
+ const bad = paths.find((p) => !withinProject(p));
8308
8492
  if (bad !== void 0) {
8309
8493
  return {
8310
8494
  ok: false,
@@ -8549,6 +8733,7 @@ async function formatFile(filePath, timeoutMs) {
8549
8733
  }
8550
8734
  const formatter = resolveFormatter();
8551
8735
  if (!formatter) return null;
8736
+ const started = Date.now();
8552
8737
  let bytesBefore;
8553
8738
  try {
8554
8739
  bytesBefore = (await stat4(filePath)).size;
@@ -8593,14 +8778,15 @@ async function formatFile(filePath, timeoutMs) {
8593
8778
  } catch {
8594
8779
  return null;
8595
8780
  }
8781
+ const durationMs = Date.now() - started;
8596
8782
  if (bytesAfter !== bytesBefore) {
8597
- return { changed: true, bytesBefore, bytesAfter };
8783
+ return { changed: true, bytesBefore, bytesAfter, durationMs };
8598
8784
  }
8599
8785
  const hashAfter = await sha256File(filePath);
8600
8786
  if (hashBefore === null || hashAfter === null) {
8601
- return { changed: false, bytesBefore, bytesAfter };
8787
+ return { changed: false, bytesBefore, bytesAfter, durationMs };
8602
8788
  }
8603
- return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter };
8789
+ return { changed: hashBefore !== hashAfter, bytesBefore, bytesAfter, durationMs };
8604
8790
  }
8605
8791
  var plugin25 = {
8606
8792
  name: "format-on-save",
@@ -8689,6 +8875,7 @@ var plugin25 = {
8689
8875
  changed: result.changed,
8690
8876
  bytesBefore: result.bytesBefore,
8691
8877
  bytesAfter: result.bytesAfter,
8878
+ durationMs: result.durationMs,
8692
8879
  when: (/* @__PURE__ */ new Date()).toISOString()
8693
8880
  };
8694
8881
  if (result.changed) {
@@ -8743,7 +8930,10 @@ var plugin25 = {
8743
8930
  formatted: state23.formattedCount,
8744
8931
  clean: state23.cleanCount,
8745
8932
  errors: state23.errorCount,
8746
- coveredSkips: state23.coveredSkipCount
8933
+ coveredSkips: state23.coveredSkipCount,
8934
+ bytesBefore: state23.lastResult?.bytesBefore ?? 0,
8935
+ bytesAfter: state23.lastResult?.bytesAfter ?? 0,
8936
+ durationMs: state23.lastResult?.durationMs ?? 0
8747
8937
  },
8748
8938
  lastResult: state23.lastResult
8749
8939
  };
@@ -9316,7 +9506,7 @@ var git_autocommit_default = plugin26;
9316
9506
 
9317
9507
  // src/gitignore-guard/index.ts
9318
9508
  import { access as access3, readFile as readFile9, writeFile as writeFile2 } from "node:fs/promises";
9319
- import { basename as basename4, isAbsolute as isAbsolute13, join as join4, relative as relative14, resolve as resolve14, sep as sep3 } from "node:path";
9509
+ import { basename as basename4, isAbsolute as isAbsolute12, join as join4, relative as relative13, resolve as resolve13, sep as sep3 } from "node:path";
9320
9510
  var API_VERSION19 = "^0.1.10";
9321
9511
  var DEFAULT_ARTIFACT_PATTERNS = Object.freeze([
9322
9512
  "dist/",
@@ -9438,9 +9628,9 @@ async function appendPatterns(target, patterns, limit) {
9438
9628
  }
9439
9629
  function projectRelativePath(rawPath, cwd) {
9440
9630
  const root = cwd ?? process.cwd();
9441
- const abs = isAbsolute13(rawPath) ? rawPath : resolve14(root, rawPath);
9442
- const rel = toForwardSlashes(relative14(root, abs));
9443
- if (rel.startsWith("..") || isAbsolute13(rel) || rel === "") return null;
9631
+ const abs = isAbsolute12(rawPath) ? rawPath : resolve13(root, rawPath);
9632
+ const rel = toForwardSlashes(relative13(root, abs));
9633
+ if (rel.startsWith("..") || isAbsolute12(rel) || rel === "") return null;
9444
9634
  return { abs, rel, root };
9445
9635
  }
9446
9636
  var DEFAULTS21 = {
@@ -9752,7 +9942,7 @@ var gitignore_guard_default = plugin27;
9752
9942
  // src/import-organizer/index.ts
9753
9943
  import { spawn } from "node:child_process";
9754
9944
  import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
9755
- import { basename as basename5, isAbsolute as isAbsolute14 } from "node:path";
9945
+ import { basename as basename5, isAbsolute as isAbsolute13 } from "node:path";
9756
9946
  var ALLOWED_FIRST_TOKENS = /* @__PURE__ */ new Set([
9757
9947
  "npx",
9758
9948
  "pnpm",
@@ -9795,17 +9985,31 @@ function resolveAllowedCommand(command) {
9795
9985
  const tokens = command.split(/\s+/).filter(Boolean);
9796
9986
  if (tokens.length === 0) return null;
9797
9987
  const head = tokens[0];
9988
+ if (PACKAGE_RUNNERS.has(head)) {
9989
+ let i = 1;
9990
+ while (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx" || tokens[i] === "run")) {
9991
+ i++;
9992
+ }
9993
+ const toolToken = tokens[i];
9994
+ if (!toolToken || !(toolToken in LOCAL_BIN_PACKAGES)) return null;
9995
+ }
9996
+ if (head === "node") {
9997
+ const target = tokens[1];
9998
+ if (!target) return null;
9999
+ const base = basename5(target);
10000
+ if (!(base in LOCAL_BIN_PACKAGES) && !ALLOWED_FIRST_TOKENS.has(base)) return null;
10001
+ }
9798
10002
  const local = ALLOWED_FIRST_TOKENS.has(head) ? resolveLocalToolCommand(tokens) : null;
9799
10003
  if (local) return local;
9800
10004
  if (ALLOWED_FIRST_TOKENS.has(head)) {
9801
10005
  return { cmd: head, args: tokens.slice(1) };
9802
10006
  }
9803
- if (isAbsolute14(head)) {
10007
+ if (isAbsolute13(head)) {
9804
10008
  if (!withinProject(head)) return null;
9805
10009
  const base = basename5(head);
9806
10010
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
9807
10011
  }
9808
- if (!isAbsolute14(head) && withinProject(head)) {
10012
+ if (!isAbsolute13(head) && withinProject(head)) {
9809
10013
  const base = basename5(head);
9810
10014
  if (ALLOWED_FIRST_TOKENS.has(base)) return { cmd: head, args: tokens.slice(1) };
9811
10015
  }
@@ -10032,6 +10236,12 @@ var plugin28 = {
10032
10236
  bytesAfter: result.bytesAfter,
10033
10237
  when: (/* @__PURE__ */ new Date()).toISOString()
10034
10238
  };
10239
+ if (/\boxlint\b/.test(result.command)) {
10240
+ return {
10241
+ additionalContext: `
10242
+ \u{1F4E6} import-organizer: oxlint has no import-organize support \u2014 ran '${result.command}' as a no-op for import sorting on '${filePath}'.`
10243
+ };
10244
+ }
10035
10245
  if (result.changed) {
10036
10246
  state25.organizedCount += 1;
10037
10247
  const delta = result.bytesAfter - result.bytesBefore;
@@ -10342,7 +10552,7 @@ var injection_shield_default = plugin29;
10342
10552
 
10343
10553
  // src/interface-contract-guard/index.ts
10344
10554
  import { readFile as readFile10 } from "node:fs/promises";
10345
- import { isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10555
+ import { isAbsolute as isAbsolute14, relative as relative14, resolve as resolve14 } from "node:path";
10346
10556
  var API_VERSION21 = "^0.1.10";
10347
10557
  var state27 = {
10348
10558
  scanCount: 0,
@@ -10375,7 +10585,7 @@ function toPosix5(p) {
10375
10585
  return p.replace(/\\/g, "/");
10376
10586
  }
10377
10587
  function relativePath5(p) {
10378
- return toPosix5(relative15(process.cwd(), p));
10588
+ return toPosix5(relative14(process.cwd(), p));
10379
10589
  }
10380
10590
  var INTERFACE_RE = /(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
10381
10591
  function extractInterfaceNames(content) {
@@ -10403,7 +10613,7 @@ function collectImplementedNames(content, into) {
10403
10613
  }
10404
10614
  async function scanPath3(rawPath, cfg) {
10405
10615
  const root = process.cwd();
10406
- const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10616
+ const resolved = isAbsolute14(rawPath) ? resolve14(rawPath) : resolve14(root, rawPath);
10407
10617
  const exts = normalizeExtensions4(cfg.extensions);
10408
10618
  const allFiles = await collectSourceFilesAsync(resolved, { extensions: exts });
10409
10619
  const files = allFiles.slice(0, cfg.maxFiles);
@@ -10493,7 +10703,7 @@ var plugin30 = {
10493
10703
  const exts = normalizeExtensions4(cfg.extensions);
10494
10704
  if (!matchesExtension(sourcePath, exts)) return;
10495
10705
  state27.hookInvocationCount += 1;
10496
- const resolved = resolve15(process.cwd(), sourcePath);
10706
+ const resolved = resolve14(process.cwd(), sourcePath);
10497
10707
  let content;
10498
10708
  try {
10499
10709
  content = await readFile10(resolved, "utf-8");
@@ -10541,7 +10751,7 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
10541
10751
  state27.findingCount += result.findings.length;
10542
10752
  return {
10543
10753
  ok: true,
10544
- path: relativePath5(resolve15(process.cwd(), rawPath)),
10754
+ path: relativePath5(resolve14(process.cwd(), rawPath)),
10545
10755
  scannedFiles: result.scannedFiles,
10546
10756
  findings: result.findings,
10547
10757
  // Say so when the corpus was cut short. A partial scan that
@@ -10622,7 +10832,7 @@ var interface_contract_guard_default = plugin30;
10622
10832
 
10623
10833
  // src/knowledge-graph/index.ts
10624
10834
  import { readFileSync as readFileSync7 } from "node:fs";
10625
- import { dirname as dirname5, isAbsolute as isAbsolute16, relative as relative16, resolve as resolve16 } from "node:path";
10835
+ import { dirname as dirname5, isAbsolute as isAbsolute15, relative as relative15, resolve as resolve15 } from "node:path";
10626
10836
  import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
10627
10837
  var API_VERSION22 = "^0.1.10";
10628
10838
  var state28 = {
@@ -10644,10 +10854,10 @@ var DEFAULTS25 = {
10644
10854
  };
10645
10855
  function resolveProjectPath5(rawPath, cwd = process.cwd()) {
10646
10856
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
10647
- const root = resolve16(cwd);
10648
- const resolved = isAbsolute16(rawPath) ? resolve16(rawPath) : resolve16(root, rawPath);
10649
- const rel = relative16(root, resolved);
10650
- if (rel === "" || !rel.startsWith("..") && !isAbsolute16(rel)) return resolved;
10857
+ const root = resolve15(cwd);
10858
+ const resolved = isAbsolute15(rawPath) ? resolve15(rawPath) : resolve15(root, rawPath);
10859
+ const rel = relative15(root, resolved);
10860
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute15(rel)) return resolved;
10651
10861
  return null;
10652
10862
  }
10653
10863
  function readConfig26(raw) {
@@ -10946,7 +11156,7 @@ var knowledge_graph_default = plugin31;
10946
11156
 
10947
11157
  // src/license-audit-gate/index.ts
10948
11158
  import { readFileSync as readFileSync8 } from "node:fs";
10949
- import { resolve as resolve17 } from "node:path";
11159
+ import { resolve as resolve16 } from "node:path";
10950
11160
  var API_VERSION23 = "^0.1.10";
10951
11161
  var state29 = {
10952
11162
  invocations: 0,
@@ -10996,24 +11206,10 @@ function extractLicenseStrings(pkg) {
10996
11206
  }
10997
11207
  return [...new Set(out)];
10998
11208
  }
10999
- var INSTALL_RE3 = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/gi;
11000
11209
  function parsePackageNames(command) {
11001
- const names = [];
11002
- INSTALL_RE3.lastIndex = 0;
11003
- for (const m of command.matchAll(INSTALL_RE3)) {
11004
- const argString = m[2] ?? "";
11005
- for (const token of argString.split(/\s+/)) {
11006
- if (!token || token.startsWith("-")) continue;
11007
- if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
11008
- const cleaned = token.replace(/^['"]|['"]$/g, "");
11009
- if (!cleaned) continue;
11010
- let name = cleaned;
11011
- const at = cleaned.lastIndexOf("@");
11012
- if (at > 0) name = cleaned.slice(0, at);
11013
- if (name) names.push(name);
11014
- }
11015
- }
11016
- return [...new Set(names)];
11210
+ return [
11211
+ ...new Set(parseInstallCommands(command).flatMap((entry) => entry.packages.map((pkg) => pkg.name)))
11212
+ ];
11017
11213
  }
11018
11214
  function auditPackages(names, allowedLicenses) {
11019
11215
  const results = [];
@@ -11022,7 +11218,7 @@ function auditPackages(names, allowedLicenses) {
11022
11218
  for (const name of names) {
11023
11219
  let licenses = [];
11024
11220
  try {
11025
- const pkgPath = resolve17("node_modules", name, "package.json");
11221
+ const pkgPath = resolve16("node_modules", name, "package.json");
11026
11222
  const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
11027
11223
  licenses = extractLicenseStrings(raw);
11028
11224
  } catch {
@@ -11207,7 +11403,7 @@ import { readFileSync as readFileSync9 } from "node:fs";
11207
11403
  import { mkdtemp, readFile as readFile11, rm, writeFile as writeFile3 } from "node:fs/promises";
11208
11404
  import { createRequire as createRequire2 } from "node:module";
11209
11405
  import { tmpdir } from "node:os";
11210
- import { dirname as dirname6, isAbsolute as isAbsolute17, join as join5, relative as relative17, resolve as resolve18, sep as sep4 } from "node:path";
11406
+ import { dirname as dirname6, isAbsolute as isAbsolute16, join as join5, relative as relative16, resolve as resolve17, sep as sep4 } from "node:path";
11211
11407
  var API_VERSION24 = "^0.1.10";
11212
11408
  var state30 = {
11213
11409
  /** Total PreToolUse invocations. */
@@ -11247,19 +11443,19 @@ var LINTER_PACKAGES = {
11247
11443
  };
11248
11444
  var linterCache = new BoundedMap({ max: 32, ttlMs: 3e5 });
11249
11445
  function isInside2(parent, candidate) {
11250
- const rel = relative17(parent, candidate);
11251
- return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute17(rel);
11446
+ const rel = relative16(parent, candidate);
11447
+ return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute16(rel);
11252
11448
  }
11253
11449
  function resolveLocalLinter(name, cwd) {
11254
11450
  try {
11255
11451
  const packageName = LINTER_PACKAGES[name];
11256
- const requireFromProject = createRequire2(resolve18(cwd, "package.json"));
11452
+ const requireFromProject = createRequire2(resolve17(cwd, "package.json"));
11257
11453
  const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
11258
11454
  const packageJson = JSON.parse(readFileSync9(packagePath, "utf-8"));
11259
11455
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[name] ?? Object.values(packageJson.bin ?? {})[0];
11260
- if (!relativeBin || isAbsolute17(relativeBin)) return null;
11456
+ if (!relativeBin || isAbsolute16(relativeBin)) return null;
11261
11457
  const packageDir = dirname6(packagePath);
11262
- const entry = resolve18(packageDir, relativeBin);
11458
+ const entry = resolve17(packageDir, relativeBin);
11263
11459
  if (!isInside2(packageDir, entry)) return null;
11264
11460
  return {
11265
11461
  cmd: process.execPath,
@@ -11311,9 +11507,10 @@ async function detectLinter(requested, cwd) {
11311
11507
  }
11312
11508
  async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
11313
11509
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
11314
- const tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-"));
11315
- const tmpFile = join5(tmpDir, `input${ext}`);
11510
+ let tmpDir;
11316
11511
  try {
11512
+ tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-"));
11513
+ const tmpFile = join5(tmpDir, `input${ext}`);
11317
11514
  await writeFile3(tmpFile, content, "utf-8");
11318
11515
  const fullArgs = [...linter.args, tmpFile];
11319
11516
  const result = await runCommand2(linter.cmd, fullArgs, timeoutMs, cwd, signal);
@@ -11324,14 +11521,15 @@ async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
11324
11521
  if (signal.aborted) throw signal.reason;
11325
11522
  return null;
11326
11523
  } finally {
11327
- await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11524
+ if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11328
11525
  }
11329
11526
  }
11330
11527
  async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
11331
11528
  const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
11332
- const tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-fix-"));
11333
- const tmpFile = join5(tmpDir, `input${ext}`);
11529
+ let tmpDir;
11334
11530
  try {
11531
+ tmpDir = await mkdtemp(join5(tmpdir(), "lint-gate-fix-"));
11532
+ const tmpFile = join5(tmpDir, `input${ext}`);
11335
11533
  await writeFile3(tmpFile, content, "utf-8");
11336
11534
  const fixArgs = linter.name === "biome" ? [linter.args[0], "check", "--write", tmpFile] : [linter.args[0], "--fix", tmpFile];
11337
11535
  await runCommand2(linter.cmd, fixArgs, timeoutMs, cwd, signal);
@@ -11341,7 +11539,7 @@ async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
11341
11539
  if (signal.aborted) throw signal.reason;
11342
11540
  return content;
11343
11541
  } finally {
11344
- await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11542
+ if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
11345
11543
  }
11346
11544
  }
11347
11545
  function parseLinterOutput(stdout, linterName) {
@@ -11436,7 +11634,7 @@ var plugin33 = {
11436
11634
  state30.lastResult = null;
11437
11635
  linterCache.clear();
11438
11636
  const cfg = readConfig28(api.config.extensions?.["lint-gate"]);
11439
- const cwd = resolve18(api.config.cwd ?? process.cwd());
11637
+ const cwd = resolve17(api.config.cwd ?? process.cwd());
11440
11638
  const linterReady = detectLinter(cfg.linter, cwd).then((linter) => {
11441
11639
  if (!linter) {
11442
11640
  api.log.warn("lint-gate: no linter found (biome or eslint) \u2014 hook will be a no-op", {
@@ -11595,9 +11793,9 @@ ${summary}${truncated}`
11595
11793
  name: "lint-gate",
11596
11794
  stage: "mutate",
11597
11795
  timeoutMs: Math.max(1e3, cfg.timeoutMs + 1e3),
11598
- // Formatter/linter availability must not create approval or denial
11599
- // loops in YOLO mode. Explicit lint findings still block in block mode.
11600
- failurePolicy: "open"
11796
+ // Fail closed: a linter crash or timeout must not let unlinted
11797
+ // content through in block mode (issue #363).
11798
+ failurePolicy: "closed"
11601
11799
  });
11602
11800
  api.tools.register({
11603
11801
  name: "lint_gate_status",
@@ -11933,7 +12131,7 @@ var llm_cache_default = plugin34;
11933
12131
 
11934
12132
  // src/loop-breaker/index.ts
11935
12133
  import { execFile as execFile9 } from "node:child_process";
11936
- import { isAbsolute as isAbsolute18, relative as relative18 } from "node:path";
12134
+ import { isAbsolute as isAbsolute17, relative as relative17 } from "node:path";
11937
12135
  var state32 = {
11938
12136
  lastFingerprint: null,
11939
12137
  streak: 0,
@@ -12044,7 +12242,7 @@ function hashString2(value) {
12044
12242
  return String(h >>> 0);
12045
12243
  }
12046
12244
  async function gitDiffFingerprint(cwd, targetPath, signal) {
12047
- const pathspec = isAbsolute18(targetPath) ? relative18(cwd, targetPath) : targetPath;
12245
+ const pathspec = isAbsolute17(targetPath) ? relative17(cwd, targetPath) : targetPath;
12048
12246
  if (!pathspec || pathspec === ".." || pathspec.startsWith("../") || pathspec.startsWith("..\\")) {
12049
12247
  return null;
12050
12248
  }
@@ -13579,6 +13777,29 @@ var plugin38 = {
13579
13777
  var notify_hub_default = plugin38;
13580
13778
 
13581
13779
  // src/path-guard/glob.ts
13780
+ var GLOB_REDOS_BUDGET_MS = 250;
13781
+ function mergeGuardedRegex(patterns) {
13782
+ const flags = /* @__PURE__ */ new Set();
13783
+ for (const p of patterns) {
13784
+ for (const f of p.flags) {
13785
+ if (f !== "g" && f !== "y") flags.add(f);
13786
+ }
13787
+ }
13788
+ const uniqueFlags = [...flags].join("");
13789
+ return new RegExp(patterns.map((p) => p.source).join("|"), uniqueFlags);
13790
+ }
13791
+ async function matchesAnyGuarded(path, patterns, options = {}) {
13792
+ const normalized = normalizePath2(path);
13793
+ if (patterns.length === 0) return false;
13794
+ const result = await withReDoSGuard(
13795
+ mergeGuardedRegex(patterns),
13796
+ normalized,
13797
+ options.budgetMs ?? GLOB_REDOS_BUDGET_MS,
13798
+ options.onTimeout ? { onTimeout: options.onTimeout } : {}
13799
+ );
13800
+ if (result.timedOut) return true;
13801
+ return result.match !== null;
13802
+ }
13582
13803
  function compilePathGlob(pattern) {
13583
13804
  const normalized = pattern.replace(/\\/g, "/");
13584
13805
  let source = "";
@@ -13713,8 +13934,7 @@ function scopesMayOverlap(left, right) {
13713
13934
  }
13714
13935
  return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
13715
13936
  }
13716
- function targetIntersectsPatterns(target, patternTexts, patterns) {
13717
- if (matchesAny(target.path, patterns)) return true;
13937
+ function targetIntersectsScope(target, patternTexts) {
13718
13938
  if (target.kind === "file") return false;
13719
13939
  const normalized = normalizePath2(target.path).replace(/\/$/, "");
13720
13940
  if (!isUnresolvedPathScope(normalized)) {
@@ -13726,6 +13946,14 @@ function targetIntersectsPatterns(target, patternTexts, patterns) {
13726
13946
  }
13727
13947
  return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath2(pattern)));
13728
13948
  }
13949
+ function targetIntersectsPatterns(target, patternTexts, patterns) {
13950
+ if (matchesAny(target.path, patterns)) return true;
13951
+ return targetIntersectsScope(target, patternTexts);
13952
+ }
13953
+ async function targetIntersectsPatternsGuarded(target, patternTexts, patterns, options = {}) {
13954
+ if (await matchesAnyGuarded(target.path, patterns, options)) return true;
13955
+ return targetIntersectsScope(target, patternTexts);
13956
+ }
13729
13957
  function targetFullyAllowed(target, allowTexts, allowRes) {
13730
13958
  if (target.kind === "file") return matchesAny(target.path, allowRes);
13731
13959
  const normalized = normalizePath2(target.path).replace(/\/$/, "");
@@ -14042,7 +14270,7 @@ function commandRecursivelyDeletes(command) {
14042
14270
  )) {
14043
14271
  return true;
14044
14272
  }
14045
- const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14273
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del|rd|Remove-Item)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14046
14274
  let match = destructive.exec(stripped);
14047
14275
  while (match !== null) {
14048
14276
  const tool = match[1]?.toLowerCase();
@@ -14055,6 +14283,8 @@ function commandRecursivelyDeletes(command) {
14055
14283
  if (token === "--") break;
14056
14284
  if (tool === "rm") {
14057
14285
  if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
14286
+ } else if (tool === "remove-item") {
14287
+ if (/^-Recurse$/i.test(token) || /^-r$/i.test(token)) recursive = true;
14058
14288
  } else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
14059
14289
  recursive = true;
14060
14290
  }
@@ -14303,7 +14533,7 @@ function destructiveTargetsAtDepth(command, depth) {
14303
14533
  }
14304
14534
  targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
14305
14535
  }
14306
- const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14536
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|rd|Remove-Item|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14307
14537
  let m = destructive.exec(normalizedCommand);
14308
14538
  while (m !== null) {
14309
14539
  if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
@@ -14791,8 +15021,27 @@ function operationLabel(toolName) {
14791
15021
  }
14792
15022
 
14793
15023
  // src/path-guard/index.ts
15024
+ import { realpathSync } from "node:fs";
15025
+ import { resolve as resolve18 } from "node:path";
15026
+ function isSymlinkEscape(path, cwd) {
15027
+ if (!withinProject(path)) return false;
15028
+ try {
15029
+ const abs = resolve18(cwd ?? process.cwd(), path);
15030
+ const real = realpathSync(abs);
15031
+ return !withinProject(real);
15032
+ } catch {
15033
+ return false;
15034
+ }
15035
+ }
14794
15036
  function createState() {
14795
- return { invocations: 0, blocks: 0, warns: 0, lastBlock: null, hookUnregister: null };
15037
+ return {
15038
+ invocations: 0,
15039
+ blocks: 0,
15040
+ warns: 0,
15041
+ redosTimeouts: 0,
15042
+ lastBlock: null,
15043
+ hookUnregister: null
15044
+ };
14796
15045
  }
14797
15046
  var states = /* @__PURE__ */ new WeakMap();
14798
15047
  var latestState = createState();
@@ -14887,7 +15136,11 @@ var plugin39 = {
14887
15136
  additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
14888
15137
  };
14889
15138
  };
14890
- const hook = (input) => {
15139
+ const bumpRedos = () => {
15140
+ state60.redosTimeouts += 1;
15141
+ api.metrics.counter("redos_timeouts");
15142
+ };
15143
+ const hook = async (input) => {
14891
15144
  if (!cfg.enabled) return;
14892
15145
  state60.invocations += 1;
14893
15146
  const toolName = input.toolName ?? "";
@@ -14908,21 +15161,27 @@ var plugin39 = {
14908
15161
  const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
14909
15162
  const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
14910
15163
  const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
14911
- for (const path of shellTargets) {
14912
- const target = {
14913
- path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
14914
- kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
14915
- };
14916
- if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14917
- const protectedShellTarget = targetIntersectsPatterns(target, cfg.protect, protectRes) || matchesAny(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes);
14918
- if (protectedShellTarget) {
14919
- return verdict(
14920
- target.path,
14921
- toolName,
14922
- "destructive shell command",
14923
- target.kind !== "file"
14924
- );
14925
- }
15164
+ const shellHits = await Promise.all(
15165
+ shellTargets.map(async (path) => {
15166
+ const target = {
15167
+ path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
15168
+ kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
15169
+ };
15170
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
15171
+ const protectedShellTarget = await targetIntersectsPatternsGuarded(target, cfg.protect, protectRes, { onTimeout: bumpRedos }) || await matchesAnyGuarded(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes, {
15172
+ onTimeout: bumpRedos
15173
+ });
15174
+ return protectedShellTarget ? target : null;
15175
+ })
15176
+ );
15177
+ const firstProtected = shellHits.find((t) => t !== null);
15178
+ if (firstProtected) {
15179
+ return verdict(
15180
+ firstProtected.path,
15181
+ toolName,
15182
+ "destructive shell command",
15183
+ firstProtected.kind !== "file"
15184
+ );
14926
15185
  }
14927
15186
  const writes = writesToDisk({ ...input, toolInput: ti });
14928
15187
  const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
@@ -14933,11 +15192,21 @@ var plugin39 = {
14933
15192
  }
14934
15193
  if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
14935
15194
  const targets = pathsFromToolInput(ti, toolName, input.cwd);
14936
- for (const target of targets) {
14937
- if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14938
- if (targetIntersectsPatterns(target, cfg.protect, protectRes)) {
14939
- return verdict(target.path, toolName, operationLabel(toolName), target.kind !== "file");
14940
- }
15195
+ const writeHits = await Promise.all(
15196
+ targets.map(async (target) => {
15197
+ if (target.kind === "file" && isSymlinkEscape(target.path, input.cwd)) return target;
15198
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
15199
+ return targetIntersectsPatterns(target, cfg.protect, protectRes) ? target : null;
15200
+ })
15201
+ );
15202
+ const firstProtectedWrite = writeHits.find((t) => t !== null);
15203
+ if (firstProtectedWrite) {
15204
+ return verdict(
15205
+ firstProtectedWrite.path,
15206
+ toolName,
15207
+ operationLabel(toolName),
15208
+ firstProtectedWrite.kind !== "file"
15209
+ );
14941
15210
  }
14942
15211
  return;
14943
15212
  };
@@ -14964,7 +15233,8 @@ var plugin39 = {
14964
15233
  counters: {
14965
15234
  invocations: state60.invocations,
14966
15235
  blocks: state60.blocks,
14967
- warns: state60.warns
15236
+ warns: state60.warns,
15237
+ redosTimeouts: state60.redosTimeouts
14968
15238
  },
14969
15239
  lastBlock: state60.lastBlock
14970
15240
  };
@@ -14987,10 +15257,16 @@ var plugin39 = {
14987
15257
  }
14988
15258
  state60.hookUnregister = null;
14989
15259
  }
14990
- const final = { invocations: state60.invocations, blocks: state60.blocks, warns: state60.warns };
15260
+ const final = {
15261
+ invocations: state60.invocations,
15262
+ blocks: state60.blocks,
15263
+ warns: state60.warns,
15264
+ redosTimeouts: state60.redosTimeouts
15265
+ };
14991
15266
  state60.invocations = 0;
14992
15267
  state60.blocks = 0;
14993
15268
  state60.warns = 0;
15269
+ state60.redosTimeouts = 0;
14994
15270
  state60.lastBlock = null;
14995
15271
  states.delete(api);
14996
15272
  api.log.info("path-guard: teardown complete", { final });
@@ -15003,7 +15279,8 @@ var plugin39 = {
15003
15279
  counters: {
15004
15280
  invocations: state60.invocations,
15005
15281
  blocks: state60.blocks,
15006
- warns: state60.warns
15282
+ warns: state60.warns,
15283
+ redosTimeouts: state60.redosTimeouts
15007
15284
  }
15008
15285
  };
15009
15286
  }
@@ -15012,7 +15289,7 @@ var path_guard_default = plugin39;
15012
15289
 
15013
15290
  // src/performance-regression-gate/index.ts
15014
15291
  import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
15015
- import { isAbsolute as isAbsolute19, relative as relative19, resolve as resolve19 } from "node:path";
15292
+ import { isAbsolute as isAbsolute18, relative as relative18, resolve as resolve19 } from "node:path";
15016
15293
  var API_VERSION26 = "^0.1.10";
15017
15294
  var state36 = {
15018
15295
  invocationCount: 0,
@@ -15035,21 +15312,21 @@ function readConfig35(raw) {
15035
15312
  thresholdPercent: threshold
15036
15313
  };
15037
15314
  }
15038
- function withinProject4(p, cwd = process.cwd()) {
15315
+ function withinProject3(p, cwd = process.cwd()) {
15039
15316
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
15040
15317
  const root = resolve19(cwd);
15041
- const resolved = isAbsolute19(p) ? resolve19(p) : resolve19(root, p);
15042
- const rel = relative19(root, resolved);
15318
+ const resolved = isAbsolute18(p) ? resolve19(p) : resolve19(root, p);
15319
+ const rel = relative18(root, resolved);
15043
15320
  if (rel === "" || rel === ".") return true;
15044
15321
  if (rel.startsWith("..")) return false;
15045
- if (isAbsolute19(rel)) return false;
15322
+ if (isAbsolute18(rel)) return false;
15046
15323
  return true;
15047
15324
  }
15048
15325
  function resolveProjectPath6(rawPath, cwd = process.cwd()) {
15049
15326
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
15050
15327
  const root = resolve19(cwd);
15051
- const resolved = isAbsolute19(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
15052
- if (!withinProject4(resolved, cwd)) return null;
15328
+ const resolved = isAbsolute18(rawPath) ? resolve19(rawPath) : resolve19(root, rawPath);
15329
+ if (!withinProject3(resolved, cwd)) return null;
15053
15330
  return resolved;
15054
15331
  }
15055
15332
  function isValidNumber(n) {
@@ -15463,7 +15740,7 @@ var plugin_stack_observer_default = PLUGIN;
15463
15740
  // src/pr-drafter/index.ts
15464
15741
  import { execFile as execFile10 } from "node:child_process";
15465
15742
  import { mkdir as mkdir2, writeFile as writeFile4 } from "node:fs/promises";
15466
- import { dirname as dirname7, isAbsolute as isAbsolute20, relative as relative20, resolve as resolve20 } from "node:path";
15743
+ import { dirname as dirname7, isAbsolute as isAbsolute19, relative as relative19, resolve as resolve20 } from "node:path";
15467
15744
  var API_VERSION27 = "^0.1.10";
15468
15745
  var state38 = {
15469
15746
  commits: [],
@@ -15503,9 +15780,9 @@ function readConfig37(raw) {
15503
15780
  function resolveProjectPath7(rawPath, cwd = process.cwd()) {
15504
15781
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
15505
15782
  const root = resolve20(cwd);
15506
- const resolved = isAbsolute20(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
15507
- const rel = relative20(root, resolved);
15508
- if (rel === "" || !rel.startsWith("..") && !isAbsolute20(rel)) return resolved;
15783
+ const resolved = isAbsolute19(rawPath) ? resolve20(rawPath) : resolve20(root, rawPath);
15784
+ const rel = relative19(root, resolved);
15785
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute19(rel)) return resolved;
15509
15786
  return null;
15510
15787
  }
15511
15788
  function runGit4(args, timeout) {
@@ -16204,6 +16481,7 @@ var state40 = {
16204
16481
  requestRedactions: 0,
16205
16482
  responseRedactions: 0,
16206
16483
  blocked: 0,
16484
+ timeoutCount: 0,
16207
16485
  byKind: /* @__PURE__ */ new Map(),
16208
16486
  lastDetection: null,
16209
16487
  extensionUnregister: null
@@ -16248,6 +16526,7 @@ var plugin43 = {
16248
16526
  state40.requestRedactions = 0;
16249
16527
  state40.responseRedactions = 0;
16250
16528
  state40.blocked = 0;
16529
+ state40.timeoutCount = 0;
16251
16530
  state40.byKind.clear();
16252
16531
  state40.lastDetection = null;
16253
16532
  if (state40.extensionUnregister) {
@@ -16270,7 +16549,16 @@ var plugin43 = {
16270
16549
  async wrapProviderRunner(_ctx, request, inner) {
16271
16550
  const req = request ?? {};
16272
16551
  state40.invocations += 1;
16273
- const detections = detectSecrets(collectText(req), cfg.allow);
16552
+ const requestText = collectText(req);
16553
+ for (const extra of EXTRA_PATTERNS) {
16554
+ const guarded = await withReDoSGuard(extra.re, requestText, 250, {
16555
+ onTimeout: () => {
16556
+ state40.timeoutCount += 1;
16557
+ }
16558
+ });
16559
+ if (guarded.timedOut) break;
16560
+ }
16561
+ const detections = detectSecrets(requestText, cfg.allow);
16274
16562
  if (detections.length > 0) {
16275
16563
  state40.requestsWithSecrets += 1;
16276
16564
  for (const d of detections) {
@@ -16340,7 +16628,8 @@ var plugin43 = {
16340
16628
  requestsWithSecrets: state40.requestsWithSecrets,
16341
16629
  requestRedactions: state40.requestRedactions,
16342
16630
  responseRedactions: state40.responseRedactions,
16343
- blocked: state40.blocked
16631
+ blocked: state40.blocked,
16632
+ timeoutCount: state40.timeoutCount
16344
16633
  },
16345
16634
  byKind: Object.fromEntries(state40.byKind),
16346
16635
  lastDetection: state40.lastDetection
@@ -16396,7 +16685,7 @@ var prompt_firewall_default = plugin43;
16396
16685
 
16397
16686
  // src/refactor-suggester/index.ts
16398
16687
  import { readFile as readFile12 } from "node:fs/promises";
16399
- import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve21 } from "node:path";
16688
+ import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve21 } from "node:path";
16400
16689
  var API_VERSION28 = "^0.1.10";
16401
16690
  var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
16402
16691
  var state41 = {
@@ -16440,7 +16729,7 @@ function toPosix6(p) {
16440
16729
  return p.replace(/\\/g, "/");
16441
16730
  }
16442
16731
  function relativePath6(p) {
16443
- return toPosix6(relative21(process.cwd(), p));
16732
+ return toPosix6(relative20(process.cwd(), p));
16444
16733
  }
16445
16734
  function leadingIndentLevel(line) {
16446
16735
  const leading = line.match(/^(\s*)/)?.[1] ?? "";
@@ -16527,7 +16816,7 @@ function detectSmells(filePath, content, rules) {
16527
16816
  }
16528
16817
  async function scanPath4(rawPath, cfg) {
16529
16818
  const root = process.cwd();
16530
- const resolved = isAbsolute21(rawPath) ? resolve21(rawPath) : resolve21(root, rawPath);
16819
+ const resolved = isAbsolute20(rawPath) ? resolve21(rawPath) : resolve21(root, rawPath);
16531
16820
  const exts = normalizeExtensions5(cfg.extensions);
16532
16821
  const files = await collectSourceFilesAsync(resolved, { extensions: exts });
16533
16822
  const suggestions = [];
@@ -17423,6 +17712,7 @@ function createState2() {
17423
17712
  allowCount: 0,
17424
17713
  /** PostToolUse: secrets detected in tool output. */
17425
17714
  leakCount: 0,
17715
+ timeoutCount: 0,
17426
17716
  /** Most recent PreToolUse block — surfaced by `secret_scanner_status`. */
17427
17717
  lastBlock: null,
17428
17718
  /** Most recent PostToolUse leak — surfaced by `secret_scanner_status`. */
@@ -17591,7 +17881,19 @@ function buildHook(cfg, log, runtime) {
17591
17881
  const { state: state60 } = runtime;
17592
17882
  if (!cfg.enabled) return;
17593
17883
  const toolName = input.toolName ?? "unknown";
17594
- const matched = scanInput(input.toolInput);
17884
+ let matched;
17885
+ try {
17886
+ matched = scanInput(input.toolInput);
17887
+ } catch (err) {
17888
+ if (String(err).includes("ReDoS")) {
17889
+ state60.timeoutCount += 1;
17890
+ return {
17891
+ decision: "block",
17892
+ reason: "secret-scanner: ReDoS timeout \u2014 regex scan exceeded the wall-clock budget. Fail-closed: treated as a block."
17893
+ };
17894
+ }
17895
+ throw err;
17896
+ }
17595
17897
  if (!matched) return;
17596
17898
  const summary = matched.join(", ");
17597
17899
  const when = (/* @__PURE__ */ new Date()).toISOString();
@@ -17769,7 +18071,8 @@ var plugin47 = {
17769
18071
  block: state60.blockCount,
17770
18072
  redact: state60.redactCount,
17771
18073
  allow: state60.allowCount,
17772
- leak: state60.leakCount
18074
+ leak: state60.leakCount,
18075
+ timeoutCount: state60.timeoutCount
17773
18076
  },
17774
18077
  lastBlock: state60.lastBlock,
17775
18078
  lastLeak: state60.lastLeak
@@ -17859,7 +18162,7 @@ var secret_scanner_default = plugin47;
17859
18162
 
17860
18163
  // src/security-hotspot-scanner/index.ts
17861
18164
  import { readdir as readdir2, readFile as readFile13, stat as stat5 } from "node:fs/promises";
17862
- import { isAbsolute as isAbsolute22, relative as relative22, resolve as resolve22 } from "node:path";
18165
+ import { isAbsolute as isAbsolute21, relative as relative21, resolve as resolve22 } from "node:path";
17863
18166
  var API_VERSION31 = "^0.1.10";
17864
18167
  var state44 = {
17865
18168
  scanCount: 0,
@@ -17950,7 +18253,7 @@ function isSourceFile2(filePath, extensions) {
17950
18253
  async function scanPath5(inputPath, cfg) {
17951
18254
  const start = Date.now();
17952
18255
  const root = process.cwd();
17953
- const resolved = isAbsolute22(inputPath) ? resolve22(inputPath) : resolve22(root, inputPath);
18256
+ const resolved = isAbsolute21(inputPath) ? resolve22(inputPath) : resolve22(root, inputPath);
17954
18257
  if (!withinProject(inputPath)) {
17955
18258
  return {
17956
18259
  path: inputPath,
@@ -17971,7 +18274,7 @@ async function scanPath5(inputPath, cfg) {
17971
18274
  filesScanned += 1;
17972
18275
  const findings = scanSource(content, maxPerFile);
17973
18276
  for (const f of findings) {
17974
- allFindings.push({ ...f, snippet: `${relative22(root, filePath)}:${f.line}: ${f.snippet}` });
18277
+ allFindings.push({ ...f, snippet: `${relative21(root, filePath)}:${f.line}: ${f.snippet}` });
17975
18278
  if (allFindings.length >= cfg.maxFindings) return;
17976
18279
  }
17977
18280
  } catch {
@@ -18255,7 +18558,7 @@ var security_hotspot_scanner_default = plugin48;
18255
18558
 
18256
18559
  // src/semantic-search-indexer/index.ts
18257
18560
  import * as fs2 from "node:fs/promises";
18258
- import { isAbsolute as isAbsolute23, relative as relative23, resolve as resolve23 } from "node:path";
18561
+ import { isAbsolute as isAbsolute22, relative as relative22, resolve as resolve23 } from "node:path";
18259
18562
  import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
18260
18563
  var API_VERSION32 = "^0.1.10";
18261
18564
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -18333,21 +18636,21 @@ function readConfig45(raw) {
18333
18636
  function normalizeSlashes2(p) {
18334
18637
  return p.replace(/\\/g, "/");
18335
18638
  }
18336
- function withinProject5(p) {
18639
+ function withinProject4(p) {
18337
18640
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
18338
18641
  const root = normalizeSlashes2(process.cwd());
18339
- const resolved = normalizeSlashes2(isAbsolute23(p) ? resolve23(p) : resolve23(root, p));
18340
- const rel = normalizeSlashes2(relative23(root, resolved));
18642
+ const resolved = normalizeSlashes2(isAbsolute22(p) ? resolve23(p) : resolve23(root, p));
18643
+ const rel = normalizeSlashes2(relative22(root, resolved));
18341
18644
  if (rel === "" || rel === ".") return true;
18342
18645
  if (rel.startsWith("..")) return false;
18343
- if (isAbsolute23(rel)) return false;
18646
+ if (isAbsolute22(rel)) return false;
18344
18647
  return true;
18345
18648
  }
18346
18649
  function resolveProjectPath8(p) {
18347
18650
  const raw = typeof p === "string" && p.length > 0 ? p : ".";
18348
- if (!withinProject5(raw)) return null;
18651
+ if (!withinProject4(raw)) return null;
18349
18652
  const root = normalizeSlashes2(process.cwd());
18350
- return normalizeSlashes2(isAbsolute23(raw) ? resolve23(raw) : resolve23(root, raw));
18653
+ return normalizeSlashes2(isAbsolute22(raw) ? resolve23(raw) : resolve23(root, raw));
18351
18654
  }
18352
18655
  function tokenize(text, minLength) {
18353
18656
  const tokens = [];
@@ -18432,7 +18735,7 @@ async function walkDirectory(absPath, cfg, excludes, fileBatch) {
18432
18735
  return;
18433
18736
  }
18434
18737
  const absChild = normalizeSlashes2(resolve23(absPath, ent.name));
18435
- const relChild = normalizeSlashes2(relative23(root, absChild));
18738
+ const relChild = normalizeSlashes2(relative22(root, absChild));
18436
18739
  if (relChild === "" || relChild === ".") continue;
18437
18740
  if (excludes.some((re) => re.test(relChild))) continue;
18438
18741
  if (ent.isDirectory()) {
@@ -18474,7 +18777,7 @@ async function buildIndex(rootPath, cfg) {
18474
18777
  return;
18475
18778
  }
18476
18779
  if (rootStats.isFile()) {
18477
- const relPath = normalizeSlashes2(relative23(normalizeSlashes2(process.cwd()), rootPath));
18780
+ const relPath = normalizeSlashes2(relative22(normalizeSlashes2(process.cwd()), rootPath));
18478
18781
  await indexFileFromStats(rootPath, relPath === "" ? "." : relPath, rootStats, cfg);
18479
18782
  state45.fileCount = state45.index.files.size;
18480
18783
  } else if (rootStats.isDirectory()) {
@@ -18771,14 +19074,14 @@ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
18771
19074
  import { toErrorMessage } from "@wrongstack/core/utils";
18772
19075
  import { execFile as execFile12 } from "node:child_process";
18773
19076
  import { access as access4, readFile as readFile15, readdir as readdir4, writeFile as writeFile5 } from "node:fs/promises";
18774
- import { isAbsolute as isAbsolute24, join as join6, relative as relative24, resolve as resolve24 } from "node:path";
19077
+ import { isAbsolute as isAbsolute23, join as join6, relative as relative23, resolve as resolve24 } from "node:path";
18775
19078
  var API_VERSION33 = "^0.1.10";
18776
19079
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
18777
19080
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
18778
19081
  const base = resolve24(root);
18779
- const resolved = isAbsolute24(rawCwd) ? resolve24(rawCwd) : resolve24(base, rawCwd);
18780
- const rel = relative24(base, resolved);
18781
- if (rel === "" || !rel.startsWith("..") && !isAbsolute24(rel)) return resolved;
19082
+ const resolved = isAbsolute23(rawCwd) ? resolve24(rawCwd) : resolve24(base, rawCwd);
19083
+ const rel = relative23(base, resolved);
19084
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute23(rel)) return resolved;
18782
19085
  return null;
18783
19086
  }
18784
19087
  var state46 = {
@@ -19801,16 +20104,16 @@ var session_recap_default = plugin51;
19801
20104
  // src/shell-check/index.ts
19802
20105
  import { execFile as execFile13 } from "node:child_process";
19803
20106
  import { readdir as readdir5 } from "node:fs/promises";
19804
- import { isAbsolute as isAbsolute25, join as join7, relative as relative25, resolve as resolve25 } from "node:path";
20107
+ import { isAbsolute as isAbsolute24, join as join7, relative as relative24, resolve as resolve25 } from "node:path";
19805
20108
  var API_VERSION34 = "^0.1.10";
19806
- function withinProject6(p) {
20109
+ function withinProject5(p) {
19807
20110
  if (p.startsWith("-")) return false;
19808
20111
  const root = process.cwd();
19809
- const resolved = isAbsolute25(p) ? resolve25(p) : resolve25(root, p);
19810
- const rel = relative25(root, resolved);
20112
+ const resolved = isAbsolute24(p) ? resolve25(p) : resolve25(root, p);
20113
+ const rel = relative24(root, resolved);
19811
20114
  if (rel === "" || rel === ".") return true;
19812
20115
  if (rel.startsWith("..")) return false;
19813
- if (isAbsolute25(rel)) return false;
20116
+ if (isAbsolute24(rel)) return false;
19814
20117
  return true;
19815
20118
  }
19816
20119
  var MAX_PATH_LEN = 4096;
@@ -19987,7 +20290,7 @@ var plugin52 = {
19987
20290
  const pattern = inp.pattern ?? "";
19988
20291
  const severity = inp.severity ?? "warning";
19989
20292
  state48.invocationCount += 1;
19990
- const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject6(p);
20293
+ const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject5(p);
19991
20294
  if (!pathIsSafe(directory)) {
19992
20295
  return {
19993
20296
  ok: false,
@@ -20103,7 +20406,7 @@ var shell_check_default = plugin52;
20103
20406
 
20104
20407
  // src/smart-rename/index.ts
20105
20408
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
20106
- import { extname as extname6, isAbsolute as isAbsolute26, relative as relative26, resolve as resolve26 } from "node:path";
20409
+ import { extname as extname6, isAbsolute as isAbsolute25, relative as relative25, resolve as resolve26 } from "node:path";
20107
20410
  var API_VERSION35 = "^0.1.10";
20108
20411
  var state49 = {
20109
20412
  renameCount: 0,
@@ -20122,21 +20425,21 @@ function readConfig47(raw) {
20122
20425
  extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS44.extensions
20123
20426
  };
20124
20427
  }
20125
- function withinProject7(p) {
20428
+ function withinProject6(p) {
20126
20429
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
20127
20430
  const root = process.cwd();
20128
- const resolved = isAbsolute26(p) ? resolve26(p) : resolve26(root, p);
20129
- const rel = relative26(root, resolved);
20431
+ const resolved = isAbsolute25(p) ? resolve26(p) : resolve26(root, p);
20432
+ const rel = relative25(root, resolved);
20130
20433
  if (rel === "" || rel === ".") return true;
20131
20434
  if (rel.startsWith("..")) return false;
20132
- if (isAbsolute26(rel)) return false;
20435
+ if (isAbsolute25(rel)) return false;
20133
20436
  return true;
20134
20437
  }
20135
20438
  function toPosix7(p) {
20136
20439
  return p.replace(/\\/g, "/");
20137
20440
  }
20138
20441
  function relativePath7(p) {
20139
- return toPosix7(relative26(process.cwd(), p));
20442
+ return toPosix7(relative25(process.cwd(), p));
20140
20443
  }
20141
20444
  function escapeRegex2(s) {
20142
20445
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -20221,7 +20524,7 @@ var plugin53 = {
20221
20524
  if (!isIdentifier(newName)) {
20222
20525
  return { ok: false, error: `newName "${newName}" is not a valid identifier` };
20223
20526
  }
20224
- if (!withinProject7(rawPath)) {
20527
+ if (!withinProject6(rawPath)) {
20225
20528
  return { ok: false, error: "path is outside the project root" };
20226
20529
  }
20227
20530
  const ext = extname6(rawPath).toLowerCase();
@@ -20499,7 +20802,7 @@ function isWrappedAsLinkOrCode(line, name) {
20499
20802
  if (lower.includes(`[\``) && lower.includes(`\`](`)) return true;
20500
20803
  return false;
20501
20804
  }
20502
- function escapeRegExp(s) {
20805
+ function escapeRegExp2(s) {
20503
20806
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20504
20807
  }
20505
20808
  function mapMarkdownFences(lines) {
@@ -20524,7 +20827,7 @@ function findUnlinkedReferences(lines, names) {
20524
20827
  const line = lines[i];
20525
20828
  if (line.length === 0) continue;
20526
20829
  for (const name of names) {
20527
- const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
20830
+ const re = new RegExp(`(^|[^\\w-])${escapeRegExp2(name)}(?![\\w-])`, "i");
20528
20831
  if (re.test(line) && !isWrappedAsLinkOrCode(line, name)) {
20529
20832
  if (!found.has(name)) found.set(name, true);
20530
20833
  }
@@ -20553,7 +20856,7 @@ function wrapLineReferences(line) {
20553
20856
  let cursor = 0;
20554
20857
  const spans = [];
20555
20858
  for (const name of PLUGIN_NAMES) {
20556
- const re = new RegExp(`(^|[^\\w-])(${escapeRegExp(name)})(?![\\w-])`, "gi");
20859
+ const re = new RegExp(`(^|[^\\w-])(${escapeRegExp2(name)})(?![\\w-])`, "gi");
20557
20860
  let m;
20558
20861
  re.lastIndex = 0;
20559
20862
  while ((m = re.exec(line)) !== null) {
@@ -20774,7 +21077,7 @@ var spec_linker_default = plugin54;
20774
21077
 
20775
21078
  // src/template-engine/index.ts
20776
21079
  import { readFile as readFile17, writeFile as writeFile6 } from "node:fs/promises";
20777
- import { isAbsolute as isAbsolute27 } from "node:path";
21080
+ import { isAbsolute as isAbsolute26 } from "node:path";
20778
21081
  var API_VERSION36 = "^0.1.10";
20779
21082
  var templates = /* @__PURE__ */ new Map();
20780
21083
  var MAX_TEMPLATES = 256;
@@ -20830,7 +21133,7 @@ function renderTemplateRaw(template, variables) {
20830
21133
  return result;
20831
21134
  }
20832
21135
  function validateRelativeTemplatePath(field, value) {
20833
- if (isAbsolute27(value) || value.split(/[\\/]+/).includes("..")) {
21136
+ if (isAbsolute26(value) || value.split(/[\\/]+/).includes("..")) {
20834
21137
  return `${field} must be a relative path without ".." components`;
20835
21138
  }
20836
21139
  if (!withinProject(value)) {
@@ -21430,7 +21733,7 @@ var test_coverage_gate_default = plugin56;
21430
21733
  import { execFile as execFile14 } from "node:child_process";
21431
21734
  import { readFileSync as readFileSync15 } from "node:fs";
21432
21735
  import { createRequire as createRequire3 } from "node:module";
21433
- import { dirname as dirname8, isAbsolute as isAbsolute28, relative as relative27, resolve as resolve27 } from "node:path";
21736
+ import { dirname as dirname8, isAbsolute as isAbsolute27, relative as relative26, resolve as resolve27 } from "node:path";
21434
21737
  var API_VERSION38 = "^0.1.10";
21435
21738
  var state52 = {
21436
21739
  invocationCount: 0,
@@ -21499,17 +21802,17 @@ var ALLOWED_RUNNER_FLAGS = /* @__PURE__ */ new Set([
21499
21802
  "--reporter=verbose",
21500
21803
  "--reporter=default"
21501
21804
  ]);
21502
- function withinProject8(p) {
21805
+ function withinProject7(p) {
21503
21806
  if (p.length === 0 || p.length > 4096 || p.startsWith("-")) return false;
21504
21807
  const root = resolve27(process.cwd());
21505
- const resolved = isAbsolute28(p) ? resolve27(p) : resolve27(root, p);
21506
- const rel = relative27(root, resolved);
21507
- return rel === "" || !rel.startsWith("..") && !isAbsolute28(rel);
21808
+ const resolved = isAbsolute27(p) ? resolve27(p) : resolve27(root, p);
21809
+ const rel = relative26(root, resolved);
21810
+ return rel === "" || !rel.startsWith("..") && !isAbsolute27(rel);
21508
21811
  }
21509
21812
  function isInside3(parent, child) {
21510
21813
  if (parent === child) return true;
21511
- const rel = relative27(parent, child);
21512
- return rel !== "" && !rel.startsWith("..") && !isAbsolute28(rel);
21814
+ const rel = relative26(parent, child);
21815
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute27(rel);
21513
21816
  }
21514
21817
  function tokenizeCommand(command) {
21515
21818
  const trimmed = command.trim();
@@ -21553,7 +21856,7 @@ function resolveTestCommand(baseCommand, testPattern) {
21553
21856
  if (!relativeBin) return null;
21554
21857
  const packageDir = dirname8(packagePath);
21555
21858
  const candidate = resolve27(packageDir, relativeBin);
21556
- if (isAbsolute28(relativeBin) || !isInside3(packageDir, candidate)) {
21859
+ if (isAbsolute27(relativeBin) || !isInside3(packageDir, candidate)) {
21557
21860
  return null;
21558
21861
  }
21559
21862
  resolvedEntry = candidate;
@@ -21562,7 +21865,7 @@ function resolveTestCommand(baseCommand, testPattern) {
21562
21865
  }
21563
21866
  const args = [resolvedEntry, ...runnerArgs];
21564
21867
  if (testPattern) {
21565
- if (!withinProject8(testPattern)) return null;
21868
+ if (!withinProject7(testPattern)) return null;
21566
21869
  args.push(testPattern);
21567
21870
  }
21568
21871
  return {
@@ -21796,7 +22099,7 @@ var test_flake_detector_default = plugin57;
21796
22099
 
21797
22100
  // src/test-generator/index.ts
21798
22101
  import { readFileSync as readFileSync16 } from "node:fs";
21799
- import { isAbsolute as isAbsolute29, relative as relative28, resolve as resolve28 } from "node:path";
22102
+ import { isAbsolute as isAbsolute28, relative as relative27, resolve as resolve28 } from "node:path";
21800
22103
  var API_VERSION39 = "^0.1.10";
21801
22104
  var state53 = {
21802
22105
  generateCount: 0,
@@ -21852,21 +22155,21 @@ var SOURCE_EXTENSIONS = [
21852
22155
  ".cpp",
21853
22156
  ".hpp"
21854
22157
  ];
21855
- function withinProject9(p) {
22158
+ function withinProject8(p) {
21856
22159
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
21857
22160
  const root = process.cwd();
21858
- const resolved = isAbsolute29(p) ? resolve28(p) : resolve28(root, p);
21859
- const rel = relative28(root, resolved);
22161
+ const resolved = isAbsolute28(p) ? resolve28(p) : resolve28(root, p);
22162
+ const rel = relative27(root, resolved);
21860
22163
  if (rel === "" || rel === ".") return true;
21861
22164
  if (rel.startsWith("..")) return false;
21862
- if (isAbsolute29(rel)) return false;
22165
+ if (isAbsolute28(rel)) return false;
21863
22166
  return true;
21864
22167
  }
21865
22168
  function toPosix8(p) {
21866
22169
  return p.replace(/\\/g, "/");
21867
22170
  }
21868
22171
  function relativePath8(p) {
21869
- return toPosix8(relative28(process.cwd(), p));
22172
+ return toPosix8(relative27(process.cwd(), p));
21870
22173
  }
21871
22174
  function detectExports(content) {
21872
22175
  const exports = [];
@@ -22076,7 +22379,7 @@ var plugin58 = {
22076
22379
  if (!rawPath || typeof rawPath !== "string") {
22077
22380
  return { ok: false, error: "path is required" };
22078
22381
  }
22079
- if (!withinProject9(rawPath)) {
22382
+ if (!withinProject8(rawPath)) {
22080
22383
  return { ok: false, error: "path is outside the project root" };
22081
22384
  }
22082
22385
  if (!SOURCE_EXTENSIONS.some((ext) => rawPath.toLowerCase().endsWith(ext))) {
@@ -22172,7 +22475,7 @@ var test_generator_default = plugin58;
22172
22475
  // src/test-runner-gate/index.ts
22173
22476
  import { execFile as execFile15 } from "node:child_process";
22174
22477
  import { access as access5 } from "node:fs/promises";
22175
- import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute30, join as join8 } from "node:path";
22478
+ import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute29, join as join8 } from "node:path";
22176
22479
  import { buildWin32CmdShimInvocation as buildWin32CmdShimInvocation2, resolveWin32Command as resolveWin32Command2 } from "@wrongstack/tools/win32";
22177
22480
  function resolveExec(command, args) {
22178
22481
  const resolved = resolveWin32Command2(command);
@@ -22341,7 +22644,7 @@ function resolveAllowedCommand2(customCommand) {
22341
22644
  if (ALLOWED_COMMAND_TOKENS.has(head)) {
22342
22645
  return { cmd: head, args: tokens.slice(1) };
22343
22646
  }
22344
- if (isAbsolute30(head)) {
22647
+ if (isAbsolute29(head)) {
22345
22648
  if (!withinProject(head)) return null;
22346
22649
  const base = basename7(head);
22347
22650
  if (ALLOWED_COMMAND_TOKENS.has(base)) {