@fungi.computer/oyster 0.0.0-stage → 0.0.1

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