Skip to content

Commit e02e93c

Browse files
authored
computer: Make git opt-in (#32)
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.
1 parent 62271ad commit e02e93c

19 files changed

Lines changed: 754 additions & 409 deletions

docs/13_git_interface.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
> `packages/computer/src/backends/worker/`. Everything below
77
> works today.
88
9-
`workspace.git` is a major typed surface on `Workspace`, alongside `fs`, `runtime`, Assets, and Artifacts. It runs every operation against the
10-
local SQLite-backed VFS through `isomorphic-git`, so a
9+
`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
1110
filesystem-only workspace (no backend) can drive a full
12-
clone/commit/diff cycle.
11+
clone/commit/diff cycle. The git subpath bundles `isomorphic-git`
12+
lazily and replaces its `pako` dependency with a small
13+
`node:zlib` shim for Workers running with `nodejs_compat`; the
14+
default `@cloudflare/computer` graph stays free of git.
1315

1416
Two doors into the same implementation:
1517

@@ -111,8 +113,9 @@ diff against HEAD — through each entry point.
111113

112114
```ts
113115
import { Workspace } from "@cloudflare/computer";
116+
import { createGitClient } from "@cloudflare/computer/git";
114117

115-
const ws = new Workspace({ storage: ctx.storage });
118+
const ws = new Workspace({ storage: ctx.storage, git: createGitClient() });
116119
await ws.git.clone({ url: "https://github.com/example/repo.git" });
117120
await ws.fs.writeFile("/README.md", "hello world\n");
118121
const patch = await ws.git.diff();
@@ -174,8 +177,8 @@ committer in this order:
174177
by `git config user.email "..."`. Only the local
175178
`<dir>/.git/config` is consulted; there is no global
176179
`~/.gitconfig` fallback.
177-
4. `defaultIdentity` from `createGitClient` / `new Workspace({
178-
defaultGitIdentity })`.
180+
4. `defaultIdentity` from `createGitClient()` / `new Workspace({
181+
git: createGitClient(), defaultGitIdentity })`.
179182

180183
If none of the four yields a name and email,
181184
`MissingIdentityError` fires. The CLI surfaces it as `git

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The package ships several entrypoints:
4646
| `@cloudflare/computer/backends/container` | `CloudflareContainerBackend` and `withWorkspaceContainer`. Pulls in the computerd / capnweb sync plumbing. |
4747
| `@cloudflare/computer/backends/worker` | `WorkerBackend` and the bundled just-bash command runtime. |
4848
| `@cloudflare/computer/backends/javascript` | `IsolateJavaScriptBackend`, configured libraries, durable relative imports, `node:fs/promises`, and trusted `ws:git` / `ws:artifacts`. |
49-
| `@cloudflare/computer/git` | Isomorphic-git glue for working with checkouts inside the workspace. |
49+
| `@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. |
5050
| `@cloudflare/computer/artifacts` | `createArtifact`, a session-scoped facade over the Cloudflare Artifacts Workers binding, plus its argv CLI. |
5151
| `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. |
5252

package-lock.json

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/computer/README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,17 @@ uniform; the counts are just always zero.
5656
surface: `exec`, `getExec`, `killExec`, and `disposeExec`. The selected
5757
backend defines the source language. JavaScript results may include a
5858
structured `value`; command backends return stdout/stderr and an exit code.
59-
- `workspace.git` — a typed git client backed by
60-
`isomorphic-git` against the local SQLite VFS. Surfaces both a
61-
TypeScript API (`workspace.git.clone({ url })`) and an
62-
argv-driven entry point (`workspace.git.cli({ argv })`). The
63-
worker backend's shell exposes the same dispatcher through a
64-
built-in `git` custom command. See
59+
- `workspace.git` — an opt-in typed git client backed by
60+
`isomorphic-git` against the local SQLite VFS. Pass
61+
`createGitClient()` from `@cloudflare/computer/git` as
62+
`WorkspaceOptions.git` to enable both the TypeScript API
63+
(`workspace.git.clone({ url })`) and the argv-driven entry point
64+
(`workspace.git.cli({ argv })`). The git subpath bundles
65+
`isomorphic-git` lazily and replaces its `pako` dependency with
66+
the Workers `node:zlib` implementation, so the default package
67+
graph stays free of git. The worker backend's shell exposes the
68+
same dispatcher through a built-in `git` custom command when git
69+
is configured. See
6570
[`docs/13_git_interface.md`](../../docs/13_git_interface.md).
6671
- `createAssets` (from `@cloudflare/computer/assets`) — `share` a
6772
workspace file to an R2 bucket and get back a presigned URL.
@@ -158,8 +163,11 @@ and `workspace.runtime` as the primary surfaces.
158163
Git, also without a backend:
159164

160165
```ts
166+
import { createGitClient } from "@cloudflare/computer/git";
167+
161168
const ws = new Workspace({
162169
storage: ctx.storage,
170+
git: createGitClient(),
163171
defaultGitIdentity: { name: "Agent", email: "agent@example.test" },
164172
});
165173
await ws.git.clone({ url: "https://github.com/example/repo.git" });

packages/computer/package.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
"peerDependencies": {
8080
"@platformatic/vfs": "*",
8181
"ai": "^6.0.196 || ^7.0.0",
82-
"isomorphic-git": "^1.27.0",
8382
"zod": "^4.4.3"
8483
},
8584
"peerDependenciesMeta": {
@@ -89,9 +88,6 @@
8988
"ai": {
9089
"optional": true
9190
},
92-
"isomorphic-git": {
93-
"optional": true
94-
},
9591
"zod": {
9692
"optional": true
9793
}

packages/computer/rolldown.config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
// - capnweb — a regular npm dep on this package.
99
// - @platformatic/vfs — optional userland import used by
1010
// examples but not the workspace core.
11-
// `node:*` builtins are externalised automatically because
12-
// `platform: "node"` is the default for `esm` output here.
11+
// - node:* — provided by nodejs_compat in workerd.
12+
//
13+
// The git entrypoint bundles isomorphic-git, but aliases pako to a
14+
// tiny node:zlib-backed compatibility layer so Workers don't carry
15+
// pako's JavaScript zlib implementation.
1316

1417
import { resolve } from "node:path";
1518
import { fileURLToPath } from "node:url";
@@ -39,18 +42,16 @@ export default defineConfig({
3942
"@platformatic/vfs",
4043
"ai",
4144
"zod",
42-
"isomorphic-git",
43-
/^isomorphic-git\//,
4445
"just-bash",
45-
"node:crypto",
46-
"node:events",
46+
/^node:/,
4747
],
4848
resolve: {
4949
alias: {
5050
"@cloudflare/dofs": resolve(here, "../dofs/src/index.ts"),
5151
"@cloudflare/dofs/testing": resolve(here, "../dofs/src/testing.ts"),
5252
"@cloudflare/computer-rpc": resolve(here, "../rpc/src/index.ts"),
5353
"@cloudflare/computer-rpc/driver": resolve(here, "../rpc/src/sync-driver.ts"),
54+
pako: resolve(here, "src/git/pako-zlib-shim.ts"),
5455
},
5556
},
5657
// ESM only. Nothing in-tree loads CJS — the example Worker, computerd,

packages/computer/src/backends/worker/entrypoint.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
1919
import { FakeArtifactsBinding } from "../../../tests/utilities/fake-artifacts-binding.js";
2020
import type { ArtifactsCLIInput, ArtifactsCLIResult } from "../../artifacts/index.js";
2121
import type { BackendHandle, WorkspaceBackend } from "../../backend.js";
22+
import { createGitClient } from "../../git/index.js";
2223
import type { WorkspaceStub } from "../../stub.js";
2324
import { Workspace } from "../../workspace.js";
2425
import { ShellWorker } from "./entrypoint.js";
@@ -268,6 +269,7 @@ describe("ShellWorker", () => {
268269
workspace = new Workspace({
269270
storage: new SQLiteTestStorage() as never,
270271
backends: [noopBackend()],
272+
git: createGitClient(),
271273
});
272274
await workspace.ready();
273275
// /workspace is the ShellWorker's default cwd; create it

packages/computer/src/client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,9 @@ export async function getWorkspace(handle: WorkspaceHandle): Promise<WorkspaceCl
372372
{
373373
fs: local.fs,
374374
runtime: local.runtime,
375-
git: local.git,
375+
get git() {
376+
return local.git;
377+
},
376378
artifacts: local.artifacts,
377379
assets: local.assets,
378380
},

packages/computer/src/git/cli.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,7 +1953,7 @@ describe("runGitCli — clean argv parsing", () => {
19531953

19541954
describe("runGitCli — end-to-end against an in-process Workspace", () => {
19551955
it("diff prints the working-tree delta against HEAD", async () => {
1956-
const ws = new Workspace({ storage: new SQLiteTestStorage() });
1956+
const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
19571957
await ws.ready();
19581958

19591959
// Seed a repo with one committed file, then mutate the
@@ -1987,6 +1987,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
19871987
// any subcommand drifts from the typed surface, the chain
19881988
// breaks here rather than in a downstream consumer.
19891989
const ws = new Workspace({
1990+
git: createGitClient(),
19901991
storage: new SQLiteTestStorage(),
19911992
defaultGitIdentity: { name: "Test", email: "test@example.test" },
19921993
});
@@ -2030,6 +2031,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
20302031

20312032
it("log / show / rev-parse / ls-files round-trip", async () => {
20322033
const ws = new Workspace({
2034+
git: createGitClient(),
20332035
storage: new SQLiteTestStorage(),
20342036
defaultGitIdentity: { name: "Test", email: "test@example.test" },
20352037
});
@@ -2065,6 +2067,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
20652067

20662068
it("hash-object / cat-file / update-ref / config round-trip", async () => {
20672069
const ws = new Workspace({
2070+
git: createGitClient(),
20682071
storage: new SQLiteTestStorage(),
20692072
defaultGitIdentity: { name: "Test", email: "test@example.test" },
20702073
});
@@ -2104,7 +2107,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
21042107
});
21052108

21062109
it("remote add / list / remove round-trip through the config file", async () => {
2107-
const ws = new Workspace({ storage: new SQLiteTestStorage() });
2110+
const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
21082111
await ws.ready();
21092112
const cli = (argv: string[]) => ws.git.cli({ argv, cwd: "/" });
21102113
await cli(["init"]);
@@ -2128,6 +2131,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
21282131

21292132
it("branch / checkout / tag round-trip moves HEAD and creates refs", async () => {
21302133
const ws = new Workspace({
2134+
git: createGitClient(),
21312135
storage: new SQLiteTestStorage(),
21322136
defaultGitIdentity: { name: "Test", email: "test@example.test" },
21332137
});
@@ -2162,6 +2166,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
21622166

21632167
it("switch restores tracked file content from the target branch", async () => {
21642168
const ws = new Workspace({
2169+
git: createGitClient(),
21652170
storage: new SQLiteTestStorage(),
21662171
defaultGitIdentity: { name: "Test", email: "test@example.test" },
21672172
});
@@ -2184,6 +2189,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
21842189

21852190
it("reset HEAD unstages all staged changes", async () => {
21862191
const ws = new Workspace({
2192+
git: createGitClient(),
21872193
storage: new SQLiteTestStorage(),
21882194
defaultGitIdentity: { name: "Test", email: "test@example.test" },
21892195
});
@@ -2205,7 +2211,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
22052211
});
22062212

22072213
it("commit without identity surfaces as exit 128", async () => {
2208-
const ws = new Workspace({ storage: new SQLiteTestStorage() });
2214+
const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
22092215
await ws.ready();
22102216
await ws.git.cli({ argv: ["init"], cwd: "/" });
22112217
await ws.fs.writeFile("/a.txt", "x\n");
@@ -2221,7 +2227,7 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
22212227
// would drive it: configure identity, stage with -A, commit
22222228
// with -am, inspect with the new flags, branch with switch
22232229
// -c, then reset / stash / clean.
2224-
const ws = new Workspace({ storage: new SQLiteTestStorage() });
2230+
const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
22252231
await ws.ready();
22262232
const cli = (argv: string[]) => ws.git.cli({ argv, cwd: "/" });
22272233

@@ -2299,18 +2305,17 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
22992305
// Force the clone path to fail by pointing at an invalid host;
23002306
// we want to pin that the dispatcher's catch arm produces a
23012307
// CLI-shaped result and doesn't propagate the rejection.
2302-
const ws = new Workspace({ storage: new SQLiteTestStorage() });
2308+
const ws = new Workspace({ git: createGitClient(), storage: new SQLiteTestStorage() });
23032309
await ws.ready();
23042310
// Swap the git client out for one whose clone rejects, so we
23052311
// don't depend on network reachability inside the test runner.
23062312
const failing: GitClient = createGitClient({
2307-
ws,
23082313
adapter: async () => ({
23092314
promises: {
23102315
readFile: vi.fn(async () => new Uint8Array()),
23112316
},
23122317
}),
2313-
});
2318+
})({ ws });
23142319
// Replace `clone` with a deterministic failure — the real
23152320
// path is exercised by `clone.test.ts`.
23162321
(failing as { clone: GitClient["clone"] }).clone = async () => {

packages/computer/src/git/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe("createGitClient", () => {
3030
const fs = stubFs();
3131
const adapter = vi.fn(async () => fs);
3232

33-
const client = createGitClient({ ws: { provider }, adapter });
33+
const client = createGitClient({ adapter })({ ws: { provider } });
3434

3535
// No work happens at construction time.
3636
expect(provider).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)