@fungi.computer/bonsai 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/backend/background.ts +259 -0
- package/backend/change-copy.ts +99 -0
- package/backend/changes.ts +530 -0
- package/backend/diff.ts +228 -0
- package/backend/file-edits.ts +850 -0
- package/backend/files-copy.ts +361 -0
- package/backend/gills.ts +125 -0
- package/backend/git.ts +123 -0
- package/backend/graph-commit-copies.ts +321 -0
- package/backend/history.ts +178 -0
- package/backend/index.ts +144 -0
- package/backend/insights.ts +256 -0
- package/backend/merge.ts +999 -0
- package/backend/methods.ts +848 -0
- package/backend/policy.ts +160 -0
- package/backend/read-copy-page.ts +77 -0
- package/backend/repository-copy.ts +70 -0
- package/backend/reviews.ts +317 -0
- package/backend/service.ts +892 -0
- package/backend/state.ts +914 -0
- package/demo/koi-pond/README.md +17 -0
- package/demo/koi-pond/index.html +11 -0
- package/demo/koi-pond/package.json +13 -0
- package/demo/koi-pond/pnpm-lock.yaml +450 -0
- package/demo/koi-pond/src/fish.ts +32 -0
- package/demo/koi-pond/src/main.ts +24 -0
- package/demo/koi-pond/src/pond.ts +27 -0
- package/docs/media/bonsai-insights.png +0 -0
- package/docs/media/overview-phone.png +0 -0
- package/docs/media/overview.png +0 -0
- package/package.json +152 -3
- package/scripts/agent.sh +55 -0
- package/scripts/demo.sh +233 -0
- package/scripts/rebuild-copies.mjs +34 -0
- package/scripts/release.mjs +52 -0
- package/scripts/runtime-proof.mjs +299 -0
- package/scripts/seed-demo.mjs +724 -0
- package/scripts/seed-step.mjs +37 -0
- package/scripts/shed-terminal-proof.mjs +774 -0
- package/shared/contract.ts +404 -0
- package/shared/hooks.ts +208 -0
- package/shared/insights.ts +90 -0
- package/shared/picker-options.ts +23 -0
- package/shared/presentation.ts +21 -0
- package/shared/repository-deletion.ts +4 -0
- package/test/apps-build-install.test.ts +302 -0
- package/test/background-runtime.test.ts +206 -0
- package/test/browser/board.browser.mjs +52 -0
- package/test/browser/capture.browser.mjs +1264 -0
- package/test/browser/edit-in-inkcap.browser.mjs +83 -0
- package/test/browser/repository-settings-fixture.tsx +108 -0
- package/test/browser/repository-settings.browser.mjs +136 -0
- package/test/browser/repository-settings.html +10 -0
- package/test/browser/visual-fixture.tsx +667 -0
- package/test/browser/visual.html +10 -0
- package/test/browser/vite.config.mjs +9 -0
- package/test/change-page.test.ts +916 -0
- package/test/changes.test.ts +1063 -0
- package/test/checks-workflow-template.test.mjs +234 -0
- package/test/diff.test.ts +70 -0
- package/test/edit-in-inkcap.test.ts +227 -0
- package/test/file-edits.test.ts +2714 -0
- package/test/files-read-copy-calls.test.ts +374 -0
- package/test/fork-adoption.test.ts +111 -0
- package/test/gills.test.ts +125 -0
- package/test/git-binding.fixture.ts +30 -0
- package/test/git-push.config.jsonc +9 -0
- package/test/git-push.native.mjs +413 -0
- package/test/git-push.worker.ts +280 -0
- package/test/graph-commit-read-copy-calls.test.ts +401 -0
- package/test/home-page.test.ts +170 -0
- package/test/hooks.test.ts +250 -0
- package/test/insight-data.test.ts +51 -0
- package/test/insight-layout.test.mjs +230 -0
- package/test/insights.test.ts +154 -0
- package/test/merge-background.test.ts +756 -0
- package/test/merge-conflict.test.ts +337 -0
- package/test/merge-declaration.test.ts +46 -0
- package/test/merge-repro.test.ts +561 -0
- package/test/merge-template-build.test.mjs +234 -0
- package/test/network-layout.test.ts +200 -0
- package/test/outbox.test.ts +424 -0
- package/test/parts.test.ts +86 -0
- package/test/pkt-line.test.ts +54 -0
- package/test/policy.test.ts +172 -0
- package/test/present.test.ts +27 -0
- package/test/read-copy-calls.test.ts +691 -0
- package/test/read-copy-composer.test.ts +388 -0
- package/test/read-copy-lifetimes.test.ts +214 -0
- package/test/read-copy-rpc.config.jsonc +12 -0
- package/test/read-copy-rpc.native.mjs +47 -0
- package/test/read-copy-rpc.worker.ts +92 -0
- package/test/recent-activity.test.ts +62 -0
- package/test/repository-deletion.test.ts +500 -0
- package/test/seed-resume.test.mjs +91 -0
- package/test/storyboard-save-https-git.fixture.mjs +119 -0
- package/test/storyboard-save-template.test.mjs +545 -0
- package/test/ui-errors.test.ts +976 -0
- package/test/workflow-store.test.ts +115 -0
- package/tsconfig.json +36 -0
- package/turbo.jsonc +25 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +47 -0
- package/web/api.ts +224 -0
- package/web/app-build-panel.tsx +171 -0
- package/web/board.tsx +259 -0
- package/web/bonsai.css +642 -0
- package/web/change-page.tsx +719 -0
- package/web/change-rows.tsx +135 -0
- package/web/code-tab.tsx +267 -0
- package/web/commit-tree.tsx +152 -0
- package/web/format.ts +54 -0
- package/web/home-page.tsx +274 -0
- package/web/index.html +17 -0
- package/web/insight-data.ts +76 -0
- package/web/insight-tiles.tsx +534 -0
- package/web/insights.tsx +250 -0
- package/web/issues-panel.tsx +305 -0
- package/web/main.tsx +72 -0
- package/web/network-graph.tsx +647 -0
- package/web/overview.tsx +339 -0
- package/web/pages.tsx +469 -0
- package/web/parts.tsx +381 -0
- package/web/public/favicon.svg +1 -0
- package/web/repo-navigation.tsx +256 -0
- package/web/repo-settings.tsx +145 -0
- package/web/router.tsx +151 -0
- package/web/transport.ts +91 -0
- package/web/views.tsx +151 -0
- package/web/working-now.tsx +98 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prove Bonsai end to end on a local runtime: start the standalone Oyster
|
|
3
|
+
// with `wrangler dev --config ../../packages/oyster/deploy/wrangler.celld.jsonc` (Artifacts over REST),
|
|
4
|
+
// install Bonsai, create a repository, push, fork, push to the fork, merge,
|
|
5
|
+
// and read the graph. Run it plain for workerd, or under celld:
|
|
6
|
+
//
|
|
7
|
+
// CELLD_PLATFORM_CELLD=<celld> node --import \
|
|
8
|
+
// ../../tooling/celld-platform/runtime/wrangler-on-celld.mjs scripts/runtime-proof.mjs
|
|
9
|
+
//
|
|
10
|
+
// Needs ARTIFACTS_ACCOUNT_ID and ARTIFACTS_API_TOKEN (Artifacts read/write).
|
|
11
|
+
// Uses the namespace bonsai-proof and deletes its repositories afterwards.
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
16
|
+
import { createServer } from "node:net";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { installRelease, packRelease } from "@fungi.computer/oyster/pack";
|
|
21
|
+
|
|
22
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
23
|
+
const repository = fileURLToPath(new URL("../../../", import.meta.url));
|
|
24
|
+
const accountId = process.env.ARTIFACTS_ACCOUNT_ID;
|
|
25
|
+
const apiToken = process.env.ARTIFACTS_API_TOKEN;
|
|
26
|
+
if (!accountId || !apiToken)
|
|
27
|
+
throw new Error("Set ARTIFACTS_ACCOUNT_ID and ARTIFACTS_API_TOKEN");
|
|
28
|
+
const namespace = "bonsai-proof";
|
|
29
|
+
const runtime = process.env.CELLD_PLATFORM_CELLD ? "celld" : "workerd";
|
|
30
|
+
const work = await mkdtemp(join(tmpdir(), "bonsai-proof-"));
|
|
31
|
+
const key = randomBytes(24).toString("hex");
|
|
32
|
+
const repo = `proof-${Date.now().toString(36)}`;
|
|
33
|
+
|
|
34
|
+
const port = await new Promise((resolve) => {
|
|
35
|
+
const server = createServer().listen(0, "127.0.0.1", () => {
|
|
36
|
+
const { port: free } = server.address();
|
|
37
|
+
server.close(() => resolve(free));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
41
|
+
|
|
42
|
+
const revision = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
43
|
+
cwd: repository,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
}).trim();
|
|
46
|
+
|
|
47
|
+
await packRelease({
|
|
48
|
+
appId: "app_bonsai",
|
|
49
|
+
releaseId: "rel_proof",
|
|
50
|
+
sourceDirectory: repository,
|
|
51
|
+
sourceRevision: `git:${revision}`,
|
|
52
|
+
packageStoreDirectory: execFileSync("pnpm", ["store", "path"], {
|
|
53
|
+
cwd: repository,
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
}).trim(),
|
|
56
|
+
recipe: {
|
|
57
|
+
entry: "apps/bonsai/web/index.html",
|
|
58
|
+
assets: {
|
|
59
|
+
maxManifestBytes: 1_048_576,
|
|
60
|
+
maxObjects: 1024,
|
|
61
|
+
maxObjectBytes: 16_777_216,
|
|
62
|
+
maxTotalBytes: 67_108_864,
|
|
63
|
+
},
|
|
64
|
+
assetBase: "./",
|
|
65
|
+
maxOutputBytes: 67_108_864,
|
|
66
|
+
backend: {
|
|
67
|
+
entry: "apps/bonsai/backend/index.ts",
|
|
68
|
+
exportName: "BonsaiApp",
|
|
69
|
+
compatibilityDate: "2026-09-04",
|
|
70
|
+
runtime: { kind: "durable-facet", stateSchema: 1 },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
outDirectory: `${work}/release`,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const vars = {
|
|
77
|
+
OYSTER_OWNER_KEY: key,
|
|
78
|
+
OYSTER_ORIGIN: origin,
|
|
79
|
+
ARTIFACTS_ACCOUNT_ID: accountId,
|
|
80
|
+
ARTIFACTS_NAMESPACE: namespace,
|
|
81
|
+
ARTIFACTS_API_TOKEN: apiToken,
|
|
82
|
+
};
|
|
83
|
+
const wrangler = spawn(
|
|
84
|
+
`${root}../../node_modules/.bin/wrangler`,
|
|
85
|
+
[
|
|
86
|
+
"dev",
|
|
87
|
+
"--config",
|
|
88
|
+
`${root}../../packages/oyster/deploy/wrangler.celld.jsonc`,
|
|
89
|
+
"--local",
|
|
90
|
+
"--ip",
|
|
91
|
+
"127.0.0.1",
|
|
92
|
+
"--port",
|
|
93
|
+
String(port),
|
|
94
|
+
"--persist-to",
|
|
95
|
+
`${work}/state`,
|
|
96
|
+
// --var, not an env file: the celld wrapper merges --vars into its project.
|
|
97
|
+
...Object.entries(vars).flatMap(([name, value]) => [
|
|
98
|
+
"--var",
|
|
99
|
+
`${name}:${value}`,
|
|
100
|
+
]),
|
|
101
|
+
],
|
|
102
|
+
{ cwd: root, stdio: ["ignore", "pipe", "pipe"], env: process.env },
|
|
103
|
+
);
|
|
104
|
+
let output = "";
|
|
105
|
+
wrangler.stdout.on("data", (chunk) => (output += chunk));
|
|
106
|
+
wrangler.stderr.on("data", (chunk) => (output += chunk));
|
|
107
|
+
const stopped = new Promise((resolve) => wrangler.once("exit", resolve));
|
|
108
|
+
|
|
109
|
+
const owner = { Authorization: `Bearer ${key}` };
|
|
110
|
+
const call = async (method, input, headers = owner, signal) => {
|
|
111
|
+
const response = await fetch(`${origin}/apps/bonsai/methods/${method}`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify(input),
|
|
115
|
+
signal,
|
|
116
|
+
});
|
|
117
|
+
const body = await response.json();
|
|
118
|
+
assert.ok(response.ok, `${method}: ${JSON.stringify(body)}`);
|
|
119
|
+
return body;
|
|
120
|
+
};
|
|
121
|
+
const git = (args, cwd) =>
|
|
122
|
+
execFileSync("git", args, {
|
|
123
|
+
cwd,
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// One changes.merge call, then bounded read-only observation of that exact
|
|
129
|
+
// pinned Change through the existing contract (changes.get) until it
|
|
130
|
+
// reaches a terminal state FOR THE EXACT HEAD PROPOSED: merged, or closed,
|
|
131
|
+
// moved to a different head, or a recorded mergeFailure -- all of which
|
|
132
|
+
// are failures, never treated as still-pending (same law
|
|
133
|
+
// apps/bonsai/test/webhooks-issues.native.mjs's waitForMergeOutcome uses).
|
|
134
|
+
// changes.get can itself answer {kind:"rebuilding"}
|
|
135
|
+
// (apps/bonsai/backend/methods.ts:446); that is not terminal either way,
|
|
136
|
+
// so it is read the same as "keep polling". The owner key here is
|
|
137
|
+
// background-eligible (packages/puffball/src/background-replay.ts
|
|
138
|
+
// mayEnqueueBackground), so changes.merge normally answers
|
|
139
|
+
// {kind:"merging"} and settles later; this never resubmits the write,
|
|
140
|
+
// only observes. One finite elapsed budget bounds the whole observation,
|
|
141
|
+
// and no single fetch's AbortSignal outlives it. This proof has no second
|
|
142
|
+
// agent to resolve a conflict, so every terminal outcome other than a
|
|
143
|
+
// clean merge of the proposed head is a hard failure.
|
|
144
|
+
const mergeAndObserve = async (repo, number, head) => {
|
|
145
|
+
const merged = await call(
|
|
146
|
+
"changes.merge",
|
|
147
|
+
{ repo, number, head },
|
|
148
|
+
owner,
|
|
149
|
+
AbortSignal.timeout(10_000),
|
|
150
|
+
);
|
|
151
|
+
if (merged.kind !== "merging") {
|
|
152
|
+
assert.equal(
|
|
153
|
+
merged.kind,
|
|
154
|
+
"merged",
|
|
155
|
+
`changes.merge did not merge: ${JSON.stringify(merged)}`,
|
|
156
|
+
);
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
const deadline = Date.now() + 60_000;
|
|
160
|
+
const budget = (ms) => Math.max(1, Math.min(ms, deadline - Date.now()));
|
|
161
|
+
for (;;) {
|
|
162
|
+
if (Date.now() >= deadline)
|
|
163
|
+
throw new Error(`Timed out waiting for Change ${number} to settle`);
|
|
164
|
+
const detail = await call(
|
|
165
|
+
"changes.get",
|
|
166
|
+
{ repo, number },
|
|
167
|
+
owner,
|
|
168
|
+
AbortSignal.timeout(budget(10_000)),
|
|
169
|
+
);
|
|
170
|
+
if ("change" in detail) {
|
|
171
|
+
const change = detail.change;
|
|
172
|
+
const settled =
|
|
173
|
+
change.state === "merged" ||
|
|
174
|
+
change.state === "closed" ||
|
|
175
|
+
change.head !== head ||
|
|
176
|
+
change.conflict?.head === head ||
|
|
177
|
+
change.mergeFailure !== undefined;
|
|
178
|
+
if (settled) {
|
|
179
|
+
assert.equal(
|
|
180
|
+
change.mergeFailure,
|
|
181
|
+
undefined,
|
|
182
|
+
`Change ${number} merge failed: ${change.mergeFailure}`,
|
|
183
|
+
);
|
|
184
|
+
assert.notEqual(
|
|
185
|
+
change.state,
|
|
186
|
+
"closed",
|
|
187
|
+
`Change ${number} closed before head ${head} could settle`,
|
|
188
|
+
);
|
|
189
|
+
assert.equal(
|
|
190
|
+
change.head,
|
|
191
|
+
head,
|
|
192
|
+
`Change ${number} moved to ${change.head} before it could settle`,
|
|
193
|
+
);
|
|
194
|
+
assert.equal(
|
|
195
|
+
change.state,
|
|
196
|
+
"merged",
|
|
197
|
+
`changes.merge did not merge: ${JSON.stringify(change)}`,
|
|
198
|
+
);
|
|
199
|
+
return { kind: "merged", after: change.mergedCommit };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await new Promise((resolve) =>
|
|
203
|
+
setTimeout(resolve, Math.min(500, budget(500))),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
210
|
+
if (
|
|
211
|
+
await fetch(`${origin}/login`).then(
|
|
212
|
+
(r) => r.ok,
|
|
213
|
+
() => false,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
break;
|
|
217
|
+
if (attempt > 240) throw new Error(`Oyster did not start:\n${output}`);
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
219
|
+
}
|
|
220
|
+
const installed = await installRelease({
|
|
221
|
+
directory: `${work}/release`,
|
|
222
|
+
url: origin,
|
|
223
|
+
key,
|
|
224
|
+
installationId: "bonsai",
|
|
225
|
+
});
|
|
226
|
+
assert.equal(installed.status, 201, await installed.text());
|
|
227
|
+
|
|
228
|
+
await call("repos.create", { name: repo, description: "runtime proof" });
|
|
229
|
+
const seed = join(work, "seed");
|
|
230
|
+
git(["init", "-q", "-b", "main", seed]);
|
|
231
|
+
await writeFile(join(seed, "a.txt"), "one\n");
|
|
232
|
+
git(["add", "."], seed);
|
|
233
|
+
git(
|
|
234
|
+
["-c", "user.name=owner", "-c", "user.email=o@x", "commit", "-qm", "start"],
|
|
235
|
+
seed,
|
|
236
|
+
);
|
|
237
|
+
const auth = ["-c", `http.extraHeader=Authorization: Bearer ${key}`];
|
|
238
|
+
git(
|
|
239
|
+
[...auth, "push", "-q", `${origin}/git/bonsai/${repo}.git`, "main"],
|
|
240
|
+
seed,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const fork = await call("repos.fork", { repo });
|
|
244
|
+
await writeFile(join(seed, "b.txt"), "fork work\n");
|
|
245
|
+
git(["add", "."], seed);
|
|
246
|
+
git(
|
|
247
|
+
[
|
|
248
|
+
"-c",
|
|
249
|
+
"user.name=owner",
|
|
250
|
+
"-c",
|
|
251
|
+
"user.email=o@x",
|
|
252
|
+
"commit",
|
|
253
|
+
"-qm",
|
|
254
|
+
"fork work",
|
|
255
|
+
],
|
|
256
|
+
seed,
|
|
257
|
+
);
|
|
258
|
+
git(
|
|
259
|
+
[...auth, "push", "-q", `${origin}/git/bonsai/${fork.name}.git`, "main"],
|
|
260
|
+
seed,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Push facts arrive after the response; wait for the fork's push.
|
|
264
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
265
|
+
const activity = await call("activity.list", { repo });
|
|
266
|
+
if (
|
|
267
|
+
activity.events.some(
|
|
268
|
+
(event) => event.type === "push" && event.repo === fork.name,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
break;
|
|
272
|
+
if (attempt > 40) throw new Error("push fact never arrived");
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
274
|
+
}
|
|
275
|
+
const page = await call("changes.list", { repo });
|
|
276
|
+
const change = page.changes.find((c) => c.sourceRepo === fork.name);
|
|
277
|
+
assert.ok(change, `no Change for fork ${fork.name}`);
|
|
278
|
+
const merged = await mergeAndObserve(repo, change.number, change.head);
|
|
279
|
+
const graph = await call("graph.get", { repo });
|
|
280
|
+
assert.ok(
|
|
281
|
+
graph.nodes.some(
|
|
282
|
+
(node) => node.hash === merged.after && node.parents.length === 2,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
const file = await call("files.read", { repo, ref: "main", path: "b.txt" });
|
|
286
|
+
assert.equal(file.text, "fork work\n");
|
|
287
|
+
console.log(
|
|
288
|
+
`bonsai runtime proof passed on ${runtime}: create, push, fork, push, merge ${merged.after.slice(0, 7)}, graph, read`,
|
|
289
|
+
);
|
|
290
|
+
} finally {
|
|
291
|
+
wrangler.kill("SIGTERM");
|
|
292
|
+
await stopped;
|
|
293
|
+
const base = `https://api.cloudflare.com/client/v4/accounts/${accountId}/artifacts/namespaces/${namespace}/repos`;
|
|
294
|
+
for (const name of [`bonsai--${repo}`, `bonsai--${repo}--owner`])
|
|
295
|
+
await fetch(`${base}/${name}`, {
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
298
|
+
});
|
|
299
|
+
}
|