Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/13_git_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -174,8 +177,8 @@ committer in this order:
by `git config user.email "..."`. Only the local
`<dir>/.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
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down
4 changes: 0 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions packages/computer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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" });
Expand Down
4 changes: 0 additions & 4 deletions packages/computer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"peerDependencies": {
"@platformatic/vfs": "*",
"ai": "^6.0.196 || ^7.0.0",
"isomorphic-git": "^1.27.0",
"zod": "^4.4.3"
},
"peerDependenciesMeta": {
Expand All @@ -89,9 +88,6 @@
"ai": {
"optional": true
},
"isomorphic-git": {
"optional": true
},
"zod": {
"optional": true
}
Expand Down
13 changes: 7 additions & 6 deletions packages/computer/rolldown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,18 +42,16 @@ export default defineConfig({
"@platformatic/vfs",
"ai",
"zod",
"isomorphic-git",
/^isomorphic-git\//,
"just-bash",
"node:crypto",
"node:events",
/^node:/,
],
resolve: {
alias: {
"@cloudflare/dofs": resolve(here, "../dofs/src/index.ts"),
"@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,
Expand Down
2 changes: 2 additions & 0 deletions packages/computer/src/backends/worker/entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/computer/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,9 @@ export async function getWorkspace(handle: WorkspaceHandle): Promise<WorkspaceCl
{
fs: local.fs,
runtime: local.runtime,
git: local.git,
get git() {
return local.git;
},
artifacts: local.artifacts,
assets: local.assets,
},
Expand Down
19 changes: 12 additions & 7 deletions packages/computer/src/git/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1953,7 +1953,7 @@ describe("runGitCli — clean argv parsing", () => {

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
Expand Down Expand Up @@ -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" },
});
Expand Down Expand Up @@ -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" },
});
Expand Down Expand Up @@ -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" },
});
Expand Down Expand Up @@ -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"]);
Expand All @@ -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" },
});
Expand Down Expand Up @@ -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" },
});
Expand All @@ -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" },
});
Expand All @@ -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");
Expand All @@ -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: "/" });

Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/computer/src/git/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading