
# GitHub

`github` puts any GitHub repository in the workbench. Type `owner/repo`, or hand it an address
you already have. The tarball is fetched, gunzipped by the browser, and unpacked straight into
the tree — no git, no clone, no server holding a checkout.

```ts
import { Workbench, FileSystem } from "codelet/workbench";
import { github } from "codelet/extensions/github";

const workbench = new Workbench({
  parent: document.getElementById("app")!,
  fs: new FileSystem({}),
  extensions: [github({ url: "/api/gh/" })],
});
```

## What a repository may be written as

The field and `repo()` both accept the short form and every shape GitHub itself hands out:

| Written                                          | What opens                               |
| ------------------------------------------------ | ---------------------------------------- |
| `h3js/h3`                                        | the default branch                       |
| `h3js/h3@v2`                                     | a branch, tag, or sha                    |
| `nitrojs/nitro@main:examples/hello-world`        | that directory, opened as the whole tree |
| `https://github.com/h3js/h3/tree/main/src`       | the same, pasted                         |
| `https://github.com/h3js/h3/blob/main/src/h3.ts` | the repository, with that file opened    |
| `https://github.com/h3js/h3/pull/1516`           | the pull request's tree                  |

A `#L12` or `?plain=1` is dropped. A route this doesn't know — `issues`, `actions`, `wiki` —
still opens the repository at its default branch, rather than refusing an address GitHub gave
you.

A directory opens **as the tree**, not as a folder inside one: `…/tree/main/examples/x` roots
the workspace at `examples/x`, which is what lets a container run `npm install` straight in it.
Whatever actually opened is handed back as `owner/repo@ref:dir` — the same short form, so a link
built from it reopens without asking GitHub the same question twice.

## Proxy required

GitHub only serves tarballs to `render.githubusercontent.com`, so a browser on your own origin
is refused before it reads a byte. `url` is where you name an origin that's allowed to ask —
either a prefix string, fetched as `${url}<owner>/<name>?ref=<ref>`, or a function for an
address of another shape.

```ts
// Any server — Nitro shown, express/hono/a lambda are the same four lines.
export default defineHandler(async (event) => {
  const url = new URL(event.req.url);
  const [owner, name] = url.pathname.split("/").slice(3);
  const ref = url.searchParams.get("ref") || "HEAD";
  if (!NAME.test(owner) || !NAME.test(name) || !REF.test(ref)) {
    return new Response("Not a repository.\n", { status: 400 });
  }
  const upstream = await fetch(`https://codeload.github.com/${owner}/${name}/tar.gz/${ref}`);
  if (!upstream.ok) return new Response(null, { status: upstream.status === 404 ? 404 : 502 });
  return new Response(upstream.body, { headers: { "content-type": "application/gzip" } });
});
```

Stream the body through and don't set `content-encoding` — the archive is gzip as content, not
as transport. Leave `url` unset and the extension calls `codeload.github.com` directly, which a
browser refuses; the error message says so.

::note
There's no token option, and no rate-limit handling: the extension never attaches or asks for
credentials. A private repository works only if your own proxy authenticates the upstream fetch
— add that to the handler above. Without it, `url` reaches only what an anonymous request can.
::

## Options

| Option    | Type                            | Default         | Description                                                              |
| --------- | ------------------------------- | --------------- | ------------------------------------------------------------------------ |
| `url`     | `string \| (tarball) => string` | codeload direct | Where the tarball comes from — see above.                                |
| `api`     | `Ungh`                          | `ungh()`        | Where the pane's questions go. Opening a repository never depends on it. |
| `repo`    | `() => string`                  | —               | Which repository to open without being asked, called on activation.      |
| `opened`  | `(name: string) => void`        | —               | A repository opened, named the way it would be typed back.               |
| `command` | `() => string`                  | —               | A shell command to run once the first repository has landed.             |
| `group`   | `string`                        | ungrouped       | Where its line sits in the explorer's welcome, `group@order`.            |

`repo` and `opened` are how a host reads and writes its own address bar — the extension never
touches `location` itself:

```ts
github({
  url: "/api/gh/",
  repo: () => new URL(location.href).searchParams.get("gh") ?? "",
  opened: (name) => {
    const url = new URL(location.href);
    url.searchParams.set("gh", name);
    history.replaceState(null, "", url);
  },
});
```

Pass `command` alongside `terminal` and `webcontainer`, and `?gh=h3js/h3&run=npm install` is a
repository, in a machine, installing — one address, nothing typed. It runs in the last shell
profile the workbench has, once the repository has actually landed.

## What it adds

- A **GitHub** pane in the activity bar: an **Open a repository…** field, the repository's
  description, stars, forks and default branch, and folded lists of branches, releases and
  contributors.
- **GitHub: Open Repository…** and **GitHub: Browse Repositories** in the palette, and the same
  two as links in the explorer's empty state.
- A status bar item naming whatever is open — click it to open another.

`api` points the pane's own questions (description, branches, releases, contributors) at
[ungh.cc](https://ungh.cc), a public mirror of GitHub's API with no rate limit for you and no
proxy to mount. It answers public repositories only, which is the only kind a tarball can be
fetched for anyway. If ungh is unreachable the pane just says less — opening a repository through
`url` isn't affected.

## Where the files go, and what's left out

Every repository lands at the root of your tree, beside anything you seeded — not in a workspace
of its own, because `codelet/extensions/webcontainer` and `codelet/extensions/remote` both mirror
the tree you're looking at, and a repository opened somewhere else would be invisible to both.
Opening a second repository removes what the first put there and nothing else.

Files over 4MB are skipped and named in the **GitHub** output channel. A repository over 96MB
unpacked is abandoned partway. Nothing is pushed anywhere — `.git` isn't in a tarball, and a save
writes into the page and no further.

## Changes since you opened it

The tarball is a base revision, so mounting [`codelet/extensions/scm`](/extensions/scm) beside
this one gets you a real, if read-only, source control: the Source Control pane fills with what
you've changed against the ref you opened, a **Discard** button puts a file back, and the status
bar shows the ref. Renamed files are recognised as one row rather than two, where the answer
isn't a guess.

```ts
import { github } from "codelet/extensions/github";
import { scm } from "codelet/extensions/scm";

extensions: [scm(), github({ url: "/api/gh/" })];
```

Without `scm` mounted, none of that draws — the registry it reads from is the workbench's own,
so nothing here costs you for not using it.

:read-more{to="/extensions/scm"}
