rnxsim 0.1.313 → 0.1.314
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 +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getCliInspectVerbMetaByPath } from '@sootsim/skills/cli'
|
|
2
|
+
import type { ParsedArgs } from './parse-args'
|
|
3
|
+
|
|
4
|
+
const HIDDEN_RUNTIME_ALIAS_GROUPS = ['do', 'get'] as const
|
|
5
|
+
|
|
6
|
+
export function resolveHiddenRuntimeAlias(parsed: ParsedArgs): ParsedArgs {
|
|
7
|
+
if (parsed.command) return parsed
|
|
8
|
+
|
|
9
|
+
const attempted = parsed.commandArgs.find((arg) => !arg.startsWith('-'))
|
|
10
|
+
if (!attempted) return parsed
|
|
11
|
+
|
|
12
|
+
for (const group of HIDDEN_RUNTIME_ALIAS_GROUPS) {
|
|
13
|
+
if (getCliInspectVerbMetaByPath(group, attempted)) {
|
|
14
|
+
return { ...parsed, command: group }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed
|
|
19
|
+
}
|
package/cli/hints.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// configurable, identity-scoped hint registry for the sootsim CLI.
|
|
2
|
+
//
|
|
3
|
+
// two problems this solves:
|
|
4
|
+
// 1. the CLI scatters "hint: ..." console.logs across many command files,
|
|
5
|
+
// each with slightly different wording.
|
|
6
|
+
// 2. agents that loop over the same command (e.g. describe, find) see the
|
|
7
|
+
// same hint every invocation, which becomes noise.
|
|
8
|
+
//
|
|
9
|
+
// each hint declares its own frequency (always, once-per-identity, or a
|
|
10
|
+
// cooldown), so hints that are informational-but-stale-quickly can still
|
|
11
|
+
// re-fire while advice-like hints stay suppressed for the rest of the
|
|
12
|
+
// CLI identity.
|
|
13
|
+
//
|
|
14
|
+
// suppression is keyed on `getCliIdentityKey()` so two agents / two terminals
|
|
15
|
+
// don't share shown-sets.
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
|
18
|
+
import { tmpdir } from 'os'
|
|
19
|
+
import { dirname, join } from 'path'
|
|
20
|
+
import { getCliIdentityKey } from './current-sim'
|
|
21
|
+
|
|
22
|
+
export type HintFrequency =
|
|
23
|
+
| 'always'
|
|
24
|
+
| 'once-per-identity'
|
|
25
|
+
| 'once-ever'
|
|
26
|
+
| { cooldownMs: number }
|
|
27
|
+
|
|
28
|
+
export interface HintSpec<Args extends unknown[] = unknown[]> {
|
|
29
|
+
frequency: HintFrequency
|
|
30
|
+
// returned string(s) are printed as-is, prefixed with two spaces.
|
|
31
|
+
// return null to skip (useful when render wants to decide conditionally).
|
|
32
|
+
render: (...args: Args) => string | string[] | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const registry = new Map<string, HintSpec<any[]>>()
|
|
36
|
+
|
|
37
|
+
export function defineHint<Args extends unknown[]>(id: string, spec: HintSpec<Args>) {
|
|
38
|
+
registry.set(id, spec as HintSpec<any[]>)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface HintStateFile {
|
|
42
|
+
version: 1
|
|
43
|
+
shown: Record<string, number> // id -> last-shown timestamp (ms)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const STATE_VERSION = 1 as const
|
|
47
|
+
|
|
48
|
+
function identityStatePath(): string {
|
|
49
|
+
return join(tmpdir(), `sootsim-cli-hints-${getCliIdentityKey()}.json`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function globalStatePath(): string {
|
|
53
|
+
return join(tmpdir(), `sootsim-cli-hints-global.json`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readState(path: string): HintStateFile {
|
|
57
|
+
if (!existsSync(path)) return { version: STATE_VERSION, shown: {} }
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
60
|
+
if (parsed?.version !== STATE_VERSION || !parsed?.shown) {
|
|
61
|
+
return { version: STATE_VERSION, shown: {} }
|
|
62
|
+
}
|
|
63
|
+
return parsed as HintStateFile
|
|
64
|
+
} catch {
|
|
65
|
+
return { version: STATE_VERSION, shown: {} }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeState(path: string, state: HintStateFile) {
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
72
|
+
writeFileSync(path, JSON.stringify(state) + '\n')
|
|
73
|
+
} catch {
|
|
74
|
+
// best-effort only; losing shown state just means a hint re-fires once.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modeFromEnv(): 'normal' | 'always' | 'off' {
|
|
79
|
+
const v = (process.env.SOOTSIM_HINTS || '').toLowerCase()
|
|
80
|
+
if (v === 'off' || v === '0' || v === 'false') return 'off'
|
|
81
|
+
if (v === 'always' || v === 'verbose') return 'always'
|
|
82
|
+
return 'normal'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shouldShow(id: string, frequency: HintFrequency): boolean {
|
|
86
|
+
const mode = modeFromEnv()
|
|
87
|
+
if (mode === 'off') return false
|
|
88
|
+
if (mode === 'always') return true
|
|
89
|
+
if (frequency === 'always') return true
|
|
90
|
+
|
|
91
|
+
const now = Date.now()
|
|
92
|
+
if (frequency === 'once-per-identity') {
|
|
93
|
+
const state = readState(identityStatePath())
|
|
94
|
+
if (state.shown[id]) return false
|
|
95
|
+
state.shown[id] = now
|
|
96
|
+
writeState(identityStatePath(), state)
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
if (frequency === 'once-ever') {
|
|
100
|
+
const state = readState(globalStatePath())
|
|
101
|
+
if (state.shown[id]) return false
|
|
102
|
+
state.shown[id] = now
|
|
103
|
+
writeState(globalStatePath(), state)
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
if (typeof frequency === 'object' && 'cooldownMs' in frequency) {
|
|
107
|
+
const state = readState(identityStatePath())
|
|
108
|
+
const last = state.shown[id] ?? 0
|
|
109
|
+
if (now - last < frequency.cooldownMs) return false
|
|
110
|
+
state.shown[id] = now
|
|
111
|
+
writeState(identityStatePath(), state)
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// print a registered hint. no-ops if suppressed for this identity/cooldown.
|
|
118
|
+
// returns whether it was actually shown.
|
|
119
|
+
export function maybeHint(id: string, ...args: unknown[]): boolean {
|
|
120
|
+
const spec = registry.get(id)
|
|
121
|
+
if (!spec) {
|
|
122
|
+
if (process.env.SOOTSIM_HINTS_DEBUG) {
|
|
123
|
+
console.error(` [hints] no hint registered for id "${id}"`)
|
|
124
|
+
}
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if (!shouldShow(id, spec.frequency)) return false
|
|
128
|
+
|
|
129
|
+
const body = spec.render(...args)
|
|
130
|
+
if (body == null) return false
|
|
131
|
+
const lines = Array.isArray(body) ? body : [body]
|
|
132
|
+
// always stderr. every hint is advice about how to drive the CLI, never part
|
|
133
|
+
// of a command's result, and several commands (`describe`, `find`, every
|
|
134
|
+
// `do` verb) put a JSON document on stdout — a hint printed there corrupts it.
|
|
135
|
+
for (const line of lines) console.error(` hint: ${line}`)
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resetHintsForIdentity() {
|
|
140
|
+
rmSync(identityStatePath(), { force: true })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resetGlobalHints() {
|
|
144
|
+
rmSync(globalStatePath(), { force: true })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function listHints(): Array<{ id: string; frequency: HintFrequency }> {
|
|
148
|
+
return Array.from(registry.entries()).map(([id, spec]) => ({
|
|
149
|
+
id,
|
|
150
|
+
frequency: spec.frequency,
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// register the repo-wide hints here. keeping them centralised makes it easy
|
|
155
|
+
// to audit wording + frequency policy in one place.
|
|
156
|
+
defineHint<[number]>('app-still-loading', {
|
|
157
|
+
// re-fires once a minute because node count may genuinely change as the
|
|
158
|
+
// bundle boots — telling the user about it again is useful, not noisy.
|
|
159
|
+
frequency: { cooldownMs: 60_000 },
|
|
160
|
+
render: (nodeCount) =>
|
|
161
|
+
`app may still be loading (${nodeCount} nodes). run \`rnxsim wait ready\` first.`,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
defineHint<[string]>('wait-selector-for-missing-testid', {
|
|
165
|
+
// advice is stable; once per CLI identity is enough.
|
|
166
|
+
frequency: 'once-per-identity',
|
|
167
|
+
render: (testId) => `rnxsim wait selector ${testId}`,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
defineHint<[string[]]>('prefer-cli-over-eval', {
|
|
171
|
+
frequency: 'once-per-identity',
|
|
172
|
+
render: (suggestions) => {
|
|
173
|
+
if (!suggestions.length) return null
|
|
174
|
+
return [`try the CLI shortcut instead:`, ...suggestions.map((s) => ` ${s}`)]
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
defineHint<[]>('describe-use-filters', {
|
|
179
|
+
// shown when a describe run emits >=80 lines without any filter active —
|
|
180
|
+
// gentle nudge that there are better tools.
|
|
181
|
+
frequency: 'once-per-identity',
|
|
182
|
+
render: () => [
|
|
183
|
+
`describe output is long. narrow it with:`,
|
|
184
|
+
` rnxsim describe --only '*Bottom Sheet*'`,
|
|
185
|
+
` rnxsim describe --testid-like 'swap-*'`,
|
|
186
|
+
` rnxsim describe --subtree <testID>`,
|
|
187
|
+
],
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
defineHint<[]>('describe-filter-context', {
|
|
191
|
+
// filtered describe is useful search output, but it intentionally drops
|
|
192
|
+
// surrounding tree context. once per identity is enough to prevent agents
|
|
193
|
+
// treating a search hit as route proof.
|
|
194
|
+
frequency: 'once-per-identity',
|
|
195
|
+
render: () => [
|
|
196
|
+
`filtered describe is search output, not current-route proof.`,
|
|
197
|
+
`run \`rnxsim describe\` without filters after navigation; clipped nodes are marked \`(clipped:...)\`.`,
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
defineHint<[string]>('subtree-root-not-found', {
|
|
202
|
+
frequency: 'always',
|
|
203
|
+
render: (id) =>
|
|
204
|
+
`no node with testID/id "${id}" — try \`rnxsim find --testid ${id}\` to discover available ids.`,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
defineHint<[]>('clean-uninstall', {
|
|
208
|
+
// shown once on the first `sootsim --help` run so users learn the clean
|
|
209
|
+
// teardown path. package-manager uninstall hooks are best-effort, so the
|
|
210
|
+
// hint documents the explicit daemon cleanup command.
|
|
211
|
+
frequency: 'once-ever',
|
|
212
|
+
render: () => [
|
|
213
|
+
`to remove the background daemon and all ~/.sootsim data, run \`rnxsim daemon uninstall\`.`,
|
|
214
|
+
`the optional desktop app and its macOS preference plist are separate from the daemon.`,
|
|
215
|
+
],
|
|
216
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { dirname, join, resolve } from 'path'
|
|
4
|
+
|
|
5
|
+
const INSPECT_NOTICE_STATE_VERSION = 1 as const
|
|
6
|
+
const INSPECT_NOTICE_PATH_ENV = 'SOOTSIM_INSPECT_NOTICE_PATH'
|
|
7
|
+
const INSPECT_NOTICE_ENTRY_TTL_MS = 5 * 60 * 1000
|
|
8
|
+
|
|
9
|
+
export const INSPECT_NOTICE_COOLDOWN_MS = 15_000
|
|
10
|
+
|
|
11
|
+
type InspectNoticeKind = 'console' | 'requests'
|
|
12
|
+
|
|
13
|
+
interface InspectNoticeEntry {
|
|
14
|
+
signature: string
|
|
15
|
+
updatedAt: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface InspectNoticeState {
|
|
19
|
+
version: 1
|
|
20
|
+
entries: Record<string, InspectNoticeEntry>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getInspectNoticeStatePath() {
|
|
24
|
+
return resolve(
|
|
25
|
+
process.env[INSPECT_NOTICE_PATH_ENV] ||
|
|
26
|
+
join(tmpdir(), 'sootsim-inspect-notice-state.json'),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pruneEntries(
|
|
31
|
+
entries: Record<string, InspectNoticeEntry>,
|
|
32
|
+
nowMs: number,
|
|
33
|
+
): Record<string, InspectNoticeEntry> {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(entries).filter(([, entry]) => {
|
|
36
|
+
return (
|
|
37
|
+
typeof entry?.signature === 'string' &&
|
|
38
|
+
Number.isFinite(entry?.updatedAt) &&
|
|
39
|
+
nowMs - entry.updatedAt <= INSPECT_NOTICE_ENTRY_TTL_MS
|
|
40
|
+
)
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readInspectNoticeState(nowMs: number): InspectNoticeState {
|
|
46
|
+
const filepath = getInspectNoticeStatePath()
|
|
47
|
+
if (!existsSync(filepath)) {
|
|
48
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(
|
|
53
|
+
readFileSync(filepath, 'utf8'),
|
|
54
|
+
) as Partial<InspectNoticeState>
|
|
55
|
+
if (
|
|
56
|
+
parsed.version !== INSPECT_NOTICE_STATE_VERSION ||
|
|
57
|
+
!parsed.entries ||
|
|
58
|
+
typeof parsed.entries !== 'object'
|
|
59
|
+
) {
|
|
60
|
+
rmSync(filepath, { force: true })
|
|
61
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
version: INSPECT_NOTICE_STATE_VERSION,
|
|
65
|
+
entries: pruneEntries(parsed.entries as Record<string, InspectNoticeEntry>, nowMs),
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
rmSync(filepath, { force: true })
|
|
69
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeInspectNoticeState(state: InspectNoticeState) {
|
|
74
|
+
const filepath = getInspectNoticeStatePath()
|
|
75
|
+
mkdirSync(dirname(filepath), { recursive: true })
|
|
76
|
+
writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getInspectNoticeKey(kind: InspectNoticeKind, scope: string) {
|
|
80
|
+
const normalizedScope = scope.trim() || 'default'
|
|
81
|
+
return `${kind}:${normalizedScope}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function shouldPrintInspectNotice(
|
|
85
|
+
kind: InspectNoticeKind,
|
|
86
|
+
scope: string,
|
|
87
|
+
signature: string,
|
|
88
|
+
opts: { nowMs?: number; cooldownMs?: number } = {},
|
|
89
|
+
) {
|
|
90
|
+
const nowMs = opts.nowMs ?? Date.now()
|
|
91
|
+
const cooldownMs = opts.cooldownMs ?? INSPECT_NOTICE_COOLDOWN_MS
|
|
92
|
+
const state = readInspectNoticeState(nowMs)
|
|
93
|
+
const key = getInspectNoticeKey(kind, scope)
|
|
94
|
+
const previous = state.entries[key]
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
previous &&
|
|
98
|
+
previous.signature === signature &&
|
|
99
|
+
nowMs - previous.updatedAt < cooldownMs
|
|
100
|
+
) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
state.entries[key] = {
|
|
105
|
+
signature,
|
|
106
|
+
updatedAt: nowMs,
|
|
107
|
+
}
|
|
108
|
+
writeInspectNoticeState(state)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function clearInspectNoticeState() {
|
|
113
|
+
rmSync(getInspectNoticeStatePath(), { force: true })
|
|
114
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// maestro-compatible flow JS engine — mirrors maestro's GraalJsEngine
|
|
2
|
+
// (maestro-client/src/main/java/maestro/js/GraalJsEngine.kt) on top of
|
|
3
|
+
// node's vm module. one context per flow run, shared by the whole flow
|
|
4
|
+
// including runFlow sub-flows, exactly like upstream:
|
|
5
|
+
//
|
|
6
|
+
// globals: http, output (persistent map), maestro ({copiedText, platform}),
|
|
7
|
+
// json(), relativePoint(), env vars as bare globals (scope-stacked per
|
|
8
|
+
// sub-flow), console.log routed to the flow log. undeclared identifiers
|
|
9
|
+
// evaluate to undefined for `${MY_VAR || 'default'}` patterns (upstream uses
|
|
10
|
+
// a has-trapping Proxy as globalThis's prototype; node/V8 forbids that, so we
|
|
11
|
+
// get the same semantics from a with-scope proxy — see the constructor).
|
|
12
|
+
//
|
|
13
|
+
// `${expr}` string templates are full JS evaluation at command-execution
|
|
14
|
+
// time (maestro-orchestra-models util/Env.kt): regex (?<!\\)\$\{([^$]*)},
|
|
15
|
+
// result stringified, `\${...}` escapes to a literal, blank exprs become ''.
|
|
16
|
+
//
|
|
17
|
+
// divergences (deliberate, sootsim-side ergonomics — keep this list short):
|
|
18
|
+
// 1. envBinding is seeded from the full process.env, not just MAESTRO_*.
|
|
19
|
+
// sootsim flows historically resolve `${SOOTSIM_*}` credentials from the
|
|
20
|
+
// shell environment and `sootsim maestro --env K=V` writes process.env.
|
|
21
|
+
// 2. a *bare identifier* template (`${NAME}`) that evaluates to undefined
|
|
22
|
+
// throws a loud "missing variable" error instead of interpolating the
|
|
23
|
+
// literal text "undefined" into a tap target or login form. complex
|
|
24
|
+
// expressions keep upstream semantics (undefined -> "undefined").
|
|
25
|
+
// 3. `faker` is not implemented — flows using it fail loudly rather than
|
|
26
|
+
// getting a half-faithful reimplementation of datafaker.
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'child_process'
|
|
29
|
+
import * as vm from 'vm'
|
|
30
|
+
|
|
31
|
+
const HTTP_TIMEOUT_MS = 300_000 // upstream: okhttp read/write/call timeout 5min
|
|
32
|
+
|
|
33
|
+
// child process source for synchronous http from inside the vm context.
|
|
34
|
+
// maestro's `http` API is synchronous (scripts do `var res = http.post(...)`),
|
|
35
|
+
// and node has no sync fetch — so the request round-trips through a blocking
|
|
36
|
+
// spawnSync of this script. stdin: JSON request; stdout: JSON response.
|
|
37
|
+
const HTTP_CHILD_SRC = `
|
|
38
|
+
let input = '';
|
|
39
|
+
process.stdin.on('data', (c) => { input += c });
|
|
40
|
+
process.stdin.on('end', async () => {
|
|
41
|
+
try {
|
|
42
|
+
const req = JSON.parse(input);
|
|
43
|
+
const res = await fetch(req.url, {
|
|
44
|
+
method: req.method,
|
|
45
|
+
headers: req.headers,
|
|
46
|
+
body: req.body === undefined || req.body === null ? undefined : req.body,
|
|
47
|
+
});
|
|
48
|
+
const body = await res.text();
|
|
49
|
+
const headers = {};
|
|
50
|
+
for (const [k, v] of res.headers.entries()) {
|
|
51
|
+
headers[k] = headers[k] ? headers[k] + ',' + v : v;
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(JSON.stringify({ ok: res.ok, status: res.status, body, headers }));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.stdout.write(JSON.stringify({ __error: (err && err.message) || String(err) }));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
interface HttpParams {
|
|
62
|
+
body?: string
|
|
63
|
+
headers?: Record<string, string>
|
|
64
|
+
method?: string
|
|
65
|
+
multipartForm?: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function executeSyncHttp(
|
|
69
|
+
url: string,
|
|
70
|
+
method: string,
|
|
71
|
+
params?: HttpParams,
|
|
72
|
+
): { ok: boolean; status: number; body: string; headers: Record<string, string> } {
|
|
73
|
+
if (params?.multipartForm) {
|
|
74
|
+
throw new Error('http: multipartForm is not supported by rnx yet')
|
|
75
|
+
}
|
|
76
|
+
// the http child must be hermetic: when the runner runs under `bun` (or any
|
|
77
|
+
// process with a debug inspector), the inherited BUN_INSPECT*/NODE_OPTIONS
|
|
78
|
+
// env makes the spawned `bun -e` attach to the parent's inspector/IPC socket
|
|
79
|
+
// and exit 1 with no output instead of running our fetch. scrub those so the
|
|
80
|
+
// child is a clean fetch worker regardless of how the parent was launched.
|
|
81
|
+
const childEnv = { ...process.env }
|
|
82
|
+
for (const key of Object.keys(childEnv)) {
|
|
83
|
+
if (key.startsWith('BUN_INSPECT') || key === 'NODE_OPTIONS') {
|
|
84
|
+
delete childEnv[key]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const child = spawnSync(process.execPath, ['-e', HTTP_CHILD_SRC], {
|
|
88
|
+
input: JSON.stringify({ url, method, headers: params?.headers, body: params?.body }),
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
timeout: HTTP_TIMEOUT_MS,
|
|
91
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
92
|
+
env: childEnv,
|
|
93
|
+
})
|
|
94
|
+
if (child.error) {
|
|
95
|
+
throw new Error(`http ${method} ${url} failed: ${child.error.message}`)
|
|
96
|
+
}
|
|
97
|
+
let parsed: unknown
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(child.stdout || '')
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`http ${method} ${url} failed: no response (${(child.stderr || '').trim().slice(0, 200)})`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const result = parsed as {
|
|
106
|
+
__error?: string
|
|
107
|
+
ok: boolean
|
|
108
|
+
status: number
|
|
109
|
+
body: string
|
|
110
|
+
headers: Record<string, string>
|
|
111
|
+
}
|
|
112
|
+
if (result.__error) {
|
|
113
|
+
throw new Error(`http ${method} ${url} failed: ${result.__error}`)
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// matches upstream Env.kt: negative-lookbehind for the \ escape, [^$]* body.
|
|
119
|
+
const TEMPLATE_RE = /(?<!\\)\$\{([^$]*)\}/g
|
|
120
|
+
const ESCAPED_TEMPLATE_RE = /\\(\$\{[^$]*\})/g
|
|
121
|
+
const BARE_IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
122
|
+
|
|
123
|
+
export class MaestroJsContext {
|
|
124
|
+
private readonly context: vm.Context
|
|
125
|
+
private readonly envBinding = new Map<string, string>()
|
|
126
|
+
private readonly envScopeStack: Array<Map<string, string>> = []
|
|
127
|
+
// env keys currently materialized as context globals — cleared and re-set
|
|
128
|
+
// before each evaluation, mirroring upstream syncBindingsToContext.
|
|
129
|
+
private syncedEnvKeys = new Set<string>()
|
|
130
|
+
readonly output: Record<string, unknown> = {}
|
|
131
|
+
readonly maestro: { copiedText: string | null; platform: string }
|
|
132
|
+
|
|
133
|
+
constructor(opts: { platform?: string; onLog?: (msg: string) => void } = {}) {
|
|
134
|
+
this.maestro = { copiedText: null, platform: opts.platform ?? 'ios' }
|
|
135
|
+
const onLog = opts.onLog ?? ((msg: string) => console.log(`[flow] js: ${msg}`))
|
|
136
|
+
const sandbox: Record<string, unknown> = {
|
|
137
|
+
output: this.output,
|
|
138
|
+
maestro: this.maestro,
|
|
139
|
+
http: {
|
|
140
|
+
get: (url: string, params?: HttpParams) => executeSyncHttp(url, 'GET', params),
|
|
141
|
+
post: (url: string, params?: HttpParams) => executeSyncHttp(url, 'POST', params),
|
|
142
|
+
put: (url: string, params?: HttpParams) => executeSyncHttp(url, 'PUT', params),
|
|
143
|
+
delete: (url: string, params?: HttpParams) =>
|
|
144
|
+
executeSyncHttp(url, 'DELETE', params),
|
|
145
|
+
request: (url: string, params?: HttpParams) =>
|
|
146
|
+
executeSyncHttp(url, (params?.method ?? 'GET').toUpperCase(), params),
|
|
147
|
+
},
|
|
148
|
+
console: {
|
|
149
|
+
log: (...args: unknown[]) =>
|
|
150
|
+
onLog(args.map((a) => (typeof a === 'string' ? a : stringifyJs(a))).join(' ')),
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
this.context = vm.createContext(sandbox)
|
|
154
|
+
// upstream GraalJsEngine resolves undeclared identifiers to undefined via
|
|
155
|
+
// a has-trapping Proxy installed as globalThis's prototype, enabling the
|
|
156
|
+
// `${VAR || 'default'}` idiom. node/V8's vm forbids a Proxy in the global
|
|
157
|
+
// prototype chain ("Proxy is not allowed in the global prototype chain"),
|
|
158
|
+
// so we get the identical semantics with a `with`-scope proxy instead:
|
|
159
|
+
// evalRaw wraps every script in `with(__maestroScope){…}`, and the proxy's
|
|
160
|
+
// has→true makes any bare identifier resolve through the scope (returning
|
|
161
|
+
// the real global, or undefined for a name that isn't defined) rather than
|
|
162
|
+
// throwing ReferenceError. plus the json/relativePoint helpers.
|
|
163
|
+
vm.runInContext(
|
|
164
|
+
`
|
|
165
|
+
globalThis.__maestroScope = new Proxy(globalThis, {
|
|
166
|
+
has() { return true },
|
|
167
|
+
get(target, key) { return target[key] },
|
|
168
|
+
})
|
|
169
|
+
function json(text) { return JSON.parse(text) }
|
|
170
|
+
function relativePoint(x, y) {
|
|
171
|
+
var xPercent = Math.ceil(x * 100) + '%'
|
|
172
|
+
var yPercent = Math.ceil(y * 100) + '%'
|
|
173
|
+
return xPercent + ',' + yPercent
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
this.context,
|
|
177
|
+
)
|
|
178
|
+
// divergence #1: seed env from the shell so `${SOOTSIM_*}` keeps working.
|
|
179
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
180
|
+
if (value !== undefined) this.envBinding.set(key, value)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
putEnv(key: string, value: string) {
|
|
185
|
+
this.envBinding.set(key, value)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setCopiedText(text: string | null) {
|
|
189
|
+
this.maestro.copiedText = text
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
enterEnvScope() {
|
|
193
|
+
this.envScopeStack.push(new Map(this.envBinding))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
leaveEnvScope() {
|
|
197
|
+
const previous = this.envScopeStack.pop()
|
|
198
|
+
if (previous) {
|
|
199
|
+
this.envBinding.clear()
|
|
200
|
+
for (const [k, v] of previous) this.envBinding.set(k, v)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// evaluate a JS snippet in the shared context. mirrors upstream
|
|
205
|
+
// evalWithIIFE: the script is embedded in a template literal and run
|
|
206
|
+
// through eval inside an IIFE so var/let/const stay scoped to this
|
|
207
|
+
// evaluation while the last expression's value is returned.
|
|
208
|
+
evaluate(script: string, opts: { env?: Record<string, string> } = {}): unknown {
|
|
209
|
+
const env = opts.env
|
|
210
|
+
if (env && Object.keys(env).length > 0) {
|
|
211
|
+
this.enterEnvScope()
|
|
212
|
+
try {
|
|
213
|
+
for (const [k, v] of Object.entries(env)) this.envBinding.set(k, v)
|
|
214
|
+
return this.evalRaw(script)
|
|
215
|
+
} finally {
|
|
216
|
+
this.leaveEnvScope()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return this.evalRaw(script)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private evalRaw(script: string): unknown {
|
|
223
|
+
this.syncEnvToContext()
|
|
224
|
+
const escaped = script
|
|
225
|
+
.replace(/\\/g, '\\\\')
|
|
226
|
+
.replace(/`/g, '\\`')
|
|
227
|
+
.replace(/\$\{/g, '\\${')
|
|
228
|
+
// `with(__maestroScope)` gives undeclared identifiers the upstream
|
|
229
|
+
// undefined semantics (see constructor); the IIFE + direct eval keep
|
|
230
|
+
// var/let/const scoped to this evaluation while returning the last value.
|
|
231
|
+
return vm.runInContext(
|
|
232
|
+
`(function(){ with(__maestroScope){ return eval(\`${escaped}\`) } })()`,
|
|
233
|
+
this.context,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private syncEnvToContext() {
|
|
238
|
+
const globals = vm.runInContext('globalThis', this.context) as Record<string, unknown>
|
|
239
|
+
for (const key of this.syncedEnvKeys) {
|
|
240
|
+
if (!this.envBinding.has(key)) delete globals[key]
|
|
241
|
+
}
|
|
242
|
+
this.syncedEnvKeys = new Set()
|
|
243
|
+
for (const [key, value] of this.envBinding) {
|
|
244
|
+
try {
|
|
245
|
+
globals[key] = value
|
|
246
|
+
this.syncedEnvKeys.add(key)
|
|
247
|
+
} catch {
|
|
248
|
+
// non-writable global (e.g. a frozen builtin name in env) — skip
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// upstream Env.kt String.evaluateScripts: replace every unescaped `${expr}`
|
|
254
|
+
// with the stringified evaluation result, then unescape `\${...}`.
|
|
255
|
+
evaluateStringTemplate(value: string): string {
|
|
256
|
+
const replaced = value.replace(TEMPLATE_RE, (_match, expr: string) => {
|
|
257
|
+
if (expr.trim().length === 0) return ''
|
|
258
|
+
const result = this.evaluate(expr)
|
|
259
|
+
// divergence #2: a bare `${NAME}` that comes back undefined is a flow
|
|
260
|
+
// bug 100% of the time — fail loudly instead of typing "undefined".
|
|
261
|
+
if (result === undefined && BARE_IDENTIFIER_RE.test(expr.trim())) {
|
|
262
|
+
throw new Error(`missing variable for flow placeholder: ${expr.trim()}`)
|
|
263
|
+
}
|
|
264
|
+
return stringifyJs(result)
|
|
265
|
+
})
|
|
266
|
+
return replaced.replace(ESCAPED_TEMPLATE_RE, (_match, inner: string) => inner)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// walk a parsed step and evaluate every string template. nested command
|
|
270
|
+
// lists are skipped — each nested command evaluates its own strings when
|
|
271
|
+
// it executes (matching upstream per-command evaluateScripts timing, and
|
|
272
|
+
// required for values that change between iterations, e.g. copyTextFrom
|
|
273
|
+
// inside repeat).
|
|
274
|
+
interpolateStep<T>(step: T): T {
|
|
275
|
+
return this.interpolateValue(step, null) as T
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private interpolateValue(value: unknown, parentKey: string | null): unknown {
|
|
279
|
+
if (typeof value === 'string') {
|
|
280
|
+
// evalScript IS the evaluation — the runner template-evaluates it in
|
|
281
|
+
// its handler. interpolating here too would run the script twice.
|
|
282
|
+
if (parentKey === 'evalScript') return value
|
|
283
|
+
return this.evaluateStringTemplate(value)
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(value)) {
|
|
286
|
+
if (
|
|
287
|
+
parentKey === 'commands' ||
|
|
288
|
+
parentKey === 'onFlowStart' ||
|
|
289
|
+
parentKey === 'onFlowComplete'
|
|
290
|
+
) {
|
|
291
|
+
return value
|
|
292
|
+
}
|
|
293
|
+
return value.map((entry) => this.interpolateValue(entry, parentKey))
|
|
294
|
+
}
|
|
295
|
+
if (value && typeof value === 'object') {
|
|
296
|
+
return Object.fromEntries(
|
|
297
|
+
Object.entries(value).map(([key, entry]) => [
|
|
298
|
+
key,
|
|
299
|
+
this.interpolateValue(entry, key),
|
|
300
|
+
]),
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
return value
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function stringifyJs(value: unknown): string {
|
|
308
|
+
if (value === undefined) return 'undefined'
|
|
309
|
+
if (value === null) return 'null'
|
|
310
|
+
if (typeof value === 'object') {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.stringify(value)
|
|
313
|
+
} catch {
|
|
314
|
+
return String(value)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return String(value)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// upstream Orchestra.evaluateCondition scriptCondition semantics: the value
|
|
321
|
+
// arrives already template-evaluated; false when blank / "false" (any case) /
|
|
322
|
+
// "undefined" / "null" / numerically zero. yaml may also hand us a real
|
|
323
|
+
// boolean or number before stringification — normalize first.
|
|
324
|
+
export function scriptConditionIsTruthy(value: unknown): boolean {
|
|
325
|
+
if (value === undefined || value === null) return false
|
|
326
|
+
if (typeof value === 'boolean') return value
|
|
327
|
+
const text = String(value)
|
|
328
|
+
if (text.trim().length === 0) return false
|
|
329
|
+
if (text.toLowerCase() === 'false') return false
|
|
330
|
+
if (text === 'undefined' || text === 'null') return false
|
|
331
|
+
const num = Number(text)
|
|
332
|
+
if (!Number.isNaN(num) && num === 0) return false
|
|
333
|
+
return true
|
|
334
|
+
}
|