everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 21.4 kB
Source Map (JSON)
{"version":3,"file":"smoke.mjs","names":[],"sources":["../../../src/services/deploy/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 // The host falls back to the client shell — status 200, valid HTML —\n // when the server render throws. Only the SSR-attempted-and-failed\n // path renders this marker (no-SSR deployments serve the shell\n // without it), so its presence is a red smoke on any deployment\n // that promises SSR.\n if (body.includes(\"SSR unavailable\")) {\n throw new Error(\"/ served the client-shell fallback — the server render failed\");\n }\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 // ADR 0034: the boot gate also self-describes — the in-image platform\n // release manifest must exist and parse (the release contract ships in\n // the same bytes the host boots from).\n console.log(` … platform manifest (in-image)`);\n const platformResult = docker([\n \"exec\",\n container,\n \"node\",\n \"-e\",\n `const m = JSON.parse(require('fs').readFileSync('./packages/everything-dev/dist/platform-manifest.json','utf8')); if (m.kind !== 'everything-dev/platform-manifest' || !m.frameworkVersion) process.exit(1); console.log(m.frameworkVersion)`,\n ]);\n if (platformResult.status !== 0) {\n throw new Error(\n `in-image platform manifest missing or invalid — the image was not built with the release contract (ADR 0034)\\n${platformResult.stdout}${platformResult.stderr}`,\n );\n }\n const platformVersion = platformResult.stdout.trim();\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}; platform: ${platformVersion}`;\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,SAAS,UAAU,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,GADG,QAAQ,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,SAAS,KAAK,QAAQ,QAAQ,OAAO;CAC3C,UAAU,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,QACZ,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,SAAS,UACjD,MAAM,YAAY,IAAI,KAAK,KAAK,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAC5E;EACF,KAAK,MAAM,QAAQ,KAAK,MAAM,GAAG;GAC/B,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG;GAC5B,MAAM,QAAQ,aAAa,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,cAAc,KAAK,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,wBAAwB,QAAQ;CAClD,MAAM,QAAQ,QAAQ;CAEtB,MAAM,WAAmC;EACvC,aAAa,QAAQ,WAAW,QAAQ,IAAI,qBAAqB;EACjE,aAAa,QAAQ,WAAW,QAAQ,IAAI,qBAAqB;EACjE,kBACE,QAAQ,kBACR,QAAQ,IAAI,0BACZ;EACF,mBACE,QAAQ,mBACR,QAAQ,IAAI,2BACZ;EACF,oBACE,QAAQ,cACR,QAAQ,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;IAM7E,IAAI,KAAK,SAAS,iBAAiB,GACjC,MAAM,IAAI,MAAM,+DAA+D;IAEjF,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;EAKA,QAAQ,IAAI,kCAAkC;EAC9C,MAAM,iBAAiB,OAAO;GAC5B;GACA;GACA;GACA;GACA;EACF,CAAC;EACD,IAAI,eAAe,WAAW,GAC5B,MAAM,IAAI,MACR,iHAAiH,eAAe,SAAS,eAAe,QAC1J;EAEF,MAAM,kBAAkB,eAAe,OAAO,KAAK;EAEnD,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,GAAG,cAAc;EAC5F,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"}