rnxsim 0.1.312 → 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-NXVDDCFG.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-DXJCFKL3.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-W5AJZQJF.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-3LOJEUDT.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-MFTYYPNX.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-GT3NN67B.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-EUAVBTKU.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-5OX7ANRA.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-L24Z3CRS.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-BKNOLP3J.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-N6A3SXMR.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-GZDHEONW.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-YZRFF4YA.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TBMVBI26.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-JG6S4RSH.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-MENV7VLD.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-3D57IBPC.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/chunk-D2FNUWAB.js +15 -0
- package/dist-cli/chunks/{chunk-4BXCZHAU.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-FTC4M26Q.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-NPVGMH2N.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-DM6KUAOZ.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-U4PN6JOT.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMZTHU7V.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-SI3BUTQR.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-Y2FSZSO7.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-T4FT6CM7.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-2SA2VTGP.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-HG7ORJMT.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-YRV7HR3P.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-WBT32R6N.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-MNGQ42QE.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-Q3RGLERJ.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-OR3XTXEB.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/chunk-QLXXE7GE.js +125 -0
- package/dist-cli/chunks/{chunk-KZW5WBGU.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-BRIZOR4K.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-TYE3WBSR.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-7VWOSTD5.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-FWYYCUAZ.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-LDQ7HVCW.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-G74YIEVS.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-274UD5FD.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-37I5AIS7.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-B7KMNHX7.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-VZBLFMYC.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-VUFRB347.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-TCWQJILS.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-XRGEEPJ5.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-TGQNQ6XT.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-H3GWQB63.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-5WG2XJV7.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-ANMBVVLL.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-NSGNQFJG.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-CHV2O2KF.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-FIPIK4CH.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-CVUJP7K6.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-6WCUM4W6.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-I6EMWEII.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-P27CYFTN.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-UOQFFYNF.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-NJYFSJKZ.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-BPFQENCX.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-5A45ZGY7.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-E4CSDMLM.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-CKMOJQX2.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-SZYNMIQF.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-FRH4SFVG.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-VXU2XJAX.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-IRIAOVND.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-RY7FPLLH.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-KNWXOE53.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-QLALR6GY.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-OQFMFVRL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-6TSRJFGB.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7V6UKX4U.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-QQBOSETH.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-TAPMKK5O.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-IAM564NA.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-CD55QXIX.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-6NW6YLPI.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-CZZDOOVG.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 +493 -282
- 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-6EAK7CUL.js +0 -22
- package/dist-cli/chunks/chunk-33FKQH54.js +0 -5
- package/dist-cli/chunks/chunk-66NF2KNG.js +0 -9
- package/dist-cli/chunks/chunk-6UKJB2VC.js +0 -125
- package/dist-cli/chunks/chunk-7INBNA3U.js +0 -26
- package/dist-cli/chunks/chunk-AY47WKS5.js +0 -23
- package/dist-cli/chunks/chunk-HMDOLCKG.js +0 -15
- package/dist-cli/chunks/chunk-KR5BGMBZ.js +0 -4
- package/dist-cli/chunks/chunk-NLEMQ4PY.js +0 -6
- package/dist-cli/chunks/chunk-PEPUGJVI.js +0 -4
- package/dist-cli/chunks/cli-version-3QPAJ3LC.js +0 -4
- package/dist-cli/chunks/daemon-JHF4BJ3U.js +0 -4
- package/dist-cli/chunks/drivers-HQ6AKZT6.js +0 -4
- package/dist-cli/chunks/flow-N27MRRNL.js +0 -4
- package/dist-cli/chunks/help-SF6FEGEE.js +0 -4
- package/dist-cli/chunks/home-paths-XOV44JEF.js +0 -4
- package/dist-cli/chunks/inspect-CWFRFQTY.js +0 -4
- package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +0 -4
- package/dist-cli/chunks/runtime-AR3GJTGK.js +0 -4
- package/dist-cli/chunks/serve-F54J2NQO.js +0 -44
- package/dist-cli/chunks/store-7NX3S43H.js +0 -4
- package/dist-cli/chunks/telemetry-E7BUK6RA.js +0 -4
- package/dist-cli/chunks/upload-GZ6LIMYM.js +0 -4
- package/dist-cli/chunks/version-3DYJ5AS3.js +0 -6
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// driver registry — hardcoded list of known drivers plus lookup helpers.
|
|
2
|
+
// kept intentionally flat: no DI, no plugin discovery, no lazy imports. both
|
|
3
|
+
// built-in drivers load unconditionally because their module bodies are cheap
|
|
4
|
+
// (no I/O, no side effects).
|
|
5
|
+
|
|
6
|
+
import { electronDriver } from './electron'
|
|
7
|
+
import { playwrightDriver } from './playwright'
|
|
8
|
+
import type { Driver, DriverId } from './types'
|
|
9
|
+
|
|
10
|
+
export const ALL_DRIVERS: readonly Driver[] = [electronDriver, playwrightDriver]
|
|
11
|
+
|
|
12
|
+
export function getAllDrivers(): readonly Driver[] {
|
|
13
|
+
return ALL_DRIVERS
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getDriver(id: DriverId): Driver | null {
|
|
17
|
+
return ALL_DRIVERS.find((d) => d.id === id) ?? null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// returns the first driver in preference order that reports availability.
|
|
21
|
+
// used by launch sites that want "pick whatever works" behavior when the
|
|
22
|
+
// user didn't pass an explicit --driver flag. if `forcedId` is given, that
|
|
23
|
+
// driver is returned even if unavailable so the caller can surface a
|
|
24
|
+
// specific error message rather than silently falling back.
|
|
25
|
+
export function resolveDriver(
|
|
26
|
+
forcedId: string | null | undefined,
|
|
27
|
+
preference: readonly DriverId[],
|
|
28
|
+
): Driver | null {
|
|
29
|
+
if (forcedId) {
|
|
30
|
+
const match = ALL_DRIVERS.find((d) => d.id === forcedId)
|
|
31
|
+
return match ?? null
|
|
32
|
+
}
|
|
33
|
+
for (const id of preference) {
|
|
34
|
+
const driver = ALL_DRIVERS.find((d) => d.id === id)
|
|
35
|
+
if (driver && driver.availability().available) return driver
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// utility for the `sootsim list --drivers` view. returns a rich row per
|
|
41
|
+
// driver with availability + kind + description, suitable for a table.
|
|
42
|
+
export interface DriverListRow {
|
|
43
|
+
id: DriverId
|
|
44
|
+
name: string
|
|
45
|
+
kind: Driver['kind']
|
|
46
|
+
description: string
|
|
47
|
+
available: boolean
|
|
48
|
+
reason: string | null
|
|
49
|
+
detail: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildDriverListRows(): DriverListRow[] {
|
|
53
|
+
return ALL_DRIVERS.map((d) => {
|
|
54
|
+
const probe = d.availability()
|
|
55
|
+
return {
|
|
56
|
+
id: d.id,
|
|
57
|
+
name: d.name,
|
|
58
|
+
kind: d.kind,
|
|
59
|
+
description: d.description,
|
|
60
|
+
available: probe.available,
|
|
61
|
+
reason: probe.reason,
|
|
62
|
+
detail: probe.detail ?? null,
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// driver / runner abstraction for sootsim. browser sims run in an isolated
|
|
2
|
+
// playwright-owned profile. electron remains the native desktop surface.
|
|
3
|
+
|
|
4
|
+
export type DriverId = 'electron' | 'playwright'
|
|
5
|
+
|
|
6
|
+
// broad category for grouping / filtering. "native" is the electron host;
|
|
7
|
+
// "automation" is the isolated playwright browser.
|
|
8
|
+
export type DriverKind = 'native' | 'automation'
|
|
9
|
+
|
|
10
|
+
export interface DriverAvailability {
|
|
11
|
+
// true when the driver can attempt a launch on this machine. a driver is
|
|
12
|
+
// allowed to be available but still fail at runtime (network issues,
|
|
13
|
+
// permission denied, ...). false when there's a hard precondition missing
|
|
14
|
+
// (binary not installed, package not resolvable).
|
|
15
|
+
available: boolean
|
|
16
|
+
// short free-text explaining why a driver is unavailable. null when
|
|
17
|
+
// `available` is true, or when no reason is known.
|
|
18
|
+
reason: string | null
|
|
19
|
+
// optional human-readable detail shown in verbose listings — e.g. the
|
|
20
|
+
// resolved binary path, version string, or install location.
|
|
21
|
+
detail?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DriverLaunchOptions {
|
|
25
|
+
// shell URL to load in the launched process. optional because the electron
|
|
26
|
+
// driver can launch the companion app with
|
|
27
|
+
// no URL (it picks its last window). other drivers return launched:false
|
|
28
|
+
// with a helpful message when url is missing.
|
|
29
|
+
url?: string
|
|
30
|
+
// run in headless mode where the driver supports it. ignored by electron.
|
|
31
|
+
headless?: boolean
|
|
32
|
+
// initial device model seed forwarded to the sootsim shell. the electron
|
|
33
|
+
// driver forwards it via argv (--device=<model>); browser drivers receive
|
|
34
|
+
// it already stamped on the URL (?device=<model>) by runOpenCommand.
|
|
35
|
+
device?: string
|
|
36
|
+
// page viewport for browser-based drivers. runOpenCommand sizes this from
|
|
37
|
+
// the device profile (device pt size + frame chrome) so the sim isn't
|
|
38
|
+
// letterboxed inside playwright's 1280x720 default.
|
|
39
|
+
viewport?: { width: number; height: number }
|
|
40
|
+
// let the caller override the port used in availability / attach
|
|
41
|
+
// heuristics. most drivers don't need this.
|
|
42
|
+
port?: number
|
|
43
|
+
// when true, the launch is detached from the parent process and the
|
|
44
|
+
// CLI returns immediately. default true.
|
|
45
|
+
detached?: boolean
|
|
46
|
+
// named persistent storage profile. honored by drivers that own their
|
|
47
|
+
// browser context (electron, playwright). plain browser tabs reject it.
|
|
48
|
+
profileId?: string
|
|
49
|
+
// unnamed temporary storage. honored by electron/playwright.
|
|
50
|
+
ephemeralProfile?: boolean
|
|
51
|
+
// stable pid for the session that owns a CLI-opened host. electron closes
|
|
52
|
+
// only that session's windows when the owner exits; playwright requires it
|
|
53
|
+
// so a detached browser can never outlive its session. omitted only for the
|
|
54
|
+
// persistent `sootsim desktop` app lifecycle.
|
|
55
|
+
ownerPid?: number
|
|
56
|
+
// expose Chrome's remote-debugging endpoint on this port so a
|
|
57
|
+
// bridge-driven sim can also be cpu-profiled. only the playwright driver
|
|
58
|
+
// honors it (launches Chromium with --remote-debugging-port).
|
|
59
|
+
cdpPort?: number
|
|
60
|
+
// maximum time the detached host should wait for the launched sim to
|
|
61
|
+
// register with the bridge before it tears itself down.
|
|
62
|
+
connectTimeoutMs?: number
|
|
63
|
+
// the sootsim WS bridge port the launched sim page should register with,
|
|
64
|
+
// injected as window.__sootsimBridgePort. load-bearing when the dev bridge
|
|
65
|
+
// drifted off the shell-port-implied default (e.g. :7668 taken at the shell's
|
|
66
|
+
// boot -> bridge on :7669 while the shell kept :5173): the page's own
|
|
67
|
+
// shell-port heuristic would compute the stale :7668 and never connect.
|
|
68
|
+
bridgePort?: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DriverLaunchResult {
|
|
72
|
+
// true when the process was successfully spawned.
|
|
73
|
+
launched: boolean
|
|
74
|
+
// free-text describing what happened, used for CLI output and skill docs.
|
|
75
|
+
message: string
|
|
76
|
+
// optional handle details — currently free-shaped so each driver can
|
|
77
|
+
// expose what makes sense for its environment. pid for spawned processes,
|
|
78
|
+
// `target` for an install path, `attachUrl` for automation drivers.
|
|
79
|
+
pid?: number
|
|
80
|
+
target?: string
|
|
81
|
+
attachUrl?: string
|
|
82
|
+
connectAckFile?: string
|
|
83
|
+
diagnosticLogPath?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Driver {
|
|
87
|
+
id: DriverId
|
|
88
|
+
// short human name shown in `sootsim list --drivers`.
|
|
89
|
+
name: string
|
|
90
|
+
// one-line description. keep under 80 chars so the list command reads
|
|
91
|
+
// nicely at default terminal width.
|
|
92
|
+
description: string
|
|
93
|
+
kind: DriverKind
|
|
94
|
+
// probe the host environment for this driver. cheap and synchronous —
|
|
95
|
+
// anything expensive belongs in `launch`.
|
|
96
|
+
availability(): DriverAvailability
|
|
97
|
+
// start the driver pointed at `opts.url`. may be async because some
|
|
98
|
+
// drivers (electron, playwright) do real I/O. must never throw on a
|
|
99
|
+
// benign "not installed" condition — return { launched: false } with a
|
|
100
|
+
// helpful message instead.
|
|
101
|
+
launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult>
|
|
102
|
+
}
|
package/cli/flow-file.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import yaml from 'yaml'
|
|
3
|
+
|
|
4
|
+
export interface FlowFrontmatter {
|
|
5
|
+
// raw `sootsim open` target: port, dev-server URL, bundle URL, or shell URL
|
|
6
|
+
app?: string | number
|
|
7
|
+
device?: string
|
|
8
|
+
// pin the sim color scheme for this flow run (screenshot baselines are
|
|
9
|
+
// theme-sensitive; a dark capture can never match a light baseline)
|
|
10
|
+
theme?: 'light' | 'dark'
|
|
11
|
+
electron?: boolean
|
|
12
|
+
// maestro-compatible: appId is accepted but not used by sootsim (ios bundle id)
|
|
13
|
+
appId?: string
|
|
14
|
+
// maestro-compatible: flow-level env vars, defined into the flow's JS
|
|
15
|
+
// context before the first step runs
|
|
16
|
+
env?: Record<string, string>
|
|
17
|
+
// maestro-compatible: workspace tag filtering (config.yml includeTags/excludeTags)
|
|
18
|
+
tags?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// parse flow file. accepts three shapes:
|
|
22
|
+
// 1. plain yaml array of steps (no frontmatter)
|
|
23
|
+
// 2. sootsim style: ---\n<fm>\n---\n<steps>
|
|
24
|
+
// 3. maestro style: <fm>\n---\n<steps> (multi-doc yaml, fm first)
|
|
25
|
+
// maestro compat matters because real users have real .maestro/*.yaml files
|
|
26
|
+
// and shouldn't have to rewrite them to drive sootsim.
|
|
27
|
+
//
|
|
28
|
+
// `${...}` templates are NOT resolved here — interpolation happens per-step
|
|
29
|
+
// at execution time in the flow runner's JS context (matching maestro), so
|
|
30
|
+
// values produced mid-flow (runScript output.*, copyTextFrom) resolve in
|
|
31
|
+
// later steps and a missing variable fails loudly at the exact step.
|
|
32
|
+
export function parseFlowFile(content: string): {
|
|
33
|
+
frontmatter: FlowFrontmatter
|
|
34
|
+
steps: any[]
|
|
35
|
+
} {
|
|
36
|
+
let frontmatter: FlowFrontmatter = {}
|
|
37
|
+
let body = content
|
|
38
|
+
const normalizeSteps = (steps: unknown): any[] => {
|
|
39
|
+
return Array.isArray(steps) ? steps : []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// shape 2: leading ---\n<fm>\n---\n<body>
|
|
43
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/)
|
|
44
|
+
if (fmMatch) {
|
|
45
|
+
try {
|
|
46
|
+
frontmatter = (yaml.parse(fmMatch[1]) as FlowFrontmatter) ?? {}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(` warn: could not parse frontmatter: ${(e as Error).message}`)
|
|
49
|
+
}
|
|
50
|
+
body = content.slice(fmMatch[0].length)
|
|
51
|
+
const steps = yaml.parse(body)
|
|
52
|
+
return { frontmatter, steps: normalizeSteps(steps) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// shape 3: maestro multi-doc (fm-first, then `---`, then steps)
|
|
56
|
+
// detect cheaply: a `---` separator on its own line not at position 0
|
|
57
|
+
if (/\n---\s*\n/.test(content)) {
|
|
58
|
+
try {
|
|
59
|
+
const docs = yaml.parseAllDocuments(content)
|
|
60
|
+
if (docs.length >= 2) {
|
|
61
|
+
const fm = docs[0].toJS() as FlowFrontmatter | null
|
|
62
|
+
const st = docs[docs.length - 1].toJS()
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: fm && typeof fm === 'object' ? fm : {},
|
|
65
|
+
steps: normalizeSteps(st),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(` warn: could not parse multi-doc flow: ${(e as Error).message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// shape 1: plain steps array
|
|
74
|
+
const steps = yaml.parse(body)
|
|
75
|
+
return { frontmatter, steps: normalizeSteps(steps) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const INTERACTION_KEYS = new Set([
|
|
79
|
+
'tapOn',
|
|
80
|
+
'inputText',
|
|
81
|
+
'pressKey',
|
|
82
|
+
'dispatchKey',
|
|
83
|
+
'hideKeyboard',
|
|
84
|
+
'swipe',
|
|
85
|
+
'pinch',
|
|
86
|
+
'scroll',
|
|
87
|
+
'scrollUntilVisible',
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
export function validateFlowSteps(steps: any[]): string[] {
|
|
91
|
+
const issues: string[] = []
|
|
92
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
93
|
+
issues.push('flow body must be a non-empty YAML array of steps')
|
|
94
|
+
return issues
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let interactions = 0
|
|
98
|
+
for (const step of steps) {
|
|
99
|
+
if (step && typeof step === 'object') {
|
|
100
|
+
const keys = Object.keys(step)
|
|
101
|
+
if (keys.some((key) => INTERACTION_KEYS.has(key))) interactions += 1
|
|
102
|
+
} else if (typeof step === 'string' && INTERACTION_KEYS.has(step)) {
|
|
103
|
+
interactions += 1
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (interactions === 0) {
|
|
108
|
+
issues.push(
|
|
109
|
+
'no interaction steps found (expected at least one tapOn / inputText / pressKey / swipe / scroll)',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return issues
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// structural-only validator used by CI runners before uploading the flow
|
|
117
|
+
// artifact. we only check: file exists, parseable yaml, has at least one
|
|
118
|
+
// real interaction step. playback validation is a separate concern handled
|
|
119
|
+
// by `sootsim maestro end --validate` during the authoring session.
|
|
120
|
+
export function validateFlowFile(flowPath: string): string[] {
|
|
121
|
+
if (!fs.existsSync(flowPath)) {
|
|
122
|
+
return [`file not found: ${flowPath}`]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let content: string
|
|
126
|
+
try {
|
|
127
|
+
content = fs.readFileSync(flowPath, 'utf8')
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return [`cannot read file: ${(e as Error).message}`]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (content.trim().length === 0) {
|
|
133
|
+
return ['file is empty']
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const parsed = parseFlowFile(content)
|
|
138
|
+
return validateFlowSteps(parsed.steps)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return [`yaml parse error: ${(e as Error).message}`]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// flow-live-status — pushes the flow runner's live progress into the sim it
|
|
2
|
+
// is driving, over the same ws bridge the steps execute on. the engine's
|
|
3
|
+
// flow-run-store renders it in the shell devtools "test" tab and replies
|
|
4
|
+
// with the pause intent, which the runner honors between steps.
|
|
5
|
+
//
|
|
6
|
+
// pushes are best-effort: an engine without the `flowStatus` handler (an
|
|
7
|
+
// older shell) throws "unknown command type" once and the reporter disables
|
|
8
|
+
// itself for the rest of the run. live status must never fail a test.
|
|
9
|
+
|
|
10
|
+
import type { WsBridge } from './ws-bridge'
|
|
11
|
+
|
|
12
|
+
export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
|
|
13
|
+
|
|
14
|
+
type PlanStep = { index: number; name: string; target?: string }
|
|
15
|
+
|
|
16
|
+
const PAUSE_POLL_MS = 400
|
|
17
|
+
|
|
18
|
+
function replyPaused(reply: unknown) {
|
|
19
|
+
if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
|
|
20
|
+
const control = reply.control
|
|
21
|
+
return (
|
|
22
|
+
typeof control === 'object' &&
|
|
23
|
+
control != null &&
|
|
24
|
+
'paused' in control &&
|
|
25
|
+
control.paused === true
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class FlowLiveStatusReporter {
|
|
30
|
+
private disabled = false
|
|
31
|
+
private paused = false
|
|
32
|
+
private lastSimId: string | undefined
|
|
33
|
+
private plannedSteps: PlanStep[] | null = null
|
|
34
|
+
private readonly completedSteps = new Map<
|
|
35
|
+
number,
|
|
36
|
+
{ index: number; status: LiveStepStatus; durationMs?: number; error?: string }
|
|
37
|
+
>()
|
|
38
|
+
private readonly runId = `flr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private bridge: WsBridge,
|
|
42
|
+
private flowName: string | null,
|
|
43
|
+
private getSimId?: () => string | undefined,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
private async send(payload: Record<string, unknown>, simId: string | undefined) {
|
|
47
|
+
const reply: unknown = await this.bridge.send({
|
|
48
|
+
type: 'flowStatus',
|
|
49
|
+
simId,
|
|
50
|
+
status: { runId: this.runId, flowName: this.flowName, ...payload },
|
|
51
|
+
})
|
|
52
|
+
this.paused = replyPaused(reply)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async push(payload: Record<string, unknown>): Promise<void> {
|
|
56
|
+
if (this.disabled) return
|
|
57
|
+
try {
|
|
58
|
+
const simId = this.getSimId?.()
|
|
59
|
+
if (
|
|
60
|
+
simId !== this.lastSimId &&
|
|
61
|
+
this.lastSimId != null &&
|
|
62
|
+
this.plannedSteps &&
|
|
63
|
+
payload.phase !== 'plan'
|
|
64
|
+
) {
|
|
65
|
+
await this.send(
|
|
66
|
+
{ phase: 'plan', state: 'running', steps: this.plannedSteps },
|
|
67
|
+
simId,
|
|
68
|
+
)
|
|
69
|
+
for (const step of this.completedSteps.values()) {
|
|
70
|
+
await this.send({ phase: 'step', state: 'running', step }, simId)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.lastSimId = simId
|
|
74
|
+
await this.send(payload, simId)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this.disabled = true
|
|
77
|
+
console.log(
|
|
78
|
+
`[flow] live status unavailable (${error instanceof Error ? error.message.slice(0, 120) : error}) — continuing without the devtools rail`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async plan(steps: PlanStep[]): Promise<void> {
|
|
84
|
+
this.plannedSteps = steps.map((step) => ({ ...step }))
|
|
85
|
+
this.completedSteps.clear()
|
|
86
|
+
await this.push({ phase: 'plan', state: 'running', steps })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async step(
|
|
90
|
+
index: number,
|
|
91
|
+
status: LiveStepStatus,
|
|
92
|
+
extra?: { durationMs?: number; error?: string },
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const step = { index, status, ...extra }
|
|
95
|
+
await this.push({
|
|
96
|
+
phase: 'step',
|
|
97
|
+
state: 'running',
|
|
98
|
+
step,
|
|
99
|
+
})
|
|
100
|
+
this.completedSteps.set(index, step)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async end(state: 'passed' | 'failed'): Promise<void> {
|
|
104
|
+
await this.push({ phase: 'end', state })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// park while the devtools pause toggle is on. checked between top-level
|
|
108
|
+
// steps only, so a pause never interrupts a step mid-gesture.
|
|
109
|
+
async waitWhilePaused(): Promise<void> {
|
|
110
|
+
if (this.disabled) return
|
|
111
|
+
while (this.paused) {
|
|
112
|
+
console.log('[flow] paused from devtools — waiting…')
|
|
113
|
+
while (this.paused && !this.disabled) {
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, PAUSE_POLL_MS))
|
|
115
|
+
await this.push({ phase: 'heartbeat', state: 'paused' })
|
|
116
|
+
}
|
|
117
|
+
if (!this.disabled) console.log('[flow] resumed')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { dirname, join, resolve } from 'path'
|
|
4
|
+
import yaml from 'yaml'
|
|
5
|
+
import { validateFlowSteps } from './flow-file'
|
|
6
|
+
|
|
7
|
+
export type FlowDraftStep = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
export interface FlowCandidate {
|
|
10
|
+
step: FlowDraftStep
|
|
11
|
+
summary: string
|
|
12
|
+
source: string
|
|
13
|
+
recordedAt: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FlowSessionState {
|
|
17
|
+
version: 1
|
|
18
|
+
startedAt: string
|
|
19
|
+
updatedAt: string
|
|
20
|
+
steps: FlowDraftStep[]
|
|
21
|
+
candidate: FlowCandidate | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FLOW_SESSION_VERSION = 1 as const
|
|
25
|
+
|
|
26
|
+
export function getFlowSessionPath() {
|
|
27
|
+
return resolve(
|
|
28
|
+
process.env.SOOTSIM_FLOW_SESSION_PATH || join(tmpdir(), 'sootsim-flow-session.json'),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeFlowSession(state: FlowSessionState) {
|
|
33
|
+
const filepath = getFlowSessionPath()
|
|
34
|
+
mkdirSync(dirname(filepath), { recursive: true })
|
|
35
|
+
writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readFlowSession(): FlowSessionState | null {
|
|
39
|
+
const filepath = getFlowSessionPath()
|
|
40
|
+
if (!existsSync(filepath)) return null
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial<FlowSessionState>
|
|
44
|
+
if (
|
|
45
|
+
parsed.version !== FLOW_SESSION_VERSION ||
|
|
46
|
+
!Array.isArray(parsed.steps) ||
|
|
47
|
+
typeof parsed.startedAt !== 'string' ||
|
|
48
|
+
typeof parsed.updatedAt !== 'string'
|
|
49
|
+
) {
|
|
50
|
+
rmSync(filepath, { force: true })
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
version: FLOW_SESSION_VERSION,
|
|
56
|
+
startedAt: parsed.startedAt,
|
|
57
|
+
updatedAt: parsed.updatedAt,
|
|
58
|
+
steps: parsed.steps as FlowDraftStep[],
|
|
59
|
+
candidate: parsed.candidate ?? null,
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
rmSync(filepath, { force: true })
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function hasActiveFlowSession() {
|
|
68
|
+
return readFlowSession() !== null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function startFlowSession() {
|
|
72
|
+
const now = new Date().toISOString()
|
|
73
|
+
const state: FlowSessionState = {
|
|
74
|
+
version: FLOW_SESSION_VERSION,
|
|
75
|
+
startedAt: now,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
steps: [],
|
|
78
|
+
candidate: null,
|
|
79
|
+
}
|
|
80
|
+
writeFlowSession(state)
|
|
81
|
+
return { path: getFlowSessionPath(), state }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clearFlowSession() {
|
|
85
|
+
rmSync(getFlowSessionPath(), { force: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function finalizeFlowSession() {
|
|
89
|
+
clearFlowSession()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function rememberFlowCandidate(input: {
|
|
93
|
+
step: FlowDraftStep
|
|
94
|
+
summary: string
|
|
95
|
+
source: string
|
|
96
|
+
}) {
|
|
97
|
+
const state = readFlowSession()
|
|
98
|
+
if (!state) return { active: false as const }
|
|
99
|
+
|
|
100
|
+
// a pending candidate that was never `maestro keep`'d is silently dropped
|
|
101
|
+
// when a new action arrives — that's the "skip mistakes" model. surface
|
|
102
|
+
// the replaced action so the caller can warn (a kept action is never
|
|
103
|
+
// lost; an unkept one is).
|
|
104
|
+
const replaced = state.candidate
|
|
105
|
+
const next: FlowSessionState = {
|
|
106
|
+
...state,
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
108
|
+
candidate: {
|
|
109
|
+
step: input.step,
|
|
110
|
+
summary: input.summary,
|
|
111
|
+
source: input.source,
|
|
112
|
+
recordedAt: new Date().toISOString(),
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
writeFlowSession(next)
|
|
116
|
+
return { active: true as const, candidate: next.candidate, replaced }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function keepFlowCandidate() {
|
|
120
|
+
const state = readFlowSession()
|
|
121
|
+
if (!state)
|
|
122
|
+
return { active: false as const, kept: false as const, reason: 'no-session' }
|
|
123
|
+
if (!state.candidate) {
|
|
124
|
+
return { active: true as const, kept: false as const, reason: 'no-candidate' }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const keptCandidate = state.candidate
|
|
128
|
+
const next: FlowSessionState = {
|
|
129
|
+
...state,
|
|
130
|
+
updatedAt: new Date().toISOString(),
|
|
131
|
+
steps: [...state.steps, keptCandidate.step],
|
|
132
|
+
candidate: null,
|
|
133
|
+
}
|
|
134
|
+
writeFlowSession(next)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
active: true as const,
|
|
138
|
+
kept: true as const,
|
|
139
|
+
candidate: keptCandidate,
|
|
140
|
+
stepCount: next.steps.length,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatFlowDraftYaml(steps: FlowDraftStep[]) {
|
|
145
|
+
return yaml.stringify(steps).trimEnd() + '\n'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function exportFlowSession(outputPath?: string) {
|
|
149
|
+
const state = readFlowSession()
|
|
150
|
+
if (!state) return { active: false as const }
|
|
151
|
+
|
|
152
|
+
const yamlText = formatFlowDraftYaml(state.steps)
|
|
153
|
+
const issues = validateFlowSteps(state.steps)
|
|
154
|
+
if (issues.length > 0) {
|
|
155
|
+
return {
|
|
156
|
+
active: true as const,
|
|
157
|
+
valid: false as const,
|
|
158
|
+
issues,
|
|
159
|
+
yaml: yamlText,
|
|
160
|
+
stepCount: state.steps.length,
|
|
161
|
+
outputPath: null,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let resolvedOutputPath: string | null = null
|
|
166
|
+
if (outputPath) {
|
|
167
|
+
resolvedOutputPath = resolve(outputPath)
|
|
168
|
+
mkdirSync(dirname(resolvedOutputPath), { recursive: true })
|
|
169
|
+
writeFileSync(resolvedOutputPath, yamlText)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
active: true as const,
|
|
174
|
+
valid: true as const,
|
|
175
|
+
issues: [],
|
|
176
|
+
yaml: yamlText,
|
|
177
|
+
stepCount: state.steps.length,
|
|
178
|
+
outputPath: resolvedOutputPath,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function endFlowSession(outputPath?: string) {
|
|
183
|
+
const result = exportFlowSession(outputPath)
|
|
184
|
+
if (!result.active) return result
|
|
185
|
+
finalizeFlowSession()
|
|
186
|
+
return result
|
|
187
|
+
}
|
package/cli/help.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderCliCommandHelp,
|
|
3
|
+
renderCliGroupHelp,
|
|
4
|
+
renderCliIndexHelp,
|
|
5
|
+
} from '@sootsim/skills/cli/renderers'
|
|
6
|
+
import { formatFlagHelp } from 'sootsim-engine/settings/cli-flags'
|
|
7
|
+
import { BETA_TAGLINE, IS_BETA } from '../src/beta'
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
10
|
+
DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
11
|
+
} from '../src/bridge-constants'
|
|
12
|
+
import { getCliVersion } from '../src/cli-version'
|
|
13
|
+
import { rnxPublicBrand } from '../src/public-brand'
|
|
14
|
+
import { maybeHint } from './hints'
|
|
15
|
+
import { getRuntimeSummary } from './runtime-summary'
|
|
16
|
+
|
|
17
|
+
// header line shown at the top of `--help` so users can always tell what
|
|
18
|
+
// they're running — the CLI npm version and the active engine runtime
|
|
19
|
+
// version are independent, which is exactly the confusion `sootsim
|
|
20
|
+
// upgrade` exists to clear up.
|
|
21
|
+
function versionSummaryLine(): string {
|
|
22
|
+
return `${rnxPublicBrand.commandName} CLI v${getCliVersion()} · ${getRuntimeSummary().primary}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function printHelp() {
|
|
26
|
+
const base = renderCliIndexHelp(
|
|
27
|
+
{
|
|
28
|
+
bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
29
|
+
defaultShellUrl: DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
30
|
+
},
|
|
31
|
+
formatFlagHelp(),
|
|
32
|
+
)
|
|
33
|
+
const betaLine = IS_BETA ? `\n${BETA_TAGLINE}\n` : ''
|
|
34
|
+
console.log(
|
|
35
|
+
`${versionSummaryLine()}\n${betaLine}${base}
|
|
36
|
+
`,
|
|
37
|
+
)
|
|
38
|
+
// once-ever nudge so users learn the clean teardown path before they need it.
|
|
39
|
+
// the npm preuninstall hook runs the same teardown automatically when it
|
|
40
|
+
// fires; this is the documented manual fallback.
|
|
41
|
+
maybeHint('clean-uninstall')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// bare `sootsim` prints the registry-backed surface. setup/open own work.
|
|
45
|
+
|
|
46
|
+
// print the help page for a grouping verb (do / get / debug / wait) built
|
|
47
|
+
// entirely from the registry — no hardcoded strings. returns true if the
|
|
48
|
+
// group was known, false if the caller should fall through.
|
|
49
|
+
export function printGroupHelp(group: string): boolean {
|
|
50
|
+
const output = renderCliGroupHelp(
|
|
51
|
+
group,
|
|
52
|
+
{
|
|
53
|
+
bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
54
|
+
defaultShellUrl: DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
55
|
+
},
|
|
56
|
+
formatFlagHelp(),
|
|
57
|
+
)
|
|
58
|
+
if (!output) return false
|
|
59
|
+
console.log(`${output}\n`)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function printCommandHelp(
|
|
64
|
+
name: string,
|
|
65
|
+
options: { prefer?: 'command' | 'verb'; group?: string | null } = {},
|
|
66
|
+
) {
|
|
67
|
+
const output = renderCliCommandHelp(
|
|
68
|
+
name,
|
|
69
|
+
{
|
|
70
|
+
bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
71
|
+
defaultShellUrl: DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
72
|
+
},
|
|
73
|
+
options,
|
|
74
|
+
)
|
|
75
|
+
if (!output) {
|
|
76
|
+
console.log(`unknown command: ${name}`)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
console.log(`${output}\n`)
|
|
80
|
+
}
|