Workflow for editing space ""

The loop

  1. Read. state.glob("**/*"), state.readFile, state.searchText.
  2. Plan. Multi-file edits: state.planEdits + state.applyEdits.
  3. Edit. state.writeFile, state.replaceInFile.
  4. Commit. git.add({ filepath: ".", dir: "/" }) + git.commit({ message, author, dir: "/" }).
  5. Build. host.build() (inside run_code). Fix warnings.
  6. Preview (optional). Branch off main (git.branch then git.checkout), commit, host.deploy() → preview URL (read it from the deploy response).
  7. Ship. Back on main (git.checkout({ ref: "main" })), merge the feature branch (git.merge({ theirs })) or commit directly, then host.deploy().

Which branch you're on decides the deploy target — check with git.branch({ list: true }), which returns { branches, current }.

Refusals:

  • DIRTY_TREE — uncommitted changes. Stage + commit before building.
  • NO_COMMITS — empty repo. Make an initial commit.
  • DETACHED_HEAD — checkout a branch first.

Elaborate example: stateful counter

This builds a tiny stateful Worker that persists a counter across requests using the App DO's KV storage (see dynamic-worker for capabilities). The whole flow happens through one run_code call.

async () => {
  // 1. Write the worker source. Named `App` export — required by the bundler
  //    (see bundling: unfolder://docs/bundling).
  await state.writeFile(
    "/src/index.ts",
    `import { DurableObject } from "cloudflare:workers";

export class App extends DurableObject {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/reset") {
      this.ctx.storage.kv.put("counter", 0);
      return new Response("reset");
    }
    let counter = this.ctx.storage.kv.get("counter") ?? 0;
    counter++;
    this.ctx.storage.kv.put("counter", counter);
    return new Response("count=" + counter + " path=" + url.pathname);
  }
}
`,
  );

  // 2. Commit on main.
  await git.add({ filepath: ".", dir: "/" });
  const { oid } = await git.commit({
    message: "feat: counter app",
    author: { name: "Agent", email: "agent@unfolder.space" },
    dir: "/",
  });

  return { committed: oid };
};

Then, in run_code (build is optional — deploy always builds first):

async () => host.build()    // → { commitSha, mainModule, moduleCount, ... }
async () => host.deploy()   // → { target: "production", url: ".../" }

A request to https://<your-space-slug>.unfolder.space/hello returns count=1 path=/hello, and the second request returns count=2. Production facets preserve the App DO's SQLite across redeploys, so the counter survives redeploys of main.

Reading and exploring

state.* (exposed inside run_code) is the full file API. drop into run_code and call the workspace primitives directly. To preserve template literals (e.g. ${name}) verbatim in a string you're writing, use write_files instead — those bytes are not JS-evaluated.

// Walk the workspace (nested tree, max depth 3)
async () => state.walkTree("/", { maxDepth: 3 })

// Find files by pattern
async () => state.glob("src/**/*.{ts,tsx}")

// One-level listing
async () => state.readdirWithFileTypes("/src")

// Read a text file
async () => state.readFile("/src/index.ts")

// Read a binary file as base64 (MCP transport is JSON; pick an encoding)
async () => {
  const bytes = await state.readFileBytes("/photo.png");
  return { encoding: "base64", contents: btoa(String.fromCharCode(...bytes)) };
}

// Search content across files
async () => state.searchText("/", "TODO")

// Aggregate counters — also exposed on the `unfolder://spaces/<your-space-slug>`
// resource as `workspaceInfo` (no tool call needed).
async () => state.summarizeTree("/")

Preview branch flow

async () => {
  await git.branch({ name: "feat-x", dir: "/" });
  await git.checkout({ ref: "feat-x", dir: "/" });
  await state.replaceInFile("/src/index.ts", "count=", "preview-count=");
  await git.add({ filepath: ".", dir: "/" });
  return await git.commit({
    message: "preview tweak",
    author: { name: "Agent", email: "agent@unfolder.space" },
    dir: "/",
  });
};
// then host.deploy() — read the preview URL from the deploy response:
// https://<your-space-slug>--{previewId}--preview.unfolder.space  (previewId is a
// generated id, NOT the branch name — don't construct the URL yourself).

Preview facets reset SQLite per redeploy, so don't rely on persisted state inside a preview when iterating. To promote a previewed change to production, check out main and git.merge({ theirs: "feat-x" }), then deploy.