GitHub
Open any repository from GitHub in the workbench, nothing cloned and nothing installed
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.
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.
// 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:
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, 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 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.
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.