@fungi.computer/oyster 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1134 -2
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +322 -0
  5. package/dist/app-builds.d.ts +210 -0
  6. package/dist/app-builds.js +499 -0
  7. package/dist/app-sandbox.d.ts +2 -0
  8. package/dist/app-sandbox.js +2 -0
  9. package/dist/app-sources.d.ts +31 -0
  10. package/dist/app-sources.js +370 -0
  11. package/dist/app-storage.d.ts +141 -0
  12. package/dist/app-storage.js +281 -0
  13. package/dist/apps/gate.d.ts +105 -0
  14. package/dist/apps/gate.js +273 -0
  15. package/dist/apps/index.d.ts +5 -0
  16. package/dist/apps/index.js +5 -0
  17. package/dist/apps/installation.d.ts +248 -0
  18. package/dist/apps/installation.js +307 -0
  19. package/dist/apps/operations.d.ts +8 -0
  20. package/dist/apps/operations.js +98 -0
  21. package/dist/apps/tools.d.ts +33 -0
  22. package/dist/apps/tools.js +17 -0
  23. package/dist/auth.d.ts +11 -0
  24. package/dist/auth.js +93 -0
  25. package/dist/backend-request.d.ts +3 -0
  26. package/dist/backend-request.js +53 -0
  27. package/dist/build/artifact-writer.d.ts +40 -0
  28. package/dist/build/artifact-writer.js +181 -0
  29. package/dist/build/build-jobs.d.ts +128 -0
  30. package/dist/build/build-jobs.js +203 -0
  31. package/dist/build/build-policy.d.ts +10 -0
  32. package/dist/build/build-policy.js +10 -0
  33. package/dist/build/build-step.d.ts +37 -0
  34. package/dist/build/build-step.js +28 -0
  35. package/dist/build/bundled-html.d.ts +7 -0
  36. package/dist/build/bundled-html.js +64 -0
  37. package/dist/build/candidate-builder.d.ts +68 -0
  38. package/dist/build/candidate-builder.js +91 -0
  39. package/dist/build/candidate-cli.d.ts +1 -0
  40. package/dist/build/candidate-cli.js +119 -0
  41. package/dist/build/candidate-cli.mjs +28170 -0
  42. package/dist/build/content-objects.d.ts +37 -0
  43. package/dist/build/content-objects.js +53 -0
  44. package/dist/build/definition-emission.d.ts +34 -0
  45. package/dist/build/definition-emission.js +201 -0
  46. package/dist/build/effect.d.ts +249 -0
  47. package/dist/build/effect.js +436 -0
  48. package/dist/build/git-environment.d.ts +9 -0
  49. package/dist/build/git-environment.js +60 -0
  50. package/dist/build/html-recipe.d.ts +20 -0
  51. package/dist/build/html-recipe.js +51 -0
  52. package/dist/build/local-build.d.ts +67 -0
  53. package/dist/build/local-build.js +1128 -0
  54. package/dist/build/snapshot-package-store.d.ts +12 -0
  55. package/dist/build/snapshot-package-store.js +36 -0
  56. package/dist/build/snapshot-program-scripts.d.ts +6 -0
  57. package/dist/build/snapshot-program-scripts.js +2 -0
  58. package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
  59. package/dist/build/snapshot-programs/snapshot-path.js +6 -0
  60. package/dist/build/submission-contract.d.ts +828 -0
  61. package/dist/build/submission-contract.js +180 -0
  62. package/dist/build/workspace-build.d.ts +55 -0
  63. package/dist/build/workspace-build.js +200 -0
  64. package/dist/build-contract.d.ts +7 -0
  65. package/dist/build-contract.js +7 -0
  66. package/dist/build-egress.d.ts +62 -0
  67. package/dist/build-egress.js +148 -0
  68. package/dist/build-jobs.d.ts +2 -0
  69. package/dist/build-jobs.js +2 -0
  70. package/dist/build.d.ts +12 -0
  71. package/dist/build.js +14 -0
  72. package/dist/capability-descriptor.d.ts +46 -0
  73. package/dist/capability-descriptor.js +51 -0
  74. package/dist/catalog-frame.d.ts +39 -0
  75. package/dist/catalog-frame.js +72 -0
  76. package/dist/cause-class.d.ts +19 -0
  77. package/dist/cause-class.js +54 -0
  78. package/dist/cloudflare.d.ts +123 -0
  79. package/dist/cloudflare.js +391 -0
  80. package/dist/computer-terminal.d.ts +38 -0
  81. package/dist/computer-terminal.js +410 -0
  82. package/dist/computer.d.ts +60 -0
  83. package/dist/computer.js +33 -0
  84. package/dist/event-recipients.d.ts +57 -0
  85. package/dist/event-recipients.js +144 -0
  86. package/dist/facet-invocation.d.ts +4 -0
  87. package/dist/facet-invocation.js +45 -0
  88. package/dist/files.d.ts +42 -0
  89. package/dist/files.js +123 -0
  90. package/dist/frame-binding-wire-script.d.ts +2 -0
  91. package/dist/frame-binding-wire-script.js +1 -0
  92. package/dist/frame-binding-wire.d.ts +28 -0
  93. package/dist/frame-binding-wire.js +54 -0
  94. package/dist/git-admission.d.ts +52 -0
  95. package/dist/git-admission.js +188 -0
  96. package/dist/git-auth.d.ts +11 -0
  97. package/dist/git-auth.js +27 -0
  98. package/dist/git-door.d.ts +35 -0
  99. package/dist/git-door.js +108 -0
  100. package/dist/git-journal.d.ts +31 -0
  101. package/dist/git-journal.js +188 -0
  102. package/dist/git.d.ts +31 -0
  103. package/dist/git.js +64 -0
  104. package/dist/guest-policy.d.ts +9 -0
  105. package/dist/guest-policy.js +13 -0
  106. package/dist/host.d.ts +244 -0
  107. package/dist/host.js +1043 -0
  108. package/dist/index.d.ts +166 -0
  109. package/dist/index.js +320 -0
  110. package/dist/installation-consent.d.ts +79 -0
  111. package/dist/installation-consent.js +82 -0
  112. package/dist/internal/background-origins.d.ts +3 -0
  113. package/dist/internal/background-origins.js +8 -0
  114. package/dist/internal/delivery.d.ts +12 -0
  115. package/dist/internal/delivery.js +167 -0
  116. package/dist/internal/host-gate.d.ts +14 -0
  117. package/dist/internal/host-gate.js +62 -0
  118. package/dist/internal/registry-installations.d.ts +6 -0
  119. package/dist/internal/registry-installations.js +12 -0
  120. package/dist/invocation.d.ts +251 -0
  121. package/dist/invocation.js +377 -0
  122. package/dist/keys.d.ts +48 -0
  123. package/dist/keys.js +52 -0
  124. package/dist/mcp-owner.d.ts +27 -0
  125. package/dist/mcp-owner.js +155 -0
  126. package/dist/mcp.d.ts +89 -0
  127. package/dist/mcp.js +429 -0
  128. package/dist/media.d.ts +23 -0
  129. package/dist/media.js +12 -0
  130. package/dist/pack.d.ts +79 -0
  131. package/dist/pack.js +151 -0
  132. package/dist/page/capabilities.d.ts +23 -0
  133. package/dist/page/capabilities.js +256 -0
  134. package/dist/page/computer-handles.d.ts +79 -0
  135. package/dist/page/computer-handles.js +44 -0
  136. package/dist/page/gill-observation.d.ts +4 -0
  137. package/dist/page/gill-observation.js +31 -0
  138. package/dist/page/lifetime.d.ts +6 -0
  139. package/dist/page/lifetime.js +19 -0
  140. package/dist/page/mount.d.ts +163 -0
  141. package/dist/page/mount.js +390 -0
  142. package/dist/page/storage.d.ts +3 -0
  143. package/dist/page/storage.js +82 -0
  144. package/dist/page/terminal.d.ts +5 -0
  145. package/dist/page/terminal.js +7 -0
  146. package/dist/page/window-launch.d.ts +18 -0
  147. package/dist/page/window-launch.js +11 -0
  148. package/dist/page-script.d.ts +14 -0
  149. package/dist/page-script.js +5 -0
  150. package/dist/r2-files.d.ts +3 -0
  151. package/dist/r2-files.js +79 -0
  152. package/dist/release.d.ts +83 -0
  153. package/dist/release.js +167 -0
  154. package/dist/root-data.d.ts +86 -0
  155. package/dist/root-data.js +69 -0
  156. package/dist/shell/app-directory.d.ts +542 -0
  157. package/dist/shell/app-directory.js +199 -0
  158. package/dist/shell/browser-actions.d.ts +7 -0
  159. package/dist/shell/browser-actions.js +56 -0
  160. package/dist/shell/command-host.d.ts +112 -0
  161. package/dist/shell/command-host.js +502 -0
  162. package/dist/shell/commands.d.ts +25 -0
  163. package/dist/shell/commands.js +78 -0
  164. package/dist/shell/index.d.ts +8 -0
  165. package/dist/shell/index.js +8 -0
  166. package/dist/shell/keys.d.ts +41 -0
  167. package/dist/shell/keys.js +134 -0
  168. package/dist/shell/shell-module.d.ts +55 -0
  169. package/dist/shell/shell-module.js +86 -0
  170. package/dist/shell/window-manager.d.ts +124 -0
  171. package/dist/shell/window-manager.js +711 -0
  172. package/dist/shell/workbench-contract.d.ts +68 -0
  173. package/dist/shell/workbench-contract.js +1 -0
  174. package/dist/shell/workbench-directory.d.ts +332 -0
  175. package/dist/shell/workbench-directory.js +35 -0
  176. package/dist/shell/workbench-host-types.d.ts +96 -0
  177. package/dist/shell/workbench-host-types.js +1 -0
  178. package/dist/shell/workbench-host.d.ts +34 -0
  179. package/dist/shell/workbench-host.js +347 -0
  180. package/dist/shell/workbench-model.d.ts +192 -0
  181. package/dist/shell/workbench-model.js +53 -0
  182. package/dist/shell/workbench.d.ts +4 -0
  183. package/dist/shell/workbench.js +2 -0
  184. package/dist/source/fork.d.ts +27 -0
  185. package/dist/source/fork.js +46 -0
  186. package/dist/source/index.d.ts +10 -0
  187. package/dist/source/index.js +10 -0
  188. package/dist/source/seed-receipt.d.ts +53 -0
  189. package/dist/source/seed-receipt.js +18 -0
  190. package/dist/source/team-source.d.ts +32 -0
  191. package/dist/source/team-source.js +42 -0
  192. package/dist/source/template-push.d.ts +33 -0
  193. package/dist/source/template-push.js +223 -0
  194. package/dist/source/template-repository.d.ts +4 -0
  195. package/dist/source/template-repository.js +21 -0
  196. package/dist/source/template-seed.d.ts +68 -0
  197. package/dist/source/template-seed.js +80 -0
  198. package/dist/standalone/agent-directory.d.ts +4 -0
  199. package/dist/standalone/agent-directory.js +4 -0
  200. package/dist/standalone/agents.d.ts +220 -0
  201. package/dist/standalone/agents.js +795 -0
  202. package/dist/standalone/app-builds-binding.d.ts +36 -0
  203. package/dist/standalone/app-builds-binding.js +119 -0
  204. package/dist/standalone/app-set.d.ts +91 -0
  205. package/dist/standalone/app-set.js +395 -0
  206. package/dist/standalone/app-source.d.ts +41 -0
  207. package/dist/standalone/app-source.js +59 -0
  208. package/dist/standalone/builds.d.ts +18 -0
  209. package/dist/standalone/builds.js +101 -0
  210. package/dist/standalone/computer-egress.d.ts +37 -0
  211. package/dist/standalone/computer-egress.js +111 -0
  212. package/dist/standalone/computer.d.ts +122 -0
  213. package/dist/standalone/computer.js +714 -0
  214. package/dist/standalone/install-apps-cli.d.ts +2 -0
  215. package/dist/standalone/install-apps-cli.js +126 -0
  216. package/dist/standalone/instance.d.ts +35 -0
  217. package/dist/standalone/instance.js +74 -0
  218. package/dist/standalone/media.d.ts +76 -0
  219. package/dist/standalone/media.js +227 -0
  220. package/dist/standalone/preview.d.ts +51 -0
  221. package/dist/standalone/preview.js +167 -0
  222. package/dist/standalone/registry.d.ts +102 -0
  223. package/dist/standalone/registry.js +193 -0
  224. package/dist/standalone/tenancy.d.ts +42 -0
  225. package/dist/standalone/tenancy.js +241 -0
  226. package/dist/standalone/worker.d.ts +55 -0
  227. package/dist/standalone/worker.js +147 -0
  228. package/dist/standalone/workflow-authority-readers.d.ts +16 -0
  229. package/dist/standalone/workflow-authority-readers.js +35 -0
  230. package/dist/standalone/workflow-binding.d.ts +216 -0
  231. package/dist/standalone/workflow-binding.js +217 -0
  232. package/dist/standalone/workflow-events.d.ts +46 -0
  233. package/dist/standalone/workflow-events.js +39 -0
  234. package/dist/standalone/workflow-git-capture.d.ts +7 -0
  235. package/dist/standalone/workflow-git-capture.js +62 -0
  236. package/dist/standalone/workflow-host.d.ts +931 -0
  237. package/dist/standalone/workflow-host.js +751 -0
  238. package/dist/standalone/workflow-node-capture.d.ts +13 -0
  239. package/dist/standalone/workflow-node-capture.js +72 -0
  240. package/dist/standalone/workflow-root-authority.d.ts +27 -0
  241. package/dist/standalone/workflow-root-authority.js +87 -0
  242. package/dist/standalone/workflow-schedules.d.ts +52 -0
  243. package/dist/standalone/workflow-schedules.js +40 -0
  244. package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
  245. package/dist/standalone/workflow-trigger-host.js +52 -0
  246. package/dist/standalone/workflows.d.ts +657 -0
  247. package/dist/standalone/workflows.js +554 -0
  248. package/dist/storage-frame.d.ts +64 -0
  249. package/dist/storage-frame.js +28 -0
  250. package/dist/tenancy/auth0.d.ts +64 -0
  251. package/dist/tenancy/auth0.js +198 -0
  252. package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
  253. package/dist/tenancy/cloudflare/rate-limit.js +19 -0
  254. package/dist/tenancy/cloudflare/registry.d.ts +50 -0
  255. package/dist/tenancy/cloudflare/registry.js +301 -0
  256. package/dist/tenancy/cloudflare.d.ts +2 -0
  257. package/dist/tenancy/cloudflare.js +2 -0
  258. package/dist/tenancy/cold-directory.d.ts +13 -0
  259. package/dist/tenancy/cold-directory.js +43 -0
  260. package/dist/tenancy/contracts.d.ts +195 -0
  261. package/dist/tenancy/contracts.js +1 -0
  262. package/dist/tenancy/index.d.ts +19 -0
  263. package/dist/tenancy/index.js +156 -0
  264. package/dist/tenancy/owner-key.d.ts +26 -0
  265. package/dist/tenancy/owner-key.js +63 -0
  266. package/dist/tenancy/oyster.d.ts +4 -0
  267. package/dist/tenancy/oyster.js +25 -0
  268. package/dist/tenancy/registry-record.d.ts +7 -0
  269. package/dist/tenancy/registry-record.js +44 -0
  270. package/dist/tenancy/registry.d.ts +27 -0
  271. package/dist/tenancy/registry.js +138 -0
  272. package/dist/tenancy/resolve.d.ts +7 -0
  273. package/dist/tenancy/resolve.js +64 -0
  274. package/dist/tenant.d.ts +81 -0
  275. package/dist/tenant.js +71 -0
  276. package/dist/testing/capabilities.d.ts +29 -0
  277. package/dist/testing/capabilities.js +130 -0
  278. package/dist/testing/catalog.d.ts +3 -0
  279. package/dist/testing/catalog.js +53 -0
  280. package/dist/testing/index.d.ts +10 -0
  281. package/dist/testing/index.js +10 -0
  282. package/dist/testing/installation.d.ts +11 -0
  283. package/dist/testing/installation.js +25 -0
  284. package/dist/testing/native-worker.d.ts +21 -0
  285. package/dist/testing/native-worker.js +106 -0
  286. package/dist/testing/worker.d.ts +18 -0
  287. package/dist/testing/worker.js +67 -0
  288. package/dist/testing/workers-runtime.d.ts +24 -0
  289. package/dist/testing/workers-runtime.js +33 -0
  290. package/dist/webhook-delivery.d.ts +14 -0
  291. package/dist/webhook-delivery.js +17 -0
  292. package/dist/workers-git.d.ts +285 -0
  293. package/dist/workers-git.js +1174 -0
  294. package/dist/workers.d.ts +352 -0
  295. package/dist/workers.js +868 -0
  296. package/docs/api/README.md +82 -0
  297. package/docs/api/agents.md +332 -0
  298. package/docs/api/app-storage.md +209 -0
  299. package/docs/api/apps.md +1375 -0
  300. package/docs/api/auth.md +63 -0
  301. package/docs/api/build-contract.md +1304 -0
  302. package/docs/api/build.md +1712 -0
  303. package/docs/api/cloudflare.md +421 -0
  304. package/docs/api/computer.md +203 -0
  305. package/docs/api/files.md +117 -0
  306. package/docs/api/git.md +470 -0
  307. package/docs/api/host.md +1328 -0
  308. package/docs/api/index.md +430 -0
  309. package/docs/api/invocation.md +738 -0
  310. package/docs/api/keys.md +175 -0
  311. package/docs/api/mcp.md +301 -0
  312. package/docs/api/media.md +40 -0
  313. package/docs/api/modules.md +50 -0
  314. package/docs/api/pack.md +141 -0
  315. package/docs/api/page/mount.md +298 -0
  316. package/docs/api/root-data.md +111 -0
  317. package/docs/api/shell/app-directory.md +599 -0
  318. package/docs/api/shell/command-host.md +224 -0
  319. package/docs/api/shell/commands.md +197 -0
  320. package/docs/api/shell/keys.md +183 -0
  321. package/docs/api/shell/shell-module.md +204 -0
  322. package/docs/api/shell/window-manager.md +267 -0
  323. package/docs/api/shell/workbench-directory.md +119 -0
  324. package/docs/api/shell/workbench.md +205 -0
  325. package/docs/api/shell.md +1915 -0
  326. package/docs/api/tenancy/auth0.md +110 -0
  327. package/docs/api/tenancy/cloudflare.md +221 -0
  328. package/docs/api/tenancy/cold-directory.md +46 -0
  329. package/docs/api/tenancy/owner-key.md +63 -0
  330. package/docs/api/tenancy/registry.md +77 -0
  331. package/docs/api/tenancy/resolve.md +69 -0
  332. package/docs/api/tenancy.md +430 -0
  333. package/docs/api/tenant.md +194 -0
  334. package/docs/api/workers-git.md +1646 -0
  335. package/docs/api/workers.md +1691 -0
  336. package/docs/site/agent-quickstart.mdx +346 -0
  337. package/examples/standalone/worker.ts +7 -0
  338. package/examples/standalone/wrangler.jsonc +82 -0
  339. package/package.json +300 -3
@@ -0,0 +1,1128 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { appBackendPackageMainFields, createDefinitionEmission, } from "./definition-emission.js";
3
+ function logEvent(operation, cause) {
4
+ console.error(JSON.stringify({
5
+ source: "app-publisher",
6
+ operation,
7
+ failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
8
+ }));
9
+ }
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";
14
+ import { resolveWorkspaceBuild, workspaceProjectsArguments, } from "./workspace-build.js";
15
+ import { readBundledHtml } from "./bundled-html.js";
16
+ import { createHash, randomBytes } from "node:crypto";
17
+ import { spawn } from "node:child_process";
18
+ import { createReadStream, mkdtempSync, realpathSync } from "node:fs";
19
+ import { access, cp, lstat, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
20
+ import { tmpdir } from "node:os";
21
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
22
+ import { pipeline } from "node:stream/promises";
23
+ import { setTimeout as delay } from "node:timers/promises";
24
+ import { parseArtifactRef, parseSha256Digest, parseSourceRevision, } from "@fungi.computer/puffball";
25
+ import { appIdSchema } from "@fungi.computer/puffball/effect";
26
+ import * as Effect from "effect/Effect";
27
+ import * as Schema from "effect/Schema";
28
+ import * as esbuild from "esbuild";
29
+ import { readWantedLockfile, writeWantedLockfile } from "@pnpm/lockfile.fs";
30
+ import { pruneSharedLockfile } from "@pnpm/lockfile.pruner";
31
+ import { AppPublisherError, appBackendExportConditions, createAppPublisher, pinnedRecipeSchema, pinsCurrentToolchain, } from "./effect.js";
32
+ import { artifactsGitEnvironment, repositoryEnvironment, } from "./git-environment.js";
33
+ const localConfigSchema = Schema.Struct({
34
+ appId: appIdSchema,
35
+ source: Schema.Struct({
36
+ repositoryPath: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
37
+ }),
38
+ recipe: pinnedRecipeSchema,
39
+ artifactRoot: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
40
+ });
41
+ const ACCEPTED_BRANCH = "main";
42
+ function localRepositoryPath(input) {
43
+ if (isAbsolute(input) && !input.startsWith("//"))
44
+ return input;
45
+ throw new AppPublisherError("configuration");
46
+ }
47
+ async function sha256File(path, signal) {
48
+ const hash = createHash("sha256");
49
+ await pipeline(createReadStream(path), hash, { signal });
50
+ return parseSha256Digest(`sha256:${hash.digest("hex")}`);
51
+ }
52
+ function runGit(args, cwd, signal, environment = repositoryEnvironment()) {
53
+ return new Promise((resolve, reject) => {
54
+ const child = spawn("git", [...args], {
55
+ cwd,
56
+ env: environment,
57
+ shell: false,
58
+ signal,
59
+ stdio: ["ignore", "pipe", "pipe"],
60
+ });
61
+ const stdout = [];
62
+ const stderr = [];
63
+ let spawnError;
64
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
65
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
66
+ child.once("error", (error) => {
67
+ spawnError = error;
68
+ });
69
+ child.once("close", (code, terminationSignal) => {
70
+ if (code === 0 && spawnError === undefined) {
71
+ resolve(Buffer.concat(stdout));
72
+ return;
73
+ }
74
+ const detail = Buffer.concat(stderr).toString("utf8").trim();
75
+ reject(spawnError ??
76
+ new Error(detail.length > 0
77
+ ? `git exited unsuccessfully: ${detail}`
78
+ : `git exited unsuccessfully (${String(code)} ${String(terminationSignal)})`));
79
+ });
80
+ });
81
+ }
82
+ function extractTar(archivePath, destination, signal) {
83
+ return new Promise((resolve, reject) => {
84
+ const child = spawn("tar", [
85
+ "--extract",
86
+ "--file",
87
+ archivePath,
88
+ "--directory",
89
+ destination,
90
+ "--no-same-owner",
91
+ ], { shell: false, signal, stdio: ["ignore", "ignore", "pipe"] });
92
+ const stderr = [];
93
+ let spawnError;
94
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
95
+ child.once("error", (error) => {
96
+ spawnError = error;
97
+ });
98
+ child.once("close", (code, terminationSignal) => {
99
+ if (code === 0 && spawnError === undefined) {
100
+ resolve();
101
+ return;
102
+ }
103
+ const detail = Buffer.concat(stderr).toString("utf8").trim();
104
+ reject(spawnError ??
105
+ new Error(detail.length > 0
106
+ ? `tar exited unsuccessfully: ${detail}`
107
+ : `tar exited unsuccessfully (${String(code)} ${String(terminationSignal)})`));
108
+ });
109
+ });
110
+ }
111
+ async function rejectSymbolicLinks(snapshotPath, signal) {
112
+ const directories = [snapshotPath];
113
+ while (directories.length > 0) {
114
+ signal.throwIfAborted();
115
+ const directory = directories.pop();
116
+ if (directory === undefined)
117
+ continue;
118
+ const entries = await readdir(directory, { withFileTypes: true });
119
+ for (const entry of entries) {
120
+ const path = join(directory, entry.name);
121
+ const stats = await lstat(path);
122
+ if (entry.isSymbolicLink() || stats.isSymbolicLink()) {
123
+ throw new Error("Symbolic links are not accepted in source snapshots");
124
+ }
125
+ if (stats.isDirectory())
126
+ directories.push(path);
127
+ }
128
+ }
129
+ }
130
+ async function captureGitSnapshot(workdir, repositoryPath, reference, expectedRevision, signal, fetchEnvironment = repositoryEnvironment()) {
131
+ const snapshotPath = join(workdir, "snapshot");
132
+ const archivePath = join(workdir, "snapshot.tar");
133
+ await mkdir(snapshotPath);
134
+ await runGit(["init", "--quiet"], workdir, signal);
135
+ await runGit([
136
+ "fetch",
137
+ "--quiet",
138
+ "--no-tags",
139
+ "--depth=1",
140
+ "--",
141
+ repositoryPath,
142
+ reference,
143
+ ], workdir, signal, fetchEnvironment);
144
+ const sourceRevision = parseSourceRevision(`git:${Buffer.from(await runGit(["rev-parse", "--verify", "FETCH_HEAD^{commit}"], workdir, signal))
145
+ .toString("utf8")
146
+ .trim()}`);
147
+ if (expectedRevision !== undefined && sourceRevision !== expectedRevision)
148
+ throw new Error("Captured Git commit differs from requested source revision");
149
+ await runGit([
150
+ "archive",
151
+ "--format=tar",
152
+ // Git's @0 date spelling means "now" in the maintained local Git
153
+ // version; this equivalent absolute spelling keeps the tar bytes fixed.
154
+ "--mtime=1970-01-01T00:00:00Z",
155
+ "--output",
156
+ archivePath,
157
+ "FETCH_HEAD^{tree}",
158
+ ], workdir, signal);
159
+ const sourceTreeDigest = await sha256File(archivePath, signal);
160
+ await extractTar(archivePath, snapshotPath, signal);
161
+ await rejectSymbolicLinks(snapshotPath, signal);
162
+ return Object.freeze({
163
+ sourceRevision,
164
+ sourceTreeDigest,
165
+ snapshotPath,
166
+ });
167
+ }
168
+ async function captureCommit(workdir, repositoryPath, sourceRevision, signal) {
169
+ const commit = sourceRevision.slice("git:".length);
170
+ return captureGitSnapshot(workdir, repositoryPath, commit, sourceRevision, signal);
171
+ }
172
+ function scopedSourceCapture(capture, failureMessage) {
173
+ return {
174
+ capture: () => Effect.acquireRelease(Effect.sync(() => mkdtempSync(join(tmpdir(), "app-publisher-"))), (workdir) => Effect.tryPromise({
175
+ try: () => rm(workdir, { recursive: true, force: true }),
176
+ catch: () => new Error("Local source cleanup failed"),
177
+ }).pipe(Effect.catch((cause) => {
178
+ logEvent("services/app-publisher/src/local.ts:289", cause);
179
+ return Effect.void;
180
+ }))).pipe(Effect.flatMap((workdir) => Effect.tryPromise({
181
+ try: (signal) => capture(workdir, signal),
182
+ catch: () => new Error(failureMessage),
183
+ }))),
184
+ };
185
+ }
186
+ function artifactsFetchEnvironment(token) {
187
+ if (token.length === 0 || /[\r\n]/.test(token))
188
+ throw new AppPublisherError("configuration");
189
+ // Keep credentials in the Git environment, never argv or the remote URL.
190
+ return artifactsGitEnvironment(token);
191
+ }
192
+ /** Local proving adapter; accepted source policy is intentionally fixed to main. */
193
+ export function createLocalSourceCapture(repositoryPath) {
194
+ const localPath = localRepositoryPath(repositoryPath);
195
+ return scopedSourceCapture((workdir, signal) => captureGitSnapshot(workdir, localPath, `refs/heads/${ACCEPTED_BRANCH}`, undefined, signal), "Local Git source capture failed");
196
+ }
197
+ /** Capture saved App files for local authoring without modifying the customer's
198
+ * repository. Provenance names the exact temporary Git snapshot, not an existing
199
+ * customer commit. Dependencies and local credentials are not source inputs.
200
+ */
201
+ export function createLocalDirectorySourceCapture(directory) {
202
+ const sourceDirectory = resolve(directory);
203
+ const excluded = new Set([
204
+ ".git",
205
+ "node_modules",
206
+ ".botanical",
207
+ ".wrangler",
208
+ "dist",
209
+ ".npmrc",
210
+ ]);
211
+ return scopedSourceCapture(async (workdir, signal) => {
212
+ const repository = join(workdir, "working");
213
+ await cp(sourceDirectory, repository, {
214
+ recursive: true,
215
+ filter: (path) => {
216
+ const parts = relative(sourceDirectory, path).split(sep);
217
+ return !parts.some((part) => excluded.has(part) ||
218
+ part.startsWith(".env") ||
219
+ part.startsWith(".dev.vars"));
220
+ },
221
+ });
222
+ await rejectSymbolicLinks(repository, signal);
223
+ const environment = {
224
+ ...repositoryEnvironment(),
225
+ GIT_CONFIG_GLOBAL: "/dev/null",
226
+ GIT_CONFIG_NOSYSTEM: "1",
227
+ GIT_AUTHOR_DATE: "1970-01-01T00:00:00Z",
228
+ GIT_COMMITTER_DATE: "1970-01-01T00:00:00Z",
229
+ };
230
+ await runGit(["init", "--quiet"], repository, signal, environment);
231
+ await runGit(["add", "."], repository, signal, environment);
232
+ await runGit([
233
+ "-c",
234
+ "user.name=Fungi App builder",
235
+ "-c",
236
+ "user.email=app-builder@localhost",
237
+ "commit",
238
+ "--quiet",
239
+ "-m",
240
+ "Capture local App source",
241
+ ], repository, signal, environment);
242
+ return captureGitSnapshot(workdir, repository, "HEAD", undefined, signal, environment);
243
+ }, "Local App source capture failed");
244
+ }
245
+ /** Capture an exact Artifacts commit through a trusted host's credential-free Git URL. */
246
+ export function createArtifactsSourceCapture(options) {
247
+ let remote;
248
+ let revision;
249
+ let fetchEnvironment;
250
+ try {
251
+ remote = new URL(options.remote);
252
+ const localHttp = remote.protocol === "http:" &&
253
+ (remote.hostname === "localhost" || remote.hostname === "127.0.0.1");
254
+ if ((remote.protocol !== "https:" && !localHttp) ||
255
+ remote.username ||
256
+ remote.password ||
257
+ remote.search ||
258
+ remote.hash)
259
+ throw new Error("Invalid Artifacts remote");
260
+ revision = parseSourceRevision(options.sourceRevision);
261
+ fetchEnvironment = artifactsFetchEnvironment(options.token);
262
+ }
263
+ catch {
264
+ throw new AppPublisherError("configuration");
265
+ }
266
+ return scopedSourceCapture((workdir, signal) => captureGitSnapshot(workdir, remote.toString(), revision.slice("git:".length), revision, signal, fetchEnvironment), "Artifacts Git source capture failed");
267
+ }
268
+ /** Capture one explicit committed revision without manufacturing a branch. */
269
+ export function createLocalCommitSourceCapture(repositoryPath, sourceRevision) {
270
+ const localPath = localRepositoryPath(repositoryPath);
271
+ const revision = parseSourceRevision(sourceRevision);
272
+ return scopedSourceCapture((workdir, signal) => captureCommit(workdir, localPath, revision, signal), "Local Git source capture failed");
273
+ }
274
+ const workspacePackageSchema = Schema.Struct({
275
+ name: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
276
+ path: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
277
+ });
278
+ /** Hydrate the App workspace and its runtime workspace dependency closure. */
279
+ function productionWorkspaceSelectors(build) {
280
+ return ["--filter-prod", `${build.packageName}...`];
281
+ }
282
+ /** Derive the App's page build from the captured tree's own manifests. */
283
+ function capturedWorkspaceBuild(snapshot, entry, run) {
284
+ return Effect.flatMap(run(workspaceProjectsArguments), (projects) => Effect.tryPromise({
285
+ try: () => resolveWorkspaceBuild({ root: snapshot, entry, projects }),
286
+ catch: (error) => error,
287
+ }));
288
+ }
289
+ const sourceDistributionRootFiles = [
290
+ ".npmrc",
291
+ "package.json",
292
+ "pnpm-lock.yaml",
293
+ "pnpm-workspace.yaml",
294
+ "tsconfig.json",
295
+ "tsconfig.build.json",
296
+ ];
297
+ /**
298
+ * Keep only the pinned dependency graph the editable template's included
299
+ * workspaces actually resolve. pnpm-lock.yaml tracks every workspace member
300
+ * in the repository; copying it whole makes every captured App's upload grow
301
+ * with dependency churn anywhere in the monorepo, including unrelated Apps,
302
+ * packages and docs tooling outside this App's build. `isIncludedPath`
303
+ * reports whether a workspace importer path (its `package.json` directory)
304
+ * is among the files already selected for this distribution.
305
+ *
306
+ * The read, the importer-closure graph walk and the write are pnpm's own
307
+ * maintained lockfile packages (`@pnpm/lockfile.fs`, `@pnpm/lockfile.pruner`),
308
+ * not a hand-rolled parse of pnpm's lockfile grammar. Only the choice of
309
+ * which importers to keep is this distribution's own decision.
310
+ */
311
+ async function prunePnpmLockfile(snapshotPath, isIncludedPath) {
312
+ const lockfile = await readWantedLockfile(snapshotPath, {
313
+ ignoreIncompatible: false,
314
+ });
315
+ if (lockfile === null)
316
+ throw new Error("Source distribution is missing its pnpm-lock.yaml");
317
+ const importers = Object.fromEntries(Object.entries(lockfile.importers).filter(([path]) => path === "." || isIncludedPath(path)));
318
+ const pruned = pruneSharedLockfile({ ...lockfile, importers }, {
319
+ warn: (message) => {
320
+ throw new Error(`pnpm-lock.yaml pruning hit an unresolved dependency: ${message}`);
321
+ },
322
+ });
323
+ const scratch = mkdtempSync(join(tmpdir(), "oyster-lockfile-prune-"));
324
+ try {
325
+ await writeWantedLockfile(scratch, pruned);
326
+ return await readFile(join(scratch, "pnpm-lock.yaml"));
327
+ }
328
+ finally {
329
+ await rm(scratch, { recursive: true, force: true });
330
+ }
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
+ }
389
+ /**
390
+ * Project one pinned first-party App and its pnpm workspace dependency graph
391
+ * without changing upstream files or manufacturing Git history. The caller
392
+ * records the returned provenance on the Artifacts template seed commit.
393
+ */
394
+ export function createFirstPartyAppSourceDistribution(options) {
395
+ const appId = Schema.decodeUnknownSync(appIdSchema)(options.appId);
396
+ if (!options.entry.endsWith(".html"))
397
+ return Effect.fail(new AppPublisherError("configuration"));
398
+ const snapshot = realpathSync(resolve(options.source.snapshotPath));
399
+ const env = {
400
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
401
+ HOME: join(snapshot, ".publisher-source-home"),
402
+ NODE_ENV: "production",
403
+ npm_config_userconfig: "/dev/null",
404
+ npm_config_globalconfig: "/dev/null",
405
+ };
406
+ return Effect.gen(function* () {
407
+ const recipe = yield* capturedWorkspaceBuild(snapshot, options.entry, (args) => runHtmlCommand(args, snapshot, env));
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);
413
+ const listed = yield* runHtmlCommand([
414
+ ...productionWorkspaceSelectors(recipe),
415
+ "list",
416
+ "--depth",
417
+ "-1",
418
+ "--prod",
419
+ "--json",
420
+ ], snapshot, env);
421
+ const workspaces = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Array(workspacePackageSchema)))(listed);
422
+ const selectedApp = workspaces.filter((workspace) => workspace.name === recipe.packageName);
423
+ const selectedAppWorkspace = selectedApp[0];
424
+ if (selectedApp.length !== 1 ||
425
+ !selectedAppWorkspace ||
426
+ realpathSync(selectedAppWorkspace.path) !==
427
+ realpathSync(resolve(snapshot, recipe.directory)))
428
+ return yield* Effect.fail(new AppPublisherError("source"));
429
+ const selectedPaths = new Set();
430
+ const packageNames = new Set();
431
+ for (const workspace of workspaces) {
432
+ if (packageNames.has(workspace.name))
433
+ return yield* Effect.fail(new AppPublisherError("source"));
434
+ packageNames.add(workspace.name);
435
+ const path = resolve(workspace.path);
436
+ const relativePath = relative(snapshot, path);
437
+ if (relativePath === "" ||
438
+ relativePath === ".." ||
439
+ relativePath.startsWith(`..${sep}`) ||
440
+ isAbsolute(relativePath) ||
441
+ (!relativePath.startsWith("apps/") &&
442
+ !relativePath.startsWith("forks/") &&
443
+ !relativePath.startsWith("packages/") &&
444
+ !relativePath.startsWith("runtimes/")))
445
+ return yield* Effect.fail(new AppPublisherError("source"));
446
+ selectedPaths.add(relativePath);
447
+ }
448
+ const files = yield* Effect.tryPromise({
449
+ try: async () => {
450
+ const collected = new Map();
451
+ const collect = async (relativePath) => {
452
+ const sourcePath = join(snapshot, relativePath);
453
+ const stats = await lstat(sourcePath);
454
+ if (stats.isSymbolicLink())
455
+ throw new Error("Source distribution cannot contain symbolic links");
456
+ if (stats.isFile()) {
457
+ collected.set(relativePath, await readFile(sourcePath));
458
+ return;
459
+ }
460
+ if (!stats.isDirectory())
461
+ throw new Error("Source distribution contains a non-file entry");
462
+ const entries = await readdir(sourcePath, { withFileTypes: true });
463
+ entries.sort((left, right) => left.name.localeCompare(right.name));
464
+ for (const entry of entries) {
465
+ // Design workbenches are outside the editable production build.
466
+ if (selectedPaths.has(relativePath) &&
467
+ entry.name === "workbench" &&
468
+ entry.isDirectory())
469
+ continue;
470
+ // A dependency's own tests are not App build inputs; the App keeps its tests.
471
+ if (selectedPaths.has(relativePath) &&
472
+ relativePath !== appDirectory &&
473
+ entry.name === "test" &&
474
+ entry.isDirectory())
475
+ continue;
476
+ // Generated API docs and documentation media are not App build inputs.
477
+ if (relativePath.startsWith("packages/") &&
478
+ basename(relativePath) === "docs" &&
479
+ selectedPaths.has(dirname(relativePath)) &&
480
+ (entry.name === "media" || entry.name === "api") &&
481
+ entry.isDirectory())
482
+ continue;
483
+ await collect(join(relativePath, entry.name));
484
+ }
485
+ };
486
+ for (const relativePath of [...selectedPaths].sort())
487
+ await collect(relativePath);
488
+ for (const relativePath of sourceDistributionRootFiles)
489
+ await collect(relativePath);
490
+ await collect("patches");
491
+ if (collected.has("pnpm-lock.yaml"))
492
+ collected.set("pnpm-lock.yaml", await prunePnpmLockfile(snapshot, (importerPath) => collected.has(join(importerPath, "package.json"))));
493
+ const provenance = provenanceFile(appId, options.source);
494
+ if (collected.has(provenance.path))
495
+ throw new Error("Source distribution reserves its provenance path");
496
+ collected.set(provenance.path, provenance.contents);
497
+ return [...collected]
498
+ .sort(([left], [right]) => left.localeCompare(right))
499
+ .map(([path, contents]) => Object.freeze({ path, contents }));
500
+ },
501
+ catch: (error) => error,
502
+ });
503
+ return Object.freeze({
504
+ appId,
505
+ sourceRevision: options.source.sourceRevision,
506
+ sourceTreeDigest: options.source.sourceTreeDigest,
507
+ appDirectory,
508
+ files,
509
+ });
510
+ });
511
+ }
512
+ function htmlGroupExists(child) {
513
+ if (child.pid === undefined)
514
+ return false;
515
+ try {
516
+ process.kill(-child.pid, 0);
517
+ return true;
518
+ }
519
+ catch (error) {
520
+ if (error instanceof Error && "code" in error && error.code === "ESRCH")
521
+ return false;
522
+ throw error;
523
+ }
524
+ }
525
+ // Narrow adaptation of Caps' workbench-process process-group teardown. The
526
+ // Effect finalizer waits for cleanup before the source snapshot can be removed.
527
+ async function stopHtmlGroup(child) {
528
+ if (!htmlGroupExists(child))
529
+ return;
530
+ const deadline = Date.now() + 5_000;
531
+ for (const [signal, until] of [
532
+ ["SIGTERM", deadline - 1_000],
533
+ ["SIGKILL", deadline],
534
+ ]) {
535
+ try {
536
+ if (child.pid !== undefined)
537
+ process.kill(-child.pid, signal);
538
+ }
539
+ catch (error) {
540
+ if (!(error instanceof Error &&
541
+ "code" in error &&
542
+ error.code === "ESRCH"))
543
+ throw error;
544
+ }
545
+ while (htmlGroupExists(child) && Date.now() < until)
546
+ await delay(25);
547
+ if (!htmlGroupExists(child))
548
+ return;
549
+ }
550
+ throw new Error("HTML process group survived its five-second cleanup bound");
551
+ }
552
+ function runHtmlCommand(args, cwd, env, command = "pnpm") {
553
+ return Effect.acquireUseRelease(Effect.sync(() => {
554
+ if (process.platform === "win32")
555
+ throw new Error("HTML builds require POSIX process groups");
556
+ const child = spawn(command, [...args], {
557
+ cwd,
558
+ env,
559
+ detached: true,
560
+ stdio: ["ignore", "pipe", "pipe"],
561
+ });
562
+ const completion = new Promise((resolveResult, reject) => {
563
+ let stdout = "";
564
+ let stderr = "";
565
+ child.stdout.on("data", (chunk) => {
566
+ stdout += chunk.toString();
567
+ if (Buffer.byteLength(stdout) > 1024 * 1024)
568
+ reject(new Error("HTML command output exceeded its bound"));
569
+ });
570
+ child.stderr.on("data", (chunk) => {
571
+ stderr += chunk.toString();
572
+ if (Buffer.byteLength(stderr) > 1024 * 1024)
573
+ reject(new Error("HTML command diagnostics exceeded their bound"));
574
+ });
575
+ child.once("error", reject);
576
+ child.once("close", (code) => {
577
+ if (code === 0)
578
+ resolveResult(stdout);
579
+ else
580
+ reject(new Error(stderr || stdout || `HTML command exited ${String(code)}`));
581
+ });
582
+ });
583
+ return { child, completion };
584
+ }), ({ completion }) => Effect.tryPromise({ try: () => completion, catch: (error) => error }), ({ child }) => Effect.promise(() => stopHtmlGroup(child)));
585
+ }
586
+ /** Load the bundled snapshot programs only when a snapshot build writes them,
587
+ * so source-level importers of this module do not need the package build. */
588
+ function snapshotPrograms() {
589
+ return import("#snapshot-program-scripts");
590
+ }
591
+ /** Compiler heap ceiling: fits a 12 GiB Computer with two builds at once. */
592
+ const snapshotBuildHeapMegabytes = 4096;
593
+ function snapshotBuildEnvironment(sourceSnapshotPath) {
594
+ // PATH locates pinned executables; the Computer's CA files authenticate
595
+ // its egress gateway. Credentials and package-manager settings stay out.
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
598
+ // fixed ceiling instead.
599
+ const env = {
600
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
601
+ HOME: join(sourceSnapshotPath, ".publisher-home"),
602
+ NODE_ENV: "production",
603
+ NODE_OPTIONS: `--max-old-space-size=${String(snapshotBuildHeapMegabytes)}`,
604
+ NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS,
605
+ SSL_CERT_FILE: process.env.SSL_CERT_FILE,
606
+ npm_config_userconfig: "/dev/null",
607
+ npm_config_globalconfig: "/dev/null",
608
+ };
609
+ return env;
610
+ }
611
+ function rebuildViteSnapshot(sourceSnapshotPath, recipe, options) {
612
+ if (recipe.kind === "source-html")
613
+ return rebuildNpmSnapshot(sourceSnapshotPath, recipe);
614
+ return Effect.gen(function* () {
615
+ const env = snapshotBuildEnvironment(sourceSnapshotPath);
616
+ const run = (args, cwd = sourceSnapshotPath) => runHtmlCommand(args, cwd, env);
617
+ if ((yield* run(["--version"])).trim() !== "11.24.0") {
618
+ return yield* Effect.fail(new Error("Publisher requires pnpm 11.24.0"));
619
+ }
620
+ const html = yield* capturedWorkspaceBuild(sourceSnapshotPath, recipe.entry, run);
621
+ yield* Effect.tryPromise({
622
+ try: () => checkPinnedWorkspaceToolchain(sourceSnapshotPath, html, recipe),
623
+ catch: (error) => error,
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();
629
+ yield* run([
630
+ ...productionWorkspaceSelectors(html),
631
+ ...frozenSnapshotInstallArguments(options.packageStoreDirectory),
632
+ ]);
633
+ const buildStarted = performance.now();
634
+ options.onPhase?.("install", buildStarted - installStarted);
635
+ const appDirectory = join(sourceSnapshotPath, html.directory);
636
+ const dependencies = join(appDirectory, ".publisher-dependencies.mjs");
637
+ const program = join(appDirectory, ".publisher-build.mjs");
638
+ yield* Effect.tryPromise({
639
+ try: async () => {
640
+ const { dependencyBuildProgram, htmlBuildProgram } = await snapshotPrograms();
641
+ await writeFile(dependencies, dependencyBuildProgram, { flag: "wx" });
642
+ await writeFile(program, htmlBuildProgram, { flag: "wx" });
643
+ },
644
+ catch: (error) => error,
645
+ });
646
+ // Keep the derived order and every diagnostic/emit check. Consecutive
647
+ // projects share one warmed compiler; stylesheet steps stay separate.
648
+ const groups = [];
649
+ for (const step of html.steps) {
650
+ const last = groups.at(-1);
651
+ if (step.kind === "typescript" &&
652
+ last?.every((member) => member.kind === "typescript"))
653
+ last.push(step);
654
+ else
655
+ groups.push([step]);
656
+ }
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
+ }
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",
674
+ "exec",
675
+ "node",
676
+ dependencies,
677
+ sourceSnapshotPath,
678
+ html.directory,
679
+ recipe.format,
680
+ JSON.stringify(group),
681
+ tailwindToolchainDirectory(html),
682
+ ], appDirectory);
683
+ }
684
+ yield* run([
685
+ "--config.verify-deps-before-run=false",
686
+ "exec",
687
+ "node",
688
+ program,
689
+ sourceSnapshotPath,
690
+ String(recipe.maxOutputBytes),
691
+ html.directory,
692
+ recipe.format,
693
+ recipe.assetBase ?? "/",
694
+ dirname(recipe.entry),
695
+ String(html.react),
696
+ html.shellEntry ?? "",
697
+ ], appDirectory);
698
+ options.onPhase?.("build", performance.now() - buildStarted);
699
+ return yield* readHtmlBuildOutput(sourceSnapshotPath, recipe);
700
+ });
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
+ }
712
+ const pinnedVersionsSchema = Schema.Record(Schema.String, Schema.String);
713
+ const pinnedManifestSchema = Schema.fromJsonString(Schema.Struct({
714
+ dependencies: Schema.optional(pinnedVersionsSchema),
715
+ devDependencies: Schema.optional(pinnedVersionsSchema),
716
+ }));
717
+ /**
718
+ * Refuse a captured tree whose declared toolchain differs from the pinned
719
+ * one before any of its dependencies hydrate.
720
+ */
721
+ async function checkPinnedWorkspaceToolchain(sourceSnapshotPath, html, recipe) {
722
+ const manifest = async (directory) => Schema.decodeUnknownSync(pinnedManifestSchema)(await readFile(join(sourceSnapshotPath, directory, "package.json"), "utf8"));
723
+ const pinned = (directory, versions, name, version) => {
724
+ if (versions?.[name] !== version)
725
+ throw new Error(`${directory} must pin ${name} ${version}`);
726
+ };
727
+ const root = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Struct({
728
+ packageManager: Schema.Literal("pnpm@11.24.0"),
729
+ devDependencies: Schema.Struct({
730
+ typescript: Schema.Literal("5.9.3"),
731
+ }),
732
+ })));
733
+ root(await readFile(join(sourceSnapshotPath, "package.json"), "utf8"));
734
+ const app = await manifest(html.directory);
735
+ pinned(html.directory, app.devDependencies, "vite", "8.2.2");
736
+ if (html.react) {
737
+ pinned(html.directory, app.dependencies, "react-dom", "18.3.1");
738
+ pinned(html.directory, app.devDependencies, "@vitejs/plugin-react", "6.1.1");
739
+ pinned(html.directory, app.dependencies?.react === undefined
740
+ ? app.devDependencies
741
+ : app.dependencies, "react", "18.3.1");
742
+ }
743
+ if (recipe.format === "html")
744
+ pinned(html.directory, app.devDependencies, "vite-plugin-singlefile", "2.3.3");
745
+ const stylesheets = html.steps.filter((step) => step.kind === "tailwind");
746
+ if (stylesheets.length === 0)
747
+ return;
748
+ // The captured Tailwind compiler and its typography plugin come from Caps.
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");
756
+ for (const directory of html.dependencies) {
757
+ if (!stylesheets.some((step) => step.input.startsWith(`${directory}/`)))
758
+ continue;
759
+ const owner = await manifest(directory);
760
+ for (const versions of [owner.dependencies, owner.devDependencies])
761
+ if (versions?.["@tailwindcss/typography"] !== undefined)
762
+ pinned(directory, versions, "@tailwindcss/typography", "0.5.19");
763
+ }
764
+ }
765
+ function readHtmlBuildOutput(sourceSnapshotPath, recipe) {
766
+ return Effect.gen(function* () {
767
+ if (recipe.assets) {
768
+ const policy = recipe.assets;
769
+ const base = recipe.assetBase;
770
+ if (base === undefined)
771
+ return yield* Effect.fail(new Error("Asset recipe requires its captured base"));
772
+ return yield* Effect.tryPromise(async () => {
773
+ const root = join(sourceSnapshotPath, ".publisher-output");
774
+ const files = await readdir(root, {
775
+ recursive: true,
776
+ withFileTypes: true,
777
+ });
778
+ const assets = [];
779
+ let total = 0;
780
+ for (const file of files) {
781
+ if (file.isDirectory())
782
+ continue;
783
+ if (!file.isFile() || assets.length >= policy.maxObjects)
784
+ throw new Error("Invalid or excessive build outputs");
785
+ const filename = join(file.parentPath, file.name);
786
+ const path = Schema.decodeUnknownSync(assetPathSchema)("/" + relative(root, filename).split(sep).join("/"));
787
+ const stat = await lstat(filename);
788
+ total += stat.size;
789
+ if (stat.size > policy.maxObjectBytes ||
790
+ !Number.isSafeInteger(total) ||
791
+ total > policy.maxTotalBytes)
792
+ throw new Error("Split output exceeds asset policy");
793
+ const bytes = await readFile(filename);
794
+ if (bytes.byteLength !== stat.size)
795
+ throw new Error("Build output changed during capture");
796
+ assets.push({ path, bytes });
797
+ }
798
+ return { kind: "assets", base, assets };
799
+ });
800
+ }
801
+ const bytes = yield* Effect.tryPromise({
802
+ try: (signal) => readFile(join(sourceSnapshotPath, ".publisher.html"), { signal }),
803
+ catch: (error) => error,
804
+ });
805
+ return Object.freeze({ kind: "bytes", bytes });
806
+ });
807
+ }
808
+ function rebuildNpmSnapshot(sourceSnapshotPath, recipe) {
809
+ return Effect.gen(function* () {
810
+ const env = snapshotBuildEnvironment(sourceSnapshotPath);
811
+ const home = join(sourceSnapshotPath, ".publisher-home");
812
+ const userConfig = join(home, "user.npmrc");
813
+ const globalConfig = join(home, "global.npmrc");
814
+ env.npm_config_userconfig = userConfig;
815
+ env.npm_config_globalconfig = globalConfig;
816
+ yield* Effect.tryPromise(async () => {
817
+ await mkdir(home, { recursive: true });
818
+ await writeFile(userConfig, "", { flag: "wx" });
819
+ await writeFile(globalConfig, "", { flag: "wx" });
820
+ });
821
+ if ((yield* runHtmlCommand(["--version"], sourceSnapshotPath, env, "npm")).trim() !== recipe.toolchain.npm)
822
+ return yield* Effect.fail(new Error("Captured npm version differs from the recipe"));
823
+ yield* runHtmlCommand(["ci", "--include=dev", "--ignore-scripts", "--no-audit", "--no-fund"], sourceSnapshotPath, env, "npm");
824
+ const program = join(sourceSnapshotPath, ".publisher-build.mjs");
825
+ yield* Effect.tryPromise(async () => {
826
+ const { htmlBuildProgram } = await snapshotPrograms();
827
+ await writeFile(program, htmlBuildProgram, { flag: "wx" });
828
+ });
829
+ yield* runHtmlCommand([
830
+ program,
831
+ sourceSnapshotPath,
832
+ String(recipe.maxOutputBytes),
833
+ ".",
834
+ recipe.format,
835
+ recipe.assetBase ?? "./",
836
+ dirname(recipe.entry),
837
+ "true",
838
+ "",
839
+ ], sourceSnapshotPath, env, "node");
840
+ return yield* readHtmlBuildOutput(sourceSnapshotPath, recipe);
841
+ });
842
+ }
843
+ async function rebuildSnapshot(context, recipe, signal, sourceSnapshotPath, emission) {
844
+ signal.throwIfAborted();
845
+ const result = await new Promise((resolve, reject) => {
846
+ let settled = false;
847
+ const finish = (operation) => {
848
+ if (settled)
849
+ return;
850
+ settled = true;
851
+ signal.removeEventListener("abort", onAbort);
852
+ operation();
853
+ };
854
+ const rejectWithError = () => {
855
+ reject(new Error("Local snapshot build was interrupted"));
856
+ };
857
+ const onAbort = () => {
858
+ void context.cancel().then(() => {
859
+ finish(() => {
860
+ rejectWithError();
861
+ });
862
+ }, () => {
863
+ finish(() => {
864
+ reject(new Error("Local snapshot build cancellation failed"));
865
+ });
866
+ });
867
+ };
868
+ signal.addEventListener("abort", onAbort, { once: true });
869
+ context.rebuild().then((value) => {
870
+ finish(() => {
871
+ resolve(value);
872
+ });
873
+ }, (cause) => {
874
+ finish(() => {
875
+ reject(cause instanceof Error
876
+ ? cause
877
+ : new Error("Local snapshot rebuild failed", { cause }));
878
+ });
879
+ });
880
+ if (signal.aborted)
881
+ onAbort();
882
+ });
883
+ if (result.metafile === undefined) {
884
+ throw new Error("Local snapshot build did not return its requested metafile");
885
+ }
886
+ const escapedInput = Object.keys(result.metafile.inputs).find((input) => {
887
+ const path = resolve(sourceSnapshotPath, input);
888
+ const fromSnapshot = relative(sourceSnapshotPath, path);
889
+ return (fromSnapshot === ".." ||
890
+ fromSnapshot.startsWith(`..${sep}`) ||
891
+ isAbsolute(fromSnapshot));
892
+ });
893
+ if (escapedInput !== undefined) {
894
+ throw new Error("Local snapshot build resolved an input outside the captured snapshot");
895
+ }
896
+ const output = emission.worker(result).file;
897
+ if (output.contents.byteLength > recipe.maxOutputBytes) {
898
+ throw new Error("Local build output exceeded its pinned bound");
899
+ }
900
+ return Object.freeze({
901
+ kind: "bytes",
902
+ bytes: output.contents.slice(),
903
+ });
904
+ }
905
+ /** Exact pinned esbuild options shared by local builds and artifact-budget proofs. */
906
+ export function localSnapshotBuildOptions(source, recipe, emission) {
907
+ const options = {
908
+ absWorkingDir: source.snapshotPath,
909
+ bundle: true,
910
+ minify: recipe.backend !== null,
911
+ entryPoints: emission.entryPoints(`./${recipe.entry}`),
912
+ outdir: emission.outdir,
913
+ format: "esm",
914
+ platform: recipe.platform,
915
+ external: recipe.backend === null ? [] : ["cloudflare:workers"],
916
+ target: ["es2022"],
917
+ write: false,
918
+ metafile: true,
919
+ plugins: [...emission.plugins],
920
+ };
921
+ if (recipe.backend !== null) {
922
+ const mainFields = appBackendPackageMainFields(recipe);
923
+ if (mainFields !== undefined)
924
+ options.mainFields = mainFields;
925
+ options.conditions = [...appBackendExportConditions];
926
+ options.tsconfigRaw = {};
927
+ }
928
+ return options;
929
+ }
930
+ /** Local proving adapter; it builds only the captured extracted snapshot. */
931
+ export function createLocalBuildExecutor(options = {}) {
932
+ const emissions = new WeakMap();
933
+ const compiler = {
934
+ build: (source, recipe) => {
935
+ if (recipe.kind === "bundled-html" && recipe.backend === null)
936
+ return Effect.tryPromise({
937
+ try: () => readBundledHtml(source.snapshotPath, recipe),
938
+ catch: () => new Error("Captured HTML bundle failed validation"),
939
+ });
940
+ if (recipe.format !== "esm" && recipe.backend !== null) {
941
+ const backend = recipe.backend;
942
+ const entry = backend.entry;
943
+ if (entry === undefined)
944
+ return Effect.fail(new Error("Joined App requires a backend entry"));
945
+ return Effect.gen(function* () {
946
+ // Build both facets from the same captured tree. The UI build hydrates
947
+ // that tree's pinned dependencies before the backend resolves them.
948
+ if (recipe.kind === "bundled-html") {
949
+ const hasLockfile = yield* Effect.tryPromise(() => access(join(source.snapshotPath, "pnpm-lock.yaml")).then(() => true, (error) => {
950
+ if (error instanceof Error &&
951
+ "code" in error &&
952
+ error.code === "ENOENT")
953
+ return false;
954
+ throw error;
955
+ }));
956
+ if (hasLockfile) {
957
+ yield* Effect.tryPromise(async () => {
958
+ Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Struct({
959
+ packageManager: Schema.Literal("pnpm@11.24.0"),
960
+ })))(await readFile(join(source.snapshotPath, "package.json"), "utf8"));
961
+ });
962
+ const environment = snapshotBuildEnvironment(source.snapshotPath);
963
+ if ((yield* runHtmlCommand(["--version"], source.snapshotPath, environment)).trim() !== "11.24.0")
964
+ return yield* Effect.fail(new Error("Publisher requires pnpm 11.24.0"));
965
+ yield* runHtmlCommand(frozenSnapshotInstallArguments(options.packageStoreDirectory), source.snapshotPath, environment);
966
+ }
967
+ }
968
+ const ui = yield* recipe.kind === "bundled-html"
969
+ ? Effect.tryPromise({
970
+ try: () => readBundledHtml(source.snapshotPath, recipe),
971
+ catch: () => new Error("Captured HTML bundle failed validation"),
972
+ })
973
+ : rebuildViteSnapshot(source.snapshotPath, recipe, options);
974
+ const code = yield* compiler.build(source, {
975
+ bundle: true,
976
+ entry,
977
+ format: "esm",
978
+ platform: "neutral",
979
+ toolchain: { ...recipe.toolchain, esbuild: "0.28.2" },
980
+ target: ["es2022"],
981
+ maxOutputBytes: recipe.maxOutputBytes,
982
+ backend,
983
+ });
984
+ if (code.kind !== "bytes" || ui.kind === "app")
985
+ return yield* Effect.fail(new Error("Invalid joined App output"));
986
+ return {
987
+ kind: "app",
988
+ ui: ui.kind === "bytes"
989
+ ? {
990
+ content: { kind: "html", bytes: ui.bytes },
991
+ mediaType: "text/html; charset=utf-8",
992
+ }
993
+ : {
994
+ content: ui,
995
+ mediaType: "application/vnd.fungi.app-assets+json",
996
+ },
997
+ backend: code.bytes,
998
+ };
999
+ });
1000
+ }
1001
+ return recipe.format !== "esm"
1002
+ ? rebuildViteSnapshot(source.snapshotPath, recipe, options)
1003
+ : Effect.acquireUseRelease(Effect.tryPromise({
1004
+ try: () => {
1005
+ const emission = emissions.get(source);
1006
+ if (emission === undefined)
1007
+ throw new Error("App backend build requires its definition emission");
1008
+ return esbuild
1009
+ .context(localSnapshotBuildOptions(source, recipe, emission))
1010
+ .then((context) => ({ context, emission }));
1011
+ },
1012
+ catch: (error) => error instanceof Error
1013
+ ? error
1014
+ : new Error("Local snapshot build failed"),
1015
+ }), ({ context, emission }) => Effect.tryPromise({
1016
+ try: (signal) => rebuildSnapshot(context, recipe, signal, source.snapshotPath, emission),
1017
+ catch: (error) => error instanceof Error
1018
+ ? error
1019
+ : new Error("Local snapshot build failed"),
1020
+ }), ({ context }) => Effect.tryPromise({
1021
+ try: () => context.dispose(),
1022
+ catch: () => new Error("Local build cleanup failed"),
1023
+ }).pipe(Effect.catch((cause) => {
1024
+ logEvent("services/app-publisher/src/local.ts:1262", cause);
1025
+ return Effect.void;
1026
+ })));
1027
+ },
1028
+ };
1029
+ const declarations = new WeakMap();
1030
+ return {
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"));
1034
+ const emission = yield* Effect.tryPromise({
1035
+ try: () => createDefinitionEmission(source, recipe),
1036
+ catch: (cause) => cause instanceof Error
1037
+ ? cause
1038
+ : new Error("App declaration emission failed", { cause }),
1039
+ });
1040
+ emissions.set(source, emission);
1041
+ const output = yield* compiler.build(source, recipe);
1042
+ const declaration = yield* Effect.tryPromise({
1043
+ try: (signal) => emission.declare(signal),
1044
+ catch: (cause) => cause instanceof Error
1045
+ ? cause
1046
+ : new Error("App declaration emission failed", { cause }),
1047
+ });
1048
+ declarations.set(source, declaration);
1049
+ return output;
1050
+ }),
1051
+ declaration: (source) => {
1052
+ const declaration = declarations.get(source);
1053
+ return declaration === undefined
1054
+ ? Effect.fail(new Error("App declaration must be read after the captured build"))
1055
+ : Effect.succeed(declaration);
1056
+ },
1057
+ };
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
+ }
1084
+ async function writeArtifact(output, artifactRoot, signal, assetPolicy) {
1085
+ signal.throwIfAborted();
1086
+ await mkdir(artifactRoot, { recursive: true });
1087
+ return writeImmutableArtifact(output, directoryArtifactObjects(artifactRoot, signal), {
1088
+ rootArtifactRef: parseArtifactRef(`artifact:${Buffer.from(randomBytes(16)).toString("hex")}`),
1089
+ assetPolicy,
1090
+ });
1091
+ }
1092
+ /** Local proving adapter; the configured root is private and write-once. */
1093
+ export function createLocalArtifactWriter(artifactRoot, assetPolicy) {
1094
+ return {
1095
+ write: (output) => Effect.tryPromise({
1096
+ try: (signal) => writeArtifact(output, artifactRoot, signal, assetPolicy),
1097
+ catch: () => new Error("Local immutable artifact write failed"),
1098
+ }),
1099
+ };
1100
+ }
1101
+ /** Construct the complete local proving adapter around the provider-neutral core. */
1102
+ export function createLocalAppPublisher(options) {
1103
+ let decoded;
1104
+ try {
1105
+ decoded = Schema.decodeUnknownSync(localConfigSchema)(options.config);
1106
+ }
1107
+ catch {
1108
+ throw new AppPublisherError("configuration");
1109
+ }
1110
+ if (decoded.recipe.assets)
1111
+ throw new AppPublisherError("configuration", {
1112
+ cause: new Error("Asset publication requires the native immutable object writer"),
1113
+ });
1114
+ const repositoryPath = localRepositoryPath(decoded.source.repositoryPath);
1115
+ const artifactRoot = decoded.artifactRoot;
1116
+ const recipe = {
1117
+ entry: decoded.recipe.entry,
1118
+ backend: decoded.recipe.backend,
1119
+ maxOutputBytes: decoded.recipe.maxOutputBytes,
1120
+ };
1121
+ return createAppPublisher({
1122
+ config: { appId: decoded.appId, recipe },
1123
+ releaseCatalog: options.releaseCatalog,
1124
+ sourceCapture: createLocalSourceCapture(repositoryPath),
1125
+ buildExecutor: createLocalBuildExecutor(),
1126
+ artifactWriter: createLocalArtifactWriter(artifactRoot),
1127
+ });
1128
+ }