sootsim 0.1.239 → 0.1.241

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 (146) hide show
  1. package/dist-cli/bin.js +4 -4
  2. package/dist-cli/chunks/{agent-5KVGVFXJ.js → agent-7Z3WG3YJ.js} +2 -2
  3. package/dist-cli/chunks/{agent-wrapper-YVXMSV3X.js → agent-wrapper-IMTGEJ7T.js} +2 -2
  4. package/dist-cli/chunks/{app-fonts-IXF6YPQZ.js → app-fonts-7E66PSAV.js} +2 -2
  5. package/dist-cli/chunks/{assert-FMNM5VTT.js → assert-IEBAQZC7.js} +2 -2
  6. package/dist-cli/chunks/{auth-LNSDYZU4.js → auth-FRMHPA2X.js} +2 -2
  7. package/dist-cli/chunks/beta-AMMCLKHD.js +2 -0
  8. package/dist-cli/chunks/{camera-INMAD3CG.js → camera-O67TQUAH.js} +2 -2
  9. package/dist-cli/chunks/{chunk-WXYRA447.js → chunk-2DY3JHFK.js} +1 -1
  10. package/dist-cli/chunks/{chunk-ZL2KSYJP.js → chunk-4JSCYLLH.js} +1 -1
  11. package/dist-cli/chunks/{chunk-HV2ZRFGP.js → chunk-4V6MFWDY.js} +1 -1
  12. package/dist-cli/chunks/{chunk-BWKVAEAJ.js → chunk-5COOJUTE.js} +1 -1
  13. package/dist-cli/chunks/{chunk-XWPQVHOL.js → chunk-5ZNYFTOB.js} +2 -2
  14. package/dist-cli/chunks/{chunk-QHJLYAOH.js → chunk-7I3PV2RE.js} +2 -2
  15. package/dist-cli/chunks/{chunk-JBUMYCFR.js → chunk-AROVTS3P.js} +2 -2
  16. package/dist-cli/chunks/{chunk-LWE27HF7.js → chunk-BIHEU677.js} +3 -3
  17. package/dist-cli/chunks/{chunk-UZRZGXQ4.js → chunk-CAOW2O5V.js} +2 -2
  18. package/dist-cli/chunks/{chunk-SOZIIFPY.js → chunk-CSZOZRKB.js} +3 -3
  19. package/dist-cli/chunks/{chunk-VIM3WDAA.js → chunk-DS2KFLBO.js} +1 -1
  20. package/dist-cli/chunks/chunk-ECRSTV5C.js +118 -0
  21. package/dist-cli/chunks/{chunk-NJQSCTFC.js → chunk-EY3ZQWUQ.js} +52 -52
  22. package/dist-cli/chunks/{chunk-544G2PCM.js → chunk-F5YD4BLA.js} +2 -2
  23. package/dist-cli/chunks/{chunk-X7JZ4KTU.js → chunk-FR7JCFAR.js} +1 -1
  24. package/dist-cli/chunks/{chunk-YIS2RXPE.js → chunk-GFZLDVLE.js} +1 -1
  25. package/dist-cli/chunks/{chunk-PVIUYYIS.js → chunk-GVTSKLLS.js} +2 -2
  26. package/dist-cli/chunks/{chunk-KCS6OYQV.js → chunk-H7G2QGUG.js} +2 -2
  27. package/dist-cli/chunks/{chunk-NDU5EJFW.js → chunk-H7SP6J3H.js} +2 -2
  28. package/dist-cli/chunks/{chunk-CJRALJVV.js → chunk-HNI6U6M5.js} +1 -1
  29. package/dist-cli/chunks/{chunk-P5JIE7R6.js → chunk-IF5VXL45.js} +2 -2
  30. package/dist-cli/chunks/{chunk-BKDHODNB.js → chunk-JMTQBHQY.js} +1 -1
  31. package/dist-cli/chunks/{chunk-CUDTQFGP.js → chunk-JX4N376W.js} +2 -2
  32. package/dist-cli/chunks/{chunk-FOTAUF7Q.js → chunk-KFI26HW5.js} +1 -1
  33. package/dist-cli/chunks/chunk-KLXLOMI6.js +1 -0
  34. package/dist-cli/chunks/{chunk-JPNTLGLA.js → chunk-KN3Q6VSC.js} +2 -2
  35. package/dist-cli/chunks/{chunk-JOSDTQZ3.js → chunk-LVUOVKJO.js} +2 -2
  36. package/dist-cli/chunks/{chunk-VBO7OQLR.js → chunk-MCAJBAWD.js} +2 -2
  37. package/dist-cli/chunks/{chunk-R5I2OBQM.js → chunk-MGLTIS7P.js} +2 -2
  38. package/dist-cli/chunks/{chunk-SLIYVKRX.js → chunk-PR6AOTP5.js} +1 -1
  39. package/dist-cli/chunks/{chunk-5VCOEPSJ.js → chunk-PVUUG5YW.js} +2 -2
  40. package/dist-cli/chunks/{chunk-EQYRXSBE.js → chunk-R3EZVIEM.js} +2 -2
  41. package/dist-cli/chunks/{chunk-HE5NBBLH.js → chunk-U7XJNNGI.js} +3 -3
  42. package/dist-cli/chunks/{chunk-ECPEYFHV.js → chunk-UF333CGY.js} +2 -2
  43. package/dist-cli/chunks/{chunk-QYVGMN3I.js → chunk-VVUANJFA.js} +72 -23
  44. package/dist-cli/chunks/{chunk-SBSL3DUQ.js → chunk-WFB3ICCN.js} +1 -1
  45. package/dist-cli/chunks/{chunk-6XF2EHL2.js → chunk-WIHKSZM4.js} +1 -1
  46. package/dist-cli/chunks/{chunk-MDOTB5DP.js → chunk-WPWLJFKF.js} +2 -2
  47. package/dist-cli/chunks/{chunk-GI2S43VF.js → chunk-YLWUYESW.js} +1 -1
  48. package/dist-cli/chunks/{chunk-D4YH5KNQ.js → chunk-YX7S6N42.js} +2 -2
  49. package/dist-cli/chunks/{chunk-44WCYVFQ.js → chunk-Z72IF5FC.js} +2 -2
  50. package/dist-cli/chunks/chunk-ZGSGMGE3.js +1 -0
  51. package/dist-cli/chunks/{chunk-SRMYUHBI.js → chunk-ZUZLQ4ZJ.js} +1 -1
  52. package/dist-cli/chunks/{chunk-DJIWF2SR.js → chunk-ZYJCVJFP.js} +1 -1
  53. package/dist-cli/chunks/cli-version-DIYZFJMN.js +2 -0
  54. package/dist-cli/chunks/{compat-VYOIJUW6.js → compat-ZRUVBIK7.js} +3 -3
  55. package/dist-cli/chunks/{config-ZJIIQF6K.js → config-VKQDFC5Z.js} +2 -2
  56. package/dist-cli/chunks/{control-KFAL3LD7.js → control-OYK2J4JW.js} +2 -2
  57. package/dist-cli/chunks/daemon-YQLY7PVM.js +2 -0
  58. package/dist-cli/chunks/{debug-RCY6BSLW.js → debug-3WEO7ZRW.js} +3 -3
  59. package/dist-cli/chunks/{desktop-FRZILY3U.js → desktop-DVOFHJKU.js} +3 -3
  60. package/dist-cli/chunks/{detox-W2HOZRDP.js → detox-GAGC276D.js} +2 -2
  61. package/dist-cli/chunks/{device-WRWT7SEE.js → device-256KFP2G.js} +2 -2
  62. package/dist-cli/chunks/{diagnose-3KSX2NJC.js → diagnose-GYKUU3N5.js} +2 -2
  63. package/dist-cli/chunks/drivers-FHWFHVQF.js +2 -0
  64. package/dist-cli/chunks/{film-QN7NUW6Z.js → film-WAV3WPQK.js} +3 -3
  65. package/dist-cli/chunks/flow-WKPPIDM6.js +2 -0
  66. package/dist-cli/chunks/help-P4A7NNSC.js +2 -0
  67. package/dist-cli/chunks/{hidden-runtime-alias-NIH62HCH.js → hidden-runtime-alias-DODMMNOJ.js} +2 -2
  68. package/dist-cli/chunks/{home-paths-3CPTCZDU.js → home-paths-QU5UX3MT.js} +2 -2
  69. package/dist-cli/chunks/inspect-NIHY67NU.js +2 -0
  70. package/dist-cli/chunks/install-desktop-NA53KD6G.js +2 -0
  71. package/dist-cli/chunks/{login-HQ3356B6.js → login-OURQ3DVM.js} +4 -4
  72. package/dist-cli/chunks/{logout-LVVXQS5S.js → logout-ZYVJAPBO.js} +2 -2
  73. package/dist-cli/chunks/{maestro-34Y7GOG5.js → maestro-H2NGXNF4.js} +3 -3
  74. package/dist-cli/chunks/{maestro-generate-XBIZ7Z6U.js → maestro-generate-DA2IZPK2.js} +3 -3
  75. package/dist-cli/chunks/{mode-VXZR6I46.js → mode-QTK3ERUW.js} +2 -2
  76. package/dist-cli/chunks/optional-demo-registry-ZLN23OYV.js +2 -0
  77. package/dist-cli/chunks/{perf-IBHKDDFN.js → perf-WZ4OPYXV.js} +2 -2
  78. package/dist-cli/chunks/{record-S5WYHPNR.js → record-ZQSUHQJU.js} +3 -3
  79. package/dist-cli/chunks/runtime-RM67M6H3.js +2 -0
  80. package/dist-cli/chunks/{screenshot-command-T65M2SYC.js → screenshot-command-UDJ6TEAM.js} +3 -3
  81. package/dist-cli/chunks/{screenshots-capture-75AHIYWA.js → screenshots-capture-4FK4NYXG.js} +2 -2
  82. package/dist-cli/chunks/serve-MCT4HKR7.js +41 -0
  83. package/dist-cli/chunks/{setup-TLQ2VM3F.js → setup-I2IELNJN.js} +3 -3
  84. package/dist-cli/chunks/{skills-LZZDB6V5.js → skills-PVXI7LM6.js} +2 -2
  85. package/dist-cli/chunks/{storage-G2IZ2WAD.js → storage-KZFCVRGD.js} +2 -2
  86. package/dist-cli/chunks/store-CEMC5MTK.js +2 -0
  87. package/dist-cli/chunks/telemetry-LFIQWPZD.js +2 -0
  88. package/dist-cli/chunks/{timeline-L4CFSXX3.js → timeline-YAULXFEC.js} +2 -2
  89. package/dist-cli/chunks/{upgrade-5USALUAU.js → upgrade-63TVREBM.js} +2 -2
  90. package/dist-cli/chunks/upload-APW46WK3.js +2 -0
  91. package/dist-cli/chunks/{version-RQZPUNJO.js → version-JWWEGZS7.js} +2 -2
  92. package/dist-cli/chunks/{web-YQ6XAB5F.js → web-XO724HJL.js} +2 -2
  93. package/dist-cli/chunks/{what-happened-JLAM7TBD.js → what-happened-DQ4MXAPJ.js} +2 -2
  94. package/dist-lib/agent-daemon-client.cjs +1 -1
  95. package/dist-lib/agent-events.cjs +1 -1
  96. package/dist-lib/agent-identity.cjs +1 -1
  97. package/dist-lib/agent-sessions.cjs +1 -1
  98. package/dist-lib/attached-projects.cjs +1 -1
  99. package/dist-lib/auth/shared-session.cjs +1 -1
  100. package/dist-lib/backend-origin.cjs +1 -1
  101. package/dist-lib/beta.cjs +1 -1
  102. package/dist-lib/beta.mjs +1 -1
  103. package/dist-lib/bridge-constants.cjs +1 -1
  104. package/dist-lib/cli-constants.cjs +1 -1
  105. package/dist-lib/config.cjs +1 -1
  106. package/dist-lib/detox/index.cjs +1 -1
  107. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  108. package/dist-lib/home-paths.cjs +1 -1
  109. package/dist-lib/host/bridge-host.cjs +77 -33
  110. package/dist-lib/host/fetch-proxy-handler.cjs +77 -33
  111. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  112. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  113. package/dist-lib/host/websocket-proxy.cjs +1 -1
  114. package/dist-lib/index.cjs +1 -1
  115. package/dist-lib/jump-to-source-babel.cjs +1 -1
  116. package/dist-lib/menu.cjs +1 -1
  117. package/dist-lib/menu.mjs +1 -1
  118. package/dist-lib/metro.cjs +1 -1
  119. package/dist-lib/profiles.cjs +1 -1
  120. package/dist-lib/render-mode.cjs +1 -1
  121. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  122. package/dist-lib/sdk.cjs +157 -5
  123. package/dist-lib/sdk.mjs +156 -5
  124. package/dist-lib/skills.cjs +221 -37
  125. package/dist-lib/vite.cjs +1 -1
  126. package/package.json +1 -1
  127. package/skills/contrast/SKILL.md +1 -1
  128. package/skills/sootsim-debug/SKILL.md +21 -12
  129. package/src/host/fetch-proxy-handler.ts +104 -52
  130. package/dist-cli/chunks/beta-BH6RPXQU.js +0 -2
  131. package/dist-cli/chunks/chunk-HJW6SASW.js +0 -118
  132. package/dist-cli/chunks/chunk-J7SEKMNI.js +0 -1
  133. package/dist-cli/chunks/chunk-NO6RQXYN.js +0 -1
  134. package/dist-cli/chunks/cli-version-7KCROGJR.js +0 -2
  135. package/dist-cli/chunks/daemon-TJXAVVUE.js +0 -2
  136. package/dist-cli/chunks/drivers-SJOWPOA3.js +0 -2
  137. package/dist-cli/chunks/flow-Q4XCAPIT.js +0 -2
  138. package/dist-cli/chunks/help-FRVJH7SB.js +0 -2
  139. package/dist-cli/chunks/inspect-5ZZN4UJN.js +0 -2
  140. package/dist-cli/chunks/install-desktop-MIY6DLN6.js +0 -2
  141. package/dist-cli/chunks/optional-demo-registry-5JSIUIMF.js +0 -2
  142. package/dist-cli/chunks/runtime-5K5JFYOZ.js +0 -2
  143. package/dist-cli/chunks/serve-OJVA6PNV.js +0 -41
  144. package/dist-cli/chunks/store-X6HDJ5SE.js +0 -2
  145. package/dist-cli/chunks/telemetry-AHMOJS6L.js +0 -2
  146. package/dist-cli/chunks/upload-OWMPT3Z7.js +0 -2
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.239 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (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;
@@ -12894,12 +12894,6 @@ function formatFetchProxyError(targetUrl, err) {
12894
12894
  }
12895
12895
  return message;
12896
12896
  }
12897
- function isTextProxyContentType(contentType) {
12898
- const value = Array.isArray(contentType) ? contentType[0] : contentType;
12899
- const normalized = value?.split(";", 1)[0]?.trim().toLowerCase();
12900
- if (!normalized) return false;
12901
- return normalized.startsWith("text/") || normalized === "application/json" || normalized.endsWith("+json") || normalized === "application/x-www-form-urlencoded" || normalized === "application/xml" || normalized.endsWith("+xml") || normalized === "application/javascript" || normalized === "application/graphql";
12902
- }
12903
12897
  function buildFetchProxyHeaders(reqHeaders, targetUrl) {
12904
12898
  const headers = {};
12905
12899
  for (const [key, value] of Object.entries(reqHeaders)) {
@@ -12958,7 +12952,6 @@ async function handleFetchProxyRequest(req, res) {
12958
12952
  res.end("invalid url param");
12959
12953
  return;
12960
12954
  }
12961
- const headers = buildFetchProxyHeaders(req.headers, upstreamUrl);
12962
12955
  let body;
12963
12956
  if (req.method !== "GET" && req.method !== "HEAD") {
12964
12957
  const chunks = [];
@@ -12966,39 +12959,89 @@ async function handleFetchProxyRequest(req, res) {
12966
12959
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
12967
12960
  }
12968
12961
  if (chunks.length > 0) {
12969
- const bytes = Buffer.concat(chunks);
12970
- body = isTextProxyContentType(req.headers["content-type"]) ? bytes.toString("utf8") : bytes;
12962
+ body = Buffer.concat(chunks);
12971
12963
  }
12972
12964
  }
12973
- let upstream;
12974
- try {
12975
- upstream = await fetch(upstreamUrl.href, {
12976
- method: req.method,
12977
- headers,
12978
- body,
12979
- redirect: "follow"
12965
+ const originalMethod = req.method || "GET";
12966
+ const proxy = async (targetUrl2, method, nextBody, redirects) => {
12967
+ const transport = targetUrl2.protocol === "https:" ? import_https.default : import_http2.default;
12968
+ const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl2);
12969
+ if (targetUrl2.origin !== upstreamUrl.origin) {
12970
+ delete targetHeaders.authorization;
12971
+ delete targetHeaders["proxy-authorization"];
12972
+ }
12973
+ if (method === "GET" || method === "HEAD") {
12974
+ delete targetHeaders["content-length"];
12975
+ delete targetHeaders["content-type"];
12976
+ } else if (nextBody) {
12977
+ targetHeaders["content-length"] = String(nextBody.byteLength);
12978
+ }
12979
+ const proxyRes = await new Promise((resolve5, reject) => {
12980
+ const proxyReq = transport.request(
12981
+ {
12982
+ hostname: targetUrl2.hostname,
12983
+ port: targetUrl2.port || (targetUrl2.protocol === "https:" ? 443 : 80),
12984
+ path: targetUrl2.pathname + targetUrl2.search,
12985
+ method,
12986
+ headers: targetHeaders
12987
+ },
12988
+ resolve5
12989
+ );
12990
+ const abort = () => proxyReq.destroy(new Error("fetch proxy client disconnected"));
12991
+ req.once("aborted", abort);
12992
+ proxyReq.once("close", () => req.off("aborted", abort));
12993
+ proxyReq.once("error", reject);
12994
+ if (nextBody === void 0) proxyReq.end();
12995
+ else proxyReq.end(nextBody);
12980
12996
  });
12997
+ const status = proxyRes.statusCode ?? 502;
12998
+ const location = proxyRes.headers.location;
12999
+ if (location && (status === 301 || status === 302 || status === 303 || status === 307 || status === 308)) {
13000
+ if (redirects >= 10) {
13001
+ proxyRes.destroy();
13002
+ throw new Error("too many redirects");
13003
+ }
13004
+ const switchToGet = (status === 301 || status === 302) && method === "POST" || status === 303 && method !== "GET" && method !== "HEAD";
13005
+ const redirectUrl = new URL(location, targetUrl2);
13006
+ proxyRes.resume();
13007
+ await (0, import_promises2.finished)(proxyRes);
13008
+ await proxy(
13009
+ redirectUrl,
13010
+ switchToGet ? "GET" : method,
13011
+ switchToGet ? void 0 : nextBody,
13012
+ redirects + 1
13013
+ );
13014
+ return;
13015
+ }
13016
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
13017
+ const lowerKey = key.toLowerCase();
13018
+ if (value === void 0 || lowerKey === "set-cookie" || lowerKey === "connection" || lowerKey === "keep-alive" || lowerKey === "proxy-authenticate" || lowerKey === "proxy-authorization" || lowerKey === "te" || lowerKey === "trailer" || lowerKey === "transfer-encoding" || lowerKey === "upgrade" || lowerKey.startsWith("access-control-")) {
13019
+ continue;
13020
+ }
13021
+ res.setHeader(key, value);
13022
+ }
13023
+ applyFetchProxyCors(res);
13024
+ const setCookie = proxyRes.headers["set-cookie"] ?? [];
13025
+ if (setCookie.length > 0) {
13026
+ res.setHeader(
13027
+ "x-sootsim-set-cookie",
13028
+ (Array.isArray(setCookie) ? setCookie : [setCookie]).join(", ")
13029
+ );
13030
+ }
13031
+ res.statusCode = status;
13032
+ await (0, import_promises2.pipeline)(proxyRes, res);
13033
+ };
13034
+ try {
13035
+ await proxy(upstreamUrl, originalMethod, body, 0);
12981
13036
  } catch (err) {
13037
+ if (res.headersSent) {
13038
+ res.destroy(err instanceof Error ? err : new Error(String(err)));
13039
+ return;
13040
+ }
12982
13041
  applyFetchProxyCors(res);
12983
13042
  res.writeHead(502, { "Content-Type": "text/plain" });
12984
13043
  res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`);
12985
- return;
12986
- }
12987
- upstream.headers.forEach((value, key) => {
12988
- const lowerKey = key.toLowerCase();
12989
- if (lowerKey === "content-encoding" || lowerKey === "transfer-encoding" || lowerKey === "content-length" || lowerKey === "set-cookie" || lowerKey.startsWith("access-control-")) {
12990
- return;
12991
- }
12992
- res.setHeader(key, value);
12993
- });
12994
- applyFetchProxyCors(res);
12995
- const setCookie = upstream.headers.getSetCookie?.() ?? [];
12996
- if (setCookie.length > 0) {
12997
- res.setHeader("x-sootsim-set-cookie", setCookie.join(", "));
12998
13044
  }
12999
- res.statusCode = upstream.status;
13000
- const buf = Buffer.from(await upstream.arrayBuffer());
13001
- res.end(buf);
13002
13045
  }
13003
13046
  function handleAppApiRequest(req, res) {
13004
13047
  const reqUrl = req.url || "";
@@ -13068,12 +13111,13 @@ function handleAppApiRequest(req, res) {
13068
13111
  req.pipe(proxyReq);
13069
13112
  return true;
13070
13113
  }
13071
- var import_http2, import_https, STRIP_FETCH_PROXY_HEADERS, FETCH_PROXY_CORS_HEADERS, APP_API_HEADER_REWRITES;
13114
+ var import_http2, import_https, import_promises2, STRIP_FETCH_PROXY_HEADERS, FETCH_PROXY_CORS_HEADERS, APP_API_HEADER_REWRITES;
13072
13115
  var init_fetch_proxy_handler = __esm({
13073
13116
  "src/host/fetch-proxy-handler.ts"() {
13074
13117
  "use strict";
13075
13118
  import_http2 = __toESM(require("http"), 1);
13076
13119
  import_https = __toESM(require("https"), 1);
13120
+ import_promises2 = require("stream/promises");
13077
13121
  init_fetch_proxy_overrides();
13078
13122
  STRIP_FETCH_PROXY_HEADERS = /* @__PURE__ */ new Set([
13079
13123
  "host",
@@ -16385,6 +16429,101 @@ var init_app_state_reset = __esm({
16385
16429
  });
16386
16430
 
16387
16431
  // cli/commands/inspect/core.ts
16432
+ function scoreLayoutTextContrast(input) {
16433
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
16434
+ const { normal, background } = input;
16435
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
16436
+ return { ratio: null, required, reason: "capture dimensions do not match" };
16437
+ }
16438
+ const left = Math.max(0, Math.floor(input.box.x));
16439
+ const top = Math.max(0, Math.floor(input.box.y));
16440
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
16441
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
16442
+ if (right <= left || bottom <= top) {
16443
+ return { ratio: null, required, reason: "text is outside the captured frame" };
16444
+ }
16445
+ const linear = (value) => {
16446
+ const channel = value / 255;
16447
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
16448
+ };
16449
+ const ratioOf = (foreground, behind) => {
16450
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
16451
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
16452
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
16453
+ };
16454
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
16455
+ const area = (right - left) * (bottom - top);
16456
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
16457
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
16458
+ const deltaAt = (offset) => Math.max(
16459
+ Math.abs(normal.data[offset] - background.data[offset]),
16460
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
16461
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
16462
+ );
16463
+ let strongestDelta = 0;
16464
+ let strongestForeground = null;
16465
+ let opaqueBackground = null;
16466
+ for (let y = top; y < bottom; y += step) {
16467
+ for (let x = left; x < right; x += step) {
16468
+ const offset = offsetAt(x, y);
16469
+ if (background.data[offset + 3] < 250) continue;
16470
+ const behind = [
16471
+ background.data[offset],
16472
+ background.data[offset + 1],
16473
+ background.data[offset + 2]
16474
+ ];
16475
+ opaqueBackground ??= behind;
16476
+ const delta = deltaAt(offset);
16477
+ if (delta <= strongestDelta) continue;
16478
+ strongestDelta = delta;
16479
+ strongestForeground = [
16480
+ normal.data[offset],
16481
+ normal.data[offset + 1],
16482
+ normal.data[offset + 2]
16483
+ ];
16484
+ }
16485
+ }
16486
+ let bestRatio = Infinity;
16487
+ let bestForeground = null;
16488
+ let bestBackground = null;
16489
+ const consider = (foreground, behind) => {
16490
+ const ratio = ratioOf(foreground, behind);
16491
+ if (ratio >= bestRatio) return;
16492
+ bestRatio = ratio;
16493
+ bestForeground = foreground;
16494
+ bestBackground = behind;
16495
+ };
16496
+ if (strongestDelta >= 2 && strongestForeground) {
16497
+ for (let y = top; y < bottom; y += step) {
16498
+ for (let x = left; x < right; x += step) {
16499
+ const offset = offsetAt(x, y);
16500
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
16501
+ continue;
16502
+ }
16503
+ consider(strongestForeground, [
16504
+ background.data[offset],
16505
+ background.data[offset + 1],
16506
+ background.data[offset + 2]
16507
+ ]);
16508
+ }
16509
+ }
16510
+ } else if (opaqueBackground) {
16511
+ consider(opaqueBackground, opaqueBackground);
16512
+ }
16513
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
16514
+ return {
16515
+ ratio: null,
16516
+ required,
16517
+ reason: "could not isolate opaque rendered text pixels"
16518
+ };
16519
+ }
16520
+ return {
16521
+ ratio: Math.round(bestRatio * 100) / 100,
16522
+ required,
16523
+ foreground: hex(bestForeground),
16524
+ background: hex(bestBackground)
16525
+ };
16526
+ }
16388
16527
  function layoutGetNativeEval(opts = {}) {
16389
16528
  const includeStyle = opts.styling === true;
16390
16529
  const filter = opts.filter?.trim().toLowerCase() ?? "";
@@ -16394,6 +16533,7 @@ function layoutGetNativeEval(opts = {}) {
16394
16533
  const list = await t.listInspectable({})
16395
16534
  if (!Array.isArray(list)) return []
16396
16535
  const includeStyle = ${JSON.stringify(includeStyle)}
16536
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
16397
16537
  // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
16398
16538
  const filter = ${JSON.stringify(filter)}
16399
16539
  const matchesFilter = (n, text) => {
@@ -16438,7 +16578,7 @@ function layoutGetNativeEval(opts = {}) {
16438
16578
  }
16439
16579
  const weightOf = (style) => {
16440
16580
  const w = style && style.fontWeight
16441
- const n = typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN)
16581
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
16442
16582
  return Number.isFinite(n) && n >= 600 ? n : undefined
16443
16583
  }
16444
16584
  const decoOf = (style) => {
@@ -16481,7 +16621,7 @@ function layoutGetNativeEval(opts = {}) {
16481
16621
  return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
16482
16622
  }
16483
16623
 
16484
- return list.filter(isVisible).filter((n) => {
16624
+ const elements = list.filter(isVisible).filter((n) => {
16485
16625
  const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
16486
16626
  return matchesFilter(n, t)
16487
16627
  }).map((n) => {
@@ -16516,7 +16656,7 @@ function layoutGetNativeEval(opts = {}) {
16516
16656
  ...base,
16517
16657
  borderRadius: radiusOf(style),
16518
16658
  padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
16519
- fontSize: fontSizeOf(style),
16659
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
16520
16660
  color: text ? colorStr(style.color) : undefined,
16521
16661
  bg: colorStr(style.backgroundColor),
16522
16662
  opacity: opacityOf(style),
@@ -16527,6 +16667,50 @@ function layoutGetNativeEval(opts = {}) {
16527
16667
  borderColor: borderWidth ? borderColorOf(style) : undefined,
16528
16668
  }
16529
16669
  })
16670
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
16671
+ if (!includeStyle || textElements.length === 0) return elements
16672
+
16673
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
16674
+ if (typeof screenshot !== 'function') {
16675
+ throw new Error('styled native layout requires the SootSim capture bridge')
16676
+ }
16677
+ const device = list.find((node) => node && node.device)?.device || {}
16678
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
16679
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
16680
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
16681
+ const capture = async (suppressText) => {
16682
+ const dataUrl = await screenshot({
16683
+ format: 'png',
16684
+ outputWidth: width,
16685
+ outputHeight: height,
16686
+ ...(suppressText ? { suppressText: true } : {}),
16687
+ })
16688
+ const image = await new Promise((resolve, reject) => {
16689
+ const next = new Image()
16690
+ next.onload = () => resolve(next)
16691
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
16692
+ next.src = dataUrl
16693
+ })
16694
+ const canvas = document.createElement('canvas')
16695
+ canvas.width = image.naturalWidth
16696
+ canvas.height = image.naturalHeight
16697
+ const context = canvas.getContext('2d', { willReadFrequently: true })
16698
+ if (!context) throw new Error('could not read native layout pixels')
16699
+ context.drawImage(image, 0, 0)
16700
+ return context.getImageData(0, 0, canvas.width, canvas.height)
16701
+ }
16702
+ const normal = await capture(false)
16703
+ const background = await capture(true)
16704
+ for (const element of textElements) {
16705
+ element.contrast = scoreContrast({
16706
+ normal,
16707
+ background,
16708
+ box: element.box,
16709
+ fontSize: element.fontSize,
16710
+ fontWeight: element.fontWeight,
16711
+ })
16712
+ }
16713
+ return elements
16530
16714
  })()`;
16531
16715
  }
16532
16716
  function readyProbeHasContent(probe) {
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! sootsim v0.1.239 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! sootsim v0.1.241 | (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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sootsim",
3
- "version": "0.1.239",
3
+ "version": "0.1.241",
4
4
  "description": "sootsim CLI + vite/metro plugins + skills registry. bridge client for driving the proprietary sootsim-engine over WebSocket.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@ automation, inspecting the render tree, and managing sims.
29
29
  | `sootsim find` | locate nodes by text / testID / role / type / predicate |
30
30
  | `sootsim assert` | run any read verb and convert its output into an exit code |
31
31
  | `sootsim screenshot` | capture PNGs and manage App Store screenshot assets |
32
- | `sootsim get` | read runtime state (tree, a11y, url, count, errors, …) |
32
+ | `sootsim get` | read runtime state (tree, a11y, layout, url, count, errors, …) |
33
33
  | `sootsim do` | drive the app (tap, type, scroll, swipe, key, reload, …) |
34
34
  | `sootsim wait` | block until the runtime reaches a known state |
35
35
  | `sootsim shell` | drive shell state and simulator chrome |
@@ -239,17 +239,19 @@ watch out for:
239
239
  ## branch — accessibility
240
240
 
241
241
  ```sh
242
- sootsim get a11y # full a11y tree
243
- sootsim get a11y --issues # only nodes with problems
242
+ sootsim get a11y # roles, labels, hints, and target geometry
243
+ sootsim get layout --styling # boxes, styles, and native pixel contrast
244
244
  sootsim describe --a11y # inline a11y info on every node
245
245
  ```
246
246
 
247
- issue summary format:
247
+ native styled layout reads rendered pixels rather than inferring contrast from
248
+ tokens. it compares the normal frame with one CanvasKit capture where text is
249
+ suppressed and adds each text score to the existing layout row and issue list.
250
+
251
+ contrast summary format:
248
252
 
249
253
  ```
250
- [input] "" role=none label=null ⚠ missing label
251
- [button] "➜" @(340,280) 20x20 (tap) ⚠ touch target <44×44
252
- <text> "Sign in" color=#777 bg=#888 ⚠ contrast 1.4:1 (need 4.5:1)
254
+ #loginButton @(24,347) 120x24 font:16 color:#777 contrast:1.4/4.5 FAIL on:#888888 "Sign in"
253
255
  ```
254
256
 
255
257
  finding offenders fast:
@@ -266,7 +268,8 @@ what gets checked:
266
268
  - **missing roles** — `Pressable`/`Touchable` without `accessibilityRole`
267
269
  - **missing hints** — complex interactions without `accessibilityHint`
268
270
  - **touch target size** — tappable nodes with hitSlop-expanded rect <44×44
269
- - **contrast** — text color vs detectable background
271
+ - **contrast** — run `sootsim get layout --styling`; it samples final text and
272
+ text-suppressed background pixels, including gradients and composited opacity
270
273
  - **label / visible-text mismatch** — label that reads differently from
271
274
  the text VoiceOver would fall back to (often a translation bug)
272
275
  - **duplicate labels** — two reachable nodes with identical labels on
@@ -282,10 +285,13 @@ triage order:
282
285
  fix → verify loop (the same shape as debugging):
283
286
 
284
287
  ```sh
285
- sootsim get a11y --issues > before.txt
288
+ sootsim get a11y > before-a11y.txt
289
+ sootsim get layout --styling --json > before-layout.json
286
290
  # apply the fix
287
- sootsim get a11y --issues > after.txt
288
- diff before.txt after.txt
291
+ sootsim get a11y > after-a11y.txt
292
+ sootsim get layout --styling --json > after-layout.json
293
+ diff before-a11y.txt after-a11y.txt
294
+ diff before-layout.json after-layout.json
289
295
  ```
290
296
 
291
297
  fixing common cases:
@@ -340,8 +346,11 @@ type names sneak in).
340
346
  `--visible`, or both.
341
347
  - **`perf shell` reports zero frames** — you didn't run `perf shell start`
342
348
  first, or the capture window closed before any frame committed.
343
- - **`get a11y --issues` returns 100+ items** — sort by P0 first; fixing
344
- one label often cascades to a parent's a11y subtree.
349
+ - **`get a11y` returns 100+ items** — narrow with `find --role`,
350
+ `find --pressable`, or `find --no-label`; the a11y tree is descriptive and
351
+ intentionally does not invent an `--issues` grading mode.
352
+ - **styled layout cannot capture pixels** — open a tenant app, dismiss system
353
+ overlays, and let the launch transition finish before retrying.
345
354
  - **timeline footer keeps showing 1 error but `get errors` is empty** —
346
355
  the footer reads "since last cursor advance"; `get errors` is absolute.
347
356
  the error already scrolled out of the absolute ring buffer.
@@ -25,6 +25,7 @@
25
25
 
26
26
  import http, { type IncomingMessage, type ServerResponse } from 'http'
27
27
  import https from 'https'
28
+ import { finished, pipeline } from 'stream/promises'
28
29
  import {
29
30
  FETCH_PROXY_BROWSER_USER_AGENT,
30
31
  getFetchProxyTargetHeaders,
@@ -98,22 +99,6 @@ function formatFetchProxyError(targetUrl: string, err: unknown): string {
98
99
  return message
99
100
  }
100
101
 
101
- function isTextProxyContentType(contentType: string | string[] | undefined): boolean {
102
- const value = Array.isArray(contentType) ? contentType[0] : contentType
103
- const normalized = value?.split(';', 1)[0]?.trim().toLowerCase()
104
- if (!normalized) return false
105
- return (
106
- normalized.startsWith('text/') ||
107
- normalized === 'application/json' ||
108
- normalized.endsWith('+json') ||
109
- normalized === 'application/x-www-form-urlencoded' ||
110
- normalized === 'application/xml' ||
111
- normalized.endsWith('+xml') ||
112
- normalized === 'application/javascript' ||
113
- normalized === 'application/graphql'
114
- )
115
- }
116
-
117
102
  export function buildFetchProxyHeaders(
118
103
  reqHeaders: Record<string, string | string[] | undefined>,
119
104
  targetUrl?: URL,
@@ -190,60 +175,127 @@ export async function handleFetchProxyRequest(
190
175
  return
191
176
  }
192
177
 
193
- const headers = buildFetchProxyHeaders(req.headers, upstreamUrl)
194
-
195
- let body: BodyInit | undefined
178
+ let body: Buffer | undefined
196
179
  if (req.method !== 'GET' && req.method !== 'HEAD') {
197
180
  const chunks: Buffer[] = []
198
181
  for await (const chunk of req) {
199
182
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
200
183
  }
201
184
  if (chunks.length > 0) {
202
- const bytes = Buffer.concat(chunks)
203
- body = isTextProxyContentType(req.headers['content-type'])
204
- ? bytes.toString('utf8')
205
- : bytes
185
+ body = Buffer.concat(chunks)
206
186
  }
207
187
  }
208
188
 
209
- let upstream: Response
210
- try {
211
- upstream = await fetch(upstreamUrl.href, {
212
- method: req.method,
213
- headers,
214
- body: body as BodyInit | undefined,
215
- redirect: 'follow',
189
+ const originalMethod = req.method || 'GET'
190
+ const proxy = async (
191
+ targetUrl: URL,
192
+ method: string,
193
+ nextBody: Buffer | undefined,
194
+ redirects: number,
195
+ ): Promise<void> => {
196
+ const transport = targetUrl.protocol === 'https:' ? https : http
197
+ const targetHeaders = buildFetchProxyHeaders(req.headers, targetUrl)
198
+ if (targetUrl.origin !== upstreamUrl.origin) {
199
+ delete targetHeaders.authorization
200
+ delete targetHeaders['proxy-authorization']
201
+ }
202
+ if (method === 'GET' || method === 'HEAD') {
203
+ delete targetHeaders['content-length']
204
+ delete targetHeaders['content-type']
205
+ } else if (nextBody) {
206
+ targetHeaders['content-length'] = String(nextBody.byteLength)
207
+ }
208
+
209
+ const proxyRes = await new Promise<IncomingMessage>((resolve, reject) => {
210
+ const proxyReq = transport.request(
211
+ {
212
+ hostname: targetUrl.hostname,
213
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
214
+ path: targetUrl.pathname + targetUrl.search,
215
+ method,
216
+ headers: targetHeaders,
217
+ },
218
+ resolve,
219
+ )
220
+ const abort = () => proxyReq.destroy(new Error('fetch proxy client disconnected'))
221
+ req.once('aborted', abort)
222
+ proxyReq.once('close', () => req.off('aborted', abort))
223
+ proxyReq.once('error', reject)
224
+ if (nextBody === undefined) proxyReq.end()
225
+ else proxyReq.end(nextBody)
216
226
  })
217
- } catch (err) {
218
- applyFetchProxyCors(res)
219
- res.writeHead(502, { 'Content-Type': 'text/plain' })
220
- res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
221
- return
222
- }
223
227
 
224
- upstream.headers.forEach((value, key) => {
225
- const lowerKey = key.toLowerCase()
228
+ const status = proxyRes.statusCode ?? 502
229
+ const location = proxyRes.headers.location
226
230
  if (
227
- lowerKey === 'content-encoding' ||
228
- lowerKey === 'transfer-encoding' ||
229
- lowerKey === 'content-length' ||
230
- lowerKey === 'set-cookie' ||
231
- lowerKey.startsWith('access-control-')
231
+ location &&
232
+ (status === 301 ||
233
+ status === 302 ||
234
+ status === 303 ||
235
+ status === 307 ||
236
+ status === 308)
232
237
  ) {
238
+ if (redirects >= 10) {
239
+ proxyRes.destroy()
240
+ throw new Error('too many redirects')
241
+ }
242
+ const switchToGet =
243
+ ((status === 301 || status === 302) && method === 'POST') ||
244
+ (status === 303 && method !== 'GET' && method !== 'HEAD')
245
+ const redirectUrl = new URL(location, targetUrl)
246
+ proxyRes.resume()
247
+ await finished(proxyRes)
248
+ await proxy(
249
+ redirectUrl,
250
+ switchToGet ? 'GET' : method,
251
+ switchToGet ? undefined : nextBody,
252
+ redirects + 1,
253
+ )
233
254
  return
234
255
  }
235
- res.setHeader(key, value)
236
- })
237
- applyFetchProxyCors(res)
238
256
 
239
- const setCookie = upstream.headers.getSetCookie?.() ?? []
240
- if (setCookie.length > 0) {
241
- res.setHeader('x-sootsim-set-cookie', setCookie.join(', '))
257
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
258
+ const lowerKey = key.toLowerCase()
259
+ if (
260
+ value === undefined ||
261
+ lowerKey === 'set-cookie' ||
262
+ lowerKey === 'connection' ||
263
+ lowerKey === 'keep-alive' ||
264
+ lowerKey === 'proxy-authenticate' ||
265
+ lowerKey === 'proxy-authorization' ||
266
+ lowerKey === 'te' ||
267
+ lowerKey === 'trailer' ||
268
+ lowerKey === 'transfer-encoding' ||
269
+ lowerKey === 'upgrade' ||
270
+ lowerKey.startsWith('access-control-')
271
+ ) {
272
+ continue
273
+ }
274
+ res.setHeader(key, value)
275
+ }
276
+ applyFetchProxyCors(res)
277
+ const setCookie = proxyRes.headers['set-cookie'] ?? []
278
+ if (setCookie.length > 0) {
279
+ res.setHeader(
280
+ 'x-sootsim-set-cookie',
281
+ (Array.isArray(setCookie) ? setCookie : [setCookie]).join(', '),
282
+ )
283
+ }
284
+ res.statusCode = status
285
+ await pipeline(proxyRes, res)
242
286
  }
243
287
 
244
- res.statusCode = upstream.status
245
- const buf = Buffer.from(await upstream.arrayBuffer())
246
- res.end(buf)
288
+ try {
289
+ await proxy(upstreamUrl, originalMethod, body, 0)
290
+ } catch (err) {
291
+ if (res.headersSent) {
292
+ res.destroy(err instanceof Error ? err : new Error(String(err)))
293
+ return
294
+ }
295
+ applyFetchProxyCors(res)
296
+ res.writeHead(502, { 'Content-Type': 'text/plain' })
297
+ res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
298
+ }
247
299
  }
248
300
 
249
301
  export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
@@ -1,2 +0,0 @@
1
- /*! sootsim v0.1.239 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import{a,b,c,d}from"./chunk-BKDHODNB.js";import"./chunk-BWKVAEAJ.js";export{d as BETA_ASK_HEADLINE,b as BETA_LABEL,c as BETA_TAGLINE,a as IS_BETA};