sootsim 0.1.82 → 0.1.84

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 (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-3C6Z6YXA.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7Z4UFACX.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-XYBIZRDK.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-EJGEDUOC.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-2EFQQWEC.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-Z6G5SDG7.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-DCFGNIJC.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-M3OULYY3.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-QPDWMYCA.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/chunk-7YHDJLO2.js +119 -0
  34. package/dist-cli/chunks/{chunk-EX6IOT23.js → chunk-AJVTY6KY.js} +2 -2
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-JVNGH5S7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-WZLKUS54.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-DSYW2NOW.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-PYDAVGCZ.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-H6NBDJIO.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-BVXP2GDN.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-TR7NIFSL.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-UOWBKSSI.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-BISEHRNE.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-2XULSYS6.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-QTJJHBCI.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-U3XCDQRH.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-C7JOLDDQ.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JUCV3VHM.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-PO64TMRT.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-4QUAOBUB.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-D3SM2JYB.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2JQIKL3B.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-GI5MF6LP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-Q4JNA5VO.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-M4ERVRM4.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-ZN2C7V5R.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-7SCQEPXK.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-IZ2OO47Y.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-JUDJXJSE.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-O3AOQP3V.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-MQXYJTXM.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-2DVSCCR7.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-YDX4Q4XM.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GU62WVZZ.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-V5NLDTSB.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-HAOCONNB.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-YLC4DLXB.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-ZQ4DN4H6.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-HNUO3Z5F.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-ZAASAHSW.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-BA3GE5W5.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-MQXRHBTW.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-T4RMS5KX.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-MH26VONS.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-IELIDRGB.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-VMT3OWOB.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-VZBANVLU.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-GWXBTQ4H.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-JYHR4HFR.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-RPZ4UQ2B.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-7FLDF2AP.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-3RC4CNDZ.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/record-JZXCQ4IN.js +70 -0
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-Z7I2KIRB.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-GRCZ6AM4.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-E4YHXHH5.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-7SXMX2AY.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-GDJ2TCRV.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-62E7NDRC.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-Y7KR5ZQ3.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-IYMSUPVC.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-QKKXCCC2.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-PF6NQ7RT.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-CE2Y3TAN.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-XEO3ZCPF.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-372J7YF7.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-B4E7KCT5.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-D2EQVL7R.js +0 -2
  187. package/dist-cli/chunks/beta-VHPXECZY.js +0 -2
  188. package/dist-cli/chunks/chunk-27HBWBE6.js +0 -4
  189. package/dist-cli/chunks/chunk-2W5C5J4O.js +0 -64
  190. package/dist-cli/chunks/chunk-3OH4VCJA.js +0 -1
  191. package/dist-cli/chunks/chunk-45HLFQRI.js +0 -2
  192. package/dist-cli/chunks/chunk-7YLCK5HG.js +0 -5
  193. package/dist-cli/chunks/chunk-BRDUKIZI.js +0 -119
  194. package/dist-cli/chunks/chunk-GADW2Q5S.js +0 -1
  195. package/dist-cli/chunks/chunk-HST43CVE.js +0 -2
  196. package/dist-cli/chunks/chunk-QJBQOGTK.js +0 -73
  197. package/dist-cli/chunks/chunk-V26REV7G.js +0 -1
  198. package/dist-cli/chunks/cli-version-WF7T6IKI.js +0 -2
  199. package/dist-cli/chunks/control-EAK2OPGB.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-52A2MI72.js +0 -2
  201. package/dist-cli/chunks/drivers-B4QPIZ4B.js +0 -2
  202. package/dist-cli/chunks/flow-PFLHFNVM.js +0 -2
  203. package/dist-cli/chunks/install-ZCPEMK6U.js +0 -2
  204. package/dist-cli/chunks/record-CZ33G5FT.js +0 -70
  205. package/dist-cli/chunks/runtime-AZKHZHJ4.js +0 -2
  206. package/dist-cli/chunks/setup-repo-3Y2QAZRK.js +0 -2
  207. package/dist-cli/chunks/store-TDTFZMGA.js +0 -2
  208. package/dist-cli/chunks/telemetry-G3NIU5NP.js +0 -2
  209. package/dist-cli/chunks/upload-CLWFS7IL.js +0 -2
@@ -0,0 +1,84 @@
1
+ // built-in skill: performance profiling
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ interface OverlayProfile {
6
+ syncCount?: number
7
+ avgSyncMs?: number
8
+ [key: string]: unknown
9
+ }
10
+
11
+ export const skill: SootSimSkill = {
12
+ name: 'perf-profile',
13
+ description: 'Profile render performance of the app',
14
+ type: 'performance',
15
+ version: '1.0.0',
16
+ triggers: ['performance', 'profile', 'render time', 'benchmark', 'perf'],
17
+ tools: [
18
+ {
19
+ name: 'profile_render',
20
+ description: 'Profile render performance of the current screen',
21
+ parameters: {
22
+ duration: {
23
+ type: 'number',
24
+ description: 'Profile duration in seconds (default: 5)',
25
+ },
26
+ interactions: {
27
+ type: 'boolean',
28
+ description: 'Include user interactions in profiling',
29
+ },
30
+ },
31
+ async execute(params, context) {
32
+ const { chromium } = await import('playwright')
33
+ const path = await import('path')
34
+ const fs = await import('fs')
35
+
36
+ const browser = await chromium.launch({ headless: true })
37
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
38
+
39
+ try {
40
+ await page.goto(context.url, { waitUntil: 'networkidle' })
41
+ await page.waitForFunction('window.__sootsimTest && window.__sootsimA11y', {
42
+ timeout: 10000,
43
+ })
44
+ await page.waitForTimeout(1000)
45
+
46
+ // reset profiler
47
+ await page.evaluate('window.__sootsimA11y.resetProfile()')
48
+
49
+ // wait for profile duration
50
+ const duration = (params.duration || 5) * 1000
51
+ await page.waitForTimeout(duration)
52
+
53
+ // collect results
54
+ const profile = (await page.evaluate(
55
+ 'window.__sootsimA11y.profile()',
56
+ )) as OverlayProfile
57
+ const nodeCount = await page.evaluate('window.__sootsimTest.getNodeCount()')
58
+ const syncCount = profile.syncCount ?? 0
59
+
60
+ const report = {
61
+ timestamp: new Date().toISOString(),
62
+ duration: params.duration || 5,
63
+ nodeCount,
64
+ ...profile,
65
+ fps: syncCount > 0 ? (syncCount / (duration / 1000)).toFixed(1) : '0',
66
+ }
67
+
68
+ const reportPath = path.join(context.outputDir, 'perf-report.json')
69
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true })
70
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
71
+
72
+ return {
73
+ success: true,
74
+ message: `${report.fps} fps, ${profile.avgSyncMs?.toFixed(1)}ms avg sync, ${nodeCount} nodes`,
75
+ artifacts: [{ type: 'report', name: 'perf-report', path: reportPath }],
76
+ data: report,
77
+ }
78
+ } finally {
79
+ await browser.close()
80
+ }
81
+ },
82
+ },
83
+ ],
84
+ }
@@ -0,0 +1,46 @@
1
+ // built-in skill: capture all screens in light/dark mode
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ export const skill: SootSimSkill = {
6
+ name: 'screenshot-all',
7
+ description: 'Capture screenshots of all screens in the app',
8
+ type: 'screenshot',
9
+ version: '1.0.0',
10
+ triggers: ['screenshot', 'capture screens', 'capture all', 'take screenshots'],
11
+ tools: [
12
+ {
13
+ name: 'capture_screenshot',
14
+ description: 'Capture a screenshot of the current screen',
15
+ parameters: {
16
+ name: { type: 'string', description: 'Screenshot name', required: true },
17
+ theme: { type: 'string', description: 'Color scheme: light or dark' },
18
+ },
19
+ async execute(params, context) {
20
+ const { chromium } = await import('playwright')
21
+ const path = await import('path')
22
+ const fs = await import('fs')
23
+
24
+ const browser = await chromium.launch({ headless: true })
25
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
26
+
27
+ try {
28
+ await page.goto(context.url, { waitUntil: 'networkidle' })
29
+ await page.waitForTimeout(2000)
30
+
31
+ const outputPath = path.join(context.outputDir, `${params.name}.png`)
32
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true })
33
+ await page.screenshot({ path: outputPath })
34
+
35
+ return {
36
+ success: true,
37
+ message: `Screenshot saved: ${params.name}`,
38
+ artifacts: [{ type: 'screenshot', name: params.name, path: outputPath }],
39
+ }
40
+ } finally {
41
+ await browser.close()
42
+ }
43
+ },
44
+ },
45
+ ],
46
+ }
@@ -0,0 +1,118 @@
1
+ // built-in skill: run test flows against the app
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ function sleep(ms: number) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms))
7
+ }
8
+
9
+ export const skill: SootSimSkill = {
10
+ name: 'test-flow',
11
+ description: 'Run YAML test flows against the app',
12
+ type: 'testing',
13
+ version: '1.0.0',
14
+ triggers: ['run test', 'test flow', 'run flow', 'test the app'],
15
+ tools: [
16
+ {
17
+ name: 'run_flow',
18
+ description: 'Run a YAML flow against the app',
19
+ parameters: {
20
+ flowPath: {
21
+ type: 'string',
22
+ description: 'Path to the YAML flow file',
23
+ required: true,
24
+ },
25
+ record: { type: 'boolean', description: 'Record video of the flow' },
26
+ },
27
+ async execute(params, context) {
28
+ const fs = await import('fs')
29
+ const path = await import('path')
30
+ const yaml = await import('yaml')
31
+ const { DEFAULT_SOOTSIM_BRIDGE_PORT } = await import('../../bridge-constants')
32
+ const { SootSimBridgeHost } = await import('../../host/bridge-host')
33
+ const { createBridge } = await import('../../../cli/ws-bridge')
34
+ const { SootSimBridgeFlowRunner } =
35
+ await import('../../../cli/bridge-flow-runner')
36
+
37
+ const host = new SootSimBridgeHost({ port: DEFAULT_SOOTSIM_BRIDGE_PORT })
38
+ host.start({ silent: true })
39
+
40
+ try {
41
+ const flowContent = fs.readFileSync(params.flowPath, 'utf8')
42
+ const fmMatch = flowContent.match(/^---\n([\s\S]*?)\n---\n?/)
43
+ const stepsBody = fmMatch ? flowContent.slice(fmMatch[0].length) : flowContent
44
+ const steps = yaml.parse(stepsBody)
45
+ const beforeIds = new Set(host.listSims().map((sim) => sim.id))
46
+
47
+ await host.openUrl(context.url)
48
+
49
+ let simId: string | null = null
50
+ for (let i = 0; i < 60; i++) {
51
+ const sims = host.listSims()
52
+ const opened =
53
+ sims.find((sim) => !beforeIds.has(sim.id) && sim.readyState === 'open') ||
54
+ sims.find((sim) => sim.readyState === 'open')
55
+ if (opened) {
56
+ simId = opened.id
57
+ break
58
+ }
59
+ await sleep(500)
60
+ }
61
+
62
+ if (!simId) {
63
+ throw new Error('no sim connected to the bridge host')
64
+ }
65
+
66
+ const bridge = createBridge(DEFAULT_SOOTSIM_BRIDGE_PORT, {
67
+ simId,
68
+ commandTimeoutMs: 15000,
69
+ })
70
+ const driver = new SootSimBridgeFlowRunner(bridge, {
71
+ screenshotDir: context.outputDir,
72
+ flowDir: path.dirname(path.resolve(params.flowPath)),
73
+ simId,
74
+ recordingOutputDir: params.record ? context.outputDir : undefined,
75
+ })
76
+
77
+ let videoPath: string | null = null
78
+
79
+ try {
80
+ await driver.waitForTree(120000)
81
+ if (params.record) {
82
+ await driver.startRecording()
83
+ }
84
+ await driver.runFlow(steps)
85
+ if (params.record) {
86
+ videoPath = await driver.stopRecording()
87
+ }
88
+
89
+ return {
90
+ success: true,
91
+ message: `Flow completed: ${steps.length} steps passed`,
92
+ data: {
93
+ steps: steps.length,
94
+ simId,
95
+ videoPath: videoPath || undefined,
96
+ },
97
+ artifacts: videoPath
98
+ ? [{ type: 'video', name: path.basename(videoPath), path: videoPath }]
99
+ : undefined,
100
+ }
101
+ } finally {
102
+ try {
103
+ await bridge.closeSim(simId)
104
+ } catch {}
105
+ bridge.close()
106
+ }
107
+ } catch (err: any) {
108
+ return {
109
+ success: false,
110
+ message: `Flow failed: ${err.message}`,
111
+ }
112
+ } finally {
113
+ await host.close().catch(() => {})
114
+ }
115
+ },
116
+ },
117
+ ],
118
+ }
@@ -0,0 +1,94 @@
1
+ // built-in skill: visual diff against baselines
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ export const skill: SootSimSkill = {
6
+ name: 'visual-diff',
7
+ description: 'Compare current rendering against baseline screenshots',
8
+ type: 'review',
9
+ version: '1.0.0',
10
+ triggers: ['visual diff', 'compare', 'visual regression', 'diff screenshots'],
11
+ tools: [
12
+ {
13
+ name: 'visual_diff',
14
+ description: 'Compare current screen against a baseline screenshot',
15
+ parameters: {
16
+ baseline: {
17
+ type: 'string',
18
+ description: 'Path to baseline screenshot',
19
+ required: true,
20
+ },
21
+ name: { type: 'string', description: 'Name for the diff output', required: true },
22
+ threshold: {
23
+ type: 'number',
24
+ description: 'Pixel match threshold (0-1, default 0.1)',
25
+ },
26
+ },
27
+ async execute(params, context) {
28
+ const { chromium } = await import('playwright')
29
+ const path = await import('path')
30
+ const fs = await import('fs')
31
+
32
+ const browser = await chromium.launch({ headless: true })
33
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
34
+
35
+ try {
36
+ await page.goto(context.url, { waitUntil: 'networkidle' })
37
+ await page.waitForTimeout(2000)
38
+
39
+ // capture current
40
+ const currentPath = path.join(context.outputDir, `${params.name}-current.png`)
41
+ fs.mkdirSync(path.dirname(currentPath), { recursive: true })
42
+ await page.screenshot({ path: currentPath })
43
+
44
+ // compare using pixelmatch if available
45
+ try {
46
+ const { PNG } = await import('pngjs')
47
+ const pixelmatch = (await import('pixelmatch')).default
48
+
49
+ const baseline = PNG.sync.read(fs.readFileSync(params.baseline))
50
+ const current = PNG.sync.read(fs.readFileSync(currentPath))
51
+
52
+ const { width, height } = baseline
53
+ const diff = new PNG({ width, height })
54
+
55
+ const numDiffPixels = pixelmatch(
56
+ baseline.data,
57
+ current.data,
58
+ diff.data,
59
+ width,
60
+ height,
61
+ { threshold: params.threshold || 0.1 },
62
+ )
63
+
64
+ const diffPath = path.join(context.outputDir, `${params.name}-diff.png`)
65
+ fs.writeFileSync(diffPath, PNG.sync.write(diff))
66
+
67
+ const totalPixels = width * height
68
+ const diffPercent = ((numDiffPixels / totalPixels) * 100).toFixed(2)
69
+
70
+ return {
71
+ success: numDiffPixels === 0,
72
+ message: `${diffPercent}% difference (${numDiffPixels} pixels)`,
73
+ artifacts: [
74
+ { type: 'screenshot', name: `${params.name}-current`, path: currentPath },
75
+ { type: 'screenshot', name: `${params.name}-diff`, path: diffPath },
76
+ ],
77
+ data: { diffPixels: numDiffPixels, totalPixels, diffPercent },
78
+ }
79
+ } catch {
80
+ return {
81
+ success: true,
82
+ message: 'Screenshot captured (pixelmatch not available for comparison)',
83
+ artifacts: [
84
+ { type: 'screenshot', name: `${params.name}-current`, path: currentPath },
85
+ ],
86
+ }
87
+ }
88
+ } finally {
89
+ await browser.close()
90
+ }
91
+ },
92
+ },
93
+ ],
94
+ }
@@ -0,0 +1,107 @@
1
+ // skill registry — load, discover, and match skills
2
+
3
+ import * as fs from 'fs'
4
+ import * as path from 'path'
5
+ import { skill as a11yReviewSkill } from './builtin/a11y-review'
6
+ import { skill as compatCheckSkill } from './builtin/compat-check'
7
+ import { skill as perfProfileSkill } from './builtin/perf-profile'
8
+ import { skill as screenshotAllSkill } from './builtin/screenshot-all'
9
+ import { skill as testFlowSkill } from './builtin/test-flow'
10
+ import { skill as visualDiffSkill } from './builtin/visual-diff'
11
+ import type { SootSimSkill, SkillContext } from './types'
12
+
13
+ const _skills: Map<string, SootSimSkill> = new Map()
14
+ const BUILTIN_SKILLS = [
15
+ a11yReviewSkill,
16
+ compatCheckSkill,
17
+ perfProfileSkill,
18
+ screenshotAllSkill,
19
+ testFlowSkill,
20
+ visualDiffSkill,
21
+ ]
22
+
23
+ export const skillRegistry = {
24
+ register(skill: SootSimSkill) {
25
+ _skills.set(skill.name, skill)
26
+ },
27
+
28
+ get(name: string): SootSimSkill | undefined {
29
+ return _skills.get(name)
30
+ },
31
+
32
+ listAll(): SootSimSkill[] {
33
+ return Array.from(_skills.values())
34
+ },
35
+
36
+ // match skills by natural language query against triggers
37
+ match(query: string): SootSimSkill[] {
38
+ const q = query.toLowerCase()
39
+ return this.listAll().filter((skill) =>
40
+ skill.triggers.some((trigger) => q.includes(trigger.toLowerCase())),
41
+ )
42
+ },
43
+
44
+ // load built-in skills
45
+ async loadBuiltins() {
46
+ for (const skill of BUILTIN_SKILLS) {
47
+ this.register(skill)
48
+ }
49
+ },
50
+
51
+ // load skills from a project manifest (sootsim-skills.json)
52
+ async loadFromManifest(projectDir: string) {
53
+ const manifestPath = path.join(projectDir, 'sootsim-skills.json')
54
+ if (!fs.existsSync(manifestPath)) return
55
+
56
+ try {
57
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
58
+ if (Array.isArray(manifest.skills)) {
59
+ for (const entry of manifest.skills) {
60
+ if (typeof entry === 'string') {
61
+ // npm package name
62
+ try {
63
+ const mod = await import(entry)
64
+ if (mod.skill) this.register(mod.skill)
65
+ } catch {}
66
+ } else if (entry.file) {
67
+ // local file
68
+ try {
69
+ const mod = await import(path.resolve(projectDir, entry.file))
70
+ if (mod.skill) this.register(mod.skill)
71
+ } catch {}
72
+ }
73
+ }
74
+ }
75
+ } catch {}
76
+ },
77
+
78
+ // auto-discover skills based on project dependencies
79
+ suggestSkills(projectDir: string): string[] {
80
+ const suggestions: string[] = []
81
+ const pkgPath = path.join(projectDir, 'package.json')
82
+ if (!fs.existsSync(pkgPath)) return suggestions
83
+
84
+ try {
85
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
86
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
87
+
88
+ // suggest testing skill if detox or maestro is in deps
89
+ if (deps['detox'] || deps['maestro']) {
90
+ suggestions.push('test-flow')
91
+ }
92
+
93
+ // suggest a11y review if no a11y testing library
94
+ if (!deps['jest-axe'] && !deps['@testing-library/jest-dom']) {
95
+ suggestions.push('a11y-review')
96
+ }
97
+
98
+ // always suggest visual-diff and screenshot
99
+ suggestions.push('visual-diff', 'screenshot-all')
100
+
101
+ // suggest perf if app has many screens
102
+ suggestions.push('perf-profile')
103
+ } catch {}
104
+
105
+ return suggestions
106
+ },
107
+ }
@@ -0,0 +1,41 @@
1
+ // skill type definitions — self-contained agent capabilities
2
+
3
+ export type SkillType = 'testing' | 'screenshot' | 'review' | 'performance' | 'custom'
4
+
5
+ export interface SkillTool {
6
+ name: string
7
+ description: string
8
+ parameters: Record<string, { type: string; description: string; required?: boolean }>
9
+ execute: (params: Record<string, any>, context: SkillContext) => Promise<SkillResult>
10
+ }
11
+
12
+ export interface SkillContext {
13
+ url: string // sootsim URL
14
+ projectDir: string // project root
15
+ outputDir: string // where to write artifacts
16
+ verbose: boolean
17
+ }
18
+
19
+ export interface SkillResult {
20
+ success: boolean
21
+ message: string
22
+ artifacts?: SkillArtifact[]
23
+ data?: Record<string, any>
24
+ }
25
+
26
+ export interface SkillArtifact {
27
+ type: 'screenshot' | 'video' | 'report' | 'flow' | 'json'
28
+ name: string
29
+ path: string
30
+ }
31
+
32
+ export interface SootSimSkill {
33
+ name: string
34
+ description: string
35
+ type: SkillType
36
+ version: string
37
+ triggers: string[] // natural language patterns that activate this skill
38
+ tools: SkillTool[]
39
+ setup?: (context: SkillContext) => Promise<void>
40
+ teardown?: (context: SkillContext) => Promise<void>
41
+ }
@@ -0,0 +1,187 @@
1
+ // sootsim plugin for One/Vite — serves pre-built sootsim at /__soot/
2
+ // no runtime vite server — just static files from dist-plugin/
3
+ //
4
+ // usage in vite.config.ts:
5
+ // import { sootsimPlugin } from 'sootsim/vite'
6
+ // export default { plugins: [one(), sootsimPlugin()] }
7
+
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import type { Plugin } from 'vite'
11
+
12
+ const sootsimRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..')
13
+ const distDir = path.join(sootsimRoot, 'dist-plugin')
14
+ const publicDir = path.join(sootsimRoot, 'public')
15
+
16
+ export interface SootPluginOptions {
17
+ // custom bundle URL (default: auto-detect from One's metro)
18
+ bundleUrl?: string
19
+ // path prefix (default: '/__soot')
20
+ prefix?: string
21
+ // disable sootsim
22
+ enabled?: boolean
23
+ }
24
+
25
+ const MIME_TYPES: Record<string, string> = {
26
+ '.js': 'application/javascript',
27
+ '.mjs': 'application/javascript',
28
+ '.css': 'text/css',
29
+ '.html': 'text/html',
30
+ '.wasm': 'application/wasm',
31
+ '.json': 'application/json',
32
+ '.png': 'image/png',
33
+ '.svg': 'image/svg+xml',
34
+ '.ttf': 'font/ttf',
35
+ '.otf': 'font/otf',
36
+ '.woff': 'font/woff',
37
+ '.woff2': 'font/woff2',
38
+ '.mp3': 'audio/mpeg',
39
+ '.wav': 'audio/wav',
40
+ '.jpg': 'image/jpeg',
41
+ '.webp': 'image/webp',
42
+ }
43
+
44
+ export function sootsimPlugin(options: SootPluginOptions = {}): Plugin[] {
45
+ if (options.enabled === false) return []
46
+
47
+ const prefix = options.prefix || '/__soot'
48
+
49
+ return [
50
+ {
51
+ name: 'sootsim-one',
52
+
53
+ configureServer(server) {
54
+ const bundleUrl =
55
+ options.bundleUrl ||
56
+ '/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false'
57
+
58
+ // mirror Set-Cookie into a readable header for sootsim's fetch wrapper
59
+ server.middlewares.use((req, res, next) => {
60
+ const origWriteHead = res.writeHead.bind(res)
61
+ res.writeHead = function (statusCode: number, ...args: unknown[]) {
62
+ const setCookie = res.getHeader('set-cookie')
63
+ if (setCookie) {
64
+ const value = Array.isArray(setCookie)
65
+ ? setCookie.join(', ')
66
+ : String(setCookie)
67
+ res.setHeader('x-sootsim-set-cookie', value)
68
+ res.setHeader('access-control-expose-headers', 'x-sootsim-set-cookie')
69
+ }
70
+ return (origWriteHead as (...args: unknown[]) => typeof res)(
71
+ statusCode,
72
+ ...args,
73
+ )
74
+ } as typeof res.writeHead
75
+ next()
76
+ })
77
+
78
+ server.middlewares.use((req, res, next) => {
79
+ const url = req.url || ''
80
+
81
+ // serve the sootsim HTML shell — inject bundle URL
82
+ if (url === prefix || url === prefix + '/' || url.startsWith(prefix + '/?')) {
83
+ const htmlPath = path.join(distDir, 'index.html')
84
+ if (!fs.existsSync(htmlPath)) {
85
+ res.statusCode = 500
86
+ res.end('[sootsim] dist-plugin not built. run: bun scripts/build-plugin.ts')
87
+ return
88
+ }
89
+ let html = fs.readFileSync(htmlPath, 'utf8')
90
+ // inject bundle URL as a query param so main.tsx picks it up
91
+ html = html.replace(
92
+ '</head>',
93
+ `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`,
94
+ )
95
+ res.setHeader('content-type', 'text/html')
96
+ res.end(html)
97
+ return
98
+ }
99
+
100
+ // serve static files from dist-plugin/
101
+ if (url.startsWith(prefix + '/')) {
102
+ const filePath = url.slice(prefix.length).split('?')[0]
103
+ const fullPath = path.join(distDir, filePath)
104
+
105
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
106
+ const ext = path.extname(fullPath)
107
+ res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
108
+ res.setHeader('cache-control', 'max-age=31536000,immutable')
109
+ fs.createReadStream(fullPath).pipe(res)
110
+ return
111
+ }
112
+
113
+ // try public/ for wasm, fonts, icons, sounds
114
+ const publicPath = path.join(publicDir, filePath)
115
+ if (fs.existsSync(publicPath) && fs.statSync(publicPath).isFile()) {
116
+ const ext = path.extname(publicPath)
117
+ res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
118
+ fs.createReadStream(publicPath).pipe(res)
119
+ return
120
+ }
121
+ }
122
+
123
+ // also serve canvaskit.wasm etc. from public/ at root (some code references /canvaskit.wasm)
124
+ const staticRoots = ['/canvaskit.wasm', '/fonts/', '/icons/', '/sounds/']
125
+ if (staticRoots.some((p) => url.startsWith(p))) {
126
+ const fullPath = path.join(publicDir, url.split('?')[0])
127
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
128
+ const ext = path.extname(fullPath)
129
+ res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
130
+ fs.createReadStream(fullPath).pipe(res)
131
+ return
132
+ }
133
+ }
134
+
135
+ next()
136
+ })
137
+
138
+ // try to open sootsim electron app
139
+ const port = server.config.server.port || 8081
140
+ const sootsimUrl = `http://localhost:${port}${prefix}/`
141
+ console.log(`[sootsim] serving at ${sootsimUrl}`)
142
+
143
+ openElectronApp(sootsimUrl)
144
+ },
145
+ },
146
+ ]
147
+ }
148
+
149
+ // try to open the sootsim electron app via URL scheme or direct launch
150
+ async function openElectronApp(sootsimUrl: string) {
151
+ if (process.platform !== 'darwin') return
152
+
153
+ const { execSync, exec } = await import('child_process')
154
+
155
+ // try sootsim:// scheme first (works if app registered the protocol)
156
+ const schemeUrl = `sootsim://dev?url=${encodeURIComponent(sootsimUrl)}`
157
+ try {
158
+ exec(`open "${schemeUrl}"`)
159
+ return
160
+ } catch {}
161
+
162
+ // fallback: find the app directly
163
+ const candidates = [
164
+ '/Applications/sootsim.app',
165
+ path.join(process.env.HOME || '', 'Applications/sootsim.app'),
166
+ path.join(sootsimRoot, 'release/mac-arm64/sootsim.app'),
167
+ ]
168
+
169
+ let appPath = candidates.find((p) => fs.existsSync(p))
170
+ if (!appPath) {
171
+ try {
172
+ const found = execSync(
173
+ 'mdfind "kMDItemCFBundleIdentifier == dev.sootsim.simulator"',
174
+ { encoding: 'utf8', timeout: 3000 },
175
+ ).trim()
176
+ if (found) appPath = found.split('\n')[0]
177
+ } catch {}
178
+ }
179
+
180
+ if (appPath) {
181
+ try {
182
+ exec(`open -a "${appPath}" "${sootsimUrl}"`)
183
+ } catch {}
184
+ }
185
+ }
186
+
187
+ export default sootsimPlugin