This file provides context for AI coding agents (GitHub Copilot, Claude, Gemini, etc.) working in this repository.
github-code-search is an interactive CLI (powered by Bun) to search GitHub code across an organisation. It aggregates results per repository, displays a keyboard-driven TUI and lets the user select extracts before printing structured markdown or JSON output. A query subcommand and an upgrade subcommand are exposed via Commander.
| Tool | Version |
|---|---|
| Bun | ≥ 1.0 (runtime, bundler, test runner, package manager) |
| TypeScript | via Bun (no separate tsc invocation needed at runtime) |
| oxlint | linter (bun run lint) |
| oxfmt | formatter (bun run format) |
| knip | dead-code detector (bun run knip) |
There is no Node.js / npm involved. Always use bun commands.
bun install # install dependencies (reads bunfig.toml + package.json)bunfig.toml sets smol = true (lighter install). No additional setup step is needed.
bun run build.ts # compile a self-contained binary → dist/github-code-search
bun run build.ts --target=bun-darwin-arm64 # cross-compile (see CONTRIBUTING.md for all targets)The build script (build.ts) injects the git commit SHA, target OS and architecture into the binary. The produced binary has no runtime dependency and can be distributed as a single file.
bun test # run the whole TypeScript test suite
bun test --watch # re-run on file changes (development)
bun run test:bats # run the shell-integration tests for install.sh (requires bats-core)All TypeScript tests use Bun's built-in test runner (@jest/globals-compatible API: describe, it, expect). No additional testing library is needed. The setup file is src/test-setup.ts (referenced in bunfig.toml).
Shell-integration tests use bats-core (brew install bats-core or install via your package manager). They cover install_completions() from install.sh and are located in install.test.bats.
bun run lint # oxlint — must pass before submitting
bun run format # oxfmt write (auto-fix)
bun run format:check # oxfmt check (CI check)
bun run knip # detect unused exports / filesAlways run bun run lint and bun run format:check before considering a change complete.
github-code-search.ts # CLI entry point — Commander subcommands: query, upgrade, completions
build.ts # Build script (Bun.build)
bunfig.toml # Bun configuration (smol install, test preload)
tsconfig.json # TypeScript configuration
knip.json # knip (dead-code) configuration
src/
types.ts # All shared TypeScript interfaces (TextMatchSegment,
# TextMatch, CodeMatch, RepoGroup, Row, TeamSection,
# OutputFormat, OutputType)
api.ts # GitHub REST API client (search, team fetching)
api-utils.ts # Shared retry (fetchWithRetry) and pagination (paginatedFetch)
# helpers used exclusively by api.ts — performs network I/O
cache.ts # Disk cache for the team list (getCacheDir, getCacheKey,
# readCache, writeCache) — performs filesystem I/O
aggregate.ts # Result grouping & filtering (applyFiltersAndExclusions)
completions.ts # Pure shell-completion generators: generateCompletion(),
# detectShell(), getCompletionFilePath() — no I/O
group.ts # groupByTeamPrefix, applyTeamPick, rebuildTeamSections,
# flattenTeamSections — team-prefix grouping + pick 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,
# help overlay, selection)
output.ts # Text (markdown) and JSON output formatters
upgrade.ts # Auto-upgrade logic (fetch latest GitHub release, replace binary)
# + refreshCompletions() — overwrites existing completion file
render/
highlight.ts # Syntax highlighting (language detection + token rules)
filter.ts # FilterStats + buildFilterStats
filter-match.ts # Pure pattern matchers — makeExtractMatcher, makeRepoMatcher
rows.ts # buildRows, rowTerminalLines, isCursorVisible
summary.ts # buildSummary, buildSummaryFull, buildSelectionSummary
selection.ts # applySelectAll, applySelectNone
team-pick.ts # renderTeamPickHeader — pick-mode candidate bar (pure, no I/O)
*.test.ts # Unit tests co-located with source files
test-setup.ts # Global test setup (Bun preload)
- Pure functions first. All business logic lives in pure, side-effect-free functions (
aggregate.ts,group.ts,output.ts,render/sub-modules). This makes them straightforward to unit-test. - Side effects are isolated. API calls (
api.ts,api-utils.ts), TTY interaction (tui.ts) and CLI parsing (github-code-search.ts) are the only side-effectful surfaces.api-utils.tshosts shared retry/pagination helpers that perform network I/O and must not be used outsideapi.ts.cache.tshosts disk-cache helpers that perform filesystem I/O and must not be used outsideapi.ts. render.tsis a façade. It re-exports everything fromrender/and adds two top-level rendering functions. Consumers import fromrender.ts, not directly from sub-modules.types.tsis the single source of truth for all shared interfaces. Any new shared type must go there.- No classes — the codebase uses plain TypeScript interfaces and functions throughout.
- Test files are named
<module>.test.tsand sit next to their source file. - Use
describe/it/expectfrom Bun's test runner. - Only pure functions need tests;
tui.tsandapi.tsare not unit-tested.api-utils.tsis the exception: its helpers are unit-tested by mockingglobalThis.fetch.cache.tsis also tested: it uses theGITHUB_CODE_SEARCH_CACHE_DIRenv var override to redirect to a temp directory, so tests have no filesystem side effects on the real cache dir.completions.tsis fully unit-tested (completions.test.ts). Tests that exercisegetCompletionFilePath()must unsetXDG_CONFIG_HOME,XDG_DATA_HOME, andZDOTDIRinbeforeEach(and restore them inafterEach) to avoid contamination from the CI runner environment. - When adding a function to an existing module, add the corresponding test case in the existing
<module>.test.ts. - When creating a new module that contains pure functions, create a companion
<module>.test.ts. - Tests must be self-contained: no network calls, no filesystem side effects.
All commits to this repository must be cryptographically signed. Unsigned commits will be rejected by branch protection rules.
For local commits — configure GPG or SSH signing once:
# Recommended: sign every commit automatically
git config --global commit.gpgsign true
# Or sign a single commit manually
git commit -S -m "feat: my change"Verify your setup:
git log --show-signature -1 # should show "Good signature from …"For agent-created commits — ensure the agent runs a local git commit -S so the local GPG/SSH key is used and the commit bears the developer's own verified signature. This is the required approach in this repository.
⚠️ Do NOT use MCP REST API push tools (mcp_github_push_files,mcp_github_create_or_update_file) to create commits in this repo.
Those tools push files via the GitHub REST API and create commits signed by GitHub's own key — not the developer's personal key. While GitHub marks them as "Verified", they do not carry the developer's identity.
Always commit locally viagit commit -S(or withcommit.gpgsign = true) and push withgit push.
| Branch type | Pattern | Example |
|---|---|---|
| Feature | feat/<short-description> |
feat/json-output-type |
| Bug fix | fix/<short-description> |
fix/exclude-repos-with-org-prefix |
| Refactoring | refactor/<short-description> |
refactor/extract-filter-module |
| Documentation | docs/<short-description> |
docs/25-init-vitepress |
Commit messages use imperative mood: Add …, Fix …, Extract …, not Added or Fixing.
For epics spanning multiple PRs, create a long-lived feature branch (feat/<epic-name>) and merge each sub-issue PR into it. Open a final PR from the feature branch into main when the epic is complete.
This project follows Semantic Versioning:
| Change type | Bump | Example |
|---|---|---|
| Bug fix (no new behaviour, no API change) | patch |
1.2.4 → 1.2.5 |
| New feature, backward-compatible | minor |
1.2.4 → 1.3.0 |
| Breaking change (CLI flag removed/renamed) | major |
1.2.4 → 2.0.0 |
# 1. Bump the version in package.json directly
# Do NOT use `bun pm version` — it creates a git commit AND a git tag
# automatically, which conflicts with the release workflow below.
sed -i '' 's/"version": ".*"/"version": "X.Y.Z"/' package.json
jq -r .version package.json # verify
# 2. Create the release branch and commit
git checkout -b release/$(jq -r .version package.json)
git add package.json
git commit -S -m "v$(jq -r .version package.json)"
# 3. Write (or update) the blog post for the release
# • Required for minor and major releases.
# • Patch releases: optional — a brief note in the GitHub Release is sufficient.
# File: docs/blog/release-v<X-Y-Z>.md (e.g. docs/blog/release-v1-3-0.md)
# Update docs/blog/index.md table too.
# 4. Tag and push — this triggers the CD pipeline
git tag v$(jq -r .version package.json)
git push origin release/$(jq -r .version package.json) --tagsPushing a tag vX.Y.Z triggers cd.yaml:
- Compiles the binary for all six targets (linux-x64, linux-arm64, linux-x64-baseline, darwin-x64, darwin-arm64, windows-x64).
- Creates a GitHub Release with all binaries attached.
generate_release_notes: true— GitHub auto-populates the release body from merged PR titles and commit messages since the previous tag. - Legacy platform aliases are also published for backward-compat with pre-v1.2.1 binaries.
Pushing a major tag (vX.0.0) additionally triggers docs.yml → snapshot job:
- Builds a versioned docs snapshot at
/github-code-search/vX/. - Auto-generates
docs/blog/release-vX-0-0.mdstub if it does not exist yet. - Prepends the new entry to
docs/public/versions.jsonand commits back tomain.
Always ask the user interactively before writing the blog post. Do not invent highlights or descriptions from code alone. Ask at minimum:
- Which changes should be highlighted?
- Is there a one-line description for the front-matter?
- Any before/after CLI output examples to include?
Only proceed to write
docs/blog/release-v<X-Y-Z>.mdonce you have the user's input.
| Release type | Blog post | Location |
|---|---|---|
| Major | Required (written by hand — CI stub automates the skeleton) | docs/blog/release-vX-0-0.md |
| Minor | Required | docs/blog/release-vX-Y-0.md |
| Patch | Optional | GitHub Release body is sufficient |
For minor/major releases update docs/blog/index.md to add a row in the version table:
| [vX.Y.Z](./release-vX-Y-Z) | One-line highlights |- TypeScript throughout — no
.jsfiles insrc/. - Bun executes
.tsfiles directly; no transpilation step is needed to run the CLI locally (bun github-code-search.ts query ...). - The
--exclude-repositoriesand--exclude-extractsoptions accept both short (repoName) and long (org/repoName) forms — this normalisation happens inaggregate.ts. - The
--group-by-team-prefixoption requires aread:orgGitHub token scope; this is documented inREADME.md. - The
upgradesubcommand replaces the running binary in-place usingsrc/upgrade.ts; be careful with filesystem operations there. - After a successful upgrade,
refreshCompletions()(insrc/upgrade.ts) silently overwrites the existing completion file if one is already present. It never creates a file from scratch — installation is the user's responsibility (viainstall.shor thecompletionssubcommand). - The
completionssubcommand (ingithub-code-search.ts) prints the completion script for the detected (or specified) shell to stdout. It is a thin wrapper aroundgenerateCompletion()insrc/completions.ts. - Shell-integration tests for
install.shlive ininstall.test.batsand requirebats-core. Run them withbun run test:bats. The CI runs them in a dedicatedtest-batsjob usingbats-core/bats-action. picocolorsis the only styling dependency; do not addchalkor similar.- Keep
knipclean: every exported symbol must be used; every import must resolve. - The
--pick-teamoption is repeatable (Commander collect function); each assignment resolves one combined section label to a single team. A warning is emitted on stderr when a label is not found. src/render/team-pick.tsis a pure module (no I/O) and must be consumed only via thesrc/render.tsfaçade — it is imported directly insiderender.tsfor internal use but is not re-exported publicly (knip would flag it).RepoGroup.pickedFrom(optional field insrc/types.ts) tracks the combined label a repo was moved from; future split-mode features will use this to offer re-assignment.