diff --git a/AGENTS.md b/AGENTS.md
index 28f5887..5456012 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -80,6 +80,9 @@ src/
completions.ts # Pure shell-completion generators: generateCompletion(),
# detectShell(), getCompletionFilePath() — no I/O
group.ts # groupByTeamPrefix — team-prefix grouping logic
+ regex.ts # Pure query parser: isRegexQuery(), buildApiQuery()
+ # Detects /pattern/ syntax, derives safe API term,
+ # returns RegExp for local client-side filtering — no I/O
render.ts # Façade re-exporting sub-modules + top-level
# renderGroups() / renderHelpOverlay()
tui.ts # Interactive keyboard-driven UI (navigation, filter mode,
diff --git a/README.md b/README.md
index 3f8320c..45e57ca 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,14 @@ github-code-search query "useFeatureFlag" --org my-org --group-by-team-prefix pl
Get a team-scoped view of every usage site before refactoring a shared hook or utility.
+**Regex search — pattern-based code audit**
+
+```bash
+github-code-search query "/from.*['\"\`]axios/" --org my-org
+```
+
+Use `/pattern/` syntax to run a regex search. The CLI automatically derives a safe API query term and filters results locally — no manual post-processing needed. Use `--regex-hint` to override the derived term when auto-extraction is too broad.
+
## Why not `gh search code`?
The official [`gh` CLI](https://cli.github.com/) does support `gh search code`, but it returns a **flat paginated list** — one result per line, no grouping, no interactive selection, no structured output.
diff --git a/bun.lock b/bun.lock
index 758c50c..fc39b84 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,9 +13,9 @@
"@resvg/resvg-js": "^2.6.2",
"bun-types": "^1.3.10",
"knip": "^5.86.0",
- "mermaid": "^11.0.0",
- "oxfmt": "^0.36.0",
- "oxlint": "^1.51.0",
+ "mermaid": "^11.13.0",
+ "oxfmt": "^0.40.0",
+ "oxlint": "^1.55.0",
"pa11y-ci": "^4.1.0",
"sharp": "^0.34.5",
"vitepress": "^1.6.3",
@@ -214,7 +214,7 @@
"@lhci/utils": ["@lhci/utils@0.15.1", "", { "dependencies": { "debug": "^4.3.1", "isomorphic-fetch": "^3.0.0", "js-yaml": "^3.13.1", "lighthouse": "12.6.1", "tree-kill": "^1.2.1" } }, "sha512-WclJnUQJeOMY271JSuaOjCv/aA0pgvuHZS29NFNdIeI14id8eiFsjith85EGKYhljgoQhJ2SiW4PsVfFiakNNw=="],
- "@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
+ "@mermaid-js/parser": ["@mermaid-js/parser@1.0.1", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
@@ -264,81 +264,81 @@
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="],
- "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.36.0", "", { "os": "android", "cpu": "arm" }, "sha512-Z4yVHJWx/swHHjtr0dXrBZb6LxS+qNz1qdza222mWwPTUK4L790+5i3LTgjx3KYGBzcYpjaiZBw4vOx94dH7MQ=="],
+ "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-S6zd5r1w/HmqR8t0CTnGjFTBLDq2QKORPwriCHxo4xFNuhmOTABGjPaNvCJJVnrKBLsohOeiDX3YqQfJPF+FXw=="],
- "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.36.0", "", { "os": "android", "cpu": "arm64" }, "sha512-3ElCJRFNPQl7jexf2CAa9XmAm8eC5JPrIDSjc9jSchkVSFTEqyL0NtZinBB2h1a4i4JgP1oGl/5G5n8YR4FN8Q=="],
+ "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-/mbS9UUP/5Vbl2D6osIdcYiP0oie63LKMoTyGj5hyMCK/SFkl3EhtyRAfdjPvuvHC0SXdW6ePaTKkBSq1SNcIw=="],
- "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.36.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nak4znWCqIExKhYSY/mz/lWsqWIpdsS7o0+SRzXR1Q0m7GrMcG1UrF1pS7TLGZhhkf7nTfEF7q6oZzJiodRDuw=="],
+ "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wRt8fRdfLiEhnRMBonlIbKrJWixoEmn6KCjKE9PElnrSDSXETGZfPb8ee+nQNTobXkCVvVLytp2o0obAsxl78Q=="],
- "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.36.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-V4GP96thDnpKx6ADnMDnhIXNdtV+Ql9D4HUU+a37VTeVbs5qQSF/s6hhUP1b3xUqU7iRcwh72jUU2Y12rtGHAw=="],
+ "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fzowhqbOE/NRy+AE5ob0+Y4X243WbWzDb00W+pKwD7d9tOqsAFbtWUwIyqqCoCLxj791m2xXIEeLH/3uz7zCCg=="],
- "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.36.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/xapWCADfI5wrhxpEUjhI9fnw7MV5BUZizVa8e24n3VSK6A3Y1TB/ClOP1tfxNspykFKXp4NBWl6NtDJP3osqQ=="],
+ "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-agZ9ITaqdBjcerRRFEHB8s0OyVcQW8F9ZxsszjxzeSthQ4fcN2MuOtQFWec1ed8/lDa50jSLHVE2/xPmTgtCfQ=="],
- "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.36.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1lOmv61XMFIH5uNm27620kRRzWt/RK6tdn250BRDoG9W7OXGOQ5UyI1HVT+SFkoOoKztBiinWgi68+NA1MjBVQ=="],
+ "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZM2oQ47p28TP1DVIp7HL1QoMUgqlBFHey0ksHct7tMXoU5BqjNvPWw7888azzMt25lnyPODVuye1wvNbvVUFOA=="],
- "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.36.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vMH23AskdR1ujUS9sPck2Df9rBVoZUnCVY86jisILzIQ/QQ/yKUTi7tgnIvydPx7TyB/48wsQ5QMr5Knq5p/aw=="],
+ "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RBFPAxRAIsMisKM47Oe6Lwdv6agZYLz02CUhVCD1sOv5ajAcRMrnwCFBPWwGXpazToW2mjnZxFos8TuFjTU15A=="],
- "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hy1V+zOBHpBiENRx77qrUTt5aPDHeCASRc8K5KwwAHkX2AKP0nV89eL17hsZrE9GmnXFjsNmd80lyf7aRTXsbw=="],
+ "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nb2XbQ+wV3W2jSIihXdPj7k83eOxeSgYP3N/SRXvQ6ZYPIk6Q86qEh5Gl/7OitX3bQoQrESqm1yMLvZV8/J7dA=="],
- "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-SPGLJkOIHSIC6ABUQ5V8NqJpvYhMJueJv26NYqfCnwi/Mn6A61amkpJJ9Suy0Nmvs+OWESJpcebrBUbXPGZyQQ=="],
+ "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tGmWhLD/0YMotCdfezlT6tC/MJG/wKpo4vnQ3Cq+4eBk/BwNv7EmkD0VkD5F/dYkT3b8FNU01X2e8vvJuWoM1w=="],
- "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.36.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3EuoyB8x9x8ysYJjbEO/M9fkSk72zQKnXCvpZMDHXlnY36/1qMp55Nm0PrCwjGO/1pen5hdOVkz9WmP3nAp2IQ=="],
+ "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rVbFyM3e7YhkVnp0IVYjaSHfrBWcTRWb60LEcdNAJcE2mbhTpbqKufx0FrhWfoxOrW/+7UJonAOShoFFLigDqQ=="],
- "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.36.0", "", { "os": "linux", "cpu": "none" }, "sha512-MpY3itLwpGh8dnywtrZtaZ604T1m715SydCKy0+qTxetv+IHzuA+aO/AGzrlzUNYZZmtWtmDBrChZGibvZxbRQ=="],
+ "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-3ZqBw14JtWeEoLiioJcXSJz8RQyPE+3jLARnYM1HdPzZG4vk+Ua8CUupt2+d+vSAvMyaQBTN2dZK+kbBS/j5mA=="],
- "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.36.0", "", { "os": "linux", "cpu": "none" }, "sha512-mmDhe4Vtx+XwQPRPn/V25+APnkApYgZ23q+6GVsNYY98pf3aU0aI3Me96pbRs/AfJ1jIiGC+/6q71FEu8dHcHw=="],
+ "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-JJ4PPSdcbGBjPvb+O7xYm2FmAsKCyuEMYhqatBAHMp/6TA6rVlf9Z/sYPa4/3Bommb+8nndm15SPFRHEPU5qFA=="],
- "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.36.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-AYXhU+DmNWLSnvVwkHM92fuYhogtVHab7UQrPNaDf1sxadugg9gWVmcgJDlIwxJdpk5CVW/TFvwUKwI432zhhA=="],
+ "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Kp0zNJoX9Ik77wUya2tpBY3W9f40VUoMQLWVaob5SgCrblH/t2xr/9B2bWHfs0WCefuGmqXcB+t0Lq77sbBmZw=="],
- "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-H16QhhQ3usoakMleiAAQ2mg0NsBDAdyE9agUgfC8IHHh3jZEbr0rIKwjEqwbOHK5M0EmfhJmr+aGO/MgZPsneA=="],
+ "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7YTCNzleWTaQTqNGUNQ66qVjpoV6DjbCOea+RnpMBly2bpzrI/uu7Rr+2zcgRfNxyjXaFTVQKaRKjqVdeUfeVA=="],
- "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EFFGkixA39BcmHiCe2ECdrq02D6FCve5ka6ObbvrheXl4V+R0U/E+/uLyVx1X65LW8TA8QQHdnbdDallRekohw=="],
+ "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hWnSzJ0oegeOwfOEeejYXfBqmnRGHusgtHfCPzmvJvHTwy1s3Neo59UKc1CmpE3zxvrCzJoVHos0rr97GHMNPw=="],
- "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.36.0", "", { "os": "none", "cpu": "arm64" }, "sha512-zr/t369wZWFOj1qf06Z5gGNjFymfUNDrxKMmr7FKiDRVI1sNsdKRCuRL4XVjtcptKQ+ao3FfxLN1vrynivmCYg=="],
+ "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.40.0", "", { "os": "none", "cpu": "arm64" }, "sha512-28sJC1lR4qtBJGzSRRbPnSW3GxU2+4YyQFE6rCmsUYqZ5XYH8jg0/w+CvEzQ8TuAQz5zLkcA25nFQGwoU0PT3Q=="],
- "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.36.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FxO7UksTv8h4olzACgrqAXNF6BP329+H322323iDrMB5V/+a1kcAw07fsOsUmqNrb9iJBsCQgH/zqcqp5903ag=="],
+ "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-cDkRnyT0dqwF5oIX1Cv59HKCeZQFbWWdUpXa3uvnHFT2iwYSSZspkhgjXjU6iDp5pFPaAEAe9FIbMoTgkTmKPg=="],
- "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.36.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OjoMQ89H01M0oLMfr/CPNH1zi48ZIwxAKObUl57oh7ssUBNDp/2Vjf7E1TQ8M4oj4VFQ/byxl2SmcPNaI2YNDg=="],
+ "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-7rPemBJjqm5Gkv6ZRCPvK8lE6AqQ/2z31DRdWazyx2ZvaSgL7QGofHXHNouRpPvNsT9yxRNQJgigsWkc+0qg4w=="],
- "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MoyeQ9S36ZTz/4bDhOKJgOBIDROd4dQ5AkT9iezhEaUBxAPdNX9Oq0jD8OSnCj3G4wam/XNxVWKMA52kmzmPtQ=="],
+ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="],
- "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.51.0", "", { "os": "android", "cpu": "arm" }, "sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw=="],
+ "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.55.0", "", { "os": "android", "cpu": "arm" }, "sha512-NhvgAhncTSOhRahQSCnkK/4YIGPjTmhPurQQ2dwt2IvwCMTvZRW5vF2K10UBOxFve4GZDMw6LtXZdC2qeuYIVQ=="],
- "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.51.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw=="],
+ "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.55.0", "", { "os": "android", "cpu": "arm64" }, "sha512-P9iWRh+Ugqhg+D7rkc7boHX8o3H2h7YPcZHQIgvVBgnua5tk4LR2L+IBlreZs58/95cd2x3/004p5VsQM9z4SA=="],
- "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.51.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ=="],
+ "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.55.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-esakkJIt7WFAhT30P/Qzn96ehFpzdZ1mNuzpOb8SCW7lI4oB8VsyQnkSHREM671jfpuBb/o2ppzBCx5l0jpgMA=="],
- "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.51.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg=="],
+ "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.55.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDMFRCCAEK9fOH6As2z8ELsC+VDGSFRHwIKVSilw+xhgLwTDFu37rtmRbmUlx8rRGS6cWKQPTc47AVxAZEVVPQ=="],
- "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.51.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw=="],
+ "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.55.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mYZqnwUD7ALCRxGenyLd1uuG+rHCL+OTT6S8FcAbVm/ZT2AZMGjvibp3F6k1SKOb2aeqFATmwRykrE41Q0GWVw=="],
- "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw=="],
+ "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.55.0", "", { "os": "linux", "cpu": "arm" }, "sha512-LcX6RYcF9vL9ESGwJW3yyIZ/d/ouzdOKXxCdey1q0XJOW1asrHsIg5MmyKdEBR4plQx+shvYeQne7AzW5f3T1w=="],
- "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ=="],
+ "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.55.0", "", { "os": "linux", "cpu": "arm" }, "sha512-C+8GS1rPtK+dI7mJFkqoRBkDuqbrNihnyYQsJPS9ez+8zF9JzfvU19lawqt4l/Y23o5uQswE/DORa8aiXUih3w=="],
- "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA=="],
+ "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.55.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ErLE4XbmcCopA4/CIDiH6J1IAaDOMnf/KSx/aFObs4/OjAAM3sFKWGZ57pNOMxhhyBdcmcXwYymph9GwcpcqgQ=="],
- "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ=="],
+ "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.55.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/kp65avi6zZfqEng56TTuhiy3P/3pgklKIdf38yvYeJ9/PgEeRA2A2AqKAKbZBNAqUzrzHhz9jF6j/PZvhJzTQ=="],
- "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.51.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q=="],
+ "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.55.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-A6pTdXwcEEwL/nmz0eUJ6WxmxcoIS+97GbH96gikAyre3s5deC7sts38ZVVowjS2QQFuSWkpA4ZmQC0jZSNvJQ=="],
- "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA=="],
+ "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.55.0", "", { "os": "linux", "cpu": "none" }, "sha512-clj0lnIN+V52G9tdtZl0LbdTSurnZ1NZj92Je5X4lC7gP5jiCSW+Y/oiDiSauBAD4wrHt2S7nN3pA0zfKYK/6Q=="],
- "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg=="],
+ "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.55.0", "", { "os": "linux", "cpu": "none" }, "sha512-NNu08pllN5x/O94/sgR3DA8lbrGBnTHsINZZR0hcav1sj79ksTiKKm1mRzvZvacwQ0hUnGinFo+JO75ok2PxYg=="],
- "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.51.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A=="],
+ "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.55.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvfQz3PRlWZRoEZ17dZCqgQsMRdpzGZomJkVATwCIGhHVVeHJMQdmdXPSjcT1DCNUrOjXnVyj1RGDj5+/Je2+Q=="],
- "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g=="],
+ "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.55.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ngSOoFCSBMKVQd24H8zkbcBNc7EHhjnF1sv3mC9NNXQ/4rRjI/4Dj9+9XoDZeFEkF1SX1COSBXF1b2Pr9rqdEw=="],
- "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug=="],
+ "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.55.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BDpP7W8GlaG7BR6QjGZAleYzxoyKc/D24spZIF2mB3XsfALQJJT/OBmP8YpeTb1rveFSBHzl8T7l0aqwkWNdGA=="],
- "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.51.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w=="],
+ "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.55.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PS6GFvmde/pc3fCA2Srt51glr8Lcxhpf6WIBFfLphndjRrD34NEcses4TSxQrEcxYo6qVywGfylM0ZhSCF2gGA=="],
- "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.51.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA=="],
+ "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.55.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-P6JcLJGs/q1UOvDLzN8otd9JsH4tsuuPDv+p7aHqHM3PrKmYdmUvkNj4K327PTd35AYcznOCN+l4ZOaq76QzSw=="],
- "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.51.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg=="],
+ "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.55.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gzkk4zE2zsE+WmRxFOiAZHpCpUNDFytEakqNXoNHW+PnYEOTPKDdW6nrzgSeTbGKVPXNAKQnRnMgrh7+n3Xueg=="],
- "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.51.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw=="],
+ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.55.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZFALNow2/og75gvYzNP7qe+rREQ5xunktwA+lgykoozHZ6hw9bqg4fn5j2UvG4gIn1FXqrZHkOAXuPf5+GOYTQ=="],
"@pa11y/html_codesniffer": ["@pa11y/html_codesniffer@2.6.0", "", {}, "sha512-BKA7qG8NyaIBdCBDep0hYuYoF/bEyWJprE6EEVJOPiwj80sSiIKDT8LUVd19qKhVqNZZD3QvJIdFZ35p+vAFPg=="],
@@ -544,6 +544,8 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+ "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
+
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
@@ -794,7 +796,7 @@
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
- "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
+ "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
@@ -1092,7 +1094,7 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
- "mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="],
+ "mermaid": ["mermaid@11.13.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw=="],
"metaviewport-parser": ["metaviewport-parser@0.3.0", "", {}, "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ=="],
@@ -1168,9 +1170,9 @@
"oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="],
- "oxfmt": ["oxfmt@0.36.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.36.0", "@oxfmt/binding-android-arm64": "0.36.0", "@oxfmt/binding-darwin-arm64": "0.36.0", "@oxfmt/binding-darwin-x64": "0.36.0", "@oxfmt/binding-freebsd-x64": "0.36.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.36.0", "@oxfmt/binding-linux-arm-musleabihf": "0.36.0", "@oxfmt/binding-linux-arm64-gnu": "0.36.0", "@oxfmt/binding-linux-arm64-musl": "0.36.0", "@oxfmt/binding-linux-ppc64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-musl": "0.36.0", "@oxfmt/binding-linux-s390x-gnu": "0.36.0", "@oxfmt/binding-linux-x64-gnu": "0.36.0", "@oxfmt/binding-linux-x64-musl": "0.36.0", "@oxfmt/binding-openharmony-arm64": "0.36.0", "@oxfmt/binding-win32-arm64-msvc": "0.36.0", "@oxfmt/binding-win32-ia32-msvc": "0.36.0", "@oxfmt/binding-win32-x64-msvc": "0.36.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-/ejJ+KoSW6J9bcNT9a9UtJSJNWhJ3yOLSBLbkoFHJs/8CZjmaZVZAJe4YgO1KMJlKpNQasrn/G9JQUEZI3p0EQ=="],
+ "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],
- "oxlint": ["oxlint@1.51.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.51.0", "@oxlint/binding-android-arm64": "1.51.0", "@oxlint/binding-darwin-arm64": "1.51.0", "@oxlint/binding-darwin-x64": "1.51.0", "@oxlint/binding-freebsd-x64": "1.51.0", "@oxlint/binding-linux-arm-gnueabihf": "1.51.0", "@oxlint/binding-linux-arm-musleabihf": "1.51.0", "@oxlint/binding-linux-arm64-gnu": "1.51.0", "@oxlint/binding-linux-arm64-musl": "1.51.0", "@oxlint/binding-linux-ppc64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-musl": "1.51.0", "@oxlint/binding-linux-s390x-gnu": "1.51.0", "@oxlint/binding-linux-x64-gnu": "1.51.0", "@oxlint/binding-linux-x64-musl": "1.51.0", "@oxlint/binding-openharmony-arm64": "1.51.0", "@oxlint/binding-win32-arm64-msvc": "1.51.0", "@oxlint/binding-win32-ia32-msvc": "1.51.0", "@oxlint/binding-win32-x64-msvc": "1.51.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ=="],
+ "oxlint": ["oxlint@1.55.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.55.0", "@oxlint/binding-android-arm64": "1.55.0", "@oxlint/binding-darwin-arm64": "1.55.0", "@oxlint/binding-darwin-x64": "1.55.0", "@oxlint/binding-freebsd-x64": "1.55.0", "@oxlint/binding-linux-arm-gnueabihf": "1.55.0", "@oxlint/binding-linux-arm-musleabihf": "1.55.0", "@oxlint/binding-linux-arm64-gnu": "1.55.0", "@oxlint/binding-linux-arm64-musl": "1.55.0", "@oxlint/binding-linux-ppc64-gnu": "1.55.0", "@oxlint/binding-linux-riscv64-gnu": "1.55.0", "@oxlint/binding-linux-riscv64-musl": "1.55.0", "@oxlint/binding-linux-s390x-gnu": "1.55.0", "@oxlint/binding-linux-x64-gnu": "1.55.0", "@oxlint/binding-linux-x64-musl": "1.55.0", "@oxlint/binding-openharmony-arm64": "1.55.0", "@oxlint/binding-win32-arm64-msvc": "1.55.0", "@oxlint/binding-win32-ia32-msvc": "1.55.0", "@oxlint/binding-win32-x64-msvc": "1.55.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-T+FjepiyWpaZMhekqRpH8Z3I4vNM610p6w+Vjfqgj5TZUxHXl7N8N5IPvmOU8U4XdTRxqtNNTh9Y4hLtr7yvFg=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
diff --git a/docs/.vitepress/theme/ComparisonTable.vue b/docs/.vitepress/theme/ComparisonTable.vue
index b4faef1..08b4fae 100644
--- a/docs/.vitepress/theme/ComparisonTable.vue
+++ b/docs/.vitepress/theme/ComparisonTable.vue
@@ -59,6 +59,13 @@ const ROWS: Row[] = [
gcs: true,
docLink: "/usage/interactive-mode",
},
+ {
+ feature: "Regex queries (/pattern/flags)",
+ desc: "Use full regular expressions in queries — top-level alternation (A|B|C) maps to GitHub OR, client-side filtering applies the real pattern. GitHub supports regex in the web UI only, not in the REST API or gh CLI.",
+ gh: false,
+ gcs: true,
+ docLink: "/usage/search-syntax",
+ },
{
feature: "Pagination (up to 1\u202f000 results)",
desc: "Both tools auto-paginate the GitHub search API \u2014 up to 1\u202f000 results per query.",
diff --git a/docs/.vitepress/theme/UseCaseTabs.vue b/docs/.vitepress/theme/UseCaseTabs.vue
index ac44f3a..4d3b274 100644
--- a/docs/.vitepress/theme/UseCaseTabs.vue
+++ b/docs/.vitepress/theme/UseCaseTabs.vue
@@ -50,6 +50,14 @@ const USE_CASES: UseCase[] = [
"Get a team-scoped view of every usage site before refactoring a shared hook or utility. Essential for onboarding or large-scale refactors.",
command: `github-code-search query "useFeatureFlag" --org my-org --group-by-team-prefix platform/`,
},
+ {
+ id: "semver",
+ label: "Semver / version audit",
+ headline: "Which repos are pinned to a vulnerable minor version?",
+ description:
+ "Use regex syntax to target a precise version range — something a plain keyword search cannot do. Find every repo still locked to axios 1.x, react 17.x, or any other outdated pin, then export the list to a migration issue.",
+ command: `github-code-search query '/"axios": "1\\./' --org my-org`,
+ },
];
const active = ref(0);
diff --git a/docs/architecture/components.md b/docs/architecture/components.md
index d8700bb..d23e84f 100644
--- a/docs/architecture/components.md
+++ b/docs/architecture/components.md
@@ -10,21 +10,25 @@ The three pure functions called by the CLI parser to transform raw API results
into a filtered, grouped, formatted output.
```mermaid
-%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Poppins, Aestetico, Arial, sans-serif", "primaryColor": "#66CCFF", "primaryTextColor": "#000000", "lineColor": "#0000CC", "tertiaryColor": "#FFCC33"}, "themeCSS": ".label,.nodeLabel,.cluster-label > span{font-family:Poppins,Arial,sans-serif;letter-spacing:.2px} .cluster-label > span{font-weight:600;font-size:13px} .edgePath .path{stroke-width:2px}"}}%%
+%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Poppins, Aestetico, Arial, sans-serif", "primaryColor": "#9933FF", "primaryTextColor": "#ffffff", "lineColor": "#0000CC", "tertiaryColor": "#FFCC33"}, "themeCSS": ".label,.nodeLabel,.cluster-label > span{font-family:Poppins,Arial,sans-serif;letter-spacing:.2px} .cluster-label > span{font-weight:600;font-size:13px} .edgePath .path{stroke-width:2px}"}}%%
C4Component
title Level 3a: CLI data pipeline
- UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="1")
+ UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="1")
Container(cli, "CLI parser", "github-code-search.ts", "Orchestrates filter,
group, output and
shell completions")
Container_Boundary(core, "Pure-function core — no I/O") {
+ Component(regexParser, "Query parser", "src/regex.ts", "isRegexQuery()
buildApiQuery()")
Component(aggregate, "Filter & aggregation", "src/aggregate.ts", "aggregate()
exclude repos & extracts")
Component(group, "Team grouping", "src/group.ts", "groupByTeamPrefix()
flattenTeamSections()")
Component(outputFn, "Output formatter", "src/output.ts", "buildOutput()
markdown or JSON")
Component(completions, "Shell completions", "src/completions.ts", "generateCompletion()
detectShell()
getCompletionFilePath()")
}
+ Rel(cli, regexParser, "Parse regex
query")
+ UpdateRelStyle(cli, regexParser, $offsetX="35", $offsetY="-17")
+
Rel(cli, aggregate, "Filter
CodeMatch[]")
UpdateRelStyle(cli, aggregate, $offsetX="0", $offsetY="-17")
@@ -37,11 +41,6 @@ C4Component
Rel(cli, completions, "Generate
script")
UpdateRelStyle(cli, completions, $offsetX="-90", $offsetY="-17")
- UpdateElementStyle(cli, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000")
- UpdateElementStyle(aggregate, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(group, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(outputFn, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(completions, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
```
## 3b — TUI render layer
@@ -52,7 +51,7 @@ The render-layer modules called by the TUI on every redraw. Most live in
provides shared pattern-matching helpers used by several render modules.
```mermaid
-%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Poppins, Aestetico, Arial, sans-serif", "primaryColor": "#66CCFF", "primaryTextColor": "#000000", "lineColor": "#0000CC", "tertiaryColor": "#FFCC33"}, "themeCSS": ".label,.nodeLabel,.cluster-label > span{font-family:Poppins,Arial,sans-serif;letter-spacing:.2px} .cluster-label > span{font-weight:600;font-size:13px} .edgePath .path{stroke-width:2px}"}}%%
+%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Poppins, Aestetico, Arial, sans-serif", "primaryColor": "#9933FF", "primaryTextColor": "#ffffff", "lineColor": "#0000CC", "tertiaryColor": "#FFCC33"}, "themeCSS": ".label,.nodeLabel,.cluster-label > span{font-family:Poppins,Arial,sans-serif;letter-spacing:.2px} .cluster-label > span{font-weight:600;font-size:13px} .edgePath .path{stroke-width:2px}"}}%%
C4Component
title Level 3b: TUI render layer
@@ -97,14 +96,6 @@ C4Component
Rel(selection, filterMatch, "Uses pattern
matchers")
UpdateRelStyle(selection, filterMatch, $offsetX="165", $offsetY="-25")
- UpdateElementStyle(tui, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000")
- UpdateElementStyle(rows, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(filterMatch, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(summary, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(filter, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(selection, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(highlight, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
- UpdateElementStyle(outputFn, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
```
## Component descriptions
diff --git a/docs/reference/cli-options.md b/docs/reference/cli-options.md
index ad110ac..9e7d08b 100644
--- a/docs/reference/cli-options.md
+++ b/docs/reference/cli-options.md
@@ -31,17 +31,18 @@ github-code-search completions [--shell ]
## Search options
-| Option | Type | Required | Default | Description |
-| ----------------------------------- | --------------------------------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `--org ` | string | ✅ | — | GitHub organization to search in. Automatically injected as `org:` in the query. |
-| `--exclude-repositories ` | string | ❌ | `""` | Comma-separated list of repositories to exclude. Short form (`repoA,repoB`) or full form (`org/repoA,org/repoB`) both accepted. |
-| `--exclude-extracts ` | string | ❌ | `""` | Comma-separated extract refs to exclude. Format: `repoName:path/to/file:index`. Short form (without org prefix) accepted. |
-| `--no-interactive` | boolean (flag) | ❌ | `true` (on) | Disable interactive mode. Interactive mode is **on** by default; pass this flag to disable it. Also triggered by `CI=true`. |
-| `--format ` | `markdown` \| `json` | ❌ | `markdown` | Output format. See [Output formats](/usage/output-formats). |
-| `--output-type ` | `repo-and-matches` \| `repo-only` | ❌ | `repo-and-matches` | Controls output detail level. `repo-only` lists repository names only, without individual extracts. |
-| `--include-archived` | boolean (flag) | ❌ | `false` | Include archived repositories in results (excluded by default). |
-| `--group-by-team-prefix ` | string | ❌ | `""` | Comma-separated team-name prefixes for grouping result repos by GitHub team (e.g. `squad-,chapter-`). Requires `read:org` scope. |
-| `--no-cache` | boolean (flag) | ❌ | `true` (on) | Bypass the 24 h team-list cache and re-fetch teams from GitHub. Cache is **on** by default; pass this flag to disable it. Only applies with `--group-by-team-prefix`. |
+| Option | Type | Required | Default | Description |
+| ----------------------------------- | --------------------------------- | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--org ` | string | ✅ | — | GitHub organization to search in. Automatically injected as `org:` in the query. |
+| `--exclude-repositories ` | string | ❌ | `""` | Comma-separated list of repositories to exclude. Short form (`repoA,repoB`) or full form (`org/repoA,org/repoB`) both accepted. |
+| `--exclude-extracts ` | string | ❌ | `""` | Comma-separated extract refs to exclude. Format: `repoName:path/to/file:index`. Short form (without org prefix) accepted. |
+| `--no-interactive` | boolean (flag) | ❌ | `true` (on) | Disable interactive mode. Interactive mode is **on** by default; pass this flag to disable it. Also triggered by `CI=true`. |
+| `--format ` | `markdown` \| `json` | ❌ | `markdown` | Output format. See [Output formats](/usage/output-formats). |
+| `--output-type ` | `repo-and-matches` \| `repo-only` | ❌ | `repo-and-matches` | Controls output detail level. `repo-only` lists repository names only, without individual extracts. |
+| `--include-archived` | boolean (flag) | ❌ | `false` | Include archived repositories in results (excluded by default). |
+| `--group-by-team-prefix ` | string | ❌ | `""` | Comma-separated team-name prefixes for grouping result repos by GitHub team (e.g. `squad-,chapter-`). Requires `read:org` scope. |
+| `--no-cache` | boolean (flag) | ❌ | `true` (on) | Bypass the 24 h team-list cache and re-fetch teams from GitHub. Cache is **on** by default; pass this flag to disable it. Only applies with `--group-by-team-prefix`. |
+| `--regex-hint ` | string | ❌ | — | Override the API search term used when the query is a regex (`/pattern/`). Useful when auto-extraction produces a term that is too broad or too narrow. See [Regex queries](/usage/search-syntax#regex-queries). |
## Global options
diff --git a/docs/usage/search-syntax.md b/docs/usage/search-syntax.md
index 37f2536..bb3b4bc 100644
--- a/docs/usage/search-syntax.md
+++ b/docs/usage/search-syntax.md
@@ -77,6 +77,53 @@ github-code-search "useFeatureFlag repo:fulll/billing-api repo:fulll/auth-servic
github-code-search "password= language:TypeScript NOT filename:test" --org fulll
```
+## Regex queries
+
+`github-code-search` supports regex syntax using the `/pattern/flags` notation, just like the GitHub web UI.
+
+Because the GitHub Code Search API does not natively support regex, the CLI automatically extracts a representative literal term from the regex to send to the API, then filters the returned results locally with the full pattern. In most cases this is fully transparent.
+
+```bash
+# Imports using the axios module (any quote style)
+github-code-search "/from.*['\"\`]axios/" --org fulll
+
+# Axios dependency in package.json (any semver prefix)
+github-code-search '/"axios": "[~^]?[0-9]"/ filename:package.json' --org fulll
+
+# Old library require() calls
+github-code-search "/require\\(['\"](old-lib)['\"]\\)/" --org fulll
+
+# Any of TODO, FIXME or HACK comments
+github-code-search "/TODO|FIXME|HACK/" --org fulll
+```
+
+::: tip Top-level alternation
+When the regex contains a **top-level `|`** (e.g. `TODO|FIXME|HACK`), the CLI sends
+an `A OR B OR C` query to the GitHub API so that **all branches are covered** — no results are missed.
+:::
+
+### When auto-extraction is not precise enough
+
+If the extracted term is very short (fewer than 3 characters), the CLI will exit with a warning and ask you to provide a manual hint:
+
+```text
+⚠ Regex mode — No meaningful search term could be extracted from the regex pattern. Use --regex-hint to specify the term to send to the GitHub API.
+```
+
+Use `--regex-hint` to override the API search term while still applying the full regex filter locally:
+
+```bash
+github-code-search '/"axios":\s*"[~^]?[0-9]/ filename:package.json' \
+ --org fulll \
+ --regex-hint '"axios"'
+```
+
+::: warning API coverage
+The GitHub Code Search API returns **at most 1,000 results** per query. The regex filter
+is applied to those results; results beyond the API cap can never be seen. Refine the
+query with qualifiers (`language:`, `path:`, `filename:`) to keep the result set small.
+:::
+
## API limits
The GitHub Code Search API returns at most **1,000 results** per query. If your query returns more, refine it with qualifiers (especially `language:` or `path:`) to stay below the limit.
diff --git a/github-code-search.ts b/github-code-search.ts
index ac26503..9f13f00 100644
--- a/github-code-search.ts
+++ b/github-code-search.ts
@@ -24,6 +24,7 @@ import { groupByTeamPrefix, flattenTeamSections } from "./src/group.ts";
import { checkForUpdate } from "./src/upgrade.ts";
import { runInteractive } from "./src/tui.ts";
import { generateCompletion, detectShell } from "./src/completions.ts";
+import { buildApiQuery, isRegexQuery } from "./src/regex.ts";
import type { OutputFormat, OutputType } from "./src/types.ts";
// Version + build metadata injected at compile time via --define (see build.ts).
@@ -179,6 +180,15 @@ function addSearchOptions(cmd: Command): Command {
.option(
"--no-cache",
"Bypass the 24 h team-list cache and re-fetch teams from GitHub (only applies with --group-by-team-prefix).",
+ )
+ .option(
+ "--regex-hint ",
+ [
+ "Override the search term sent to the GitHub API when using a regex query.",
+ "Useful when auto-extraction produces a term that is too broad or too narrow.",
+ 'Example: --regex-hint "axios" (for query /from.*[\'"]axios/)',
+ "Docs: https://fulll.github.io/github-code-search/usage/search-syntax#regex-queries",
+ ].join("\n"),
);
}
@@ -195,6 +205,7 @@ async function searchAction(
includeArchived: boolean;
groupByTeamPrefix: string;
cache: boolean;
+ regexHint?: string;
},
): Promise {
// ─── GitHub API token ───────────────────────────────────────────────────────
@@ -264,8 +275,37 @@ async function searchAction(
return activeCooldown;
};
- const rawMatches = await fetchAllResults(query, org, GITHUB_TOKEN!, onRateLimit);
- let groups = aggregate(rawMatches, excludedRepos, excludedExtractRefs, includeArchived);
+ // ─── Regex query detection ───────────────────────────────────────────────
+ let effectiveQuery = query;
+ let regexFilter: RegExp | undefined;
+ if (isRegexQuery(query)) {
+ const { apiQuery, regexFilter: rf, warn } = buildApiQuery(query);
+ if (rf === null) {
+ // Compile error — always fatal, even if --regex-hint is provided,
+ // because no local regex filter can be applied.
+ console.error(pc.yellow(`⚠ Regex mode — ${warn}`));
+ process.exit(1);
+ }
+ if (warn && !opts.regexHint) {
+ // warn already contains the --regex-hint guidance; print it as-is.
+ console.error(pc.yellow(`⚠ Regex mode — ${warn}`));
+ process.exit(1);
+ }
+ effectiveQuery = opts.regexHint ?? apiQuery;
+ regexFilter = rf ?? undefined;
+ process.stderr.write(
+ pc.dim(` ℹ Regex mode — GitHub query: "${effectiveQuery}", local filter: ${query}\n`),
+ );
+ }
+
+ const rawMatches = await fetchAllResults(effectiveQuery, org, GITHUB_TOKEN!, onRateLimit);
+ let groups = aggregate(
+ rawMatches,
+ excludedRepos,
+ excludedExtractRefs,
+ includeArchived,
+ regexFilter,
+ );
// ─── Team-prefix grouping ─────────────────────────────────────────────────
if (opts.groupByTeamPrefix) {
@@ -288,6 +328,7 @@ async function searchAction(
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
includeArchived,
groupByTeamPrefix: opts.groupByTeamPrefix,
+ regexHint: opts.regexHint,
}),
);
// Check for a newer version and notify on stderr so it never pollutes piped output.
@@ -338,6 +379,7 @@ async function searchAction(
outputType,
includeArchived,
opts.groupByTeamPrefix,
+ opts.regexHint ?? "",
);
}
}
diff --git a/package.json b/package.json
index 712ab76..ee4a892 100644
--- a/package.json
+++ b/package.json
@@ -61,9 +61,9 @@
"@resvg/resvg-js": "^2.6.2",
"bun-types": "^1.3.10",
"knip": "^5.86.0",
- "mermaid": "^11.0.0",
- "oxfmt": "^0.36.0",
- "oxlint": "^1.51.0",
+ "mermaid": "^11.13.0",
+ "oxfmt": "^0.40.0",
+ "oxlint": "^1.55.0",
"pa11y-ci": "^4.1.0",
"sharp": "^0.34.5",
"vitepress": "^1.6.3",
diff --git a/src/aggregate.test.ts b/src/aggregate.test.ts
index a72aaf4..40081f1 100644
--- a/src/aggregate.test.ts
+++ b/src/aggregate.test.ts
@@ -238,4 +238,77 @@ describe("aggregate — regexFilter", () => {
aggregate(matches, new Set(), new Set(), false, regex);
expect(regex.lastIndex).toBe(savedIndex);
});
+
+ it("recomputes segments to point at the actual regex match (not the API literal)", () => {
+ // Simulate: regex /axios": "1\.12/, API literal "axios", API gives segment
+ // at [9,14] (pointing at "axios" only). After aggregation the segment must
+ // cover the full regex match.
+ //
+ // Fragment offsets: d e p s : \n " a x i o s " : " 1 . 1 2 . 0 "
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
+ // regex match: 'axios": "1.12' starts at offset 9, ends at 22
+ const fragment = 'deps:\n "axios": "1.12.0"';
+ const matches: CodeMatch[] = [
+ {
+ path: "package.json",
+ repoFullName: "myorg/repoA",
+ htmlUrl: "https://github.com/myorg/repoA/blob/main/package.json",
+ archived: false,
+ textMatches: [
+ {
+ fragment,
+ // API-provided segment: only covers "axios" at offset 9..14
+ matches: [{ text: "axios", indices: [9, 14], line: 2, col: 4 }],
+ },
+ ],
+ },
+ ];
+
+ const groups = aggregate(matches, new Set(), new Set(), false, /axios": "1\.12/);
+ expect(groups).toHaveLength(1);
+
+ const seg = groups[0].matches[0].textMatches[0].matches[0];
+ // The regex matches 'axios": "1.12' starting at offset 9 in the fragment
+ expect(seg.text).toBe('axios": "1.12');
+ expect(seg.indices[0]).toBe(9);
+ expect(seg.indices[1]).toBe(22);
+ expect(seg.line).toBe(2); // second line of the fragment
+ expect(seg.col).toBe(4); // after the leading ' "'
+ });
+
+ it("recomputes correct line and col for multiline fragments", () => {
+ const fragment = "line1\nline2\nfoo bar\nline4";
+ // 01234 5 67890 1 234567 8 9012
+ // ^ "foo" at offset 12 = line 3, col 1
+ const matches: CodeMatch[] = [makeMatchWithFragments("myorg/repoA", "src/a.ts", [fragment])];
+
+ const groups = aggregate(matches, new Set(), new Set(), false, /foo/);
+ const seg = groups[0].matches[0].textMatches[0].matches[0];
+ expect(seg.text).toBe("foo");
+ expect(seg.indices).toEqual([12, 15]);
+ expect(seg.line).toBe(3);
+ expect(seg.col).toBe(1);
+ });
+
+ it("filters out textMatches where the regex does not match, keeps those where it does", () => {
+ // One file with two textMatches: only the second one matches the regex.
+ const matches: CodeMatch[] = [
+ {
+ path: "src/a.ts",
+ repoFullName: "myorg/repoA",
+ htmlUrl: "",
+ archived: false,
+ textMatches: [
+ { fragment: "unrelated code", matches: [] },
+ { fragment: "import axios from 'axios'", matches: [] },
+ ],
+ },
+ ];
+
+ const groups = aggregate(matches, new Set(), new Set(), false, /axios/);
+ expect(groups).toHaveLength(1);
+ // Only the matching textMatch is kept
+ expect(groups[0].matches[0].textMatches).toHaveLength(1);
+ expect(groups[0].matches[0].textMatches[0].fragment).toBe("import axios from 'axios'");
+ });
});
diff --git a/src/aggregate.ts b/src/aggregate.ts
index bc94f14..f1fc662 100644
--- a/src/aggregate.ts
+++ b/src/aggregate.ts
@@ -1,4 +1,4 @@
-import type { CodeMatch, RepoGroup } from "./types.ts";
+import type { CodeMatch, RepoGroup, TextMatch, TextMatchSegment } from "./types.ts";
// ─── Normalisation helpers ────────────────────────────────────────────────────
@@ -27,6 +27,56 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe
return `${repoFullName}:${path}:${matchIndex}`;
}
+// ─── Regex segment helper ────────────────────────────────────────────────────
+
+/**
+ * Run `regex` against `fragment` and return `TextMatchSegment[]` for every
+ * match — replacing the API-provided segments (which point at the literal
+ * search term) with the actual regex match positions.
+ *
+ * `fragmentStartLine` is the 1-based absolute file line where the fragment
+ * starts (derived from the existing API segments or falling back to 1). It is
+ * used to produce absolute `line` values that match those stored by the API
+ * path so that `output.ts` generates correct `#L{line}` GitHub anchors.
+ */
+function recomputeSegments(
+ fragment: string,
+ re: RegExp,
+ fragmentStartLine: number,
+): TextMatchSegment[] {
+ // Reset lastIndex so exec() always searches from the start of the fragment.
+ // The caller is responsible for providing a global (g) regex constructed once
+ // per aggregate() call — not recompiled per fragment.
+ re.lastIndex = 0;
+ // Precompute newline positions once — O(n) — so per-match line/col lookup
+ // is O(log n) via binary search instead of O(n) per match (O(n²) overall).
+ const newlines: number[] = [];
+ for (let i = 0; i < fragment.length; i++) {
+ if (fragment[i] === "\n") newlines.push(i);
+ }
+ const segments: TextMatchSegment[] = [];
+ let m: RegExpExecArray | null;
+ while ((m = re.exec(fragment)) !== null) {
+ const start = m.index;
+ const end = start + m[0].length;
+ // Binary-search for the number of newlines before `start`.
+ let lo = 0;
+ let hi = newlines.length;
+ while (lo < hi) {
+ const mid = (lo + hi) >> 1;
+ if (newlines[mid] < start) lo = mid + 1;
+ else hi = mid;
+ }
+ // `lo` = number of fragment-local lines before `start` (0-based offset).
+ // Add `fragmentStartLine - 1` to make it absolute.
+ const line = fragmentStartLine + lo;
+ const col = (lo === 0 ? start : start - newlines[lo - 1] - 1) + 1; // 1-based
+ segments.push({ text: m[0], indices: [start, end], line, col });
+ if (m[0].length === 0) re.lastIndex++; // guard against zero-width matches
+ }
+ return segments;
+}
+
// ─── Aggregation ─────────────────────────────────────────────────────────────
export function aggregate(
@@ -36,29 +86,50 @@ export function aggregate(
includeArchived = false,
regexFilter?: RegExp | null,
): RepoGroup[] {
+ // Compile the global regex once per aggregate() call rather than once per
+ // fragment inside recomputeSegments — avoids repeated RegExp construction
+ // on large result sets. Strip g and y first to prevent double-flag and
+ // sticky-mode issues; recomputeSegments resets lastIndex per call.
+ const globalRe = regexFilter
+ ? new RegExp(regexFilter.source, regexFilter.flags.replace(/[gy]/g, "") + "g")
+ : null;
const map = new Map();
for (const m of matches) {
if (excludedRepos.has(m.repoFullName)) continue;
if (!includeArchived && m.archived) continue;
- // Fix: when a regex filter is active, only keep matches where at least one
- // text_match fragment satisfies the pattern — see issue #111
- if (regexFilter != null) {
+ // Fix: when a regex filter is active, replace each TextMatch's API-provided
+ // segments (which point at the literal search term) with segments derived
+ // from the actual regex match positions — see issue #111 / fix highlight bug
+ let matchToAdd: CodeMatch = m;
+ if (globalRe != null) {
// Preserve the caller's lastIndex: aggregate() must not have observable
// side-effects on the passed-in RegExp instance.
- const savedLastIndex = regexFilter.lastIndex;
- const hasMatch = m.textMatches.some((tm) => {
- // Fix: reset lastIndex before each call — a global/sticky regex is
- // stateful and would produce false negatives on subsequent fragments.
- regexFilter.lastIndex = 0;
- return regexFilter.test(tm.fragment);
- });
+ const savedLastIndex = regexFilter!.lastIndex;
+ const updatedTextMatches: TextMatch[] = m.textMatches
+ .map((tm) => {
+ // Derive the absolute start line of this fragment from the first API
+ // segment. If no API segment is available, fall back to 1 so that
+ // recomputeSegments emits fragment-relative lines (which equal
+ // absolute lines when the fragment starts at line 1).
+ let fragmentStartLine = 1;
+ const firstApiSeg = tm.matches[0];
+ if (firstApiSeg) {
+ const before = tm.fragment.slice(0, firstApiSeg.indices[0]);
+ const fragLine = (before.match(/\n/g)?.length ?? 0) + 1;
+ fragmentStartLine = firstApiSeg.line - fragLine + 1;
+ }
+ const segs = recomputeSegments(tm.fragment, globalRe, fragmentStartLine);
+ return segs.length > 0 ? { fragment: tm.fragment, matches: segs } : null;
+ })
+ .filter((tm): tm is TextMatch => tm !== null);
// Restore the caller's original lastIndex (rather than hard-coding 0),
// so aggregate() doesn't have observable side effects on its inputs.
- regexFilter.lastIndex = savedLastIndex;
- if (!hasMatch) continue;
+ regexFilter!.lastIndex = savedLastIndex;
+ if (updatedTextMatches.length === 0) continue;
+ matchToAdd = { ...m, textMatches: updatedTextMatches };
}
const list = map.get(m.repoFullName) ?? [];
- list.push(m);
+ list.push(matchToAdd);
map.set(m.repoFullName, list);
}
diff --git a/src/completions.test.ts b/src/completions.test.ts
index 48c3909..4ca14d6 100644
--- a/src/completions.test.ts
+++ b/src/completions.test.ts
@@ -28,6 +28,7 @@ describe("generateCompletion", () => {
expect(script).toContain("--format");
expect(script).toContain("--output-type");
expect(script).toContain("--no-interactive");
+ expect(script).toContain("--regex-hint");
});
it("contains format values (markdown, json)", () => {
@@ -71,6 +72,7 @@ describe("generateCompletion", () => {
expect(script).toContain("--org");
expect(script).toContain("--format");
expect(script).toContain("--output-type");
+ expect(script).toContain("--regex-hint");
});
it("contains a 'compdef' directive (zsh-style)", () => {
@@ -102,6 +104,7 @@ describe("generateCompletion", () => {
expect(script).toContain("org");
expect(script).toContain("format");
expect(script).toContain("output-type");
+ expect(script).toContain("regex-hint");
});
it("uses fish 'complete -c' syntax", () => {
diff --git a/src/completions.ts b/src/completions.ts
index ce02d36..5b03447 100644
--- a/src/completions.ts
+++ b/src/completions.ts
@@ -63,6 +63,12 @@ const OPTIONS = [
takesArg: false,
values: [],
},
+ {
+ flag: "regex-hint",
+ description: "Override the API search term for regex queries",
+ takesArg: true,
+ values: [],
+ },
] as const;
// ─── Bash completion script ───────────────────────────────────────────────────
diff --git a/src/output.test.ts b/src/output.test.ts
index ee24f4d..4e1e3a6 100644
--- a/src/output.test.ts
+++ b/src/output.test.ts
@@ -182,6 +182,22 @@ describe("buildReplayCommand", () => {
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
expect(cmd).not.toContain("--group-by-team-prefix");
});
+
+ it("includes --regex-hint when regexHint is set", () => {
+ const groups = [makeGroup("myorg/repoA", ["a.ts"])];
+ const opts: ReplayOptions = { regexHint: '"axios"' };
+ const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
+ expect(cmd).toContain("--regex-hint");
+ expect(cmd).toContain("axios");
+ // Must use single-quote shell escaping, not JSON.stringify
+ expect(cmd).toContain(`--regex-hint '"`);
+ });
+
+ it("does not include --regex-hint when regexHint is not set (default)", () => {
+ const groups = [makeGroup("myorg/repoA", ["a.ts"])];
+ const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set());
+ expect(cmd).not.toContain("--regex-hint");
+ });
});
describe("buildReplayDetails", () => {
diff --git a/src/output.ts b/src/output.ts
index 11b24df..45e73ef 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -21,11 +21,21 @@ export function shortExtractRef(full: string, org: string): string {
// ─── Replay options ───────────────────────────────────────────────────────────
/** Options that affect the generated replay command. */
+/** Wraps `s` in POSIX single quotes, escaping any embedded single quotes as '\''.
+ * Produces output that is safe to paste into bash / zsh regardless of the
+ * content (no `$()`, backtick, or glob expansion). */
+function shellQuote(s: string): string {
+ return "'" + s.replace(/'/g, "'\\''") + "'";
+}
+
export interface ReplayOptions {
format?: OutputFormat;
outputType?: OutputType;
includeArchived?: boolean;
groupByTeamPrefix?: string;
+ /** When set, appends `--regex-hint ` to the replay command so the
+ * result set from a regex query can be reproduced exactly. */
+ regexHint?: string;
}
// ─── Replay command ───────────────────────────────────────────────────────────
@@ -39,10 +49,8 @@ export function buildReplayCommand(
// Fix: forward all input options so the replay command is fully reproducible — see issue #11
options: ReplayOptions = {},
): string {
- const { format, outputType, includeArchived, groupByTeamPrefix } = options;
- const parts: string[] = [
- `github-code-search ${JSON.stringify(query)} --org ${org} --no-interactive`,
- ];
+ const { format, outputType, includeArchived, groupByTeamPrefix, regexHint } = options;
+ const parts: string[] = [`github-code-search ${shellQuote(query)} --org ${org} --no-interactive`];
const excludedReposList: string[] = [...excludedRepos].map((r) => shortRepo(r, org));
for (const group of groups) {
@@ -85,6 +93,9 @@ export function buildReplayCommand(
if (groupByTeamPrefix) {
parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`);
}
+ if (regexHint) {
+ parts.push(`--regex-hint ${shellQuote(regexHint)}`);
+ }
return `# Replay:\n${parts.join(" \\\n ")}`;
}
@@ -253,7 +264,7 @@ export function buildOutput(
excludedExtractRefs: Set,
format: OutputFormat,
outputType: OutputType = "repo-and-matches",
- extraOptions: Pick = {},
+ extraOptions: Pick = {},
): string {
const options: ReplayOptions = { format, outputType, ...extraOptions };
if (format === "json") {
diff --git a/src/tui.ts b/src/tui.ts
index f514f67..bb3209c 100644
--- a/src/tui.ts
+++ b/src/tui.ts
@@ -109,6 +109,7 @@ export async function runInteractive(
outputType: OutputType = "repo-and-matches",
includeArchived = false,
groupByTeamPrefix = "",
+ regexHint = "",
): Promise {
if (groups.length === 0) {
console.log(pc.yellow("No results found."));
@@ -371,6 +372,7 @@ export async function runInteractive(
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
includeArchived,
groupByTeamPrefix,
+ regexHint: regexHint || undefined,
}),
);
process.exit(0);
diff --git a/src/types.ts b/src/types.ts
index 4a90939..8445103 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,7 +3,8 @@
export interface TextMatchSegment {
text: string;
indices: [number, number];
- /** 1-based line within the fragment (fragment-relative, not absolute file line). */
+ /** 1-based absolute file line (computed by api.ts / recomputeSegments in aggregate.ts).
+ * Used for `#L{line}` GitHub anchors in output.ts. */
line: number;
/** 1-based column within that line. */
col: number;