rnxsim 0.1.313 → 0.1.315
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-CBU2WDKV.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-UNB4R27L.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-YMLRF3JD.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-5N6HUCL5.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-BHDR3A6G.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-LWUVCPYK.js} +2 -2
- package/dist-cli/chunks/camera-CNO6B2JZ.js +33 -0
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-25VCR44B.js} +1 -1
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-26CSIR76.js} +8 -9
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-26WMWUH4.js} +1 -1
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-2JZ53D4A.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-2POODCTW.js} +2 -2
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-4LTBK3ZN.js} +2 -2
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-54MIRTFE.js} +4 -4
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-56H6EVO4.js} +2 -2
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-657SQG4D.js} +1 -1
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-6ZMRJPKI.js} +1 -1
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-AOGGOC4V.js} +2 -2
- package/dist-cli/chunks/chunk-BPCDO7ST.js +15 -0
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-C5O5CTXJ.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-CMM64PPR.js} +1 -1
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-D3M36D7O.js} +62 -87
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-DNCNFZ7V.js} +1 -1
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-FNTTIR2P.js} +2 -2
- package/dist-cli/chunks/chunk-H362IX2I.js +4 -0
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-J6HMTUZH.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-JZ6VTZ5B.js} +3 -3
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-KQSJVNFE.js} +4 -4
- package/dist-cli/chunks/chunk-L4JZKVCC.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-LP2BTEJ6.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-LT6UC7I3.js} +2 -2
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-MCKQH4AM.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-MF7WUHQ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-MSSY4EEZ.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-N5ZCYP5F.js} +2 -2
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-NCKAD3V7.js} +2 -2
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-O2KRHVTU.js} +2 -2
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-OF2HTH25.js} +2 -2
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-OXS2IWO2.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-PVL7XSPN.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-PVPAP3TV.js} +1 -1
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-PXNAZYGV.js} +1 -1
- package/dist-cli/chunks/chunk-QR3HTFRB.js +5 -0
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-QR7MWAWE.js} +1 -1
- package/dist-cli/chunks/chunk-RJIEZ2NY.js +4 -0
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RVM4EZ4E.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-SZKSFMWS.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-TEL7G234.js} +2 -2
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-U2HNIHBY.js} +1 -1
- package/dist-cli/chunks/chunk-UROXJVY3.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-WM32MGTJ.js} +2 -2
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-WMZ7NJAO.js} +89 -133
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-WUXCH6WN.js} +10 -9
- package/dist-cli/chunks/chunk-XOXGC3EH.js +9 -0
- package/dist-cli/chunks/chunk-XSSHENNY.js +4 -0
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-XZ6O67AR.js} +1 -1
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-Y2MWQKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-YY24QI25.js} +1 -1
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-ZWX6DDIS.js} +2 -2
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-F3NUJ3SL.js} +2 -2
- package/dist-cli/chunks/cli-version-XIW6O3FG.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-AX2SEPTH.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4QQO5JVU.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-OCKKSOWA.js} +2 -2
- package/dist-cli/chunks/daemon-N5VLMYKQ.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-T6MWVEVX.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-NCW7R7CB.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-G6GITO2X.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-7BTEJCTH.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-SC5LACE3.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KK56QQZA.js} +2 -2
- package/dist-cli/chunks/drivers-NPGVBWMG.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-WGYEHGIA.js} +3 -3
- package/dist-cli/chunks/flow-KQBIBM3N.js +4 -0
- package/dist-cli/chunks/help-G7CAKI65.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-CWTZWH3L.js} +2 -2
- package/dist-cli/chunks/home-paths-EBT4XHAS.js +4 -0
- package/dist-cli/chunks/inspect-CCPZXVDU.js +4 -0
- package/dist-cli/chunks/install-desktop-NZ3VGTQZ.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-GSYOTGVQ.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-JPRIR7YK.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-LXKDNO5M.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-5ZIKOU53.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-N3QHXCES.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-XDH6X24N.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-VOPL26FM.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-CZFKM4UD.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-FODSOGMA.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-FBPCZKOV.js} +2 -2
- package/dist-cli/chunks/reset-3H35GFO3.js +4 -0
- package/dist-cli/chunks/runtime-4OVFRXV5.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7S3FYVO7.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-CDYHPNC3.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-EBIIY2YU.js} +2 -2
- package/dist-cli/chunks/serve-NELNSCZP.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-DF5ZDDIV.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-E2QZF6WZ.js} +2 -2
- package/dist-cli/chunks/state-MBZDKZ7U.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-J5BMNVXM.js} +6 -6
- package/dist-cli/chunks/store-U3DXC7HH.js +4 -0
- package/dist-cli/chunks/telemetry-XIPJVT5E.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-MC6N5BHY.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-7UTU53DH.js} +2 -2
- package/dist-cli/chunks/upload-2M2KOTYI.js +4 -0
- package/dist-cli/chunks/version-VNACNYQK.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-MGJ2TUKR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-7DLKH3OD.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,226 @@
|
|
|
1
|
+
// shared test-run registration for both runners (Maestro / Detox). posts a
|
|
2
|
+
// `sootRun` row to /api/sootsim/test-runs so the org
|
|
3
|
+
// dashboard and PR comments get a pass/fail list with git provenance and a
|
|
4
|
+
// link into the hosted replay (the uploaded preview share).
|
|
5
|
+
//
|
|
6
|
+
// registration policy: a run registers when it produced a hosted share
|
|
7
|
+
// (--preview) or when it runs in CI (github-token / actions env) — local
|
|
8
|
+
// iteration without --preview stays local. registration failures warn and
|
|
9
|
+
// never change the run's exit code; the test result is the playback result.
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'child_process'
|
|
12
|
+
import { authHeaderValue, resolveCliAuth, type CliAuth } from './auth'
|
|
13
|
+
import type { SootSimFlowTraceStep } from './bridge-flow-runner'
|
|
14
|
+
|
|
15
|
+
export type RunKind = 'maestro' | 'detox'
|
|
16
|
+
|
|
17
|
+
export type RunProvenance = {
|
|
18
|
+
owner: string | null
|
|
19
|
+
repo: string | null
|
|
20
|
+
branch: string | null
|
|
21
|
+
commitSha: string | null
|
|
22
|
+
githubUsername: string | null
|
|
23
|
+
pullRequestNumber: number | null
|
|
24
|
+
pullRequestTitle: string | null
|
|
25
|
+
githubRunId: string | null
|
|
26
|
+
source: 'cli' | 'ci'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function gitOutput(command: string): string | null {
|
|
30
|
+
try {
|
|
31
|
+
const out = execSync(command, {
|
|
32
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
33
|
+
timeout: 3000,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
}).trim()
|
|
36
|
+
return out || null
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parsePositiveInt(value: string | undefined): number | null {
|
|
43
|
+
const trimmed = value?.trim()
|
|
44
|
+
if (!trimmed || !/^\d+$/.test(trimmed)) return null
|
|
45
|
+
return Number(trimmed)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// owner/repo from the checkout's github remote, for local runs with no
|
|
49
|
+
// CONTRAST_REPO/GITHUB_REPOSITORY env. runs are repo-scoped on the dashboard
|
|
50
|
+
// (/org/<owner>/<repo>/runs), so a repo-less registration has no home — the
|
|
51
|
+
// server 400s it. handles ssh (git@github.com:o/r.git) and https
|
|
52
|
+
// (https://github.com/o/r(.git)) remote shapes.
|
|
53
|
+
function ownerRepoFromGitRemote(): { owner: string; repo: string } | null {
|
|
54
|
+
const url = gitOutput('git remote get-url origin')
|
|
55
|
+
if (!url) return null
|
|
56
|
+
const match = url.match(/github\.com[/:]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i)
|
|
57
|
+
if (!match) return null
|
|
58
|
+
return { owner: match[1], repo: match[2] }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// git provenance for the current run. in GitHub Actions the CONTRAST_*/GITHUB_*
|
|
62
|
+
// env is authoritative (the checkout can be a detached merge ref); locally
|
|
63
|
+
// we ask git itself.
|
|
64
|
+
export function resolveRunProvenance(): RunProvenance {
|
|
65
|
+
const isActions = process.env.GITHUB_ACTIONS === 'true'
|
|
66
|
+
const contrastBranch = process.env.CONTRAST_BRANCH?.trim() || null
|
|
67
|
+
const contrastSha = process.env.CONTRAST_SHA?.trim() || null
|
|
68
|
+
const pullRequestTitle = process.env.CONTRAST_PR_TITLE?.trim() || null
|
|
69
|
+
const repoSlug = (
|
|
70
|
+
process.env.CONTRAST_REPO ||
|
|
71
|
+
process.env.GITHUB_REPOSITORY ||
|
|
72
|
+
''
|
|
73
|
+
).trim()
|
|
74
|
+
const [envOwner, envRepo] = repoSlug.includes('/')
|
|
75
|
+
? repoSlug.split('/', 2)
|
|
76
|
+
: [null, null]
|
|
77
|
+
|
|
78
|
+
if (isActions) {
|
|
79
|
+
// pull_request events: GITHUB_HEAD_REF is the PR head branch and
|
|
80
|
+
// GITHUB_REF looks like refs/pull/<n>/merge. push events: GITHUB_REF_NAME.
|
|
81
|
+
const prFromRef = process.env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\//)?.[1]
|
|
82
|
+
return {
|
|
83
|
+
owner: envOwner,
|
|
84
|
+
repo: envRepo,
|
|
85
|
+
branch:
|
|
86
|
+
contrastBranch ||
|
|
87
|
+
process.env.GITHUB_HEAD_REF?.trim() ||
|
|
88
|
+
process.env.GITHUB_REF_NAME?.trim() ||
|
|
89
|
+
null,
|
|
90
|
+
commitSha: contrastSha || process.env.GITHUB_SHA?.trim() || null,
|
|
91
|
+
githubUsername: process.env.GITHUB_ACTOR?.trim() || null,
|
|
92
|
+
pullRequestNumber:
|
|
93
|
+
parsePositiveInt(process.env.CONTRAST_PR_NUMBER) ?? parsePositiveInt(prFromRef),
|
|
94
|
+
pullRequestTitle,
|
|
95
|
+
githubRunId: process.env.GITHUB_RUN_ID?.trim() || null,
|
|
96
|
+
source: 'ci',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const branch = contrastBranch ?? gitOutput('git rev-parse --abbrev-ref HEAD')
|
|
101
|
+
// env slug wins when present; otherwise infer from the github remote so a
|
|
102
|
+
// a plain local `sootsim maestro test --preview` still lands on its repo's dashboard.
|
|
103
|
+
const remote = envOwner ? null : ownerRepoFromGitRemote()
|
|
104
|
+
return {
|
|
105
|
+
owner: envOwner ?? remote?.owner ?? null,
|
|
106
|
+
repo: envRepo ?? remote?.repo ?? null,
|
|
107
|
+
branch: branch === 'HEAD' ? null : branch,
|
|
108
|
+
commitSha: contrastSha ?? gitOutput('git rev-parse HEAD'),
|
|
109
|
+
githubUsername: null,
|
|
110
|
+
pullRequestNumber: parsePositiveInt(process.env.CONTRAST_PR_NUMBER),
|
|
111
|
+
pullRequestTitle,
|
|
112
|
+
githubRunId: null,
|
|
113
|
+
source: 'cli',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function stepSummaryFromTrace(steps: SootSimFlowTraceStep[]): {
|
|
118
|
+
stepCount: number | null
|
|
119
|
+
failedStepIndex: number | null
|
|
120
|
+
} {
|
|
121
|
+
if (steps.length === 0) return { stepCount: null, failedStepIndex: null }
|
|
122
|
+
const failed = steps.find((step) => step.status === 'failure')
|
|
123
|
+
return {
|
|
124
|
+
stepCount: steps.length,
|
|
125
|
+
failedStepIndex: failed ? failed.stepIndex : null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// whether this run should register at all — see the policy note at the top.
|
|
130
|
+
export function shouldRegisterRun(args: {
|
|
131
|
+
uploadedShare: boolean
|
|
132
|
+
auth: CliAuth | null
|
|
133
|
+
}): boolean {
|
|
134
|
+
if (!args.auth) return false
|
|
135
|
+
if (args.uploadedShare) return true
|
|
136
|
+
return args.auth.kind === 'github' || process.env.GITHUB_ACTIONS === 'true'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type RegisterRunArgs = {
|
|
140
|
+
origin: string
|
|
141
|
+
kind: RunKind
|
|
142
|
+
status: 'passed' | 'failed'
|
|
143
|
+
name?: string | null
|
|
144
|
+
prompt?: string | null
|
|
145
|
+
summary?: string | null
|
|
146
|
+
failureMessage?: string | null
|
|
147
|
+
previewShareId?: string | null
|
|
148
|
+
// explicit --owner/--repo flags override env/git detection
|
|
149
|
+
owner?: string | null
|
|
150
|
+
repo?: string | null
|
|
151
|
+
durationMs?: number | null
|
|
152
|
+
flowYamlSizeBytes?: number | null
|
|
153
|
+
stepCount?: number | null
|
|
154
|
+
failedStepIndex?: number | null
|
|
155
|
+
auth?: CliAuth | null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type RegisteredRun = {
|
|
159
|
+
id: string
|
|
160
|
+
previewUrl: string | null
|
|
161
|
+
traceUrl: string | null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function registerRun(args: RegisterRunArgs): Promise<RegisteredRun | null> {
|
|
165
|
+
const auth = args.auth ?? resolveCliAuth()
|
|
166
|
+
if (!auth) return null
|
|
167
|
+
const provenance = resolveRunProvenance()
|
|
168
|
+
const owner =
|
|
169
|
+
args.owner ?? (auth.kind === 'github' ? auth.owner : null) ?? provenance.owner
|
|
170
|
+
const repo = args.repo ?? (auth.kind === 'github' ? auth.repo : null) ?? provenance.repo
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`${args.origin.replace(/\/$/, '')}/api/sootsim/test-runs`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
authorization: authHeaderValue(auth),
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
kind: args.kind,
|
|
181
|
+
source: provenance.source,
|
|
182
|
+
name: args.name ?? undefined,
|
|
183
|
+
prompt: args.prompt ?? undefined,
|
|
184
|
+
summary: args.summary ?? undefined,
|
|
185
|
+
status: args.status,
|
|
186
|
+
failureMessage: args.failureMessage ?? undefined,
|
|
187
|
+
previewShareId: args.previewShareId ?? undefined,
|
|
188
|
+
owner: owner ?? undefined,
|
|
189
|
+
repo: repo ?? undefined,
|
|
190
|
+
branch: provenance.branch ?? undefined,
|
|
191
|
+
commitSha: provenance.commitSha ?? undefined,
|
|
192
|
+
githubUsername: provenance.githubUsername ?? undefined,
|
|
193
|
+
pullRequestNumber: provenance.pullRequestNumber ?? undefined,
|
|
194
|
+
githubRunId: provenance.githubRunId ?? undefined,
|
|
195
|
+
stepCount: args.stepCount ?? undefined,
|
|
196
|
+
failedStepIndex: args.failedStepIndex ?? undefined,
|
|
197
|
+
durationMs: args.durationMs ?? undefined,
|
|
198
|
+
flowYamlSizeBytes: args.flowYamlSizeBytes ?? undefined,
|
|
199
|
+
}),
|
|
200
|
+
})
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const text = await res.text().catch(() => '')
|
|
203
|
+
console.warn(
|
|
204
|
+
` warn: run registration failed: /api/sootsim/test-runs ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`,
|
|
205
|
+
)
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
const body = (await res.json()) as {
|
|
209
|
+
run?: { id?: string; traceUrl?: string | null; previewUrl?: string | null }
|
|
210
|
+
}
|
|
211
|
+
if (!body.run?.id) {
|
|
212
|
+
console.warn(' warn: run registration returned no id')
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
id: body.run.id,
|
|
217
|
+
previewUrl: body.run.previewUrl ?? null,
|
|
218
|
+
traceUrl: body.run.traceUrl ?? null,
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.warn(
|
|
222
|
+
` warn: run registration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
223
|
+
)
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SOOTSIM_CHANGELOG_API_URL, SOOTSIM_CHANGELOG_URL } from '../src/runtime-delivery'
|
|
2
|
+
import type { ChangelogEntry } from '../../../scripts/changelog/types'
|
|
3
|
+
|
|
4
|
+
function isChangelogEntry(value: unknown): value is ChangelogEntry {
|
|
5
|
+
if (!value || typeof value !== 'object') return false
|
|
6
|
+
const sections = Reflect.get(value, 'sections')
|
|
7
|
+
return (
|
|
8
|
+
typeof Reflect.get(value, 'title') === 'string' &&
|
|
9
|
+
Array.isArray(sections) &&
|
|
10
|
+
sections.every(
|
|
11
|
+
(section) =>
|
|
12
|
+
section &&
|
|
13
|
+
typeof section === 'object' &&
|
|
14
|
+
typeof Reflect.get(section, 'title') === 'string' &&
|
|
15
|
+
Array.isArray(Reflect.get(section, 'items')) &&
|
|
16
|
+
Reflect.get(section, 'items').every(
|
|
17
|
+
(item: unknown) =>
|
|
18
|
+
item &&
|
|
19
|
+
typeof item === 'object' &&
|
|
20
|
+
typeof Reflect.get(item, 'summary') === 'string' &&
|
|
21
|
+
typeof Reflect.get(item, 'hash') === 'string',
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchRuntimeReleaseNotes(
|
|
28
|
+
version: string,
|
|
29
|
+
request: typeof fetch = fetch,
|
|
30
|
+
): Promise<ChangelogEntry | null> {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(SOOTSIM_CHANGELOG_API_URL)
|
|
33
|
+
url.searchParams.set('version', version)
|
|
34
|
+
const response = await request(url, {
|
|
35
|
+
headers: { accept: 'application/json' },
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
})
|
|
38
|
+
if (!response.ok) return null
|
|
39
|
+
const payload: unknown = await response.json()
|
|
40
|
+
if (!payload || typeof payload !== 'object') return null
|
|
41
|
+
const entries = Reflect.get(payload, 'entries')
|
|
42
|
+
if (!Array.isArray(entries) || !isChangelogEntry(entries[0])) return null
|
|
43
|
+
return entries[0]
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatRuntimeReleaseNotes(
|
|
50
|
+
version: string,
|
|
51
|
+
entry: ChangelogEntry | null,
|
|
52
|
+
): string {
|
|
53
|
+
const lines = [`what's new in rnx engine v${version}:`]
|
|
54
|
+
if (!entry || entry.sections.length === 0) {
|
|
55
|
+
lines.push(` release notes are not available yet`)
|
|
56
|
+
} else {
|
|
57
|
+
for (const section of entry.sections) {
|
|
58
|
+
lines.push(` ${section.title}:`)
|
|
59
|
+
for (const item of section.items) {
|
|
60
|
+
lines.push(` • ${item.summary}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push(` full changelog: ${SOOTSIM_CHANGELOG_URL}`)
|
|
65
|
+
return lines.join('\n')
|
|
66
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readActiveRuntime, shouldSkipPersistentDaemon } from '../src/home-paths'
|
|
2
|
+
import { resolveBridgeWorld } from './ws-bridge'
|
|
3
|
+
|
|
4
|
+
export type RuntimeSummary = {
|
|
5
|
+
primary: string
|
|
6
|
+
installedRuntime: string | null
|
|
7
|
+
isDevBridge: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getRuntimeSummary(): RuntimeSummary {
|
|
11
|
+
const active = readActiveRuntime()
|
|
12
|
+
const dev = shouldSkipPersistentDaemon() ? resolveBridgeWorld() : null
|
|
13
|
+
if (dev?.source === 'dev-bridge') {
|
|
14
|
+
return {
|
|
15
|
+
primary: `engine dev source (:${dev.bridgePort})`,
|
|
16
|
+
installedRuntime: active ? `installed runtime v${active}` : null,
|
|
17
|
+
isDevBridge: true,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
primary: active ? `runtime v${active}` : 'runtime not installed',
|
|
22
|
+
installedRuntime: null,
|
|
23
|
+
isDevBridge: false,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { rnxPublicBrand } from '../src/public-brand'
|
|
5
|
+
|
|
6
|
+
export type SetupPackageManager = 'bun' | 'pnpm' | 'npm'
|
|
7
|
+
|
|
8
|
+
export interface RepositoryOperation {
|
|
9
|
+
command: string
|
|
10
|
+
args: string[]
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RepositorySetupPlan {
|
|
15
|
+
appDir: string
|
|
16
|
+
packageManager: SetupPackageManager
|
|
17
|
+
operations: RepositoryOperation[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PackageManifest {
|
|
21
|
+
scripts?: Record<string, string>
|
|
22
|
+
devDependencies?: Record<string, string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function findPackageRoot(startDir: string): string | null {
|
|
26
|
+
let current = path.resolve(startDir)
|
|
27
|
+
while (true) {
|
|
28
|
+
if (fs.existsSync(path.join(current, 'package.json'))) return current
|
|
29
|
+
const parent = path.dirname(current)
|
|
30
|
+
if (parent === current) return null
|
|
31
|
+
current = parent
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function detectPreferredPackageManager(startDir: string): SetupPackageManager {
|
|
36
|
+
let current = path.resolve(startDir)
|
|
37
|
+
while (true) {
|
|
38
|
+
if (fs.existsSync(path.join(current, 'bun.lock'))) return 'bun'
|
|
39
|
+
if (fs.existsSync(path.join(current, 'bun.lockb'))) return 'bun'
|
|
40
|
+
if (fs.existsSync(path.join(current, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
41
|
+
if (fs.existsSync(path.join(current, 'package-lock.json'))) return 'npm'
|
|
42
|
+
const parent = path.dirname(current)
|
|
43
|
+
if (parent === current) return 'npm'
|
|
44
|
+
current = parent
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function planRepositorySetup(input: {
|
|
49
|
+
appDir: string
|
|
50
|
+
cliVersion: string
|
|
51
|
+
}): RepositorySetupPlan {
|
|
52
|
+
const appDir = path.resolve(input.appDir)
|
|
53
|
+
const manifest = readManifest(path.join(appDir, 'package.json'))
|
|
54
|
+
const expectedScript = `${rnxPublicBrand.commandName} open`
|
|
55
|
+
const existingScript = manifest.scripts?.[rnxPublicBrand.commandName]
|
|
56
|
+
if (existingScript && existingScript !== expectedScript) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`package.json already defines scripts.${rnxPublicBrand.commandName} as ${JSON.stringify(existingScript)}; resolve that conflict before setup`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const packageManager = detectPreferredPackageManager(appDir)
|
|
63
|
+
const operations: RepositoryOperation[] = []
|
|
64
|
+
if (manifest.devDependencies?.[rnxPublicBrand.packageName] !== input.cliVersion) {
|
|
65
|
+
operations.push(addDependencyOperation(packageManager, input.cliVersion))
|
|
66
|
+
}
|
|
67
|
+
if (existingScript !== expectedScript) {
|
|
68
|
+
operations.push(scriptOperation(packageManager))
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
appDir,
|
|
72
|
+
packageManager,
|
|
73
|
+
operations,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function applyRepositorySetup(
|
|
78
|
+
plan: RepositorySetupPlan,
|
|
79
|
+
options: { dryRun: boolean; json: boolean },
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
for (const operation of plan.operations) {
|
|
82
|
+
if (options.dryRun) continue
|
|
83
|
+
await runOperation(operation, plan.appDir, options.json)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatRepositoryOperations(plan: RepositorySetupPlan): string[] {
|
|
88
|
+
return plan.operations.map(
|
|
89
|
+
(operation) => `${operation.command} ${operation.args.map(quoteArg).join(' ')}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readManifest(packagePath: string): PackageManifest {
|
|
94
|
+
let parsed: unknown
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
99
|
+
throw new Error(`could not read ${packagePath}: ${message}`)
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
102
|
+
throw new Error(`${packagePath} must contain a JSON object`)
|
|
103
|
+
}
|
|
104
|
+
const scripts = readStringRecord(Reflect.get(parsed, 'scripts'))
|
|
105
|
+
const devDependencies = readStringRecord(Reflect.get(parsed, 'devDependencies'))
|
|
106
|
+
return {
|
|
107
|
+
...(scripts ? { scripts } : {}),
|
|
108
|
+
...(devDependencies ? { devDependencies } : {}),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readStringRecord(value: unknown): Record<string, string> | null {
|
|
113
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
114
|
+
const entries = Object.entries(value).filter(
|
|
115
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
116
|
+
)
|
|
117
|
+
return Object.fromEntries(entries)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addDependencyOperation(
|
|
121
|
+
packageManager: SetupPackageManager,
|
|
122
|
+
version: string,
|
|
123
|
+
): RepositoryOperation {
|
|
124
|
+
const spec = `${rnxPublicBrand.packageName}@${version}`
|
|
125
|
+
switch (packageManager) {
|
|
126
|
+
case 'bun':
|
|
127
|
+
return {
|
|
128
|
+
command: 'bun',
|
|
129
|
+
args: ['add', '--dev', '--exact', spec],
|
|
130
|
+
description: `install ${spec} as an exact dev dependency`,
|
|
131
|
+
}
|
|
132
|
+
case 'pnpm':
|
|
133
|
+
return {
|
|
134
|
+
command: 'pnpm',
|
|
135
|
+
args: ['add', '--save-dev', '--save-exact', spec],
|
|
136
|
+
description: `install ${spec} as an exact dev dependency`,
|
|
137
|
+
}
|
|
138
|
+
case 'npm':
|
|
139
|
+
return {
|
|
140
|
+
command: 'npm',
|
|
141
|
+
args: ['install', '--save-dev', '--save-exact', spec],
|
|
142
|
+
description: `install ${spec} as an exact dev dependency`,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function quoteArg(value: string): string {
|
|
148
|
+
return /^[a-zA-Z0-9_./@=-]+$/.test(value) ? value : JSON.stringify(value)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function runOperation(
|
|
152
|
+
operation: RepositoryOperation,
|
|
153
|
+
cwd: string,
|
|
154
|
+
json: boolean,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const child = spawn(operation.command, operation.args, {
|
|
158
|
+
cwd,
|
|
159
|
+
stdio: json ? ['ignore', 2, 'inherit'] : 'inherit',
|
|
160
|
+
})
|
|
161
|
+
child.once('error', reject)
|
|
162
|
+
child.once('exit', (code) => {
|
|
163
|
+
if (code === 0) resolve()
|
|
164
|
+
else reject(new Error(`${operation.command} exited with code ${code ?? 'unknown'}`))
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scriptOperation(packageManager: SetupPackageManager): RepositoryOperation {
|
|
170
|
+
return {
|
|
171
|
+
command: packageManager,
|
|
172
|
+
args:
|
|
173
|
+
packageManager === 'bun'
|
|
174
|
+
? [
|
|
175
|
+
'pm',
|
|
176
|
+
'pkg',
|
|
177
|
+
'set',
|
|
178
|
+
`scripts.${rnxPublicBrand.commandName}=${rnxPublicBrand.commandName} open`,
|
|
179
|
+
]
|
|
180
|
+
: [
|
|
181
|
+
'pkg',
|
|
182
|
+
'set',
|
|
183
|
+
`scripts.${rnxPublicBrand.commandName}=${rnxPublicBrand.commandName} open`,
|
|
184
|
+
],
|
|
185
|
+
description: `add the \`${rnxPublicBrand.commandName}\` package script`,
|
|
186
|
+
}
|
|
187
|
+
}
|
package/cli/telemetry.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// cli-side telemetry. fire-and-forget posthog events tagged with
|
|
2
|
+
// source='cli' plus whatever identity is available from the local shared
|
|
3
|
+
// desktop auth session.
|
|
4
|
+
//
|
|
5
|
+
// this file is intentionally self-contained — no imports from Contrast `src/`
|
|
6
|
+
// or other in-monorepo packages — so the sootsim npm package stays small
|
|
7
|
+
// and doesn't drag app code into the distributed cli.
|
|
8
|
+
//
|
|
9
|
+
// scope: only emit at cli boundaries that matter for product metering
|
|
10
|
+
// (e.g. upload command start/completion). authoritative metering for
|
|
11
|
+
// uploads lives on the server; the cli event is there so we can observe
|
|
12
|
+
// cli invocation volume even when the request never reaches the server.
|
|
13
|
+
|
|
14
|
+
import { isValidTeamMachineSessionId } from '../src/agent-identity'
|
|
15
|
+
import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
|
|
16
|
+
import { readOrCreateAnonymousId, readPrivacyPreferences } from '../src/home-paths'
|
|
17
|
+
|
|
18
|
+
type Plan = 'free' | 'pro' | 'enterprise'
|
|
19
|
+
type Source = 'cli' | 'browser' | 'electron' | 'github-app' | 'server'
|
|
20
|
+
|
|
21
|
+
type Identity = {
|
|
22
|
+
userId?: string | null
|
|
23
|
+
repoId?: string | null
|
|
24
|
+
installationId?: string | number | null
|
|
25
|
+
shareId?: string | null
|
|
26
|
+
plan?: Plan | null
|
|
27
|
+
source?: Source
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CliTelemetryEvent = {
|
|
31
|
+
event: string
|
|
32
|
+
identity?: Identity
|
|
33
|
+
properties?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Config = { apiKey: string; host: string }
|
|
37
|
+
|
|
38
|
+
function isOptedOut(): boolean {
|
|
39
|
+
// standard cross-tool opt-out signal. honored by npm, homebrew, supabase,
|
|
40
|
+
// turbo, etc. — set DO_NOT_TRACK=1 to silence telemetry across the board.
|
|
41
|
+
const v = process.env.DO_NOT_TRACK
|
|
42
|
+
if (v === '1' || v === 'true') return true
|
|
43
|
+
// shared user preference written from the electron splash screen and any
|
|
44
|
+
// future cli `sootsim config telemetry off` command. false in config.json wins
|
|
45
|
+
// over an absent env var; missing/default means opted in.
|
|
46
|
+
if (!readPrivacyPreferences().productAnalytics) return true
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadConfig(): Config | null {
|
|
51
|
+
if (isOptedOut()) return null
|
|
52
|
+
// env var precedence: explicit SootSim CLI override → vite-mirrored value.
|
|
53
|
+
// the published cli bakes CONTRAST_POSTHOG_API_KEY in at build time (see
|
|
54
|
+
// scripts/build-cli.ts); a keyless build silently no-ops, keeping the
|
|
55
|
+
// beta plan's "cli remains ungated" rule.
|
|
56
|
+
const apiKey =
|
|
57
|
+
process.env.CONTRAST_POSTHOG_API_KEY || process.env.VITE_POSTHOG_API_KEY || ''
|
|
58
|
+
if (!apiKey) return null
|
|
59
|
+
const host =
|
|
60
|
+
process.env.CONTRAST_POSTHOG_HOST ||
|
|
61
|
+
process.env.VITE_POSTHOG_HOST ||
|
|
62
|
+
'https://us.i.posthog.com'
|
|
63
|
+
return { apiKey, host: host.replace(/\/+$/, '') }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function anonymousDistinctId(): string {
|
|
67
|
+
// per-machine persisted id. the old literal 'anonymous-cli' merged every
|
|
68
|
+
// signed-out user into one posthog person, hiding individual first-run
|
|
69
|
+
// journeys (like a login that hung). never let telemetry throw.
|
|
70
|
+
try {
|
|
71
|
+
return readOrCreateAnonymousId()
|
|
72
|
+
} catch {
|
|
73
|
+
return 'anonymous-cli'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveDistinctId(identity: Identity | undefined): string {
|
|
78
|
+
if (!identity) return anonymousDistinctId()
|
|
79
|
+
if (identity.userId) return identity.userId
|
|
80
|
+
if (identity.installationId != null) return `install:${identity.installationId}`
|
|
81
|
+
if (identity.repoId) return `repo:${identity.repoId}`
|
|
82
|
+
if (identity.shareId) return `share:${identity.shareId}`
|
|
83
|
+
return anonymousDistinctId()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toPosthog(event: CliTelemetryEvent) {
|
|
87
|
+
const identity = event.identity ?? {}
|
|
88
|
+
const agentSession = process.env.TM_SESSION
|
|
89
|
+
const automationSource =
|
|
90
|
+
agentSession && isValidTeamMachineSessionId(agentSession)
|
|
91
|
+
? 'team-machine'
|
|
92
|
+
: process.env.CI === '1' ||
|
|
93
|
+
process.env.CI === 'true' ||
|
|
94
|
+
process.env.GITHUB_ACTIONS === 'true'
|
|
95
|
+
? 'ci'
|
|
96
|
+
: null
|
|
97
|
+
const properties: Record<string, unknown> = {
|
|
98
|
+
$lib: 'sootsim-cli',
|
|
99
|
+
source: identity.source ?? 'cli',
|
|
100
|
+
...event.properties,
|
|
101
|
+
traffic_type: automationSource ? 'automated' : 'human',
|
|
102
|
+
is_automated: automationSource !== null,
|
|
103
|
+
...(automationSource ? { automation_source: automationSource } : {}),
|
|
104
|
+
}
|
|
105
|
+
if (identity.userId) properties.userId = identity.userId
|
|
106
|
+
if (identity.repoId) properties.repoId = identity.repoId
|
|
107
|
+
if (identity.installationId != null) {
|
|
108
|
+
properties.installationId = String(identity.installationId)
|
|
109
|
+
}
|
|
110
|
+
if (identity.shareId) properties.shareId = identity.shareId
|
|
111
|
+
if (identity.plan) properties.plan = identity.plan
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
event: event.event,
|
|
115
|
+
distinct_id: resolveDistinctId(identity),
|
|
116
|
+
properties,
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const queue: CliTelemetryEvent[] = []
|
|
122
|
+
let exitHooksInstalled = false
|
|
123
|
+
|
|
124
|
+
async function drain(): Promise<void> {
|
|
125
|
+
if (queue.length === 0) return
|
|
126
|
+
const cfg = loadConfig()
|
|
127
|
+
if (!cfg) {
|
|
128
|
+
// silently drop — the cli shouldn't die because telemetry isn't
|
|
129
|
+
// configured on this machine.
|
|
130
|
+
queue.length = 0
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const batch = queue.splice(0, queue.length).map(toPosthog)
|
|
134
|
+
try {
|
|
135
|
+
// hard timeout: a hung posthog endpoint must never block a cli
|
|
136
|
+
// command. 2s is well under any user's patience threshold and gives
|
|
137
|
+
// a healthy posthog plenty of time to ack the batch.
|
|
138
|
+
await fetch(`${cfg.host}/batch/`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ api_key: cfg.apiKey, batch }),
|
|
142
|
+
signal: AbortSignal.timeout(2000),
|
|
143
|
+
})
|
|
144
|
+
} catch {
|
|
145
|
+
// swallow — never fail a cli command because of telemetry.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function installExitHooks() {
|
|
150
|
+
if (exitHooksInstalled) return
|
|
151
|
+
exitHooksInstalled = true
|
|
152
|
+
// best-effort: beforeExit/exit handlers can't await async work, so
|
|
153
|
+
// process.exit() paths drop in-flight events. call flushCliTelemetry()
|
|
154
|
+
// explicitly before `process.exit()` at sites that enqueued events.
|
|
155
|
+
process.on('beforeExit', () => void drain())
|
|
156
|
+
process.on('exit', () => void drain())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// enrich + enqueue a cli event. reads the shared desktop session once per
|
|
160
|
+
// call so logout between events is reflected. source defaults to 'cli'.
|
|
161
|
+
export function trackCliEvent(event: CliTelemetryEvent): void {
|
|
162
|
+
if (isOptedOut()) return
|
|
163
|
+
installExitHooks()
|
|
164
|
+
|
|
165
|
+
let userId: string | null = null
|
|
166
|
+
try {
|
|
167
|
+
userId = readSharedDesktopAuthSession()?.user?.id ?? null
|
|
168
|
+
} catch {
|
|
169
|
+
userId = null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
queue.push({
|
|
173
|
+
...event,
|
|
174
|
+
identity: {
|
|
175
|
+
source: 'cli',
|
|
176
|
+
userId,
|
|
177
|
+
...event.identity,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// explicit flush — use before a process.exit() call that won't give the
|
|
183
|
+
// beforeExit hook time to complete its fetch. idempotent: safe to call
|
|
184
|
+
// multiple times; drains whatever is currently queued.
|
|
185
|
+
export async function flushCliTelemetry(): Promise<void> {
|
|
186
|
+
await drain()
|
|
187
|
+
}
|