@fungi.computer/bonsai 0.0.1 → 0.2.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 +92 -0
- package/README.md +66 -12
- package/backend/build-workflow.ts +137 -0
- package/backend/index.ts +13 -7
- package/backend/methods.ts +222 -19
- package/package.json +54 -18
- package/scripts/release.mjs +12 -1
- package/scripts/shed-terminal-proof.mjs +30 -28
- 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/present.test.ts +27 -0
- 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/vitest.config.ts +43 -1
- package/web/app-build-panel.tsx +183 -95
- package/web/bonsai.css +1 -0
- package/web/change-page.tsx +10 -6
- 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,14 @@
|
|
|
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";
|
|
11
|
+
import { resultRows } from "@fungi.computer/agar/method-presentation";
|
|
1
12
|
import { openChangesGill, recentCommitsGill } from "./gills.js";
|
|
2
13
|
import {
|
|
3
14
|
pickerOptionsInput,
|
|
@@ -34,6 +45,8 @@ import {
|
|
|
34
45
|
} from "@fungi.computer/agar/backend-definition";
|
|
35
46
|
import type { AppMethodCallContext } from "@fungi.computer/puffball/app-method-runtime";
|
|
36
47
|
import * as z from "zod";
|
|
48
|
+
import * as S from "effect/Schema";
|
|
49
|
+
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
37
50
|
import {
|
|
38
51
|
gitSha1Schema,
|
|
39
52
|
gitRepositoryNameSchema,
|
|
@@ -41,6 +54,10 @@ import {
|
|
|
41
54
|
import {
|
|
42
55
|
activityPage,
|
|
43
56
|
appBuildView,
|
|
57
|
+
appBuildWorkflowInput,
|
|
58
|
+
appBuildStatusInput,
|
|
59
|
+
appRegistrationInput,
|
|
60
|
+
appRegistration,
|
|
44
61
|
approvalResult,
|
|
45
62
|
checkRequestResult,
|
|
46
63
|
checkReportResult,
|
|
@@ -101,6 +118,9 @@ export type AppsBindingReply = Readonly<{
|
|
|
101
118
|
|
|
102
119
|
/** The host `APPS` binding: fork a source, build it, and install a Release. */
|
|
103
120
|
export type BonsaiAppsBinding = Readonly<{
|
|
121
|
+
register(
|
|
122
|
+
input: z.infer<typeof appRegistrationInput>,
|
|
123
|
+
): Promise<AppsBindingReply>;
|
|
104
124
|
fork(input: {
|
|
105
125
|
platformAppId: string;
|
|
106
126
|
name: string;
|
|
@@ -108,9 +128,13 @@ export type BonsaiAppsBinding = Readonly<{
|
|
|
108
128
|
build(input: {
|
|
109
129
|
appId: string;
|
|
110
130
|
sourceRevision: string;
|
|
131
|
+
installationId?: string;
|
|
111
132
|
}): Promise<AppsBindingReply>;
|
|
112
|
-
buildStatus(
|
|
133
|
+
buildStatus(
|
|
134
|
+
input: z.infer<typeof appBuildStatusInput>,
|
|
135
|
+
): Promise<AppsBindingReply>;
|
|
113
136
|
select(input: {
|
|
137
|
+
jobId: string;
|
|
114
138
|
installationId: string;
|
|
115
139
|
release: unknown;
|
|
116
140
|
}): Promise<AppsBindingReply>;
|
|
@@ -119,10 +143,11 @@ export type BonsaiAppsBinding = Readonly<{
|
|
|
119
143
|
/** What a Bonsai method runs against: the facet supplies it. */
|
|
120
144
|
type BonsaiMethodContext = Readonly<{
|
|
121
145
|
bonsai: Bonsai;
|
|
122
|
-
env?:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
env?: BuildWorkflowBindings &
|
|
147
|
+
Readonly<{
|
|
148
|
+
BACKGROUND?: BackgroundBinding;
|
|
149
|
+
APPS?: BonsaiAppsBinding;
|
|
150
|
+
}>;
|
|
126
151
|
}>;
|
|
127
152
|
|
|
128
153
|
const bindingErrorBody = z.object({ error: z.string() });
|
|
@@ -132,13 +157,15 @@ function refuseBindingReply(reply: AppsBindingReply): never {
|
|
|
132
157
|
const parsed = bindingErrorBody.safeParse(reply.body);
|
|
133
158
|
const message = parsed.success ? parsed.data.error : "Request refused";
|
|
134
159
|
throw new AppMethodRefusal(
|
|
135
|
-
reply.status
|
|
136
|
-
? "
|
|
137
|
-
: reply.status ===
|
|
138
|
-
? "
|
|
139
|
-
: reply.status ===
|
|
140
|
-
? "
|
|
141
|
-
:
|
|
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",
|
|
142
169
|
message,
|
|
143
170
|
);
|
|
144
171
|
}
|
|
@@ -219,6 +246,72 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
219
246
|
},
|
|
220
247
|
},
|
|
221
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
|
+
}),
|
|
222
315
|
"apps.build": defineAppMethod({
|
|
223
316
|
description:
|
|
224
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.",
|
|
@@ -229,28 +322,44 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
229
322
|
input: z.strictObject({
|
|
230
323
|
appId: z.string().min(1).max(256),
|
|
231
324
|
sourceRevision: z.string().min(1).max(256),
|
|
325
|
+
installationId: z.string().min(1).max(256).optional(),
|
|
232
326
|
}),
|
|
233
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
|
+
},
|
|
234
337
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
235
338
|
if (caller.role !== "owner")
|
|
236
339
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
237
340
|
if (!context.env?.APPS)
|
|
238
341
|
throw new AppMethodRefusal("unavailable", "Build is unavailable");
|
|
239
|
-
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);
|
|
240
349
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
241
350
|
return appBuildView.parse(reply.body);
|
|
242
351
|
},
|
|
243
352
|
}),
|
|
244
353
|
"apps.buildStatus": defineAppMethod({
|
|
245
354
|
description:
|
|
246
|
-
"
|
|
355
|
+
"Read one build job or discover this repository's latest job. Pending until build and automatic installation settle. Owner only.",
|
|
247
356
|
effect: "read",
|
|
248
357
|
// The host grants its APPS operation only under these approvals.
|
|
249
358
|
requires: ["git.repository", "computer.execute"],
|
|
250
359
|
// A Workflow step waits on the job by polling this same call.
|
|
251
360
|
pending: true,
|
|
252
|
-
input:
|
|
253
|
-
output: appBuildView,
|
|
361
|
+
input: appBuildStatusInput,
|
|
362
|
+
output: appBuildView.nullable(),
|
|
254
363
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
255
364
|
if (caller.role !== "owner")
|
|
256
365
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
@@ -258,27 +367,104 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
258
367
|
throw new AppMethodRefusal("unavailable", "Build is unavailable");
|
|
259
368
|
const reply = await context.env.APPS.buildStatus(input);
|
|
260
369
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
261
|
-
const build = appBuildView.parse(reply.body);
|
|
262
|
-
|
|
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
|
+
};
|
|
263
437
|
},
|
|
264
438
|
}),
|
|
265
439
|
"apps.install": defineAppMethod({
|
|
266
440
|
description:
|
|
267
441
|
"Install a built Release at an installation id, or switch an already-live one onto it. Owner only.",
|
|
268
442
|
effect: "write",
|
|
443
|
+
pending: true,
|
|
269
444
|
// The host grants its APPS operation only under these approvals.
|
|
270
445
|
requires: ["git.repository", "computer.execute"],
|
|
271
446
|
input: z.strictObject({
|
|
447
|
+
jobId: z.string().min(1).max(256),
|
|
272
448
|
installationId: z.string().min(1).max(256),
|
|
273
449
|
release: z.unknown(),
|
|
274
450
|
}),
|
|
275
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
|
+
},
|
|
276
461
|
handler: async (input, context: BonsaiMethodContext, caller) => {
|
|
277
462
|
if (caller.role !== "owner")
|
|
278
463
|
throw new AppMethodRefusal("forbidden", "Owner only");
|
|
279
464
|
if (!context.env?.APPS)
|
|
280
465
|
throw new AppMethodRefusal("unavailable", "Install is unavailable");
|
|
281
466
|
const reply = await context.env.APPS.select(input);
|
|
467
|
+
if (reply.status === 202) return pending(5000);
|
|
282
468
|
if (reply.status >= 400) refuseBindingReply(reply);
|
|
283
469
|
return reply.body;
|
|
284
470
|
},
|
|
@@ -339,6 +525,18 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
339
525
|
requires: [],
|
|
340
526
|
input: z.strictObject({}),
|
|
341
527
|
output: repoPage,
|
|
528
|
+
present: {
|
|
529
|
+
rows: resultRows,
|
|
530
|
+
verb: "listed",
|
|
531
|
+
row: ({ output }) => ({
|
|
532
|
+
id: "repos.list",
|
|
533
|
+
title: `Bonsai listed ${String(output.repos.length)} ${output.repos.length === 1 ? "repository" : "repositories"}`,
|
|
534
|
+
detail: output.repos
|
|
535
|
+
.slice(0, 5)
|
|
536
|
+
.map((repo) => repo.name)
|
|
537
|
+
.join(" · "),
|
|
538
|
+
}),
|
|
539
|
+
},
|
|
342
540
|
handler: (_input, context) => ({
|
|
343
541
|
repos: service.listRepos(context.bonsai),
|
|
344
542
|
}),
|
|
@@ -684,7 +882,12 @@ const definition = defineAppBackend<BonsaiMethodContext>({
|
|
|
684
882
|
effect: "write",
|
|
685
883
|
// The continuation covers every write's requirements (apps.build's
|
|
686
884
|
// computer.execute included); it only replays queued Git writes.
|
|
687
|
-
requires: [
|
|
885
|
+
requires: [
|
|
886
|
+
"git.repository",
|
|
887
|
+
"computer.execute",
|
|
888
|
+
"workflow.run",
|
|
889
|
+
"workflow.publish",
|
|
890
|
+
],
|
|
688
891
|
input: z.strictObject({}),
|
|
689
892
|
output: z.strictObject({ more: z.boolean() }),
|
|
690
893
|
handler: (_input, context, caller) =>
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/bonsai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
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.0
|
|
10
|
-
"@fungi.computer/issues": "0.0
|
|
11
|
-
"@fungi.computer/oyster": "0.
|
|
12
|
-
"@fungi.computer/puffball": "0.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.0",
|
|
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.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.0.1",
|
|
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.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`,
|
|
@@ -377,16 +379,17 @@ try {
|
|
|
377
379
|
"The VM Computer fixture's sole Computer is named workspace",
|
|
378
380
|
);
|
|
379
381
|
const nonce = randomBytes(8).toString("hex");
|
|
380
|
-
// Shed
|
|
381
|
-
// typed line to the real PTY
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
382
|
+
// Shed is one Ghostty terminal whose light prompt is drawn on its
|
|
383
|
+
// canvas; `full` escalates one typed line to the real PTY in place, and
|
|
384
|
+
// every later command is typed into the same canvas. Typing goes to
|
|
385
|
+
// Ghostty's hidden key-capture textarea, which a click on the canvas
|
|
386
|
+
// focuses.
|
|
387
|
+
const canvas = shed.locator(".terminal-host canvas");
|
|
388
|
+
const typeLine = async (line) => {
|
|
389
|
+
await canvas.click();
|
|
390
|
+
await page.keyboard.type(line);
|
|
391
|
+
await page.keyboard.press("Enter");
|
|
392
|
+
};
|
|
390
393
|
const receivedBytes = () =>
|
|
391
394
|
Buffer.concat(received.filter((frame) => Buffer.isBuffer(frame)));
|
|
392
395
|
const sentBytesLength = () =>
|
|
@@ -404,14 +407,15 @@ try {
|
|
|
404
407
|
runCommandCalls.push(request.postDataJSON());
|
|
405
408
|
});
|
|
406
409
|
const beforeLightInput = sentBytesLength();
|
|
407
|
-
await
|
|
408
|
-
await prompt.fill("pwd");
|
|
409
|
-
await prompt.press("Enter");
|
|
410
|
+
await typeLine("pwd");
|
|
410
411
|
await expect
|
|
411
|
-
.poll(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
.poll(
|
|
413
|
+
() => runCommandCalls.some((call) => call.command.endsWith("; pwd")),
|
|
414
|
+
{
|
|
415
|
+
message: "A light command calls Shed's runCommand method",
|
|
416
|
+
timeout: 30_000,
|
|
417
|
+
},
|
|
418
|
+
)
|
|
415
419
|
.toBe(true);
|
|
416
420
|
assert.equal(
|
|
417
421
|
sentBytesLength(),
|
|
@@ -420,9 +424,7 @@ try {
|
|
|
420
424
|
);
|
|
421
425
|
|
|
422
426
|
const marker = `oyster-${nonce}-command`;
|
|
423
|
-
await
|
|
424
|
-
await prompt.fill(`full printf 'oyster-%s-command\\n' '${nonce}'`);
|
|
425
|
-
await prompt.press("Enter");
|
|
427
|
+
await typeLine(`full printf 'oyster-%s-command\\n' '${nonce}'`);
|
|
426
428
|
await expect
|
|
427
429
|
.poll(() => receivedBytes().includes(marker), {
|
|
428
430
|
message: "A real shell command returns through Shed's terminal",
|
|
@@ -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
|