@fungi.computer/bonsai 0.1.0 → 0.2.1
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 +73 -0
- package/README.md +66 -12
- package/backend/build-workflow.ts +137 -0
- package/backend/index.ts +13 -7
- package/backend/methods.ts +209 -19
- package/package.json +54 -18
- package/scripts/release.mjs +12 -1
- package/scripts/shed-terminal-proof.mjs +10 -8
- package/shared/contract.ts +41 -0
- package/test/apps-build-install.test.ts +399 -26
- package/test/browser/build-workflow.browser.mjs +104 -0
- package/test/browser/repository-browser.fixture.mjs +84 -0
- package/test/browser/repository-settings-fixture.tsx +198 -2
- package/test/browser/repository-settings.browser.mjs +188 -26
- package/test/checks-workflow-template.test.mjs +12 -10
- package/test/favicon.test.mjs +25 -0
- package/test/git-push.native.mjs +30 -0
- package/test/git-push.worker.ts +51 -3
- package/test/merge-template-build.test.mjs +44 -201
- package/test/storyboard-save-https-git.fixture.mjs +35 -29
- package/test/storyboard-save-template.test.mjs +61 -12
- package/vite.config.ts +19 -1
- package/web/app-build-panel.tsx +183 -95
- package/web/bonsai.css +1 -0
- package/web/index.html +0 -1
- package/web/repo-settings.tsx +1 -1
- package/web/public/favicon.svg +0 -1
package/backend/methods.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runReply,
|
|
3
|
+
statusReply,
|
|
4
|
+
runsReplySchema,
|
|
5
|
+
} from "@fungi.computer/workflows-wire";
|
|
6
|
+
import {
|
|
7
|
+
startManualBuildWorkflow,
|
|
8
|
+
readBuildWorkflowRuns,
|
|
9
|
+
type BuildWorkflowBindings,
|
|
10
|
+
} from "./build-workflow.js";
|
|
1
11
|
import { resultRows } from "@fungi.computer/agar/method-presentation";
|
|
2
12
|
import { openChangesGill, recentCommitsGill } from "./gills.js";
|
|
3
13
|
import {
|
|
@@ -35,6 +45,8 @@ import {
|
|
|
35
45
|
} from "@fungi.computer/agar/backend-definition";
|
|
36
46
|
import type { AppMethodCallContext } from "@fungi.computer/puffball/app-method-runtime";
|
|
37
47
|
import * as z from "zod";
|
|
48
|
+
import * as S from "effect/Schema";
|
|
49
|
+
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
38
50
|
import {
|
|
39
51
|
gitSha1Schema,
|
|
40
52
|
gitRepositoryNameSchema,
|
|
@@ -42,6 +54,10 @@ import {
|
|
|
42
54
|
import {
|
|
43
55
|
activityPage,
|
|
44
56
|
appBuildView,
|
|
57
|
+
appBuildWorkflowInput,
|
|
58
|
+
appBuildStatusInput,
|
|
59
|
+
appRegistrationInput,
|
|
60
|
+
appRegistration,
|
|
45
61
|
approvalResult,
|
|
46
62
|
checkRequestResult,
|
|
47
63
|
checkReportResult,
|
|
@@ -102,6 +118,9 @@ export type AppsBindingReply = Readonly<{
|
|
|
102
118
|
|
|
103
119
|
/** The host `APPS` binding: fork a source, build it, and install a Release. */
|
|
104
120
|
export type BonsaiAppsBinding = Readonly<{
|
|
121
|
+
register(
|
|
122
|
+
input: z.infer<typeof appRegistrationInput>,
|
|
123
|
+
): Promise<AppsBindingReply>;
|
|
105
124
|
fork(input: {
|
|
106
125
|
platformAppId: string;
|
|
107
126
|
name: string;
|
|
@@ -109,9 +128,13 @@ export type BonsaiAppsBinding = Readonly<{
|
|
|
109
128
|
build(input: {
|
|
110
129
|
appId: string;
|
|
111
130
|
sourceRevision: string;
|
|
131
|
+
installationId?: string;
|
|
112
132
|
}): Promise<AppsBindingReply>;
|
|
113
|
-
buildStatus(
|
|
133
|
+
buildStatus(
|
|
134
|
+
input: z.infer<typeof appBuildStatusInput>,
|
|
135
|
+
): Promise<AppsBindingReply>;
|
|
114
136
|
select(input: {
|
|
137
|
+
jobId: string;
|
|
115
138
|
installationId: string;
|
|
116
139
|
release: unknown;
|
|
117
140
|
}): Promise<AppsBindingReply>;
|
|
@@ -120,10 +143,11 @@ export type BonsaiAppsBinding = Readonly<{
|
|
|
120
143
|
/** What a Bonsai method runs against: the facet supplies it. */
|
|
121
144
|
type BonsaiMethodContext = Readonly<{
|
|
122
145
|
bonsai: Bonsai;
|
|
123
|
-
env?:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
env?: BuildWorkflowBindings &
|
|
147
|
+
Readonly<{
|
|
148
|
+
BACKGROUND?: BackgroundBinding;
|
|
149
|
+
APPS?: BonsaiAppsBinding;
|
|
150
|
+
}>;
|
|
127
151
|
}>;
|
|
128
152
|
|
|
129
153
|
const bindingErrorBody = z.object({ error: z.string() });
|
|
@@ -133,13 +157,15 @@ function refuseBindingReply(reply: AppsBindingReply): never {
|
|
|
133
157
|
const parsed = bindingErrorBody.safeParse(reply.body);
|
|
134
158
|
const message = parsed.success ? parsed.data.error : "Request refused";
|
|
135
159
|
throw new AppMethodRefusal(
|
|
136
|
-
reply.status
|
|
137
|
-
? "
|
|
138
|
-
: reply.status ===
|
|
139
|
-
? "
|
|
140
|
-
: reply.status ===
|
|
141
|
-
? "
|
|
142
|
-
:
|
|
160
|
+
reply.status >= 500
|
|
161
|
+
? "unavailable"
|
|
162
|
+
: reply.status === 404
|
|
163
|
+
? "not_found"
|
|
164
|
+
: reply.status === 409
|
|
165
|
+
? "conflict"
|
|
166
|
+
: reply.status === 403
|
|
167
|
+
? "forbidden"
|
|
168
|
+
: "invalid",
|
|
143
169
|
message,
|
|
144
170
|
);
|
|
145
171
|
}
|
|
@@ -220,6 +246,72 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
220
246
|
},
|
|
221
247
|
},
|
|
222
248
|
methods: {
|
|
249
|
+
"apps.register": defineAppMethod({
|
|
250
|
+
description:
|
|
251
|
+
"Make this repository an App using its main commit's package.json build recipe. Owner only; does not build or install it.",
|
|
252
|
+
effect: "write",
|
|
253
|
+
requires: ["git.repository"],
|
|
254
|
+
input: appRegistrationInput,
|
|
255
|
+
output: appRegistration,
|
|
256
|
+
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
257
|
+
if (caller.role !== "owner")
|
|
258
|
+
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
259
|
+
if (!context.env?.APPS)
|
|
260
|
+
throw new AppMethodRefusal(
|
|
261
|
+
"unavailable",
|
|
262
|
+
"App registration is unavailable",
|
|
263
|
+
);
|
|
264
|
+
const reply = await context.env.APPS.register(input);
|
|
265
|
+
if (reply.status >= 400) refuseBindingReply(reply);
|
|
266
|
+
return appRegistration.parse(reply.body);
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
"apps.startBuildWorkflow": defineAppMethod({
|
|
270
|
+
description:
|
|
271
|
+
"Build, verify and install this repository through the published Workflow. Owner only.",
|
|
272
|
+
effect: "write",
|
|
273
|
+
requires: ["git.repository", "workflow.run", "workflow.publish"],
|
|
274
|
+
input: appBuildWorkflowInput,
|
|
275
|
+
output: runReply,
|
|
276
|
+
handler: (input, context: BonsaiMethodContext, caller) => {
|
|
277
|
+
if (caller.role !== "owner")
|
|
278
|
+
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
279
|
+
return startManualBuildWorkflow(context.env ?? {}, input);
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
"apps.buildWorkflowRuns": defineAppMethod({
|
|
283
|
+
description:
|
|
284
|
+
"Discover up to fifty runs of this installation's build Workflow, including accepted main pushes. Read-only; owner only.",
|
|
285
|
+
effect: "read",
|
|
286
|
+
requires: ["workflow.run"],
|
|
287
|
+
input: z.strictObject({}),
|
|
288
|
+
output: runsReplySchema,
|
|
289
|
+
handler: (_input, context: BonsaiMethodContext, caller) => {
|
|
290
|
+
if (caller.role !== "owner")
|
|
291
|
+
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
292
|
+
return readBuildWorkflowRuns(context.env ?? {});
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
"apps.buildWorkflowRun": defineAppMethod({
|
|
296
|
+
description:
|
|
297
|
+
"Read this installation's build Workflow and its recorded step results. Owner only.",
|
|
298
|
+
effect: "read",
|
|
299
|
+
requires: ["workflow.run"],
|
|
300
|
+
input: z.strictObject({ runId: z.string().min(1).max(256) }),
|
|
301
|
+
output: statusReply,
|
|
302
|
+
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
303
|
+
if (caller.role !== "owner")
|
|
304
|
+
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
305
|
+
if (context.env?.WORKFLOWS === undefined)
|
|
306
|
+
throw new AppMethodRefusal(
|
|
307
|
+
"unavailable",
|
|
308
|
+
"Workflow access is not granted",
|
|
309
|
+
);
|
|
310
|
+
return statusReply.parse(
|
|
311
|
+
await context.env.WORKFLOWS.graphStatus(input),
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
223
315
|
"apps.build": defineAppMethod({
|
|
224
316
|
description:
|
|
225
317
|
"Build an App's registered source at one exact commit on this Oyster's Computer; the job keeps running after this call returns. Owner only.",
|
|
@@ -230,28 +322,44 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
230
322
|
input: z.strictObject({
|
|
231
323
|
appId: z.string().min(1).max(256),
|
|
232
324
|
sourceRevision: z.string().min(1).max(256),
|
|
325
|
+
installationId: z.string().min(1).max(256).optional(),
|
|
233
326
|
}),
|
|
234
327
|
output: appBuildView,
|
|
328
|
+
present: {
|
|
329
|
+
rows: resultRows,
|
|
330
|
+
verb: "started",
|
|
331
|
+
row: ({ output }) => ({
|
|
332
|
+
id: output.jobId,
|
|
333
|
+
title: "Build " + output.status,
|
|
334
|
+
detail: output.sourceRevision,
|
|
335
|
+
}),
|
|
336
|
+
},
|
|
235
337
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
236
338
|
if (caller.role !== "owner")
|
|
237
339
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
238
340
|
if (!context.env?.APPS)
|
|
239
341
|
throw new AppMethodRefusal("unavailable", "Build is unavailable");
|
|
240
|
-
const
|
|
342
|
+
const request: Parameters<BonsaiAppsBinding["build"]>[0] = {
|
|
343
|
+
appId: input.appId,
|
|
344
|
+
sourceRevision: input.sourceRevision,
|
|
345
|
+
};
|
|
346
|
+
if (input.installationId !== undefined)
|
|
347
|
+
request.installationId = input.installationId;
|
|
348
|
+
const reply = await context.env.APPS.build(request);
|
|
241
349
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
242
350
|
return appBuildView.parse(reply.body);
|
|
243
351
|
},
|
|
244
352
|
}),
|
|
245
353
|
"apps.buildStatus": defineAppMethod({
|
|
246
354
|
description:
|
|
247
|
-
"
|
|
355
|
+
"Read one build job or discover this repository's latest job. Pending until build and automatic installation settle. Owner only.",
|
|
248
356
|
effect: "read",
|
|
249
357
|
// The host grants its APPS operation only under these approvals.
|
|
250
358
|
requires: ["git.repository", "computer.execute"],
|
|
251
359
|
// A Workflow step waits on the job by polling this same call.
|
|
252
360
|
pending: true,
|
|
253
|
-
input:
|
|
254
|
-
output: appBuildView,
|
|
361
|
+
input: appBuildStatusInput,
|
|
362
|
+
output: appBuildView.nullable(),
|
|
255
363
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
256
364
|
if (caller.role !== "owner")
|
|
257
365
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
@@ -259,27 +367,104 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
259
367
|
throw new AppMethodRefusal("unavailable", "Build is unavailable");
|
|
260
368
|
const reply = await context.env.APPS.buildStatus(input);
|
|
261
369
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
262
|
-
const build = appBuildView.parse(reply.body);
|
|
263
|
-
|
|
370
|
+
const build = appBuildView.nullable().parse(reply.body);
|
|
371
|
+
if (build === null) return null;
|
|
372
|
+
return build.status === "running" ||
|
|
373
|
+
(build.status === "succeeded" &&
|
|
374
|
+
build.installation?.kind === "pending")
|
|
375
|
+
? pending(5_000)
|
|
376
|
+
: build;
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
379
|
+
"apps.verifyBuild": defineAppMethod({
|
|
380
|
+
description:
|
|
381
|
+
"Wait for the build's boot gate and verify that its Release is the exact requested commit. A failed or mismatched build never installs.",
|
|
382
|
+
effect: "read",
|
|
383
|
+
requires: ["git.repository", "computer.execute"],
|
|
384
|
+
pending: true,
|
|
385
|
+
input: z.strictObject({
|
|
386
|
+
jobId: z.string().min(1).max(256),
|
|
387
|
+
sourceRevision: z.string().regex(/^git:[0-9a-f]{40}$/),
|
|
388
|
+
}),
|
|
389
|
+
output: z.strictObject({
|
|
390
|
+
release: z.unknown(),
|
|
391
|
+
summary: z.string().max(4000),
|
|
392
|
+
}),
|
|
393
|
+
present: {
|
|
394
|
+
rows: resultRows,
|
|
395
|
+
verb: "verified",
|
|
396
|
+
row: ({ output }) => ({
|
|
397
|
+
id: "apps.verifyBuild",
|
|
398
|
+
title: "Release verified",
|
|
399
|
+
detail: output.summary,
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
403
|
+
if (caller.role !== "owner")
|
|
404
|
+
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
405
|
+
if (!context.env?.APPS)
|
|
406
|
+
throw new AppMethodRefusal("unavailable", "Build is unavailable");
|
|
407
|
+
const reply = await context.env.APPS.buildStatus({
|
|
408
|
+
jobId: input.jobId,
|
|
409
|
+
});
|
|
410
|
+
if (reply.status >= 400) refuseBindingReply(reply);
|
|
411
|
+
const build = appBuildView.nullable().parse(reply.body);
|
|
412
|
+
if (build === null)
|
|
413
|
+
throw new AppMethodRefusal("not_found", "Build not found");
|
|
414
|
+
if (build.status === "running") return pending(5_000);
|
|
415
|
+
if (build.status !== "succeeded" || build.release === null)
|
|
416
|
+
throw new AppMethodRefusal(
|
|
417
|
+
"invalid",
|
|
418
|
+
"The build did not pass" +
|
|
419
|
+
(build.detail === null ? "" : ": " + build.detail.slice(-2000)),
|
|
420
|
+
);
|
|
421
|
+
const release = S.decodeUnknownSync(releaseSchema)(build.release);
|
|
422
|
+
if (release.artifact.provenance.sourceRevision !== input.sourceRevision)
|
|
423
|
+
throw new AppMethodRefusal(
|
|
424
|
+
"invalid",
|
|
425
|
+
"The build's Release is not the requested commit",
|
|
426
|
+
);
|
|
427
|
+
return {
|
|
428
|
+
release,
|
|
429
|
+
summary:
|
|
430
|
+
"Built " +
|
|
431
|
+
release.releaseId +
|
|
432
|
+
" of " +
|
|
433
|
+
release.appId +
|
|
434
|
+
" from " +
|
|
435
|
+
input.sourceRevision,
|
|
436
|
+
};
|
|
264
437
|
},
|
|
265
438
|
}),
|
|
266
439
|
"apps.install": defineAppMethod({
|
|
267
440
|
description:
|
|
268
441
|
"Install a built Release at an installation id, or switch an already-live one onto it. Owner only.",
|
|
269
442
|
effect: "write",
|
|
443
|
+
pending: true,
|
|
270
444
|
// The host grants its APPS operation only under these approvals.
|
|
271
445
|
requires: ["git.repository", "computer.execute"],
|
|
272
446
|
input: z.strictObject({
|
|
447
|
+
jobId: z.string().min(1).max(256),
|
|
273
448
|
installationId: z.string().min(1).max(256),
|
|
274
449
|
release: z.unknown(),
|
|
275
450
|
}),
|
|
276
451
|
output: z.unknown(),
|
|
452
|
+
present: {
|
|
453
|
+
rows: resultRows,
|
|
454
|
+
verb: "installed",
|
|
455
|
+
row: ({ input }) => ({
|
|
456
|
+
id: "apps.install",
|
|
457
|
+
title: "Release installed",
|
|
458
|
+
detail: input.installationId,
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
277
461
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
278
462
|
if (caller.role !== "owner")
|
|
279
463
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
280
464
|
if (!context.env?.APPS)
|
|
281
465
|
throw new AppMethodRefusal("unavailable", "Install is unavailable");
|
|
282
466
|
const reply = await context.env.APPS.select(input);
|
|
467
|
+
if (reply.status === 202) return pending(5000);
|
|
283
468
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
284
469
|
return reply.body;
|
|
285
470
|
},
|
|
@@ -697,7 +882,12 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
697
882
|
effect: "write",
|
|
698
883
|
// The continuation covers every write's requirements (apps.build's
|
|
699
884
|
// computer.execute included); it only replays queued Git writes.
|
|
700
|
-
requires: [
|
|
885
|
+
requires: [
|
|
886
|
+
"git.repository",
|
|
887
|
+
"computer.execute",
|
|
888
|
+
"workflow.run",
|
|
889
|
+
"workflow.publish",
|
|
890
|
+
],
|
|
701
891
|
input: z.strictObject({}),
|
|
702
892
|
output: z.strictObject({ more: z.boolean() }),
|
|
703
893
|
handler: (_input, context, caller) =>
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/bonsai",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Bonsai: a git platform for many agents working at once, on Workers and Artifacts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@fungi.computer/agar": "0.
|
|
9
|
-
"@fungi.computer/caps": "0.
|
|
10
|
-
"@fungi.computer/issues": "0.0
|
|
11
|
-
"@fungi.computer/oyster": "0.
|
|
12
|
-
"@fungi.computer/puffball": "0.
|
|
13
|
-
"@fungi.computer/
|
|
8
|
+
"@fungi.computer/agar": "0.3.0",
|
|
9
|
+
"@fungi.computer/caps": "0.2.0",
|
|
10
|
+
"@fungi.computer/issues": "0.1.0",
|
|
11
|
+
"@fungi.computer/oyster": "0.3.1",
|
|
12
|
+
"@fungi.computer/puffball": "0.2.0",
|
|
13
|
+
"@fungi.computer/sigils": "0.2.0",
|
|
14
|
+
"@fungi.computer/stipe": "0.1.0",
|
|
14
15
|
"@pierre/diffs": "1.3.6",
|
|
15
16
|
"@pierre/trees": "1.0.0-beta.6",
|
|
16
17
|
"@tanstack/react-query": "5.102.8",
|
|
@@ -22,12 +23,16 @@
|
|
|
22
23
|
"react": "18.3.1",
|
|
23
24
|
"react-dom": "18.3.1",
|
|
24
25
|
"zod": "4.4.3",
|
|
25
|
-
"@fungi.computer/watchdog": "0.0
|
|
26
|
+
"@fungi.computer/watchdog": "0.1.0",
|
|
26
27
|
"effect": "4.0.2",
|
|
27
|
-
"@fungi.computer/cairn": "0.0
|
|
28
|
-
"@fungi.computer/maosaico": "0.0
|
|
28
|
+
"@fungi.computer/cairn": "0.1.0",
|
|
29
|
+
"@fungi.computer/maosaico": "0.1.0",
|
|
29
30
|
"@visx/hierarchy": "3.12.0",
|
|
30
|
-
"@fungi.computer/gills": "0.
|
|
31
|
+
"@fungi.computer/gills": "0.2.0",
|
|
32
|
+
"@fungi.computer/nacre": "0.0.1",
|
|
33
|
+
"@fungi.computer/trellis": "0.2.0",
|
|
34
|
+
"@fungi.computer/braid": "0.2.0",
|
|
35
|
+
"@fungi.computer/workflows-wire": "0.2.0"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
38
|
"playwright": "1.62.1",
|
|
@@ -43,12 +48,11 @@
|
|
|
43
48
|
"vitest": "4.1.11",
|
|
44
49
|
"wrangler": "4.135.0",
|
|
45
50
|
"tsx": "4.23.12",
|
|
46
|
-
"@fungi.computer/
|
|
47
|
-
"@fungi.computer/gills": "0.1.0",
|
|
51
|
+
"@fungi.computer/gills": "0.2.0",
|
|
48
52
|
"eslint": "10.9.1",
|
|
49
53
|
"oxlint": "1.78.0",
|
|
50
54
|
"jsdom": "29.0.1",
|
|
51
|
-
"@fungi.computer/computer": "0.1
|
|
55
|
+
"@fungi.computer/computer-contract": "0.0.1"
|
|
52
56
|
},
|
|
53
57
|
"botanical": {
|
|
54
58
|
"role": "app",
|
|
@@ -89,7 +93,9 @@
|
|
|
89
93
|
"git.repository",
|
|
90
94
|
"apps.open",
|
|
91
95
|
"app.events.emit",
|
|
92
|
-
"computer.execute"
|
|
96
|
+
"computer.execute",
|
|
97
|
+
"workflow.run",
|
|
98
|
+
"workflow.publish"
|
|
93
99
|
],
|
|
94
100
|
"receives": [
|
|
95
101
|
"git.push"
|
|
@@ -117,9 +123,21 @@
|
|
|
117
123
|
{
|
|
118
124
|
"name": "APPS",
|
|
119
125
|
"operations": [
|
|
126
|
+
"register",
|
|
127
|
+
"fork",
|
|
120
128
|
"build",
|
|
121
129
|
"buildStatus",
|
|
122
|
-
"select"
|
|
130
|
+
"select",
|
|
131
|
+
"repository"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "WORKFLOWS",
|
|
136
|
+
"operations": [
|
|
137
|
+
"catalog",
|
|
138
|
+
"runGraph",
|
|
139
|
+
"graphStatus",
|
|
140
|
+
"runs"
|
|
123
141
|
]
|
|
124
142
|
}
|
|
125
143
|
]
|
|
@@ -132,7 +150,25 @@
|
|
|
132
150
|
],
|
|
133
151
|
"stylesheets": [
|
|
134
152
|
"web/bonsai.css"
|
|
135
|
-
]
|
|
153
|
+
],
|
|
154
|
+
"entry": "web/index.html",
|
|
155
|
+
"maxOutputBytes": 67108864,
|
|
156
|
+
"backend": {
|
|
157
|
+
"entry": "backend/index.ts",
|
|
158
|
+
"exportName": "BonsaiApp",
|
|
159
|
+
"compatibilityDate": "2026-09-04",
|
|
160
|
+
"runtime": {
|
|
161
|
+
"kind": "durable-facet",
|
|
162
|
+
"stateSchema": 1
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"assetBase": "./",
|
|
166
|
+
"assets": {
|
|
167
|
+
"maxManifestBytes": 1048576,
|
|
168
|
+
"maxObjects": 1024,
|
|
169
|
+
"maxObjectBytes": 16777216,
|
|
170
|
+
"maxTotalBytes": 67108864
|
|
171
|
+
}
|
|
136
172
|
}
|
|
137
173
|
},
|
|
138
174
|
"scripts": {
|
|
@@ -140,7 +176,7 @@
|
|
|
140
176
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
141
177
|
"lint": "eslint backend web shared test --max-warnings=0 && oxlint --config ../../oxlint.config.ts --deny-warnings backend web shared test",
|
|
142
178
|
"test": "pnpm run test:unit && pnpm run test:merge",
|
|
143
|
-
"test:unit": "vitest run && node --import tsx --test test/seed-resume.test.mjs test/insight-layout.test.mjs test/checks-workflow-template.test.mjs test/merge-template-build.test.mjs test/storyboard-save-template.test.mjs",
|
|
179
|
+
"test:unit": "vitest run && node --import tsx --test test/seed-resume.test.mjs test/insight-layout.test.mjs test/checks-workflow-template.test.mjs test/merge-template-build.test.mjs test/storyboard-save-template.test.mjs test/favicon.test.mjs",
|
|
144
180
|
"test:merge": "node --import tsx test/git-push.native.mjs",
|
|
145
181
|
"format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
|
|
146
182
|
"lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs",
|
package/scripts/release.mjs
CHANGED
|
@@ -9,6 +9,10 @@ import process from "node:process";
|
|
|
9
9
|
import { log } from "node:console";
|
|
10
10
|
import { installRelease, packRelease } from "@fungi.computer/oyster/pack";
|
|
11
11
|
import { standaloneAppSet } from "@fungi.computer/oyster/standalone/app-set";
|
|
12
|
+
import {
|
|
13
|
+
appBuildRecipe,
|
|
14
|
+
readBuildManifest,
|
|
15
|
+
} from "@fungi.computer/oyster/build";
|
|
12
16
|
|
|
13
17
|
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
14
18
|
const repository = fileURLToPath(new URL("../../../", import.meta.url));
|
|
@@ -22,6 +26,13 @@ const revision = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
|
22
26
|
}).trim();
|
|
23
27
|
const bonsai = standaloneAppSet.find((app) => app.installationId === "bonsai");
|
|
24
28
|
if (!bonsai) throw new Error("The standalone App set must include Bonsai");
|
|
29
|
+
const manifest = readBuildManifest(
|
|
30
|
+
execFileSync(
|
|
31
|
+
"git",
|
|
32
|
+
["show", revision + ":" + bonsai.directory + "/package.json"],
|
|
33
|
+
{ cwd: repository, encoding: "utf8" },
|
|
34
|
+
),
|
|
35
|
+
);
|
|
25
36
|
const stamp = Date.now().toString(36);
|
|
26
37
|
const outDirectory = `${root}.botanical/release`;
|
|
27
38
|
|
|
@@ -34,7 +45,7 @@ await packRelease({
|
|
|
34
45
|
cwd: repository,
|
|
35
46
|
encoding: "utf8",
|
|
36
47
|
}).trim(),
|
|
37
|
-
recipe: bonsai.
|
|
48
|
+
recipe: appBuildRecipe(manifest, bonsai.directory),
|
|
38
49
|
outDirectory,
|
|
39
50
|
});
|
|
40
51
|
|
|
@@ -20,7 +20,7 @@ import * as z from "zod";
|
|
|
20
20
|
import { installRelease } from "@fungi.computer/oyster/pack";
|
|
21
21
|
import { parseSourceRevision } from "@fungi.computer/puffball";
|
|
22
22
|
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
23
|
-
import { workbenchDirectorySchema } from "@fungi.computer/
|
|
23
|
+
import { workbenchDirectorySchema } from "@fungi.computer/nacre/workbench-directory";
|
|
24
24
|
|
|
25
25
|
const [packedDirectory, expectedRevision, tourtoisDirectory, tourtoisRevision] =
|
|
26
26
|
z
|
|
@@ -227,11 +227,13 @@ try {
|
|
|
227
227
|
"The unchanged Shed backend runs a native command",
|
|
228
228
|
);
|
|
229
229
|
const result = await command.json();
|
|
230
|
-
assert.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
assert.deepEqual(result, {
|
|
231
|
+
stdout: "standalone-shed-terminal",
|
|
232
|
+
stderr: "",
|
|
233
|
+
exitCode: 0,
|
|
234
|
+
stdoutTruncated: false,
|
|
235
|
+
stderrTruncated: false,
|
|
236
|
+
});
|
|
235
237
|
|
|
236
238
|
const directoryResponse = await globalThis.fetch(
|
|
237
239
|
`${origin}/api/workbench/directory`,
|
|
@@ -568,8 +570,8 @@ try {
|
|
|
568
570
|
// Tourtois window chrome, not Shed's own light/full prompt, so they
|
|
569
571
|
// work the same escalated or not.
|
|
570
572
|
// Port 8099, not 8080: 8080 is computerDaemonPort, computerd's own
|
|
571
|
-
// reserved control-plane port (packages/computer/src/
|
|
572
|
-
//
|
|
573
|
+
// reserved control-plane port (packages/computer-contract/src/
|
|
574
|
+
// index.ts); starting a demo process there throws
|
|
573
575
|
// ComputerPortReservedError, confirmed against the real VM.
|
|
574
576
|
//
|
|
575
577
|
// Not python3 -m http.server: packages/computer/Dockerfile only
|
package/shared/contract.ts
CHANGED
|
@@ -12,6 +12,15 @@ import { commit } from "@fungi.computer/agar/roles/files";
|
|
|
12
12
|
|
|
13
13
|
export const productName = "Bonsai";
|
|
14
14
|
|
|
15
|
+
/** An explicit owner action starts the same published build/install Workflow. */
|
|
16
|
+
export const appBuildWorkflowInput = z.strictObject({
|
|
17
|
+
repo: repoName,
|
|
18
|
+
sourceRevision: z.string().min(1).max(256),
|
|
19
|
+
installationId: z.string().min(1).max(256),
|
|
20
|
+
requestId: z.string().min(1).max(256),
|
|
21
|
+
});
|
|
22
|
+
export type AppBuildWorkflowInput = z.infer<typeof appBuildWorkflowInput>;
|
|
23
|
+
|
|
15
24
|
/** Any JSON value: App method inputs and results travel as JSON. */
|
|
16
25
|
export const jsonValue = z.json();
|
|
17
26
|
export type JsonValue = z.infer<typeof jsonValue>;
|
|
@@ -389,6 +398,22 @@ export const issueFiles = z.strictObject({
|
|
|
389
398
|
export type IssueFiles = z.infer<typeof issueFiles>;
|
|
390
399
|
export const repoPage = z.strictObject({ repos: z.array(repoSummary) });
|
|
391
400
|
|
|
401
|
+
/** The host's install decision on this build, not a consent request. */
|
|
402
|
+
const appBuildInstallation = z.discriminatedUnion("kind", [
|
|
403
|
+
z.strictObject({ kind: z.literal("pending"), installationId: z.string() }),
|
|
404
|
+
z.strictObject({ kind: z.literal("installed"), installationId: z.string() }),
|
|
405
|
+
z.strictObject({
|
|
406
|
+
kind: z.literal("needs_consent"),
|
|
407
|
+
installationId: z.string(),
|
|
408
|
+
}),
|
|
409
|
+
z.strictObject({
|
|
410
|
+
kind: z.literal("failed"),
|
|
411
|
+
installationId: z.string(),
|
|
412
|
+
detail: z.string(),
|
|
413
|
+
}),
|
|
414
|
+
]);
|
|
415
|
+
export type AppBuildInstallation = z.infer<typeof appBuildInstallation>;
|
|
416
|
+
|
|
392
417
|
/** One App build job as its owner sees it; a settled success carries its Release. */
|
|
393
418
|
export const appBuildView = z.strictObject({
|
|
394
419
|
jobId: z.string().min(1).max(256),
|
|
@@ -399,6 +424,22 @@ export const appBuildView = z.strictObject({
|
|
|
399
424
|
detail: z.string().max(65_536).nullable(),
|
|
400
425
|
/** The recorded Release, parsed by its owner where it is installed. */
|
|
401
426
|
release: z.unknown(),
|
|
427
|
+
installation: appBuildInstallation.nullable(),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/** Register the calling installation's repository, never a caller-supplied recipe. */
|
|
431
|
+
export const appRegistrationInput = z.strictObject({ repo: repoName });
|
|
432
|
+
|
|
433
|
+
/** Workflows wait for one job; Settings discovers its repository's latest job. */
|
|
434
|
+
export const appBuildStatusInput = z.union([
|
|
435
|
+
z.strictObject({ jobId: z.string().min(1).max(256) }),
|
|
436
|
+
appRegistrationInput,
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
/** The host's App identity and exact main commit whose metadata it registered. */
|
|
440
|
+
export const appRegistration = appBuildView.pick({
|
|
441
|
+
appId: true,
|
|
442
|
+
sourceRevision: true,
|
|
402
443
|
});
|
|
403
444
|
|
|
404
445
|
export const errorBody = z.strictObject({ error: z.string() });
|