everything-dev 2.0.0-rc.6 → 2.0.0-rc.8

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 (148) hide show
  1. package/README.md +2 -2
  2. package/dist/api/auth-middleware.cjs +14 -1
  3. package/dist/api/auth-middleware.cjs.map +1 -1
  4. package/dist/api/auth-middleware.d.cts +9 -0
  5. package/dist/api/auth-middleware.d.mts +9 -0
  6. package/dist/api/auth-middleware.mjs +14 -1
  7. package/dist/api/auth-middleware.mjs.map +1 -1
  8. package/dist/build.cjs +1 -1
  9. package/dist/build.mjs +1 -1
  10. package/dist/bundle-cache.cjs +1 -1
  11. package/dist/bundle-cache.cjs.map +1 -1
  12. package/dist/bundle-cache.mjs +1 -1
  13. package/dist/bundle-cache.mjs.map +1 -1
  14. package/dist/cli/init.cjs +22 -10
  15. package/dist/cli/init.cjs.map +1 -1
  16. package/dist/cli/init.mjs +22 -10
  17. package/dist/cli/init.mjs.map +1 -1
  18. package/dist/cli.cjs +6 -5
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.mjs +5 -4
  21. package/dist/cli.mjs.map +1 -1
  22. package/dist/commands/deploy.cjs +111 -1
  23. package/dist/commands/deploy.cjs.map +1 -1
  24. package/dist/commands/deploy.mjs +112 -2
  25. package/dist/commands/deploy.mjs.map +1 -1
  26. package/dist/commands/keys.cjs +1 -1
  27. package/dist/commands/keys.cjs.map +1 -1
  28. package/dist/commands/keys.mjs +1 -1
  29. package/dist/commands/keys.mjs.map +1 -1
  30. package/dist/commands/smoke.cjs +41 -0
  31. package/dist/commands/smoke.cjs.map +1 -0
  32. package/dist/commands/smoke.mjs +41 -0
  33. package/dist/commands/smoke.mjs.map +1 -0
  34. package/dist/commands/upgrade.cjs +2 -2
  35. package/dist/commands/upgrade.cjs.map +1 -1
  36. package/dist/commands/upgrade.mjs +1 -1
  37. package/dist/commands/upgrade.mjs.map +1 -1
  38. package/dist/contract.cjs +27 -1
  39. package/dist/contract.cjs.map +1 -1
  40. package/dist/contract.d.cts +65 -1
  41. package/dist/contract.d.mts +65 -1
  42. package/dist/contract.meta.cjs +14 -2
  43. package/dist/contract.meta.cjs.map +1 -1
  44. package/dist/contract.meta.d.cts +24 -2
  45. package/dist/contract.meta.d.mts +24 -2
  46. package/dist/contract.meta.mjs +14 -2
  47. package/dist/contract.meta.mjs.map +1 -1
  48. package/dist/contract.mjs +26 -2
  49. package/dist/contract.mjs.map +1 -1
  50. package/dist/db/core.cjs +22 -4
  51. package/dist/db/core.cjs.map +1 -1
  52. package/dist/db/core.mjs +22 -4
  53. package/dist/db/core.mjs.map +1 -1
  54. package/dist/dev-log-pipeline.cjs +6 -0
  55. package/dist/dev-log-pipeline.cjs.map +1 -1
  56. package/dist/dev-log-pipeline.mjs +6 -0
  57. package/dist/dev-log-pipeline.mjs.map +1 -1
  58. package/dist/dev-program.cjs +20 -3
  59. package/dist/dev-program.cjs.map +1 -1
  60. package/dist/dev-program.mjs +20 -4
  61. package/dist/dev-program.mjs.map +1 -1
  62. package/dist/dev-session.cjs +1 -0
  63. package/dist/dev-session.cjs.map +1 -1
  64. package/dist/dev-session.mjs +1 -0
  65. package/dist/dev-session.mjs.map +1 -1
  66. package/dist/fastkv.cjs +21 -3
  67. package/dist/fastkv.cjs.map +1 -1
  68. package/dist/fastkv.d.cts +14 -1
  69. package/dist/fastkv.d.mts +14 -1
  70. package/dist/fastkv.mjs +21 -5
  71. package/dist/fastkv.mjs.map +1 -1
  72. package/dist/{mf.cjs → federation.cjs} +6 -13
  73. package/dist/federation.cjs.map +1 -0
  74. package/dist/{mf.d.cts → federation.d.cts} +2 -2
  75. package/dist/{mf.d.mts → federation.d.mts} +2 -2
  76. package/dist/{mf.mjs → federation.mjs} +6 -13
  77. package/dist/federation.mjs.map +1 -0
  78. package/dist/http-client.cjs +84 -57
  79. package/dist/http-client.cjs.map +1 -1
  80. package/dist/http-client.mjs +84 -58
  81. package/dist/http-client.mjs.map +1 -1
  82. package/dist/image-deploy.cjs +9 -0
  83. package/dist/image-deploy.cjs.map +1 -1
  84. package/dist/image-deploy.mjs +9 -1
  85. package/dist/image-deploy.mjs.map +1 -1
  86. package/dist/index.cjs +4 -0
  87. package/dist/index.d.cts +3 -3
  88. package/dist/index.d.mts +3 -3
  89. package/dist/index.mjs +3 -3
  90. package/dist/infra/planner.cjs +6 -2
  91. package/dist/infra/planner.cjs.map +1 -1
  92. package/dist/infra/planner.mjs +6 -2
  93. package/dist/infra/planner.mjs.map +1 -1
  94. package/dist/orchestrator.cjs +39 -35
  95. package/dist/orchestrator.cjs.map +1 -1
  96. package/dist/orchestrator.mjs +39 -35
  97. package/dist/orchestrator.mjs.map +1 -1
  98. package/dist/plugin.cjs +2 -0
  99. package/dist/plugin.cjs.map +1 -1
  100. package/dist/plugin.d.cts +24 -0
  101. package/dist/plugin.d.mts +24 -0
  102. package/dist/plugin.mjs +2 -0
  103. package/dist/plugin.mjs.map +1 -1
  104. package/dist/process-registry.cjs +1 -1
  105. package/dist/process-registry.mjs +1 -1
  106. package/dist/publish.cjs +3 -3
  107. package/dist/publish.cjs.map +1 -1
  108. package/dist/publish.mjs +4 -4
  109. package/dist/publish.mjs.map +1 -1
  110. package/dist/resolver.cjs +293 -0
  111. package/dist/resolver.cjs.map +1 -0
  112. package/dist/resolver.d.cts +99 -0
  113. package/dist/resolver.d.mts +99 -0
  114. package/dist/resolver.mjs +284 -0
  115. package/dist/resolver.mjs.map +1 -0
  116. package/dist/smoke.cjs +262 -0
  117. package/dist/smoke.cjs.map +1 -0
  118. package/dist/smoke.mjs +260 -0
  119. package/dist/smoke.mjs.map +1 -0
  120. package/dist/ui/auth.d.cts +217 -123
  121. package/dist/ui/auth.d.mts +217 -123
  122. package/dist/ui/router-client.cjs +2 -1
  123. package/dist/ui/router-client.cjs.map +1 -1
  124. package/dist/ui/router-client.mjs +2 -1
  125. package/dist/ui/router-client.mjs.map +1 -1
  126. package/dist/version-manifest-resolve.cjs +3 -3
  127. package/dist/version-manifest-resolve.cjs.map +1 -1
  128. package/dist/version-manifest-resolve.mjs +1 -1
  129. package/dist/version-manifest-resolve.mjs.map +1 -1
  130. package/dist/workspace.cjs +1 -1
  131. package/dist/workspace.mjs +1 -1
  132. package/package.json +18 -18
  133. package/skills/api-and-auth/SKILL.md +12 -9
  134. package/skills/api-and-auth/references/middleware.md +38 -0
  135. package/skills/cli-reference/SKILL.md +22 -6
  136. package/skills/talk-to-the-app/SKILL.md +32 -5
  137. package/dist/bundle-fs-resolve.cjs +0 -235
  138. package/dist/bundle-fs-resolve.cjs.map +0 -1
  139. package/dist/bundle-fs-resolve.d.cts +0 -82
  140. package/dist/bundle-fs-resolve.d.mts +0 -82
  141. package/dist/bundle-fs-resolve.mjs +0 -228
  142. package/dist/bundle-fs-resolve.mjs.map +0 -1
  143. package/dist/bundle-path.cjs +0 -48
  144. package/dist/bundle-path.cjs.map +0 -1
  145. package/dist/bundle-path.mjs +0 -45
  146. package/dist/bundle-path.mjs.map +0 -1
  147. package/dist/mf.cjs.map +0 -1
  148. package/dist/mf.mjs.map +0 -1
package/dist/smoke.cjs ADDED
@@ -0,0 +1,262 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_process = require("node:process");
5
+ node_process = require_runtime.__toESM(node_process, 1);
6
+ let node_child_process = require("node:child_process");
7
+
8
+ //#region src/smoke.ts
9
+ const BOOT_DEADLINE_MS = 24e4;
10
+ const POLL_INTERVAL_MS = 1e3;
11
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ function run(cmd, args) {
13
+ const result = (0, node_child_process.spawnSync)(cmd, args, { encoding: "utf8" });
14
+ if (result.error) throw result.error;
15
+ return {
16
+ status: result.status,
17
+ stdout: result.stdout ?? "",
18
+ stderr: result.stderr ?? ""
19
+ };
20
+ }
21
+ const docker = (args) => run("docker", args);
22
+ async function fetchStatusOk(url) {
23
+ const response = await fetch(url);
24
+ if (!response.ok) throw new Error(`${url} → ${response.status}`);
25
+ return response;
26
+ }
27
+ /** Bounded poll: resolves with the check's result once it passes, rejects at the deadline. */
28
+ async function pollUntil(name, check, deadlineMs = BOOT_DEADLINE_MS) {
29
+ const deadline = Date.now() + deadlineMs;
30
+ let lastFailure = "no attempt";
31
+ let attempt = 0;
32
+ while (Date.now() < deadline) {
33
+ attempt += 1;
34
+ try {
35
+ const detail = await check();
36
+ console.log(` ✓ ${name} (attempt ${attempt})`);
37
+ return detail;
38
+ } catch (error) {
39
+ lastFailure = error instanceof Error ? error.message : String(error);
40
+ }
41
+ await sleep(POLL_INTERVAL_MS);
42
+ }
43
+ throw new Error(`${name} did not pass within the boot deadline — last failure: ${lastFailure}`);
44
+ }
45
+ /** Resolves "exited" once the boot container has exited — the start stack
46
+ * exits non-zero when a service fails to boot, so a dead container means the
47
+ * remaining pollUntil time would be wasted polling nothing. Resolves
48
+ * undefined when aborted. */
49
+ function watchContainerExit(container) {
50
+ const controller = { aborted: false };
51
+ return {
52
+ done: (async () => {
53
+ while (!controller.aborted) {
54
+ if ((docker([
55
+ "inspect",
56
+ "-f",
57
+ "{{.State.Running}}",
58
+ container
59
+ ]).stdout ?? "").trim() === "false") return "exited";
60
+ await sleep(2e3);
61
+ }
62
+ })(),
63
+ abort: () => controller.aborted = true
64
+ };
65
+ }
66
+ /** While the container is still running, probe the CDN from inside it — a
67
+ * boot failure on :3000 is either the CDN edge (network-class: unreachable,
68
+ * 429/5xx, challenge) or a boot bug (module eval, shared-dependency skew,
69
+ * server start), and the two need opposite fixes. Falls back to a one-off
70
+ * container from the same image (equivalent network class) when the boot
71
+ * container already exited. */
72
+ function probeCdnFromContainer(container, image, account, gateway) {
73
+ const base = `${node_process.default.env.SMOKE_CDN_BASE ?? "https://cdn.everything.dev"}/bundles/${account}/${gateway}/host`;
74
+ const script = `
75
+ const base = ${JSON.stringify(base)};
76
+ (async () => {
77
+ const urls = [base + "/mf-manifest.json"];
78
+ try {
79
+ const manifest = await fetch(urls[0]).then((r) => (r.ok ? r.json() : null));
80
+ const expose = manifest?.exposes?.[0] ?? {};
81
+ const files = expose.assets?.js?.sync ?? expose.files?.js?.sync ?? [];
82
+ for (const f of files.slice(0, 3)) urls.push(base + "/" + f);
83
+ } catch {}
84
+ for (const url of urls) {
85
+ try {
86
+ const res = await fetch(url);
87
+ const bytes = res.ok ? (await res.arrayBuffer()).byteLength : 0;
88
+ console.log("probe " + url + " -> " + res.status + (bytes ? " " + bytes + "B" : ""));
89
+ } catch (error) {
90
+ console.log("probe " + url + " -> FETCH_FAILED " + String(error));
91
+ }
92
+ }
93
+ })();
94
+ `;
95
+ let probe = docker([
96
+ "exec",
97
+ container,
98
+ "node",
99
+ "-e",
100
+ script
101
+ ]);
102
+ if (probe.status !== 0) probe = docker([
103
+ "run",
104
+ "--rm",
105
+ image,
106
+ "node",
107
+ "-e",
108
+ script
109
+ ]);
110
+ const output = `${probe.stdout}${probe.stderr}`.trim();
111
+ console.error("--- in-container CDN probe ---");
112
+ if (output) console.error(output);
113
+ else console.error(`(no output — probe exit ${probe.status}: ${probe.stderr.trim()})`);
114
+ const verdicts = output.split("\n").filter((line) => line.startsWith("probe ")).map((line) => {
115
+ const status = line.split(" -> ")[1] ?? "";
116
+ if (status.startsWith("FETCH_FAILED")) return "unreachable";
117
+ if (Number.parseInt(status, 10) === 200) return "ok";
118
+ return "edge-error";
119
+ });
120
+ return verdicts.length === 0 ? "probe did not run — see probe exit above" : verdicts.every((v) => v === "ok") ? "CDN reachable from inside the container — app-class boot failure; the cause is in the captured logs, not the network" : verdicts.some((v) => v === "unreachable") ? "CDN unreachable from inside the container — network-class failure (runner egress or CDN edge)" : "CDN answered non-200 from inside the container — edge or published-config mismatch (see statuses above)";
121
+ }
122
+ /** Copy the stack's .bos/logs out of the container (into the CI artifact
123
+ * path) and capture the full container stdout — the failure reason for a
124
+ * failed service only exists inside the container, which otherwise gets
125
+ * deleted, and a tail window can cut the error behind spinner output. */
126
+ function captureContainerLogs(container, dockerLogs) {
127
+ const target = (0, node_path.join)(".bos", "logs", "smoke");
128
+ (0, node_fs.mkdirSync)(target, { recursive: true });
129
+ const cp = docker([
130
+ "cp",
131
+ `${container}:/app/.bos/logs/.`,
132
+ target
133
+ ]);
134
+ if (cp.status !== 0) console.error(`smoke: could not copy container logs: ${cp.stderr.trim()}`);
135
+ else {
136
+ const walk = (dir) => (0, node_fs.readdirSync)(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk((0, node_path.join)(dir, entry.name)) : [(0, node_path.join)(dir, entry.name)]);
137
+ for (const file of walk(target)) {
138
+ if (!file.endsWith(".log")) continue;
139
+ const lines = (0, node_fs.readFileSync)(file, "utf8").split("\n");
140
+ console.error(`--- ${file} (last 60 of ${lines.length} lines) ---`);
141
+ console.error(lines.slice(-60).join("\n"));
142
+ }
143
+ }
144
+ (0, node_fs.writeFileSync)((0, node_path.join)(target, "smoke-stdout.log"), dockerLogs.join("\n"));
145
+ console.error(`--- container stdout (full, ${dockerLogs.length} lines) → ${target}/smoke-stdout.log ---`);
146
+ console.error(dockerLogs.slice(-100).join("\n"));
147
+ }
148
+ async function runSmoke(options) {
149
+ const port = options.port ?? 3e3;
150
+ const keep = options.keep ?? false;
151
+ const baseUrl = `http://localhost:${port}`;
152
+ const container = `everything-dev-smoke-${node_process.default.pid}`;
153
+ const image = options.image;
154
+ const envPairs = {
155
+ BOS_ACCOUNT: options.account ?? node_process.default.env.SMOKE_BOS_ACCOUNT ?? "dev.everything.near",
156
+ BOS_GATEWAY: options.gateway ?? node_process.default.env.SMOKE_BOS_GATEWAY ?? "everything.dev",
157
+ API_DATABASE_URL: options.apiDatabaseUrl ?? node_process.default.env.SMOKE_API_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5434/api_test_db",
158
+ AUTH_DATABASE_URL: options.authDatabaseUrl ?? node_process.default.env.SMOKE_AUTH_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5435/auth_test_db",
159
+ BETTER_AUTH_SECRET: options.authSecret ?? node_process.default.env.SMOKE_BETTER_AUTH_SECRET ?? "smoke-test-secret-do-not-use-in-production"
160
+ };
161
+ const up = docker([
162
+ "run",
163
+ "-d",
164
+ "--name",
165
+ container,
166
+ "-p",
167
+ `${port}:${port}`,
168
+ "--add-host=host.docker.internal:host-gateway",
169
+ ...Object.entries(envPairs).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
170
+ image
171
+ ]);
172
+ if (up.status !== 0) return {
173
+ ok: false,
174
+ image,
175
+ error: `docker run failed: ${up.stderr.trim()}`
176
+ };
177
+ try {
178
+ const exitWatch = watchContainerExit(container);
179
+ const bootChecks = Promise.all([
180
+ pollUntil("health", () => fetchStatusOk(`${baseUrl}/health`).then((r) => `status ${r.status}`)),
181
+ pollUntil("ssr page", async () => {
182
+ const response = await fetchStatusOk(`${baseUrl}/`);
183
+ const contentType = response.headers.get("content-type") ?? "";
184
+ if (!contentType.includes("text/html")) throw new Error(`/ content-type ${contentType}`);
185
+ const body = await response.text();
186
+ if (!/<html/i.test(body)) throw new Error("/ did not return an HTML document");
187
+ return `${body.length} bytes`;
188
+ }),
189
+ pollUntil("api spec", async () => {
190
+ const spec = await (await fetchStatusOk(`${baseUrl}/api/spec.json`)).json();
191
+ if (!spec.openapi && !spec.paths) throw new Error("/api/spec.json is not an OpenAPI document");
192
+ return `${Object.keys(spec.paths ?? {}).length} paths`;
193
+ })
194
+ ]);
195
+ bootChecks.catch(() => {});
196
+ if (await Promise.race([bootChecks.then(() => "ready"), exitWatch.done]).finally(() => exitWatch.abort()) === "exited") throw new Error("boot container exited — a service failed to start (the cause is in the captured logs below)");
197
+ const [health, ssr, api] = await bootChecks;
198
+ console.log(` ℹ health: ${health}; ssr: ${ssr}; api: ${api}`);
199
+ console.log(` … bos mf check (in-image)`);
200
+ const mf = await pollUntil("bos mf check", async () => {
201
+ const result = docker([
202
+ "exec",
203
+ container,
204
+ "node",
205
+ "./node_modules/everything-dev/dist/cli.mjs",
206
+ "mf",
207
+ "check"
208
+ ]);
209
+ if (result.status !== 0) throw new Error(`bos mf check failed (exit ${result.status})\n${result.stdout}${result.stderr}`);
210
+ return (result.stdout.trim().split("\n").at(-1) ?? "").slice(0, 400);
211
+ }, 6e4);
212
+ const sizeResult = docker([
213
+ "image",
214
+ "inspect",
215
+ "-f",
216
+ "{{.Size}}",
217
+ image
218
+ ]);
219
+ if (sizeResult.status !== 0) throw new Error(`image inspect failed: ${sizeResult.stderr}`);
220
+ const mb = (Number(sizeResult.stdout.trim()) / 1048576).toFixed(1);
221
+ console.log(` ℹ image size: ${mb} MB (${image})`);
222
+ const summary = `health: ${health}; ssr: ${ssr}; api: ${api}; mf compat: ${mf}`;
223
+ console.log("smoke: PASS");
224
+ return {
225
+ ok: true,
226
+ image,
227
+ summary
228
+ };
229
+ } catch (error) {
230
+ const failure = error instanceof Error ? error.message : String(error);
231
+ console.error(`smoke: FAIL — ${failure}`);
232
+ const diagnosis = probeCdnFromContainer(container, image, envPairs.BOS_ACCOUNT, envPairs.BOS_GATEWAY);
233
+ console.error(`smoke diagnosis: ${diagnosis}`);
234
+ const stop = docker([
235
+ "stop",
236
+ "-t",
237
+ "15",
238
+ container
239
+ ]);
240
+ if (stop.status !== 0) console.error(`smoke: graceful stop failed — ${stop.stderr.trim()}`);
241
+ const logs = docker(["logs", container]);
242
+ captureContainerLogs(container, `${logs.stdout}${logs.stderr}`.split("\n"));
243
+ if (/pins no version manifest|redeploy required/i.test(`${logs.stdout}${logs.stderr}`)) console.error("smoke: the published config predates version-manifest pins — run `bos deploy` to republish with pins; the smoke gate goes green after that");
244
+ return {
245
+ ok: false,
246
+ image,
247
+ diagnosis,
248
+ error: failure
249
+ };
250
+ } finally {
251
+ if (!keep) docker([
252
+ "rm",
253
+ "-f",
254
+ container
255
+ ]);
256
+ else console.log(`smoke: kept container ${container}`);
257
+ }
258
+ }
259
+
260
+ //#endregion
261
+ exports.runSmoke = runSmoke;
262
+ //# sourceMappingURL=smoke.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.cjs","names":["spawnSync","process","join","readdirSync","readFileSync"],"sources":["../src/smoke.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/**\n * The runtime-image boot gate: boots a runtime image the way production\n * boots — identity from BOS_ACCOUNT/BOS_GATEWAY env and the published config\n * from the registry — and asserts the boot surface: /health, one SSR page,\n * the OpenAPI spec, and `bos mf check` in-image. Health/SSR/API polls run\n * concurrently against a shared boot deadline; a container that exits early\n * fails fast (the start stack exits non-zero when a service fails to boot),\n * and the failure path probes the CDN from inside the container to classify\n * network-class vs app-class boot failures before stopping gracefully (the\n * dev-session finalizer flushes its pending log lines on SIGTERM) and\n * capturing the full container stdout and session logs to `.bos/logs/smoke`.\n *\n * As the post-publish leg of `bos deploy --smoke`, this is the lockstep gate\n * between the published config and the Railway promotion: an image that\n * cannot boot against its own bundles never reaches production.\n */\n\nexport interface SmokeOptions {\n /** Runtime image ref to boot — required. */\n readonly image: string;\n /** Host port to publish the container on (default 3000). */\n readonly port?: number;\n /** Keep the container after the run (debugging). */\n readonly keep?: boolean;\n /** Registry identity the image boots under (defaults to the dev-everything identity). */\n readonly account?: string;\n readonly gateway?: string;\n readonly apiDatabaseUrl?: string;\n readonly authDatabaseUrl?: string;\n readonly authSecret?: string;\n}\n\nexport interface SmokeResult {\n readonly ok: boolean;\n readonly image: string;\n /** One-line boot-surface summary on success. */\n readonly summary?: string;\n /** The failure classification from the in-container CDN probe. */\n readonly diagnosis?: string;\n readonly error?: string;\n}\n\nconst BOOT_DEADLINE_MS = 240_000;\nconst POLL_INTERVAL_MS = 1_000;\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\ninterface RunResult {\n status: number | null;\n stdout: string;\n stderr: string;\n}\n\nfunction run(cmd: string, args: readonly string[]): RunResult {\n const result = spawnSync(cmd, args, { encoding: \"utf8\" });\n if (result.error) throw result.error;\n return { status: result.status, stdout: result.stdout ?? \"\", stderr: result.stderr ?? \"\" };\n}\n\nconst docker = (args: readonly string[]): RunResult => run(\"docker\", args);\n\nasync function fetchStatusOk(url: string): Promise<Response> {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`${url} → ${response.status}`);\n return response;\n}\n\n/** Bounded poll: resolves with the check's result once it passes, rejects at the deadline. */\nasync function pollUntil<T>(\n name: string,\n check: () => Promise<T>,\n deadlineMs = BOOT_DEADLINE_MS,\n): Promise<T> {\n const deadline = Date.now() + deadlineMs;\n let lastFailure = \"no attempt\";\n let attempt = 0;\n while (Date.now() < deadline) {\n attempt += 1;\n try {\n const detail = await check();\n console.log(` ✓ ${name} (attempt ${attempt})`);\n return detail;\n } catch (error) {\n lastFailure = error instanceof Error ? error.message : String(error);\n }\n await sleep(POLL_INTERVAL_MS);\n }\n throw new Error(`${name} did not pass within the boot deadline — last failure: ${lastFailure}`);\n}\n\n/** Resolves \"exited\" once the boot container has exited — the start stack\n * exits non-zero when a service fails to boot, so a dead container means the\n * remaining pollUntil time would be wasted polling nothing. Resolves\n * undefined when aborted. */\nfunction watchContainerExit(container: string): {\n done: Promise<\"exited\" | undefined>;\n abort: () => void;\n} {\n const controller = { aborted: false };\n const done = (async () => {\n while (!controller.aborted) {\n const state = docker([\"inspect\", \"-f\", \"{{.State.Running}}\", container]);\n if ((state.stdout ?? \"\").trim() === \"false\") return \"exited\" as const;\n await sleep(2_000);\n }\n return undefined;\n })();\n return { done, abort: () => (controller.aborted = true) };\n}\n\n/** While the container is still running, probe the CDN from inside it — a\n * boot failure on :3000 is either the CDN edge (network-class: unreachable,\n * 429/5xx, challenge) or a boot bug (module eval, shared-dependency skew,\n * server start), and the two need opposite fixes. Falls back to a one-off\n * container from the same image (equivalent network class) when the boot\n * container already exited. */\nfunction probeCdnFromContainer(\n container: string,\n image: string,\n account: string,\n gateway: string,\n): string {\n const cdnBase = process.env.SMOKE_CDN_BASE ?? \"https://cdn.everything.dev\";\n const base = `${cdnBase}/bundles/${account}/${gateway}/host`;\n const script = `\nconst base = ${JSON.stringify(base)};\n(async () => {\n const urls = [base + \"/mf-manifest.json\"];\n try {\n const manifest = await fetch(urls[0]).then((r) => (r.ok ? r.json() : null));\n const expose = manifest?.exposes?.[0] ?? {};\n const files = expose.assets?.js?.sync ?? expose.files?.js?.sync ?? [];\n for (const f of files.slice(0, 3)) urls.push(base + \"/\" + f);\n } catch {}\n for (const url of urls) {\n try {\n const res = await fetch(url);\n const bytes = res.ok ? (await res.arrayBuffer()).byteLength : 0;\n console.log(\"probe \" + url + \" -> \" + res.status + (bytes ? \" \" + bytes + \"B\" : \"\"));\n } catch (error) {\n console.log(\"probe \" + url + \" -> FETCH_FAILED \" + String(error));\n }\n }\n})();\n`;\n let probe = docker([\"exec\", container, \"node\", \"-e\", script]);\n if (probe.status !== 0) {\n probe = docker([\"run\", \"--rm\", image, \"node\", \"-e\", script]);\n }\n const output = `${probe.stdout}${probe.stderr}`.trim();\n console.error(\"--- in-container CDN probe ---\");\n if (output) console.error(output);\n else console.error(`(no output — probe exit ${probe.status}: ${probe.stderr.trim()})`);\n const verdicts = output\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"probe \"))\n .map((line) => {\n const status = line.split(\" -> \")[1] ?? \"\";\n if (status.startsWith(\"FETCH_FAILED\")) return \"unreachable\";\n const code = Number.parseInt(status, 10);\n if (code === 200) return \"ok\";\n return \"edge-error\";\n });\n return verdicts.length === 0\n ? \"probe did not run — see probe exit above\"\n : verdicts.every((v) => v === \"ok\")\n ? \"CDN reachable from inside the container — app-class boot failure; the cause is in the captured logs, not the network\"\n : verdicts.some((v) => v === \"unreachable\")\n ? \"CDN unreachable from inside the container — network-class failure (runner egress or CDN edge)\"\n : \"CDN answered non-200 from inside the container — edge or published-config mismatch (see statuses above)\";\n}\n\n/** Copy the stack's .bos/logs out of the container (into the CI artifact\n * path) and capture the full container stdout — the failure reason for a\n * failed service only exists inside the container, which otherwise gets\n * deleted, and a tail window can cut the error behind spinner output. */\nfunction captureContainerLogs(container: string, dockerLogs: string[]): void {\n const target = join(\".bos\", \"logs\", \"smoke\");\n mkdirSync(target, { recursive: true });\n const cp = docker([\"cp\", `${container}:/app/.bos/logs/.`, target]);\n if (cp.status !== 0) {\n console.error(`smoke: could not copy container logs: ${cp.stderr.trim()}`);\n } else {\n const walk = (dir: string): string[] =>\n readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>\n entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)],\n );\n for (const file of walk(target)) {\n if (!file.endsWith(\".log\")) continue;\n const lines = readFileSync(file, \"utf8\").split(\"\\n\");\n console.error(`--- ${file} (last 60 of ${lines.length} lines) ---`);\n console.error(lines.slice(-60).join(\"\\n\"));\n }\n }\n writeFileSync(join(target, \"smoke-stdout.log\"), dockerLogs.join(\"\\n\"));\n console.error(\n `--- container stdout (full, ${dockerLogs.length} lines) → ${target}/smoke-stdout.log ---`,\n );\n console.error(dockerLogs.slice(-100).join(\"\\n\"));\n}\n\nexport async function runSmoke(options: SmokeOptions): Promise<SmokeResult> {\n const port = options.port ?? 3000;\n const keep = options.keep ?? false;\n const baseUrl = `http://localhost:${port}`;\n const container = `everything-dev-smoke-${process.pid}`;\n const image = options.image;\n\n const envPairs: Record<string, string> = {\n BOS_ACCOUNT: options.account ?? process.env.SMOKE_BOS_ACCOUNT ?? \"dev.everything.near\",\n BOS_GATEWAY: options.gateway ?? process.env.SMOKE_BOS_GATEWAY ?? \"everything.dev\",\n API_DATABASE_URL:\n options.apiDatabaseUrl ??\n process.env.SMOKE_API_DATABASE_URL ??\n \"postgres://everythingdev:everythingdev@host.docker.internal:5434/api_test_db\",\n AUTH_DATABASE_URL:\n options.authDatabaseUrl ??\n process.env.SMOKE_AUTH_DATABASE_URL ??\n \"postgres://everythingdev:everythingdev@host.docker.internal:5435/auth_test_db\",\n BETTER_AUTH_SECRET:\n options.authSecret ??\n process.env.SMOKE_BETTER_AUTH_SECRET ??\n \"smoke-test-secret-do-not-use-in-production\",\n };\n\n const up = docker([\n \"run\",\n \"-d\",\n \"--name\",\n container,\n \"-p\",\n `${port}:${port}`,\n \"--add-host=host.docker.internal:host-gateway\",\n ...Object.entries(envPairs).flatMap(([key, value]) => [\"-e\", `${key}=${value}`]),\n image,\n ]);\n if (up.status !== 0) {\n return { ok: false, image, error: `docker run failed: ${up.stderr.trim()}` };\n }\n\n try {\n const exitWatch = watchContainerExit(container);\n const bootChecks = Promise.all([\n pollUntil(\"health\", () =>\n fetchStatusOk(`${baseUrl}/health`).then((r) => `status ${r.status}`),\n ),\n pollUntil(\"ssr page\", async () => {\n const response = await fetchStatusOk(`${baseUrl}/`);\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"text/html\")) throw new Error(`/ content-type ${contentType}`);\n const body = await response.text();\n if (!/<html/i.test(body)) throw new Error(\"/ did not return an HTML document\");\n return `${body.length} bytes`;\n }),\n pollUntil(\"api spec\", async () => {\n const response = await fetchStatusOk(`${baseUrl}/api/spec.json`);\n const spec = (await response.json()) as { openapi?: unknown; paths?: unknown };\n if (!spec.openapi && !spec.paths) {\n throw new Error(\"/api/spec.json is not an OpenAPI document\");\n }\n return `${Object.keys((spec.paths as Record<string, unknown>) ?? {}).length} paths`;\n }),\n ]);\n bootChecks.catch(() => {});\n const outcome = await Promise.race([bootChecks.then(() => \"ready\"), exitWatch.done]).finally(\n () => exitWatch.abort(),\n );\n if (outcome === \"exited\") {\n throw new Error(\n \"boot container exited — a service failed to start (the cause is in the captured logs below)\",\n );\n }\n const [health, ssr, api] = await bootChecks;\n console.log(` ℹ health: ${health}; ssr: ${ssr}; api: ${api}`);\n\n console.log(` … bos mf check (in-image)`);\n const mf = await pollUntil(\n \"bos mf check\",\n async () => {\n const result = docker([\n \"exec\",\n container,\n \"node\",\n \"./node_modules/everything-dev/dist/cli.mjs\",\n \"mf\",\n \"check\",\n ]);\n if (result.status !== 0) {\n throw new Error(\n `bos mf check failed (exit ${result.status})\\n${result.stdout}${result.stderr}`,\n );\n }\n return (result.stdout.trim().split(\"\\n\").at(-1) ?? \"\").slice(0, 400);\n },\n 60_000,\n );\n\n const sizeResult = docker([\"image\", \"inspect\", \"-f\", \"{{.Size}}\", image]);\n if (sizeResult.status !== 0) throw new Error(`image inspect failed: ${sizeResult.stderr}`);\n const mb = (Number(sizeResult.stdout.trim()) / (1024 * 1024)).toFixed(1);\n console.log(` ℹ image size: ${mb} MB (${image})`);\n\n const summary = `health: ${health}; ssr: ${ssr}; api: ${api}; mf compat: ${mf}`;\n console.log(\"smoke: PASS\");\n return { ok: true, image, summary };\n } catch (error) {\n const failure = error instanceof Error ? error.message : String(error);\n console.error(`smoke: FAIL — ${failure}`);\n const diagnosis = probeCdnFromContainer(\n container,\n image,\n envPairs.BOS_ACCOUNT,\n envPairs.BOS_GATEWAY,\n );\n console.error(`smoke diagnosis: ${diagnosis}`);\n const stop = docker([\"stop\", \"-t\", \"15\", container]);\n if (stop.status !== 0) {\n console.error(`smoke: graceful stop failed — ${stop.stderr.trim()}`);\n }\n const logs = docker([\"logs\", container]);\n captureContainerLogs(container, `${logs.stdout}${logs.stderr}`.split(\"\\n\"));\n if (/pins no version manifest|redeploy required/i.test(`${logs.stdout}${logs.stderr}`)) {\n console.error(\n \"smoke: the published config predates version-manifest pins — run `bos deploy` to republish with pins; the smoke gate goes green after that\",\n );\n }\n return { ok: false, image, diagnosis, error: failure };\n } finally {\n if (!keep) docker([\"rm\", \"-f\", container]);\n else console.log(`smoke: kept container ${container}`);\n }\n}\n"],"mappings":";;;;;;;;AA+CA,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AAEzB,MAAM,SAAS,OAA8B,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AAQ7F,SAAS,IAAI,KAAa,MAAoC;CAC5D,MAAM,aAASA,8BAAU,KAAK,MAAM,EAAE,UAAU,OAAO,CAAC;CACxD,IAAI,OAAO,OAAO,MAAM,OAAO;CAC/B,OAAO;EAAE,QAAQ,OAAO;EAAQ,QAAQ,OAAO,UAAU;EAAI,QAAQ,OAAO,UAAU;CAAG;AAC3F;AAEA,MAAM,UAAU,SAAuC,IAAI,UAAU,IAAI;AAEzE,eAAe,cAAc,KAAgC;CAC3D,MAAM,WAAW,MAAM,MAAM,GAAG;CAChC,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,KAAK,SAAS,QAAQ;CAC/D,OAAO;AACT;;AAGA,eAAe,UACb,MACA,OACA,aAAa,kBACD;CACZ,MAAM,WAAW,KAAK,IAAI,IAAI;CAC9B,IAAI,cAAc;CAClB,IAAI,UAAU;CACd,OAAO,KAAK,IAAI,IAAI,UAAU;EAC5B,WAAW;EACX,IAAI;GACF,MAAM,SAAS,MAAM,MAAM;GAC3B,QAAQ,IAAI,OAAO,KAAK,YAAY,QAAQ,EAAE;GAC9C,OAAO;EACT,SAAS,OAAO;GACd,cAAc,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE;EACA,MAAM,MAAM,gBAAgB;CAC9B;CACA,MAAM,IAAI,MAAM,GAAG,KAAK,yDAAyD,aAAa;AAChG;;;;;AAMA,SAAS,mBAAmB,WAG1B;CACA,MAAM,aAAa,EAAE,SAAS,MAAM;CASpC,OAAO;EAAE,OARK,YAAY;GACxB,OAAO,CAAC,WAAW,SAAS;IAE1B,KADc,OAAO;KAAC;KAAW;KAAM;KAAsB;IAAS,CAC7D,CAAC,CAAC,UAAU,GAAE,CAAE,KAAK,MAAM,SAAS,OAAO;IACpD,MAAM,MAAM,GAAK;GACnB;EAEF,EAAC,CACW;EAAG,aAAc,WAAW,UAAU;CAAM;AAC1D;;;;;;;AAQA,SAAS,sBACP,WACA,OACA,SACA,SACQ;CAER,MAAM,OAAO,GADGC,qBAAQ,IAAI,kBAAkB,6BACtB,WAAW,QAAQ,GAAG,QAAQ;CACtD,MAAM,SAAS;eACF,KAAK,UAAU,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;CAoBlC,IAAI,QAAQ,OAAO;EAAC;EAAQ;EAAW;EAAQ;EAAM;CAAM,CAAC;CAC5D,IAAI,MAAM,WAAW,GACnB,QAAQ,OAAO;EAAC;EAAO;EAAQ;EAAO;EAAQ;EAAM;CAAM,CAAC;CAE7D,MAAM,SAAS,GAAG,MAAM,SAAS,MAAM,SAAS,KAAK;CACrD,QAAQ,MAAM,gCAAgC;CAC9C,IAAI,QAAQ,QAAQ,MAAM,MAAM;MAC3B,QAAQ,MAAM,2BAA2B,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK,EAAE,EAAE;CACrF,MAAM,WAAW,OACd,MAAM,IAAI,CAAC,CACX,QAAQ,SAAS,KAAK,WAAW,QAAQ,CAAC,CAAC,CAC3C,KAAK,SAAS;EACb,MAAM,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC,MAAM;EACxC,IAAI,OAAO,WAAW,cAAc,GAAG,OAAO;EAE9C,IADa,OAAO,SAAS,QAAQ,EAC9B,MAAM,KAAK,OAAO;EACzB,OAAO;CACT,CAAC;CACH,OAAO,SAAS,WAAW,IACvB,6CACA,SAAS,OAAO,MAAM,MAAM,IAAI,IAC9B,yHACA,SAAS,MAAM,MAAM,MAAM,aAAa,IACtC,kGACA;AACV;;;;;AAMA,SAAS,qBAAqB,WAAmB,YAA4B;CAC3E,MAAM,aAASC,gBAAK,QAAQ,QAAQ,OAAO;CAC3C,uBAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,MAAM,KAAK,OAAO;EAAC;EAAM,GAAG,UAAU;EAAoB;CAAM,CAAC;CACjE,IAAI,GAAG,WAAW,GAChB,QAAQ,MAAM,yCAAyC,GAAG,OAAO,KAAK,GAAG;MACpE;EACL,MAAM,QAAQ,YACZC,qBAAY,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,SAAS,UACjD,MAAM,YAAY,IAAI,SAAKD,gBAAK,KAAK,MAAM,IAAI,CAAC,IAAI,KAACA,gBAAK,KAAK,MAAM,IAAI,CAAC,CAC5E;EACF,KAAK,MAAM,QAAQ,KAAK,MAAM,GAAG;GAC/B,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG;GAC5B,MAAM,YAAQE,sBAAa,MAAM,MAAM,CAAC,CAAC,MAAM,IAAI;GACnD,QAAQ,MAAM,OAAO,KAAK,eAAe,MAAM,OAAO,YAAY;GAClE,QAAQ,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC;EAC3C;CACF;CACA,+BAAcF,gBAAK,QAAQ,kBAAkB,GAAG,WAAW,KAAK,IAAI,CAAC;CACrE,QAAQ,MACN,+BAA+B,WAAW,OAAO,YAAY,OAAO,sBACtE;CACA,QAAQ,MAAM,WAAW,MAAM,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;AACjD;AAEA,eAAsB,SAAS,SAA6C;CAC1E,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,UAAU,oBAAoB;CACpC,MAAM,YAAY,wBAAwBD,qBAAQ;CAClD,MAAM,QAAQ,QAAQ;CAEtB,MAAM,WAAmC;EACvC,aAAa,QAAQ,WAAWA,qBAAQ,IAAI,qBAAqB;EACjE,aAAa,QAAQ,WAAWA,qBAAQ,IAAI,qBAAqB;EACjE,kBACE,QAAQ,kBACRA,qBAAQ,IAAI,0BACZ;EACF,mBACE,QAAQ,mBACRA,qBAAQ,IAAI,2BACZ;EACF,oBACE,QAAQ,cACRA,qBAAQ,IAAI,4BACZ;CACJ;CAEA,MAAM,KAAK,OAAO;EAChB;EACA;EACA;EACA;EACA;EACA,GAAG,KAAK,GAAG;EACX;EACA,GAAG,OAAO,QAAQ,QAAQ,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC;EAC/E;CACF,CAAC;CACD,IAAI,GAAG,WAAW,GAChB,OAAO;EAAE,IAAI;EAAO;EAAO,OAAO,sBAAsB,GAAG,OAAO,KAAK;CAAI;CAG7E,IAAI;EACF,MAAM,YAAY,mBAAmB,SAAS;EAC9C,MAAM,aAAa,QAAQ,IAAI;GAC7B,UAAU,gBACR,cAAc,GAAG,QAAQ,QAAQ,CAAC,CAAC,MAAM,MAAM,UAAU,EAAE,QAAQ,CACrE;GACA,UAAU,YAAY,YAAY;IAChC,MAAM,WAAW,MAAM,cAAc,GAAG,QAAQ,EAAE;IAClD,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;IAC5D,IAAI,CAAC,YAAY,SAAS,WAAW,GAAG,MAAM,IAAI,MAAM,kBAAkB,aAAa;IACvF,MAAM,OAAO,MAAM,SAAS,KAAK;IACjC,IAAI,CAAC,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,MAAM,mCAAmC;IAC7E,OAAO,GAAG,KAAK,OAAO;GACxB,CAAC;GACD,UAAU,YAAY,YAAY;IAEhC,MAAM,OAAQ,OAAM,MADG,cAAc,GAAG,QAAQ,eAAe,EACnC,CAAC,KAAK;IAClC,IAAI,CAAC,KAAK,WAAW,CAAC,KAAK,OACzB,MAAM,IAAI,MAAM,2CAA2C;IAE7D,OAAO,GAAG,OAAO,KAAM,KAAK,SAAqC,CAAC,CAAC,CAAC,CAAC,OAAO;GAC9E,CAAC;EACH,CAAC;EACD,WAAW,YAAY,CAAC,CAAC;EAIzB,IAAI,MAHkB,QAAQ,KAAK,CAAC,WAAW,WAAW,OAAO,GAAG,UAAU,IAAI,CAAC,CAAC,CAAC,cAC7E,UAAU,MAAM,CACxB,MACgB,UACd,MAAM,IAAI,MACR,6FACF;EAEF,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM;EACjC,QAAQ,IAAI,eAAe,OAAO,SAAS,IAAI,SAAS,KAAK;EAE7D,QAAQ,IAAI,6BAA6B;EACzC,MAAM,KAAK,MAAM,UACf,gBACA,YAAY;GACV,MAAM,SAAS,OAAO;IACpB;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GACD,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MACR,6BAA6B,OAAO,OAAO,KAAK,OAAO,SAAS,OAAO,QACzE;GAEF,QAAQ,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,GAAE,CAAE,MAAM,GAAG,GAAG;EACrE,GACA,GACF;EAEA,MAAM,aAAa,OAAO;GAAC;GAAS;GAAW;GAAM;GAAa;EAAK,CAAC;EACxE,IAAI,WAAW,WAAW,GAAG,MAAM,IAAI,MAAM,yBAAyB,WAAW,QAAQ;EACzF,MAAM,MAAM,OAAO,WAAW,OAAO,KAAK,CAAC,IAAK,QAAY,CAAE,QAAQ,CAAC;EACvE,QAAQ,IAAI,mBAAmB,GAAG,OAAO,MAAM,EAAE;EAEjD,MAAM,UAAU,WAAW,OAAO,SAAS,IAAI,SAAS,IAAI,eAAe;EAC3E,QAAQ,IAAI,aAAa;EACzB,OAAO;GAAE,IAAI;GAAM;GAAO;EAAQ;CACpC,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,QAAQ,MAAM,iBAAiB,SAAS;EACxC,MAAM,YAAY,sBAChB,WACA,OACA,SAAS,aACT,SAAS,WACX;EACA,QAAQ,MAAM,oBAAoB,WAAW;EAC7C,MAAM,OAAO,OAAO;GAAC;GAAQ;GAAM;GAAM;EAAS,CAAC;EACnD,IAAI,KAAK,WAAW,GAClB,QAAQ,MAAM,iCAAiC,KAAK,OAAO,KAAK,GAAG;EAErE,MAAM,OAAO,OAAO,CAAC,QAAQ,SAAS,CAAC;EACvC,qBAAqB,WAAW,GAAG,KAAK,SAAS,KAAK,SAAS,MAAM,IAAI,CAAC;EAC1E,IAAI,8CAA8C,KAAK,GAAG,KAAK,SAAS,KAAK,QAAQ,GACnF,QAAQ,MACN,4IACF;EAEF,OAAO;GAAE,IAAI;GAAO;GAAO;GAAW,OAAO;EAAQ;CACvD,UAAU;EACR,IAAI,CAAC,MAAM,OAAO;GAAC;GAAM;GAAM;EAAS,CAAC;OACpC,QAAQ,IAAI,yBAAyB,WAAW;CACvD;AACF"}
package/dist/smoke.mjs ADDED
@@ -0,0 +1,260 @@
1
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import process from "node:process";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ //#region src/smoke.ts
7
+ const BOOT_DEADLINE_MS = 24e4;
8
+ const POLL_INTERVAL_MS = 1e3;
9
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+ function run(cmd, args) {
11
+ const result = spawnSync(cmd, args, { encoding: "utf8" });
12
+ if (result.error) throw result.error;
13
+ return {
14
+ status: result.status,
15
+ stdout: result.stdout ?? "",
16
+ stderr: result.stderr ?? ""
17
+ };
18
+ }
19
+ const docker = (args) => run("docker", args);
20
+ async function fetchStatusOk(url) {
21
+ const response = await fetch(url);
22
+ if (!response.ok) throw new Error(`${url} → ${response.status}`);
23
+ return response;
24
+ }
25
+ /** Bounded poll: resolves with the check's result once it passes, rejects at the deadline. */
26
+ async function pollUntil(name, check, deadlineMs = BOOT_DEADLINE_MS) {
27
+ const deadline = Date.now() + deadlineMs;
28
+ let lastFailure = "no attempt";
29
+ let attempt = 0;
30
+ while (Date.now() < deadline) {
31
+ attempt += 1;
32
+ try {
33
+ const detail = await check();
34
+ console.log(` ✓ ${name} (attempt ${attempt})`);
35
+ return detail;
36
+ } catch (error) {
37
+ lastFailure = error instanceof Error ? error.message : String(error);
38
+ }
39
+ await sleep(POLL_INTERVAL_MS);
40
+ }
41
+ throw new Error(`${name} did not pass within the boot deadline — last failure: ${lastFailure}`);
42
+ }
43
+ /** Resolves "exited" once the boot container has exited — the start stack
44
+ * exits non-zero when a service fails to boot, so a dead container means the
45
+ * remaining pollUntil time would be wasted polling nothing. Resolves
46
+ * undefined when aborted. */
47
+ function watchContainerExit(container) {
48
+ const controller = { aborted: false };
49
+ return {
50
+ done: (async () => {
51
+ while (!controller.aborted) {
52
+ if ((docker([
53
+ "inspect",
54
+ "-f",
55
+ "{{.State.Running}}",
56
+ container
57
+ ]).stdout ?? "").trim() === "false") return "exited";
58
+ await sleep(2e3);
59
+ }
60
+ })(),
61
+ abort: () => controller.aborted = true
62
+ };
63
+ }
64
+ /** While the container is still running, probe the CDN from inside it — a
65
+ * boot failure on :3000 is either the CDN edge (network-class: unreachable,
66
+ * 429/5xx, challenge) or a boot bug (module eval, shared-dependency skew,
67
+ * server start), and the two need opposite fixes. Falls back to a one-off
68
+ * container from the same image (equivalent network class) when the boot
69
+ * container already exited. */
70
+ function probeCdnFromContainer(container, image, account, gateway) {
71
+ const base = `${process.env.SMOKE_CDN_BASE ?? "https://cdn.everything.dev"}/bundles/${account}/${gateway}/host`;
72
+ const script = `
73
+ const base = ${JSON.stringify(base)};
74
+ (async () => {
75
+ const urls = [base + "/mf-manifest.json"];
76
+ try {
77
+ const manifest = await fetch(urls[0]).then((r) => (r.ok ? r.json() : null));
78
+ const expose = manifest?.exposes?.[0] ?? {};
79
+ const files = expose.assets?.js?.sync ?? expose.files?.js?.sync ?? [];
80
+ for (const f of files.slice(0, 3)) urls.push(base + "/" + f);
81
+ } catch {}
82
+ for (const url of urls) {
83
+ try {
84
+ const res = await fetch(url);
85
+ const bytes = res.ok ? (await res.arrayBuffer()).byteLength : 0;
86
+ console.log("probe " + url + " -> " + res.status + (bytes ? " " + bytes + "B" : ""));
87
+ } catch (error) {
88
+ console.log("probe " + url + " -> FETCH_FAILED " + String(error));
89
+ }
90
+ }
91
+ })();
92
+ `;
93
+ let probe = docker([
94
+ "exec",
95
+ container,
96
+ "node",
97
+ "-e",
98
+ script
99
+ ]);
100
+ if (probe.status !== 0) probe = docker([
101
+ "run",
102
+ "--rm",
103
+ image,
104
+ "node",
105
+ "-e",
106
+ script
107
+ ]);
108
+ const output = `${probe.stdout}${probe.stderr}`.trim();
109
+ console.error("--- in-container CDN probe ---");
110
+ if (output) console.error(output);
111
+ else console.error(`(no output — probe exit ${probe.status}: ${probe.stderr.trim()})`);
112
+ const verdicts = output.split("\n").filter((line) => line.startsWith("probe ")).map((line) => {
113
+ const status = line.split(" -> ")[1] ?? "";
114
+ if (status.startsWith("FETCH_FAILED")) return "unreachable";
115
+ if (Number.parseInt(status, 10) === 200) return "ok";
116
+ return "edge-error";
117
+ });
118
+ return verdicts.length === 0 ? "probe did not run — see probe exit above" : verdicts.every((v) => v === "ok") ? "CDN reachable from inside the container — app-class boot failure; the cause is in the captured logs, not the network" : verdicts.some((v) => v === "unreachable") ? "CDN unreachable from inside the container — network-class failure (runner egress or CDN edge)" : "CDN answered non-200 from inside the container — edge or published-config mismatch (see statuses above)";
119
+ }
120
+ /** Copy the stack's .bos/logs out of the container (into the CI artifact
121
+ * path) and capture the full container stdout — the failure reason for a
122
+ * failed service only exists inside the container, which otherwise gets
123
+ * deleted, and a tail window can cut the error behind spinner output. */
124
+ function captureContainerLogs(container, dockerLogs) {
125
+ const target = join(".bos", "logs", "smoke");
126
+ mkdirSync(target, { recursive: true });
127
+ const cp = docker([
128
+ "cp",
129
+ `${container}:/app/.bos/logs/.`,
130
+ target
131
+ ]);
132
+ if (cp.status !== 0) console.error(`smoke: could not copy container logs: ${cp.stderr.trim()}`);
133
+ else {
134
+ const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)]);
135
+ for (const file of walk(target)) {
136
+ if (!file.endsWith(".log")) continue;
137
+ const lines = readFileSync(file, "utf8").split("\n");
138
+ console.error(`--- ${file} (last 60 of ${lines.length} lines) ---`);
139
+ console.error(lines.slice(-60).join("\n"));
140
+ }
141
+ }
142
+ writeFileSync(join(target, "smoke-stdout.log"), dockerLogs.join("\n"));
143
+ console.error(`--- container stdout (full, ${dockerLogs.length} lines) → ${target}/smoke-stdout.log ---`);
144
+ console.error(dockerLogs.slice(-100).join("\n"));
145
+ }
146
+ async function runSmoke(options) {
147
+ const port = options.port ?? 3e3;
148
+ const keep = options.keep ?? false;
149
+ const baseUrl = `http://localhost:${port}`;
150
+ const container = `everything-dev-smoke-${process.pid}`;
151
+ const image = options.image;
152
+ const envPairs = {
153
+ BOS_ACCOUNT: options.account ?? process.env.SMOKE_BOS_ACCOUNT ?? "dev.everything.near",
154
+ BOS_GATEWAY: options.gateway ?? process.env.SMOKE_BOS_GATEWAY ?? "everything.dev",
155
+ API_DATABASE_URL: options.apiDatabaseUrl ?? process.env.SMOKE_API_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5434/api_test_db",
156
+ AUTH_DATABASE_URL: options.authDatabaseUrl ?? process.env.SMOKE_AUTH_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5435/auth_test_db",
157
+ BETTER_AUTH_SECRET: options.authSecret ?? process.env.SMOKE_BETTER_AUTH_SECRET ?? "smoke-test-secret-do-not-use-in-production"
158
+ };
159
+ const up = docker([
160
+ "run",
161
+ "-d",
162
+ "--name",
163
+ container,
164
+ "-p",
165
+ `${port}:${port}`,
166
+ "--add-host=host.docker.internal:host-gateway",
167
+ ...Object.entries(envPairs).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
168
+ image
169
+ ]);
170
+ if (up.status !== 0) return {
171
+ ok: false,
172
+ image,
173
+ error: `docker run failed: ${up.stderr.trim()}`
174
+ };
175
+ try {
176
+ const exitWatch = watchContainerExit(container);
177
+ const bootChecks = Promise.all([
178
+ pollUntil("health", () => fetchStatusOk(`${baseUrl}/health`).then((r) => `status ${r.status}`)),
179
+ pollUntil("ssr page", async () => {
180
+ const response = await fetchStatusOk(`${baseUrl}/`);
181
+ const contentType = response.headers.get("content-type") ?? "";
182
+ if (!contentType.includes("text/html")) throw new Error(`/ content-type ${contentType}`);
183
+ const body = await response.text();
184
+ if (!/<html/i.test(body)) throw new Error("/ did not return an HTML document");
185
+ return `${body.length} bytes`;
186
+ }),
187
+ pollUntil("api spec", async () => {
188
+ const spec = await (await fetchStatusOk(`${baseUrl}/api/spec.json`)).json();
189
+ if (!spec.openapi && !spec.paths) throw new Error("/api/spec.json is not an OpenAPI document");
190
+ return `${Object.keys(spec.paths ?? {}).length} paths`;
191
+ })
192
+ ]);
193
+ bootChecks.catch(() => {});
194
+ if (await Promise.race([bootChecks.then(() => "ready"), exitWatch.done]).finally(() => exitWatch.abort()) === "exited") throw new Error("boot container exited — a service failed to start (the cause is in the captured logs below)");
195
+ const [health, ssr, api] = await bootChecks;
196
+ console.log(` ℹ health: ${health}; ssr: ${ssr}; api: ${api}`);
197
+ console.log(` … bos mf check (in-image)`);
198
+ const mf = await pollUntil("bos mf check", async () => {
199
+ const result = docker([
200
+ "exec",
201
+ container,
202
+ "node",
203
+ "./node_modules/everything-dev/dist/cli.mjs",
204
+ "mf",
205
+ "check"
206
+ ]);
207
+ if (result.status !== 0) throw new Error(`bos mf check failed (exit ${result.status})\n${result.stdout}${result.stderr}`);
208
+ return (result.stdout.trim().split("\n").at(-1) ?? "").slice(0, 400);
209
+ }, 6e4);
210
+ const sizeResult = docker([
211
+ "image",
212
+ "inspect",
213
+ "-f",
214
+ "{{.Size}}",
215
+ image
216
+ ]);
217
+ if (sizeResult.status !== 0) throw new Error(`image inspect failed: ${sizeResult.stderr}`);
218
+ const mb = (Number(sizeResult.stdout.trim()) / 1048576).toFixed(1);
219
+ console.log(` ℹ image size: ${mb} MB (${image})`);
220
+ const summary = `health: ${health}; ssr: ${ssr}; api: ${api}; mf compat: ${mf}`;
221
+ console.log("smoke: PASS");
222
+ return {
223
+ ok: true,
224
+ image,
225
+ summary
226
+ };
227
+ } catch (error) {
228
+ const failure = error instanceof Error ? error.message : String(error);
229
+ console.error(`smoke: FAIL — ${failure}`);
230
+ const diagnosis = probeCdnFromContainer(container, image, envPairs.BOS_ACCOUNT, envPairs.BOS_GATEWAY);
231
+ console.error(`smoke diagnosis: ${diagnosis}`);
232
+ const stop = docker([
233
+ "stop",
234
+ "-t",
235
+ "15",
236
+ container
237
+ ]);
238
+ if (stop.status !== 0) console.error(`smoke: graceful stop failed — ${stop.stderr.trim()}`);
239
+ const logs = docker(["logs", container]);
240
+ captureContainerLogs(container, `${logs.stdout}${logs.stderr}`.split("\n"));
241
+ if (/pins no version manifest|redeploy required/i.test(`${logs.stdout}${logs.stderr}`)) console.error("smoke: the published config predates version-manifest pins — run `bos deploy` to republish with pins; the smoke gate goes green after that");
242
+ return {
243
+ ok: false,
244
+ image,
245
+ diagnosis,
246
+ error: failure
247
+ };
248
+ } finally {
249
+ if (!keep) docker([
250
+ "rm",
251
+ "-f",
252
+ container
253
+ ]);
254
+ else console.log(`smoke: kept container ${container}`);
255
+ }
256
+ }
257
+
258
+ //#endregion
259
+ export { runSmoke };
260
+ //# sourceMappingURL=smoke.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.mjs","names":[],"sources":["../src/smoke.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/**\n * The runtime-image boot gate: boots a runtime image the way production\n * boots — identity from BOS_ACCOUNT/BOS_GATEWAY env and the published config\n * from the registry — and asserts the boot surface: /health, one SSR page,\n * the OpenAPI spec, and `bos mf check` in-image. Health/SSR/API polls run\n * concurrently against a shared boot deadline; a container that exits early\n * fails fast (the start stack exits non-zero when a service fails to boot),\n * and the failure path probes the CDN from inside the container to classify\n * network-class vs app-class boot failures before stopping gracefully (the\n * dev-session finalizer flushes its pending log lines on SIGTERM) and\n * capturing the full container stdout and session logs to `.bos/logs/smoke`.\n *\n * As the post-publish leg of `bos deploy --smoke`, this is the lockstep gate\n * between the published config and the Railway promotion: an image that\n * cannot boot against its own bundles never reaches production.\n */\n\nexport interface SmokeOptions {\n /** Runtime image ref to boot — required. */\n readonly image: string;\n /** Host port to publish the container on (default 3000). */\n readonly port?: number;\n /** Keep the container after the run (debugging). */\n readonly keep?: boolean;\n /** Registry identity the image boots under (defaults to the dev-everything identity). */\n readonly account?: string;\n readonly gateway?: string;\n readonly apiDatabaseUrl?: string;\n readonly authDatabaseUrl?: string;\n readonly authSecret?: string;\n}\n\nexport interface SmokeResult {\n readonly ok: boolean;\n readonly image: string;\n /** One-line boot-surface summary on success. */\n readonly summary?: string;\n /** The failure classification from the in-container CDN probe. */\n readonly diagnosis?: string;\n readonly error?: string;\n}\n\nconst BOOT_DEADLINE_MS = 240_000;\nconst POLL_INTERVAL_MS = 1_000;\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\ninterface RunResult {\n status: number | null;\n stdout: string;\n stderr: string;\n}\n\nfunction run(cmd: string, args: readonly string[]): RunResult {\n const result = spawnSync(cmd, args, { encoding: \"utf8\" });\n if (result.error) throw result.error;\n return { status: result.status, stdout: result.stdout ?? \"\", stderr: result.stderr ?? \"\" };\n}\n\nconst docker = (args: readonly string[]): RunResult => run(\"docker\", args);\n\nasync function fetchStatusOk(url: string): Promise<Response> {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`${url} → ${response.status}`);\n return response;\n}\n\n/** Bounded poll: resolves with the check's result once it passes, rejects at the deadline. */\nasync function pollUntil<T>(\n name: string,\n check: () => Promise<T>,\n deadlineMs = BOOT_DEADLINE_MS,\n): Promise<T> {\n const deadline = Date.now() + deadlineMs;\n let lastFailure = \"no attempt\";\n let attempt = 0;\n while (Date.now() < deadline) {\n attempt += 1;\n try {\n const detail = await check();\n console.log(` ✓ ${name} (attempt ${attempt})`);\n return detail;\n } catch (error) {\n lastFailure = error instanceof Error ? error.message : String(error);\n }\n await sleep(POLL_INTERVAL_MS);\n }\n throw new Error(`${name} did not pass within the boot deadline — last failure: ${lastFailure}`);\n}\n\n/** Resolves \"exited\" once the boot container has exited — the start stack\n * exits non-zero when a service fails to boot, so a dead container means the\n * remaining pollUntil time would be wasted polling nothing. Resolves\n * undefined when aborted. */\nfunction watchContainerExit(container: string): {\n done: Promise<\"exited\" | undefined>;\n abort: () => void;\n} {\n const controller = { aborted: false };\n const done = (async () => {\n while (!controller.aborted) {\n const state = docker([\"inspect\", \"-f\", \"{{.State.Running}}\", container]);\n if ((state.stdout ?? \"\").trim() === \"false\") return \"exited\" as const;\n await sleep(2_000);\n }\n return undefined;\n })();\n return { done, abort: () => (controller.aborted = true) };\n}\n\n/** While the container is still running, probe the CDN from inside it — a\n * boot failure on :3000 is either the CDN edge (network-class: unreachable,\n * 429/5xx, challenge) or a boot bug (module eval, shared-dependency skew,\n * server start), and the two need opposite fixes. Falls back to a one-off\n * container from the same image (equivalent network class) when the boot\n * container already exited. */\nfunction probeCdnFromContainer(\n container: string,\n image: string,\n account: string,\n gateway: string,\n): string {\n const cdnBase = process.env.SMOKE_CDN_BASE ?? \"https://cdn.everything.dev\";\n const base = `${cdnBase}/bundles/${account}/${gateway}/host`;\n const script = `\nconst base = ${JSON.stringify(base)};\n(async () => {\n const urls = [base + \"/mf-manifest.json\"];\n try {\n const manifest = await fetch(urls[0]).then((r) => (r.ok ? r.json() : null));\n const expose = manifest?.exposes?.[0] ?? {};\n const files = expose.assets?.js?.sync ?? expose.files?.js?.sync ?? [];\n for (const f of files.slice(0, 3)) urls.push(base + \"/\" + f);\n } catch {}\n for (const url of urls) {\n try {\n const res = await fetch(url);\n const bytes = res.ok ? (await res.arrayBuffer()).byteLength : 0;\n console.log(\"probe \" + url + \" -> \" + res.status + (bytes ? \" \" + bytes + \"B\" : \"\"));\n } catch (error) {\n console.log(\"probe \" + url + \" -> FETCH_FAILED \" + String(error));\n }\n }\n})();\n`;\n let probe = docker([\"exec\", container, \"node\", \"-e\", script]);\n if (probe.status !== 0) {\n probe = docker([\"run\", \"--rm\", image, \"node\", \"-e\", script]);\n }\n const output = `${probe.stdout}${probe.stderr}`.trim();\n console.error(\"--- in-container CDN probe ---\");\n if (output) console.error(output);\n else console.error(`(no output — probe exit ${probe.status}: ${probe.stderr.trim()})`);\n const verdicts = output\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"probe \"))\n .map((line) => {\n const status = line.split(\" -> \")[1] ?? \"\";\n if (status.startsWith(\"FETCH_FAILED\")) return \"unreachable\";\n const code = Number.parseInt(status, 10);\n if (code === 200) return \"ok\";\n return \"edge-error\";\n });\n return verdicts.length === 0\n ? \"probe did not run — see probe exit above\"\n : verdicts.every((v) => v === \"ok\")\n ? \"CDN reachable from inside the container — app-class boot failure; the cause is in the captured logs, not the network\"\n : verdicts.some((v) => v === \"unreachable\")\n ? \"CDN unreachable from inside the container — network-class failure (runner egress or CDN edge)\"\n : \"CDN answered non-200 from inside the container — edge or published-config mismatch (see statuses above)\";\n}\n\n/** Copy the stack's .bos/logs out of the container (into the CI artifact\n * path) and capture the full container stdout — the failure reason for a\n * failed service only exists inside the container, which otherwise gets\n * deleted, and a tail window can cut the error behind spinner output. */\nfunction captureContainerLogs(container: string, dockerLogs: string[]): void {\n const target = join(\".bos\", \"logs\", \"smoke\");\n mkdirSync(target, { recursive: true });\n const cp = docker([\"cp\", `${container}:/app/.bos/logs/.`, target]);\n if (cp.status !== 0) {\n console.error(`smoke: could not copy container logs: ${cp.stderr.trim()}`);\n } else {\n const walk = (dir: string): string[] =>\n readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>\n entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)],\n );\n for (const file of walk(target)) {\n if (!file.endsWith(\".log\")) continue;\n const lines = readFileSync(file, \"utf8\").split(\"\\n\");\n console.error(`--- ${file} (last 60 of ${lines.length} lines) ---`);\n console.error(lines.slice(-60).join(\"\\n\"));\n }\n }\n writeFileSync(join(target, \"smoke-stdout.log\"), dockerLogs.join(\"\\n\"));\n console.error(\n `--- container stdout (full, ${dockerLogs.length} lines) → ${target}/smoke-stdout.log ---`,\n );\n console.error(dockerLogs.slice(-100).join(\"\\n\"));\n}\n\nexport async function runSmoke(options: SmokeOptions): Promise<SmokeResult> {\n const port = options.port ?? 3000;\n const keep = options.keep ?? false;\n const baseUrl = `http://localhost:${port}`;\n const container = `everything-dev-smoke-${process.pid}`;\n const image = options.image;\n\n const envPairs: Record<string, string> = {\n BOS_ACCOUNT: options.account ?? process.env.SMOKE_BOS_ACCOUNT ?? \"dev.everything.near\",\n BOS_GATEWAY: options.gateway ?? process.env.SMOKE_BOS_GATEWAY ?? \"everything.dev\",\n API_DATABASE_URL:\n options.apiDatabaseUrl ??\n process.env.SMOKE_API_DATABASE_URL ??\n \"postgres://everythingdev:everythingdev@host.docker.internal:5434/api_test_db\",\n AUTH_DATABASE_URL:\n options.authDatabaseUrl ??\n process.env.SMOKE_AUTH_DATABASE_URL ??\n \"postgres://everythingdev:everythingdev@host.docker.internal:5435/auth_test_db\",\n BETTER_AUTH_SECRET:\n options.authSecret ??\n process.env.SMOKE_BETTER_AUTH_SECRET ??\n \"smoke-test-secret-do-not-use-in-production\",\n };\n\n const up = docker([\n \"run\",\n \"-d\",\n \"--name\",\n container,\n \"-p\",\n `${port}:${port}`,\n \"--add-host=host.docker.internal:host-gateway\",\n ...Object.entries(envPairs).flatMap(([key, value]) => [\"-e\", `${key}=${value}`]),\n image,\n ]);\n if (up.status !== 0) {\n return { ok: false, image, error: `docker run failed: ${up.stderr.trim()}` };\n }\n\n try {\n const exitWatch = watchContainerExit(container);\n const bootChecks = Promise.all([\n pollUntil(\"health\", () =>\n fetchStatusOk(`${baseUrl}/health`).then((r) => `status ${r.status}`),\n ),\n pollUntil(\"ssr page\", async () => {\n const response = await fetchStatusOk(`${baseUrl}/`);\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"text/html\")) throw new Error(`/ content-type ${contentType}`);\n const body = await response.text();\n if (!/<html/i.test(body)) throw new Error(\"/ did not return an HTML document\");\n return `${body.length} bytes`;\n }),\n pollUntil(\"api spec\", async () => {\n const response = await fetchStatusOk(`${baseUrl}/api/spec.json`);\n const spec = (await response.json()) as { openapi?: unknown; paths?: unknown };\n if (!spec.openapi && !spec.paths) {\n throw new Error(\"/api/spec.json is not an OpenAPI document\");\n }\n return `${Object.keys((spec.paths as Record<string, unknown>) ?? {}).length} paths`;\n }),\n ]);\n bootChecks.catch(() => {});\n const outcome = await Promise.race([bootChecks.then(() => \"ready\"), exitWatch.done]).finally(\n () => exitWatch.abort(),\n );\n if (outcome === \"exited\") {\n throw new Error(\n \"boot container exited — a service failed to start (the cause is in the captured logs below)\",\n );\n }\n const [health, ssr, api] = await bootChecks;\n console.log(` ℹ health: ${health}; ssr: ${ssr}; api: ${api}`);\n\n console.log(` … bos mf check (in-image)`);\n const mf = await pollUntil(\n \"bos mf check\",\n async () => {\n const result = docker([\n \"exec\",\n container,\n \"node\",\n \"./node_modules/everything-dev/dist/cli.mjs\",\n \"mf\",\n \"check\",\n ]);\n if (result.status !== 0) {\n throw new Error(\n `bos mf check failed (exit ${result.status})\\n${result.stdout}${result.stderr}`,\n );\n }\n return (result.stdout.trim().split(\"\\n\").at(-1) ?? \"\").slice(0, 400);\n },\n 60_000,\n );\n\n const sizeResult = docker([\"image\", \"inspect\", \"-f\", \"{{.Size}}\", image]);\n if (sizeResult.status !== 0) throw new Error(`image inspect failed: ${sizeResult.stderr}`);\n const mb = (Number(sizeResult.stdout.trim()) / (1024 * 1024)).toFixed(1);\n console.log(` ℹ image size: ${mb} MB (${image})`);\n\n const summary = `health: ${health}; ssr: ${ssr}; api: ${api}; mf compat: ${mf}`;\n console.log(\"smoke: PASS\");\n return { ok: true, image, summary };\n } catch (error) {\n const failure = error instanceof Error ? error.message : String(error);\n console.error(`smoke: FAIL — ${failure}`);\n const diagnosis = probeCdnFromContainer(\n container,\n image,\n envPairs.BOS_ACCOUNT,\n envPairs.BOS_GATEWAY,\n );\n console.error(`smoke diagnosis: ${diagnosis}`);\n const stop = docker([\"stop\", \"-t\", \"15\", container]);\n if (stop.status !== 0) {\n console.error(`smoke: graceful stop failed — ${stop.stderr.trim()}`);\n }\n const logs = docker([\"logs\", container]);\n captureContainerLogs(container, `${logs.stdout}${logs.stderr}`.split(\"\\n\"));\n if (/pins no version manifest|redeploy required/i.test(`${logs.stdout}${logs.stderr}`)) {\n console.error(\n \"smoke: the published config predates version-manifest pins — run `bos deploy` to republish with pins; the smoke gate goes green after that\",\n );\n }\n return { ok: false, image, diagnosis, error: failure };\n } finally {\n if (!keep) docker([\"rm\", \"-f\", container]);\n else console.log(`smoke: kept container ${container}`);\n }\n}\n"],"mappings":"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"}