rnxsim 0.0.0 → 0.1.313
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.
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// built-in skill: run Maestro tests 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: 'maestro-test',
|
|
11
|
+
description: 'Run Maestro YAML tests against the app',
|
|
12
|
+
type: 'testing',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
triggers: ['run test', 'test flow', 'run maestro', 'test the app'],
|
|
15
|
+
tools: [
|
|
16
|
+
{
|
|
17
|
+
name: 'run_maestro',
|
|
18
|
+
description: 'Run a Maestro YAML test 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 { runOpenCommand } = await import('../../../cli/commands/control')
|
|
35
|
+
const { SootSimBridgeFlowRunner } =
|
|
36
|
+
await import('../../../cli/bridge-flow-runner')
|
|
37
|
+
|
|
38
|
+
const host = new SootSimBridgeHost({ port: DEFAULT_SOOTSIM_BRIDGE_PORT })
|
|
39
|
+
host.start({ silent: true })
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const flowContent = fs.readFileSync(params.flowPath, 'utf8')
|
|
43
|
+
const fmMatch = flowContent.match(/^---\n([\s\S]*?)\n---\n?/)
|
|
44
|
+
const stepsBody = fmMatch ? flowContent.slice(fmMatch[0].length) : flowContent
|
|
45
|
+
const steps = yaml.parse(stepsBody)
|
|
46
|
+
const beforeIds = new Set(host.listSims().map((sim) => sim.id))
|
|
47
|
+
|
|
48
|
+
await runOpenCommand([context.url, '--new', '--headless', '--no-describe'], {
|
|
49
|
+
port: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
let simId: string | null = null
|
|
53
|
+
for (let i = 0; i < 60; i++) {
|
|
54
|
+
const sims = host.listSims()
|
|
55
|
+
const opened =
|
|
56
|
+
sims.find((sim) => !beforeIds.has(sim.id) && sim.readyState === 'open') ||
|
|
57
|
+
sims.find((sim) => sim.readyState === 'open')
|
|
58
|
+
if (opened) {
|
|
59
|
+
simId = opened.id
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
await sleep(500)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!simId) {
|
|
66
|
+
throw new Error('no sim connected to the bridge host')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bridge = createBridge(DEFAULT_SOOTSIM_BRIDGE_PORT, {
|
|
70
|
+
simId,
|
|
71
|
+
commandTimeoutMs: 15000,
|
|
72
|
+
})
|
|
73
|
+
const driver = new SootSimBridgeFlowRunner(bridge, {
|
|
74
|
+
screenshotDir: context.outputDir,
|
|
75
|
+
flowDir: path.dirname(path.resolve(params.flowPath)),
|
|
76
|
+
simId,
|
|
77
|
+
recordingOutputDir: params.record ? context.outputDir : undefined,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
let videoPath: string | null = null
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await driver.waitForTree(120000)
|
|
84
|
+
if (params.record) {
|
|
85
|
+
await driver.startRecording()
|
|
86
|
+
}
|
|
87
|
+
await driver.runFlow(steps)
|
|
88
|
+
if (params.record) {
|
|
89
|
+
videoPath = await driver.stopRecording()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
message: `Flow completed: ${steps.length} steps passed`,
|
|
95
|
+
data: {
|
|
96
|
+
steps: steps.length,
|
|
97
|
+
simId,
|
|
98
|
+
videoPath: videoPath || undefined,
|
|
99
|
+
},
|
|
100
|
+
artifacts: videoPath
|
|
101
|
+
? [{ type: 'video', name: path.basename(videoPath), path: videoPath }]
|
|
102
|
+
: undefined,
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
try {
|
|
106
|
+
await bridge.closeSim(simId)
|
|
107
|
+
} catch {}
|
|
108
|
+
bridge.close()
|
|
109
|
+
}
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
message: `Flow failed: ${err.message}`,
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
await host.close().catch(() => {})
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// built-in skill: performance profiling
|
|
2
|
+
|
|
3
|
+
import { launchReapedChrome } from '../../../../../scripts/lib/reap-browser'
|
|
4
|
+
import type { SootSimSkill } from '../types'
|
|
5
|
+
|
|
6
|
+
interface OverlayProfile {
|
|
7
|
+
syncCount?: number
|
|
8
|
+
avgSyncMs?: number
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const skill: SootSimSkill = {
|
|
13
|
+
name: 'perf-profile',
|
|
14
|
+
description: 'Profile render performance of the app',
|
|
15
|
+
type: 'performance',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
triggers: ['performance', 'profile', 'render time', 'benchmark', 'perf'],
|
|
18
|
+
tools: [
|
|
19
|
+
{
|
|
20
|
+
name: 'profile_render',
|
|
21
|
+
description: 'Profile render performance of the current screen',
|
|
22
|
+
parameters: {
|
|
23
|
+
duration: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'Profile duration in seconds (default: 5)',
|
|
26
|
+
},
|
|
27
|
+
interactions: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Include user interactions in profiling',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
async execute(params, context) {
|
|
33
|
+
const { chromium } = await import('playwright')
|
|
34
|
+
const path = await import('path')
|
|
35
|
+
const fs = await import('fs')
|
|
36
|
+
|
|
37
|
+
// sootsim renders CanvasKit — require GPU-backed Chrome (Metal). the
|
|
38
|
+
// bundled swiftshader shell software-renders the canvas and pegs every
|
|
39
|
+
// core; never use it for sootsim.
|
|
40
|
+
const browser = await launchReapedChrome((options) => chromium.launch(options), {
|
|
41
|
+
headless: true,
|
|
42
|
+
})
|
|
43
|
+
const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await page.goto(context.url, { waitUntil: 'networkidle' })
|
|
47
|
+
await page.waitForFunction('window.__sootsimTest && window.__sootsimA11y', {
|
|
48
|
+
timeout: 10000,
|
|
49
|
+
})
|
|
50
|
+
await page.waitForTimeout(1000)
|
|
51
|
+
|
|
52
|
+
// reset profiler
|
|
53
|
+
await page.evaluate('window.__sootsimA11y.resetProfile()')
|
|
54
|
+
|
|
55
|
+
// wait for profile duration
|
|
56
|
+
const duration = (params.duration || 5) * 1000
|
|
57
|
+
await page.waitForTimeout(duration)
|
|
58
|
+
|
|
59
|
+
// collect results
|
|
60
|
+
const profile = (await page.evaluate(
|
|
61
|
+
'window.__sootsimA11y.profile()',
|
|
62
|
+
)) as OverlayProfile
|
|
63
|
+
const nodeCount = await page.evaluate('window.__sootsimTest.getNodeCount()')
|
|
64
|
+
const syncCount = profile.syncCount ?? 0
|
|
65
|
+
|
|
66
|
+
const report = {
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
duration: params.duration || 5,
|
|
69
|
+
nodeCount,
|
|
70
|
+
...profile,
|
|
71
|
+
fps: syncCount > 0 ? (syncCount / (duration / 1000)).toFixed(1) : '0',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const reportPath = path.join(context.outputDir, 'perf-report.json')
|
|
75
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true })
|
|
76
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
message: `${report.fps} fps, ${profile.avgSyncMs?.toFixed(1)}ms avg sync, ${nodeCount} nodes`,
|
|
81
|
+
artifacts: [{ type: 'report', name: 'perf-report', path: reportPath }],
|
|
82
|
+
data: report,
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
await browser.close()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// built-in skill: capture all screens in light/dark mode
|
|
2
|
+
|
|
3
|
+
import { launchReapedChrome } from '../../../../../scripts/lib/reap-browser'
|
|
4
|
+
import type { SootSimSkill } from '../types'
|
|
5
|
+
|
|
6
|
+
export const skill: SootSimSkill = {
|
|
7
|
+
name: 'screenshot-all',
|
|
8
|
+
description: 'Capture screenshots of all screens in the app',
|
|
9
|
+
type: 'screenshot',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
triggers: ['screenshot', 'capture screens', 'capture all', 'take screenshots'],
|
|
12
|
+
tools: [
|
|
13
|
+
{
|
|
14
|
+
name: 'capture_screenshot',
|
|
15
|
+
description: 'Capture a screenshot of the current screen',
|
|
16
|
+
parameters: {
|
|
17
|
+
name: { type: 'string', description: 'Screenshot name', required: true },
|
|
18
|
+
theme: { type: 'string', description: 'Color scheme: light or dark' },
|
|
19
|
+
},
|
|
20
|
+
async execute(params, context) {
|
|
21
|
+
const { chromium } = await import('playwright')
|
|
22
|
+
const path = await import('path')
|
|
23
|
+
const fs = await import('fs')
|
|
24
|
+
|
|
25
|
+
// sootsim renders CanvasKit — require GPU-backed Chrome (Metal). the
|
|
26
|
+
// bundled swiftshader shell software-renders the canvas and pegs every
|
|
27
|
+
// core; never use it for sootsim.
|
|
28
|
+
const browser = await launchReapedChrome((options) => chromium.launch(options), {
|
|
29
|
+
headless: true,
|
|
30
|
+
})
|
|
31
|
+
const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await page.goto(context.url, { waitUntil: 'networkidle' })
|
|
35
|
+
await page.waitForTimeout(2000)
|
|
36
|
+
|
|
37
|
+
const outputPath = path.join(context.outputDir, `${params.name}.png`)
|
|
38
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
39
|
+
await page.screenshot({ path: outputPath })
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
message: `Screenshot saved: ${params.name}`,
|
|
44
|
+
artifacts: [{ type: 'screenshot', name: params.name, path: outputPath }],
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
await browser.close()
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// built-in skill: visual diff against baselines
|
|
2
|
+
|
|
3
|
+
import { launchReapedChrome } from '../../../../../scripts/lib/reap-browser'
|
|
4
|
+
import type { SootSimSkill } from '../types'
|
|
5
|
+
|
|
6
|
+
export const skill: SootSimSkill = {
|
|
7
|
+
name: 'visual-diff',
|
|
8
|
+
description: 'Compare current rendering against baseline screenshots',
|
|
9
|
+
type: 'review',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
triggers: ['visual diff', 'compare', 'visual regression', 'diff screenshots'],
|
|
12
|
+
tools: [
|
|
13
|
+
{
|
|
14
|
+
name: 'visual_diff',
|
|
15
|
+
description: 'Compare current screen against a baseline screenshot',
|
|
16
|
+
parameters: {
|
|
17
|
+
baseline: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Path to baseline screenshot',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
name: { type: 'string', description: 'Name for the diff output', required: true },
|
|
23
|
+
threshold: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'Pixel match threshold (0-1, default 0.1)',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async execute(params, context) {
|
|
29
|
+
const { chromium } = await import('playwright')
|
|
30
|
+
const path = await import('path')
|
|
31
|
+
const fs = await import('fs')
|
|
32
|
+
|
|
33
|
+
// sootsim renders CanvasKit — require GPU-backed Chrome (Metal). the
|
|
34
|
+
// bundled swiftshader shell software-renders the canvas and pegs every
|
|
35
|
+
// core; never use it for sootsim.
|
|
36
|
+
const browser = await launchReapedChrome((options) => chromium.launch(options), {
|
|
37
|
+
headless: true,
|
|
38
|
+
})
|
|
39
|
+
const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await page.goto(context.url, { waitUntil: 'networkidle' })
|
|
43
|
+
await page.waitForTimeout(2000)
|
|
44
|
+
|
|
45
|
+
// capture current
|
|
46
|
+
const currentPath = path.join(context.outputDir, `${params.name}-current.png`)
|
|
47
|
+
fs.mkdirSync(path.dirname(currentPath), { recursive: true })
|
|
48
|
+
await page.screenshot({ path: currentPath })
|
|
49
|
+
|
|
50
|
+
// compare using pixelmatch if available
|
|
51
|
+
try {
|
|
52
|
+
const { PNG } = await import('pngjs')
|
|
53
|
+
const pixelmatch = (await import('pixelmatch')).default
|
|
54
|
+
|
|
55
|
+
const baseline = PNG.sync.read(fs.readFileSync(params.baseline))
|
|
56
|
+
const current = PNG.sync.read(fs.readFileSync(currentPath))
|
|
57
|
+
|
|
58
|
+
const { width, height } = baseline
|
|
59
|
+
const diff = new PNG({ width, height })
|
|
60
|
+
|
|
61
|
+
const numDiffPixels = pixelmatch(
|
|
62
|
+
baseline.data,
|
|
63
|
+
current.data,
|
|
64
|
+
diff.data,
|
|
65
|
+
width,
|
|
66
|
+
height,
|
|
67
|
+
{ threshold: params.threshold || 0.1 },
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const diffPath = path.join(context.outputDir, `${params.name}-diff.png`)
|
|
71
|
+
fs.writeFileSync(diffPath, PNG.sync.write(diff))
|
|
72
|
+
|
|
73
|
+
const totalPixels = width * height
|
|
74
|
+
const diffPercent = ((numDiffPixels / totalPixels) * 100).toFixed(2)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: numDiffPixels === 0,
|
|
78
|
+
message: `${diffPercent}% difference (${numDiffPixels} pixels)`,
|
|
79
|
+
artifacts: [
|
|
80
|
+
{ type: 'screenshot', name: `${params.name}-current`, path: currentPath },
|
|
81
|
+
{ type: 'screenshot', name: `${params.name}-diff`, path: diffPath },
|
|
82
|
+
],
|
|
83
|
+
data: { diffPixels: numDiffPixels, totalPixels, diffPercent },
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
message: 'Screenshot captured (pixelmatch not available for comparison)',
|
|
89
|
+
artifacts: [
|
|
90
|
+
{ type: 'screenshot', name: `${params.name}-current`, path: currentPath },
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
await browser.close()
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
@@ -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 maestroTestSkill } from './builtin/maestro-test'
|
|
8
|
+
import { skill as perfProfileSkill } from './builtin/perf-profile'
|
|
9
|
+
import { skill as screenshotAllSkill } from './builtin/screenshot-all'
|
|
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
|
+
maestroTestSkill,
|
|
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('maestro-test')
|
|
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,73 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
isValidTeamMachineSessionId,
|
|
4
|
+
teamMachineSessionIdFromCliIdentity,
|
|
5
|
+
} from './agent-identity.ts'
|
|
6
|
+
import type { AgentPromptEnvelope } from './agent-prompt.ts'
|
|
7
|
+
|
|
8
|
+
export { teamMachineSessionIdFromCliIdentity } from './agent-identity.ts'
|
|
9
|
+
|
|
10
|
+
export function formatTeamMachinePromptText(prompt: AgentPromptEnvelope): string {
|
|
11
|
+
return typeof prompt.text === 'string' ? prompt.text.trim() : ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveTeamMachineCommand(explicit?: string): string {
|
|
15
|
+
const fromArg = explicit?.trim()
|
|
16
|
+
if (fromArg) return fromArg
|
|
17
|
+
return 'tm'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function sendPromptToTeamMachineSession(input: {
|
|
21
|
+
sessionId: string
|
|
22
|
+
prompt: AgentPromptEnvelope
|
|
23
|
+
command?: string
|
|
24
|
+
timeoutMs?: number
|
|
25
|
+
}): Promise<void> {
|
|
26
|
+
const sessionId = input.sessionId.trim()
|
|
27
|
+
if (!isValidTeamMachineSessionId(sessionId)) {
|
|
28
|
+
throw new Error(`invalid Team Machine session id: ${sessionId || '<empty>'}`)
|
|
29
|
+
}
|
|
30
|
+
const text = formatTeamMachinePromptText(input.prompt)
|
|
31
|
+
if (!text) {
|
|
32
|
+
throw new Error('prompt text is empty')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const command = resolveTeamMachineCommand(input.command)
|
|
36
|
+
const timeoutMs = input.timeoutMs ?? 15_000
|
|
37
|
+
await new Promise<void>((resolve, reject) => {
|
|
38
|
+
const child = spawn(command, ['send', sessionId, text], {
|
|
39
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
40
|
+
env: process.env,
|
|
41
|
+
})
|
|
42
|
+
let stderr = ''
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
try {
|
|
45
|
+
child.kill('SIGTERM')
|
|
46
|
+
} catch {}
|
|
47
|
+
reject(new Error(`tm send timed out after ${Math.round(timeoutMs / 1000)}s`))
|
|
48
|
+
}, timeoutMs)
|
|
49
|
+
|
|
50
|
+
child.stderr?.setEncoding('utf8')
|
|
51
|
+
child.stderr?.on('data', (chunk) => {
|
|
52
|
+
if (stderr.length < 4000) stderr += String(chunk)
|
|
53
|
+
})
|
|
54
|
+
child.on('error', (err) => {
|
|
55
|
+
clearTimeout(timer)
|
|
56
|
+
reject(err)
|
|
57
|
+
})
|
|
58
|
+
child.on('exit', (code, signal) => {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
if (code === 0) {
|
|
61
|
+
resolve()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const detail = stderr.trim()
|
|
65
|
+
reject(
|
|
66
|
+
new Error(
|
|
67
|
+
detail ||
|
|
68
|
+
`tm send exited with ${signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`}`,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
}
|