repoq
v1.4.0
Published
Query Git, GitHub, and Forgejo repository state, and create pull requests on either forge, with structured, machine-readable output.
Maintainers
Readme
repoq
Query Git repository state without parsing command output — and create pull requests on whichever forge backs the repo.
Each repoq command replaces multiple git/gh/fgj commands, handling edge cases
(detached HEAD, unborn branches, missing remotes) and returning validated JSON.
It detects the forge from your origin remote and routes queries to GitHub (via
gh) or Forgejo (via the Forgejo REST API). Works offline, degrading gracefully
when the forge CLI is unavailable. The one write verb, pr create, opens a pull
request through the same routing, taking the body from a file or stdin so no
shell ever expands its backticks or $ references.
Why repoq?
Getting reliable repository state from git and gh is harder than it looks:
- Parsing is fragile.
git status --porcelainuses two-character codes.git logoutput varies by format string. Worktree listings are multi-line records. - Edge cases break scripts. Detached HEAD, unborn branches, missing upstream tracking, and offline mode all need special handling.
- GitHub data requires multiple calls. PR checks/reviews come from
gh pr view. Unresolved review threads require GraphQL. Combining them takes orchestration. - Fallbacks are tedious. Finding the default branch might need
gh repo view, orgit symbolic-ref, or probing formain/master—depending on what's available.
repoq handles all of this internally and returns typed, validated JSON.
What it does
repoq can query:
status: one-shot repository snapshot- Runs multiple git commands plus optional gh/Forgejo queries
- Returns branch state, working tree breakdown, worktree summary, and last commit
- Handles detached HEAD, unborn branches, and missing forge auth gracefully
branch: branch lifecycle and tracking- Runs multiple git commands plus optional gh/Forgejo queries
- Reports local/remote existence, upstream tracking, worktree location, PR status
- Detects detached HEAD and unborn state without cryptic errors
diff-range: merge-base diff summary- Runs multiple git commands, with optional gh default-branch lookup
- Returns commit count, conventional commit metadata, file changes, line stats
- Tries remote branch first, falls back to local; handles unborn HEAD
worktrees: worktree inventory with status- Runs 1 + N git commands (status per worktree)
- Returns cleanliness, uncommitted counts, issue IDs from branch names
- Filters by
--issue-idor--uncommitted-only
pr view(the defaultprsubcommand): pull request with derived summaries- GitHub:
gh pr viewplus GraphQL for unresolved thread summaries - Forgejo: the Forgejo REST API for PR metadata, reviews, and commit-status checks
- Returns checks (passing/failing/pending), reviews, unresolved thread counts
- Works without error if PR doesn't exist
- GitHub:
pr create: open a pull request on the detected forge- GitHub:
gh pr createwith the body piped over stdin - Forgejo:
POST /repos/{owner}/{repo}/pullsauthenticated byfgj auth token - Takes the body only from
--body-file <path>(or stdin via-), never argv, so no shell ever expands backticks or$...inside it - Defaults
--headto the current branch and--baseto the default branch
- GitHub:
default-branch: resilient resolution- Fallback order: GitHub API → symref →
remote set-head --autoretry → probemain/master→ fallbackmain - Works offline, without gh, and on fresh clones
- Fallback order: GitHub API → symref →
forge: forge detection fromorigin- Resolves otherwise-unrecognized SSH
Hostaliases throughssh -G - May run commands from trusted SSH configuration because OpenSSH evaluates
Match exec - Classifies GitHub's forge and SSH transport hosts as GitHub; any other resolved host is a candidate Forgejo (self-hostable on any domain)
- Returns
unknownwhen the origin has no parseable host, or when SSH configuration evaluation fails for a short host that may be an unresolved alias - Derives the slug, CLI (
gh/fgj), API host, and connection coordinates
- Resolves otherwise-unrecognized SSH
Requirements
- Node.js >= 24.0.0
- Git
Optional:
- OpenSSH (
ssh) — resolves otherwise-unrecognized SSH hosts through local configuration. OpenSSH evaluatesMatch execwhile processingssh -G, so this may run commands from your SSH configuration even during an otherwise read-onlyrepoqcommand. - GitHub CLI (
gh) — PR queries, PR creation, and GitHub-backed default-branch detection on GitHub origins. - Forgejo CLI (
fgj) — authenticates Forgejo REST queries (PR metadata, reviews, checks) and PR creation on Forgejo origins.
Without the matching CLI, commands degrade gracefully—git-only data still works.
Install / run
# one-off
npx -y repoq status --json
# global
pnpm add -g repoq
repoq status --jsonRun it from inside a Git repository.
Usage
repoq <command> [options]Common examples:
# Get current branch name
repoq status --json | jq -r '.branch.current'
# Check if working tree is clean
repoq status --json | jq '.workingTree.isClean'
# Check upstream tracking
repoq status --json | jq '.branch.tracking | {ahead, behind}'
# Check if PR exists for current branch
repoq status --json | jq '.branch.pr.exists'
# Count commits since main
repoq diff-range --json | jq '.commits.count'
# List worktrees with uncommitted changes
repoq worktrees --uncommitted-only --json | jq '.[].path'
# Check if PR is approved
repoq pr --json | jq '.reviews.approved > 0'
# Check CI status
repoq pr --json | jq '.checks.failing == 0'
# Verify all commits are conventional
repoq diff-range --json | jq '[.commits.messages[].isConventional] | all'
# List changed files
repoq diff-range --json | jq '.files.added + .files.modified | .[]'
# Inspect a specific branch
repoq branch feature/login --json | jq '.tracking'
# Detect which forge and CLI back the current repo
repoq forge --json | jq -r '.cli'
# Create a PR for the current branch (single-quoted heredoc: the shell never
# expands the body, and repoq hands its bytes to the forge verbatim)
repoq pr create --title 'docs: clarify example' --body-file - <<'EOF'
Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`.
EOFBefore and after
Getting PR approval status without repoq:
# Check if PR is approved (fragile, verbose)
pr_json=$(gh pr view --json reviews)
approved=$(echo "$pr_json" | jq '[.reviews[] | select(.state == "APPROVED")] | length')
if [ "$approved" -gt 0 ]; then echo "Approved"; fiWith repoq:
repoq pr --json | jq '.reviews.approved > 0'The difference grows with complexity. Checking if a branch is ready for merge
(clean working tree + CI passing + approved + no unresolved threads) takes
50+ lines of shell. With repoq:
repoq status --json | jq '.workingTree.isClean' && \
repoq pr --json | jq '.checks.failing == 0 and .reviews.approved > 0 and .unresolved.threads == 0'Command reference
| Command | Purpose |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| status [--json] | One-shot repo snapshot with full branch info, working tree, last commit |
| branch <branch> [--json] [--issue-id <id>] | Branch lifecycle/tracking/worktree/PR status with unborn detection |
| diff-range [base] [--json] [--head <branch>] | Merge-base diff summary, commit metadata, file/stats, fast-forwardability |
| worktrees [--json] [--issue-id <id>] [--uncommitted-only] | Enumerate worktrees with cleanliness/unborn state and filters |
| pr view [branch] [--json] (default: pr [branch]) | PR metadata plus checks/reviews/unresolved summaries |
| pr create --title <t> --body-file <p> [--base] [--head] [--json] | Open a PR on the detected forge; body from file or stdin (-), shell-safe |
| default-branch [--json] | Resolve default branch with GitHub + git fallbacks |
| forge [--json] | Detect the forge from origin: provider, slug, CLI, API host, coordinates |
For a branch named like a subcommand (view, create), use the explicit form:
repoq pr view create.
For detailed command help:
repoq --help
repoq <command> --helpLibrary API
repoq exports its forge detection for programmatic use, so other tools can share one implementation instead of re-deriving the provider from origin:
import { detectForge, FORGEJO_API_HOST, type ForgeInfo } from "repoq";
const forge: ForgeInfo = detectForge("ssh://[email protected]:2222/j4k/cluster.git");
// → { provider: "forgejo", slug: "j4k/cluster", cli: "fgj",
// apiHost: "code.j4k.dev", apiBase: "https://code.j4k.dev/api/v1",
// hostnameFlag: "--hostname code.j4k.dev", ... }detectForge(originUrl) is pure: it parses any remote URL shape (scp-like, ssh://, https://), matches the host case-insensitively, and never touches the filesystem. Because local SSH configuration is intentionally outside this pure API, callers pass the canonical host when they need alias-aware results. The repoq CLI leaves recognized forge hosts unchanged and resolves other SSH origins with ssh -G before calling detectForge. A changed HostName becomes the classification host; an unchanged HostName retains the original candidate-Forgejo interpretation because OpenSSH uses the input host as its default. If configuration evaluation fails or times out, an FQDN-like, dot-qualified host retains that candidate-Forgejo interpretation, while a short host returns unknown because it may be an unresolved local alias. Dot-qualified SSH aliases therefore share the same fallback tradeoff as canonical hosts when configuration cannot be evaluated. GitHub's ssh.github.com transport endpoint normalizes to the github.com forge identity. github.com resolves to GitHub; any other host resolves to a candidate Forgejo whose API host is the origin host (preserving a custom HTTPS port; an alias map covers asymmetric deployments like the Forge, whose API host differs from its clone host). Actual Forgejo support is gated at request time by whether fgj auth token --hostname <host> succeeds, so an unrelated host fails gracefully. Pass null for "no origin". The returned ForgeInfo has the same shape as repoq forge --json.
The CLI treats the local SSH configuration as trusted input. OpenSSH's -G mode still evaluates Match exec conditions, which run their configured commands through the user's shell. Any CLI command that encounters an otherwise-unrecognized SSH origin can therefore run a matching command from ~/.ssh/config (including included files), even though the repoq operation itself only reads repository state. The one-second timeout bounds evaluation time but does not prevent those side effects.
For alias evaluation, REPOQ_SSH_PATH is the explicit, highest-precedence executable override. Without it, the CLI honors the argv-safe executable path in GIT_SSH and reads Git's ssh.variant. The auto variant follows Git's basename detection for known PuTTY-family clients and otherwise probes the executable with OpenSSH arguments; unrecognized explicit variant values follow Git's OpenSSH fallback. Shell-valued GIT_SSH_COMMAND and core.sshCommand cannot be tokenized without executing a shell, so the CLI detects them and returns unknown with a stderr diagnostic rather than evaluating a potentially different SSH configuration. A documented non-OpenSSH Git SSH variant also returns unknown, because OpenSSH's -G arguments are not portable to other clients. Set REPOQ_SSH_PATH to an executable wrapper that implements the OpenSSH -G interface when an explicit safe adapter is available. Consecutive detection of the same origin and environment is memoized for the lifetime of a CLI command.
Environment variables
| Variable | Description |
| ---------------- | ------------------------------------------------------------------------------------------ |
| REPOQ_GIT_PATH | Path to git binary (default: git from PATH) |
| REPOQ_GH_PATH | Path to gh binary (default: gh from PATH) |
| REPOQ_FGJ_PATH | Path to fgj binary (default: fgj from PATH) |
| REPOQ_SSH_PATH | Explicit OpenSSH-compatible executable override (default: GIT_SSH, then ssh from PATH) |
Agent rule
Add to your CLAUDE.md or AGENTS.md:
# Rule: Use `repoq` for Repository Queries
Run `npx -y repoq --help` to learn available options.
Use `repoq` instead of piping `git`/`gh` commands through `awk`/`jq`/`grep`.
Each command handles edge cases (detached HEAD, unborn branches, missing auth)
and returns validated JSON. Prefer `repoq` for reading state; use raw `git`/`gh`
for mutations (commit, push, merge).License
MIT
