rnxsim 0.1.433 → 0.1.434

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 (59) hide show
  1. package/cli/cloud-client.ts +55 -13
  2. package/cli/commands/box.ts +89 -14
  3. package/cli/outbound-endpoints.ts +5 -0
  4. package/cli/send-to-box.ts +112 -0
  5. package/dist-lib/agent-daemon-client.cjs +1 -1
  6. package/dist-lib/agent-events.cjs +1 -1
  7. package/dist-lib/agent-identity.cjs +1 -1
  8. package/dist-lib/agent-sessions.cjs +1 -1
  9. package/dist-lib/attached-projects.cjs +1 -1
  10. package/dist-lib/auth/shared-session.cjs +1 -1
  11. package/dist-lib/backend-origin.cjs +1 -1
  12. package/dist-lib/beta.cjs +1 -1
  13. package/dist-lib/beta.mjs +1 -1
  14. package/dist-lib/bridge-constants.cjs +1 -1
  15. package/dist-lib/bridge-contract-input.cjs +1 -1
  16. package/dist-lib/bridge-contract-input.mjs +1 -1
  17. package/dist-lib/bridge-contract.cjs +1 -1
  18. package/dist-lib/bridge-contract.mjs +1 -1
  19. package/dist-lib/capture-contract.cjs +1 -1
  20. package/dist-lib/capture-contract.mjs +1 -1
  21. package/dist-lib/cli-constants.cjs +1 -1
  22. package/dist-lib/cloud-contract.cjs +1 -1
  23. package/dist-lib/cloud-contract.mjs +1 -1
  24. package/dist-lib/cloud.cjs +1 -1
  25. package/dist-lib/cloud.mjs +1 -1
  26. package/dist-lib/config.cjs +1 -1
  27. package/dist-lib/detox/index.cjs +1 -1
  28. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  29. package/dist-lib/home-paths.cjs +1 -1
  30. package/dist-lib/host/bridge-host.cjs +5324 -1371
  31. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  32. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  33. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  34. package/dist-lib/host/replacement-module-handler.cjs +298 -91
  35. package/dist-lib/host/websocket-proxy.cjs +1 -1
  36. package/dist-lib/index.cjs +313 -104
  37. package/dist-lib/jump-to-source-babel.cjs +1 -1
  38. package/dist-lib/menu.cjs +7 -1
  39. package/dist-lib/menu.mjs +7 -1
  40. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  41. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  42. package/dist-lib/metro-production-bundle.cjs +1 -1
  43. package/dist-lib/metro-production-bundle.mjs +1 -1
  44. package/dist-lib/metro.cjs +302 -95
  45. package/dist-lib/profiles.cjs +1 -1
  46. package/dist-lib/public-brand.cjs +1 -1
  47. package/dist-lib/react-native-host-modules.cjs +1 -1
  48. package/dist-lib/react-native-host-modules.mjs +1 -1
  49. package/dist-lib/render-mode.cjs +1 -1
  50. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  51. package/dist-lib/sdk.cjs +1 -1
  52. package/dist-lib/sdk.mjs +1 -1
  53. package/dist-lib/skills.cjs +5006 -1441
  54. package/dist-lib/vite.cjs +298 -91
  55. package/package.json +2 -2
  56. package/src/bridge-contract.ts +6 -0
  57. package/src/host/bridge-host.ts +79 -0
  58. package/src/host/replacement-module-handler.ts +20 -96
  59. package/src/menu.ts +8 -0
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -189,7 +189,287 @@ function isDaemonLockfileFresh(lock, now = Date.now()) {
189
189
  // src/host/replacement-module-handler.ts
190
190
  var import_promises = __toESM(require("node:fs/promises"), 1);
191
191
  var import_node_path3 = __toESM(require("node:path"), 1);
192
- var import_esbuild = require("esbuild");
192
+
193
+ // ../contrast-bundler/src/replacementModule.ts
194
+ var import_dist = require("sucrase/dist/index.js");
195
+ var import_parser = require("sucrase/dist/parser/index.js");
196
+ var import_types = require("sucrase/dist/parser/tokenizer/types.js");
197
+
198
+ // ../contrast-bundler/src/posixPath.ts
199
+ var sep = "/";
200
+ var delimiter = ":";
201
+ function isAbsolute(p) {
202
+ return p.charCodeAt(0) === 47;
203
+ }
204
+ function normalizeSegments(parts, allowAboveRoot) {
205
+ const out = [];
206
+ for (const part of parts) {
207
+ if (part === "" || part === ".") continue;
208
+ if (part === "..") {
209
+ if (out.length > 0 && out[out.length - 1] !== "..") out.pop();
210
+ else if (allowAboveRoot) out.push("..");
211
+ continue;
212
+ }
213
+ out.push(part);
214
+ }
215
+ return out;
216
+ }
217
+ function normalize(p) {
218
+ if (p.length === 0) return ".";
219
+ const absolute = isAbsolute(p);
220
+ const trailingSlash = p.charCodeAt(p.length - 1) === 47;
221
+ const segments = normalizeSegments(p.split("/"), !absolute);
222
+ let joined = segments.join("/");
223
+ if (joined.length === 0) {
224
+ if (absolute) return "/";
225
+ return trailingSlash ? "./" : ".";
226
+ }
227
+ if (trailingSlash) joined += "/";
228
+ return absolute ? "/" + joined : joined;
229
+ }
230
+ function join(...parts) {
231
+ const present = parts.filter((part) => part.length > 0);
232
+ if (present.length === 0) return ".";
233
+ return normalize(present.join("/"));
234
+ }
235
+ function resolve(...parts) {
236
+ let resolved = "";
237
+ let absolute = false;
238
+ for (let i = parts.length - 1; i >= 0 && !absolute; i--) {
239
+ const part = parts[i];
240
+ if (part.length === 0) continue;
241
+ resolved = resolved.length > 0 ? `${part}/${resolved}` : part;
242
+ absolute = isAbsolute(part);
243
+ }
244
+ if (!absolute)
245
+ throw new Error(
246
+ `posixPath.resolve needs an absolute segment, got: ${parts.join(", ")}`
247
+ );
248
+ const normalized = normalizeSegments(resolved.split("/"), false).join("/");
249
+ return normalized.length > 0 ? "/" + normalized : "/";
250
+ }
251
+ function dirname(p) {
252
+ const idx = p.lastIndexOf("/");
253
+ if (idx < 0) return ".";
254
+ if (idx === 0) return "/";
255
+ return p.slice(0, idx);
256
+ }
257
+ function basename(p, ext) {
258
+ const idx = p.lastIndexOf("/");
259
+ const base = idx < 0 ? p : p.slice(idx + 1);
260
+ if (ext != null && ext.length < base.length && base.endsWith(ext)) {
261
+ return base.slice(0, base.length - ext.length);
262
+ }
263
+ return base;
264
+ }
265
+ function extname(p) {
266
+ const base = basename(p);
267
+ const idx = base.lastIndexOf(".");
268
+ if (idx <= 0) return "";
269
+ return base.slice(idx);
270
+ }
271
+ function relative(from, to) {
272
+ if (from === to) return "";
273
+ const fromParts = normalizeSegments(from.split("/"), false);
274
+ const toParts = normalizeSegments(to.split("/"), false);
275
+ let shared = 0;
276
+ while (shared < fromParts.length && shared < toParts.length && fromParts[shared] === toParts[shared]) {
277
+ shared++;
278
+ }
279
+ const up = new Array(fromParts.length - shared).fill("..");
280
+ return [...up, ...toParts.slice(shared)].join("/");
281
+ }
282
+ var posix = {
283
+ sep,
284
+ delimiter,
285
+ isAbsolute,
286
+ normalize,
287
+ join,
288
+ resolve,
289
+ dirname,
290
+ basename,
291
+ extname,
292
+ relative
293
+ };
294
+ var posixPath_default = { ...posix, posix };
295
+
296
+ // ../contrast-bundler/src/replacementModule.ts
297
+ var REPLACEMENT_MODULE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
298
+ var IMPORT_KEYWORD = import_types.TokenType["_import"];
299
+ var EXPORT_KEYWORD = import_types.TokenType["_export"];
300
+ var REACT_BRIDGE = "globalThis.__sootsim_react_bridge";
301
+ var REACT_NATIVE_BRIDGE = "globalThis.__sootsim_components__";
302
+ var PACKAGE_BRIDGES = "globalThis.__sootsim_config_package_modules";
303
+ var JSX_RUNTIME_SPECIFIERS = /* @__PURE__ */ new Set(["react/jsx-runtime", "react/jsx-dev-runtime"]);
304
+ function transformsFor(filePath) {
305
+ switch (extname(filePath).toLowerCase()) {
306
+ case ".ts":
307
+ return ["typescript"];
308
+ case ".tsx":
309
+ return ["typescript", "jsx"];
310
+ case ".js":
311
+ case ".jsx":
312
+ return ["jsx"];
313
+ default:
314
+ throw new Error(
315
+ `replacement files must be ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")}`
316
+ );
317
+ }
318
+ }
319
+ function unsupportedSpecifier(specifier) {
320
+ return new Error(
321
+ `replacement modules may only import react, react-native, and the packages rnx stubs for this app; '${specifier}' is none of those. vendor the code into the replacement file, or add the package to rnx.config so the simulator bridges it.`
322
+ );
323
+ }
324
+ function bridgeDeclaration(name, specifier) {
325
+ const notReady = (what) => JSON.stringify(`[rnx] replacement ${what} bridge is not ready`);
326
+ if (specifier === "react") {
327
+ return `const ${name} = ${REACT_BRIDGE}; if (!${name}) throw new Error(${notReady("React")});`;
328
+ }
329
+ if (specifier === "react-native") {
330
+ return `const ${name} = ${REACT_NATIVE_BRIDGE}; if (!${name}) throw new Error(${notReady("React Native")});`;
331
+ }
332
+ if (JSX_RUNTIME_SPECIFIERS.has(specifier)) {
333
+ return `const ${name} = (() => { const react = ${REACT_BRIDGE}; if (!react) throw new Error(${notReady("React")}); const jsx = (type, props, key) => react.createElement(type, key === undefined ? props : { ...props, key }); return { Fragment: react.Fragment, jsx, jsxs: jsx, jsxDEV: jsx }; })();`;
334
+ }
335
+ return `const ${name} = (${PACKAGE_BRIDGES} || {})[${JSON.stringify(specifier)}]; if (!${name}) throw new Error(${JSON.stringify(`[rnx] replacement package bridge ${specifier} is not ready`)});`;
336
+ }
337
+ var DEFAULT_INTEROP = "__rnxReplacementDefault";
338
+ var DEFAULT_INTEROP_DECLARATION = `const ${DEFAULT_INTEROP} = (bridge) => (bridge && bridge.__esModule ? bridge.default : bridge);`;
339
+ function compileReplacementModule(input) {
340
+ const transformed = (0, import_dist.transform)(input.source, {
341
+ transforms: transformsFor(input.filePath),
342
+ filePath: input.filePath,
343
+ // classic runtime, matching what the app's own Metro build does for a file
344
+ // that has no automatic-runtime pragma: the file imports React itself.
345
+ jsxRuntime: "classic",
346
+ jsxPragma: "React.createElement",
347
+ jsxFragmentPragma: "React.Fragment",
348
+ production: true,
349
+ sourceMapOptions: { compiledFilename: input.filePath }
350
+ });
351
+ const code = transformed.code;
352
+ const sourceMap = transformed.sourceMap;
353
+ if (!sourceMap) throw new Error("sucrase compiled the replacement with no source map");
354
+ const { tokens } = (0, import_parser.parse)(code, false, false, false);
355
+ const read = (token) => code.slice(token.start, token.end);
356
+ const readSpecifier = (token) => {
357
+ if (token.type !== import_types.TokenType.string) {
358
+ throw new Error("replacement module import has no string specifier");
359
+ }
360
+ return code.slice(token.start + 1, token.end - 1);
361
+ };
362
+ const bridgeNames = /* @__PURE__ */ new Map();
363
+ const declarations = [];
364
+ let needsDefaultInterop = false;
365
+ const bridgeFor = (specifier) => {
366
+ const existing = bridgeNames.get(specifier);
367
+ if (existing) return existing;
368
+ const name = `__rnxReplacementBridge${bridgeNames.size}`;
369
+ bridgeNames.set(specifier, name);
370
+ declarations.push(bridgeDeclaration(name, specifier));
371
+ return name;
372
+ };
373
+ const edits = [];
374
+ for (let index = 0; index < tokens.length; index++) {
375
+ const token = tokens[index];
376
+ if (token.type === EXPORT_KEYWORD) {
377
+ const after2 = tokens[index + 1];
378
+ let fromAt = -1;
379
+ if (after2?.type === import_types.TokenType.star) {
380
+ fromAt = read(tokens[index + 2]) === "from" ? index + 2 : index + 4;
381
+ } else if (after2?.type === import_types.TokenType.braceL) {
382
+ let depth = 1;
383
+ let cursor2 = index + 2;
384
+ while (cursor2 < tokens.length && depth > 0) {
385
+ if (tokens[cursor2].type === import_types.TokenType.braceL) depth++;
386
+ else if (tokens[cursor2].type === import_types.TokenType.braceR) depth--;
387
+ cursor2++;
388
+ }
389
+ fromAt = cursor2;
390
+ }
391
+ if (fromAt > 0 && tokens[fromAt] && read(tokens[fromAt]) === "from" && tokens[fromAt + 1]?.type === import_types.TokenType.string) {
392
+ const specifier2 = readSpecifier(tokens[fromAt + 1]);
393
+ throw new Error(
394
+ `a replacement module cannot re-export from another module, and this one re-exports from '${specifier2}'. import what you need and export the binding.`
395
+ );
396
+ }
397
+ continue;
398
+ }
399
+ if (token.type !== IMPORT_KEYWORD) continue;
400
+ const after = tokens[index + 1];
401
+ if (!after || after.type === import_types.TokenType.parenL || after.type === import_types.TokenType.dot) continue;
402
+ const bindings = [];
403
+ let cursor = index + 1;
404
+ if (after.type !== import_types.TokenType.string) {
405
+ while (cursor < tokens.length) {
406
+ const clause = tokens[cursor];
407
+ if (clause.type === import_types.TokenType.name && read(clause) === "from") break;
408
+ if (clause.type === import_types.TokenType.comma) {
409
+ cursor++;
410
+ continue;
411
+ }
412
+ if (clause.type === import_types.TokenType.star) {
413
+ bindings.push({ local: read(tokens[cursor + 2]), imported: null });
414
+ cursor += 3;
415
+ continue;
416
+ }
417
+ if (clause.type === import_types.TokenType.braceL) {
418
+ cursor++;
419
+ while (cursor < tokens.length && tokens[cursor].type !== import_types.TokenType.braceR) {
420
+ if (tokens[cursor].type === import_types.TokenType.comma) {
421
+ cursor++;
422
+ continue;
423
+ }
424
+ const nameToken = tokens[cursor];
425
+ const imported = nameToken.type === import_types.TokenType.string ? readSpecifier(nameToken) : read(nameToken);
426
+ let local = imported;
427
+ if (tokens[cursor + 1] && read(tokens[cursor + 1]) === "as") {
428
+ local = read(tokens[cursor + 2]);
429
+ cursor += 2;
430
+ }
431
+ bindings.push({ local, imported });
432
+ cursor++;
433
+ }
434
+ cursor++;
435
+ continue;
436
+ }
437
+ bindings.push({ local: read(clause), imported: "default" });
438
+ cursor++;
439
+ }
440
+ cursor++;
441
+ }
442
+ const specifierToken = tokens[cursor];
443
+ if (!specifierToken) throw new Error("replacement module import is unterminated");
444
+ const specifier = readSpecifier(specifierToken);
445
+ if (specifier !== "react" && specifier !== "react-native" && !JSX_RUNTIME_SPECIFIERS.has(specifier) && !input.runtimePackages.has(specifier)) {
446
+ throw unsupportedSpecifier(specifier);
447
+ }
448
+ const bridge = bindings.length > 0 ? bridgeFor(specifier) : null;
449
+ const replacement = bindings.map((binding) => {
450
+ if (binding.imported === null) return `const ${binding.local} = ${bridge};`;
451
+ if (binding.imported === "default") {
452
+ needsDefaultInterop = true;
453
+ return `const ${binding.local} = ${DEFAULT_INTEROP}(${bridge});`;
454
+ }
455
+ return `const ${binding.local} = ${bridge}[${JSON.stringify(binding.imported)}];`;
456
+ }).join(" ");
457
+ const start = token.start;
458
+ const end = specifierToken.end;
459
+ const newlines = code.slice(start, end).split("\n").length - 1;
460
+ edits.push({ start, end, text: replacement + "\n".repeat(newlines) });
461
+ }
462
+ let out = code;
463
+ for (const edit of edits.reverse()) {
464
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
465
+ }
466
+ if (needsDefaultInterop) declarations.unshift(DEFAULT_INTEROP_DECLARATION);
467
+ const preamble = declarations.join(" ");
468
+ return {
469
+ code: preamble ? `${preamble} ${out}` : out,
470
+ sourceMap: { ...sourceMap, sourcesContent: [input.source] }
471
+ };
472
+ }
193
473
 
194
474
  // src/backend-origin.ts
195
475
  function normalizeHostname(hostname) {
@@ -202,21 +482,6 @@ function isLoopbackHost(hostname) {
202
482
 
203
483
  // src/host/replacement-module-handler.ts
204
484
  var REPLACEMENT_MODULE_ROUTE = "/__sootsim-replacement-module";
205
- function getReplacementLoader(filePath) {
206
- const ext = import_node_path3.default.extname(filePath).toLowerCase();
207
- switch (ext) {
208
- case ".js":
209
- return "js";
210
- case ".jsx":
211
- return "jsx";
212
- case ".ts":
213
- return "ts";
214
- case ".tsx":
215
- return "tsx";
216
- default:
217
- return null;
218
- }
219
- }
220
485
  function sendJavaScript(req, res, source) {
221
486
  const origin = req.headers.origin;
222
487
  let allowedOrigin;
@@ -270,10 +535,10 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
270
535
  res.end("missing path param");
271
536
  return true;
272
537
  }
273
- const loader = getReplacementLoader(targetPath);
274
- if (!loader) {
538
+ const extension = import_node_path3.default.extname(targetPath).toLowerCase();
539
+ if (!REPLACEMENT_MODULE_EXTENSIONS.some((allowed) => allowed === extension)) {
275
540
  res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
276
- res.end("replacement files must be .js, .jsx, .ts, or .tsx");
541
+ res.end(`replacement files must be ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")}`);
277
542
  return true;
278
543
  }
279
544
  if (!import_node_path3.default.isAbsolute(targetPath)) {
@@ -311,81 +576,23 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
311
576
  }
312
577
  }
313
578
  try {
314
- const result = await (0, import_esbuild.build)({
315
- entryPoints: [resolvedTargetPath],
316
- bundle: true,
317
- write: false,
318
- format: "esm",
319
- platform: "browser",
320
- target: "es2022",
321
- jsx: "transform",
322
- jsxFactory: "React.createElement",
323
- jsxFragment: "React.Fragment",
324
- tsconfigRaw: { compilerOptions: { jsx: "react" } },
325
- sourcemap: "inline",
326
- logLevel: "silent",
327
- plugins: [
328
- {
329
- name: "sootsim-replacement-runtime",
330
- setup(runtimeBuild) {
331
- runtimeBuild.onResolve(
332
- {
333
- filter: /^(react|react-native|react\/jsx-runtime|react\/jsx-dev-runtime)$/
334
- },
335
- (args) => ({ path: args.path, namespace: "sootsim-replacement-runtime" })
336
- );
337
- runtimeBuild.onResolve(
338
- { filter: /.*/ },
339
- (args) => runtimePackages.has(args.path) ? { path: args.path, namespace: "sootsim-replacement-runtime" } : void 0
340
- );
341
- runtimeBuild.onLoad(
342
- { filter: /.*/, namespace: "sootsim-replacement-runtime" },
343
- (args) => {
344
- if (args.path === "react-native") {
345
- return {
346
- contents: `const bridge = globalThis.__sootsim_components__
347
- if (!bridge) throw new Error('[rnx] replacement React Native bridge is not ready')
348
- module.exports = bridge`,
349
- loader: "js"
350
- };
351
- }
352
- if (args.path === "react") {
353
- return {
354
- contents: `const bridge = globalThis.__sootsim_react_bridge
355
- if (!bridge) throw new Error('[rnx] replacement React bridge is not ready')
356
- module.exports = bridge`,
357
- loader: "js"
358
- };
359
- }
360
- if (args.path !== "react/jsx-runtime" && args.path !== "react/jsx-dev-runtime") {
361
- return {
362
- contents: `const packages = globalThis.__sootsim_config_package_modules
363
- const bridge = packages?.[${JSON.stringify(args.path)}]
364
- if (!bridge) throw new Error(${JSON.stringify(`[rnx] replacement package bridge ${args.path} is not ready`)})
365
- module.exports = bridge`,
366
- loader: "js"
367
- };
368
- }
369
- return {
370
- contents: `const React = globalThis.__sootsim_react_bridge
371
- if (!React) throw new Error('[rnx] replacement React bridge is not ready')
372
- exports.Fragment = React.Fragment
373
- exports.jsx = (type, props, key) => React.createElement(type, key === undefined ? props : { ...props, key })
374
- exports.jsxs = exports.jsx
375
- exports.jsxDEV = exports.jsx`,
376
- loader: "js"
377
- };
378
- }
379
- );
380
- }
381
- }
382
- ]
579
+ const compiled = compileReplacementModule({
580
+ filePath: resolvedTargetPath,
581
+ source: await import_promises.default.readFile(resolvedTargetPath, "utf8"),
582
+ runtimePackages
383
583
  });
384
- sendJavaScript(req, res, result.outputFiles[0]?.text ?? "");
584
+ const map = Buffer.from(JSON.stringify(compiled.sourceMap), "utf8").toString("base64");
585
+ sendJavaScript(
586
+ req,
587
+ res,
588
+ `${compiled.code}
589
+ //# sourceMappingURL=data:application/json;base64,${map}
590
+ `
591
+ );
385
592
  } catch (error) {
386
593
  const message = error instanceof Error ? error.message : String(error);
387
594
  res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
388
- res.end(`replacement bundle failed: ${message}`);
595
+ res.end(`replacement compile failed: ${message}`);
389
596
  }
390
597
  return true;
391
598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.433",
3
+ "version": "0.1.434",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -231,9 +231,9 @@
231
231
  },
232
232
  "dependencies": {
233
233
  "@expo/plist": "^0.5.4",
234
- "esbuild": "^0.25.0",
235
234
  "jiti": "2.7.0",
236
235
  "playwright-core": "1.61.0",
236
+ "sucrase": "3.35.1",
237
237
  "terser": "^5.49.0",
238
238
  "ws": "^8.18.0"
239
239
  },
@@ -38,6 +38,7 @@ export type BridgeCommandType =
38
38
  | 'reset'
39
39
  | 'camera'
40
40
  | 'diagnostics'
41
+ | 'storageSnapshot'
41
42
 
42
43
  export interface WsCommand {
43
44
  id: string | number
@@ -90,6 +91,11 @@ export interface WsCommand {
90
91
  // diagnostics — read-only engine debug state: channel recording, the sheet /
91
92
  // portal / boundary finders, and the recent event ring.
92
93
  diagnostics?: DiagnosticsOptions
94
+ // storageSnapshot — the running app's own storage and route, as
95
+ // `StorageSnapshot` from the engine's preview/storage-snapshot module. the
96
+ // default result is share-safe; `includeCredentials` carries the signed-in
97
+ // session and belongs only to a send to the owner's own box.
98
+ storageSnapshotOptions?: { includeCredentials?: boolean }
93
99
  }
94
100
 
95
101
  // ---------------------------------------------------------------------------
@@ -33,6 +33,7 @@ import {
33
33
  type DevBridgeLockfile,
34
34
  type SharedConfig,
35
35
  } from '../home-paths.ts'
36
+ import { rnxPublicBrand } from '../public-brand.ts'
36
37
  import {
37
38
  isRuntimeVersionHostname,
38
39
  resolveDevCheckoutRuntimeRoot,
@@ -1651,6 +1652,82 @@ export class SootSimBridgeHost {
1651
1652
  return
1652
1653
  }
1653
1654
 
1655
+ // /__send-to-box — the rail button. the shell cannot create a cloud box
1656
+ // itself: the account credential lives on this machine, not in the page.
1657
+ // it sits above the read-only guard below because it is a POST.
1658
+ if (url.pathname === '/__send-to-box') {
1659
+ res.setHeader('Access-Control-Allow-Origin', '*')
1660
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
1661
+ res.setHeader('Cache-Control', 'no-store')
1662
+ if (method === 'OPTIONS') {
1663
+ res.writeHead(204, { Allow: 'POST, OPTIONS' })
1664
+ res.end()
1665
+ return
1666
+ }
1667
+ if (method !== 'POST') {
1668
+ res.writeHead(405, { Allow: 'POST, OPTIONS' })
1669
+ res.end('method not allowed')
1670
+ return
1671
+ }
1672
+ void (async () => {
1673
+ try {
1674
+ const chunks: Buffer[] = []
1675
+ for await (const chunk of req) {
1676
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
1677
+ }
1678
+ const text = Buffer.concat(chunks).toString('utf8').trim()
1679
+ const parsed: unknown = text ? JSON.parse(text) : {}
1680
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1681
+ throw new Error('send to box needs a json object body')
1682
+ }
1683
+ const body: Record<string, unknown> = { ...parsed }
1684
+ const { CHOOSE_ACCOUNT_HINT, resolveCliAuth, authHeaderValue } =
1685
+ await import('../../cli/auth.ts')
1686
+ const auth = resolveCliAuth()
1687
+ if (!auth) {
1688
+ res.writeHead(401, { 'Content-Type': 'application/json' })
1689
+ res.end(
1690
+ JSON.stringify({
1691
+ error: `send to box needs an account: run \`${rnxPublicBrand.commandName} login\` in this checkout`,
1692
+ }),
1693
+ )
1694
+ return
1695
+ }
1696
+ // the CLI would exit the process here; a daemon serving the rail
1697
+ // answers the button instead.
1698
+ const accountId = auth.kind === 'session' ? auth.accountId : null
1699
+ if (auth.kind === 'session' && accountId === null) {
1700
+ res.writeHead(400, { 'Content-Type': 'application/json' })
1701
+ res.end(JSON.stringify({ error: CHOOSE_ACCOUNT_HINT }))
1702
+ return
1703
+ }
1704
+ const { sendToBox } = await import('../../cli/send-to-box.ts')
1705
+ const result = await sendToBox({
1706
+ send: (command) =>
1707
+ this.sendCommand({
1708
+ ...command,
1709
+ simId: typeof command.simId === 'string' ? command.simId : undefined,
1710
+ }),
1711
+ authorization: authHeaderValue(auth),
1712
+ accountId,
1713
+ carryAuth: body.carryAuth !== false,
1714
+ boxName: typeof body.boxName === 'string' ? body.boxName : null,
1715
+ simId: typeof body.simId === 'string' ? body.simId : undefined,
1716
+ })
1717
+ res.writeHead(200, { 'Content-Type': 'application/json' })
1718
+ res.end(JSON.stringify(result))
1719
+ } catch (error) {
1720
+ res.writeHead(500, { 'Content-Type': 'application/json' })
1721
+ res.end(
1722
+ JSON.stringify({
1723
+ error: error instanceof Error ? error.message : String(error),
1724
+ }),
1725
+ )
1726
+ }
1727
+ })()
1728
+ return
1729
+ }
1730
+
1654
1731
  // /__camera-fixtures/<name> — the staged video behind `rnx camera play`.
1655
1732
  // the shell page is a different origin from this daemon (vite dev on 5173,
1656
1733
  // daemon on 7668+), and the camera pipeline draws the <video> to a canvas
@@ -2568,6 +2645,8 @@ export class SootSimBridgeHost {
2568
2645
  return msg?.action === 'type' ? 'typed text' : 'used keyboard'
2569
2646
  case 'tree':
2570
2647
  return 'dumped tree'
2648
+ case 'storageSnapshot':
2649
+ return 'dumped storage snapshot'
2571
2650
  case 'query':
2572
2651
  return 'queried semantic tree'
2573
2652
  case 'resolve':