claude-crap 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +22 -25
  3. package/dist/dashboard/file-detail.d.ts +6 -0
  4. package/dist/dashboard/file-detail.d.ts.map +1 -1
  5. package/dist/dashboard/file-detail.js +1 -0
  6. package/dist/dashboard/file-detail.js.map +1 -1
  7. package/dist/monorepo/project-map.d.ts.map +1 -1
  8. package/dist/monorepo/project-map.js +135 -6
  9. package/dist/monorepo/project-map.js.map +1 -1
  10. package/dist/scanner/bootstrap.d.ts.map +1 -1
  11. package/dist/scanner/bootstrap.js +2 -2
  12. package/dist/scanner/bootstrap.js.map +1 -1
  13. package/dist/shared/exclusions.d.ts.map +1 -1
  14. package/dist/shared/exclusions.js +22 -0
  15. package/dist/shared/exclusions.js.map +1 -1
  16. package/package.json +1 -1
  17. package/plugin/.claude-plugin/plugin.json +1 -1
  18. package/plugin/bundle/dashboard/public/index.html +216 -7
  19. package/plugin/bundle/mcp-server.mjs +145 -31
  20. package/plugin/bundle/mcp-server.mjs.map +3 -3
  21. package/plugin/hooks/lib/gatekeeper-rules.mjs +274 -45
  22. package/plugin/hooks/lib/quality-gate.mjs +3 -0
  23. package/plugin/package-lock.json +8 -8
  24. package/plugin/package.json +1 -1
  25. package/src/dashboard/file-detail.ts +7 -0
  26. package/src/dashboard/public/index.html +216 -7
  27. package/src/monorepo/project-map.ts +144 -6
  28. package/src/scanner/bootstrap.ts +7 -2
  29. package/src/shared/exclusions.ts +26 -0
  30. package/src/tests/exclusions.test.ts +53 -0
  31. package/src/tests/file-detail-api.test.ts +38 -0
  32. package/src/tests/gatekeeper-rules.test.ts +173 -0
  33. package/src/tests/project-map.test.ts +216 -0
  34. package/src/tests/workspace-walker.test.ts +94 -0
@@ -317,6 +317,95 @@
317
317
  opacity: 0.7;
318
318
  margin-left: 12px;
319
319
  }
320
+ /* CC heat bar — ReportGenerator-style visual severity */
321
+ .heat-bar {
322
+ position: relative;
323
+ width: 140px;
324
+ height: 8px;
325
+ background: rgba(255, 255, 255, 0.06);
326
+ border-radius: 4px;
327
+ overflow: hidden;
328
+ }
329
+ .heat-bar-fill {
330
+ height: 100%;
331
+ border-radius: 4px;
332
+ transition: width 120ms ease-out;
333
+ }
334
+ .heat-bar-threshold {
335
+ position: absolute;
336
+ top: -2px;
337
+ bottom: -2px;
338
+ width: 1px;
339
+ background: rgba(255, 255, 255, 0.35);
340
+ }
341
+ /* CC chip — numeric value + % of threshold */
342
+ .cc-chip {
343
+ display: inline-flex;
344
+ align-items: baseline;
345
+ gap: 6px;
346
+ font-variant-numeric: tabular-nums;
347
+ }
348
+ .cc-chip .cc-value {
349
+ font-weight: 700;
350
+ font-size: 13px;
351
+ }
352
+ .cc-chip .cc-ratio {
353
+ font-size: 11px;
354
+ color: var(--muted);
355
+ }
356
+ /* Method name button — scrolls source view to fn start line */
357
+ .method-jump {
358
+ background: none;
359
+ border: none;
360
+ padding: 0;
361
+ font: inherit;
362
+ font-family: "SF Mono", "Fira Code", "Consolas", monospace;
363
+ font-size: 13px;
364
+ color: var(--accent);
365
+ cursor: pointer;
366
+ }
367
+ .method-jump:hover { text-decoration: underline; }
368
+ /* "Open in editor" icon link */
369
+ .editor-link {
370
+ display: inline-flex;
371
+ align-items: center;
372
+ justify-content: center;
373
+ width: 24px;
374
+ height: 24px;
375
+ border-radius: 4px;
376
+ color: var(--muted);
377
+ text-decoration: none;
378
+ font-size: 13px;
379
+ transition: background 120ms, color 120ms;
380
+ }
381
+ .editor-link:hover {
382
+ background: rgba(62, 166, 255, 0.12);
383
+ color: var(--accent);
384
+ text-decoration: none;
385
+ }
386
+ /* "Show all / show fewer" toggle under the methods table */
387
+ .show-all-btn {
388
+ display: inline-block;
389
+ margin: 12px 0 0 0;
390
+ padding: 6px 12px;
391
+ background: transparent;
392
+ border: 1px solid var(--border);
393
+ border-radius: 6px;
394
+ color: var(--accent);
395
+ font-size: 12px;
396
+ cursor: pointer;
397
+ }
398
+ .show-all-btn:hover {
399
+ background: rgba(62, 166, 255, 0.08);
400
+ }
401
+ /* Line-flash animation when the user jumps to a source line */
402
+ .source-line.jump-target {
403
+ animation: jumpFlash 1.2s ease-out;
404
+ }
405
+ @keyframes jumpFlash {
406
+ 0% { background: rgba(62, 166, 255, 0.35); }
407
+ 100% { background: transparent; }
408
+ }
320
409
  </style>
321
410
  </head>
322
411
  <body>
@@ -372,33 +461,74 @@
372
461
  </div>
373
462
  </div>
374
463
 
375
- <!-- Methods table -->
376
- <div v-if="fileDetail.functions.length" class="section-title">Methods</div>
464
+ <!-- Methods table — top-5 by CC with heat bar + jump + editor link -->
465
+ <div v-if="fileDetail.functions.length" class="section-title">
466
+ Methods
467
+ <span style="color: var(--muted); font-weight: 400; text-transform: none; letter-spacing: 0; margin-left: 8px;">
468
+ threshold CC {{ fileDetail.cyclomaticMax }}
469
+ </span>
470
+ </div>
377
471
  <div v-if="fileDetail.functions.length" class="card">
378
472
  <table>
379
473
  <thead>
380
474
  <tr>
381
475
  <th>Method</th>
382
476
  <th style="text-align: right">Line</th>
383
- <th style="text-align: right">CC</th>
477
+ <th style="width: 180px;">CC</th>
384
478
  <th style="text-align: right">Lines</th>
385
479
  <th>Status</th>
480
+ <th style="width: 32px;"></th>
386
481
  </tr>
387
482
  </thead>
388
483
  <tbody>
389
- <tr v-for="fn in sortedFunctions" :key="fn.startLine">
390
- <td><code>{{ fn.name }}</code></td>
484
+ <tr v-for="fn in visibleFunctions" :key="fn.startLine">
485
+ <td>
486
+ <button
487
+ class="method-jump"
488
+ @click="jumpToLine(fn.startLine)"
489
+ :title="'Jump to line ' + fn.startLine"
490
+ >{{ fn.name }}</button>
491
+ </td>
391
492
  <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.startLine }}</td>
392
- <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.cyclomaticComplexity }}</td>
493
+ <td>
494
+ <div class="cc-chip">
495
+ <div class="heat-bar" :title="ccTooltip(fn)">
496
+ <div
497
+ class="heat-bar-fill"
498
+ :style="heatBarStyle(fn)"
499
+ ></div>
500
+ <div
501
+ class="heat-bar-threshold"
502
+ :style="{ left: thresholdMarker() + '%' }"
503
+ ></div>
504
+ </div>
505
+ <span class="cc-value" :style="{ color: ccColor(fn) }">{{ fn.cyclomaticComplexity }}</span>
506
+ <span class="cc-ratio">{{ ccRatio(fn) }}%</span>
507
+ </div>
508
+ </td>
393
509
  <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.lineCount }}</td>
394
510
  <td>
395
511
  <span v-if="fn.cyclomaticComplexity >= fileDetail.cyclomaticMax * 2" class="pill pill-error">error</span>
396
512
  <span v-else-if="fn.cyclomaticComplexity > fileDetail.cyclomaticMax" class="pill pill-warning">warning</span>
397
513
  <span v-else class="pill pill-note">ok</span>
398
514
  </td>
515
+ <td>
516
+ <a
517
+ class="editor-link"
518
+ :href="editorLink(fn)"
519
+ :title="'Open ' + fileDetail.filePath + ':' + fn.startLine + ' in VS Code'"
520
+ >&#x2197;</a>
521
+ </td>
399
522
  </tr>
400
523
  </tbody>
401
524
  </table>
525
+ <button
526
+ v-if="fileDetail.functions.length > topMethodsLimit"
527
+ class="show-all-btn"
528
+ @click="toggleShowAllMethods()"
529
+ >
530
+ {{ showAllMethods ? 'Show top ' + topMethodsLimit : 'Show all ' + fileDetail.functions.length + ' methods' }}
531
+ </button>
402
532
  </div>
403
533
 
404
534
  <!-- Findings table -->
@@ -622,11 +752,87 @@
622
752
  });
623
753
 
624
754
  // ── File detail computed ──
755
+ const topMethodsLimit = 5;
756
+ const showAllMethods = ref(false);
757
+
625
758
  const sortedFunctions = computed(() => {
626
759
  if (!fileDetail.value) return [];
627
760
  return [...fileDetail.value.functions].sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
628
761
  });
629
762
 
763
+ const visibleFunctions = computed(() => {
764
+ if (showAllMethods.value) return sortedFunctions.value;
765
+ return sortedFunctions.value.slice(0, topMethodsLimit);
766
+ });
767
+
768
+ function toggleShowAllMethods() {
769
+ showAllMethods.value = !showAllMethods.value;
770
+ }
771
+
772
+ // ── CC heat-bar helpers ──
773
+ // Fill width is clamped at 3× threshold so a CC of 80 with
774
+ // threshold 15 still produces a visually meaningful bar rather
775
+ // than overflowing. The threshold marker sits at the "1.0×"
776
+ // position (i.e. threshold/3threshold = 33%).
777
+ function ccRatio(fn) {
778
+ const max = fileDetail.value?.cyclomaticMax || 1;
779
+ return Math.round((fn.cyclomaticComplexity / max) * 100);
780
+ }
781
+
782
+ function heatBarStyle(fn) {
783
+ const max = fileDetail.value?.cyclomaticMax || 1;
784
+ const cap = max * 3;
785
+ const pct = Math.min(100, (fn.cyclomaticComplexity / cap) * 100);
786
+ return { width: pct + "%", background: ccColor(fn) };
787
+ }
788
+
789
+ function thresholdMarker() {
790
+ // threshold sits at 1/3 of the bar (since we cap at 3× threshold)
791
+ return 33.33;
792
+ }
793
+
794
+ function ccColor(fn) {
795
+ const max = fileDetail.value?.cyclomaticMax || 1;
796
+ const r = fn.cyclomaticComplexity / max;
797
+ if (r >= 2) return "var(--rating-E)"; // red — error (≥ 2×)
798
+ if (r > 1) return "var(--rating-C)"; // yellow — warning
799
+ if (r > 0.66) return "var(--rating-B)"; // yellow-green — near threshold
800
+ return "var(--rating-A)"; // green — healthy
801
+ }
802
+
803
+ function ccTooltip(fn) {
804
+ const max = fileDetail.value?.cyclomaticMax || 1;
805
+ return (
806
+ "CC " + fn.cyclomaticComplexity +
807
+ " / threshold " + max +
808
+ " (" + ccRatio(fn) + "% of threshold)"
809
+ );
810
+ }
811
+
812
+ // ── Editor deep-link + jump-to-line ──
813
+ // vscode:// handler accepts absolute paths. The `absolutePath`
814
+ // field is resolved server-side through the workspace-traversal
815
+ // guard, so this is safe to paste into an href.
816
+ function editorLink(fn) {
817
+ const abs = fileDetail.value?.absolutePath;
818
+ if (!abs) return "#";
819
+ return "vscode://file" + abs + ":" + fn.startLine + ":1";
820
+ }
821
+
822
+ function jumpToLine(lineNum) {
823
+ // Source lines are keyed by 0-based index, so line N lives at
824
+ // child index N-1 of `.source-view`.
825
+ const view = document.querySelector(".source-view");
826
+ if (!view) return;
827
+ const row = view.children[lineNum - 1];
828
+ if (!row) return;
829
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
830
+ row.classList.remove("jump-target");
831
+ // force reflow so the animation restarts on repeat clicks
832
+ void row.offsetWidth;
833
+ row.classList.add("jump-target");
834
+ }
835
+
630
836
  const sortedFindings = computed(() => {
631
837
  if (!fileDetail.value) return [];
632
838
  return [...fileDetail.value.findings].sort((a, b) => a.startLine - b.startLine);
@@ -777,7 +983,10 @@
777
983
  currentView, selectedFile, fileDetail,
778
984
  score, complexity, loading, error,
779
985
  toolEntries, fileEntries, formatTimestamp,
780
- sortedFunctions, sortedFindings,
986
+ sortedFunctions, sortedFindings, visibleFunctions,
987
+ showAllMethods, toggleShowAllMethods, topMethodsLimit,
988
+ ccRatio, ccColor, ccTooltip, heatBarStyle, thresholdMarker,
989
+ editorLink, jumpToLine,
781
990
  navigateToFile, goBack,
782
991
  lineFindings, lineClass, gutterClass, lineFnLabel,
783
992
  };
@@ -3585,49 +3585,49 @@ var require_fast_uri = __commonJS({
3585
3585
  schemelessOptions.skipEscape = true;
3586
3586
  return serialize(resolved, schemelessOptions);
3587
3587
  }
3588
- function resolveComponent(base, relative6, options, skipNormalization) {
3588
+ function resolveComponent(base, relative7, options, skipNormalization) {
3589
3589
  const target = {};
3590
3590
  if (!skipNormalization) {
3591
3591
  base = parse(serialize(base, options), options);
3592
- relative6 = parse(serialize(relative6, options), options);
3592
+ relative7 = parse(serialize(relative7, options), options);
3593
3593
  }
3594
3594
  options = options || {};
3595
- if (!options.tolerant && relative6.scheme) {
3596
- target.scheme = relative6.scheme;
3597
- target.userinfo = relative6.userinfo;
3598
- target.host = relative6.host;
3599
- target.port = relative6.port;
3600
- target.path = removeDotSegments(relative6.path || "");
3601
- target.query = relative6.query;
3595
+ if (!options.tolerant && relative7.scheme) {
3596
+ target.scheme = relative7.scheme;
3597
+ target.userinfo = relative7.userinfo;
3598
+ target.host = relative7.host;
3599
+ target.port = relative7.port;
3600
+ target.path = removeDotSegments(relative7.path || "");
3601
+ target.query = relative7.query;
3602
3602
  } else {
3603
- if (relative6.userinfo !== void 0 || relative6.host !== void 0 || relative6.port !== void 0) {
3604
- target.userinfo = relative6.userinfo;
3605
- target.host = relative6.host;
3606
- target.port = relative6.port;
3607
- target.path = removeDotSegments(relative6.path || "");
3608
- target.query = relative6.query;
3603
+ if (relative7.userinfo !== void 0 || relative7.host !== void 0 || relative7.port !== void 0) {
3604
+ target.userinfo = relative7.userinfo;
3605
+ target.host = relative7.host;
3606
+ target.port = relative7.port;
3607
+ target.path = removeDotSegments(relative7.path || "");
3608
+ target.query = relative7.query;
3609
3609
  } else {
3610
- if (!relative6.path) {
3610
+ if (!relative7.path) {
3611
3611
  target.path = base.path;
3612
- if (relative6.query !== void 0) {
3613
- target.query = relative6.query;
3612
+ if (relative7.query !== void 0) {
3613
+ target.query = relative7.query;
3614
3614
  } else {
3615
3615
  target.query = base.query;
3616
3616
  }
3617
3617
  } else {
3618
- if (relative6.path[0] === "/") {
3619
- target.path = removeDotSegments(relative6.path);
3618
+ if (relative7.path[0] === "/") {
3619
+ target.path = removeDotSegments(relative7.path);
3620
3620
  } else {
3621
3621
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3622
- target.path = "/" + relative6.path;
3622
+ target.path = "/" + relative7.path;
3623
3623
  } else if (!base.path) {
3624
- target.path = relative6.path;
3624
+ target.path = relative7.path;
3625
3625
  } else {
3626
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
3626
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative7.path;
3627
3627
  }
3628
3628
  target.path = removeDotSegments(target.path);
3629
3629
  }
3630
- target.query = relative6.query;
3630
+ target.query = relative7.query;
3631
3631
  }
3632
3632
  target.userinfo = base.userinfo;
3633
3633
  target.host = base.host;
@@ -3635,7 +3635,7 @@ var require_fast_uri = __commonJS({
3635
3635
  }
3636
3636
  target.scheme = base.scheme;
3637
3637
  }
3638
- target.fragment = relative6.fragment;
3638
+ target.fragment = relative7.fragment;
3639
3639
  return target;
3640
3640
  }
3641
3641
  function equal(uriA, uriB, options) {
@@ -7454,6 +7454,42 @@ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
7454
7454
  "out",
7455
7455
  "target",
7456
7456
  "coverage",
7457
+ "artifacts",
7458
+ // CI artefact staging, Maven
7459
+ "publish",
7460
+ // `dotnet publish` output
7461
+ // Test coverage report bundles (generated HTML/JS from coverage tools;
7462
+ // walking them floods the complexity scanner with synthetic minified
7463
+ // functions like `coverage-report/main.js::gG` at CC 80+).
7464
+ "coverage-report",
7465
+ // ReportGenerator default (.NET)
7466
+ "CoverageReport",
7467
+ // ReportGenerator PascalCase variant
7468
+ "coveragereport",
7469
+ // ReportGenerator lowercase fallback
7470
+ "TestResults",
7471
+ // `dotnet test` default output
7472
+ "cobertura",
7473
+ // Cobertura XML reporter
7474
+ "lcov-report",
7475
+ // Istanbul HTML reporter
7476
+ "htmlcov",
7477
+ // coverage.py HTML output
7478
+ // Desktop / mobile packaging outputs
7479
+ "dist-electron",
7480
+ // Electron-builder
7481
+ "release",
7482
+ // Electron-builder, Tauri
7483
+ // .NET per-project build outputs (conventional at any depth)
7484
+ "bin",
7485
+ "obj",
7486
+ // iOS / macOS dependency + build caches
7487
+ "Pods",
7488
+ // CocoaPods
7489
+ "DerivedData",
7490
+ // Xcode
7491
+ "Carthage",
7492
+ // Swift
7457
7493
  // Framework build outputs
7458
7494
  ".next",
7459
7495
  // Next.js
@@ -7816,6 +7852,7 @@ async function buildFileDetail(input) {
7816
7852
  ) / 100 : 0;
7817
7853
  return {
7818
7854
  filePath: relativePath,
7855
+ absolutePath,
7819
7856
  language,
7820
7857
  physicalLoc,
7821
7858
  logicalLoc,
@@ -9124,7 +9161,9 @@ function detectProjectType(workspaceRoot) {
9124
9161
  if (has("Directory.Build.props")) return "csharp";
9125
9162
  try {
9126
9163
  const entries = readdirSync2(workspaceRoot);
9127
- if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
9164
+ if (entries.some(
9165
+ (e) => e.endsWith(".csproj") || e.endsWith(".sln") || e.endsWith(".slnx")
9166
+ )) {
9128
9167
  return "csharp";
9129
9168
  }
9130
9169
  } catch {
@@ -9716,7 +9755,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
9716
9755
  // src/monorepo/project-map.ts
9717
9756
  import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
9718
9757
  import { promises as fs8 } from "node:fs";
9719
- import { join as join12, basename as basename2, resolve as resolve8 } from "node:path";
9758
+ import { join as join12, basename as basename2, resolve as resolve8, relative as relative6, isAbsolute as isAbsolute5 } from "node:path";
9720
9759
  import { execFile as execFile4 } from "node:child_process";
9721
9760
  var MONOREPO_DIRS2 = ["apps", "packages", "libs", "modules", "services"];
9722
9761
  var SCANNER_FOR_TYPE = {
@@ -9758,7 +9797,9 @@ function detectProjectType2(dir) {
9758
9797
  if (has("Directory.Build.props")) return "csharp";
9759
9798
  try {
9760
9799
  const entries = readdirSync3(dir);
9761
- if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
9800
+ if (entries.some(
9801
+ (e) => e.endsWith(".csproj") || e.endsWith(".sln") || e.endsWith(".slnx")
9802
+ )) {
9762
9803
  return "csharp";
9763
9804
  }
9764
9805
  } catch {
@@ -9776,17 +9817,66 @@ function extractWorkspacePatterns(workspaces) {
9776
9817
  }
9777
9818
  return [];
9778
9819
  }
9820
+ function stripYamlComment(line) {
9821
+ let inSingle = false;
9822
+ let inDouble = false;
9823
+ for (let i = 0; i < line.length; i++) {
9824
+ const ch = line[i];
9825
+ if (ch === "\\" && inDouble && i + 1 < line.length) {
9826
+ i++;
9827
+ continue;
9828
+ }
9829
+ if (!inDouble && ch === "'") {
9830
+ inSingle = !inSingle;
9831
+ } else if (!inSingle && ch === '"') {
9832
+ inDouble = !inDouble;
9833
+ } else if (!inSingle && !inDouble && ch === "#") {
9834
+ return line.slice(0, i);
9835
+ }
9836
+ }
9837
+ return line;
9838
+ }
9839
+ function parsePnpmWorkspaceYaml(yaml) {
9840
+ const patterns = [];
9841
+ const lines = yaml.split(/\r?\n/);
9842
+ let inPackages = false;
9843
+ for (const rawLine of lines) {
9844
+ const line = stripYamlComment(rawLine).replace(/\s+$/, "");
9845
+ if (line.length === 0) continue;
9846
+ if (/^packages\s*:\s*$/.test(line)) {
9847
+ inPackages = true;
9848
+ continue;
9849
+ }
9850
+ if (inPackages && /^[^\s-]/.test(line)) {
9851
+ inPackages = false;
9852
+ continue;
9853
+ }
9854
+ if (!inPackages) continue;
9855
+ const m = /^\s*-\s*("([^"]*)"|'([^']*)'|(\S+))\s*$/.exec(line);
9856
+ if (m) {
9857
+ const value = m[2] ?? m[3] ?? m[4] ?? "";
9858
+ if (value.length > 0) patterns.push(value);
9859
+ }
9860
+ }
9861
+ return patterns;
9862
+ }
9779
9863
  function expandWorkspacePattern(workspaceRoot, pattern) {
9864
+ const isInsideWorkspace = (candidate) => {
9865
+ const rel = relative6(workspaceRoot, candidate);
9866
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
9867
+ };
9780
9868
  if (pattern.endsWith("/*")) {
9781
9869
  const parentDir = join12(workspaceRoot, pattern.slice(0, -2));
9870
+ if (!isInsideWorkspace(parentDir)) return [];
9782
9871
  try {
9783
9872
  const entries = readdirSync3(parentDir, { withFileTypes: true });
9784
- return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join12(parentDir, e.name));
9873
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join12(parentDir, e.name)).filter(isInsideWorkspace);
9785
9874
  } catch {
9786
9875
  return [];
9787
9876
  }
9788
9877
  }
9789
9878
  const full = resolve8(workspaceRoot, pattern);
9879
+ if (!isInsideWorkspace(full)) return [];
9790
9880
  try {
9791
9881
  const entries = readdirSync3(full, { withFileTypes: true });
9792
9882
  void entries;
@@ -9811,12 +9901,24 @@ function collectSubdirectories(workspaceRoot, extraDirs) {
9811
9901
  } catch {
9812
9902
  }
9813
9903
  }
9904
+ const pnpmPath = join12(workspaceRoot, "pnpm-workspace.yaml");
9905
+ if (existsSync6(pnpmPath)) {
9906
+ try {
9907
+ const raw = readFileSync5(pnpmPath, "utf-8");
9908
+ const patterns = parsePnpmWorkspaceYaml(raw);
9909
+ for (const pattern of patterns) {
9910
+ for (const absPath of expandWorkspacePattern(workspaceRoot, pattern)) {
9911
+ subdirs.add(absPath);
9912
+ }
9913
+ }
9914
+ } catch {
9915
+ }
9916
+ }
9814
9917
  if (extraDirs && extraDirs.length > 0) {
9815
9918
  for (const dir of extraDirs) {
9816
9919
  const absDir = resolve8(workspaceRoot, dir);
9817
9920
  if (!existsSync6(absDir)) continue;
9818
- const hasMarker = PROJECT_MARKERS.some((m) => existsSync6(join12(absDir, m)));
9819
- if (hasMarker) {
9921
+ if (directoryIsProjectRoot(absDir)) {
9820
9922
  subdirs.add(absDir);
9821
9923
  continue;
9822
9924
  }
@@ -9857,6 +9959,18 @@ var PROJECT_MARKERS = [
9857
9959
  "build.gradle.kts",
9858
9960
  "Directory.Build.props"
9859
9961
  ];
9962
+ var DOTNET_PROJECT_EXTENSIONS = [".csproj", ".sln", ".slnx"];
9963
+ function directoryIsProjectRoot(absDir) {
9964
+ if (PROJECT_MARKERS.some((m) => existsSync6(join12(absDir, m)))) return true;
9965
+ try {
9966
+ const entries = readdirSync3(absDir);
9967
+ return entries.some(
9968
+ (e) => DOTNET_PROJECT_EXTENSIONS.some((ext) => e.endsWith(ext))
9969
+ );
9970
+ } catch {
9971
+ return false;
9972
+ }
9973
+ }
9860
9974
  async function discoverProjectMap(workspaceRoot, options) {
9861
9975
  const subdirs = collectSubdirectories(workspaceRoot, options?.projectDirs);
9862
9976
  const binaryCache = /* @__PURE__ */ new Map();