@fungi.computer/oyster 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.
Files changed (195) hide show
  1. package/README.md +228 -176
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +12 -2
  5. package/dist/agents.js +39 -2
  6. package/dist/app-builds.d.ts +76 -17
  7. package/dist/app-builds.js +207 -63
  8. package/dist/app-sources.d.ts +7 -22
  9. package/dist/app-sources.js +49 -14
  10. package/dist/apps/gate.d.ts +7 -7
  11. package/dist/apps/gate.js +6 -6
  12. package/dist/apps/installation.d.ts +35 -35
  13. package/dist/apps/installation.js +20 -19
  14. package/dist/build/artifact-writer.d.ts +40 -0
  15. package/dist/build/artifact-writer.js +181 -0
  16. package/dist/build/build-jobs.js +7 -1
  17. package/dist/build/build-policy.d.ts +10 -0
  18. package/dist/build/build-policy.js +10 -0
  19. package/dist/build/build-step.d.ts +12 -1
  20. package/dist/build/build-step.js +9 -1
  21. package/dist/build/candidate-builder.d.ts +68 -0
  22. package/dist/build/candidate-builder.js +91 -0
  23. package/dist/build/candidate-cli.d.ts +1 -0
  24. package/dist/build/candidate-cli.js +119 -0
  25. package/dist/build/candidate-cli.mjs +28220 -0
  26. package/dist/build/content-objects.d.ts +37 -0
  27. package/dist/build/content-objects.js +53 -0
  28. package/dist/build/effect.d.ts +18 -0
  29. package/dist/build/effect.js +52 -12
  30. package/dist/build/git-environment.d.ts +3 -0
  31. package/dist/build/git-environment.js +21 -9
  32. package/dist/build/local-build.d.ts +13 -3
  33. package/dist/build/local-build.js +149 -99
  34. package/dist/build/snapshot-package-store.d.ts +10 -0
  35. package/dist/build/snapshot-package-store.js +15 -0
  36. package/dist/build/snapshot-program-scripts.js +1 -1
  37. package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
  38. package/dist/build/snapshot-programs/snapshot-path.js +6 -0
  39. package/dist/build/submission-contract.d.ts +461 -17
  40. package/dist/build/submission-contract.js +136 -26
  41. package/dist/build/workspace-build.d.ts +7 -2
  42. package/dist/build/workspace-build.js +55 -11
  43. package/dist/build-contract.d.ts +3 -0
  44. package/dist/build-contract.js +3 -0
  45. package/dist/build-egress.d.ts +62 -0
  46. package/dist/build-egress.js +148 -0
  47. package/dist/build.d.ts +1 -0
  48. package/dist/build.js +1 -0
  49. package/dist/cloudflare.js +4 -4
  50. package/dist/frame-binding-wire-script.js +1 -1
  51. package/dist/frame-binding-wire.d.ts +1 -1
  52. package/dist/frame-binding-wire.js +1 -1
  53. package/dist/git-journal.js +4 -4
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +1 -0
  56. package/dist/installation-consent.d.ts +3 -1
  57. package/dist/installation-consent.js +2 -0
  58. package/dist/mcp-owner.js +20 -8
  59. package/dist/pack.d.ts +6 -3
  60. package/dist/pack.js +28 -11
  61. package/dist/page/gill-observation.d.ts +4 -0
  62. package/dist/page/gill-observation.js +31 -0
  63. package/dist/page/mount.d.ts +4 -8
  64. package/dist/page/window-launch.d.ts +18 -0
  65. package/dist/page/window-launch.js +11 -0
  66. package/dist/page-script.js +4 -4
  67. package/dist/release.d.ts +11 -0
  68. package/dist/release.js +4 -1
  69. package/dist/shell/app-directory.d.ts +50 -45
  70. package/dist/shell/app-directory.js +19 -18
  71. package/dist/shell/shell-module.d.ts +26 -2
  72. package/dist/shell/shell-module.js +51 -2
  73. package/dist/shell/window-manager.d.ts +5 -1
  74. package/dist/shell/window-manager.js +21 -2
  75. package/dist/shell/workbench-directory.d.ts +332 -0
  76. package/dist/shell/workbench-directory.js +35 -0
  77. package/dist/shell/workbench-host-types.d.ts +1 -1
  78. package/dist/shell/workbench-host.js +5 -0
  79. package/dist/shell/workbench-model.d.ts +3 -1
  80. package/dist/source/index.d.ts +6 -7
  81. package/dist/source/index.js +6 -7
  82. package/dist/source/template-push.d.ts +33 -0
  83. package/dist/source/template-push.js +223 -0
  84. package/dist/source/template-repository.d.ts +2 -0
  85. package/dist/source/template-repository.js +13 -0
  86. package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
  87. package/dist/source/template-seed.js +80 -0
  88. package/dist/standalone/agent-directory.d.ts +4 -0
  89. package/dist/standalone/agent-directory.js +4 -0
  90. package/dist/standalone/agents.d.ts +227 -0
  91. package/dist/standalone/agents.js +834 -0
  92. package/dist/standalone/app-builds-binding.d.ts +36 -0
  93. package/dist/standalone/app-builds-binding.js +119 -0
  94. package/dist/standalone/app-set.d.ts +91 -0
  95. package/dist/standalone/app-set.js +395 -0
  96. package/dist/standalone/app-source.d.ts +41 -0
  97. package/dist/standalone/app-source.js +59 -0
  98. package/dist/standalone/builds.d.ts +18 -0
  99. package/dist/standalone/builds.js +101 -0
  100. package/dist/standalone/computer-egress.d.ts +37 -0
  101. package/dist/standalone/computer-egress.js +111 -0
  102. package/dist/standalone/computer.d.ts +122 -0
  103. package/dist/standalone/computer.js +714 -0
  104. package/dist/standalone/install-apps-cli.d.ts +2 -0
  105. package/dist/standalone/install-apps-cli.js +126 -0
  106. package/dist/standalone/instance.d.ts +35 -0
  107. package/dist/standalone/instance.js +74 -0
  108. package/dist/standalone/media.d.ts +76 -0
  109. package/dist/standalone/media.js +227 -0
  110. package/dist/standalone/preview.d.ts +51 -0
  111. package/dist/standalone/preview.js +167 -0
  112. package/dist/standalone/registry.d.ts +102 -0
  113. package/dist/standalone/registry.js +193 -0
  114. package/dist/standalone/tenancy.d.ts +42 -0
  115. package/dist/standalone/tenancy.js +241 -0
  116. package/dist/standalone/worker.d.ts +55 -0
  117. package/dist/standalone/worker.js +147 -0
  118. package/dist/standalone/workflow-authority-readers.d.ts +16 -0
  119. package/dist/standalone/workflow-authority-readers.js +35 -0
  120. package/dist/standalone/workflow-binding.d.ts +216 -0
  121. package/dist/standalone/workflow-binding.js +217 -0
  122. package/dist/standalone/workflow-events.d.ts +46 -0
  123. package/dist/standalone/workflow-events.js +39 -0
  124. package/dist/standalone/workflow-git-capture.d.ts +7 -0
  125. package/dist/standalone/workflow-git-capture.js +62 -0
  126. package/dist/standalone/workflow-host.d.ts +955 -0
  127. package/dist/standalone/workflow-host.js +778 -0
  128. package/dist/standalone/workflow-node-capture.d.ts +13 -0
  129. package/dist/standalone/workflow-node-capture.js +72 -0
  130. package/dist/standalone/workflow-root-authority.d.ts +27 -0
  131. package/dist/standalone/workflow-root-authority.js +87 -0
  132. package/dist/standalone/workflow-schedules.d.ts +52 -0
  133. package/dist/standalone/workflow-schedules.js +40 -0
  134. package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
  135. package/dist/standalone/workflow-trigger-host.js +52 -0
  136. package/dist/standalone/workflows.d.ts +713 -0
  137. package/dist/standalone/workflows.js +554 -0
  138. package/dist/tenancy/auth0.d.ts +64 -0
  139. package/dist/tenancy/auth0.js +198 -0
  140. package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
  141. package/dist/tenancy/cloudflare/rate-limit.js +19 -0
  142. package/dist/tenancy/cloudflare/registry.d.ts +50 -0
  143. package/dist/tenancy/cloudflare/registry.js +301 -0
  144. package/dist/tenancy/cloudflare.d.ts +2 -0
  145. package/dist/tenancy/cloudflare.js +2 -0
  146. package/dist/tenancy/cold-directory.d.ts +13 -0
  147. package/dist/tenancy/cold-directory.js +43 -0
  148. package/dist/tenancy/contracts.d.ts +195 -0
  149. package/dist/tenancy/contracts.js +1 -0
  150. package/dist/tenancy/index.d.ts +19 -0
  151. package/dist/tenancy/index.js +156 -0
  152. package/dist/tenancy/owner-key.d.ts +26 -0
  153. package/dist/tenancy/owner-key.js +63 -0
  154. package/dist/tenancy/oyster.d.ts +4 -0
  155. package/dist/tenancy/oyster.js +25 -0
  156. package/dist/tenancy/registry-record.d.ts +7 -0
  157. package/dist/tenancy/registry-record.js +44 -0
  158. package/dist/tenancy/registry.d.ts +27 -0
  159. package/dist/tenancy/registry.js +138 -0
  160. package/dist/tenancy/resolve.d.ts +7 -0
  161. package/dist/tenancy/resolve.js +64 -0
  162. package/dist/workers-git.js +1 -4
  163. package/dist/workers.d.ts +2 -2
  164. package/dist/workers.js +3 -1
  165. package/docs/api/build-contract.md +829 -32
  166. package/docs/api/build.md +253 -17
  167. package/docs/api/modules.md +40 -32
  168. package/docs/api/pack.md +6 -3
  169. package/docs/api/shell/app-directory.md +267 -0
  170. package/docs/api/shell/shell-module.md +64 -3
  171. package/docs/api/shell/workbench-directory.md +119 -0
  172. package/docs/api/shell/workbench.md +19 -0
  173. package/docs/api/shell.md +127 -0
  174. package/docs/api/tenancy/auth0.md +110 -0
  175. package/docs/api/tenancy/cloudflare.md +221 -0
  176. package/docs/api/tenancy/cold-directory.md +46 -0
  177. package/docs/api/tenancy/owner-key.md +63 -0
  178. package/docs/api/tenancy/registry.md +77 -0
  179. package/docs/api/tenancy/resolve.md +69 -0
  180. package/docs/api/tenancy.md +430 -0
  181. package/docs/api/workers-git.md +140 -21
  182. package/docs/api/workers.md +15 -2
  183. package/docs/site/agent-quickstart.mdx +52 -55
  184. package/examples/standalone/worker.ts +7 -14
  185. package/examples/standalone/wrangler.jsonc +67 -6
  186. package/package.json +78 -25
  187. package/dist/build/r2-artifact-writer.d.ts +0 -31
  188. package/dist/build/r2-artifact-writer.js +0 -179
  189. package/dist/source/template-write.js +0 -344
  190. package/docs/media/architecture.mmd +0 -32
  191. package/docs/media/architecture.png +0 -0
  192. package/docs/media/bonsai-network.png +0 -0
  193. package/docs/media/bonsai-repository.png +0 -0
  194. package/examples/git/worker.ts +0 -39
  195. package/examples/git/wrangler.jsonc +0 -32
@@ -7,9 +7,10 @@ function logEvent(operation, cause) {
7
7
  failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
8
8
  }));
9
9
  }
10
- import { frozenSnapshotInstallArguments } from "./snapshot-package-store.js";
11
- import mime from "mime";
12
- import { assetPathSchema, assetBuildJsonSchema, decodeAssetBuild, } from "@fungi.computer/puffball/assets";
10
+ import { frozenSnapshotInstallArguments, packageScriptArguments, } from "./snapshot-package-store.js";
11
+ import { isInsideSnapshot } from "./snapshot-programs/snapshot-path.js";
12
+ import { writeImmutableArtifact, } from "./artifact-writer.js";
13
+ import { assetPathSchema, } from "@fungi.computer/puffball/assets";
13
14
  import { resolveWorkspaceBuild, workspaceProjectsArguments, } from "./workspace-build.js";
14
15
  import { readBundledHtml } from "./bundled-html.js";
15
16
  import { createHash, randomBytes } from "node:crypto";
@@ -22,13 +23,12 @@ import { pipeline } from "node:stream/promises";
22
23
  import { setTimeout as delay } from "node:timers/promises";
23
24
  import { parseArtifactRef, parseSha256Digest, parseSourceRevision, } from "@fungi.computer/puffball";
24
25
  import { appIdSchema } from "@fungi.computer/puffball/effect";
25
- import { appBuildJsonSchema, createAppBuild, } from "@fungi.computer/puffball/app";
26
26
  import * as Effect from "effect/Effect";
27
27
  import * as Schema from "effect/Schema";
28
28
  import * as esbuild from "esbuild";
29
29
  import { readWantedLockfile, writeWantedLockfile } from "@pnpm/lockfile.fs";
30
30
  import { pruneSharedLockfile } from "@pnpm/lockfile.pruner";
31
- import { AppPublisherError, appBackendExportConditions, createAppPublisher, pinnedRecipeSchema, } from "./effect.js";
31
+ import { AppPublisherError, appBackendExportConditions, createAppPublisher, pinnedRecipeSchema, pinsCurrentToolchain, } from "./effect.js";
32
32
  import { artifactsGitEnvironment, repositoryEnvironment, } from "./git-environment.js";
33
33
  const localConfigSchema = Schema.Struct({
34
34
  appId: appIdSchema,
@@ -44,9 +44,6 @@ function localRepositoryPath(input) {
44
44
  return input;
45
45
  throw new AppPublisherError("configuration");
46
46
  }
47
- function sha256(bytes) {
48
- return parseSha256Digest(`sha256:${createHash("sha256").update(bytes).digest("hex")}`);
49
- }
50
47
  async function sha256File(path, signal) {
51
48
  const hash = createHash("sha256");
52
49
  await pipeline(createReadStream(path), hash, { signal });
@@ -332,6 +329,63 @@ async function prunePnpmLockfile(snapshotPath, isIncludedPath) {
332
329
  await rm(scratch, { recursive: true, force: true });
333
330
  }
334
331
  }
332
+ /** The provenance file every distribution records beside the App source. */
333
+ function provenanceFile(appId, source) {
334
+ return {
335
+ path: "botanical-app-source.json",
336
+ contents: new TextEncoder().encode(JSON.stringify({
337
+ appId,
338
+ sourceRevision: source.sourceRevision,
339
+ sourceTreeDigest: source.sourceTreeDigest,
340
+ }, null, 2) + "\n"),
341
+ };
342
+ }
343
+ /** Builder state a source listing writes into the snapshot, never source. */
344
+ const snapshotScratch = new Set([".publisher-source-home", "node_modules"]);
345
+ /**
346
+ * An App repository's distribution: every captured file, unchanged, plus its
347
+ * provenance. The repository's own pnpm lockfile pins its registry packages.
348
+ */
349
+ function repositoryAppSourceDistribution(appId, source) {
350
+ return Effect.tryPromise({
351
+ try: async () => {
352
+ const snapshot = realpathSync(resolve(source.snapshotPath));
353
+ const collected = new Map();
354
+ const collect = async (relativePath) => {
355
+ const sourcePath = join(snapshot, relativePath);
356
+ const stats = await lstat(sourcePath);
357
+ if (stats.isSymbolicLink())
358
+ throw new Error("Source distribution cannot contain symbolic links");
359
+ if (stats.isFile()) {
360
+ collected.set(relativePath, await readFile(sourcePath));
361
+ return;
362
+ }
363
+ if (!stats.isDirectory())
364
+ throw new Error("Source distribution contains a non-file entry");
365
+ const entries = await readdir(sourcePath, { withFileTypes: true });
366
+ entries.sort((left, right) => left.name.localeCompare(right.name));
367
+ for (const entry of entries)
368
+ if (relativePath !== "" || !snapshotScratch.has(entry.name))
369
+ await collect(join(relativePath, entry.name));
370
+ };
371
+ await collect("");
372
+ const provenance = provenanceFile(appId, source);
373
+ if (collected.has(provenance.path))
374
+ throw new Error("Source distribution reserves its provenance path");
375
+ collected.set(provenance.path, provenance.contents);
376
+ return Object.freeze({
377
+ appId,
378
+ sourceRevision: source.sourceRevision,
379
+ sourceTreeDigest: source.sourceTreeDigest,
380
+ appDirectory: "",
381
+ files: [...collected]
382
+ .sort(([left], [right]) => left.localeCompare(right))
383
+ .map(([path, contents]) => Object.freeze({ path, contents })),
384
+ });
385
+ },
386
+ catch: (error) => error,
387
+ });
388
+ }
335
389
  /**
336
390
  * Project one pinned first-party App and its pnpm workspace dependency graph
337
391
  * without changing upstream files or manufacturing Git history. The caller
@@ -352,6 +406,10 @@ export function createFirstPartyAppSourceDistribution(options) {
352
406
  return Effect.gen(function* () {
353
407
  const recipe = yield* capturedWorkspaceBuild(snapshot, options.entry, (args) => runHtmlCommand(args, snapshot, env));
354
408
  const appDirectory = recipe.directory;
409
+ // An App's own repository is the App at its root: its editable template
410
+ // is that repository's whole captured tree, as forking it would copy.
411
+ if (appDirectory === "")
412
+ return yield* repositoryAppSourceDistribution(appId, options.source);
355
413
  const listed = yield* runHtmlCommand([
356
414
  ...productionWorkspaceSelectors(recipe),
357
415
  "list",
@@ -432,13 +490,10 @@ export function createFirstPartyAppSourceDistribution(options) {
432
490
  await collect("patches");
433
491
  if (collected.has("pnpm-lock.yaml"))
434
492
  collected.set("pnpm-lock.yaml", await prunePnpmLockfile(snapshot, (importerPath) => collected.has(join(importerPath, "package.json"))));
435
- if (collected.has("botanical-app-source.json"))
493
+ const provenance = provenanceFile(appId, options.source);
494
+ if (collected.has(provenance.path))
436
495
  throw new Error("Source distribution reserves its provenance path");
437
- collected.set("botanical-app-source.json", new TextEncoder().encode(JSON.stringify({
438
- appId,
439
- sourceRevision: options.source.sourceRevision,
440
- sourceTreeDigest: options.source.sourceTreeDigest,
441
- }, null, 2) + "\n"));
496
+ collected.set(provenance.path, provenance.contents);
442
497
  return [...collected]
443
498
  .sort(([left], [right]) => left.localeCompare(right))
444
499
  .map(([path, contents]) => Object.freeze({ path, contents }));
@@ -533,13 +588,13 @@ function runHtmlCommand(args, cwd, env, command = "pnpm") {
533
588
  function snapshotPrograms() {
534
589
  return import("#snapshot-program-scripts");
535
590
  }
536
- /** Compiler heap ceiling: fits a 6 GB Computer with room for pnpm and esbuild. */
591
+ /** Compiler heap ceiling: fits a 12 GiB Computer with two builds at once. */
537
592
  const snapshotBuildHeapMegabytes = 4096;
538
593
  function snapshotBuildEnvironment(sourceSnapshotPath) {
539
594
  // PATH locates pinned executables; the Computer's CA files authenticate
540
595
  // its egress gateway. Credentials and package-manager settings stay out.
541
- // V8 sizes its default heap from the machine, about 2 GB in a 6 GB
542
- // standard-2 Computer, where compiling Bonsai runs out; the build owns one
596
+ // V8 sizes its default heap from the machine, about 3 GB in a 12 GiB
597
+ // standard-4 Computer, where compiling Bonsai runs out; the build owns one
543
598
  // fixed ceiling instead.
544
599
  const env = {
545
600
  PATH: process.env.PATH ?? "/usr/bin:/bin",
@@ -553,7 +608,7 @@ function snapshotBuildEnvironment(sourceSnapshotPath) {
553
608
  };
554
609
  return env;
555
610
  }
556
- function rebuildViteSnapshot(sourceSnapshotPath, recipe, packageStoreDirectory) {
611
+ function rebuildViteSnapshot(sourceSnapshotPath, recipe, options) {
557
612
  if (recipe.kind === "source-html")
558
613
  return rebuildNpmSnapshot(sourceSnapshotPath, recipe);
559
614
  return Effect.gen(function* () {
@@ -567,10 +622,16 @@ function rebuildViteSnapshot(sourceSnapshotPath, recipe, packageStoreDirectory)
567
622
  try: () => checkPinnedWorkspaceToolchain(sourceSnapshotPath, html, recipe),
568
623
  catch: (error) => error,
569
624
  });
625
+ if (options.packageScripts !== "run" &&
626
+ html.steps.some((step) => step.kind === "script"))
627
+ return yield* Effect.fail(new Error("This source builds a dependency with its package script, which only a Computer build runs"));
628
+ const installStarted = performance.now();
570
629
  yield* run([
571
630
  ...productionWorkspaceSelectors(html),
572
- ...frozenSnapshotInstallArguments(packageStoreDirectory),
631
+ ...frozenSnapshotInstallArguments(options.packageStoreDirectory),
573
632
  ]);
633
+ const buildStarted = performance.now();
634
+ options.onPhase?.("install", buildStarted - installStarted);
574
635
  const appDirectory = join(sourceSnapshotPath, html.directory);
575
636
  const dependencies = join(appDirectory, ".publisher-dependencies.mjs");
576
637
  const program = join(appDirectory, ".publisher-build.mjs");
@@ -593,8 +654,23 @@ function rebuildViteSnapshot(sourceSnapshotPath, recipe, packageStoreDirectory)
593
654
  else
594
655
  groups.push([step]);
595
656
  }
596
- for (const group of groups)
657
+ for (const group of groups) {
658
+ const [step] = group;
659
+ if (step?.kind === "script") {
660
+ // Exactly the named script, in its own package: no pre/post hooks or
661
+ // implicit install, whatever the captured repository's pnpm settings
662
+ // ask for. The script is arbitrary package code: its isolation is the
663
+ // admitted Computer, not this builder's compiler path checks.
664
+ const directory = resolve(sourceSnapshotPath, step.directory);
665
+ if (!isInsideSnapshot(sourceSnapshotPath, directory))
666
+ return yield* Effect.fail(new Error("Package script directory escapes the captured source"));
667
+ yield* run([...packageScriptArguments, step.script], directory);
668
+ continue;
669
+ }
597
670
  yield* run([
671
+ // The frozen install above is the only install, whatever the
672
+ // captured pnpm settings ask `exec` to verify first.
673
+ "--config.verify-deps-before-run=false",
598
674
  "exec",
599
675
  "node",
600
676
  dependencies,
@@ -602,8 +678,11 @@ function rebuildViteSnapshot(sourceSnapshotPath, recipe, packageStoreDirectory)
602
678
  html.directory,
603
679
  recipe.format,
604
680
  JSON.stringify(group),
681
+ tailwindToolchainDirectory(html),
605
682
  ], appDirectory);
683
+ }
606
684
  yield* run([
685
+ "--config.verify-deps-before-run=false",
607
686
  "exec",
608
687
  "node",
609
688
  program,
@@ -616,9 +695,20 @@ function rebuildViteSnapshot(sourceSnapshotPath, recipe, packageStoreDirectory)
616
695
  String(html.react),
617
696
  html.shellEntry ?? "",
618
697
  ], appDirectory);
698
+ options.onPhase?.("build", performance.now() - buildStarted);
619
699
  return yield* readHtmlBuildOutput(sourceSnapshotPath, recipe);
620
700
  });
621
701
  }
702
+ /**
703
+ * Where the captured Tailwind compiler lives: Caps, when the App builds Caps
704
+ * from its workspace, or else the App itself, which then pins the compiler
705
+ * and installs Caps' built stylesheet from a registry.
706
+ */
707
+ function tailwindToolchainDirectory(html) {
708
+ return html.dependencies.includes("packages/caps")
709
+ ? "packages/caps"
710
+ : html.directory;
711
+ }
622
712
  const pinnedVersionsSchema = Schema.Record(Schema.String, Schema.String);
623
713
  const pinnedManifestSchema = Schema.fromJsonString(Schema.Struct({
624
714
  dependencies: Schema.optional(pinnedVersionsSchema),
@@ -656,9 +746,13 @@ async function checkPinnedWorkspaceToolchain(sourceSnapshotPath, html, recipe) {
656
746
  if (stylesheets.length === 0)
657
747
  return;
658
748
  // The captured Tailwind compiler and its typography plugin come from Caps.
659
- const caps = await manifest("packages/caps");
660
- pinned("packages/caps", caps.devDependencies, "@tailwindcss/cli", "4.2.1");
661
- pinned("packages/caps", caps.dependencies, "@tailwindcss/typography", "0.5.19");
749
+ // An App that installs Caps from a registry pins the compiler itself, and
750
+ // Caps' own dependencies bring the typography plugin its stylesheet uses.
751
+ const toolchain = tailwindToolchainDirectory(html);
752
+ const compiler = await manifest(toolchain);
753
+ pinned(toolchain, compiler.devDependencies, "@tailwindcss/cli", "4.2.1");
754
+ if (toolchain === "packages/caps")
755
+ pinned(toolchain, compiler.dependencies, "@tailwindcss/typography", "0.5.19");
662
756
  for (const directory of html.dependencies) {
663
757
  if (!stylesheets.some((step) => step.input.startsWith(`${directory}/`)))
664
758
  continue;
@@ -876,7 +970,7 @@ export function createLocalBuildExecutor(options = {}) {
876
970
  try: () => readBundledHtml(source.snapshotPath, recipe),
877
971
  catch: () => new Error("Captured HTML bundle failed validation"),
878
972
  })
879
- : rebuildViteSnapshot(source.snapshotPath, recipe, options.packageStoreDirectory);
973
+ : rebuildViteSnapshot(source.snapshotPath, recipe, options);
880
974
  const code = yield* compiler.build(source, {
881
975
  bundle: true,
882
976
  entry,
@@ -905,7 +999,7 @@ export function createLocalBuildExecutor(options = {}) {
905
999
  });
906
1000
  }
907
1001
  return recipe.format !== "esm"
908
- ? rebuildViteSnapshot(source.snapshotPath, recipe, options.packageStoreDirectory)
1002
+ ? rebuildViteSnapshot(source.snapshotPath, recipe, options)
909
1003
  : Effect.acquireUseRelease(Effect.tryPromise({
910
1004
  try: () => {
911
1005
  const emission = emissions.get(source);
@@ -935,6 +1029,8 @@ export function createLocalBuildExecutor(options = {}) {
935
1029
  const declarations = new WeakMap();
936
1030
  return {
937
1031
  build: (source, recipe) => Effect.gen(function* () {
1032
+ if (!pinsCurrentToolchain(recipe))
1033
+ return yield* Effect.fail(new Error("Recipe pins another build toolchain or policy"));
938
1034
  const emission = yield* Effect.tryPromise({
939
1035
  try: () => createDefinitionEmission(source, recipe),
940
1036
  catch: (cause) => cause instanceof Error
@@ -960,83 +1056,37 @@ export function createLocalBuildExecutor(options = {}) {
960
1056
  },
961
1057
  };
962
1058
  }
1059
+ /** The configured artifact directory as write-once immutable objects. */
1060
+ function directoryArtifactObjects(artifactRoot, signal) {
1061
+ const pathOf = (artifactRef) => join(artifactRoot, artifactRef.slice("artifact:".length));
1062
+ return {
1063
+ async create(artifactRef, bytes) {
1064
+ try {
1065
+ await writeFile(pathOf(artifactRef), bytes, { flag: "wx", signal });
1066
+ return "created";
1067
+ }
1068
+ catch (error) {
1069
+ if (error instanceof Error &&
1070
+ "code" in error &&
1071
+ error.code === "EEXIST")
1072
+ return "exists";
1073
+ throw error;
1074
+ }
1075
+ finally {
1076
+ signal.throwIfAborted();
1077
+ }
1078
+ },
1079
+ async read(artifactRef) {
1080
+ return new Uint8Array(await readFile(pathOf(artifactRef)));
1081
+ },
1082
+ };
1083
+ }
963
1084
  async function writeArtifact(output, artifactRoot, signal, assetPolicy) {
964
1085
  signal.throwIfAborted();
965
- const ui = output.kind === "backend" ? null : output.ui;
966
1086
  await mkdir(artifactRoot, { recursive: true });
967
- const persist = async (bytes, ref) => {
968
- const digest = sha256(bytes);
969
- const artifactRef = parseArtifactRef(ref ?? `artifact:sha256-${digest.slice("sha256:".length)}`);
970
- const artifactPath = join(artifactRoot, artifactRef.slice("artifact:".length));
971
- try {
972
- await writeFile(artifactPath, bytes, { flag: "wx", signal });
973
- }
974
- catch (error) {
975
- const code = error instanceof Error && "code" in error ? error.code : undefined;
976
- if (code !== "EEXIST" || sha256(await readFile(artifactPath)) !== digest)
977
- throw error;
978
- }
979
- signal.throwIfAborted();
980
- return {
981
- artifactRef,
982
- digest,
983
- version: digest,
984
- byteLength: bytes.byteLength,
985
- };
986
- };
987
- const uiBytes = await (async () => {
988
- if (ui === null)
989
- return null;
990
- if (ui.content.kind === "html")
991
- return ui.content.bytes.slice();
992
- if (!assetPolicy)
993
- throw new Error("Asset writer requires explicit policy");
994
- const content = {};
995
- for (const asset of ui.content.assets) {
996
- const path = Schema.decodeUnknownSync(assetPathSchema)(asset.path);
997
- if (Object.hasOwn(content, path))
998
- throw new Error("Duplicate build path");
999
- content[path] = new Uint8Array(asset.bytes).buffer;
1000
- }
1001
- const assets = [];
1002
- for (const path of Object.keys(content).sort()) {
1003
- const bytes = content[path];
1004
- const mediaType = mime.getType(path);
1005
- const contentType = mediaType?.startsWith("text/")
1006
- ? `${mediaType}; charset=utf-8`
1007
- : mediaType;
1008
- if (!bytes || !contentType)
1009
- throw new Error("Build asset has no maintained content type");
1010
- assets.push({
1011
- path,
1012
- ...(await persist(new Uint8Array(bytes))),
1013
- contentType,
1014
- });
1015
- }
1016
- const manifest = new TextEncoder().encode(Schema.encodeUnknownSync(assetBuildJsonSchema)({
1017
- format: "fungi-assets-v1",
1018
- base: ui.content.base,
1019
- assets,
1020
- }));
1021
- decodeAssetBuild(manifest, assetPolicy);
1022
- return manifest;
1023
- })();
1024
- const storedUi = ui === null || uiBytes === null
1025
- ? null
1026
- : { ...(await persist(uiBytes)), mediaType: ui.mediaType };
1027
- const backendBytes = output.kind === "ui" ? null : output.backend;
1028
- const storedBackend = backendBytes === null
1029
- ? null
1030
- : await persist(backendBytes.slice()).then((stored) => ({
1031
- ...stored,
1032
- mediaType: "application/vnd.fungi.app-backend+json",
1033
- }));
1034
- const root = new TextEncoder().encode(Schema.encodeUnknownSync(appBuildJsonSchema)(createAppBuild(storedUi, storedBackend, output.declaration)));
1035
- const rootRef = `artifact:${Buffer.from(randomBytes(16)).toString("hex")}`;
1036
- const storedRoot = await persist(root, rootRef);
1037
- return Object.freeze({
1038
- artifactRef: storedRoot.artifactRef,
1039
- artifactDigest: storedRoot.digest,
1087
+ return writeImmutableArtifact(output, directoryArtifactObjects(artifactRoot, signal), {
1088
+ rootArtifactRef: parseArtifactRef(`artifact:${Buffer.from(randomBytes(16)).toString("hex")}`),
1089
+ assetPolicy,
1040
1090
  });
1041
1091
  }
1042
1092
  /** Local proving adapter; the configured root is private and write-once. */
@@ -1,2 +1,12 @@
1
1
  /** Frozen hydration policy. Cached bytes are verified and copied into each snapshot. */
2
2
  export declare function frozenSnapshotInstallArguments(packageStoreDirectory?: string): string[];
3
+ /**
4
+ * `pnpm run` for one declared package build script, which follows `--` so
5
+ * pnpm selects that literal name (never an option or a `/pattern/`). pnpm 11
6
+ * runs `pre<name>`/`post<name>` by default, and installs first when it judges
7
+ * dependencies stale, running their lifecycle scripts; a captured `.npmrc` or
8
+ * `pnpm-workspace.yaml` may also ask for either. These command line settings
9
+ * win, so the frozen, script-free install stays the only install. What the
10
+ * named script itself runs is that package's own build.
11
+ */
12
+ export declare const packageScriptArguments: readonly string[];
@@ -19,3 +19,18 @@ export function frozenSnapshotInstallArguments(packageStoreDirectory) {
19
19
  "--reporter=append-only",
20
20
  ];
21
21
  }
22
+ /**
23
+ * `pnpm run` for one declared package build script, which follows `--` so
24
+ * pnpm selects that literal name (never an option or a `/pattern/`). pnpm 11
25
+ * runs `pre<name>`/`post<name>` by default, and installs first when it judges
26
+ * dependencies stale, running their lifecycle scripts; a captured `.npmrc` or
27
+ * `pnpm-workspace.yaml` may also ask for either. These command line settings
28
+ * win, so the frozen, script-free install stays the only install. What the
29
+ * named script itself runs is that package's own build.
30
+ */
31
+ export const packageScriptArguments = Object.freeze([
32
+ "run",
33
+ "--config.enable-pre-post-scripts=false",
34
+ "--config.verify-deps-before-run=false",
35
+ "--",
36
+ ]);