@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.
@@ -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(input: { jobId: string }): Promise<AppsBindingReply>;
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?: Readonly<{
124
- BACKGROUND?: BackgroundBinding;
125
- APPS?: BonsaiAppsBinding;
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 === 404
137
- ? "not_found"
138
- : reply.status === 409
139
- ? "conflict"
140
- : reply.status === 403
141
- ? "forbidden"
142
- : "invalid",
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 reply = await context.env.APPS.build(input);
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
- "Wait for one build job: Pending while it runs, then its settled status and Release. Owner only.",
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: z.strictObject({ jobId: z.string().min(1).max(256) }),
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
- return build.status === "running" ? pending(5_000) : build;
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: ["git.repository", "computer.execute"],
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.0",
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.2.0",
9
- "@fungi.computer/caps": "0.1.0",
10
- "@fungi.computer/issues": "0.0.1",
11
- "@fungi.computer/oyster": "0.2.0",
12
- "@fungi.computer/puffball": "0.1.0",
13
- "@fungi.computer/stipe": "0.0.1",
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.1",
26
+ "@fungi.computer/watchdog": "0.1.0",
26
27
  "effect": "4.0.2",
27
- "@fungi.computer/cairn": "0.0.1",
28
- "@fungi.computer/maosaico": "0.0.1",
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.1.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/braid": "0.1.0",
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.0"
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",
@@ -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.recipe,
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/oyster/shell/workbench-directory";
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.equal(result.status, "succeeded");
231
- assert.equal(result.output.status, "completed");
232
- assert.equal(result.output.stdout, "standalone-shed-terminal");
233
- assert.equal(result.output.stderr, "");
234
- assert.equal(result.output.exitCode, 0);
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
- // operation-contract.ts); starting a demo process there throws
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
@@ -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() });