From 490cab36c0681ed0806260675dc331ef838de343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Thu, 12 Mar 2026 15:02:50 +0100 Subject: [PATCH 1/9] Fix regex highlight: recompute segments from regex match positions When a regex query is active, the GitHub API returns TextMatchSegments pointing at the literal search term (e.g. 'axios'), not at the full regex match ('axios": "1.12'). highlightFragment() was therefore bold-yellowing only the literal fragment. Fix: in aggregate(), when regexFilter is set, run the regex against each fragment and replace API-provided segments with TextMatchSegments derived from the actual regex match positions (correct 1-based line/col). TextMatch entries where the regex finds no match are filtered out. Add 3 new test cases: exact segment positions, multiline line/col, and partial textMatches filtering. Closes part of #112 --- src/aggregate.test.ts | 73 +++++++++++++++++++++++++++++++++++++++++++ src/aggregate.ts | 53 ++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/aggregate.test.ts b/src/aggregate.test.ts index a72aaf4..18a6789 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 [8,13] (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..856b5cd 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,34 @@ 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. + * + * Line (1-based) and col (1-based) are computed from the fragment text so + * that `highlightFragment` can map them to the correct terminal lines. + */ +function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[] { + // Force global flag so exec() advances; strip it first to avoid double-g + const re = new RegExp(regex.source, regex.flags.replace("g", "") + "g"); + const segments: TextMatchSegment[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(fragment)) !== null) { + const start = m.index; + const end = start + m[0].length; + const before = fragment.slice(0, start); + const nlIdx = before.lastIndexOf("\n"); + const line = (before.match(/\n/g)?.length ?? 0) + 1; + const col = (nlIdx === -1 ? start : start - nlIdx - 1) + 1; + 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( @@ -40,25 +68,28 @@ export function aggregate( 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 + // 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 (regexFilter != 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 updatedTextMatches: TextMatch[] = m.textMatches + .map((tm) => { + const segs = recomputeSegments(tm.fragment, regexFilter); + 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; + 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); } From 2f4ba11fa6828b2322c44f55cd6729e506fb2f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Thu, 12 Mar 2026 15:03:04 +0100 Subject: [PATCH 2/9] Wire regex support into CLI + docs, homepage and comparison table - Parse /pattern/flags in query subcommand via isRegexQuery() / buildApiQuery() - Pass regexFilter to aggregate(); display UX warning when query is broadened - Add --regex-hint escape hatch for edge cases - docs/usage/search-syntax.md: new Regex queries section with 4 examples - docs/reference/cli-options.md: document --regex-hint flag - docs/architecture/components.md: add src/regex.ts to C4 Level 3a diagram - docs/.vitepress/theme/ComparisonTable.vue: add Regex queries row (web UI only note) - docs/.vitepress/theme/UseCaseTabs.vue: add Semver / version audit use case - AGENTS.md: add src/regex.ts entry + symptom table row - README.md: add Regex search use case Closes #112 --- AGENTS.md | 3 ++ README.md | 8 ++++ docs/.vitepress/theme/ComparisonTable.vue | 7 ++++ docs/.vitepress/theme/UseCaseTabs.vue | 8 ++++ docs/architecture/components.md | 7 +++- docs/reference/cli-options.md | 23 +++++------ docs/usage/search-syntax.md | 48 +++++++++++++++++++++++ github-code-search.ts | 39 +++++++++++++++++- 8 files changed, 129 insertions(+), 14 deletions(-) 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/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..fc6d46a 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..d50ce76 100644 --- a/docs/architecture/components.md +++ b/docs/architecture/components.md @@ -14,17 +14,21 @@ into a filtered, grouped, formatted output. 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") @@ -38,6 +42,7 @@ C4Component UpdateRelStyle(cli, completions, $offsetX="-90", $offsetY="-17") UpdateElementStyle(cli, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000") + UpdateElementStyle(regexParser, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") UpdateElementStyle(aggregate, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") UpdateElementStyle(group, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") UpdateElementStyle(outputFn, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") 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..98ff3b8 100644 --- a/docs/usage/search-syntax.md +++ b/docs/usage/search-syntax.md @@ -77,6 +77,54 @@ 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: + +``` +⚠ Regex mode — could not extract a term longer than 2 chars from /[~^]?[0-9]/ + Provide a manual hint with --regex-hint . +``` + +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..727ca4f 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,32 @@ 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 (warn && !opts.regexHint) { + console.error( + pc.yellow(`⚠ Regex mode — ${warn}\n Provide a manual hint with --regex-hint .`), + ); + 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) { From ae8fa109b500a0ffaedf4cbd723aa44800dbe6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 11:37:15 +0100 Subject: [PATCH 3/9] Fix recomputeSegments: strip y flag, precompute newline offsets, fix docs code block lang --- docs/usage/search-syntax.md | 2 +- src/aggregate.ts | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/usage/search-syntax.md b/docs/usage/search-syntax.md index 98ff3b8..f0ec7ee 100644 --- a/docs/usage/search-syntax.md +++ b/docs/usage/search-syntax.md @@ -106,7 +106,7 @@ an `A OR B OR C` query to the GitHub API so that **all branches are covered** 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 — could not extract a term longer than 2 chars from /[~^]?[0-9]/ Provide a manual hint with --regex-hint . ``` diff --git a/src/aggregate.ts b/src/aggregate.ts index 856b5cd..f813451 100644 --- a/src/aggregate.ts +++ b/src/aggregate.ts @@ -38,17 +38,30 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe * that `highlightFragment` can map them to the correct terminal lines. */ function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[] { - // Force global flag so exec() advances; strip it first to avoid double-g - const re = new RegExp(regex.source, regex.flags.replace("g", "") + "g"); + // Force global flag so exec() advances; strip g and y first to avoid double-g + // and to prevent sticky mode from anchoring every match at lastIndex. + const re = new RegExp(regex.source, regex.flags.replace(/[gy]/g, "") + "g"); + // 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; - const before = fragment.slice(0, start); - const nlIdx = before.lastIndexOf("\n"); - const line = (before.match(/\n/g)?.length ?? 0) + 1; - const col = (nlIdx === -1 ? start : start - nlIdx - 1) + 1; + // 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; + } + const line = lo + 1; // 1-based + 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 } From c6fcdba73feb386b656fa8edb397f2d26edb0773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 18:43:39 +0100 Subject: [PATCH 4/9] Address PR #114 review: absolute line numbers, invalid-regex fatal error, completions --regex-hint, fix comment --- github-code-search.ts | 6 ++++++ src/aggregate.test.ts | 2 +- src/aggregate.ts | 29 ++++++++++++++++++++++++----- src/completions.test.ts | 3 +++ src/completions.ts | 6 ++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/github-code-search.ts b/github-code-search.ts index 727ca4f..97cdc0f 100644 --- a/github-code-search.ts +++ b/github-code-search.ts @@ -280,6 +280,12 @@ async function searchAction( 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) { console.error( pc.yellow(`⚠ Regex mode — ${warn}\n Provide a manual hint with --regex-hint .`), diff --git a/src/aggregate.test.ts b/src/aggregate.test.ts index 18a6789..40081f1 100644 --- a/src/aggregate.test.ts +++ b/src/aggregate.test.ts @@ -241,7 +241,7 @@ describe("aggregate — regexFilter", () => { 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 [8,13] (pointing at "axios" only). After aggregation the segment must + // 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 " diff --git a/src/aggregate.ts b/src/aggregate.ts index f813451..9fd707a 100644 --- a/src/aggregate.ts +++ b/src/aggregate.ts @@ -34,10 +34,16 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe * match — replacing the API-provided segments (which point at the literal * search term) with the actual regex match positions. * - * Line (1-based) and col (1-based) are computed from the fragment text so - * that `highlightFragment` can map them to the correct terminal lines. + * `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, regex: RegExp): TextMatchSegment[] { +function recomputeSegments( + fragment: string, + regex: RegExp, + fragmentStartLine: number, +): TextMatchSegment[] { // Force global flag so exec() advances; strip g and y first to avoid double-g // and to prevent sticky mode from anchoring every match at lastIndex. const re = new RegExp(regex.source, regex.flags.replace(/[gy]/g, "") + "g"); @@ -60,7 +66,9 @@ function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[] if (newlines[mid] < start) lo = mid + 1; else hi = mid; } - const line = lo + 1; // 1-based + // `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 @@ -91,7 +99,18 @@ export function aggregate( const savedLastIndex = regexFilter.lastIndex; const updatedTextMatches: TextMatch[] = m.textMatches .map((tm) => { - const segs = recomputeSegments(tm.fragment, regexFilter); + // 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, regexFilter, fragmentStartLine); return segs.length > 0 ? { fragment: tm.fragment, matches: segs } : null; }) .filter((tm): tm is TextMatch => tm !== null); 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 ─────────────────────────────────────────────────── From 67fad0baf07d91b6a712ee7ff98f4637ff88ad2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 19:04:15 +0100 Subject: [PATCH 5/9] Plumb --regex-hint into replay command; remove duplicate warn guidance --- github-code-search.ts | 6 +++--- src/output.test.ts | 14 ++++++++++++++ src/output.ts | 10 ++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/github-code-search.ts b/github-code-search.ts index 97cdc0f..93dfb1a 100644 --- a/github-code-search.ts +++ b/github-code-search.ts @@ -287,9 +287,8 @@ async function searchAction( process.exit(1); } if (warn && !opts.regexHint) { - console.error( - pc.yellow(`⚠ Regex mode — ${warn}\n Provide a manual hint with --regex-hint .`), - ); + // 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; @@ -329,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. diff --git a/src/output.test.ts b/src/output.test.ts index ee24f4d..ca40455 100644 --- a/src/output.test.ts +++ b/src/output.test.ts @@ -182,6 +182,20 @@ 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"); + }); + + 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..bb3a800 100644 --- a/src/output.ts +++ b/src/output.ts @@ -26,6 +26,9 @@ export interface ReplayOptions { 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,7 +42,7 @@ 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 { format, outputType, includeArchived, groupByTeamPrefix, regexHint } = options; const parts: string[] = [ `github-code-search ${JSON.stringify(query)} --org ${org} --no-interactive`, ]; @@ -85,6 +88,9 @@ export function buildReplayCommand( if (groupByTeamPrefix) { parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`); } + if (regexHint) { + parts.push(`--regex-hint ${JSON.stringify(regexHint)}`); + } return `# Replay:\n${parts.join(" \\\n ")}`; } @@ -253,7 +259,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") { From ba9c31320399d2e8f9aacf5dbc82ccd153e3d627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 19:21:39 +0100 Subject: [PATCH 6/9] Thread regexHint through interactive path and document Mermaid palette --- docs/architecture/components.md | 6 ++++++ github-code-search.ts | 1 + src/tui.ts | 2 ++ 3 files changed, 9 insertions(+) diff --git a/docs/architecture/components.md b/docs/architecture/components.md index d50ce76..7a1b685 100644 --- a/docs/architecture/components.md +++ b/docs/architecture/components.md @@ -41,6 +41,9 @@ C4Component Rel(cli, completions, "Generate
script") UpdateRelStyle(cli, completions, $offsetX="-90", $offsetY="-17") + %% Colour palette — consistent with containers.md. + %% Mermaid C4 UpdateElementStyle only accepts literal hex values (CSS variables are not supported). + %% #FFCC33 = CLI / orchestration layer #9933FF = pure-function core #0000CC = border/line UpdateElementStyle(cli, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000") UpdateElementStyle(regexParser, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") UpdateElementStyle(aggregate, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") @@ -102,6 +105,9 @@ C4Component Rel(selection, filterMatch, "Uses pattern
matchers") UpdateRelStyle(selection, filterMatch, $offsetX="165", $offsetY="-25") + %% Colour palette — consistent with containers.md. + %% Mermaid C4 UpdateElementStyle only accepts literal hex values (CSS variables are not supported). + %% #FFCC33 = TUI / orchestration layer #9933FF = pure-function core #0000CC = border/line UpdateElementStyle(tui, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000") UpdateElementStyle(rows, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") UpdateElementStyle(filterMatch, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") diff --git a/github-code-search.ts b/github-code-search.ts index 93dfb1a..a435920 100644 --- a/github-code-search.ts +++ b/github-code-search.ts @@ -379,6 +379,7 @@ async function searchAction( outputType, includeArchived, opts.groupByTeamPrefix, + opts.regexHint, ); } } 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); From 19b6b6c26708cc9b5ae0d16f9675c725332f6cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 19:31:23 +0100 Subject: [PATCH 7/9] deps(upgrade): mermaid, oxlint, oxfmt --- bun.lock | 94 +++++++++++++++++++++++++++------------------------- package.json | 6 ++-- 2 files changed, 51 insertions(+), 49 deletions(-) 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/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", From ae52a6c55bebe99da89fa7c6dcb025dda4a13dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 19:37:41 +0100 Subject: [PATCH 8/9] Fix type mismatch, docs examples, and hoist regex compilation in aggregate() --- docs/.vitepress/theme/UseCaseTabs.vue | 2 +- docs/usage/search-syntax.md | 5 ++--- github-code-search.ts | 2 +- src/aggregate.ts | 24 ++++++++++++++++-------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/.vitepress/theme/UseCaseTabs.vue b/docs/.vitepress/theme/UseCaseTabs.vue index fc6d46a..4d3b274 100644 --- a/docs/.vitepress/theme/UseCaseTabs.vue +++ b/docs/.vitepress/theme/UseCaseTabs.vue @@ -56,7 +56,7 @@ const USE_CASES: UseCase[] = [ 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`, + command: `github-code-search query '/"axios": "1\\./' --org my-org`, }, ]; diff --git a/docs/usage/search-syntax.md b/docs/usage/search-syntax.md index f0ec7ee..bb3b4bc 100644 --- a/docs/usage/search-syntax.md +++ b/docs/usage/search-syntax.md @@ -88,7 +88,7 @@ Because the GitHub Code Search API does not natively support regex, the CLI auto 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 +github-code-search '/"axios": "[~^]?[0-9]"/ filename:package.json' --org fulll # Old library require() calls github-code-search "/require\\(['\"](old-lib)['\"]\\)/" --org fulll @@ -107,8 +107,7 @@ an `A OR B OR C` query to the GitHub API so that **all branches are covered** 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 — could not extract a term longer than 2 chars from /[~^]?[0-9]/ - Provide a manual hint with --regex-hint . +⚠ 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: diff --git a/github-code-search.ts b/github-code-search.ts index a435920..9f13f00 100644 --- a/github-code-search.ts +++ b/github-code-search.ts @@ -379,7 +379,7 @@ async function searchAction( outputType, includeArchived, opts.groupByTeamPrefix, - opts.regexHint, + opts.regexHint ?? "", ); } } diff --git a/src/aggregate.ts b/src/aggregate.ts index 9fd707a..f1fc662 100644 --- a/src/aggregate.ts +++ b/src/aggregate.ts @@ -41,12 +41,13 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe */ function recomputeSegments( fragment: string, - regex: RegExp, + re: RegExp, fragmentStartLine: number, ): TextMatchSegment[] { - // Force global flag so exec() advances; strip g and y first to avoid double-g - // and to prevent sticky mode from anchoring every match at lastIndex. - const re = new RegExp(regex.source, regex.flags.replace(/[gy]/g, "") + "g"); + // 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[] = []; @@ -85,6 +86,13 @@ 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; @@ -93,10 +101,10 @@ export function aggregate( // 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 (regexFilter != null) { + 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 savedLastIndex = regexFilter!.lastIndex; const updatedTextMatches: TextMatch[] = m.textMatches .map((tm) => { // Derive the absolute start line of this fragment from the first API @@ -110,13 +118,13 @@ export function aggregate( const fragLine = (before.match(/\n/g)?.length ?? 0) + 1; fragmentStartLine = firstApiSeg.line - fragLine + 1; } - const segs = recomputeSegments(tm.fragment, regexFilter, fragmentStartLine); + 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; + regexFilter!.lastIndex = savedLastIndex; if (updatedTextMatches.length === 0) continue; matchToAdd = { ...m, textMatches: updatedTextMatches }; } From 1df768af21b34ed788ebef7fdd450a79c61aa6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Fri, 13 Mar 2026 19:57:49 +0100 Subject: [PATCH 9/9] Correct TextMatchSegment.line docs, add shellQuote helper, remove UpdateElementStyle hex --- docs/architecture/components.md | 24 ++---------------------- src/output.test.ts | 2 ++ src/output.ts | 13 +++++++++---- src/types.ts | 3 ++- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/docs/architecture/components.md b/docs/architecture/components.md index 7a1b685..d23e84f 100644 --- a/docs/architecture/components.md +++ b/docs/architecture/components.md @@ -10,7 +10,7 @@ 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 @@ -41,15 +41,6 @@ C4Component Rel(cli, completions, "Generate
script") UpdateRelStyle(cli, completions, $offsetX="-90", $offsetY="-17") - %% Colour palette — consistent with containers.md. - %% Mermaid C4 UpdateElementStyle only accepts literal hex values (CSS variables are not supported). - %% #FFCC33 = CLI / orchestration layer #9933FF = pure-function core #0000CC = border/line - UpdateElementStyle(cli, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000") - UpdateElementStyle(regexParser, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff") - 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 @@ -60,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 @@ -105,17 +96,6 @@ C4Component Rel(selection, filterMatch, "Uses pattern
matchers") UpdateRelStyle(selection, filterMatch, $offsetX="165", $offsetY="-25") - %% Colour palette — consistent with containers.md. - %% Mermaid C4 UpdateElementStyle only accepts literal hex values (CSS variables are not supported). - %% #FFCC33 = TUI / orchestration layer #9933FF = pure-function core #0000CC = border/line - 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/src/output.test.ts b/src/output.test.ts index ca40455..4e1e3a6 100644 --- a/src/output.test.ts +++ b/src/output.test.ts @@ -189,6 +189,8 @@ describe("buildReplayCommand", () => { 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)", () => { diff --git a/src/output.ts b/src/output.ts index bb3a800..45e73ef 100644 --- a/src/output.ts +++ b/src/output.ts @@ -21,6 +21,13 @@ 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; @@ -43,9 +50,7 @@ export function buildReplayCommand( options: ReplayOptions = {}, ): string { const { format, outputType, includeArchived, groupByTeamPrefix, regexHint } = options; - const parts: string[] = [ - `github-code-search ${JSON.stringify(query)} --org ${org} --no-interactive`, - ]; + 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) { @@ -89,7 +94,7 @@ export function buildReplayCommand( parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`); } if (regexHint) { - parts.push(`--regex-hint ${JSON.stringify(regexHint)}`); + parts.push(`--regex-hint ${shellQuote(regexHint)}`); } return `# Replay:\n${parts.join(" \\\n ")}`; 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;