Skip to content

Commit 740c888

Browse files
committed
[Artifacts] refresh ArtifactFS and examples
1 parent b7b1ad5 commit 740c888

4 files changed

Lines changed: 332 additions & 22 deletions

File tree

src/content/docs/artifacts/api/artifactfs.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,29 @@ sidebar:
66
order: 4
77
---
88

9-
ArtifactFS mounts a git repo as a local filesystem without waiting for a full clone.
9+
ArtifactFS mounts a Git repository as a local filesystem without waiting for a full clone.
1010

11-
It uses a blobless clone, a Filesystem in Userspace (FUSE) driver, and background hydration to make large repos visible almost immediately.
11+
It exposes the full tree quickly, then hydrates file contents on demand and in the background as tools read them. This reduces startup time for large repositories.
1212

13-
ArtifactFS works with [Artifacts git remotes](/artifacts/api/git-protocol/) and other git repositories.
13+
ArtifactFS works with [Artifacts Git remotes](/artifacts/api/git-protocol/) and other Git repositories.
1414

15-
## Choose ArtifactFS
15+
## When to use
1616

17-
Use ArtifactFS for large, multi-gigabyte repos in sandboxes, containers, and virtual machines when startup time matters more than clone completeness.
17+
Use ArtifactFS when startup time matters more than a complete local clone. It is most useful for large, multi-gigabyte repos in sandboxes, containers, and virtual machines.
1818

19-
As a rule of thumb, for smaller repos below about `1 GB`, we recommend a regular `git clone`. It is simpler and usually fast enough without a FUSE mount.
19+
For smaller repos below about `1 GB`, use a regular `git clone`. It is simpler and usually fast enough without a FUSE mount.
2020

2121
ArtifactFS becomes more useful as repos get larger or contain very large object counts.
2222

23-
## Understand how it works
23+
## How it works
2424

2525
ArtifactFS starts with a blobless clone. It fetches commits, trees, and refs, but does not download file contents up front.
2626

2727
The daemon then mounts the repo through FUSE and exposes the full tree almost immediately. File contents hydrate asynchronously in the background as tools or agents read them.
2828

2929
Reads only block when a requested blob is not hydrated yet. Once hydrated, ArtifactFS serves that file from its local blob cache.
3030

31-
## Review hydration heuristics
31+
## File hydration algorithm
3232

3333
ArtifactFS prioritizes files that unblock agents first. That includes package and dependency manifests such as `package.json`, `go.mod`, `go.sum`, `Cargo.toml`, `pyproject.toml`, and `pnpm-lock.yaml`.
3434

@@ -38,7 +38,7 @@ Large binary assets such as images, archives, videos, and PDFs are deprioritized
3838

3939
These heuristics are intentionally simple. They focus on getting likely-to-be-read text and code in front of agents before large binary blobs.
4040

41-
## Try a repo
41+
## Example
4242

4343
The following example installs ArtifactFS, registers a public repo, starts the daemon, and reads files from the mounted working tree.
4444

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
---
2+
title: isomorphic-git
3+
description: Push commits to Artifacts repos from Workers.
4+
pcx_content_type: example
5+
sidebar:
6+
order: 2
7+
---
8+
9+
import { Details, PackageManagers, TypeScriptExample } from "~/components";
10+
11+
Use [isomorphic-git](https://isomorphic-git.org/) in a Cloudflare Worker when you need Git operations without a Git binary.
12+
13+
This works with Artifacts because Artifacts exposes standard Git smart HTTP remotes. In Workers, pair `isomorphic-git/http/web` with a small in-memory filesystem because the runtime does not expose a local disk.
14+
15+
## Install the dependency
16+
17+
Install `isomorphic-git` in your Worker project:
18+
19+
<PackageManagers pkg="isomorphic-git" />
20+
21+
## Example
22+
23+
This demo creates a new repo on each request, writes two files, commits them, and pushes `main` to the new remote.
24+
25+
Use this as a reference for the end-to-end flow. In a production Worker, look up or reuse an existing repo instead of creating a new one for every request.
26+
27+
<TypeScriptExample filename="src/index.ts" typescriptFirst={true}>
28+
29+
```ts
30+
import git from "isomorphic-git";
31+
import http from "isomorphic-git/http/web";
32+
import { MemoryFS } from "./memory-fs";
33+
34+
export interface Env {
35+
ARTIFACTS: Artifacts;
36+
}
37+
38+
export default {
39+
async fetch(_request: Request, env: Env) {
40+
const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`;
41+
const created = await env.ARTIFACTS.create(repoName);
42+
43+
// Artifacts returns art_v1_<secret>?expires=<unix_seconds>.
44+
// For Git Basic auth, pass only the secret as the password.
45+
const tokenSecret = created.token.split("?expires=")[0];
46+
const dir = "/workspace";
47+
const fs = new MemoryFS();
48+
49+
await git.init({ fs, dir, defaultBranch: "main" });
50+
51+
await fs.promises.writeFile(
52+
`${dir}/README.md`,
53+
"# Artifact repo created from a Worker\n",
54+
);
55+
await fs.promises.writeFile(
56+
`${dir}/src/index.ts`,
57+
'export const message = "hello from Artifacts";\n',
58+
);
59+
60+
await git.add({ fs, dir, filepath: "README.md" });
61+
await git.add({ fs, dir, filepath: "src/index.ts" });
62+
63+
const commit = await git.commit({
64+
fs,
65+
dir,
66+
message: "Create starter files",
67+
author: {
68+
name: "Artifacts example",
69+
email: "artifacts@example.com",
70+
},
71+
});
72+
73+
const push = await git.push({
74+
fs,
75+
http,
76+
dir,
77+
url: created.remote,
78+
ref: "main",
79+
onAuth: () => ({
80+
username: "x",
81+
password: tokenSecret,
82+
}),
83+
});
84+
85+
return Response.json({
86+
repo: created.name,
87+
remote: created.remote,
88+
commit,
89+
refs: push.refs,
90+
});
91+
},
92+
};
93+
```
94+
95+
</TypeScriptExample>
96+
97+
<Details header="In-memory filesystem helper">
98+
99+
Use this helper with `isomorphic-git` in Workers when you need a short-lived working tree in memory.
100+
101+
<TypeScriptExample filename="src/memory-fs.ts" typescriptFirst={true}>
102+
103+
```ts
104+
type Entry =
105+
| {
106+
kind: "dir";
107+
children: Set<string>;
108+
mtimeMs: number;
109+
}
110+
| {
111+
kind: "file";
112+
data: Uint8Array;
113+
mtimeMs: number;
114+
};
115+
116+
class MemoryStats {
117+
constructor(private entry: Entry) {}
118+
119+
get size() {
120+
return this.entry.kind === "file" ? this.entry.data.byteLength : 0;
121+
}
122+
123+
get mtimeMs() {
124+
return this.entry.mtimeMs;
125+
}
126+
127+
get ctimeMs() {
128+
return this.entry.mtimeMs;
129+
}
130+
131+
get mode() {
132+
return this.entry.kind === "file" ? 0o100644 : 0o040000;
133+
}
134+
135+
isFile() {
136+
return this.entry.kind === "file";
137+
}
138+
139+
isDirectory() {
140+
return this.entry.kind === "dir";
141+
}
142+
143+
isSymbolicLink() {
144+
return false;
145+
}
146+
}
147+
148+
export class MemoryFS {
149+
private encoder = new TextEncoder();
150+
private decoder = new TextDecoder();
151+
private entries = new Map<string, Entry>([
152+
["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }],
153+
]);
154+
155+
promises = {
156+
readFile: this.readFile.bind(this),
157+
writeFile: this.writeFile.bind(this),
158+
unlink: this.unlink.bind(this),
159+
readdir: this.readdir.bind(this),
160+
mkdir: this.mkdir.bind(this),
161+
rmdir: this.rmdir.bind(this),
162+
stat: this.stat.bind(this),
163+
lstat: this.lstat.bind(this),
164+
};
165+
166+
private normalize(input: string) {
167+
const segments: string[] = [];
168+
169+
for (const part of input.split("/")) {
170+
if (!part || part === ".") {
171+
continue;
172+
}
173+
174+
if (part === "..") {
175+
segments.pop();
176+
continue;
177+
}
178+
179+
segments.push(part);
180+
}
181+
182+
return `/${segments.join("/")}` || "/";
183+
}
184+
185+
private parent(path: string) {
186+
const normalized = this.normalize(path);
187+
if (normalized === "/") {
188+
return "/";
189+
}
190+
191+
const parts = normalized.split("/").filter(Boolean);
192+
parts.pop();
193+
return parts.length ? `/${parts.join("/")}` : "/";
194+
}
195+
196+
private basename(path: string) {
197+
return this.normalize(path).split("/").filter(Boolean).pop() ?? "";
198+
}
199+
200+
private getEntry(path: string) {
201+
return this.entries.get(this.normalize(path));
202+
}
203+
204+
private requireEntry(path: string) {
205+
const entry = this.getEntry(path);
206+
if (!entry) {
207+
throw new Error(`ENOENT: ${path}`);
208+
}
209+
210+
return entry;
211+
}
212+
213+
private requireDir(path: string) {
214+
const entry = this.requireEntry(path);
215+
if (entry.kind !== "dir") {
216+
throw new Error(`ENOTDIR: ${path}`);
217+
}
218+
219+
return entry;
220+
}
221+
222+
async mkdir(path: string, options?: { recursive?: boolean } | number) {
223+
const target = this.normalize(path);
224+
if (target === "/") {
225+
return;
226+
}
227+
228+
const recursive =
229+
typeof options === "object" && options !== null && options.recursive;
230+
const parent = this.parent(target);
231+
232+
if (!this.entries.has(parent)) {
233+
if (!recursive) {
234+
throw new Error(`ENOENT: ${parent}`);
235+
}
236+
237+
await this.mkdir(parent, { recursive: true });
238+
}
239+
240+
if (this.entries.has(target)) {
241+
return;
242+
}
243+
244+
this.entries.set(target, {
245+
kind: "dir",
246+
children: new Set(),
247+
mtimeMs: Date.now(),
248+
});
249+
250+
this.requireDir(parent).children.add(this.basename(target));
251+
}
252+
253+
async writeFile(path: string, data: string | Uint8Array | ArrayBuffer) {
254+
const target = this.normalize(path);
255+
await this.mkdir(this.parent(target), { recursive: true });
256+
257+
const bytes =
258+
typeof data === "string"
259+
? this.encoder.encode(data)
260+
: data instanceof Uint8Array
261+
? data
262+
: new Uint8Array(data);
263+
264+
this.entries.set(target, {
265+
kind: "file",
266+
data: bytes,
267+
mtimeMs: Date.now(),
268+
});
269+
270+
this.requireDir(this.parent(target)).children.add(this.basename(target));
271+
}
272+
273+
async readFile(path: string, options?: string | { encoding?: string }) {
274+
const entry = this.requireEntry(path);
275+
if (entry.kind !== "file") {
276+
throw new Error(`EISDIR: ${path}`);
277+
}
278+
279+
const encoding = typeof options === "string" ? options : options?.encoding;
280+
return encoding ? this.decoder.decode(entry.data) : entry.data;
281+
}
282+
283+
async readdir(path: string) {
284+
return [...this.requireDir(path).children].sort();
285+
}
286+
287+
async unlink(path: string) {
288+
const target = this.normalize(path);
289+
const entry = this.requireEntry(target);
290+
if (entry.kind !== "file") {
291+
throw new Error(`EISDIR: ${path}`);
292+
}
293+
294+
this.entries.delete(target);
295+
this.requireDir(this.parent(target)).children.delete(this.basename(target));
296+
}
297+
298+
async rmdir(path: string) {
299+
const target = this.normalize(path);
300+
const entry = this.requireDir(target);
301+
if (entry.children.size > 0) {
302+
throw new Error(`ENOTEMPTY: ${path}`);
303+
}
304+
305+
this.entries.delete(target);
306+
this.requireDir(this.parent(target)).children.delete(this.basename(target));
307+
}
308+
309+
async stat(path: string) {
310+
return new MemoryStats(this.requireEntry(path));
311+
}
312+
313+
async lstat(path: string) {
314+
return this.stat(path);
315+
}
316+
}
317+
```
318+
319+
</TypeScriptExample>
320+
321+
</Details>

src/content/docs/artifacts/examples/opencode-plugin.mdx

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/content/docs/artifacts/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Artifacts is in beta. Use Artifacts to persist file trees such as repositories,
3232
</LinkTitleCard>
3333

3434
<LinkTitleCard title="Concepts" href="/artifacts/concepts/" icon="document">
35-
Learn how Artifacts works and how to structure Artifact usage.
35+
Learn how Artifacts works and how to structure repository workflows.
3636
</LinkTitleCard>
3737

3838
<LinkTitleCard title="API" href="/artifacts/api/" icon="document">
@@ -48,7 +48,7 @@ Artifacts is in beta. Use Artifacts to persist file trees such as repositories,
4848
</LinkTitleCard>
4949

5050
<LinkTitleCard title="Examples" href="/artifacts/examples/" icon="document">
51-
See example integrations with git clients, OpenCode, and Sandbox SDK.
51+
See example integrations with Git clients, isomorphic-git, and Sandbox SDK.
5252
</LinkTitleCard>
5353

5454
<LinkTitleCard title="Platform" href="/artifacts/platform/" icon="document">

0 commit comments

Comments
 (0)