@cimplify/cli 0.15.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/add-C3DPTVH3.mjs +140 -0
  4. package/dist/{add-KRIIKAS4.mjs → add-JX2GXWXU.mjs} +4 -4
  5. package/dist/ai-HUZTVJIQ.mjs +17 -0
  6. package/dist/{assets-CWB72PZG.mjs → assets-MPESJSUV.mjs} +12 -7
  7. package/dist/{auth-step-up-Z6KOTTV7.mjs → auth-step-up-VJAFGSW6.mjs} +5 -5
  8. package/dist/build-E7RRQWVF.mjs +616 -0
  9. package/dist/{cancel-ZHVVILOU.mjs → cancel-UMKR274A.mjs} +4 -4
  10. package/dist/capture-TK4NIUYM.mjs +69 -0
  11. package/dist/chunk-25ZPFHLF.mjs +51 -0
  12. package/dist/{chunk-OLZKNUMK.mjs → chunk-3LJG22FS.mjs} +6 -6
  13. package/dist/chunk-454RQ6NR.mjs +10614 -0
  14. package/dist/{chunk-O2TO2J4I.mjs → chunk-6CQMRZ2W.mjs} +2 -2
  15. package/dist/chunk-6KKLF5KG.mjs +32 -0
  16. package/dist/chunk-74QZQSCL.mjs +84 -0
  17. package/dist/chunk-BGM7CH5G.mjs +81 -0
  18. package/dist/chunk-BTNVYAC4.mjs +5557 -0
  19. package/dist/{chunk-I2UP7YTG.mjs → chunk-ECGDRU56.mjs} +1 -1
  20. package/dist/{chunk-UTD34C2H.mjs → chunk-EOOOXVHR.mjs} +2 -4
  21. package/dist/chunk-EYEMPIMP.mjs +74 -0
  22. package/dist/{chunk-3RPR33MO.mjs → chunk-FV273ECI.mjs} +11 -7
  23. package/dist/{chunk-MLE4MVYC.mjs → chunk-GGPN4QYQ.mjs} +2 -2
  24. package/dist/chunk-HQPZQIWW.mjs +1456 -0
  25. package/dist/{chunk-3QUK5BPY.mjs → chunk-I7GEHLDD.mjs} +16 -1
  26. package/dist/chunk-ITDAICBY.mjs +363 -0
  27. package/dist/chunk-JKECUAAV.mjs +2169 -0
  28. package/dist/chunk-JNCEKW2Y.mjs +41 -0
  29. package/dist/chunk-JTYVGORV.mjs +101 -0
  30. package/dist/chunk-KQNZKV6E.mjs +535 -0
  31. package/dist/chunk-LJDIQ4PF.mjs +6463 -0
  32. package/dist/{chunk-2MKZKUVH.mjs → chunk-LLRUOIMU.mjs} +1 -2
  33. package/dist/chunk-MJMRIMR7.mjs +86 -0
  34. package/dist/chunk-N2WZJX5P.mjs +1989 -0
  35. package/dist/chunk-NEB372WD.mjs +1152 -0
  36. package/dist/chunk-OPX7KPOU.mjs +68 -0
  37. package/dist/chunk-PHUBMRAS.mjs +623 -0
  38. package/dist/chunk-PQBALZUH.mjs +1941 -0
  39. package/dist/chunk-PVW5D5AW.mjs +42 -0
  40. package/dist/{chunk-LZEG3FEY.mjs → chunk-QQWYQ2HS.mjs} +7 -7
  41. package/dist/chunk-QVR6V6QP.mjs +46 -0
  42. package/dist/chunk-SGT5IJVW.mjs +28 -0
  43. package/dist/chunk-SZXO6YSX.mjs +502 -0
  44. package/dist/chunk-THATLMEX.mjs +1227 -0
  45. package/dist/chunk-TYR4Q37L.mjs +391 -0
  46. package/dist/{chunk-DMLQBAGC.mjs → chunk-V3R2NYQR.mjs} +1 -1
  47. package/dist/chunk-VHK7WBO3.mjs +189 -0
  48. package/dist/{chunk-TH4XENKH.mjs → chunk-XASDB7YS.mjs} +1 -1
  49. package/dist/{chunk-NYXA6X56.mjs → chunk-XBQ4LO2N.mjs} +31 -11
  50. package/dist/chunk-XF3MGADK.mjs +2946 -0
  51. package/dist/{chunk-5KYR6WJB.mjs → chunk-XQAHREKO.mjs} +17 -1
  52. package/dist/chunk-XZG3DTXV.mjs +696 -0
  53. package/dist/chunk-Y4YGR2NY.mjs +52 -0
  54. package/dist/{chunk-LJEKKFC7.mjs → chunk-YS2JUNSQ.mjs} +19 -11
  55. package/dist/chunk-ZG7NJLXN.mjs +104 -0
  56. package/dist/create-3TGG7B3X.mjs +10 -0
  57. package/dist/credits-MEG7V5DI.mjs +33 -0
  58. package/dist/{deploy-STWZ3OSM.mjs → deploy-CNRORHJ7.mjs} +11 -11
  59. package/dist/dev-RXSBY7U4.mjs +8 -0
  60. package/dist/dev-YLYC6B7R.mjs +401 -0
  61. package/dist/dispatcher.mjs +131 -30
  62. package/dist/{doctor-3RW3OGZJ.mjs → doctor-XRTNG2IT.mjs} +13 -21
  63. package/dist/{domains-FTNK4IKY.mjs → domains-JIHFKDWR.mjs} +6 -6
  64. package/dist/engine-runtime-fetch-QVKJVC3W.mjs +10 -0
  65. package/dist/{env-TNCYUAMT.mjs → env-LJ3DCAA5.mjs} +7 -7
  66. package/dist/env-R72AHBT2.mjs +19 -0
  67. package/dist/explain-32RSXEXC.mjs +251 -0
  68. package/dist/inspect-PYLYN45K.mjs +86 -0
  69. package/dist/{inspect-X3M7WSTL.mjs → inspect-ZOLZHNSZ.mjs} +28 -13
  70. package/dist/introspect-27MMTACH.mjs +9 -0
  71. package/dist/kernel-IC4ZC54Z.mjs +662 -0
  72. package/dist/kernel-plugin.js +6862 -0
  73. package/dist/keys-WKOQ4KQE.mjs +71 -0
  74. package/dist/lines-I6ZB2IOU.mjs +56 -0
  75. package/dist/{link-76Z6V6CJ.mjs → link-TE2FUX74.mjs} +6 -6
  76. package/dist/{lint-DDZ7HUQN.mjs → lint-735IPMC6.mjs} +2 -2
  77. package/dist/{list-2MFOQKKQ.mjs → list-CHYY2P4T.mjs} +7 -11
  78. package/dist/lock-XVR3RRZ6.mjs +57 -0
  79. package/dist/{login-RLMDEWAL.mjs → login-LW7CHZDZ.mjs} +10 -8
  80. package/dist/{logout-Z57X6HPE.mjs → logout-6U6OP5DM.mjs} +2 -2
  81. package/dist/{logs-TNDM4RFE.mjs → logs-HKH4H6PT.mjs} +5 -5
  82. package/dist/logs-SCQC5HNO.mjs +44 -0
  83. package/dist/mcp-VRHQOEGG.mjs +1077 -0
  84. package/dist/{mock-EVER34IX.mjs → mock-5H3CVPHC.mjs} +2 -2
  85. package/dist/outdated-T7QW6F3Z.mjs +58 -0
  86. package/dist/port-24R7ZNP4.mjs +2408 -0
  87. package/dist/{projects-YL4T24VV.mjs → projects-HYITVEXS.mjs} +6 -6
  88. package/dist/publish-DZRR5A5X.mjs +20 -0
  89. package/dist/remix-4HVXRIEB.mjs +125 -0
  90. package/dist/repo-FG3CMZQS.mjs +9 -0
  91. package/dist/report-M7F4ZYP7.mjs +2 -0
  92. package/dist/rollback-H7Q4XYGX.mjs +43 -0
  93. package/dist/{rollback-47VQ4HJK.mjs → rollback-OM64O67F.mjs} +6 -6
  94. package/dist/scene-QTHT3ZPL.mjs +324 -0
  95. package/dist/{status-TWPOMWNA.mjs → status-5D4RR3MM.mjs} +5 -5
  96. package/dist/status-AKI6E3JK.mjs +18 -0
  97. package/dist/test-W6MURUGZ.mjs +233 -0
  98. package/dist/testers-EI32JB5F.mjs +49 -0
  99. package/dist/{unlink-H3BO3DOQ.mjs → unlink-BND47IWW.mjs} +2 -2
  100. package/dist/{update-FLY46XI3.mjs → update-XKKFREKH.mjs} +4 -4
  101. package/dist/{whoami-CQPPARRS.mjs → whoami-WHLGCVRC.mjs} +3 -3
  102. package/dist/workshop-F2T4IDOU.mjs +1703 -0
  103. package/package.json +11 -7
  104. package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -1
  105. package/templates/storefront-auto/package.json +1 -1
  106. package/templates/storefront-bakery/package.json +1 -1
  107. package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -1
  108. package/templates/storefront-fashion/package.json +1 -1
  109. package/templates/storefront-grocery/package.json +1 -1
  110. package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -1
  111. package/templates/storefront-pharmacy/package.json +1 -1
  112. package/templates/storefront-restaurant/package.json +1 -1
  113. package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -1
  114. package/templates/storefront-retail/package.json +1 -1
  115. package/templates/storefront-services/package.json +1 -1
  116. package/dist/chunk-ARIDYKLO.mjs +0 -6463
  117. package/dist/dev-7O65HY6C.mjs +0 -8
  118. package/dist/explain-DLRUUCNS.mjs +0 -251
  119. package/dist/introspect-RC265ACF.mjs +0 -9
  120. package/dist/repo-UUT3AZ3L.mjs +0 -9
@@ -0,0 +1,2946 @@
1
+ #!/usr/bin/env node
2
+ import { modelFromEnv, askAuthoring, buildCheck, trimHistory } from './chunk-74QZQSCL.mjs';
3
+ import { LocalData, DEV_DATA_ROUTE, DATA_FAMILIES } from './chunk-NEB372WD.mjs';
4
+ import { parseChannel, publishProject } from './chunk-TYR4Q37L.mjs';
5
+ import { remoteFromEnv, signingKeyDir } from './chunk-XZG3DTXV.mjs';
6
+ import { brotliTwinBytes, TICK_GUEST_PATH, TICK_GUEST_MAP_PATH, buildLocal, EXCLUDED_DIRECTORIES, outputPathOf, LAZY_GROUP_PREFIX, CODE_GROUP, TICK_GUEST_ORIGIN } from './chunk-BTNVYAC4.mjs';
7
+ import { findPackageDir, readPackageJson, exportsMap, pickTarget, BROWSER_CONDITIONS, stripDotSlash, exportTarget } from './chunk-N2WZJX5P.mjs';
8
+ import { LIBRARY_ALLOWLIST, SDK_PACKAGE } from './chunk-BGM7CH5G.mjs';
9
+ import { digestHex } from './chunk-25ZPFHLF.mjs';
10
+ import { formatDiagnostic } from './chunk-SGT5IJVW.mjs';
11
+ import { CliError, CLI_ERROR_CODE } from './chunk-I7GEHLDD.mjs';
12
+ import { promises, watch, existsSync, readdirSync, statSync, readFileSync } from 'fs';
13
+ import path from 'path';
14
+ import { HOST_PAGE_ISOLATION_HEADERS, FACE_LANDMARKER, parseContentOrigin, servesContent, serveContent, sha256Hex } from '@cimplify/sdk/interactives/web';
15
+ import { createServer } from 'http';
16
+ import { createHash } from 'crypto';
17
+ import { SharedPartStore } from '@cimplify/sdk/interactives';
18
+ import { spawn } from 'child_process';
19
+ import { createInterface } from 'readline';
20
+ import { fileURLToPath } from 'url';
21
+ import { createRequire } from 'module';
22
+
23
+ var RESULT_FILE = "result.json";
24
+ var PACKAGE_FILE = "package.json";
25
+ var CHUNK_DIR = "chunks";
26
+ var FINDING_KINDS = {
27
+ "analysis.dynamic_code": "dynamic_code",
28
+ "analysis.dynamic_import": "dynamic_code",
29
+ "analysis.dynamic_global": "dynamic_code",
30
+ "analysis.require": "dynamic_code",
31
+ "analysis.wasm_runtime": "dynamic_code",
32
+ "analysis.game_dynamic_import": "dynamic_code",
33
+ "analysis.network": "forbidden_api",
34
+ "analysis.forbidden_api": "forbidden_api",
35
+ "analysis.native_dom": "forbidden_api",
36
+ "refusal.minified": "obfuscated",
37
+ "refusal.obfuscated": "obfuscated",
38
+ "refusal.binary": "prebuilt_binary",
39
+ "refusal.wasm": "prebuilt_binary",
40
+ "resolve.not_allowlisted": "dependency_not_allowed",
41
+ "resolve.not_locked": "dependency_not_allowed",
42
+ "resolve.profile": "dependency_not_allowed",
43
+ "resolve.builtin": "dependency_not_allowed",
44
+ "resolve.url": "dependency_not_allowed",
45
+ "analysis.game_engine_import": "dependency_not_allowed",
46
+ "analysis.game_foreign_engine_import": "dependency_not_allowed",
47
+ "analysis.game_backend_import": "dependency_not_allowed",
48
+ "analysis.credential_text": "credential_collection",
49
+ "analysis.payment_text": "payment_text",
50
+ "analysis.secure_entry": "secure_entry_field",
51
+ "analysis.url": "undeclared_provider"
52
+ };
53
+ var MANIFEST_PATH = "interactive.toml";
54
+ function findingsOf(diagnostics) {
55
+ const out = [];
56
+ for (const d of diagnostics) {
57
+ if (d.severity !== "error") continue;
58
+ const kind = FINDING_KINDS[d.code];
59
+ if (kind !== void 0) out.push({ kind, path: d.file ?? MANIFEST_PATH, line: d.line ?? 0 });
60
+ }
61
+ return out;
62
+ }
63
+ function buildReport(result, frames = null) {
64
+ const gatesFailed = frames !== null && frames.gates.some((g) => g.status === "failed");
65
+ return {
66
+ ok: result.ok && !gatesFailed,
67
+ ...result.audioPreviews ? { audio_previews: { encoder: result.audioPreviews.encoder, sounds: result.audioPreviews.sounds } } : {},
68
+ gates: frames === null ? null : frames.gates,
69
+ measurements: frames === null ? null : frames.measurements,
70
+ source_digest: result.sourceDigest,
71
+ package_digest: result.packageDigest,
72
+ findings: findingsOf(result.diagnostics),
73
+ diagnostics: result.diagnostics,
74
+ stats: {
75
+ files: result.stats.files,
76
+ modules: result.stats.modules,
77
+ assets: result.stats.assets,
78
+ chunks: result.stats.chunks,
79
+ total_bytes: result.stats.totalBytes,
80
+ before_play_bytes: result.stats.beforePlayBytes,
81
+ lazy_bytes: result.stats.lazyBytes,
82
+ tiers: result.stats.tiers.map((t) => ({
83
+ tier: t.tier,
84
+ download_bytes: t.downloadBytes,
85
+ first_play_bytes: t.firstPlayBytes,
86
+ lazy_bytes: t.lazyBytes,
87
+ unique_bytes: t.uniqueBytes,
88
+ download_budget: t.downloadBudget,
89
+ first_play_budget: t.firstPlayBudget,
90
+ fits: t.fits
91
+ }))
92
+ },
93
+ chunks: result.chunks.map((c) => {
94
+ const twin = brotliTwinBytes(c);
95
+ return { digest: c.digest, kind: c.kind, bytes: c.totalBytes, ...c.payloadDigest === null ? {} : { payload_digest: c.payloadDigest }, ...twin === null ? {} : { brotli_bytes: twin } };
96
+ })
97
+ };
98
+ }
99
+ async function writeChunk(dir, chunk) {
100
+ const target = path.join(dir, digestHex(chunk.digest));
101
+ const handle = await promises.open(target, "w");
102
+ try {
103
+ await handle.writev(chunk.parts);
104
+ } finally {
105
+ await handle.close();
106
+ }
107
+ }
108
+ async function writeBuildOutput(outDir, result, frames = null) {
109
+ await promises.mkdir(outDir, { recursive: true });
110
+ if (result.ok && result.packageBytes !== null) {
111
+ const chunkDir = path.join(outDir, CHUNK_DIR);
112
+ await promises.mkdir(chunkDir, { recursive: true });
113
+ for (const chunk of result.chunks) await writeChunk(chunkDir, chunk);
114
+ await promises.writeFile(path.join(outDir, PACKAGE_FILE), result.packageBytes);
115
+ if (result.tickGuest) await writeTickGuest(outDir, result.tickGuest);
116
+ }
117
+ await promises.writeFile(path.join(outDir, RESULT_FILE), `${JSON.stringify(buildReport(result, frames), null, 2)}
118
+ `);
119
+ }
120
+ async function writeTickGuest(outDir, bundle) {
121
+ const bundlePath = path.join(outDir, ...TICK_GUEST_PATH.split("/"));
122
+ const mapPath = path.join(outDir, ...TICK_GUEST_MAP_PATH.split("/"));
123
+ await promises.mkdir(path.dirname(bundlePath), { recursive: true });
124
+ await promises.writeFile(bundlePath, bundle.code);
125
+ await promises.writeFile(mapPath, bundle.map);
126
+ return { bundle: bundlePath, map: mapPath };
127
+ }
128
+ async function writeModuleTree(dir, modules) {
129
+ await promises.rm(dir, { recursive: true, force: true });
130
+ for (const m of modules) {
131
+ const target = path.join(dir, ...m.output.split("/"));
132
+ await promises.mkdir(path.dirname(target), { recursive: true });
133
+ await promises.writeFile(target, m.code);
134
+ }
135
+ }
136
+
137
+ // src/interactives/inspect/probe.ts
138
+ var RECORD_MESSAGE = "__cimplify_record";
139
+ var EMPTY_READINESS = { first_frame: -1, first_frame_ms: -1, bytes_before_first_play: 0, bytes_total: 0, frames: 0, stall_frames: 0, stalls: [] };
140
+ function installProbe(config, scope) {
141
+ const GLOBAL = "__cimplify_probe";
142
+ const HOOK = "__cimplify_inspect";
143
+ const RECORD_KEY = "__cimplify_record";
144
+ const LANES = 1;
145
+ const WORKER_REFUSAL_TEXT = "capture and input recording refuse new Worker(...) and new SharedWorker(...): a worker's messages land on frames that depend on thread timing, so captures and replays would differ from run to run. The engine's own job threads are off in capture (navigator.hardwareConcurrency is 1); do this work on the game thread";
146
+ const BARE_STYLE = "body{display:block!important}#stage{display:block!important;padding:0!important}#phone{width:100vw!important;height:100vh!important;border:0!important;border-radius:0!important}#studio{display:none!important}#errors{opacity:0!important}";
147
+ const VIEW_WORDS = 7;
148
+ const RECORD_BYTES = 52;
149
+ const SETTLE_TURNS = 400;
150
+ const SETTLE_SLEEP_MS = 5;
151
+ const CHUNK = 32768;
152
+ const encode = (bytes) => {
153
+ let text = "";
154
+ for (let i = 0; i < bytes.length; i += CHUNK) text += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK)));
155
+ return scope.btoa(text);
156
+ };
157
+ const decode = (text) => {
158
+ const raw2 = scope.atob(text);
159
+ const out = new Uint8Array(raw2.length);
160
+ for (let i = 0; i < raw2.length; i++) out[i] = raw2.charCodeAt(i);
161
+ return out;
162
+ };
163
+ if (scope.top === void 0 || scope.top === scope) {
164
+ const document2 = scope.document;
165
+ if (config.bare && document2 !== void 0) {
166
+ const add = () => {
167
+ const style = document2.createElement("style");
168
+ style.textContent = BARE_STYLE;
169
+ (document2.head ?? document2.documentElement).appendChild(style);
170
+ };
171
+ if (document2.readyState === "loading") document2.addEventListener("DOMContentLoaded", add);
172
+ else add();
173
+ }
174
+ const host = {
175
+ guest: false,
176
+ ready: () => false,
177
+ step: async () => 0,
178
+ snapshot: () => ({ ready: false, frame: 0, clock_ms: 0, ui: false, viewport: null, patches: [], sites: [], exempt: [], scene: { requested: null, applied: null, current: null }, errors: [] }),
179
+ shadowOpen: () => false,
180
+ shadowApply: () => -1,
181
+ shadowFrame: () => null,
182
+ shadowClose: () => {
183
+ },
184
+ refused: () => null
185
+ };
186
+ scope[GLOBAL] = host;
187
+ return host;
188
+ }
189
+ let seed = config.seed >>> 0 || 1;
190
+ const random = () => {
191
+ seed = seed + 1831565813 >>> 0;
192
+ let t = seed;
193
+ t = Math.imul(t ^ t >>> 15, t | 1);
194
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
195
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
196
+ };
197
+ let clock = 0;
198
+ let frame = 0;
199
+ let manual = false;
200
+ let nextId = 1;
201
+ let inflight = 0;
202
+ const errors = [];
203
+ const queue = /* @__PURE__ */ new Map();
204
+ const realTimeout = scope.setTimeout.bind(scope);
205
+ const realRaf = typeof scope.requestAnimationFrame === "function" ? scope.requestAnimationFrame.bind(scope) : null;
206
+ scope.Math.random = random;
207
+ const crypto = scope.crypto;
208
+ if (crypto !== void 0) {
209
+ crypto.getRandomValues = (array) => {
210
+ const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
211
+ for (let i = 0; i < bytes.length; i++) bytes[i] = random() * 256 | 0;
212
+ return array;
213
+ };
214
+ crypto.randomUUID = () => {
215
+ const hex = (n) => Array.from({ length: n }, () => (random() * 16 | 0).toString(16)).join("");
216
+ return `${hex(8)}-${hex(4)}-4${hex(3)}-${(8 + random() * 4 | 0).toString(16)}${hex(3)}-${hex(12)}`;
217
+ };
218
+ }
219
+ const performance2 = scope.performance;
220
+ const realNow = performance2 === void 0 ? () => 0 : performance2.now.bind(performance2);
221
+ const loopSamples = {};
222
+ scope.__cimplify_loop_probe = {
223
+ enter: () => realNow(),
224
+ exit: (loop, at, rows) => {
225
+ let sample = loopSamples[loop];
226
+ if (sample === void 0) {
227
+ sample = [0, 0, 0];
228
+ loopSamples[loop] = sample;
229
+ }
230
+ sample[0] += realNow() - at;
231
+ sample[1] += 1;
232
+ sample[2] = rows < 0 || sample[2] < 0 ? -1 : sample[2] + rows;
233
+ },
234
+ samples: () => loopSamples
235
+ };
236
+ const READINESS_STALL_LIST = 64;
237
+ const installedAt = realNow();
238
+ const readiness = { first_frame: -1, first_frame_ms: -1, bytes_before_first_play: 0, bytes_total: 0, frames: 0, stall_frames: 0, stalls: [] };
239
+ scope.__cimplify_readiness_probe = {
240
+ chunk: (bytes) => {
241
+ readiness.bytes_total += bytes;
242
+ if (readiness.first_frame < 0) readiness.bytes_before_first_play += bytes;
243
+ },
244
+ frame: (stalled) => {
245
+ if (readiness.first_frame < 0) {
246
+ readiness.first_frame = frame;
247
+ readiness.first_frame_ms = Math.round((realNow() - installedAt) * 1e3) / 1e3;
248
+ } else if (stalled) {
249
+ readiness.stall_frames++;
250
+ if (readiness.stalls.length < READINESS_STALL_LIST) readiness.stalls.push(frame);
251
+ }
252
+ readiness.frames++;
253
+ },
254
+ report: () => ({ ...readiness, stalls: readiness.stalls.slice() })
255
+ };
256
+ if (performance2 !== void 0) performance2.now = () => clock;
257
+ scope.Date.now = () => config.epoch + clock;
258
+ const RealDate = scope.Date;
259
+ if (typeof RealDate === "function") {
260
+ scope.Date = new Proxy(RealDate, {
261
+ construct: (target, args, newTarget) => Reflect.construct(target, args.length === 0 ? [config.epoch + clock] : args, newTarget),
262
+ apply: (target) => String(Reflect.construct(target, [config.epoch + clock]))
263
+ });
264
+ }
265
+ let refusal = null;
266
+ const refuse = () => {
267
+ refusal = WORKER_REFUSAL_TEXT;
268
+ errors.push(WORKER_REFUSAL_TEXT);
269
+ throw new Error(WORKER_REFUSAL_TEXT);
270
+ };
271
+ const refusingWorker = function RefusedWorker() {
272
+ return refuse();
273
+ };
274
+ if (typeof scope.Worker === "function") scope.Worker = refusingWorker;
275
+ if (typeof scope.SharedWorker === "function") scope.SharedWorker = refusingWorker;
276
+ if (scope.navigator !== void 0) Object.defineProperty(scope.navigator, "hardwareConcurrency", { configurable: true, get: () => LANES });
277
+ if (scope.Event !== void 0) Object.defineProperty(scope.Event.prototype, "timeStamp", { configurable: true, get: () => clock });
278
+ scope.requestAnimationFrame = (callback) => {
279
+ const id = nextId++;
280
+ queue.set(id, callback);
281
+ if (!manual && realRaf !== null) {
282
+ realRaf(() => {
283
+ if (manual) return;
284
+ const due = queue.get(id);
285
+ if (due === void 0) return;
286
+ queue.delete(id);
287
+ due(clock);
288
+ });
289
+ }
290
+ return id;
291
+ };
292
+ scope.cancelAnimationFrame = (id) => {
293
+ queue.delete(id);
294
+ };
295
+ const realFetch = scope.fetch;
296
+ if (typeof realFetch === "function") {
297
+ const bound = realFetch.bind(scope);
298
+ scope.fetch = (...args) => {
299
+ inflight++;
300
+ return bound(...args).finally(() => {
301
+ inflight--;
302
+ });
303
+ };
304
+ }
305
+ let raw = null;
306
+ let memory = null;
307
+ let render2 = 0;
308
+ let maxNodes = config.max_nodes;
309
+ let active = 0;
310
+ let viewport = null;
311
+ const tracked = /* @__PURE__ */ new Map();
312
+ const sites = /* @__PURE__ */ new Map();
313
+ const exempt = [];
314
+ let sceneApplied = null;
315
+ let sceneCurrent = null;
316
+ let shadow = 0;
317
+ const bytesAt2 = (at, length) => new Uint8Array(memory.buffer, at, length).slice();
318
+ const wrapExports = (exports) => {
319
+ const out = {};
320
+ for (const name of Object.keys(exports)) out[name] = exports[name];
321
+ const wrap = (name, make) => {
322
+ const original = exports[name];
323
+ if (typeof original !== "function") return;
324
+ const replaced = make(original);
325
+ Object.defineProperty(replaced, "length", { value: original.length });
326
+ out[name] = replaced;
327
+ };
328
+ wrap("engine_ui_create", (original) => (r, max) => {
329
+ const handle = original(r, max);
330
+ if (handle !== 0) {
331
+ render2 = r;
332
+ if (max > 0) maxNodes = max;
333
+ active = handle;
334
+ tracked.set(handle, { patchAt: 0, patches: [] });
335
+ }
336
+ return handle;
337
+ });
338
+ wrap("engine_ui_destroy", (original) => (ui) => {
339
+ tracked.delete(ui);
340
+ if (active === ui) active = 0;
341
+ return original(ui);
342
+ });
343
+ wrap("engine_ui_patch_buffer", (original) => (ui, capacity) => {
344
+ const at = original(ui, capacity);
345
+ const entry = tracked.get(ui);
346
+ if (entry !== void 0) entry.patchAt = at;
347
+ return at;
348
+ });
349
+ wrap("engine_ui_apply", (original) => (ui, length) => {
350
+ const entry = tracked.get(ui);
351
+ if (entry !== void 0 && entry.patchAt !== 0 && length > 0) entry.patches.push(bytesAt2(entry.patchAt, length));
352
+ return original(ui, length);
353
+ });
354
+ wrap("engine_ui_apply_bytes", (original) => (ui, data, length) => {
355
+ const entry = tracked.get(ui);
356
+ if (entry !== void 0 && length > 0) entry.patches.push(bytesAt2(data, length));
357
+ return original(ui, data, length);
358
+ });
359
+ wrap("engine_ui_viewport", (original) => (ui, width, height, ratio, top, right, bottom, left) => {
360
+ const insets = config.insets;
361
+ const args = insets === null ? [width, height, ratio, top, right, bottom, left] : [width, height, ratio, insets.top, insets.right, insets.bottom, insets.left];
362
+ if (tracked.has(ui)) viewport = args;
363
+ return original(ui, args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
364
+ });
365
+ return out;
366
+ };
367
+ const wasm = scope.WebAssembly;
368
+ if (wasm !== void 0 && typeof wasm.instantiate === "function") {
369
+ const realInstantiate = wasm.instantiate.bind(wasm);
370
+ wasm.instantiate = async (source, imports) => {
371
+ const result = await realInstantiate(source, imports);
372
+ const direct = result instanceof wasm.Instance;
373
+ const pair = result;
374
+ const instance = direct ? result : pair.instance;
375
+ const exports = instance.exports;
376
+ const mem = imports?.env?.memory ?? exports.memory;
377
+ if (raw !== null || typeof exports.engine_ui_create !== "function" || mem === void 0) return result;
378
+ raw = exports;
379
+ memory = mem;
380
+ manual = true;
381
+ if (config.record === true) {
382
+ startRecording();
383
+ return result;
384
+ }
385
+ const fake = { exports: Object.freeze(wrapExports(exports)) };
386
+ return direct ? fake : { module: pair.module, instance: fake };
387
+ };
388
+ }
389
+ scope[HOOK] = {
390
+ site(node, where, name) {
391
+ sites.set(node, [where, name]);
392
+ },
393
+ exempt(node, check, reason) {
394
+ exempt.push([node, check, reason]);
395
+ },
396
+ scene: () => sceneApplied === null ? config.scene : null,
397
+ sceneApplied(name) {
398
+ sceneApplied = name;
399
+ },
400
+ sceneEntered(name) {
401
+ sceneCurrent = name;
402
+ }
403
+ };
404
+ const round = (value) => Math.round((value ?? 0) * 100) / 100;
405
+ const post = (input) => {
406
+ try {
407
+ scope.parent?.postMessage({ [RECORD_KEY]: input }, "*");
408
+ } catch (error) {
409
+ errors.push(String(error));
410
+ }
411
+ };
412
+ const editable = (event) => {
413
+ const target = event.target;
414
+ if (target === null || target === void 0) return false;
415
+ return target.isContentEditable === true || target.tagName === "INPUT" || target.tagName === "TEXTAREA";
416
+ };
417
+ const startRecording = () => {
418
+ post({ kind: "start", width: scope.innerWidth ?? 0, height: scope.innerHeight ?? 0, scale: scope.devicePixelRatio ?? 1, seed: config.seed });
419
+ let pressed = false;
420
+ const held = /* @__PURE__ */ new Set();
421
+ const pointer = (kind, event) => post({ kind, frame, x: round(event.clientX), y: round(event.clientY) });
422
+ const listen = scope.addEventListener;
423
+ if (typeof listen === "function") {
424
+ const on = (type, listener) => listen.call(scope, type, listener, true);
425
+ on("pointerdown", (event) => {
426
+ if (event.isPrimary === false || (event.button ?? 0) !== 0 || pressed) return;
427
+ pressed = true;
428
+ pointer("down", event);
429
+ });
430
+ on("pointermove", (event) => {
431
+ if (pressed && event.isPrimary !== false) pointer("move", event);
432
+ });
433
+ const release = (event) => {
434
+ if (!pressed || event.isPrimary === false) return;
435
+ pressed = false;
436
+ pointer("up", event);
437
+ };
438
+ on("pointerup", release);
439
+ on("pointercancel", release);
440
+ on("keydown", (event) => {
441
+ const key = event.code || event.key || "";
442
+ if (event.repeat === true || key.length === 0 || held.has(key)) return;
443
+ if (editable(event) && (event.key ?? "").length === 1 && event.ctrlKey !== true && event.metaKey !== true && event.altKey !== true) return;
444
+ held.add(key);
445
+ post({ kind: "key_down", frame, key });
446
+ });
447
+ on("keyup", (event) => {
448
+ const key = event.code || event.key || "";
449
+ if (!held.delete(key)) return;
450
+ post({ kind: "key_up", frame, key });
451
+ });
452
+ on("input", (event) => {
453
+ if (typeof event.data === "string" && event.data.length > 0 && (event.inputType ?? "insertText").startsWith("insert")) post({ kind: "text", frame, text: event.data });
454
+ });
455
+ }
456
+ const next = () => {
457
+ if (realRaf !== null) realRaf(drive);
458
+ else realTimeout(drive, config.frame_ms);
459
+ };
460
+ const drive = () => {
461
+ probe.step(1).then(next, (error) => {
462
+ if (refusal === null) errors.push(String(error));
463
+ });
464
+ };
465
+ next();
466
+ };
467
+ const sleep = (ms) => new Promise((resolve) => {
468
+ realTimeout(resolve, ms);
469
+ });
470
+ const settle = async () => {
471
+ await sleep(0);
472
+ for (let i = 0; i < SETTLE_TURNS && inflight > 0; i++) await sleep(SETTLE_SLEEP_MS);
473
+ };
474
+ const exportView = (engine, handle) => {
475
+ const at = engine.engine_ui_accessibility_export(handle);
476
+ if (at === 0 || memory === null) return null;
477
+ const view = new Uint32Array(memory.buffer, at, VIEW_WORDS);
478
+ const count = view[2];
479
+ return { full: view[0] !== 0, count, records: encode(bytesAt2(view[1], count * RECORD_BYTES)), strings: encode(bytesAt2(view[5], view[6])) };
480
+ };
481
+ const applyTo = (engine, handle, bytes) => {
482
+ const at = engine.engine_ui_patch_buffer(handle, bytes.length);
483
+ if (at === 0 || memory === null) return -1;
484
+ new Uint8Array(memory.buffer, at, bytes.length).set(bytes);
485
+ return engine.engine_ui_apply(handle, bytes.length);
486
+ };
487
+ const probe = {
488
+ guest: true,
489
+ ready: () => raw !== null,
490
+ async step(frames) {
491
+ for (let i = 0; i < frames; i++) {
492
+ await settle();
493
+ if (refusal !== null) throw new Error(refusal);
494
+ clock += config.frame_ms;
495
+ frame++;
496
+ const due = Array.from(queue.values());
497
+ queue.clear();
498
+ for (const callback of due) {
499
+ try {
500
+ callback(clock);
501
+ } catch (error) {
502
+ errors.push(String(error instanceof Error && error.stack ? error.stack : error));
503
+ }
504
+ }
505
+ }
506
+ await settle();
507
+ if (refusal !== null) throw new Error(refusal);
508
+ return frame;
509
+ },
510
+ snapshot() {
511
+ const entry = tracked.get(active);
512
+ return {
513
+ ready: raw !== null,
514
+ frame,
515
+ clock_ms: clock,
516
+ ui: entry !== void 0,
517
+ viewport,
518
+ patches: entry === void 0 ? [] : entry.patches.map(encode),
519
+ sites: Array.from(sites.entries()).map(([node, site]) => [node, site[0], site[1]]),
520
+ exempt: exempt.slice(),
521
+ scene: { requested: config.scene, applied: sceneApplied, current: sceneCurrent },
522
+ errors: errors.slice()
523
+ };
524
+ },
525
+ refused: () => refusal,
526
+ shadowOpen() {
527
+ const engine = raw;
528
+ const entry = tracked.get(active);
529
+ if (engine === null || entry === void 0 || viewport === null) return false;
530
+ if (shadow !== 0) engine.engine_ui_destroy(shadow);
531
+ shadow = engine.engine_ui_create(render2, maxNodes);
532
+ if (shadow === 0) return false;
533
+ engine.engine_ui_accessibility(shadow, 1);
534
+ engine.engine_ui_viewport(shadow, viewport[0], viewport[1], viewport[2], viewport[3], viewport[4], viewport[5], viewport[6]);
535
+ for (const patch of entry.patches) applyTo(engine, shadow, patch);
536
+ return true;
537
+ },
538
+ shadowApply(patch) {
539
+ if (raw === null || shadow === 0) return -1;
540
+ return applyTo(raw, shadow, decode(patch));
541
+ },
542
+ shadowFrame(dtMicros, full) {
543
+ const engine = raw;
544
+ if (engine === null || shadow === 0) return null;
545
+ if (full) {
546
+ engine.engine_ui_accessibility(shadow, 0);
547
+ engine.engine_ui_accessibility(shadow, 1);
548
+ }
549
+ if (typeof engine.engine_ui_view_from_camera === "function") engine.engine_ui_view_from_camera(shadow, render2);
550
+ if (engine.engine_ui_frame(shadow, dtMicros) < 0) return null;
551
+ return exportView(engine, shadow);
552
+ },
553
+ shadowClose() {
554
+ if (raw !== null && shadow !== 0) raw.engine_ui_destroy(shadow);
555
+ shadow = 0;
556
+ }
557
+ };
558
+ scope[GLOBAL] = probe;
559
+ return probe;
560
+ }
561
+ function probeScript(config) {
562
+ return `(${installProbe.toString()})(${JSON.stringify(config)}, globalThis);`;
563
+ }
564
+
565
+ // src/interactives/dev/locale.ts
566
+ var IMPERIAL_REGIONS = /* @__PURE__ */ new Set(["US", "LR", "MM"]);
567
+ var UNITED_KINGDOM = "GB";
568
+ var SUNDAY_REGIONS = /* @__PURE__ */ new Set(["US", "CA", "BR", "JP", "IL", "MX", "PH", "ZA", "KE", "IN", "SA"]);
569
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "dv", "yi"]);
570
+ var NUMBER_SAMPLE = 12345.6;
571
+ var FALLBACK_LANGUAGE = "en-US";
572
+ var FALLBACK_CURRENCY = "USD";
573
+ var FALLBACK_ZONE = "UTC";
574
+ var REGION_CURRENCIES = "AD:EUR AE:AED AF:AFN AG:XCD AI:XCD AL:ALL AM:AMD AO:AOA AR:ARS AS:USD AT:EUR AU:AUD AW:AWG AX:EUR AZ:AZN BA:BAM BB:BBD BD:BDT BE:EUR BF:XOF BG:BGN BH:BHD BI:BIF BJ:XOF BL:EUR BM:BMD BN:BND BO:BOB BQ:USD BR:BRL BS:BSD BT:BTN BW:BWP BY:BYN BZ:BZD CA:CAD CC:AUD CD:CDF CF:XAF CG:XAF CH:CHF CI:XOF CK:NZD CL:CLP CM:XAF CN:CNY CO:COP CR:CRC CU:CUP CV:CVE CW:ANG CX:AUD CY:EUR CZ:CZK DE:EUR DJ:DJF DK:DKK DM:XCD DO:DOP DZ:DZD EC:USD EE:EUR EG:EGP EH:MAD ER:ERN ES:EUR ET:ETB FI:EUR FJ:FJD FK:FKP FM:USD FO:DKK FR:EUR GA:XAF GB:GBP GD:XCD GE:GEL GF:EUR GG:GBP GH:GHS GI:GIP GL:DKK GM:GMD GN:GNF GP:EUR GQ:XAF GR:EUR GT:GTQ GU:USD GW:XOF GY:GYD HK:HKD HN:HNL HR:EUR HT:HTG HU:HUF ID:IDR IE:EUR IL:ILS IM:GBP IN:INR IO:USD IQ:IQD IR:IRR IS:ISK IT:EUR JE:GBP JM:JMD JO:JOD JP:JPY KE:KES KG:KGS KH:KHR KI:AUD KM:KMF KN:XCD KP:KPW KR:KRW KW:KWD KY:KYD KZ:KZT LA:LAK LB:LBP LC:XCD LI:CHF LK:LKR LR:LRD LS:LSL LT:EUR LU:EUR LV:EUR LY:LYD MA:MAD MC:EUR MD:MDL ME:EUR MF:EUR MG:MGA MH:USD MK:MKD ML:XOF MM:MMK MN:MNT MO:MOP MP:USD MQ:EUR MR:MRU MS:XCD MT:EUR MU:MUR MV:MVR MW:MWK MX:MXN MY:MYR MZ:MZN NA:NAD NC:XPF NE:XOF NF:AUD NG:NGN NI:NIO NL:EUR NO:NOK NP:NPR NR:AUD NU:NZD NZ:NZD OM:OMR PA:PAB PE:PEN PF:XPF PG:PGK PH:PHP PK:PKR PL:PLN PM:EUR PN:NZD PR:USD PS:ILS PT:EUR PW:USD PY:PYG QA:QAR RE:EUR RO:RON RS:RSD RU:RUB RW:RWF SA:SAR SB:SBD SC:SCR SD:SDG SE:SEK SG:SGD SH:SHP SI:EUR SK:EUR SL:SLE SM:EUR SN:XOF SO:SOS SR:SRD SS:SSP ST:STN SV:USD SX:ANG SY:SYP SZ:SZL TC:USD TD:XAF TG:XOF TH:THB TJ:TJS TK:NZD TL:USD TM:TMT TN:TND TO:TOP TR:TRY TT:TTD TV:AUD TW:TWD TZ:TZS UA:UAH UG:UGX US:USD UY:UYU UZ:UZS VA:EUR VC:XCD VE:VES VG:USD VI:USD VN:VND VU:VUV WF:XPF WS:WST XK:EUR YE:YER YT:EUR ZA:ZAR ZM:ZMW ZW:ZWG";
575
+ var CURRENCY_OF = new Map(
576
+ REGION_CURRENCIES.split(" ").map((pair) => pair.split(":"))
577
+ );
578
+ function part(parts, type, fallback) {
579
+ return parts.find((entry) => entry.type === type)?.value ?? fallback;
580
+ }
581
+ function currencyOfRegion(region) {
582
+ return (region === null ? void 0 : CURRENCY_OF.get(region)) ?? FALLBACK_CURRENCY;
583
+ }
584
+ function language(value) {
585
+ try {
586
+ return new Intl.Locale(value);
587
+ } catch {
588
+ throw new CliError(
589
+ CLI_ERROR_CODE.INVALID_INPUT,
590
+ `--locale takes a language tag such as en-GB or fr-SN, got '${value}'`
591
+ );
592
+ }
593
+ }
594
+ function checkedZone(value) {
595
+ try {
596
+ new Intl.DateTimeFormat("en", { timeZone: value });
597
+ return value;
598
+ } catch {
599
+ throw new CliError(
600
+ CLI_ERROR_CODE.INVALID_INPUT,
601
+ `--time-zone takes a zone such as Europe/London, got '${value}'`
602
+ );
603
+ }
604
+ }
605
+ function checkedCurrency(value) {
606
+ const code = value.toUpperCase();
607
+ if (!Intl.supportedValuesOf("currency").includes(code)) {
608
+ throw new CliError(
609
+ CLI_ERROR_CODE.INVALID_INPUT,
610
+ `--currency takes an ISO currency code such as GBP, got '${value}'`
611
+ );
612
+ }
613
+ return code;
614
+ }
615
+ function devLocale(choice = {}) {
616
+ const system = new Intl.DateTimeFormat().resolvedOptions();
617
+ const locale = language(choice.language ?? (system.locale || FALLBACK_LANGUAGE));
618
+ const region = locale.region ?? locale.maximize().region ?? null;
619
+ const parts = new Intl.NumberFormat(locale.baseName).formatToParts(NUMBER_SAMPLE);
620
+ const hourCycle = new Intl.DateTimeFormat(locale.baseName, { hour: "numeric" }).resolvedOptions().hourCycle;
621
+ return {
622
+ currency: choice.currency === void 0 ? currencyOfRegion(region) : checkedCurrency(choice.currency),
623
+ decimal_separator: part(parts, "decimal", "."),
624
+ direction: RTL_LANGUAGES.has(locale.language) ? "rtl" : "ltr",
625
+ first_day_of_week: region !== null && SUNDAY_REGIONS.has(region) ? "sunday" : "monday",
626
+ grouping_separator: part(parts, "group", ","),
627
+ hour_cycle: hourCycle === "h11" || hourCycle === "h12" ? "h12" : "h23",
628
+ language: locale.baseName,
629
+ measurement: region === UNITED_KINGDOM ? "united_kingdom" : region !== null && IMPERIAL_REGIONS.has(region) ? "imperial" : "metric",
630
+ region,
631
+ time_zone: choice.timeZone === void 0 ? system.timeZone || FALLBACK_ZONE : checkedZone(choice.timeZone)
632
+ };
633
+ }
634
+
635
+ // src/interactives/dev/routes.ts
636
+ var CHUNK_ROUTE = "/_chunks/";
637
+ var NEXT_ROUTE = "/_next/";
638
+ var TICK_ROUTE = "/_tick";
639
+ var FEED_PACKAGES_ROUTE = "/v1/interactives/packages";
640
+ var TICK_EVENT = "tick";
641
+ var WEB_HOST_MODULE = "dist/interactives-web.mjs";
642
+ var GUEST_WORKER_MODULE = "dist/interactives-guest.mjs";
643
+ var RUNTIME_MODULE = "dist/interactives-runtime.mjs";
644
+ var CANVAS_GUEST_MODULE = "dist/interactives-canvas.global.js";
645
+
646
+ // src/interactives/dev/content.ts
647
+ var LOCAL_ANCESTORS = ["http://localhost:*", "http://127.0.0.1:*"];
648
+ var HOST_HEADER = "host";
649
+ async function contentEdge(origin, packagesRoot, chunks, runtimePrefix = "") {
650
+ const parsed = parseContentOrigin(origin);
651
+ if (parsed === null) throw new Error(`${origin} is not a content origin: use a scheme and a registrable host such as http://cimplifyusercontent.localhost:8790`);
652
+ const sdk = findPackageDir(packagesRoot, SDK_PACKAGE);
653
+ const runtimeFile = sdk === null ? null : path.join(sdk, CANVAS_GUEST_MODULE);
654
+ const store = {
655
+ chunk: (digest2) => Promise.resolve(chunks().get(digest2) ?? null),
656
+ async runtime(digest2) {
657
+ if (runtimeFile === null) return null;
658
+ const file = await promises.readFile(runtimeFile);
659
+ const bytes = runtimePrefix.length === 0 ? new Uint8Array(file) : new Uint8Array(Buffer.concat([Buffer.from(runtimePrefix, "utf8"), file]));
660
+ return sha256Hex(bytes) === digest2 ? bytes : null;
661
+ }
662
+ };
663
+ return {
664
+ async serve(req, res) {
665
+ const host = req.headers[HOST_HEADER] ?? "";
666
+ if (!servesContent(parsed, host)) return false;
667
+ const headers = new Headers();
668
+ for (const [name, value] of Object.entries(req.headers)) if (typeof value === "string") headers.set(name, value);
669
+ const request = new Request(`${parsed.scheme}//${host}${req.url ?? "/"}`, { method: req.method ?? "GET", headers });
670
+ const response = await serveContent(request, store, { frame_ancestors: LOCAL_ANCESTORS });
671
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
672
+ res.end(Buffer.from(await response.arrayBuffer()));
673
+ return true;
674
+ }
675
+ };
676
+ }
677
+ var FACE_PARTS_DIR = [".cimplify", "dev", "face-landmarker"];
678
+ var SHA256 = "sha256";
679
+ var HEX = "hex";
680
+ function digestOf(bytes) {
681
+ return createHash(SHA256).update(bytes).digest(HEX);
682
+ }
683
+ var FacePartCache = class {
684
+ constructor(dir, fetcher = fetch, parts = Object.values(FACE_LANDMARKER.parts)) {
685
+ this.dir = dir;
686
+ this.fetcher = fetcher;
687
+ this.parts = new Map(parts.map((part2) => [part2.sha256, part2]));
688
+ }
689
+ dir;
690
+ fetcher;
691
+ loading = /* @__PURE__ */ new Map();
692
+ parts;
693
+ pinned(digest2) {
694
+ return this.parts.has(digest2);
695
+ }
696
+ fetch(digest2) {
697
+ if (!this.parts.has(digest2)) return Promise.resolve(null);
698
+ let pending = this.loading.get(digest2);
699
+ if (pending === void 0) {
700
+ pending = this.load(digest2).finally(() => this.loading.delete(digest2));
701
+ this.loading.set(digest2, pending);
702
+ }
703
+ return pending;
704
+ }
705
+ async load(digest2) {
706
+ const file = path.join(this.dir, digest2);
707
+ const cached = await promises.readFile(file).catch(() => null);
708
+ if (cached !== null && digestOf(cached) === digest2) return cached;
709
+ const part2 = this.parts.get(digest2);
710
+ if (part2 === void 0) return null;
711
+ const response = await this.fetcher(part2.upstream);
712
+ if (!response.ok) throw new Error(`${part2.upstream} answered ${response.status}`);
713
+ const body = Buffer.from(await response.arrayBuffer());
714
+ if (digestOf(body) !== digest2) throw new Error(`${part2.upstream} does not match its pinned digest ${digest2}`);
715
+ await promises.mkdir(this.dir, { recursive: true });
716
+ const temp = `${file}.${process.pid}.tmp`;
717
+ await promises.writeFile(temp, body);
718
+ await promises.rename(temp, file);
719
+ return body;
720
+ }
721
+ };
722
+ var NPM_ROUTE = "/_npm/";
723
+ function createNpmResolver(root) {
724
+ const importMap = {};
725
+ const packages = /* @__PURE__ */ new Map();
726
+ for (const library of LIBRARY_ALLOWLIST) {
727
+ const dir = findPackageDir(root, library.name);
728
+ if (dir === null) continue;
729
+ packages.set(library.name, dir);
730
+ const pkg = readPackageJson(dir);
731
+ const map = exportsMap(pkg);
732
+ for (const entry of library.entries) {
733
+ const subpath = `.${entry.specifier.slice(library.name.length)}`;
734
+ if (entry.prefix) {
735
+ const target2 = pickTarget(map?.[`${subpath}*`], BROWSER_CONDITIONS);
736
+ if (target2 !== null && target2.endsWith("*")) {
737
+ importMap[entry.specifier] = `${NPM_ROUTE}${library.name}/${stripDotSlash(target2.slice(0, -1))}`;
738
+ }
739
+ continue;
740
+ }
741
+ const target = exportTarget(pkg, subpath, BROWSER_CONDITIONS);
742
+ if (target !== null) importMap[entry.specifier] = `${NPM_ROUTE}${library.name}/${stripDotSlash(target)}`;
743
+ }
744
+ }
745
+ return {
746
+ importMap,
747
+ file(urlPath) {
748
+ if (!urlPath.startsWith(NPM_ROUTE)) return null;
749
+ const rest = decodeURIComponent(urlPath.slice(NPM_ROUTE.length));
750
+ for (const [name, dir] of packages) {
751
+ if (!rest.startsWith(`${name}/`)) continue;
752
+ const resolved = path.resolve(dir, rest.slice(name.length + 1));
753
+ return resolved.startsWith(dir + path.sep) ? resolved : null;
754
+ }
755
+ return null;
756
+ }
757
+ };
758
+ }
759
+ var DEV_AI_ROUTE = "/_ctx/ai";
760
+ var DEV_AI_KEY_ENV = "ANTHROPIC_API_KEY";
761
+ var DEV_AI_LOOPBACK = "[dev loopback] ";
762
+ var ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
763
+ var ANTHROPIC_VERSION = "2023-06-01";
764
+ var QUALITY_MODELS = {
765
+ fast: "claude-haiku-4-5-20251001",
766
+ standard: "claude-sonnet-5-5",
767
+ best: "claude-opus-5-5"
768
+ };
769
+ var DEFAULT_QUALITY = "standard";
770
+ var MAX_TOKENS = 1024;
771
+ var RUN_PREFIX = "airun_dev_";
772
+ var ROUTE = "server";
773
+ var SYSTEM = "You are the in-game model for a Cimplify Interactive, running in the creator's local simulator. Follow the declared prompt exactly and answer with the requested output only.";
774
+ var STRUCTURED_SYSTEM = " Reply with one JSON value that matches this JSON schema, and nothing else: ";
775
+ var ACTIONS_SYSTEM = ' Reply with one JSON object {"line": string, "actions": [{"action": name, "args": object}]} choosing only from these actions: ';
776
+ function asRecord(value) {
777
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
778
+ }
779
+ function asList(value) {
780
+ return Array.isArray(value) ? value : [];
781
+ }
782
+ function named(list, key, name) {
783
+ for (const item of list) {
784
+ const record = asRecord(item);
785
+ if (record !== null && record[key] === name) return record;
786
+ }
787
+ return null;
788
+ }
789
+ function invalid(field, problem) {
790
+ return { error: { code: "invalid_arguments", field, problem } };
791
+ }
792
+ function render(template, slots, playerText) {
793
+ let out = template;
794
+ for (const slot of slots) out = out.split(`{{${slot.name}}}`).join(String(slot.value));
795
+ if (playerText !== null && playerText.length > 0) out += `
796
+
797
+ The player says: ${playerText}`;
798
+ return out;
799
+ }
800
+ function digest(value) {
801
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
802
+ }
803
+ function placeholder(schema, depth) {
804
+ const record = asRecord(schema);
805
+ if (record === null || depth > 4) return null;
806
+ switch (record.type) {
807
+ case "object": {
808
+ const out = {};
809
+ const properties = asRecord(record.properties) ?? {};
810
+ for (const key of asList(record.required)) if (typeof key === "string") out[key] = placeholder(properties[key], depth + 1);
811
+ return out;
812
+ }
813
+ case "array":
814
+ return [];
815
+ case "integer":
816
+ case "number":
817
+ return typeof record.minimum === "number" ? record.minimum : 0;
818
+ case "boolean":
819
+ return false;
820
+ case "string":
821
+ return `${DEV_AI_LOOPBACK.trim()} text`;
822
+ default:
823
+ return null;
824
+ }
825
+ }
826
+ function parseJson(text) {
827
+ const start = text.search(/[[{]/);
828
+ if (start < 0) return null;
829
+ try {
830
+ return JSON.parse(text.slice(start));
831
+ } catch {
832
+ return null;
833
+ }
834
+ }
835
+ var DevAi = class {
836
+ constructor(declarations, model) {
837
+ this.declarations = declarations;
838
+ this.model = model;
839
+ }
840
+ declarations;
841
+ model;
842
+ runs = 0;
843
+ get live() {
844
+ return this.model !== null;
845
+ }
846
+ async answer(call) {
847
+ const ai = this.declarations();
848
+ if (ai === null) return { error: { code: "denied", reason: "policy" } };
849
+ const args = call.arguments;
850
+ const slots = Array.isArray(args.slots) ? args.slots : [];
851
+ const playerText = typeof args.player_text === "string" ? args.player_text : null;
852
+ const output = asRecord(args.output) ?? { kind: "text", max_units: MAX_TOKENS };
853
+ if (call.operation === "ai.generate") {
854
+ const prompt = named(asList(ai.prompts), "name", String(args.prompt));
855
+ if (prompt === null) return invalid("prompt", "not_declared");
856
+ const quality = typeof prompt.quality === "string" ? prompt.quality : DEFAULT_QUALITY;
857
+ return this.run(ai, output, quality, "", render(String(prompt.template ?? ""), slots, playerText));
858
+ }
859
+ if (call.operation === "ai.converse") {
860
+ const character = named(asList(ai.characters), "name", String(args.character));
861
+ if (character === null) return invalid("character", "not_declared");
862
+ const quality = typeof character.quality === "string" ? character.quality : DEFAULT_QUALITY;
863
+ const persona = typeof character.persona === "string" ? character.persona : "";
864
+ const context = slots.map((slot) => `${slot.name}: ${slot.value}`).join("\n");
865
+ return this.run(ai, output, quality, persona, [context, playerText ?? ""].filter((part2) => part2.length > 0).join("\n\n"));
866
+ }
867
+ if (call.operation === "ai.usage") return { result: { runs: this.runs, units: 0 } };
868
+ if (call.operation === "ai.content") return this.content(ai, args, slots);
869
+ if (call.operation === "ai.content_prefetch") {
870
+ const requests = Array.isArray(args.requests) ? args.requests.length : 0;
871
+ return { result: { accepted: 0, already_cached: 0, refused: requests } };
872
+ }
873
+ return { error: { code: "unavailable", family: "ai", retry_after_ms: null } };
874
+ }
875
+ async content(ai, args, slots) {
876
+ const prompt = named(asList(ai.prompts), "name", String(args.prompt));
877
+ if (prompt === null) return invalid("prompt", "not_declared");
878
+ const kind = String(args.kind);
879
+ const declared = asRecord(prompt.output);
880
+ const schema = declared?.kind === "structured" ? named(asList(ai.schemas), "name", String(declared.schema))?.schema : args.schema;
881
+ const seed = typeof args.seed === "number" ? args.seed : 0;
882
+ this.runs++;
883
+ const runId = `${RUN_PREFIX}${this.runs}`;
884
+ const base = { run_id: runId, kind, seed, attempts: 1, units: 0, display_token: null };
885
+ if (this.model === null) return { result: { ...base, outcome: "fallback", fallback: "unavailable", value: null, output_digest: null } };
886
+ const quality = typeof prompt.quality === "string" ? prompt.quality : DEFAULT_QUALITY;
887
+ const context = args.context === void 0 || args.context === null ? "" : `
888
+
889
+ Game state: ${JSON.stringify(args.context)}`;
890
+ const system = schema === void 0 || schema === null ? SYSTEM : SYSTEM + STRUCTURED_SYSTEM + JSON.stringify(schema);
891
+ let text;
892
+ try {
893
+ text = await this.model.complete({ model: QUALITY_MODELS[quality] ?? QUALITY_MODELS[DEFAULT_QUALITY], system, prompt: render(String(prompt.template ?? ""), slots, null) + context, maxTokens: MAX_TOKENS });
894
+ } catch {
895
+ return { result: { ...base, outcome: "fallback", fallback: "unavailable", value: null, output_digest: null } };
896
+ }
897
+ const value = kind === "text" ? text.trim() : parseJson(text);
898
+ if (value === null) return { result: { ...base, outcome: "fallback", fallback: "invalid", value: null, output_digest: null } };
899
+ return { result: { ...base, outcome: "generated", fallback: null, value, output_digest: digest(value) } };
900
+ }
901
+ async run(ai, output, quality, persona, prompt) {
902
+ const kind = output.kind;
903
+ let system = persona.length > 0 ? `${SYSTEM}
904
+
905
+ ${persona}` : SYSTEM;
906
+ let schema;
907
+ let actions = [];
908
+ if (kind === "structured") {
909
+ schema = named(asList(ai.schemas), "name", String(output.schema))?.schema;
910
+ if (asRecord(schema) === null) return invalid("output.schema", "not_declared");
911
+ system += STRUCTURED_SYSTEM + JSON.stringify(schema);
912
+ } else if (kind === "actions") {
913
+ const set = named(asList(ai.actions), "set", String(output.set));
914
+ if (set === null) return invalid("output.set", "not_declared");
915
+ actions = asList(set.actions);
916
+ system += ACTIONS_SYSTEM + JSON.stringify(actions);
917
+ }
918
+ const maxTokens = kind === "text" && typeof output.max_units === "number" ? Math.max(1, Math.min(MAX_TOKENS, output.max_units)) : MAX_TOKENS;
919
+ let text;
920
+ if (this.model === null) text = `${DEV_AI_LOOPBACK}${prompt}`;
921
+ else {
922
+ try {
923
+ text = await this.model.complete({ model: QUALITY_MODELS[quality] ?? QUALITY_MODELS[DEFAULT_QUALITY], system, prompt, maxTokens });
924
+ } catch {
925
+ return { error: { code: "unavailable", family: "ai", retry_after_ms: null } };
926
+ }
927
+ }
928
+ const value = this.output(kind, text, schema, actions);
929
+ this.runs++;
930
+ const runId = `${RUN_PREFIX}${this.runs}`;
931
+ return {
932
+ result: {
933
+ run_id: runId,
934
+ status: "completed",
935
+ route: ROUTE,
936
+ result: { run_id: runId, route: ROUTE, units: 0, output_digest: digest(value), verdict: "approved", output: value }
937
+ }
938
+ };
939
+ }
940
+ output(kind, text, schema, actions) {
941
+ if (kind === "structured") {
942
+ const parsed = this.model === null ? null : parseJson(text);
943
+ return { kind: "structured", value: parsed ?? placeholder(schema, 0) };
944
+ }
945
+ if (kind === "actions") {
946
+ const parsed = asRecord(this.model === null ? null : parseJson(text));
947
+ if (parsed !== null && Array.isArray(parsed.actions)) return { kind: "actions", actions: parsed.actions, line: typeof parsed.line === "string" ? parsed.line : null };
948
+ const first = asRecord(actions[0]);
949
+ return { kind: "actions", actions: first === null ? [] : [{ action: String(first.name), args: {} }], line: text };
950
+ }
951
+ return { kind: "text", text };
952
+ }
953
+ };
954
+ function anthropicDevModel(apiKey, fetcher = fetch) {
955
+ return {
956
+ async complete({ model, system, prompt, maxTokens }) {
957
+ const response = await fetcher(ANTHROPIC_URL, {
958
+ method: "POST",
959
+ headers: { "content-type": "application/json", "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION },
960
+ body: JSON.stringify({ model, max_tokens: maxTokens, system, messages: [{ role: "user", content: prompt }] })
961
+ });
962
+ if (!response.ok) throw new Error(`the model answered ${response.status}`);
963
+ const body = await response.json();
964
+ return (body.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
965
+ }
966
+ };
967
+ }
968
+ function devAiFromEnv(declarations, env = process.env) {
969
+ const key = env[DEV_AI_KEY_ENV];
970
+ return new DevAi(declarations, key !== void 0 && key.length > 0 ? anthropicDevModel(key) : null);
971
+ }
972
+
973
+ // src/interactives/dev/page.ts
974
+ var STYLE = `
975
+ :root { color-scheme: light dark; --bg: #f4f4f5; --fg: #18181b; --card: #ffffff; --muted: #71717a; --line: #e4e4e7; --accent: #f2b33d; --accent-fg: #9a6209; --on-accent: #1a1205; }
976
+ @media (prefers-color-scheme: dark) { :root { --bg: #09090b; --fg: #fafafa; --card: #18181b; --muted: #a1a1aa; --line: #27272a; --accent-fg: #f2b33d; } }
977
+ * { box-sizing: border-box; }
978
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg); font: 15px/1.4 system-ui, sans-serif; }
979
+ #phone { position: relative; width: min(390px, 100vw); height: min(780px, 100vh); background: var(--card); border: 1px solid var(--line); border-radius: 28px; overflow: hidden; }
980
+ #screen { position: absolute; inset: 0; overflow: auto; padding: 16px; }
981
+ #screen canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
982
+ #sheet { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.4); display: flex; align-items: flex-end; }
983
+ #sheet[hidden] { display: none; }
984
+ #sheet > div { width: 100%; background: var(--card); border-radius: 20px 20px 0 0; padding: 16px; max-height: 90%; overflow: auto; }
985
+ #toast { position: absolute; left: 16px; right: 16px; bottom: 16px; padding: 10px 14px; border-radius: 12px; background: var(--fg); color: var(--card); opacity: 0; transition: opacity 0.2s; pointer-events: none; }
986
+ #toast.on { opacity: 1; }
987
+ #errors { position: fixed; inset: auto 0 0 0; margin: 0; padding: 12px 16px; max-height: 40vh; overflow: auto; background: #7f1d1d; color: #fff; font: 12px/1.5 ui-monospace, monospace; white-space: pre-wrap; }
988
+ #errors:empty { display: none; }
989
+ .stack { display: flex; flex-direction: column; gap: 12px; }
990
+ .stack.row { flex-direction: row; flex-wrap: wrap; }
991
+ .card { border: 1px solid var(--line); border-radius: 16px; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
992
+ .card > h3, .sheet-title { margin: 0; font-size: 17px; }
993
+ .t-display { font-size: 32px; font-weight: 700; } .t-heading { font-size: 24px; font-weight: 700; } .t-title { font-size: 19px; font-weight: 600; }
994
+ .t-label { font-size: 13px; font-weight: 600; } .t-caption { font-size: 12px; color: var(--muted); }
995
+ .kv { display: flex; justify-content: space-between; gap: 12px; } .kv span:first-child { color: var(--muted); }
996
+ .stat b { display: block; font-size: 22px; } .stat span { color: var(--muted); font-size: 12px; }
997
+ button { font: inherit; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--line); background: var(--card); color: var(--fg); cursor: pointer; }
998
+ button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
999
+ button.on { border-color: var(--accent); color: var(--accent-fg); }
1000
+ .choice { display: flex; gap: 8px; flex-wrap: wrap; }
1001
+ .stepper { display: flex; align-items: center; justify-content: space-between; }
1002
+ .stepper div { display: flex; align-items: center; gap: 10px; }
1003
+ .progress { height: 6px; border-radius: 3px; background: var(--line); overflow: hidden; } .progress i { display: block; height: 100%; background: var(--accent); }
1004
+ .unknown { border: 1px dashed var(--muted); border-radius: 10px; padding: 8px; font: 12px ui-monospace, monospace; color: var(--muted); }
1005
+ hr { border: 0; border-top: 1px solid var(--line); margin: 0; width: 100%; }
1006
+ input, select { font: inherit; padding: 10px; border-radius: 10px; border: 1px solid var(--line); background: var(--card); color: var(--fg); width: 100%; }
1007
+ label.field { display: flex; flex-direction: column; gap: 6px; }
1008
+ .sheet-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
1009
+ #stage { display: flex; gap: 20px; align-items: flex-start; justify-content: center; flex-wrap: wrap; padding: 16px; box-sizing: border-box; width: 100%; }
1010
+ #studio { width: min(360px, calc(100vw - 32px)); display: flex; flex-direction: column; gap: 12px; }
1011
+ #studio section { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
1012
+ #studio h2 { margin: 0; font-size: 15px; }
1013
+ #tick-log { max-height: 240px; overflow: auto; display: flex; flex-direction: column; gap: 4px; }
1014
+ #studio section[hidden] { display: none; }
1015
+ #ai-log { max-height: 360px; overflow: auto; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
1016
+ #ai-log:empty::before { content: "Describe a change, for example: add traffic on the loop, make the bus red, or add a horn on Space."; color: var(--muted); }
1017
+ .ai-me { align-self: flex-end; background: var(--accent); color: var(--on-accent); padding: 6px 10px; border-radius: 12px; max-width: 85%; white-space: pre-wrap; }
1018
+ .ai-say { white-space: pre-wrap; }
1019
+ .ai-step, .ai-file { color: var(--muted); font: 12px/1.4 ui-monospace, monospace; overflow-wrap: anywhere; }
1020
+ .ai-error { color: #dc2626; font: 12px/1.4 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
1021
+ #ai-input { min-height: 72px; resize: vertical; font: inherit; padding: 10px; border-radius: 10px; border: 1px solid var(--line); background: var(--card); color: var(--fg); width: 100%; }
1022
+ .studio-row { display: flex; gap: 8px; flex-wrap: wrap; }
1023
+ #publish-result { font-size: 13px; display: flex; flex-direction: column; gap: 4px; }
1024
+ `;
1025
+ var SCRIPT = `
1026
+ import { createSimulatorCtx } from "@cimplify/sdk/interactives";
1027
+ const screen = document.getElementById("screen");
1028
+ const sheet = document.getElementById("sheet");
1029
+ const toastEl = document.getElementById("toast");
1030
+ const errors = document.getElementById("errors");
1031
+ const events = new EventSource("/_events");
1032
+ events.addEventListener("reload", () => { void swapOrReload(); });
1033
+ async function swapOrReload() {
1034
+ const swap = globalThis.cimplifyHotSwap;
1035
+ const result = typeof swap === "function" ? await swap().catch(() => ({ swapped: false })) : { swapped: false };
1036
+ if (!result.swapped) { location.reload(); return; }
1037
+ errors.textContent = "";
1038
+ }
1039
+ events.addEventListener("failed", (e) => { errors.textContent = JSON.parse(e.data).join("\\n"); });
1040
+ const tickPanel = document.getElementById("tick");
1041
+ const tickStatus = document.getElementById("tick-status");
1042
+ const tickBudget = document.getElementById("tick-budget");
1043
+ const tickDiff = document.getElementById("tick-diff");
1044
+ const tickLog = document.getElementById("tick-log");
1045
+ const MIB = 1048576;
1046
+ function tickNote(cls, text) {
1047
+ const d = document.createElement("div");
1048
+ d.className = cls;
1049
+ d.textContent = text;
1050
+ tickLog.append(d);
1051
+ while (tickLog.childElementCount > 200) tickLog.firstElementChild.remove();
1052
+ tickLog.scrollTop = tickLog.scrollHeight;
1053
+ }
1054
+ function bodyText(b) { return "seat " + b.seat + (b.alive ? " at " + b.x + ", " + b.y + ", " + b.z + " yaw " + b.yaw : " empty"); }
1055
+ events.addEventListener(TICK_EVENT, (e) => {
1056
+ const t = JSON.parse(e.data);
1057
+ tickPanel.hidden = false;
1058
+ if (t.type === "ready") tickStatus.textContent = "Running " + t.seats + " seats at " + t.rate_hz + " Hz with " + t.state_bytes + " state bytes";
1059
+ else if (t.type === "budget") tickBudget.textContent = "tick " + t.tick + ": " + t.percent.toFixed(1) + "% of the " + t.deadline_ns / 1e6 + " ms budget, heap " + (t.heap_used_bytes / MIB).toFixed(1) + " of " + (t.heap_limit_bytes / MIB).toFixed(0) + " MiB";
1060
+ else if (t.type === "diff") tickDiff.textContent = "tick " + t.tick + ": " + t.bodies.map(bodyText).join("; ") + (t.state.length > 0 ? " | state " + t.state.map((r) => "@" + r.offset + " " + r.bytes).join(" ") : "");
1061
+ else if (t.type === "log") tickNote("ai-step", "[" + t.tick + "] " + t.message);
1062
+ else if (t.type === "error") tickNote("ai-error", t.kind.replaceAll("_", " ") + " at tick " + t.tick + ": " + t.message + (t.source ? "\\n at " + t.source : ""));
1063
+ else if (t.type === "unavailable") tickStatus.textContent = t.message;
1064
+ else if (t.type === "exit") tickStatus.textContent = t.code ? "The tick room stopped with code " + t.code : "The tick room stopped";
1065
+ });
1066
+ function tickSend(body) { fetch(TICK_ROUTE, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }).catch(() => {}); }
1067
+ document.getElementById("tick-join").addEventListener("click", () => tickSend({ type: "join", seat: 0 }));
1068
+ document.getElementById("tick-leave").addEventListener("click", () => tickSend({ type: "leave", seat: 0 }));
1069
+ const aiLog = document.getElementById("ai-log");
1070
+ const aiInput = document.getElementById("ai-input");
1071
+ const aiAsk = document.getElementById("ai-ask");
1072
+ const publishResult = document.getElementById("publish-result");
1073
+ const publishButtons = [document.getElementById("publish-test"), document.getElementById("publish-live")];
1074
+ function note(cls, text) { const d = el("div", cls, text); aiLog.append(d); aiLog.scrollTop = aiLog.scrollHeight; return d; }
1075
+ function diagnosticLines(list) { return list.map((d) => (d.file ? d.file + (d.line ? ":" + d.line : "") + ": " : "") + d.message).join("\\n"); }
1076
+ function showTurn(turn) {
1077
+ note("ai-me", turn.request);
1078
+ for (const c of turn.changed) note("ai-file", (c.kind === "delete" ? "deleted " : "changed ") + c.path);
1079
+ if (turn.summary) note("ai-say", turn.summary);
1080
+ if (turn.error) note("ai-error", turn.error);
1081
+ if (turn.end === "refused") note("ai-error", "The model declined this request.");
1082
+ if (turn.end === "steps" || turn.end === "truncated") note("ai-error", "The AI stopped before finishing; ask it to continue.");
1083
+ if (turn.diagnostics.length > 0) note("ai-error", diagnosticLines(turn.diagnostics));
1084
+ }
1085
+ function describeProgress(e) {
1086
+ if (e.kind === "check") return e.errors === 0 ? "build passed" : "build found " + e.errors + " errors, repairing";
1087
+ if (e.kind !== "tool") return null;
1088
+ if (e.name === "write_file") return "editing " + e.path;
1089
+ if (e.name === "delete_file") return "deleting " + e.path;
1090
+ if (e.name === "read_file") return "reading " + e.path;
1091
+ if (e.name === "read_guide") return "reading the Interactives guide";
1092
+ if (e.name === "check") return "building";
1093
+ return null;
1094
+ }
1095
+ function setBusy(busy) { aiAsk.disabled = busy; aiAsk.textContent = busy ? "Working..." : "Ask"; }
1096
+ function showPublish(p) {
1097
+ publishResult.replaceChildren();
1098
+ if (!p.ok) publishResult.append(el("div", "ai-error", (p.error || "publish failed") + (p.remediation ? "\\n" + p.remediation : "")));
1099
+ else publishResult.append(el("div", "", (p.state === "in_review" ? "Revision " + p.revision + " passed admission and waits for review before release to " + p.channel : "Published " + p.revision + " to " + p.channel) + " (" + p.uploaded + " uploaded, " + p.skipped + " unchanged)"));
1100
+ for (const log of p.logs) publishResult.append(el("div", log.level === "error" ? "ai-error" : "ai-step", log.message));
1101
+ }
1102
+ async function ask() {
1103
+ const text = aiInput.value.trim();
1104
+ if (!text || aiAsk.disabled) return;
1105
+ setBusy(true);
1106
+ const mine = note("ai-me", text);
1107
+ aiInput.value = "";
1108
+ const status = note("ai-step", "thinking");
1109
+ try {
1110
+ const res = await fetch("/_ai", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ request: text }) });
1111
+ if (!res.ok || !res.body) {
1112
+ const body = await res.json().catch(() => ({ error: res.statusText, remediation: null }));
1113
+ status.remove();
1114
+ note("ai-error", body.error + (body.remediation ? "\\n" + body.remediation : ""));
1115
+ return;
1116
+ }
1117
+ const reader = res.body.getReader();
1118
+ const decoder = new TextDecoder();
1119
+ let buffer = "";
1120
+ for (;;) {
1121
+ const { value, done } = await reader.read();
1122
+ if (done) break;
1123
+ buffer += decoder.decode(value, { stream: true });
1124
+ let cut = buffer.indexOf("\\n\\n");
1125
+ while (cut >= 0) {
1126
+ const chunk = buffer.slice(0, cut);
1127
+ buffer = buffer.slice(cut + 2);
1128
+ cut = buffer.indexOf("\\n\\n");
1129
+ let event = "message";
1130
+ let data = "";
1131
+ for (const line of chunk.split("\\n")) {
1132
+ if (line.startsWith("event: ")) event = line.slice(7);
1133
+ else if (line.startsWith("data: ")) data += line.slice(6);
1134
+ }
1135
+ if (!data) continue;
1136
+ const payload = JSON.parse(data);
1137
+ if (event === "progress") { const said = describeProgress(payload); if (said) status.textContent = said; }
1138
+ else if (event === "done") { status.remove(); mine.remove(); showTurn(payload); }
1139
+ }
1140
+ }
1141
+ } catch (err) {
1142
+ note("ai-error", String(err));
1143
+ } finally {
1144
+ setBusy(false);
1145
+ }
1146
+ }
1147
+ async function publish(channel) {
1148
+ if (channel === "live" && !confirm("Publish this build to everyone?")) return;
1149
+ for (const b of publishButtons) b.disabled = true;
1150
+ publishResult.textContent = "Building and publishing to " + (channel === "live" ? "everyone" : "testers") + "...";
1151
+ try {
1152
+ const res = await fetch("/_publish", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ channel }) });
1153
+ showPublish(await res.json());
1154
+ } catch (err) {
1155
+ publishResult.textContent = String(err);
1156
+ } finally {
1157
+ for (const b of publishButtons) b.disabled = false;
1158
+ }
1159
+ }
1160
+ aiAsk.onclick = ask;
1161
+ aiInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) ask(); });
1162
+ document.getElementById("ai-reset").onclick = async () => {
1163
+ await fetch("/_ai", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reset: true }) });
1164
+ aiLog.replaceChildren();
1165
+ };
1166
+ publishButtons[0].onclick = () => publish("test");
1167
+ publishButtons[1].onclick = () => publish("live");
1168
+ (async () => {
1169
+ try {
1170
+ const state = await (await fetch("/_ai")).json();
1171
+ for (const turn of state.turns) showTurn(turn);
1172
+ setBusy(state.busy);
1173
+ const published = await (await fetch("/_publish")).json();
1174
+ if (published.last) showPublish(published.last);
1175
+ } catch (err) {
1176
+ note("ai-error", String(err));
1177
+ }
1178
+ })();
1179
+ function el(tag, cls, text) {
1180
+ const node = document.createElement(tag);
1181
+ if (cls) node.className = cls;
1182
+ if (text !== undefined && text !== null) node.textContent = String(text);
1183
+ return node;
1184
+ }
1185
+ let sim = null;
1186
+ function bound(name) { return typeof name === "string" && name.startsWith("$") ? name.slice(1) : null; }
1187
+ function actionName(action) { return typeof action === "string" && action.startsWith("action:") ? action.slice(7) : null; }
1188
+ function act(node, action, trigger, scope) {
1189
+ const name = actionName(action);
1190
+ if (name === null) { toast("action steps run on the host"); return; }
1191
+ const values = scope ? Object.entries(scope).map(([binding, value]) => ({ binding, value })) : null;
1192
+ dispatchUi({ kind: "action", action: name, element: node.key ?? name, trigger, values });
1193
+ }
1194
+ let dispatchUi = () => {};
1195
+ function optionsOf(options) {
1196
+ return (Array.isArray(options) ? options : []).map((o) => typeof o === "object" && o !== null ? { value: String(o.value), label: String(o.label ?? o.value), available: o.available !== false } : { value: String(o), label: String(o), available: true });
1197
+ }
1198
+ function render(node, form) {
1199
+ if (node === null || typeof node !== "object") return el("span", "", node);
1200
+ const p = node.props || {};
1201
+ const scope = node.type === "Form" ? {} : form;
1202
+ const kids = (node.children || []).map((c) => render(c, scope));
1203
+ const key = bound(p.value);
1204
+ switch (node.type) {
1205
+ case "Stack": case "Grid": { const d = el("div", p.direction === "row" ? "stack row" : "stack"); d.append(...kids); return d; }
1206
+ case "Card": { const d = el("div", "card"); if (p.title) d.append(el("h3", "", p.title)); d.append(...kids); return d; }
1207
+ case "Form": {
1208
+ const d = el("div", "card"); if (p.title) d.append(el("h3", "", p.title)); if (p.subtitle) d.append(el("div", "t-caption", p.subtitle));
1209
+ const submit = el("button", "primary", p.submitLabel ?? "Submit"); submit.type = "button";
1210
+ submit.onclick = () => act(node, p.submit, "submit", scope);
1211
+ d.append(...kids, submit); return d;
1212
+ }
1213
+ case "TextContent": return el("div", "t-" + (p.size || "body"), p.text);
1214
+ case "Stat": case "Metric": { const d = el("div", "stat"); d.append(el("span", "", p.label), el("b", "", p.value)); if (p.sub) d.append(el("span", "", p.sub)); return d; }
1215
+ case "KeyValue": case "Money": { const d = el("div", "kv"); d.append(el("span", "", p.label ?? ""), el("span", "", p.value ?? p.amount)); return d; }
1216
+ case "Divider": return el("hr");
1217
+ case "Badge": case "Callout": case "Banner": return el("div", "card t-caption", p.text ?? p.title ?? p.label);
1218
+ case "EmptyState": { const d = el("div", "card"); d.append(el("div", "t-title", p.title), el("div", "t-caption", p.message ?? p.subtitle)); return d; }
1219
+ case "Progress": { const d = el("div", "progress"); const i = el("i"); i.style.width = Math.round(Math.max(0, Math.min(1, Number(p.value) || 0)) * 100) + "%"; d.append(i); return d; }
1220
+ case "Image": { const img = el("img"); img.src = p.src ?? p.url ?? ""; img.alt = p.alt ?? ""; img.style.maxWidth = "100%"; return img; }
1221
+ case "Button": { const b = el("button", p.variant === "primary" || p.variant === "cta" ? "primary" : "", p.label); b.onclick = () => act(node, p.action, "tap", null); return b; }
1222
+ case "Buttons": { const d = el("div", "stack row"); d.append(...kids); return d; }
1223
+ case "Choice": {
1224
+ const wrap = el("label", "field"); wrap.append(el("span", "t-label", p.label));
1225
+ const row = el("div", "choice");
1226
+ for (const o of optionsOf(p.options)) {
1227
+ const b = el("button", "", o.label); b.disabled = !o.available; b.type = "button";
1228
+ b.onclick = () => { if (key) form[key] = o.value; for (const x of row.children) x.classList.remove("on"); b.classList.add("on"); };
1229
+ row.append(b);
1230
+ }
1231
+ wrap.append(row); return wrap;
1232
+ }
1233
+ case "Select": {
1234
+ const wrap = el("label", "field"); wrap.append(el("span", "t-label", p.label));
1235
+ const s = el("select"); s.append(el("option", "", p.placeholder ?? "Choose"));
1236
+ for (const o of optionsOf(p.options)) { const opt = el("option", "", o.label); opt.value = o.value; opt.disabled = !o.available; s.append(opt); }
1237
+ s.onchange = () => { if (key) form[key] = s.value; };
1238
+ wrap.append(s); return wrap;
1239
+ }
1240
+ case "Stepper": {
1241
+ const min = Number(p.min ?? 0); const max = Number(p.max ?? 99); const step = Number(p.step ?? 1);
1242
+ if (key && form[key] === undefined) form[key] = min;
1243
+ const d = el("div", "stepper"); const value = el("b", "", key ? form[key] : min);
1244
+ const minus = el("button", "", "-"); const plus = el("button", "", "+"); minus.type = plus.type = "button";
1245
+ minus.onclick = () => { if (key) { form[key] = Math.max(min, form[key] - step); value.textContent = form[key]; } };
1246
+ plus.onclick = () => { if (key) { form[key] = Math.min(max, form[key] + step); value.textContent = form[key]; } };
1247
+ const ctl = el("div"); ctl.append(minus, value, plus); d.append(el("span", "", p.label), ctl); return d;
1248
+ }
1249
+ case "Toggle": {
1250
+ const wrap = el("label", "kv"); const box = el("input"); box.type = "checkbox"; box.style.width = "auto";
1251
+ if (key) form[key] = false; box.onchange = () => { if (key) form[key] = box.checked; };
1252
+ wrap.append(el("span", "", p.label), box); return wrap;
1253
+ }
1254
+ case "Input": {
1255
+ const wrap = el("label", "field"); wrap.append(el("span", "t-label", p.label));
1256
+ const input = el("input"); input.placeholder = p.placeholder ?? ""; input.type = p.kind === "number" || p.kind === "money" ? "number" : p.kind === "email" ? "email" : "text";
1257
+ input.oninput = () => { if (key) form[key] = input.type === "number" ? Number(input.value) : input.value; };
1258
+ wrap.append(input); return wrap;
1259
+ }
1260
+ default: { const d = el("div", "unknown", node.type + " " + JSON.stringify(p)); d.append(...kids); return d; }
1261
+ }
1262
+ }
1263
+ let toastTimer = 0;
1264
+ function toast(text) { toastEl.textContent = text; toastEl.classList.add("on"); clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove("on"), 1800); }
1265
+ const host = {
1266
+ render(tree) { const canvas = screen.querySelector("canvas"); screen.replaceChildren(render(tree, {})); if (canvas) screen.append(canvas); },
1267
+ openSheet(request) {
1268
+ return new Promise((resolve) => {
1269
+ const form = {};
1270
+ const panel = el("div");
1271
+ panel.append(el("h3", "sheet-title", request.title), render(request.tree, form));
1272
+ const actions = el("div", "sheet-actions");
1273
+ const close = (result) => { sheet.hidden = true; sheet.replaceChildren(); resolve(result); };
1274
+ if (request.dismissible) { const c = el("button", "", "Cancel"); c.onclick = () => close({ status: "dismissed" }); actions.append(c); }
1275
+ const ok = el("button", "primary", "Done"); ok.onclick = () => close({ status: "submitted", value: { ...form } }); actions.append(ok);
1276
+ panel.append(actions); sheet.replaceChildren(panel); sheet.hidden = false;
1277
+ });
1278
+ },
1279
+ toast,
1280
+ async acquireSurface(options) {
1281
+ const canvas = el("canvas");
1282
+ const ratio = Math.min(devicePixelRatio || 1, options.pixel_ratio_cap || 1);
1283
+ screen.style.padding = "0"; screen.append(canvas);
1284
+ const width = screen.clientWidth; const height = screen.clientHeight;
1285
+ canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
1286
+ canvas.getContext("2d")?.scale(ratio, ratio);
1287
+ return { canvas, width, height, pixel_ratio: ratio, on_resize() { return () => {}; }, release() { canvas.remove(); } };
1288
+ },
1289
+ requestFrame: (cb) => requestAnimationFrame(cb),
1290
+ cancelFrame: (h) => cancelAnimationFrame(h),
1291
+ };
1292
+ const DEV_PERSONA = { age_range: "adult", avatar: null, display_name: "Local Player", handle: "local", participant_id: "participant_local" };
1293
+ function command(c) {
1294
+ switch (c.kind) {
1295
+ case "toast": case "announce": toast(c.text); return null;
1296
+ case "open_sheet": return host.openSheet(c.request);
1297
+ case "open_handoff": return { status: "cancelled" };
1298
+ default: return null;
1299
+ }
1300
+ }
1301
+ function showFault(f) {
1302
+ errors.textContent = (f.source === "guest" ? f.phase : f.source) + ": " + f.message + (f.stack ? "\\n" + f.stack : "");
1303
+ }
1304
+ const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
1305
+ if (RECORD_PRELUDE !== null) {
1306
+ let recorded = [];
1307
+ let recordTimer = 0;
1308
+ const sendRecorded = () => {
1309
+ recordTimer = 0;
1310
+ if (recorded.length === 0) return;
1311
+ const body = JSON.stringify(recorded);
1312
+ recorded = [];
1313
+ fetch(RECORD_ROUTE, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).catch(() => {});
1314
+ };
1315
+ addEventListener("message", (e) => {
1316
+ const data = e.data;
1317
+ if (data === null || typeof data !== "object" || !(RECORD_KEY in data)) return;
1318
+ if (![...screen.querySelectorAll("iframe")].some((f) => f.contentWindow === e.source)) return;
1319
+ recorded.push(data[RECORD_KEY]);
1320
+ if (data[RECORD_KEY].kind === "start") sendRecorded();
1321
+ else if (recordTimer === 0) recordTimer = setTimeout(sendRecorded, RECORD_FLUSH_MS);
1322
+ });
1323
+ addEventListener("pagehide", sendRecorded);
1324
+ }
1325
+ function askCtxAi(call) {
1326
+ const family = call.operation.slice(0, call.operation.indexOf("."));
1327
+ const route = family === "ai" ? CTX_AI_ROUTE : CTX_DATA_FAMILIES.includes(family) ? CTX_DATA_ROUTE : null;
1328
+ if (route === null) return undefined;
1329
+ return fetch(route, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ operation: call.operation, arguments: call.arguments }) }).then((r) => r.json());
1330
+ }
1331
+ async function enginesFor(web, build, chunks) {
1332
+ if (build.profile !== "canvas") return { engines: web.workerEngines(SDK_ROOT + GUEST_WORKER_MODULE), runtimes: undefined };
1333
+ const runtime = (RECORD_PRELUDE ?? "") + (await (await fetch(SDK_ROOT + CANVAS_GUEST_MODULE)).text());
1334
+ screen.style.padding = "0";
1335
+ const content = CONTENT_ORIGIN === null ? null : web.parseContentOrigin(CONTENT_ORIGIN);
1336
+ const camera = { runner: web.mediaPipeFaceRunner(chunks), container: document.getElementById("phone") };
1337
+ const engines = web.iframeEngines({ container: screen, runtime, content, engine_session: ENGINE_SESSION, camera });
1338
+ return { engines, runtimes: new web.RuntimePool(engines, chunks) };
1339
+ }
1340
+ async function runHosted(build) {
1341
+ const web = await import(SDK_ROOT + WEB_HOST_MODULE);
1342
+ const feedMode = new URLSearchParams(location.search).has(FEED_PARAM);
1343
+ const feedCache = feedMode ? new web.ChunkCache((base) => web.httpChunks(new URL(base, location.origin).href)) : null;
1344
+ const chunks = feedCache === null ? web.httpChunks(location.origin + CHUNK_ROUTE) : feedCache.from(CHUNK_ROUTE);
1345
+ if (feedCache !== null) {
1346
+ const feed = new web.PackagePrefetcher(web.httpRegistry(location.origin), feedCache, "live", web.IDLE_SCHEDULER);
1347
+ globalThis.cimplifyFeed = feed;
1348
+ feed.visible(build.interactive_id ?? LOCAL_INTERACTIVE);
1349
+ }
1350
+ const { engines, runtimes } = await enginesFor(web, build, chunks);
1351
+ const partStore = await web.openWebPartStore().catch(() => undefined);
1352
+ let hosted = null;
1353
+ let overlay = false;
1354
+ const overlayButton = document.getElementById("overlay");
1355
+ document.getElementById("performance").hidden = false;
1356
+ overlayButton.onclick = () => {
1357
+ overlay = !overlay;
1358
+ overlayButton.classList.toggle("on", overlay);
1359
+ overlayButton.textContent = overlay ? "Hide performance overlay" : "Show performance overlay";
1360
+ hosted?.setInspector(overlay);
1361
+ };
1362
+ const home = { build, chunks };
1363
+ const launch = async (target = home) => {
1364
+ const silent = new URLSearchParams(location.search).has(SILENT_PARAM);
1365
+ const session = web.localSession({ interactive_id: target.build.interactive_id ?? LOCAL_INTERACTIVE, profile: target.build.profile, persona: DEV_PERSONA, answerAsync: silent ? () => new Promise(() => {}) : askCtxAi });
1366
+ const next = new web.WebHost({
1367
+ engines,
1368
+ runtimes,
1369
+ transport: session.transport,
1370
+ hello: session.hello,
1371
+ package: { chunks: target.build.chunks },
1372
+ chunks: target.chunks,
1373
+ part_store: partStore,
1374
+ locale: DEV_LOCALE,
1375
+ on_command: command,
1376
+ on_fault: showFault,
1377
+ on_milestone: (name) => performance.mark("cimplify:" + name),
1378
+ });
1379
+ hosted = next;
1380
+ if (overlay) next.setInspector(true);
1381
+ next.tree.subscribe(() => { const tree = next.tree.snapshot(); if (tree) host.render(tree); });
1382
+ dispatchUi = (event) => next.dispatch(event);
1383
+ await nextFrame();
1384
+ performance.clearMarks();
1385
+ performance.mark("cimplify:host-start");
1386
+ globalThis.cimplifyFeed?.pause();
1387
+ await next.start();
1388
+ };
1389
+ globalThis.cimplifyHotSwap = () => web.swapInPlace(hosted, async () => (await fetch("/_build")).json());
1390
+ globalThis.cimplifyRelaunch = async () => {
1391
+ const ending = hosted;
1392
+ ending.dispose();
1393
+ while (ending.phase !== "disposed") await nextFrame();
1394
+ const warm = runtimes === undefined ? false : await runtimes.warmed(build.chunks, build.interactive_id ?? LOCAL_INTERACTIVE);
1395
+ await launch();
1396
+ return warm;
1397
+ };
1398
+ let showing = home;
1399
+ let away = null;
1400
+ globalThis.cimplifySwipe = async () => {
1401
+ if (away === null) {
1402
+ const nextBuild = await (await fetch(NEXT_ROUTE + "_build")).json();
1403
+ away = { build: nextBuild, chunks: web.httpChunks(location.origin + NEXT_ROUTE.slice(0, -1) + CHUNK_ROUTE) };
1404
+ }
1405
+ const ending = hosted;
1406
+ ending?.dispose();
1407
+ while (ending !== null && ending.phase !== "disposed") await nextFrame();
1408
+ showing = showing === home ? away : home;
1409
+ await launch(showing);
1410
+ return showing === away ? "next" : "home";
1411
+ };
1412
+ addEventListener("visibilitychange", () => hosted?.lifecycle(document.hidden ? "hidden" : "visible"));
1413
+ addEventListener("pagehide", () => { hosted?.dispose(); runtimes?.drain(); });
1414
+ if (feedMode || new URLSearchParams(location.search).has(HOLD_PARAM)) globalThis.cimplifyLaunch = launch;
1415
+ else await launch();
1416
+ }
1417
+ const build = await (await fetch("/_build")).json();
1418
+ if (!build.ok) {
1419
+ errors.textContent = build.diagnostics.join("\\n");
1420
+ } else if (build.host && build.chunks) {
1421
+ if (build.warnings.length > 0) console.warn(build.warnings.join("\\n"));
1422
+ runHosted(build).catch((err) => { errors.textContent = String(err && err.stack ? err.stack : err); });
1423
+ } else {
1424
+ if (build.warnings.length > 0) console.warn(build.warnings.join("\\n"));
1425
+ sim = createSimulatorCtx({ host, profile: build.profile, interactiveId: build.interactive_id ?? undefined });
1426
+ dispatchUi = (event) => sim.dispatch(event);
1427
+ addEventListener("visibilitychange", () => sim.emit(document.hidden ? "hidden" : "visible"));
1428
+ addEventListener("pagehide", () => sim.dispose());
1429
+ try {
1430
+ const runtime = await import(SDK_ROOT + RUNTIME_MODULE);
1431
+ const modules = await Promise.all(build.modules.map(async (m) => ({ path: m.path, imports: m.imports.map((i) => ({ ...i, export: null })), code: await (await fetch("/app/" + m.path)).text() })));
1432
+ const compiler = runtime.blobCompiler(runtime.BLOB_URLS, (url) => import(url));
1433
+ const slice = new runtime.Slice({ begin() {}, over: () => false });
1434
+ const mod = await runtime.commonJsLinker(compiler).link(modules, build.entry, slice.tick);
1435
+ await mod.default(sim.ctx);
1436
+ } catch (err) {
1437
+ errors.textContent = String(err && err.stack ? err.stack : err);
1438
+ }
1439
+ }
1440
+ `;
1441
+ var LOCAL_INTERACTIVE = "itv_local";
1442
+ var HOLD_PARAM = "hold";
1443
+ var SILENT_PARAM = "silent";
1444
+ var FEED_PARAM = "feed";
1445
+ var RECORD_ROUTE = "/_record";
1446
+ var RECORD_FLUSH_MS = 200;
1447
+ function hostConstants(contentOrigin, engineSession, recordPrelude, locale) {
1448
+ const constants = {
1449
+ DEV_LOCALE: locale,
1450
+ RECORD_PRELUDE: recordPrelude,
1451
+ RECORD_ROUTE,
1452
+ RECORD_KEY: RECORD_MESSAGE,
1453
+ RECORD_FLUSH_MS,
1454
+ CONTENT_ORIGIN: contentOrigin,
1455
+ ENGINE_SESSION: engineSession,
1456
+ LOCAL_INTERACTIVE,
1457
+ HOLD_PARAM,
1458
+ SILENT_PARAM,
1459
+ FEED_PARAM,
1460
+ SDK_ROOT: `${NPM_ROUTE}${SDK_PACKAGE}/`,
1461
+ WEB_HOST_MODULE,
1462
+ RUNTIME_MODULE,
1463
+ GUEST_WORKER_MODULE,
1464
+ CANVAS_GUEST_MODULE,
1465
+ CHUNK_ROUTE,
1466
+ NEXT_ROUTE,
1467
+ CTX_AI_ROUTE: DEV_AI_ROUTE,
1468
+ CTX_DATA_ROUTE: DEV_DATA_ROUTE,
1469
+ CTX_DATA_FAMILIES: [...DATA_FAMILIES],
1470
+ TICK_ROUTE,
1471
+ TICK_EVENT
1472
+ };
1473
+ let out = "";
1474
+ for (const [name, value] of Object.entries(constants))
1475
+ out += `const ${name} = ${escapeJsonForHtml(JSON.stringify(value))};
1476
+ `;
1477
+ return out;
1478
+ }
1479
+ function escapeJsonForHtml(value) {
1480
+ return value.replaceAll("<", "\\u003c");
1481
+ }
1482
+ function simulatorPage(title, importMap, contentOrigin = null, engineSession = null, recordPrelude = null, locale = devLocale()) {
1483
+ const map = escapeJsonForHtml(JSON.stringify({ imports: importMap }));
1484
+ const safeTitle = title.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
1485
+ return `<!doctype html>
1486
+ <html lang="en">
1487
+ <head>
1488
+ <meta charset="utf-8">
1489
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1490
+ <title>${safeTitle} simulator</title>
1491
+ <style>${STYLE}</style>
1492
+ <script type="importmap">${map}</script>
1493
+ </head>
1494
+ <body>
1495
+ <main id="stage">
1496
+ <div id="phone"><div id="screen"></div><div id="sheet" hidden></div><div id="toast"></div></div>
1497
+ <aside id="studio">
1498
+ <section><h2>Ask AI</h2><div id="ai-log"></div><textarea id="ai-input" placeholder="What should change? Ctrl or Cmd + Enter to send"></textarea><div class="studio-row"><button id="ai-ask" class="primary" type="button">Ask</button><button id="ai-reset" type="button">New chat</button></div></section>
1499
+ <section id="tick" hidden><h2>Tick room</h2><div id="tick-status" class="ai-step">Starting</div><div id="tick-budget" class="ai-step"></div><div id="tick-diff" class="ai-step"></div><div id="tick-log"></div><div class="studio-row"><button id="tick-join" type="button">Join seat 0</button><button id="tick-leave" type="button">Leave</button></div></section>
1500
+ <section id="performance" hidden><h2>Performance</h2><div class="studio-row"><button id="overlay" type="button">Show performance overlay</button></div></section>
1501
+ <section><h2>Publish</h2><div class="studio-row"><button id="publish-test" type="button">Publish to testers</button><button id="publish-live" class="primary" type="button">Publish live</button></div><div id="publish-result"></div></section>
1502
+ </aside>
1503
+ </main>
1504
+ <pre id="errors"></pre>
1505
+ <script type="module">${hostConstants(contentOrigin, engineSession, recordPrelude, locale)}${SCRIPT}</script>
1506
+ </body>
1507
+ </html>
1508
+ `;
1509
+ }
1510
+ var AI_ROUTE = "/_ai";
1511
+ var PUBLISH_ROUTE = "/_publish";
1512
+ var MAX_BODY_BYTES = 64 * 1024;
1513
+ var MAX_TURNS_KEPT = 30;
1514
+ var JSON_TYPE = "application/json; charset=utf-8";
1515
+ function sendJson(res, status, body) {
1516
+ res.writeHead(status, { "content-type": JSON_TYPE, "cache-control": "no-store", ...HOST_PAGE_ISOLATION_HEADERS });
1517
+ res.end(JSON.stringify(body));
1518
+ }
1519
+ async function readJsonBody(req) {
1520
+ const chunks = [];
1521
+ let size = 0;
1522
+ for await (const chunk of req) {
1523
+ const buffer = chunk;
1524
+ size += buffer.length;
1525
+ if (size > MAX_BODY_BYTES) throw new Error("the request body is too large");
1526
+ chunks.push(buffer);
1527
+ }
1528
+ if (size === 0) return {};
1529
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1530
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1531
+ }
1532
+ function errorView(error) {
1533
+ if (error instanceof CliError) return { error: error.message, remediation: error.remediation ?? null };
1534
+ return { error: error instanceof Error ? error.message : String(error), remediation: null };
1535
+ }
1536
+ var Studio = class {
1537
+ constructor(options) {
1538
+ this.options = options;
1539
+ }
1540
+ options;
1541
+ authoringNow = false;
1542
+ publishingNow = false;
1543
+ history = [];
1544
+ turns = [];
1545
+ lastPublish = null;
1546
+ get quiet() {
1547
+ return this.authoringNow || this.publishingNow;
1548
+ }
1549
+ async serve(req, res, route) {
1550
+ if (route === AI_ROUTE) {
1551
+ if (req.method === "POST") await this.ask(req, res);
1552
+ else sendJson(res, 200, { busy: this.authoringNow, turns: this.turns });
1553
+ return true;
1554
+ }
1555
+ if (route === PUBLISH_ROUTE) {
1556
+ if (req.method === "POST") await this.publish(req, res);
1557
+ else sendJson(res, 200, { busy: this.publishingNow, last: this.lastPublish });
1558
+ return true;
1559
+ }
1560
+ return false;
1561
+ }
1562
+ remember(turn) {
1563
+ this.turns.push(turn);
1564
+ if (this.turns.length > MAX_TURNS_KEPT) this.turns.splice(0, this.turns.length - MAX_TURNS_KEPT);
1565
+ }
1566
+ async ask(req, res) {
1567
+ let request = "";
1568
+ try {
1569
+ const body = await readJsonBody(req);
1570
+ if (body.reset === true) {
1571
+ this.history = [];
1572
+ this.turns.length = 0;
1573
+ sendJson(res, 200, { reset: true });
1574
+ return;
1575
+ }
1576
+ request = typeof body.request === "string" ? body.request.trim() : "";
1577
+ } catch (error) {
1578
+ sendJson(res, 400, errorView(error));
1579
+ return;
1580
+ }
1581
+ if (request.length === 0) {
1582
+ sendJson(res, 400, { error: "say what to change", remediation: null });
1583
+ return;
1584
+ }
1585
+ if (this.authoringNow || this.publishingNow) {
1586
+ sendJson(res, 409, { error: "the AI or a publish is already running; wait for it to finish", remediation: null });
1587
+ return;
1588
+ }
1589
+ let model;
1590
+ try {
1591
+ model = this.options.model();
1592
+ } catch (error) {
1593
+ sendJson(res, 400, errorView(error));
1594
+ return;
1595
+ }
1596
+ this.authoringNow = true;
1597
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive", ...HOST_PAGE_ISOLATION_HEADERS });
1598
+ const write = (event, data) => {
1599
+ if (!res.writableEnded) res.write(`event: ${event}
1600
+ data: ${JSON.stringify(data)}
1601
+
1602
+ `);
1603
+ };
1604
+ let changed = 0;
1605
+ try {
1606
+ const outcome = await askAuthoring(this.options.root, request, {
1607
+ model,
1608
+ history: this.history,
1609
+ check: buildCheck(this.options.rebuild),
1610
+ onEvent: (event) => write("progress", event)
1611
+ });
1612
+ this.history = trimHistory(outcome.messages);
1613
+ changed = outcome.changed.length;
1614
+ const turn = {
1615
+ request,
1616
+ summary: outcome.summary,
1617
+ changed: outcome.changed,
1618
+ diagnostics: outcome.diagnostics,
1619
+ end: outcome.end,
1620
+ error: null,
1621
+ lines: outcome.lines,
1622
+ lineDelta: outcome.lineDelta
1623
+ };
1624
+ this.remember(turn);
1625
+ write("done", turn);
1626
+ } catch (error) {
1627
+ const turn = { request, summary: "", changed: [], diagnostics: [], end: "failed", error: errorView(error).error, lines: null, lineDelta: 0 };
1628
+ this.remember(turn);
1629
+ write("done", turn);
1630
+ } finally {
1631
+ this.authoringNow = false;
1632
+ res.end();
1633
+ }
1634
+ if (changed > 0) await this.options.rebuild();
1635
+ }
1636
+ async publish(req, res) {
1637
+ if (this.authoringNow || this.publishingNow) {
1638
+ sendJson(res, 409, { ok: false, error: "the AI or a publish is already running; wait for it to finish", remediation: null });
1639
+ return;
1640
+ }
1641
+ this.publishingNow = true;
1642
+ try {
1643
+ const body = await readJsonBody(req);
1644
+ const channel = parseChannel(typeof body.channel === "string" ? body.channel : "test");
1645
+ const summary = await publishProject(this.options.root, channel, { remote: await this.options.remote(), keyDir: this.options.keyDir });
1646
+ const rev = summary.outcome.revision;
1647
+ const view = {
1648
+ ok: rev.state !== "refused",
1649
+ channel,
1650
+ interactive_id: summary.interactiveId,
1651
+ revision: rev.revision,
1652
+ state: rev.state,
1653
+ package_digest: rev.packageDigest,
1654
+ uploaded: summary.uploaded,
1655
+ skipped: summary.skipped,
1656
+ logs: summary.outcome.logs,
1657
+ error: rev.state === "refused" ? `revision ${rev.revision} was refused at admission` : null,
1658
+ remediation: null
1659
+ };
1660
+ this.lastPublish = view;
1661
+ sendJson(res, 200, view);
1662
+ } catch (error) {
1663
+ const failed = errorView(error);
1664
+ const view = {
1665
+ ok: false,
1666
+ channel: null,
1667
+ interactive_id: null,
1668
+ revision: null,
1669
+ state: null,
1670
+ package_digest: null,
1671
+ uploaded: 0,
1672
+ skipped: 0,
1673
+ logs: [],
1674
+ error: failed.error,
1675
+ remediation: failed.remediation
1676
+ };
1677
+ this.lastPublish = view;
1678
+ sendJson(res, error instanceof CliError ? 400 : 500, view);
1679
+ } finally {
1680
+ this.publishingNow = false;
1681
+ }
1682
+ await this.options.rebuild();
1683
+ }
1684
+ };
1685
+
1686
+ // src/interactives/dev/source-map.ts
1687
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1688
+ var VLQ_SHIFT = 5;
1689
+ var VLQ_CONTINUE = 32;
1690
+ var VLQ_MASK = 31;
1691
+ var SEGMENT_SEPARATOR = ",";
1692
+ var LINE_SEPARATOR = ";";
1693
+ var DIGITS = new Map([...BASE64].map((c, i) => [c, i]));
1694
+ function decodeVlq(segment) {
1695
+ const out = [];
1696
+ let value = 0;
1697
+ let shift = 0;
1698
+ for (const c of segment) {
1699
+ const digit = DIGITS.get(c);
1700
+ if (digit === void 0) throw new Error(`'${c}' is not a base64 VLQ digit`);
1701
+ value += (digit & VLQ_MASK) * 2 ** shift;
1702
+ if (digit & VLQ_CONTINUE) {
1703
+ shift += VLQ_SHIFT;
1704
+ continue;
1705
+ }
1706
+ const magnitude = Math.floor(value / 2);
1707
+ out.push(value % 2 === 1 ? -magnitude : magnitude);
1708
+ value = 0;
1709
+ shift = 0;
1710
+ }
1711
+ return out;
1712
+ }
1713
+ var SourceMapIndex = class _SourceMapIndex {
1714
+ lines;
1715
+ sources;
1716
+ constructor(map) {
1717
+ const root = map.sourceRoot ?? "";
1718
+ this.sources = map.sources.map((s) => root + s);
1719
+ const lines = [];
1720
+ let source = 0;
1721
+ let line = 0;
1722
+ let originalColumn = 0;
1723
+ for (const text of map.mappings.split(LINE_SEPARATOR)) {
1724
+ const segments = [];
1725
+ let column = 0;
1726
+ if (text.length > 0) {
1727
+ for (const raw of text.split(SEGMENT_SEPARATOR)) {
1728
+ const fields = decodeVlq(raw);
1729
+ column += fields[0];
1730
+ if (fields.length < 4) continue;
1731
+ source += fields[1];
1732
+ line += fields[2];
1733
+ originalColumn += fields[3];
1734
+ segments.push({ column, source, line, originalColumn });
1735
+ }
1736
+ }
1737
+ segments.sort((a, b) => a.column - b.column);
1738
+ lines.push(segments);
1739
+ }
1740
+ this.lines = lines;
1741
+ }
1742
+ static parse(text) {
1743
+ return new _SourceMapIndex(JSON.parse(text));
1744
+ }
1745
+ original(line, column) {
1746
+ const segments = this.lines[line - 1];
1747
+ if (segments === void 0 || segments.length === 0) return null;
1748
+ const target = column - 1;
1749
+ let low = 0;
1750
+ let high = segments.length - 1;
1751
+ let found = -1;
1752
+ while (low <= high) {
1753
+ const mid = low + high >> 1;
1754
+ if (segments[mid].column <= target) {
1755
+ found = mid;
1756
+ low = mid + 1;
1757
+ } else {
1758
+ high = mid - 1;
1759
+ }
1760
+ }
1761
+ const segment = segments[found === -1 ? 0 : found];
1762
+ const source = this.sources[segment.source];
1763
+ if (source === void 0) return null;
1764
+ return { source, line: segment.line + 1, column: segment.originalColumn + 1 };
1765
+ }
1766
+ };
1767
+ var PART_STORE_ENV = "CIMPLIFY_PART_STORE";
1768
+ var PARTS_ROUTE = "/_parts";
1769
+ var BODIES = "bodies";
1770
+ var INDEX = "index.json";
1771
+ var DIGEST = /^[0-9a-f]{64}$/;
1772
+ var FsPartBackend = class {
1773
+ constructor(dir) {
1774
+ this.dir = dir;
1775
+ }
1776
+ dir;
1777
+ body(digest2) {
1778
+ if (!DIGEST.test(digest2)) throw new Error(`'${digest2}' is not a part digest`);
1779
+ return path.join(this.dir, BODIES, digest2);
1780
+ }
1781
+ async replace(file, bytes) {
1782
+ await promises.mkdir(path.dirname(file), { recursive: true });
1783
+ const temp = `${file}.${process.pid}.tmp`;
1784
+ await promises.writeFile(temp, bytes);
1785
+ await promises.rename(temp, file);
1786
+ }
1787
+ async read(digest2) {
1788
+ try {
1789
+ return new Uint8Array(await promises.readFile(this.body(digest2)));
1790
+ } catch {
1791
+ return null;
1792
+ }
1793
+ }
1794
+ write(digest2, bytes) {
1795
+ return this.replace(this.body(digest2), bytes);
1796
+ }
1797
+ async remove(digest2) {
1798
+ await promises.rm(this.body(digest2), { force: true });
1799
+ }
1800
+ async load() {
1801
+ try {
1802
+ return JSON.parse(await promises.readFile(path.join(this.dir, INDEX), "utf8"));
1803
+ } catch {
1804
+ return null;
1805
+ }
1806
+ }
1807
+ save(snapshot) {
1808
+ return this.replace(path.join(this.dir, INDEX), JSON.stringify(snapshot));
1809
+ }
1810
+ };
1811
+ function partStoreDir(option, env = process.env) {
1812
+ if (option !== void 0) return option;
1813
+ const value = env[PART_STORE_ENV];
1814
+ return value === void 0 || value.length === 0 ? null : path.resolve(value);
1815
+ }
1816
+ async function openDevPartStore(dir, game, budgetBytes) {
1817
+ const store = await SharedPartStore.open(new FsPartBackend(dir), {} );
1818
+ let bodies = /* @__PURE__ */ new Map();
1819
+ let last = { downloaded: 0, shared: 0 };
1820
+ const source = {
1821
+ fetch: (digest2) => {
1822
+ const body = bodies.get(digest2);
1823
+ return body === void 0 ? Promise.reject(new Error(`chunk ${digest2} is not in the current build`)) : Promise.resolve(body);
1824
+ }
1825
+ };
1826
+ return {
1827
+ store,
1828
+ async install(digests, next) {
1829
+ bodies = next;
1830
+ const before = store.stats().downloaded_bytes;
1831
+ let shared = 0;
1832
+ await store.install(game, digests);
1833
+ for (const digest2 of digests) {
1834
+ const held = store.size(digest2);
1835
+ if (held !== null) shared += held;
1836
+ else await store.fetch(digest2, source);
1837
+ }
1838
+ await store.flush();
1839
+ last = { downloaded: store.stats().downloaded_bytes - before, shared };
1840
+ return last;
1841
+ },
1842
+ async fetch(digest2) {
1843
+ if (!store.has(digest2) && !bodies.has(digest2)) return null;
1844
+ return store.fetch(digest2, source);
1845
+ },
1846
+ report() {
1847
+ return { game, unique_bytes: store.uniqueBytes(game), last_install: last, ...store.stats() };
1848
+ }
1849
+ };
1850
+ }
1851
+ var TICK_GUEST_DEV_BINARY = "tick-guest-dev";
1852
+ var TICK_GUEST_DEV_ENV = "CIMPLIFY_TICK_GUEST_DEV";
1853
+ var TICK_ROOM_UNAVAILABLE_MESSAGE = "The tick room doesn't run locally with this CLI, so the simulator keeps going without it. To play your tick room, run cimplify publish --channel test and open the test channel as one of your testers (cimplify testers add <account_id>).";
1854
+ var DEV_TICK_SEATS = 8;
1855
+ var DEV_TICK_RATE_HZ = 60;
1856
+ var DEV_TICK_SEED = 1n;
1857
+ var TARGET_PROFILES = ["release", "debug"];
1858
+ var CARGO_MANIFEST = "Cargo.toml";
1859
+ var TARGET_DIR = "target";
1860
+ var LOCATION_SEPARATOR = ":";
1861
+ var ERROR_KINDS = /* @__PURE__ */ new Set(["threw", "deadline", "memory", "fuel", "invalid_decision", "program"]);
1862
+ var HEX2 = /^(?:[0-9a-f]{2})*$/;
1863
+ function whole(value) {
1864
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
1865
+ }
1866
+ function inRange(value, low, high) {
1867
+ return typeof value === "number" && Number.isInteger(value) && value >= low && value <= high;
1868
+ }
1869
+ function bodyOf(value) {
1870
+ if (value === null || typeof value !== "object") return null;
1871
+ const b = value;
1872
+ if (!whole(b.seat) || typeof b.alive !== "boolean" || !inRange(b.x, -2147483648, 2147483647) || !inRange(b.y, -2147483648, 2147483647) || !inRange(b.z, -2147483648, 2147483647)) return null;
1873
+ if (!inRange(b.yaw, 0, 65535) || !inRange(b.animation, 0, 255) || !inRange(b.flags, 0, 255)) return null;
1874
+ return { seat: b.seat, alive: b.alive, x: b.x, y: b.y, z: b.z, yaw: b.yaw, animation: b.animation, flags: b.flags };
1875
+ }
1876
+ function runOf(value) {
1877
+ if (value === null || typeof value !== "object") return null;
1878
+ const r = value;
1879
+ return whole(r.offset) && typeof r.bytes === "string" && HEX2.test(r.bytes) ? { offset: r.offset, bytes: r.bytes } : null;
1880
+ }
1881
+ function listOf(value, read) {
1882
+ if (!Array.isArray(value)) return null;
1883
+ const out = [];
1884
+ for (const item of value) {
1885
+ const parsed = read(item);
1886
+ if (parsed === null) return null;
1887
+ out.push(parsed);
1888
+ }
1889
+ return out;
1890
+ }
1891
+ function parseRunnerLine(line) {
1892
+ let raw;
1893
+ try {
1894
+ raw = JSON.parse(line);
1895
+ } catch {
1896
+ return null;
1897
+ }
1898
+ if (raw === null || typeof raw !== "object") return null;
1899
+ const e = raw;
1900
+ switch (e.type) {
1901
+ case "ready":
1902
+ return whole(e.state_bytes) && whole(e.seats) && whole(e.rate_hz) ? { type: "ready", state_bytes: e.state_bytes, seats: e.seats, rate_hz: e.rate_hz } : null;
1903
+ case "log":
1904
+ return whole(e.tick) && typeof e.message === "string" ? { type: "log", tick: e.tick, message: e.message } : null;
1905
+ case "diff": {
1906
+ const bodies = listOf(e.bodies, bodyOf);
1907
+ const state = listOf(e.state, runOf);
1908
+ return whole(e.tick) && bodies !== null && state !== null ? { type: "diff", tick: e.tick, bodies, state } : null;
1909
+ }
1910
+ case "budget":
1911
+ return whole(e.tick) && whole(e.step_ns) && whole(e.deadline_ns) && whole(e.heap_used_bytes) && whole(e.heap_limit_bytes) ? { type: "budget", tick: e.tick, step_ns: e.step_ns, deadline_ns: e.deadline_ns, heap_used_bytes: e.heap_used_bytes, heap_limit_bytes: e.heap_limit_bytes } : null;
1912
+ case "error":
1913
+ if (!whole(e.tick) || typeof e.kind !== "string" || !ERROR_KINDS.has(e.kind) || typeof e.message !== "string") return null;
1914
+ if (e.location !== null && typeof e.location !== "string") return null;
1915
+ return { type: "error", tick: e.tick, kind: e.kind, message: e.message, location: e.location };
1916
+ default:
1917
+ return null;
1918
+ }
1919
+ }
1920
+ function bundlePosition(location) {
1921
+ if (!location.startsWith(`${TICK_GUEST_ORIGIN}${LOCATION_SEPARATOR}`)) return null;
1922
+ const parts = location.slice(TICK_GUEST_ORIGIN.length + 1).split(LOCATION_SEPARATOR);
1923
+ if (parts.length !== 2) return null;
1924
+ const line = Number(parts[0]);
1925
+ const column = Number(parts[1]);
1926
+ return Number.isInteger(line) && line > 0 && Number.isInteger(column) && column > 0 ? { line, column } : null;
1927
+ }
1928
+ function mapLocation(location, map) {
1929
+ if (location === null) return null;
1930
+ const at = bundlePosition(location);
1931
+ if (at === null || map === null) return location;
1932
+ const original = map.original(at.line, at.column);
1933
+ return original === null ? location : `${original.source}:${original.line}:${original.column}`;
1934
+ }
1935
+ function inspectEvent(event, map) {
1936
+ if (event.type === "error") return { ...event, source: mapLocation(event.location, map) };
1937
+ if (event.type === "budget") return { ...event, percent: event.deadline_ns === 0 ? 0 : event.step_ns / event.deadline_ns * 100 };
1938
+ return event;
1939
+ }
1940
+ function repoRoots(start, exists) {
1941
+ const out = [];
1942
+ let dir = path.resolve(start);
1943
+ for (; ; ) {
1944
+ if (exists(path.join(dir, CARGO_MANIFEST))) out.push(dir);
1945
+ const parent = path.dirname(dir);
1946
+ if (parent === dir) return out;
1947
+ dir = parent;
1948
+ }
1949
+ }
1950
+ function resolveTickGuestBinary(probe) {
1951
+ if (probe.env !== void 0 && probe.env.length > 0) return probe.exists(probe.env) ? probe.env : null;
1952
+ for (const start of probe.starts) {
1953
+ for (const root of repoRoots(start, probe.exists)) {
1954
+ for (const profile of TARGET_PROFILES) {
1955
+ const candidate = path.join(root, TARGET_DIR, profile, TICK_GUEST_DEV_BINARY);
1956
+ if (probe.exists(candidate)) return candidate;
1957
+ }
1958
+ }
1959
+ }
1960
+ for (const dir of (probe.pathEnv ?? "").split(path.delimiter)) {
1961
+ if (dir.length === 0) continue;
1962
+ const candidate = path.join(dir, TICK_GUEST_DEV_BINARY);
1963
+ if (probe.exists(candidate)) return candidate;
1964
+ }
1965
+ return null;
1966
+ }
1967
+ function defaultBinaryProbe(projectRoot) {
1968
+ return {
1969
+ env: process.env[TICK_GUEST_DEV_ENV],
1970
+ starts: [projectRoot, path.dirname(fileURLToPath(import.meta.url))],
1971
+ pathEnv: process.env.PATH,
1972
+ exists: existsSync
1973
+ };
1974
+ }
1975
+ var spawnRunner = (binary, args) => spawn(binary, [...args], { stdio: ["pipe", "pipe", "inherit"] });
1976
+ function runnerArgs(bundle, seats, rateHz, seed) {
1977
+ return ["--bundle", bundle, "--seats", String(seats), "--rate", String(rateHz), "--seed", seed.toString()];
1978
+ }
1979
+ var TickRunner = class {
1980
+ child;
1981
+ exited = false;
1982
+ constructor(options) {
1983
+ const spawn = options.spawn ?? spawnRunner;
1984
+ this.child = spawn(options.binary, runnerArgs(options.bundle, options.seats ?? DEV_TICK_SEATS, options.rateHz ?? DEV_TICK_RATE_HZ, options.seed ?? DEV_TICK_SEED));
1985
+ const lines = createInterface({ input: this.child.stdout });
1986
+ lines.on("line", (line) => {
1987
+ const event = parseRunnerLine(line);
1988
+ if (event !== null) options.onEvent(inspectEvent(event, options.map));
1989
+ });
1990
+ this.child.on("error", (error) => {
1991
+ this.exited = true;
1992
+ options.onEvent({ type: "unavailable", message: error.message });
1993
+ });
1994
+ this.child.on("exit", (code) => {
1995
+ this.exited = true;
1996
+ options.onEvent({ type: "exit", code });
1997
+ });
1998
+ }
1999
+ get running() {
2000
+ return !this.exited;
2001
+ }
2002
+ send(input) {
2003
+ if (this.exited) return false;
2004
+ this.child.stdin.write(`${JSON.stringify(input)}
2005
+ `);
2006
+ return true;
2007
+ }
2008
+ stop() {
2009
+ if (this.exited) return;
2010
+ this.send({ type: "stop" });
2011
+ this.child.stdin.end();
2012
+ this.child.kill();
2013
+ }
2014
+ };
2015
+ function parseRunnerInput(value) {
2016
+ if (value === null || typeof value !== "object") return null;
2017
+ const v = value;
2018
+ switch (v.type) {
2019
+ case "join":
2020
+ case "leave":
2021
+ return inRange(v.seat, 0, 65535) ? { type: v.type, seat: v.seat } : null;
2022
+ case "input":
2023
+ return inRange(v.seat, 0, 65535) && inRange(v.axis_x, -32768, 32767) && inRange(v.axis_z, -32768, 32767) && inRange(v.buttons, 0, 255) ? { type: "input", seat: v.seat, axis_x: v.axis_x, axis_z: v.axis_z, buttons: v.buttons } : null;
2024
+ case "command":
2025
+ return inRange(v.seat, 0, 65535) && inRange(v.action, 0, 65535) && typeof v.payload === "string" && HEX2.test(v.payload) ? { type: "command", seat: v.seat, action: v.action, payload: v.payload } : null;
2026
+ case "stop":
2027
+ return { type: "stop" };
2028
+ default:
2029
+ return null;
2030
+ }
2031
+ }
2032
+
2033
+ // src/interactives/dev/server.ts
2034
+ var MAX_AI_BODY_BYTES = 256 * 1024;
2035
+ var MAX_RECORD_BYTES = 1024 * 1024;
2036
+ async function readCall(req) {
2037
+ const parts = [];
2038
+ let size = 0;
2039
+ for await (const part2 of req) {
2040
+ const chunk = part2;
2041
+ size += chunk.length;
2042
+ if (size > MAX_AI_BODY_BYTES) return null;
2043
+ parts.push(chunk);
2044
+ }
2045
+ try {
2046
+ const call = JSON.parse(Buffer.concat(parts).toString("utf8"));
2047
+ return typeof call.operation === "string" && call.arguments !== null && typeof call.arguments === "object" ? call : null;
2048
+ } catch {
2049
+ return null;
2050
+ }
2051
+ }
2052
+ async function readJson(req, limit) {
2053
+ const parts = [];
2054
+ let size = 0;
2055
+ for await (const part2 of req) {
2056
+ const chunk = part2;
2057
+ size += chunk.length;
2058
+ if (size > limit) return null;
2059
+ parts.push(chunk);
2060
+ }
2061
+ try {
2062
+ return JSON.parse(Buffer.concat(parts).toString("utf8"));
2063
+ } catch {
2064
+ return null;
2065
+ }
2066
+ }
2067
+ var TickDev = class {
2068
+ constructor(root, options, broadcast) {
2069
+ this.root = root;
2070
+ this.options = options;
2071
+ this.broadcast = broadcast;
2072
+ }
2073
+ root;
2074
+ options;
2075
+ broadcast;
2076
+ runner = null;
2077
+ code = null;
2078
+ binary = void 0;
2079
+ emit(event) {
2080
+ this.broadcast(event);
2081
+ this.options.onEvent?.(event);
2082
+ }
2083
+ async sync(build) {
2084
+ const guest = build.result.tickGuest ?? null;
2085
+ if (guest === null) {
2086
+ if (build.result.ok || build.result.manifest?.entries.tick === null) this.stop();
2087
+ return;
2088
+ }
2089
+ if (this.runner?.running && this.code !== null && this.code.equals(guest.code)) return;
2090
+ this.stop();
2091
+ this.code = guest.code;
2092
+ if (this.binary === void 0) this.binary = resolveTickGuestBinary(this.options.probe ?? defaultBinaryProbe(this.root));
2093
+ if (this.binary === null) {
2094
+ this.emit({ type: "unavailable", message: TICK_ROOM_UNAVAILABLE_MESSAGE });
2095
+ return;
2096
+ }
2097
+ const written = await writeTickGuest(path.join(this.root, ...TICK_DEV_DIR), guest);
2098
+ let map = null;
2099
+ try {
2100
+ map = SourceMapIndex.parse(guest.map);
2101
+ } catch {
2102
+ map = null;
2103
+ }
2104
+ this.runner = new TickRunner({ binary: this.binary, bundle: written.bundle, map, spawn: this.options.spawn, onEvent: (event) => this.emit(event) });
2105
+ }
2106
+ send(input) {
2107
+ return this.runner?.send(input) ?? false;
2108
+ }
2109
+ stop() {
2110
+ this.runner?.stop();
2111
+ this.runner = null;
2112
+ this.code = null;
2113
+ }
2114
+ };
2115
+ var APP_ROUTE = "/app/";
2116
+ var BUILD_ROUTE = "/_build";
2117
+ var EVENTS_ROUTE = "/_events";
2118
+ var TICK_DEV_DIR = [".cimplify", "dev"];
2119
+ var MAX_TICK_INPUT_BYTES = 4096;
2120
+ var REBUILD_DEBOUNCE_MS = 40;
2121
+ var KEEPALIVE_MS = 15e3;
2122
+ var CONTENT_TYPES = {
2123
+ ".js": "text/javascript; charset=utf-8",
2124
+ ".mjs": "text/javascript; charset=utf-8",
2125
+ ".json": "application/json; charset=utf-8",
2126
+ ".wasm": "application/wasm",
2127
+ ".css": "text/css; charset=utf-8"
2128
+ };
2129
+ function payloadDigests(build) {
2130
+ const out = /* @__PURE__ */ new Map();
2131
+ for (const chunk of build.result.chunks) if (chunk.payloadDigest !== null) out.set(chunk.digest, chunk.payloadDigest);
2132
+ return out;
2133
+ }
2134
+ function chunkSizes(build) {
2135
+ const out = /* @__PURE__ */ new Map();
2136
+ for (const chunk of build.result.chunks) out.set(chunk.digest, chunk.totalBytes);
2137
+ return out;
2138
+ }
2139
+ function packageChunks(build) {
2140
+ const pkg = build.result.package;
2141
+ if (pkg === null) return null;
2142
+ const payloads = payloadDigests(build);
2143
+ const sizes = chunkSizes(build);
2144
+ const seen = /* @__PURE__ */ new Set();
2145
+ const out = [];
2146
+ for (const group of pkg.groups) {
2147
+ const lazy = group.name.startsWith(LAZY_GROUP_PREFIX);
2148
+ for (const file of group.files) {
2149
+ if (file.guest !== void 0 || seen.has(file.chunk)) continue;
2150
+ seen.add(file.chunk);
2151
+ out.push({
2152
+ digest: file.chunk,
2153
+ before_play: group.before_play,
2154
+ asset_group: group.name,
2155
+ runtime: file.runtime ?? null,
2156
+ payload_digest: payloads.get(file.chunk) ?? null,
2157
+ lazy,
2158
+ size: sizes.get(file.chunk) ?? null
2159
+ });
2160
+ }
2161
+ }
2162
+ for (const runtime of pkg.runtime) {
2163
+ if (seen.has(runtime.chunk)) continue;
2164
+ seen.add(runtime.chunk);
2165
+ out.push({
2166
+ digest: runtime.chunk,
2167
+ before_play: true,
2168
+ asset_group: CODE_GROUP,
2169
+ runtime: { name: runtime.name, version: runtime.version },
2170
+ payload_digest: payloads.get(runtime.chunk) ?? null,
2171
+ lazy: false,
2172
+ size: sizes.get(runtime.chunk) ?? null
2173
+ });
2174
+ }
2175
+ return out;
2176
+ }
2177
+ function payloadOf(build, host) {
2178
+ const r = build.result;
2179
+ const manifest = r.manifest;
2180
+ const hasModules = r.modules.length > 0;
2181
+ return {
2182
+ ok: r.ok || hasModules && !r.diagnostics.some((d) => d.severity === "error" && !d.code.startsWith("delivery.")),
2183
+ digest: r.sourceDigest,
2184
+ entry: manifest ? outputPathOf(manifest.entries.client) : null,
2185
+ profile: manifest ? manifest.profile.kind : null,
2186
+ interactive_id: manifest ? manifest.identity.id : null,
2187
+ chunks: packageChunks(build),
2188
+ modules: r.modules.map((m) => ({ path: m.output, imports: m.imports })),
2189
+ host,
2190
+ diagnostics: r.diagnostics.filter((d) => d.severity === "error").map(formatDiagnostic),
2191
+ warnings: r.diagnostics.filter((d) => d.severity === "warning").map(formatDiagnostic)
2192
+ };
2193
+ }
2194
+ var FEED_IDS_PARAM = "ids";
2195
+ var FEED_ID_SEPARATOR = ",";
2196
+ var LOCAL_REVISION = "local";
2197
+ function feedPackages(build, ids) {
2198
+ const chunks = packageChunks(build);
2199
+ const local = build.result.manifest?.identity.id ?? LOCAL_INTERACTIVE;
2200
+ if (chunks === null) return [];
2201
+ const sized = chunks.map((chunk) => ({ ...chunk, size: chunk.size ?? 0 }));
2202
+ return ids.filter((id) => id === local).map((id) => ({ interactive_id: id, package: { revision_id: LOCAL_REVISION, package_digest: build.result.sourceDigest, chunk_base_url: CHUNK_ROUTE, chunks: sized } }));
2203
+ }
2204
+ function webHostAvailable(root) {
2205
+ const sdk = findPackageDir(root, SDK_PACKAGE);
2206
+ return sdk !== null && [WEB_HOST_MODULE, GUEST_WORKER_MODULE, RUNTIME_MODULE, CANVAS_GUEST_MODULE].every((module) => existsSync(path.join(sdk, module)));
2207
+ }
2208
+ function contentType(file) {
2209
+ const dot = file.lastIndexOf(".");
2210
+ return CONTENT_TYPES[dot >= 0 ? file.slice(dot) : ""] ?? "application/octet-stream";
2211
+ }
2212
+ function send(res, status, type, body) {
2213
+ res.writeHead(status, { "content-type": type, "cache-control": "no-store", ...HOST_PAGE_ISOLATION_HEADERS });
2214
+ res.end(body);
2215
+ }
2216
+ async function startDevServer(options) {
2217
+ let build = options.build ?? await buildLocal(options.root);
2218
+ const packagesRoot = options.packagesRoot ?? options.root;
2219
+ let modules = /* @__PURE__ */ new Map();
2220
+ let chunks = /* @__PURE__ */ new Map();
2221
+ const index = (b) => {
2222
+ const next = /* @__PURE__ */ new Map();
2223
+ for (const m of b.result.modules) next.set(m.output, m.code);
2224
+ modules = next;
2225
+ const bodies = /* @__PURE__ */ new Map();
2226
+ for (const c of b.result.chunks) bodies.set(c.digest, Buffer.concat(c.parts));
2227
+ chunks = bodies;
2228
+ };
2229
+ index(build);
2230
+ const storeDir = partStoreDir(options.partStore);
2231
+ const partStore = storeDir === null ? null : await openDevPartStore(storeDir, options.root);
2232
+ const faceParts = new FacePartCache(path.join(options.root, ...FACE_PARTS_DIR));
2233
+ const installParts = async () => {
2234
+ if (partStore !== null) await partStore.install([...chunks.keys()], chunks);
2235
+ };
2236
+ await installParts();
2237
+ let npm = createNpmResolver(packagesRoot);
2238
+ let host = webHostAvailable(packagesRoot);
2239
+ const clients = /* @__PURE__ */ new Set();
2240
+ const broadcast = (event, data) => {
2241
+ for (const c of clients) c.write(`event: ${event}
2242
+ data: ${data}
2243
+
2244
+ `);
2245
+ };
2246
+ const tick = options.tick === void 0 ? null : new TickDev(options.root, options.tick, (event) => broadcast(TICK_EVENT, JSON.stringify(event)));
2247
+ await tick?.sync(build);
2248
+ const rebuild = async () => {
2249
+ build = await buildLocal(options.root);
2250
+ index(build);
2251
+ await installParts();
2252
+ await tick?.sync(build);
2253
+ npm = createNpmResolver(packagesRoot);
2254
+ host = webHostAvailable(packagesRoot);
2255
+ const payload = payloadOf(build, host);
2256
+ if (!studio.quiet) {
2257
+ if (payload.ok) broadcast("reload", JSON.stringify(payload.digest));
2258
+ else broadcast("failed", JSON.stringify(payload.diagnostics));
2259
+ }
2260
+ options.onRebuild?.(build);
2261
+ return build;
2262
+ };
2263
+ const studio = new Studio({
2264
+ root: options.root,
2265
+ rebuild,
2266
+ model: options.authoringModel ?? (() => modelFromEnv()),
2267
+ remote: options.publishRemote ?? remoteFromEnv,
2268
+ keyDir: options.keyDir ?? signingKeyDir()
2269
+ });
2270
+ const ctxAi = options.ctxAi ?? devAiFromEnv(() => build.result.manifest?.ai ?? null);
2271
+ const ctxData = new LocalData({ root: options.root, environment: options.environment, declarations: () => build.result.manifest?.data ?? null });
2272
+ const record = options.record ?? null;
2273
+ const locale = options.locale ?? devLocale();
2274
+ const content = options.contentOrigin === void 0 ? null : await contentEdge(options.contentOrigin, packagesRoot, () => chunks, record?.prelude ?? "");
2275
+ const handle = async (req, res) => {
2276
+ if (content !== null && await content.serve(req, res)) return;
2277
+ const url = new URL(req.url ?? "/", "http://localhost");
2278
+ const route = url.pathname;
2279
+ if (options.next !== void 0 && route.startsWith(NEXT_ROUTE)) {
2280
+ const upstream = await fetch(`${options.next}${route.slice(NEXT_ROUTE.length - 1)}${url.search}`).catch(() => null);
2281
+ if (upstream === null) send(res, 502, "text/plain", "the next Interactive's dev server did not answer");
2282
+ else send(res, upstream.status, upstream.headers.get("content-type") ?? "application/octet-stream", Buffer.from(await upstream.arrayBuffer()));
2283
+ return;
2284
+ }
2285
+ if (await studio.serve(req, res, route)) return;
2286
+ if (route === DEV_DATA_ROUTE && req.method === "POST") {
2287
+ const call = await readCall(req);
2288
+ if (call === null) send(res, 400, "text/plain", "expected a ctx call");
2289
+ else send(res, 200, CONTENT_TYPES[".json"], JSON.stringify(await ctxData.answer(call)));
2290
+ return;
2291
+ }
2292
+ if (route === DEV_AI_ROUTE && req.method === "POST") {
2293
+ const call = await readCall(req);
2294
+ if (call === null) send(res, 400, "text/plain", "expected a ctx call");
2295
+ else send(res, 200, CONTENT_TYPES[".json"], JSON.stringify(await ctxAi.answer(call)));
2296
+ return;
2297
+ }
2298
+ if (route === "/" || route === "/index.html") {
2299
+ const engineSession = options.engineSessions ? await options.engineSessions.session() : null;
2300
+ send(res, 200, "text/html; charset=utf-8", simulatorPage(build.result.manifest?.identity.name ?? "Interactive", npm.importMap, options.contentOrigin ?? null, engineSession, record?.prelude ?? null, locale));
2301
+ return;
2302
+ }
2303
+ if (route === RECORD_ROUTE && req.method === "POST") {
2304
+ const inputs = record === null ? null : await readJson(req, MAX_RECORD_BYTES);
2305
+ if (record === null) send(res, 404, "text/plain", "not recording; run cimplify dev --record <file>");
2306
+ else if (!Array.isArray(inputs)) send(res, 400, "text/plain", "expected an array of recorded inputs");
2307
+ else {
2308
+ if (record.recorder.add(inputs) > 0) {
2309
+ await record.recorder.flush();
2310
+ record.onInput?.(record.recorder);
2311
+ }
2312
+ send(res, 204, "text/plain", "");
2313
+ }
2314
+ return;
2315
+ }
2316
+ if (route === BUILD_ROUTE) {
2317
+ send(res, 200, CONTENT_TYPES[".json"], JSON.stringify(payloadOf(build, host)));
2318
+ return;
2319
+ }
2320
+ if (route === FEED_PACKAGES_ROUTE && req.method === "GET") {
2321
+ const ids = (url.searchParams.get(FEED_IDS_PARAM) ?? "").split(FEED_ID_SEPARATOR).filter((id) => id.length > 0);
2322
+ send(res, 200, CONTENT_TYPES[".json"], JSON.stringify({ data: feedPackages(build, ids) }));
2323
+ return;
2324
+ }
2325
+ if (route.startsWith(CHUNK_ROUTE)) {
2326
+ const digest2 = route.slice(CHUNK_ROUTE.length);
2327
+ const stored = partStore === null ? null : await partStore.fetch(digest2).catch(() => null);
2328
+ const face = stored === null && faceParts.pinned(digest2) ? await faceParts.fetch(digest2).catch(() => null) : null;
2329
+ const body = stored !== null ? Buffer.from(stored.buffer, stored.byteOffset, stored.byteLength) : face ?? chunks.get(digest2);
2330
+ if (body === void 0) send(res, 404, "text/plain", "not found");
2331
+ else send(res, 200, "application/octet-stream", body);
2332
+ return;
2333
+ }
2334
+ if (route === PARTS_ROUTE) {
2335
+ if (partStore === null) send(res, 404, "text/plain", `no shared part store; set ${PART_STORE_ENV} to a directory`);
2336
+ else send(res, 200, CONTENT_TYPES[".json"], JSON.stringify(partStore.report()));
2337
+ return;
2338
+ }
2339
+ if (route === TICK_ROUTE && req.method === "POST") {
2340
+ const input = parseRunnerInput(await readJson(req, MAX_TICK_INPUT_BYTES));
2341
+ if (input === null) send(res, 400, "text/plain", "expected a tick runner input");
2342
+ else if (tick === null || !tick.send(input)) send(res, 409, "text/plain", "the tick room runner is not running");
2343
+ else send(res, 204, "text/plain", "");
2344
+ return;
2345
+ }
2346
+ if (route === EVENTS_ROUTE) {
2347
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive", ...HOST_PAGE_ISOLATION_HEADERS });
2348
+ res.write(": connected\n\n");
2349
+ clients.add(res);
2350
+ req.on("close", () => clients.delete(res));
2351
+ return;
2352
+ }
2353
+ if (route.startsWith(APP_ROUTE)) {
2354
+ const code = modules.get(decodeURIComponent(route.slice(APP_ROUTE.length)));
2355
+ if (code === void 0) send(res, 404, "text/plain", "not found");
2356
+ else send(res, 200, CONTENT_TYPES[".js"], code);
2357
+ return;
2358
+ }
2359
+ if (route.startsWith(NPM_ROUTE)) {
2360
+ const file = npm.file(route);
2361
+ if (file === null) {
2362
+ send(res, 404, "text/plain", "not found");
2363
+ return;
2364
+ }
2365
+ try {
2366
+ send(res, 200, contentType(file), await promises.readFile(file));
2367
+ } catch {
2368
+ send(res, 404, "text/plain", "not found");
2369
+ }
2370
+ return;
2371
+ }
2372
+ send(res, 404, "text/plain", "not found");
2373
+ };
2374
+ const server = createServer((req, res) => {
2375
+ handle(req, res).catch((err) => {
2376
+ if (!res.headersSent) send(res, 500, "text/plain", err.message);
2377
+ else if (!res.writableEnded) res.end();
2378
+ });
2379
+ });
2380
+ await new Promise((resolve, reject) => {
2381
+ server.once("error", reject);
2382
+ server.listen(options.port, options.host, () => resolve());
2383
+ });
2384
+ const port = server.address().port;
2385
+ const keepalive = setInterval(() => {
2386
+ for (const c of clients) c.write(": keepalive\n\n");
2387
+ }, KEEPALIVE_MS);
2388
+ keepalive.unref();
2389
+ let watcher = null;
2390
+ let timer = null;
2391
+ let running = null;
2392
+ let pending = false;
2393
+ const schedule = () => {
2394
+ if (timer !== null) clearTimeout(timer);
2395
+ timer = setTimeout(() => {
2396
+ timer = null;
2397
+ if (running !== null) {
2398
+ pending = true;
2399
+ return;
2400
+ }
2401
+ running = rebuild().finally(() => {
2402
+ running = null;
2403
+ if (pending) {
2404
+ pending = false;
2405
+ schedule();
2406
+ }
2407
+ });
2408
+ }, REBUILD_DEBOUNCE_MS);
2409
+ };
2410
+ if (options.watch) {
2411
+ watcher = watch(options.root, { recursive: true }, (_event, file) => {
2412
+ if (file === null) return;
2413
+ const first = String(file).split(/[\\/]/)[0];
2414
+ if (EXCLUDED_DIRECTORIES.has(first)) return;
2415
+ schedule();
2416
+ });
2417
+ }
2418
+ return {
2419
+ url: `http://${options.host === "0.0.0.0" ? "localhost" : options.host}:${port}`,
2420
+ port,
2421
+ current: () => build,
2422
+ rebuild,
2423
+ async close() {
2424
+ clearInterval(keepalive);
2425
+ if (timer !== null) clearTimeout(timer);
2426
+ watcher?.close();
2427
+ tick?.stop();
2428
+ ctxData.close();
2429
+ await record?.recorder.flush();
2430
+ for (const c of clients) c.end();
2431
+ clients.clear();
2432
+ await new Promise((resolve) => server.close(() => resolve()));
2433
+ await partStore?.store.flush();
2434
+ }
2435
+ };
2436
+ }
2437
+ var LOG_FILE = /^isolate-0x[0-9a-f]+-\d+-v8\.log$/;
2438
+ var CODE_CREATION = "code-creation";
2439
+ var BUILTIN = "Builtin";
2440
+ var WASM_FUNCTION = "wasm-function";
2441
+ var MICROS_PER_MS = 1e3;
2442
+ var FIELD_TYPE = 1;
2443
+ var FIELD_TIME = 3;
2444
+ var FIELD_SIZE = 5;
2445
+ var FIELD_NAME = 6;
2446
+ var CODE_LOG_FLAGS = "--log-code";
2447
+ function codeLogArgs(directory) {
2448
+ return [`--js-flags=${CODE_LOG_FLAGS} --logfile=${path.join(directory, "v8.log")}`];
2449
+ }
2450
+ function codeCreatedBetween(directory, startMs, endMs) {
2451
+ let isolates = 0;
2452
+ let bytes = 0;
2453
+ for (const name of readdirSync(directory)) {
2454
+ if (!LOG_FILE.test(name)) continue;
2455
+ const file = path.join(directory, name);
2456
+ const epoch = statSync(file).birthtimeMs;
2457
+ isolates++;
2458
+ for (const line of readFileSync(file, "utf8").split("\n")) {
2459
+ if (!line.startsWith(CODE_CREATION)) continue;
2460
+ const fields = line.split(",");
2461
+ if (fields[FIELD_TYPE] === BUILTIN || (fields[FIELD_NAME] ?? "").startsWith(WASM_FUNCTION)) continue;
2462
+ const at = epoch + Number(fields[FIELD_TIME]) / MICROS_PER_MS;
2463
+ if (at >= startMs && at < endMs) bytes += Number(fields[FIELD_SIZE]);
2464
+ }
2465
+ }
2466
+ return { isolates, bytes };
2467
+ }
2468
+
2469
+ // src/interactives/harness/canvas.ts
2470
+ var COLD_FIRST_FRAME_MS = 400;
2471
+ var STEP_TIMEOUT_MS = 3e4;
2472
+ var SETTLE_FRAMES = Number(process.env.CANVAS_SETTLE_FRAMES ?? 2400);
2473
+ var MEASURED_FRAMES = 600;
2474
+ var MEASURED_WINDOWS = 2;
2475
+ var REPORTED_SITES = 12;
2476
+ var SAMPLING_INTERVAL_BYTES = 16;
2477
+ var YOUNG_GARBAGE_BUDGET_BYTES = 0;
2478
+ var GATE_YOUNG_GARBAGE = "game and runtime code leave zero young-generation garbage across the window, because young garbage is what scavenges collect and a scavenge is a pause on the frame path (tracker garbage confirmed by the unperturbed sampler)";
2479
+ var GATE_NO_COLLECTIONS = "no garbage collection of any kind runs during the window, because any collection is a pause on the frame path";
2480
+ var GATE_JIT_SETTLES = "JIT code-space allocation, reported apart from young garbage because compiled code is not collected by scavenges, shrinks from one measured window to the next, so code that keeps recompiling still fails";
2481
+ var GC_EVENT_BUDGET = 0;
2482
+ var GATE_COLD_FIRST_FRAME = "the first launch of an Interactive, with an empty package and chunk cache and a fresh guest context, draws its first frame within 400 ms of the launch starting, measured after the host page has loaded and the browser and GPU have painted, because players always launch from an already running hub";
2483
+ var WARM_FIRST_FRAME_MS = 150;
2484
+ var GATE_WARM_FIRST_FRAME = "a launch after the first, once the host has staged and compiled the engine runtime in a pooled guest while the player browsed, draws its first frame within 150 ms of the launch starting";
2485
+ var BROWSE_MS = Number(process.env.CANVAS_BROWSE_MS ?? 1e3);
2486
+ var RELAUNCH_HOOK = "cimplifyRelaunch";
2487
+ var LAUNCH_HOOK = "cimplifyLaunch";
2488
+ var HOOK_POLL_MS = 20;
2489
+ var REDUCER_START_EVENT = "V8.GCIncrementalMarkingStart";
2490
+ var REDUCER_REASON = "memory reducer";
2491
+ var HUB_REDUCER_COLLECTIONS = 2;
2492
+ var HUB_SETTLE_LIMIT_MS = 32e3;
2493
+ var HUB_REDUCER_FOLLOW_UP_MS = 1e4;
2494
+ var HUB_TRACE_SLICE_MS = 2e3;
2495
+ var THREADED_NEEDS_LOGS = "the game runs worker threads, whose isolates block in Atomics.wait and stall memory dumps; launch the browser with codeLogArgs(dir) and pass that dir so code space is read from each isolate's V8 code log";
2496
+ var MIN_WINDOW_FRAME_SHARE = 0.9;
2497
+ var HOST_START_MARK = "cimplify:host-start";
2498
+ var MILESTONE_PREFIX = "cimplify:";
2499
+ var SRCDOC_URL = "about:srcdoc";
2500
+ var OUT_OF_PROCESS_SRCDOC_URL = "";
2501
+ var MODULE_URL_PREFIX = "blob:";
2502
+ var RUNTIME_SITE = "canvas runtime";
2503
+ var GAME_SITE = "module";
2504
+ var JIT_SPACES = ["code_space", "code_large_object_space", "trusted_space", "trusted_large_object_space"];
2505
+ var NEW_SPACES = ["new_space", "new_large_object_space"];
2506
+ var HEAP_ALLOCATOR = /^v8\/main\/heap\/([a-z_]+)$/;
2507
+ var GC_EVENTS = /* @__PURE__ */ new Set(["MinorGC", "MajorGC", "V8.GCScavenger", "V8.GCFinalizeMC", "V8.GCCompactor", "V8.GCIncrementalMarking"]);
2508
+ var TRACE_CATEGORIES = ["v8", "devtools.timeline", "disabled-by-default-v8.gc", "disabled-by-default-memory-infra"];
2509
+ var JS_FLAG_ARGS = process.env.CANVAS_JS_FLAGS ? [`--js-flags=${process.env.CANVAS_JS_FLAGS}`, "--single-process"] : [];
2510
+ var BROWSER_ARGS = ["--enable-unsafe-swiftshader", "--use-angle=swiftshader", "--ignore-gpu-blocklist", "--enable-unsafe-webgpu", ...JS_FLAG_ARGS];
2511
+ var HARDWARE_GPU_ARGS = [
2512
+ ...process.platform === "darwin" ? ["--use-angle=metal"] : [],
2513
+ "--enable-gpu",
2514
+ "--ignore-gpu-blocklist",
2515
+ "--enable-unsafe-webgpu",
2516
+ ...JS_FLAG_ARGS
2517
+ ];
2518
+ var SOFTWARE_RENDERERS = /SwiftShader|llvmpipe|softpipe|Software/i;
2519
+ var RENDERER_DOCUMENT = "<!doctype html><canvas></canvas>";
2520
+ async function softwareRenderer(page) {
2521
+ await page.setContent(RENDERER_DOCUMENT);
2522
+ const renderer = await page.evaluate(() => {
2523
+ const gl = document.querySelector("canvas")?.getContext("webgl2");
2524
+ if (gl === null || gl === void 0) return "";
2525
+ const info = gl.getExtension("WEBGL_debug_renderer_info");
2526
+ return String(gl.getParameter(info === null ? gl.RENDERER : info.UNMASKED_RENDERER_WEBGL));
2527
+ }, null);
2528
+ return renderer === "" || SOFTWARE_RENDERERS.test(renderer);
2529
+ }
2530
+ var DISPLAY_HZ = 120;
2531
+ var FRAME_INTERVAL_MS = 1e3 / DISPLAY_HZ;
2532
+ var WINDOW_MS = Math.round(MEASURED_FRAMES * FRAME_INTERVAL_MS);
2533
+ var SETTLE_MS = Math.round(SETTLE_FRAMES * FRAME_INTERVAL_MS);
2534
+ var ANIMATION_FRAME_EVENT = "FireAnimationFrame";
2535
+ var MICROS_PER_MS2 = 1e3;
2536
+ var MS_PER_MINUTE = 6e4;
2537
+ var P50 = 0.5;
2538
+ var P99 = 0.99;
2539
+ var PERCENT = 100;
2540
+ var WARM_DOCUMENT = `<!doctype html><canvas width="64" height="64"></canvas><iframe sandbox="allow-scripts" srcdoc="<script>requestAnimationFrame(()=>0)</script>"></iframe>`;
2541
+ var WARM_FRAMES = 2;
2542
+ function percentile(sorted, at) {
2543
+ return sorted.length === 0 ? 0 : sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * at))];
2544
+ }
2545
+ function busiestThread(events) {
2546
+ const byThread = /* @__PURE__ */ new Map();
2547
+ for (const event of events) {
2548
+ if (event.name !== ANIMATION_FRAME_EVENT || event.ph !== "X") continue;
2549
+ const key = `${event.pid}:${event.tid}`;
2550
+ const list = byThread.get(key) ?? [];
2551
+ list.push(event);
2552
+ byThread.set(key, list);
2553
+ }
2554
+ let busiest = [];
2555
+ for (const list of byThread.values()) if (list.length > busiest.length) busiest = list;
2556
+ return busiest.sort((a, b) => a.ts - b.ts);
2557
+ }
2558
+ function frameIntervals(frames) {
2559
+ const out = [];
2560
+ for (let i = 1; i < frames.length; i++) out.push((frames[i].ts - frames[i - 1].ts) / MICROS_PER_MS2);
2561
+ return out;
2562
+ }
2563
+ function siteOf(frame) {
2564
+ const owner = frame.url.startsWith(MODULE_URL_PREFIX) ? GAME_SITE : frame.url === SRCDOC_URL ? RUNTIME_SITE : null;
2565
+ if (owner === null) return null;
2566
+ return `${frame.functionName || "(anonymous)"} ${owner}:${frame.lineNumber}:${frame.columnNumber}`;
2567
+ }
2568
+ function ranked(sites) {
2569
+ return [...sites.entries()].filter(([, bytes]) => bytes > 0).map(([site, bytes]) => ({ site, bytes })).sort((a, b) => b.bytes - a.bytes);
2570
+ }
2571
+ function spaceTotals(events) {
2572
+ const dumps = [];
2573
+ const seen = /* @__PURE__ */ new Map();
2574
+ for (const event of events) {
2575
+ const allocators = event.args?.dumps?.allocators;
2576
+ if (event.ph !== "v" || allocators === void 0) continue;
2577
+ const spaces = /* @__PURE__ */ new Map();
2578
+ for (const [name, allocator] of Object.entries(allocators)) {
2579
+ const match = HEAP_ALLOCATOR.exec(name);
2580
+ const value = allocator.attrs?.allocated_objects_size?.value;
2581
+ if (match !== null && value !== void 0) spaces.set(match[1], Number.parseInt(value, 16));
2582
+ }
2583
+ if (spaces.size === 0) continue;
2584
+ const ordinal = seen.get(event.pid) ?? 0;
2585
+ seen.set(event.pid, ordinal + 1);
2586
+ dumps[ordinal] ??= /* @__PURE__ */ new Map();
2587
+ dumps[ordinal].set(event.pid, spaces);
2588
+ }
2589
+ return dumps;
2590
+ }
2591
+ function spaceDelta(dumps, names) {
2592
+ const [before, after] = dumps;
2593
+ if (before === void 0 || after === void 0) throw new Error("the memory dumps around the window are missing");
2594
+ let total = 0;
2595
+ for (const [pid, spaces] of after) {
2596
+ const start = before.get(pid);
2597
+ if (start === void 0) continue;
2598
+ for (const [name, size] of spaces) if (names === null || names.indexOf(name) >= 0) total += size - (start.get(name) ?? 0);
2599
+ }
2600
+ return total;
2601
+ }
2602
+ async function traced(cdp, body, dumps) {
2603
+ const events = [];
2604
+ const collect = (event) => {
2605
+ events.push(...event.value);
2606
+ };
2607
+ cdp.on("Tracing.dataCollected", collect);
2608
+ const complete = new Promise((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
2609
+ await cdp.send("Tracing.start", {
2610
+ traceConfig: { includedCategories: TRACE_CATEGORIES, memoryDumpConfig: { triggers: [] } },
2611
+ transferMode: "ReportEvents"
2612
+ });
2613
+ if (dumps) await cdp.send("Tracing.requestMemoryDump", { deterministic: false, levelOfDetail: "background" });
2614
+ const result = await body();
2615
+ if (dumps) await cdp.send("Tracing.requestMemoryDump", { deterministic: false, levelOfDetail: "background" });
2616
+ await cdp.send("Tracing.end");
2617
+ await complete;
2618
+ cdp.off("Tracing.dataCollected", collect);
2619
+ return { result, events };
2620
+ }
2621
+ var TRACE_NODE_FIELDS = 5;
2622
+ function traceAllocations(snap) {
2623
+ const meta = snap.snapshot.meta;
2624
+ const nodeFields = meta.node_fields.length;
2625
+ const sizeAt = meta.node_fields.indexOf("self_size");
2626
+ const traceAt = meta.node_fields.indexOf("trace_node_id");
2627
+ const fnFields = meta.trace_function_info_fields.length;
2628
+ const fnName = meta.trace_function_info_fields.indexOf("name");
2629
+ const fnScript = meta.trace_function_info_fields.indexOf("script_name");
2630
+ const fnLine = meta.trace_function_info_fields.indexOf("line");
2631
+ const fnColumn = meta.trace_function_info_fields.indexOf("column");
2632
+ const liveByTrace = /* @__PURE__ */ new Map();
2633
+ for (let i = 0; i < snap.nodes.length; i += nodeFields) {
2634
+ const trace = snap.nodes[i + traceAt];
2635
+ if (trace !== 0) liveByTrace.set(trace, (liveByTrace.get(trace) ?? 0) + snap.nodes[i + sizeAt]);
2636
+ }
2637
+ const garbage = /* @__PURE__ */ new Map();
2638
+ const live = /* @__PURE__ */ new Map();
2639
+ const visit = (tree) => {
2640
+ for (let i = 0; i < tree.length; i += TRACE_NODE_FIELDS) {
2641
+ const id = tree[i];
2642
+ const fn = tree[i + 1];
2643
+ const bytes = tree[i + 3];
2644
+ const site = siteOf({
2645
+ functionName: snap.strings[snap.trace_function_infos[fn * fnFields + fnName]] ?? "",
2646
+ url: snap.strings[snap.trace_function_infos[fn * fnFields + fnScript]] ?? "",
2647
+ lineNumber: snap.trace_function_infos[fn * fnFields + fnLine],
2648
+ columnNumber: snap.trace_function_infos[fn * fnFields + fnColumn]
2649
+ });
2650
+ if (site !== null && bytes !== 0) {
2651
+ const kept = liveByTrace.get(id) ?? 0;
2652
+ garbage.set(site, (garbage.get(site) ?? 0) + bytes - kept);
2653
+ live.set(site, (live.get(site) ?? 0) + kept);
2654
+ }
2655
+ visit(tree[i + 4]);
2656
+ }
2657
+ };
2658
+ visit(snap.trace_tree);
2659
+ return { garbage, live };
2660
+ }
2661
+ async function trackedSnapshot(cdp) {
2662
+ const chunks = [];
2663
+ const collect = (event) => {
2664
+ chunks.push(event.chunk);
2665
+ };
2666
+ cdp.on("HeapProfiler.addHeapSnapshotChunk", collect);
2667
+ await cdp.send("HeapProfiler.stopTrackingHeapObjects", { reportProgress: false, treatGlobalObjectsAsRoots: true });
2668
+ cdp.off("HeapProfiler.addHeapSnapshotChunk", collect);
2669
+ return JSON.parse(chunks.join(""));
2670
+ }
2671
+ function sampledSites(head) {
2672
+ const out = /* @__PURE__ */ new Map();
2673
+ const walk = (node) => {
2674
+ const site = node.selfSize > 0 ? siteOf(node.callFrame) : null;
2675
+ if (site !== null) out.set(site, (out.get(site) ?? 0) + node.selfSize);
2676
+ for (const child of node.children) walk(child);
2677
+ };
2678
+ walk(head);
2679
+ return out;
2680
+ }
2681
+ async function isolateWindow(cdp, codeLogs, body) {
2682
+ const before = await cdp.send("Runtime.getHeapUsage");
2683
+ const started = Date.now();
2684
+ const { events } = await traced(cdp, body, false);
2685
+ const ended = Date.now();
2686
+ const after = await cdp.send("Runtime.getHeapUsage");
2687
+ return { events, heap: after.usedSize - before.usedSize, jit: codeCreatedBetween(codeLogs, started, ended).bytes };
2688
+ }
2689
+ async function pacedWindow(cdp, frame, codeLogs) {
2690
+ await cdp.send("HeapProfiler.startSampling", {
2691
+ samplingInterval: SAMPLING_INTERVAL_BYTES,
2692
+ includeObjectsCollectedByMajorGC: true,
2693
+ includeObjectsCollectedByMinorGC: true
2694
+ });
2695
+ const wait = () => frame.page().waitForTimeout(WINDOW_MS);
2696
+ const isolates = codeLogs === null ? null : await isolateWindow(cdp, codeLogs, wait);
2697
+ const events = isolates?.events ?? (await traced(cdp, wait, true)).events;
2698
+ const frames = busiestThread(events);
2699
+ const intervals = frameIntervals(frames).sort((a, b) => a - b);
2700
+ const work = frames.map((event) => (event.dur ?? 0) / MICROS_PER_MS2).sort((a, b) => a - b);
2701
+ const collections = events.filter((event) => GC_EVENTS.has(event.name) && (event.ph === "X" || event.ph === "B"));
2702
+ const profile = await cdp.send("HeapProfiler.stopSampling");
2703
+ if (isolates !== null) {
2704
+ return {
2705
+ frames: intervals.length,
2706
+ frame_ms_p50: Number(percentile(intervals, P50).toFixed(2)),
2707
+ frame_ms_p99: Number(percentile(intervals, P99).toFixed(2)),
2708
+ work_ms_p99: Number(percentile(work, P99).toFixed(3)),
2709
+ gc_events: collections.length,
2710
+ gc_pause_ms_max: collections.reduce((longest, event) => Math.max(longest, (event.dur ?? 0) / MICROS_PER_MS2), 0),
2711
+ heap_growth_bytes: isolates.heap,
2712
+ jit_code_bytes: isolates.jit,
2713
+ new_space_bytes_all_sources: 0,
2714
+ sampled: sampledSites(profile.profile.head),
2715
+ measured_by: "isolate_logs"
2716
+ };
2717
+ }
2718
+ const dumps = spaceTotals(events);
2719
+ return {
2720
+ frames: intervals.length,
2721
+ frame_ms_p50: Number(percentile(intervals, P50).toFixed(2)),
2722
+ frame_ms_p99: Number(percentile(intervals, P99).toFixed(2)),
2723
+ work_ms_p99: Number(percentile(work, P99).toFixed(3)),
2724
+ gc_events: collections.length,
2725
+ gc_pause_ms_max: collections.reduce((longest, event) => Math.max(longest, (event.dur ?? 0) / MICROS_PER_MS2), 0),
2726
+ heap_growth_bytes: spaceDelta(dumps, null),
2727
+ jit_code_bytes: spaceDelta(dumps, JIT_SPACES),
2728
+ new_space_bytes_all_sources: spaceDelta(dumps, NEW_SPACES),
2729
+ sampled: sampledSites(profile.profile.head),
2730
+ measured_by: "memory_dumps"
2731
+ };
2732
+ }
2733
+ async function garbageWindow(cdp, frame) {
2734
+ await cdp.send("HeapProfiler.startTrackingHeapObjects", { trackAllocations: true });
2735
+ await frame.page().waitForTimeout(WINDOW_MS);
2736
+ const { garbage, live } = traceAllocations(await trackedSnapshot(cdp));
2737
+ return { young_garbage_sites: ranked(garbage).slice(0, REPORTED_SITES), retained_sites: ranked(live).slice(0, REPORTED_SITES) };
2738
+ }
2739
+ function bytesAt(sites, marker) {
2740
+ return sites.filter((a) => a.site.includes(marker)).reduce((sum, a) => sum + a.bytes, 0);
2741
+ }
2742
+ function combine(measured, tracked) {
2743
+ const { sampled, ...paced } = measured;
2744
+ const confirmed = tracked.young_garbage_sites.filter((a) => (sampled.get(a.site) ?? 0) > 0);
2745
+ return {
2746
+ ...paced,
2747
+ game_young_garbage_bytes: bytesAt(confirmed, ` ${GAME_SITE}:`),
2748
+ runtime_young_garbage_bytes: bytesAt(confirmed, RUNTIME_SITE),
2749
+ young_garbage_sites: confirmed,
2750
+ tracker_only_sites: tracked.young_garbage_sites.filter((a) => (sampled.get(a.site) ?? 0) === 0),
2751
+ retained_sites: tracked.retained_sites
2752
+ };
2753
+ }
2754
+ async function canvasFrame(page) {
2755
+ const deadline = Date.now() + STEP_TIMEOUT_MS;
2756
+ for (; ; ) {
2757
+ const frame = page.frames().find((f) => f.url() === SRCDOC_URL || f !== page.mainFrame() && f.url() === OUT_OF_PROCESS_SRCDOC_URL);
2758
+ if (frame !== void 0) return frame;
2759
+ if (Date.now() > deadline) throw new Error(`no canvas frame appeared; errors: ${await page.locator("#errors").textContent()}`);
2760
+ await page.waitForTimeout(5);
2761
+ }
2762
+ }
2763
+ async function firstFrame(page, url) {
2764
+ await page.goto(`${url}?${HOLD_PARAM}`);
2765
+ await page.waitForFunction(
2766
+ (hook) => typeof globalThis[hook] === "function",
2767
+ LAUNCH_HOOK,
2768
+ { polling: HOOK_POLL_MS, timeout: STEP_TIMEOUT_MS }
2769
+ );
2770
+ await hubSettled(page);
2771
+ await page.evaluate((hook) => {
2772
+ void globalThis[hook]();
2773
+ }, LAUNCH_HOOK);
2774
+ return drawnSinceHostStart(page);
2775
+ }
2776
+ function reducerCollections(events) {
2777
+ return events.filter((event) => event.name === REDUCER_START_EVENT && event.args?.value?.gc_reason === REDUCER_REASON).length;
2778
+ }
2779
+ async function hubSettled(page) {
2780
+ const cdp = await page.context().newCDPSession(page);
2781
+ let deadline = Date.now() + HUB_SETTLE_LIMIT_MS;
2782
+ let seen = 0;
2783
+ try {
2784
+ while (seen < HUB_REDUCER_COLLECTIONS && Date.now() < deadline) {
2785
+ const { events } = await traced(cdp, () => page.waitForTimeout(HUB_TRACE_SLICE_MS), false);
2786
+ const before = seen;
2787
+ seen += reducerCollections(events);
2788
+ if (before === 0 && seen > 0) deadline = Math.min(deadline, Date.now() + HUB_REDUCER_FOLLOW_UP_MS);
2789
+ }
2790
+ } finally {
2791
+ await cdp.detach();
2792
+ }
2793
+ }
2794
+ async function warmFirstFrame(page) {
2795
+ const staged = await page.evaluate(async (relaunch) => {
2796
+ const run = globalThis[relaunch];
2797
+ return run === void 0 ? null : run();
2798
+ }, RELAUNCH_HOOK);
2799
+ if (staged === null) throw new Error("the host page cannot relaunch the Interactive");
2800
+ return { first_frame_ms: await drawnSinceHostStart(page), runtime_staged: staged };
2801
+ }
2802
+ async function drawnSinceHostStart(page) {
2803
+ const frame = await canvasFrame(page);
2804
+ await frame.waitForFunction(
2805
+ () => {
2806
+ const canvas = document.querySelector("canvas");
2807
+ return canvas !== null && (!canvas.hasAttribute("data-cimplify-engine") || canvas.hasAttribute("data-cimplify-frame"));
2808
+ },
2809
+ void 0,
2810
+ { polling: "raf", timeout: STEP_TIMEOUT_MS }
2811
+ );
2812
+ const drawnAt = await frame.evaluate(() => performance.timeOrigin + performance.now());
2813
+ const startedAt = await page.evaluate((mark) => {
2814
+ const entry = performance.getEntriesByName(mark)[0];
2815
+ return entry === void 0 ? Number.NaN : performance.timeOrigin + entry.startTime;
2816
+ }, HOST_START_MARK);
2817
+ return Math.round(drawnAt - startedAt);
2818
+ }
2819
+ function drawnFrames(frame, count) {
2820
+ return frame.evaluate(
2821
+ (frames) => new Promise((resolve) => {
2822
+ let left = frames;
2823
+ const tick = () => {
2824
+ if (--left <= 0) resolve();
2825
+ else requestAnimationFrame(tick);
2826
+ };
2827
+ requestAnimationFrame(tick);
2828
+ }),
2829
+ count
2830
+ );
2831
+ }
2832
+ async function measureLaunches(page, url) {
2833
+ const first_frame_ms = await firstFrame(page, url);
2834
+ await page.waitForTimeout(BROWSE_MS);
2835
+ return { first_frame_ms, warm: await warmFirstFrame(page) };
2836
+ }
2837
+ async function measureCanvas(page, url, template, codeLogs = null, measured = null) {
2838
+ const launched = await measureLaunches(page, url);
2839
+ const { first_frame_ms, warm } = measured ?? launched;
2840
+ const software_launches = codeLogs === null ? launched : null;
2841
+ const frame = await canvasFrame(page);
2842
+ const isolated = await frame.evaluate(() => globalThis.crossOriginIsolated === true);
2843
+ const milestones = await page.evaluate(
2844
+ ([start, prefix]) => {
2845
+ const origin = performance.getEntriesByName(start)[0]?.startTime ?? 0;
2846
+ const out = {};
2847
+ for (const entry of performance.getEntriesByType("mark")) {
2848
+ if (entry.name.startsWith(prefix)) out[entry.name.slice(prefix.length)] = Math.round(entry.startTime - origin);
2849
+ }
2850
+ return out;
2851
+ },
2852
+ [HOST_START_MARK, MILESTONE_PREFIX]
2853
+ );
2854
+ await Promise.all([page.waitForTimeout(SETTLE_MS), drawnFrames(frame, SETTLE_FRAMES)]);
2855
+ const cdp = await page.context().newCDPSession(page);
2856
+ await cdp.send("HeapProfiler.enable");
2857
+ const windows = [];
2858
+ const paced = [];
2859
+ const threaded = page.workers().length > 0;
2860
+ if (threaded && codeLogs === null) throw new Error(THREADED_NEEDS_LOGS);
2861
+ for (let i = 0; i < MEASURED_WINDOWS; i++) paced.push(await pacedWindow(cdp, frame, threaded ? codeLogs : null));
2862
+ for (let i = 0; i < MEASURED_WINDOWS; i++) windows.push(combine(paced[i], await garbageWindow(cdp, frame)));
2863
+ await cdp.detach();
2864
+ return { template, first_frame_ms, warm, milestones_ms: milestones, cross_origin_isolated: isolated, windows, software_launches };
2865
+ }
2866
+ function jitComparable(first, second) {
2867
+ const minimum = Math.floor(MEASURED_FRAMES * MIN_WINDOW_FRAME_SHARE);
2868
+ return first.frames >= minimum && second.frames >= minimum;
2869
+ }
2870
+ function gateMisses(report) {
2871
+ const misses = [];
2872
+ if (report.first_frame_ms > COLD_FIRST_FRAME_MS) misses.push(`${GATE_COLD_FIRST_FRAME} failed: ${report.first_frame_ms} ms`);
2873
+ if (!report.warm.runtime_staged) misses.push(`${GATE_WARM_FIRST_FRAME} failed: the host had no warm guest for the runtime when the player launched again`);
2874
+ else if (report.warm.first_frame_ms > WARM_FIRST_FRAME_MS) misses.push(`${GATE_WARM_FIRST_FRAME} failed: ${report.warm.first_frame_ms} ms`);
2875
+ if (!report.cross_origin_isolated) misses.push("the canvas is not cross-origin isolated");
2876
+ report.windows.forEach((w, i) => {
2877
+ const young = w.game_young_garbage_bytes + w.runtime_young_garbage_bytes;
2878
+ if (young > YOUNG_GARBAGE_BUDGET_BYTES) {
2879
+ misses.push(`${GATE_YOUNG_GARBAGE} failed in window ${i + 1}: ${young} bytes from ${JSON.stringify(w.young_garbage_sites)}`);
2880
+ }
2881
+ if (w.gc_events > GC_EVENT_BUDGET) misses.push(`${GATE_NO_COLLECTIONS} failed in window ${i + 1}: ${w.gc_events} collection events`);
2882
+ });
2883
+ const [first, second] = report.windows;
2884
+ if (first !== void 0 && second !== void 0 && jitComparable(first, second) && second.jit_code_bytes > 0 && second.jit_code_bytes >= first.jit_code_bytes) {
2885
+ misses.push(`${GATE_JIT_SETTLES} failed: ${first.jit_code_bytes} then ${second.jit_code_bytes} bytes of code-space allocation`);
2886
+ }
2887
+ return misses;
2888
+ }
2889
+ function gateNotices(report) {
2890
+ const [first, second] = report.windows;
2891
+ if (first === void 0 || second === void 0 || jitComparable(first, second)) return [];
2892
+ return [
2893
+ `the JIT gate was skipped: the windows drew ${first.frames} and ${second.frames} of ${MEASURED_FRAMES} frames, under ${Math.round(MIN_WINDOW_FRAME_SHARE * PERCENT)}%, so the machine was too loaded to compare them`
2894
+ ];
2895
+ }
2896
+ function canvasMeasurements(report) {
2897
+ let work = 0;
2898
+ let pause = 0;
2899
+ let growth = 0;
2900
+ for (const w of report.windows) {
2901
+ work = Math.max(work, w.work_ms_p99);
2902
+ pause = Math.max(pause, w.gc_pause_ms_max);
2903
+ growth = Math.max(growth, w.heap_growth_bytes);
2904
+ }
2905
+ return {
2906
+ frame_micros_p99: Math.round(work * MICROS_PER_MS2),
2907
+ gc_pause_micros_p99: Math.round(pause * MICROS_PER_MS2),
2908
+ heap_growth_bytes_per_minute: Math.round(growth * MS_PER_MINUTE / WINDOW_MS)
2909
+ };
2910
+ }
2911
+ async function warmBrowser(browser) {
2912
+ const page = await browser.newPage();
2913
+ await page.setContent(WARM_DOCUMENT);
2914
+ await page.evaluate(
2915
+ (frames) => new Promise((resolve) => {
2916
+ const gl = document.querySelector("canvas")?.getContext("webgl2") ?? null;
2917
+ let left = frames;
2918
+ const paint = () => {
2919
+ gl?.clearColor(0, 0, 0, 1);
2920
+ gl?.clear(gl.COLOR_BUFFER_BIT);
2921
+ if (--left <= 0) resolve();
2922
+ else requestAnimationFrame(paint);
2923
+ };
2924
+ requestAnimationFrame(paint);
2925
+ }),
2926
+ WARM_FRAMES
2927
+ );
2928
+ await page.close();
2929
+ }
2930
+ var PLAYWRIGHT_PACKAGE = "playwright";
2931
+ var PINNED_PLAYWRIGHT = "1.62.1";
2932
+ var load = createRequire(import.meta.url);
2933
+ function locateChromium(bases) {
2934
+ for (const base of bases) {
2935
+ const dir = findPackageDir(base, PLAYWRIGHT_PACKAGE);
2936
+ if (dir === null) continue;
2937
+ const version = readPackageJson(dir).version;
2938
+ if (version !== PINNED_PLAYWRIGHT) return `the build service runs Playwright ${version ?? "unknown"}; the pinned Playwright is ${PINNED_PLAYWRIGHT}`;
2939
+ const loaded = load(dir);
2940
+ if (loaded.chromium === void 0) return `${dir} does not export chromium`;
2941
+ return loaded.chromium;
2942
+ }
2943
+ return `Playwright ${PINNED_PLAYWRIGHT} with Chromium is not installed with the build service, so engine games cannot be measured`;
2944
+ }
2945
+
2946
+ export { BROWSER_ARGS, EMPTY_READINESS, HARDWARE_GPU_ARGS, HOLD_PARAM, SILENT_PARAM, buildReport, canvasMeasurements, codeLogArgs, devLocale, gateMisses, gateNotices, locateChromium, measureCanvas, measureLaunches, probeScript, softwareRenderer, startDevServer, warmBrowser, writeBuildOutput, writeModuleTree };