From 70933c30fcbae7bfc4fb26c4bbcffdcf6828ed70 Mon Sep 17 00:00:00 2001
From: aron <263346377+aron-cf@users.noreply.github.com>
Date: Fri, 31 Jul 2026 13:30:37 +0000
Subject: [PATCH] computer: Make git opt-in
Move the git client behind an explicit Workspace option so the default package graph no longer pulls in isomorphic-git. The git subpath now exposes createGitClient() as a factory provider and keeps the pako replacement local to the bundled git entrypoint.
Add a node:zlib-backed pako shim for the isomorphic-git paths used by the workspace git client. This preserves automatic git support for callers that opt in while avoiding pako for Worker builds that only use the core workspace.
---
docs/13_git_interface.md | 15 +-
docs/README.md | 2 +-
package-lock.json | 4 -
packages/computer/README.md | 20 +-
packages/computer/package.json | 4 -
packages/computer/rolldown.config.ts | 13 +-
.../src/backends/worker/entrypoint.test.ts | 2 +
packages/computer/src/client.ts | 4 +-
packages/computer/src/git/cli.test.ts | 19 +-
packages/computer/src/git/index.test.ts | 2 +-
packages/computer/src/git/index.ts | 704 +++++++++---------
.../computer/src/git/pako-zlib-shim.test.ts | 62 ++
packages/computer/src/git/pako-zlib-shim.ts | 221 ++++++
packages/computer/src/index.ts | 1 +
packages/computer/src/stub.test.ts | 4 +-
packages/computer/src/workspace.test.ts | 31 +-
packages/computer/src/workspace.ts | 47 +-
.../computer/tests/script-runner-worker.ts | 2 +
packages/computer/vitest.config.ts | 6 +
19 files changed, 754 insertions(+), 409 deletions(-)
create mode 100644 packages/computer/src/git/pako-zlib-shim.test.ts
create mode 100644 packages/computer/src/git/pako-zlib-shim.ts
diff --git a/docs/13_git_interface.md b/docs/13_git_interface.md
index 5ed59b6a..45bf2b28 100644
--- a/docs/13_git_interface.md
+++ b/docs/13_git_interface.md
@@ -6,10 +6,12 @@
> `packages/computer/src/backends/worker/`. Everything below
> works today.
-`workspace.git` is a major typed surface on `Workspace`, alongside `fs`, `runtime`, Assets, and Artifacts. It runs every operation against the
-local SQLite-backed VFS through `isomorphic-git`, so a
+`workspace.git` is a major typed surface on `Workspace`, alongside `fs`, `runtime`, Assets, and Artifacts. It is opt-in: pass `createGitClient()` from `@cloudflare/computer/git` as `WorkspaceOptions.git` to enable it. Git runs every operation against the local SQLite-backed VFS through `isomorphic-git`, so a
filesystem-only workspace (no backend) can drive a full
-clone/commit/diff cycle.
+clone/commit/diff cycle. The git subpath bundles `isomorphic-git`
+lazily and replaces its `pako` dependency with a small
+`node:zlib` shim for Workers running with `nodejs_compat`; the
+default `@cloudflare/computer` graph stays free of git.
Two doors into the same implementation:
@@ -111,8 +113,9 @@ diff against HEAD — through each entry point.
```ts
import { Workspace } from "@cloudflare/computer";
+import { createGitClient } from "@cloudflare/computer/git";
-const ws = new Workspace({ storage: ctx.storage });
+const ws = new Workspace({ storage: ctx.storage, git: createGitClient() });
await ws.git.clone({ url: "https://github.com/example/repo.git" });
await ws.fs.writeFile("/README.md", "hello world\n");
const patch = await ws.git.diff();
@@ -174,8 +177,8 @@ committer in this order:
by `git config user.email "..."`. Only the local
`
/.git/config` is consulted; there is no global
`~/.gitconfig` fallback.
-4. `defaultIdentity` from `createGitClient` / `new Workspace({
- defaultGitIdentity })`.
+4. `defaultIdentity` from `createGitClient()` / `new Workspace({
+ git: createGitClient(), defaultGitIdentity })`.
If none of the four yields a name and email,
`MissingIdentityError` fires. The CLI surfaces it as `git
diff --git a/docs/README.md b/docs/README.md
index 1e856a04..d9cfccc2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -46,7 +46,7 @@ The package ships several entrypoints:
| `@cloudflare/computer/backends/container` | `CloudflareContainerBackend` and `withWorkspaceContainer`. Pulls in the computerd / capnweb sync plumbing. |
| `@cloudflare/computer/backends/worker` | `WorkerBackend` and the bundled just-bash command runtime. |
| `@cloudflare/computer/backends/javascript` | `IsolateJavaScriptBackend`, configured libraries, durable relative imports, `node:fs/promises`, and trusted `ws:git` / `ws:artifacts`. |
-| `@cloudflare/computer/git` | Isomorphic-git glue for working with checkouts inside the workspace. |
+| `@cloudflare/computer/git` | Opt-in isomorphic-git glue for working with checkouts inside the workspace. Bundled lazily, with `pako` replaced by Workers `node:zlib`, and kept out of the default `@cloudflare/computer` graph. |
| `@cloudflare/computer/artifacts` | `createArtifact`, a session-scoped facade over the Cloudflare Artifacts Workers binding, plus its argv CLI. |
| `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. |
diff --git a/package-lock.json b/package-lock.json
index acd4bcd1..4137a7cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12197,7 +12197,6 @@
"peerDependencies": {
"@platformatic/vfs": "*",
"ai": "^6.0.196 || ^7.0.0",
- "isomorphic-git": "^1.27.0",
"zod": "^4.4.3"
},
"peerDependenciesMeta": {
@@ -12207,9 +12206,6 @@
"ai": {
"optional": true
},
- "isomorphic-git": {
- "optional": true
- },
"zod": {
"optional": true
}
diff --git a/packages/computer/README.md b/packages/computer/README.md
index 53046000..907c239c 100644
--- a/packages/computer/README.md
+++ b/packages/computer/README.md
@@ -56,12 +56,17 @@ uniform; the counts are just always zero.
surface: `exec`, `getExec`, `killExec`, and `disposeExec`. The selected
backend defines the source language. JavaScript results may include a
structured `value`; command backends return stdout/stderr and an exit code.
-- `workspace.git` — a typed git client backed by
- `isomorphic-git` against the local SQLite VFS. Surfaces both a
- TypeScript API (`workspace.git.clone({ url })`) and an
- argv-driven entry point (`workspace.git.cli({ argv })`). The
- worker backend's shell exposes the same dispatcher through a
- built-in `git` custom command. See
+- `workspace.git` — an opt-in typed git client backed by
+ `isomorphic-git` against the local SQLite VFS. Pass
+ `createGitClient()` from `@cloudflare/computer/git` as
+ `WorkspaceOptions.git` to enable both the TypeScript API
+ (`workspace.git.clone({ url })`) and the argv-driven entry point
+ (`workspace.git.cli({ argv })`). The git subpath bundles
+ `isomorphic-git` lazily and replaces its `pako` dependency with
+ the Workers `node:zlib` implementation, so the default package
+ graph stays free of git. The worker backend's shell exposes the
+ same dispatcher through a built-in `git` custom command when git
+ is configured. See
[`docs/13_git_interface.md`](../../docs/13_git_interface.md).
- `createAssets` (from `@cloudflare/computer/assets`) — `share` a
workspace file to an R2 bucket and get back a presigned URL.
@@ -158,8 +163,11 @@ and `workspace.runtime` as the primary surfaces.
Git, also without a backend:
```ts
+import { createGitClient } from "@cloudflare/computer/git";
+
const ws = new Workspace({
storage: ctx.storage,
+ git: createGitClient(),
defaultGitIdentity: { name: "Agent", email: "agent@example.test" },
});
await ws.git.clone({ url: "https://github.com/example/repo.git" });
diff --git a/packages/computer/package.json b/packages/computer/package.json
index 33b5ba77..20f9a33d 100644
--- a/packages/computer/package.json
+++ b/packages/computer/package.json
@@ -79,7 +79,6 @@
"peerDependencies": {
"@platformatic/vfs": "*",
"ai": "^6.0.196 || ^7.0.0",
- "isomorphic-git": "^1.27.0",
"zod": "^4.4.3"
},
"peerDependenciesMeta": {
@@ -89,9 +88,6 @@
"ai": {
"optional": true
},
- "isomorphic-git": {
- "optional": true
- },
"zod": {
"optional": true
}
diff --git a/packages/computer/rolldown.config.ts b/packages/computer/rolldown.config.ts
index 10b21606..044c57ec 100644
--- a/packages/computer/rolldown.config.ts
+++ b/packages/computer/rolldown.config.ts
@@ -8,8 +8,11 @@
// - capnweb — a regular npm dep on this package.
// - @platformatic/vfs — optional userland import used by
// examples but not the workspace core.
-// `node:*` builtins are externalised automatically because
-// `platform: "node"` is the default for `esm` output here.
+// - node:* — provided by nodejs_compat in workerd.
+//
+// The git entrypoint bundles isomorphic-git, but aliases pako to a
+// tiny node:zlib-backed compatibility layer so Workers don't carry
+// pako's JavaScript zlib implementation.
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
@@ -39,11 +42,8 @@ export default defineConfig({
"@platformatic/vfs",
"ai",
"zod",
- "isomorphic-git",
- /^isomorphic-git\//,
"just-bash",
- "node:crypto",
- "node:events",
+ /^node:/,
],
resolve: {
alias: {
@@ -51,6 +51,7 @@ export default defineConfig({
"@cloudflare/dofs/testing": resolve(here, "../dofs/src/testing.ts"),
"@cloudflare/computer-rpc": resolve(here, "../rpc/src/index.ts"),
"@cloudflare/computer-rpc/driver": resolve(here, "../rpc/src/sync-driver.ts"),
+ pako: resolve(here, "src/git/pako-zlib-shim.ts"),
},
},
// ESM only. Nothing in-tree loads CJS — the example Worker, computerd,
diff --git a/packages/computer/src/backends/worker/entrypoint.test.ts b/packages/computer/src/backends/worker/entrypoint.test.ts
index 0a988568..5dc55e13 100644
--- a/packages/computer/src/backends/worker/entrypoint.test.ts
+++ b/packages/computer/src/backends/worker/entrypoint.test.ts
@@ -19,6 +19,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { FakeArtifactsBinding } from "../../../tests/utilities/fake-artifacts-binding.js";
import type { ArtifactsCLIInput, ArtifactsCLIResult } from "../../artifacts/index.js";
import type { BackendHandle, WorkspaceBackend } from "../../backend.js";
+import { createGitClient } from "../../git/index.js";
import type { WorkspaceStub } from "../../stub.js";
import { Workspace } from "../../workspace.js";
import { ShellWorker } from "./entrypoint.js";
@@ -268,6 +269,7 @@ describe("ShellWorker", () => {
workspace = new Workspace({
storage: new SQLiteTestStorage() as never,
backends: [noopBackend()],
+ git: createGitClient(),
});
await workspace.ready();
// /workspace is the ShellWorker's default cwd; create it
diff --git a/packages/computer/src/client.ts b/packages/computer/src/client.ts
index ea54f8e2..61e5d10c 100644
--- a/packages/computer/src/client.ts
+++ b/packages/computer/src/client.ts
@@ -372,7 +372,9 @@ export async function getWorkspace(handle: WorkspaceHandle): Promise {
describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("diff prints the working-tree delta against HEAD", async () => {
- const ws = new Workspace({ storage: new SQLiteTestStorage() });
+ const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
await ws.ready();
// Seed a repo with one committed file, then mutate the
@@ -1987,6 +1987,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
// any subcommand drifts from the typed surface, the chain
// breaks here rather than in a downstream consumer.
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2030,6 +2031,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("log / show / rev-parse / ls-files round-trip", async () => {
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2065,6 +2067,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("hash-object / cat-file / update-ref / config round-trip", async () => {
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2104,7 +2107,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
});
it("remote add / list / remove round-trip through the config file", async () => {
- const ws = new Workspace({ storage: new SQLiteTestStorage() });
+ const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
await ws.ready();
const cli = (argv: string[]) => ws.git.cli({ argv, cwd: "/" });
await cli(["init"]);
@@ -2128,6 +2131,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("branch / checkout / tag round-trip moves HEAD and creates refs", async () => {
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2162,6 +2166,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("switch restores tracked file content from the target branch", async () => {
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2184,6 +2189,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
it("reset HEAD unstages all staged changes", async () => {
const ws = new Workspace({
+ git: createGitClient(),
storage: new SQLiteTestStorage(),
defaultGitIdentity: { name: "Test", email: "test@example.test" },
});
@@ -2205,7 +2211,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
});
it("commit without identity surfaces as exit 128", async () => {
- const ws = new Workspace({ storage: new SQLiteTestStorage() });
+ const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
await ws.ready();
await ws.git.cli({ argv: ["init"], cwd: "/" });
await ws.fs.writeFile("/a.txt", "x\n");
@@ -2221,7 +2227,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
// would drive it: configure identity, stage with -A, commit
// with -am, inspect with the new flags, branch with switch
// -c, then reset / stash / clean.
- const ws = new Workspace({ storage: new SQLiteTestStorage() });
+ const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
await ws.ready();
const cli = (argv: string[]) => ws.git.cli({ argv, cwd: "/" });
@@ -2299,18 +2305,17 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
// Force the clone path to fail by pointing at an invalid host;
// we want to pin that the dispatcher's catch arm produces a
// CLI-shaped result and doesn't propagate the rejection.
- const ws = new Workspace({ storage: new SQLiteTestStorage() });
+ const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
await ws.ready();
// Swap the git client out for one whose clone rejects, so we
// don't depend on network reachability inside the test runner.
const failing: GitClient = createGitClient({
- ws,
adapter: async () => ({
promises: {
readFile: vi.fn(async () => new Uint8Array()),
},
}),
- });
+ })({ ws });
// Replace `clone` with a deterministic failure — the real
// path is exercised by `clone.test.ts`.
(failing as { clone: GitClient["clone"] }).clone = async () => {
diff --git a/packages/computer/src/git/index.test.ts b/packages/computer/src/git/index.test.ts
index db39a117..7e084468 100644
--- a/packages/computer/src/git/index.test.ts
+++ b/packages/computer/src/git/index.test.ts
@@ -30,7 +30,7 @@ describe("createGitClient", () => {
const fs = stubFs();
const adapter = vi.fn(async () => fs);
- const client = createGitClient({ ws: { provider }, adapter });
+ const client = createGitClient({ adapter })({ ws: { provider } });
// No work happens at construction time.
expect(provider).not.toHaveBeenCalled();
diff --git a/packages/computer/src/git/index.ts b/packages/computer/src/git/index.ts
index 478324b5..a69300aa 100644
--- a/packages/computer/src/git/index.ts
+++ b/packages/computer/src/git/index.ts
@@ -1,15 +1,16 @@
// Public surface of @cloudflare/computer/git.
//
-// `createGitClient({ ws })` is the one entry point. It binds a
-// workspace handle once and returns a `GitClient` whose methods
-// don't repeat the workspace argument. Today the typed surface is
+// `createGitClient()` is the one entry point. It returns a
+// WorkspaceOptions.git factory; the factory binds a workspace
+// handle once and returns a `GitClient` whose methods don't repeat
+// the workspace argument. Today the typed surface is
// `clone` and `diff`; `cli` is the argv-driven door into the same
// implementations, used by the worker-backend's `git` custom
// command in the shell isolate.
//
-// Internally each method lazy-loads its optional peer deps
-// (`isomorphic-git`, the http transport, and for `diff` the
-// `diff` package) and delegates to `cloneWith` / `diffWith`. The
+// Internally each method lazy-loads its heavy deps
+// (`isomorphic-git`, the http transport, and `diff` for patches)
+// and delegates to `cloneWith` / `diffWith`. The
// loaders are memoised on the client so the dynamic imports fire
// once across the lifetime of the client — without that, the CLI
// would multiply the cost (re-importing isomorphic-git on every
@@ -315,7 +316,7 @@ export interface GitClient {
cli(input: GitCliInput): Promise;
}
-export interface CreateGitClientOptions {
+export interface WorkspaceGitClientOptions {
/** Workspace whose provider backs the git operations. */
ws: WorkspaceLike;
/**
@@ -325,6 +326,9 @@ export interface CreateGitClientOptions {
* `GIT_COMMITTER_*` env vars are absent.
*/
defaultIdentity?: GitIdentity;
+}
+
+export interface CreateGitClientOptions {
/**
* Test seam for substituting the @platformatic/vfs adapter.
* Production callers do not pass this.
@@ -332,6 +336,8 @@ export interface CreateGitClientOptions {
adapter?: (provider: SQLiteWorkspaceProvider) => Promise;
}
+export type GitClientFactory = (options: WorkspaceGitClientOptions) => GitClient;
+
/**
* Build a git client bound to a workspace.
*
@@ -353,344 +359,347 @@ export interface CreateGitClientOptions {
* or CLI) reuses the resolved modules.
*/
export function createGitClient({
- ws,
- defaultIdentity,
adapter = workspaceIsomorphicGitClient,
-}: CreateGitClientOptions): GitClient {
- let fsPromise: Promise | undefined;
- const fs = () => {
- if (!fsPromise) fsPromise = adapter(ws.provider());
- return fsPromise;
- };
- const cache: Record = {};
+}: CreateGitClientOptions = {}): GitClientFactory {
+ return function createWorkspaceGitClient({
+ ws,
+ defaultIdentity,
+ }: WorkspaceGitClientOptions): GitClient {
+ let fsPromise: Promise | undefined;
+ const fs = () => {
+ if (!fsPromise) fsPromise = adapter(ws.provider());
+ return fsPromise;
+ };
+ const cache: Record = {};
- // Memoised module loaders. Each holds the promise returned by
- // the dynamic import so concurrent first-use callers share one
- // import pass, and subsequent callers reuse the resolved module
- // synchronously through the cached promise.
- let gitPromise: Promise | undefined;
- let httpPromise: Promise