gh implementation uses the vocabulary of the official
cli/cli project. A repository is a tree, so
mirage already reads one as files: the
github mount is the read half, and ls,
cat and grep are how an agent explores it. gh is the write half,
plus the account-level operations a filesystem has no shape for.
Licenses: The upstream cli/cli project uses the
MIT License; Mirage’s
independent implementation uses
Apache-2.0.
Install
clis: section; see the
CLI overview.
An Enterprise Server base ends in
/api/v3, and GraphQL (gh api graphql
and the verbs that query it, such as repo view --json) then goes to
/api/graphql on the same host, outside the REST base, as real gh sends
it. Any other baseUrl answers GraphQL at baseUrl/graphql.
repo and branch are what real gh reads off the current directory’s git
remote and checkout. A workspace has neither, so the install carries them:
repo answers a line that names no repository, and both feed the
{owner}/{repo}/{branch} placeholders. Two installs under different head words are two
accounts.
Authentication status
gh auth status checks the configured token with GET /user, reports the
host and account, and exits 1 when GitHub rejects the credential. It needs
no mount. The token source is reported as Mirage configuration: configuration
retains the resolved secret, not the environment variable or file that supplied
it. The credential itself is never printed.
Reading is the mount, acting is the CLI
gh lands on the same repository the mount reads, but it
lands by repository name rather than by any vfs path, so the mount has
nothing to aim a per-path invalidation at. The executor expires every
mount’s caches after a write verb of an account CLI, so the next cat or
ls refetches instead of serving the pre-write bytes. Nothing is required
of the caller, and nothing on the spec names the mount.
Verbs
--help; man gh, man gh issue, and man gh pr create render the same text from the same spec. list also accepts ls,
and issue and pull request create also accept new.
Typed workflows
Typed read commands accept-R/--repo [HOST/]OWNER/REPO where applicable.
Most also accept --json field1,field2 and -q/--jq EXPRESSION; --jq
requires --json, so the selected field set stays explicit and bounded.
List commands use -L/--limit and fetch as many pages as needed.
gh issue view, gh issue list, gh pr view and gh pr list accept every
--json field gh 2.85 does, and gh repo view and gh repo list every
repository field. Like gh, they ask GraphQL for exactly the fields named and
print each in gh’s own shape, so files, commits, reviews,
statusCheckRollup and closedByPullRequestsReferences need no fallback to
gh api. pr view reads reviews, comments, closing issues and checks past
their first 100, and issue view its comments and closing pull requests.
issue view answers a pull request’s number too, as gh does, with the
fields only an issue has at their zero. The JSON prints compact, one value
per line, the way gh prints it to anything but a terminal.
-:
gh workflow view --yaml prints the workflow’s file as the repository holds
it at -r/--ref, a branch or tag, or on the default branch; that is the one
read gh makes, and --ref without --yaml is refused as gh refuses it.
gh run view --log prints a completed run’s log a line at a time, each line
behind its job and step name, from the log archive GitHub ships; a job the
archive has no log for is fetched on its own, and --log-failed keeps only the
failed steps of failed jobs. A run still going is refused in gh’s words, run N is still in progress; logs will be available when it is complete, before
any log is asked for.
repo
view prints a name: line, a description: line, then --
and the README, with the separator omitted when the repository has none.
Use --json for any of gh’s repository fields or gh api for the complete
REST object.
rename takes the new name as the operand and the repository to
rename on -R, which is the reverse of what the shape of the line
suggests; that is upstream’s grammar, not a mirage choice.
edit sends the settings named on the line in one PATCH, as gh does, and
reads and replaces the topic list when --add-topic or --remove-topic
changes it. A switch such as --enable-wiki is on when bare and off as
--enable-wiki=false. With no setting named gh would prompt, so it is
refused, and --visibility needs --accept-visibility-change-consequences.
delete needs --yes and a named repository, since gh only prompts for the
current one; a name with no owner is the viewer’s. Both print nothing on
success, as gh does when it is not writing to a terminal.
[HOST/]OWNER/REPO is parsed from the right, so the owner and the
repository are the last two segments and a leading host is dropped. The
host is accepted for compatibility with lines copied from real gh but
does not route: every call goes to the install’s baseUrl. A second
host means a second install.
api
gh api reaches every endpoint that has no typed verb, which is most of
them.
{owner}, {repo} and {branch} expand from the install, the way real
gh expands them from the current repository. Any other brace pair is left
exactly as typed and reaches the wire, which is gh’s behavior too. Quote
the endpoint so the shell does not eat the braces.
The rules are gh’s own:
- The method is
GETwith no fields andPOSTonce a field is given, unless-Xsays otherwise. - A
GETcarries its fields in the query string; every other method carries them in a JSON body. -f/--raw-fieldis always a string.-F/--fieldreadstrue,false,nulland integers as their JSON types. A typed value beginning with@reads that workspace file, and@-reads stdin.- Object keys such as
config[enabled]and arrays such aslabels[]build nested JSON rather than flat keys. --input FILEsends that JSON document as the body and moves any fields to the query string.--input -reads stdin.-H/--headeradds or replaces request headers.--paginatefollows RESTLinkheaders, and pages that are arrays print as one array, joined the way gh joins them;--slurpwraps every page in one array instead.--silentsuppresses response output without suppressing the request.-i/--includeprints each response’s status line and headers before its body:HTTP/1.1 200 OK, then the headers in name order, each line ending\r\n, then a blank line. Pages print as they came, a newline between two, and--silentstill prints the headers. A failing response is headed too.- A call with no fields sends no body at all, so a bare
DELETEis a bareDELETErather than an empty JSON object with a content type. Some endpoints read those differently. - The leading slash is optional.
- A placeholder expands in an endpoint and in a
-Fvalue, but not in a-fone, which is the split gh’s own--helpdescribes. - A read (
GET,HEAD,OPTIONS) leaves the mount’s cache alone; only a write expires it. - A request that gets no response at all fails at once, with exit 1 and gh’s
words:
error connecting to HOSTand a pointer at githubstatus.com for a host that does not resolve,Get "URL": dial tcp ADDR: connect: connection refusedfor a refused connection. It is never retried, and neither is a500. This holds for every verb, not onlyapi.
-q/--jq evaluates in-process and prints each output as gh does:
a string raw, null as an empty line, a number in fixed notation with two
decimals unless it is whole (1.50, 3), and anything else as compact JSON
with its keys sorted and <, > and & escaped. The rest of the shell can
consume stdout just as well:
--jq program that fails ends the command with exit 1, after the lines
it printed first. The report follows gh’s gojq where mirage’s jq can tell
what gojq would say: an error the program raises with error(v) reads
error: <v>, with v as it is when it is a string and as compact JSON with
sorted keys otherwise; halt_error reads halt error: <v>; and halt, like
halt_error on null, only ends the output. As with every CLI failure,
mirage puts the command’s name first (gh api: error: boom), where gh
prints error: boom alone.
Divergences from upstream gh
gh is virtualized, not wrapped. The intentional boundaries are: