Codelet logoCodelet

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:

WrittenWhat opens
h3js/h3the default branch
h3js/h3@v2a branch, tag, or sha
nitrojs/nitro@main:examples/hello-worldthat directory, opened as the whole tree
https://github.com/h3js/h3/tree/main/srcthe same, pasted
https://github.com/h3js/h3/blob/main/src/h3.tsthe repository, with that file opened
https://github.com/h3js/h3/pull/1516the 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

OptionTypeDefaultDescription
urlstring \| (tarball) => stringcodeload directWhere the tarball comes from — see above.
apiUnghungh()Where the pane's questions go. Opening a repository never depends on it.
repo() => stringWhich repository to open without being asked, called on activation.
opened(name: string) => voidA repository opened, named the way it would be typed back.
command() => stringA shell command to run once the first repository has landed.
groupstringungroupedWhere 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.

Read more in Extensions > Scm.