@monoes/monobrowse 1.0.11 → 1.0.13
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/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +5 -5
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
- package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
- package/dist/src/__tests__/profiler-heap.test.js +154 -0
- package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
- package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-a11y.test.js +147 -0
- package/dist/src/__tests__/report-a11y.test.js.map +1 -0
- package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
- package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-analyze.test.js +157 -0
- package/dist/src/__tests__/report-analyze.test.js.map +1 -0
- package/dist/src/__tests__/report-budget.test.d.ts +2 -0
- package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-budget.test.js +80 -0
- package/dist/src/__tests__/report-budget.test.js.map +1 -0
- package/dist/src/__tests__/report-diff.test.d.ts +2 -0
- package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-diff.test.js +231 -0
- package/dist/src/__tests__/report-diff.test.js.map +1 -0
- package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
- package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-evidence.test.js +214 -0
- package/dist/src/__tests__/report-evidence.test.js.map +1 -0
- package/dist/src/__tests__/report-flake.test.d.ts +2 -0
- package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-flake.test.js +182 -0
- package/dist/src/__tests__/report-flake.test.js.map +1 -0
- package/dist/src/__tests__/report-history.test.d.ts +2 -0
- package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-history.test.js +229 -0
- package/dist/src/__tests__/report-history.test.js.map +1 -0
- package/dist/src/__tests__/report-output.test.d.ts +2 -0
- package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-output.test.js +169 -0
- package/dist/src/__tests__/report-output.test.js.map +1 -0
- package/dist/src/__tests__/report-png.test.d.ts +2 -0
- package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-png.test.js +84 -0
- package/dist/src/__tests__/report-png.test.js.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.js +285 -0
- package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
- package/dist/src/__tests__/report-render.test.d.ts +2 -0
- package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render.test.js +143 -0
- package/dist/src/__tests__/report-render.test.js.map +1 -0
- package/dist/src/__tests__/report-run.test.d.ts +2 -0
- package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-run.test.js +234 -0
- package/dist/src/__tests__/report-run.test.js.map +1 -0
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +44 -14
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +17 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +17 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +22 -20
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +707 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/report/a11y.d.ts +52 -0
- package/dist/src/report/a11y.d.ts.map +1 -0
- package/dist/src/report/a11y.js +231 -0
- package/dist/src/report/a11y.js.map +1 -0
- package/dist/src/report/analyze.d.ts +20 -0
- package/dist/src/report/analyze.d.ts.map +1 -0
- package/dist/src/report/analyze.js +136 -0
- package/dist/src/report/analyze.js.map +1 -0
- package/dist/src/report/budget.d.ts +27 -0
- package/dist/src/report/budget.d.ts.map +1 -0
- package/dist/src/report/budget.js +143 -0
- package/dist/src/report/budget.js.map +1 -0
- package/dist/src/report/collect-a11y.d.ts +25 -0
- package/dist/src/report/collect-a11y.d.ts.map +1 -0
- package/dist/src/report/collect-a11y.js +154 -0
- package/dist/src/report/collect-a11y.js.map +1 -0
- package/dist/src/report/collect.d.ts +45 -0
- package/dist/src/report/collect.d.ts.map +1 -0
- package/dist/src/report/collect.js +275 -0
- package/dist/src/report/collect.js.map +1 -0
- package/dist/src/report/evidence.d.ts +80 -0
- package/dist/src/report/evidence.d.ts.map +1 -0
- package/dist/src/report/evidence.js +279 -0
- package/dist/src/report/evidence.js.map +1 -0
- package/dist/src/report/flake.d.ts +88 -0
- package/dist/src/report/flake.d.ts.map +1 -0
- package/dist/src/report/flake.js +232 -0
- package/dist/src/report/flake.js.map +1 -0
- package/dist/src/report/history.d.ts +85 -0
- package/dist/src/report/history.d.ts.map +1 -0
- package/dist/src/report/history.js +176 -0
- package/dist/src/report/history.js.map +1 -0
- package/dist/src/report/index.d.ts +102 -0
- package/dist/src/report/index.d.ts.map +1 -0
- package/dist/src/report/index.js +216 -0
- package/dist/src/report/index.js.map +1 -0
- package/dist/src/report/pixel-diff.d.ts +30 -0
- package/dist/src/report/pixel-diff.d.ts.map +1 -0
- package/dist/src/report/pixel-diff.js +175 -0
- package/dist/src/report/pixel-diff.js.map +1 -0
- package/dist/src/report/png.d.ts +28 -0
- package/dist/src/report/png.d.ts.map +1 -0
- package/dist/src/report/png.js +259 -0
- package/dist/src/report/png.js.map +1 -0
- package/dist/src/report/render-evidence.d.ts +16 -0
- package/dist/src/report/render-evidence.d.ts.map +1 -0
- package/dist/src/report/render-evidence.js +174 -0
- package/dist/src/report/render-evidence.js.map +1 -0
- package/dist/src/report/render-trend.d.ts +16 -0
- package/dist/src/report/render-trend.d.ts.map +1 -0
- package/dist/src/report/render-trend.js +194 -0
- package/dist/src/report/render-trend.js.map +1 -0
- package/dist/src/report/render.d.ts +21 -0
- package/dist/src/report/render.d.ts.map +1 -0
- package/dist/src/report/render.js +286 -0
- package/dist/src/report/render.js.map +1 -0
- package/dist/src/report/run-diff.d.ts +18 -0
- package/dist/src/report/run-diff.d.ts.map +1 -0
- package/dist/src/report/run-diff.js +51 -0
- package/dist/src/report/run-diff.js.map +1 -0
- package/dist/src/report/structure.d.ts +41 -0
- package/dist/src/report/structure.d.ts.map +1 -0
- package/dist/src/report/structure.js +210 -0
- package/dist/src/report/structure.js.map +1 -0
- package/dist/src/report/trend.d.ts +26 -0
- package/dist/src/report/trend.d.ts.map +1 -0
- package/dist/src/report/trend.js +164 -0
- package/dist/src/report/trend.js.map +1 -0
- package/dist/src/report/types.d.ts +320 -0
- package/dist/src/report/types.d.ts.map +1 -0
- package/dist/src/report/types.js +11 -0
- package/dist/src/report/types.js.map +1 -0
- package/dist/src/report/util.d.ts +17 -0
- package/dist/src/report/util.d.ts.map +1 -0
- package/dist/src/report/util.js +45 -0
- package/dist/src/report/util.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +11 -6
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/profiler-heap.test.ts +191 -0
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/report-a11y.test.ts +180 -0
- package/src/__tests__/report-analyze.test.ts +202 -0
- package/src/__tests__/report-budget.test.ts +92 -0
- package/src/__tests__/report-diff.test.ts +272 -0
- package/src/__tests__/report-evidence.test.ts +276 -0
- package/src/__tests__/report-flake.test.ts +214 -0
- package/src/__tests__/report-history.test.ts +293 -0
- package/src/__tests__/report-output.test.ts +210 -0
- package/src/__tests__/report-png.test.ts +95 -0
- package/src/__tests__/report-render-extras.test.ts +315 -0
- package/src/__tests__/report-render.test.ts +175 -0
- package/src/__tests__/report-run.test.ts +264 -0
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +60 -21
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +17 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +39 -19
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1365 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
- package/src/report/a11y.ts +242 -0
- package/src/report/analyze.ts +181 -0
- package/src/report/budget.ts +155 -0
- package/src/report/collect-a11y.ts +200 -0
- package/src/report/collect.ts +408 -0
- package/src/report/evidence.ts +379 -0
- package/src/report/flake.ts +303 -0
- package/src/report/history.ts +219 -0
- package/src/report/index.ts +307 -0
- package/src/report/pixel-diff.ts +197 -0
- package/src/report/png.ts +290 -0
- package/src/report/render-evidence.ts +194 -0
- package/src/report/render-trend.ts +225 -0
- package/src/report/render.ts +314 -0
- package/src/report/run-diff.ts +63 -0
- package/src/report/structure.ts +212 -0
- package/src/report/trend.ts +189 -0
- package/src/report/types.ts +356 -0
- package/src/report/util.ts +48 -0
package/src/cli/commands.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Provides ref-based element model and token-efficient accessibility snapshots
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import { output } from './output.js';
|
|
6
|
+
import type { CdpClient, ElementRef, FindAction, NetworkRoute } from '../index.js';
|
|
8
7
|
import { createActionCommand } from './action.js';
|
|
8
|
+
import { output } from './output.js';
|
|
9
9
|
import { createPlatformCommand } from './platform.js';
|
|
10
|
-
import type {
|
|
10
|
+
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
11
11
|
|
|
12
12
|
// Runtime state (single session per CLI process)
|
|
13
13
|
const DEFAULT_PORT = 9222;
|
|
@@ -51,7 +51,11 @@ function ensureSignalCleanupHandlers(): void {
|
|
|
51
51
|
} else {
|
|
52
52
|
const pid = browser.getLaunchedPid(_port);
|
|
53
53
|
if (pid !== undefined) {
|
|
54
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 'SIGKILL');
|
|
56
|
+
} catch {
|
|
57
|
+
/* already exited */
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
} catch {
|
|
@@ -69,7 +73,9 @@ function ensureSignalCleanupHandlers(): void {
|
|
|
69
73
|
// ref-cache.ts's saveActivePort/loadActivePort) so subsequent commands
|
|
70
74
|
// attach to the browser the user actually opened instead of silently
|
|
71
75
|
// launching/attaching to a second, unrelated Chrome instance on 9222.
|
|
72
|
-
async function resolveDefaultPort(
|
|
76
|
+
async function resolveDefaultPort(
|
|
77
|
+
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
78
|
+
): Promise<number> {
|
|
73
79
|
const persisted = await browser.loadActivePortInfo();
|
|
74
80
|
if (!persisted) return DEFAULT_PORT;
|
|
75
81
|
if (!persisted.launched) {
|
|
@@ -78,10 +84,14 @@ async function resolveDefaultPort(browser: Awaited<ReturnType<typeof getBrowser>
|
|
|
78
84
|
// user's debug port and silently swap which browser commands act on —
|
|
79
85
|
// fail loudly instead.
|
|
80
86
|
try {
|
|
81
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
87
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
88
|
+
signal: AbortSignal.timeout(1500),
|
|
89
|
+
});
|
|
82
90
|
} catch {
|
|
83
91
|
await browser.clearActivePort();
|
|
84
|
-
throw new Error(
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Connected browser on port ${persisted.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`,
|
|
94
|
+
);
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
return persisted.port;
|
|
@@ -89,7 +99,7 @@ async function resolveDefaultPort(browser: Awaited<ReturnType<typeof getBrowser>
|
|
|
89
99
|
|
|
90
100
|
async function ensureConnected(port: number, targetId?: string) {
|
|
91
101
|
const browser = await getBrowser();
|
|
92
|
-
if (!_client
|
|
102
|
+
if (!_client?.isConnected()) {
|
|
93
103
|
if (_client && _sessionId) {
|
|
94
104
|
browser.teardownRouteInterception(_sessionId);
|
|
95
105
|
browser.stopRequestCapture(_sessionId);
|
|
@@ -128,20 +138,20 @@ async function ensureConnected(port: number, targetId?: string) {
|
|
|
128
138
|
async function hydrateRefsFromCache(
|
|
129
139
|
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
130
140
|
targetId: string,
|
|
131
|
-
currentUrl: string
|
|
141
|
+
currentUrl: string,
|
|
132
142
|
): Promise<void> {
|
|
133
143
|
const cached = await browser.loadRefCache(targetId);
|
|
134
144
|
if (!cached) return;
|
|
135
145
|
if (currentUrl && cached.url && currentUrl !== cached.url) {
|
|
136
146
|
output.printError(
|
|
137
|
-
`Stale references — page has navigated (cache: ${cached.url} → current: ${currentUrl}). Re-run snapshot before using @eN refs
|
|
147
|
+
`Stale references — page has navigated (cache: ${cached.url} → current: ${currentUrl}). Re-run snapshot before using @eN refs.`,
|
|
138
148
|
);
|
|
139
149
|
return; // leave _refs empty — do not attempt to resolve refs against a different page
|
|
140
150
|
}
|
|
141
151
|
_refs = cached.refs;
|
|
142
152
|
if (cached.stale) {
|
|
143
153
|
output.printWarning(
|
|
144
|
-
`AX ref cache is ${Math.round(cached.ageMs / 1000)}s old — page may have changed since the last snapshot; re-run snapshot if refs don't resolve as expected
|
|
154
|
+
`AX ref cache is ${Math.round(cached.ageMs / 1000)}s old — page may have changed since the last snapshot; re-run snapshot if refs don't resolve as expected`,
|
|
145
155
|
);
|
|
146
156
|
}
|
|
147
157
|
}
|
|
@@ -150,23 +160,45 @@ async function hydrateRefsFromCache(
|
|
|
150
160
|
// Excludes 'auth', 'oauth', 'sso', 'saml' — their callback/ACS/token paths
|
|
151
161
|
// (/auth/callback, /sso/callback, /saml/acs) are completion endpoints, not walls.
|
|
152
162
|
// DOM detection (password field, CAPTCHA widgets) handles SSO/SAML login pages.
|
|
153
|
-
const ATTENTION_URL_RE =
|
|
163
|
+
const ATTENTION_URL_RE =
|
|
164
|
+
/\/(login|log-in|signin|sign-in|captcha|mfa|2fa|account\/login|accounts\/login|session\/new|users\/sign_in)(?:[/?#]|$)/i;
|
|
154
165
|
|
|
155
|
-
async function detectAttentionNeeded(
|
|
166
|
+
async function detectAttentionNeeded(
|
|
167
|
+
client: CdpClient,
|
|
168
|
+
sessionId: string,
|
|
169
|
+
url: string,
|
|
170
|
+
): Promise<'login' | 'captcha' | null> {
|
|
156
171
|
if (ATTENTION_URL_RE.test(url)) return 'login';
|
|
157
172
|
try {
|
|
158
|
-
const hasPassword = await client
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
const hasPassword = await client
|
|
174
|
+
.send<{ result: { value: boolean } }>(
|
|
175
|
+
'Runtime.evaluate',
|
|
176
|
+
{
|
|
177
|
+
expression: '!!document.querySelector("input[type=password]")',
|
|
178
|
+
returnByValue: true,
|
|
179
|
+
},
|
|
180
|
+
sessionId,
|
|
181
|
+
)
|
|
182
|
+
.then((r) => r.result?.value === true)
|
|
183
|
+
.catch(() => false);
|
|
162
184
|
if (hasPassword) return 'login';
|
|
163
185
|
|
|
164
|
-
const hasCaptcha = await client
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
const hasCaptcha = await client
|
|
187
|
+
.send<{ result: { value: boolean } }>(
|
|
188
|
+
'Runtime.evaluate',
|
|
189
|
+
{
|
|
190
|
+
expression:
|
|
191
|
+
'!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
|
|
192
|
+
returnByValue: true,
|
|
193
|
+
},
|
|
194
|
+
sessionId,
|
|
195
|
+
)
|
|
196
|
+
.then((r) => r.result?.value === true)
|
|
197
|
+
.catch(() => false);
|
|
168
198
|
if (hasCaptcha) return 'captcha';
|
|
169
|
-
} catch {
|
|
199
|
+
} catch {
|
|
200
|
+
/* ignore CDP errors */
|
|
201
|
+
}
|
|
170
202
|
return null;
|
|
171
203
|
}
|
|
172
204
|
|
|
@@ -174,13 +206,19 @@ function waitForEnter(): Promise<void> {
|
|
|
174
206
|
if (!process.stdin.isTTY) {
|
|
175
207
|
// Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
|
|
176
208
|
// Auto-continue after a short grace period so automation isn't blocked.
|
|
177
|
-
output.printWarning(
|
|
178
|
-
|
|
209
|
+
output.printWarning(
|
|
210
|
+
'Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.',
|
|
211
|
+
);
|
|
212
|
+
return new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
179
213
|
}
|
|
180
|
-
return new Promise(resolve => {
|
|
214
|
+
return new Promise((resolve) => {
|
|
181
215
|
process.stdin.resume();
|
|
182
216
|
process.stdin.setEncoding('utf8');
|
|
183
|
-
const onData = () => {
|
|
217
|
+
const onData = () => {
|
|
218
|
+
process.stdin.pause();
|
|
219
|
+
process.stdin.off('data', onData);
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
184
222
|
process.stdin.once('data', onData);
|
|
185
223
|
});
|
|
186
224
|
}
|
|
@@ -191,7 +229,11 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
191
229
|
// Snapshot cookies from current headless session before closing
|
|
192
230
|
let savedCookies: unknown[] = [];
|
|
193
231
|
if (_client && _sessionId) {
|
|
194
|
-
try {
|
|
232
|
+
try {
|
|
233
|
+
savedCookies = await browser.getCookies(_client, _sessionId);
|
|
234
|
+
} catch {
|
|
235
|
+
/* ignore */
|
|
236
|
+
}
|
|
195
237
|
browser.teardownRouteInterception(_sessionId);
|
|
196
238
|
browser.stopRequestCapture(_sessionId);
|
|
197
239
|
browser.teardownDialogHandling(_sessionId);
|
|
@@ -215,7 +257,9 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
215
257
|
_sessionId = fallback.sessionId;
|
|
216
258
|
_targetId = fallback.target.id;
|
|
217
259
|
_refs = new Map();
|
|
218
|
-
throw new Error(
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Cannot open headed browser: ${err instanceof Error ? err.message : String(err)}`,
|
|
262
|
+
);
|
|
219
263
|
}
|
|
220
264
|
const conn = await browser.connectToTarget(headedPort);
|
|
221
265
|
_client = conn.client;
|
|
@@ -224,16 +268,28 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
224
268
|
_refs = new Map();
|
|
225
269
|
|
|
226
270
|
if (savedCookies.length) {
|
|
227
|
-
try {
|
|
271
|
+
try {
|
|
272
|
+
await browser.setCookies(
|
|
273
|
+
_client,
|
|
274
|
+
_sessionId,
|
|
275
|
+
savedCookies as Parameters<typeof browser.setCookies>[2],
|
|
276
|
+
);
|
|
277
|
+
} catch {
|
|
278
|
+
/* ignore */
|
|
279
|
+
}
|
|
228
280
|
}
|
|
229
281
|
await browser.openUrl(_client, _sessionId, url);
|
|
230
282
|
|
|
231
|
-
output.printInfo(
|
|
283
|
+
output.printInfo(
|
|
284
|
+
'Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...',
|
|
285
|
+
);
|
|
232
286
|
await waitForEnter();
|
|
233
287
|
|
|
234
288
|
// Capture post-auth cookies
|
|
235
289
|
const authCookies = await browser.getCookies(_client, _sessionId).catch(() => [] as unknown[]);
|
|
236
|
-
const authLocalStorage = await browser
|
|
290
|
+
const authLocalStorage = await browser
|
|
291
|
+
.getLocalStorage(_client, _sessionId)
|
|
292
|
+
.catch(() => ({}) as Record<string, string>);
|
|
237
293
|
|
|
238
294
|
// Close headed session — actually terminate the underlying Chrome process
|
|
239
295
|
// (Browser.close CDP command, PID-kill fallback), not just our CDP client
|
|
@@ -258,10 +314,22 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
258
314
|
_refs = new Map();
|
|
259
315
|
|
|
260
316
|
if (authCookies.length) {
|
|
261
|
-
try {
|
|
317
|
+
try {
|
|
318
|
+
await browser.setCookies(
|
|
319
|
+
_client,
|
|
320
|
+
_sessionId,
|
|
321
|
+
authCookies as Parameters<typeof browser.setCookies>[2],
|
|
322
|
+
);
|
|
323
|
+
} catch {
|
|
324
|
+
/* ignore */
|
|
325
|
+
}
|
|
262
326
|
}
|
|
263
327
|
if (authLocalStorage && Object.keys(authLocalStorage).length) {
|
|
264
|
-
try {
|
|
328
|
+
try {
|
|
329
|
+
await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
|
|
330
|
+
} catch {
|
|
331
|
+
/* ignore */
|
|
332
|
+
}
|
|
265
333
|
}
|
|
266
334
|
}
|
|
267
335
|
|
|
@@ -278,7 +346,7 @@ function imageFormat<T extends string>(value: unknown, allowed: readonly T[], fa
|
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
function print(msg: string) {
|
|
281
|
-
process.stdout.write(msg
|
|
349
|
+
process.stdout.write(`${msg}\n`);
|
|
282
350
|
}
|
|
283
351
|
|
|
284
352
|
// ---------------------------------------------------------------------------
|
|
@@ -290,7 +358,12 @@ const openCommand: Command = {
|
|
|
290
358
|
description: 'Open a URL in the browser. Usage: monomind browse open <url>',
|
|
291
359
|
options: [
|
|
292
360
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
293
|
-
{
|
|
361
|
+
{
|
|
362
|
+
name: 'headed',
|
|
363
|
+
type: 'boolean',
|
|
364
|
+
description: 'Force visible browser window',
|
|
365
|
+
default: false,
|
|
366
|
+
},
|
|
294
367
|
{ name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
|
|
295
368
|
{ name: 'state', type: 'string', description: 'State file to load' },
|
|
296
369
|
],
|
|
@@ -306,13 +379,25 @@ const openCommand: Command = {
|
|
|
306
379
|
const prevSid = _sessionId;
|
|
307
380
|
const prevClient = _client;
|
|
308
381
|
if (browser.getHarStatus(prevSid).recording) {
|
|
309
|
-
try {
|
|
382
|
+
try {
|
|
383
|
+
await browser.stopHarRecording(prevClient, prevSid);
|
|
384
|
+
} catch {
|
|
385
|
+
/* ignore */
|
|
386
|
+
}
|
|
310
387
|
}
|
|
311
388
|
if (browser.getTraceStatus(prevSid)) {
|
|
312
|
-
try {
|
|
389
|
+
try {
|
|
390
|
+
await browser.stopTrace(prevClient, prevSid);
|
|
391
|
+
} catch {
|
|
392
|
+
/* ignore */
|
|
393
|
+
}
|
|
313
394
|
}
|
|
314
395
|
if (browser.isProfilingActive(prevSid)) {
|
|
315
|
-
try {
|
|
396
|
+
try {
|
|
397
|
+
await browser.stopCpuProfile(prevClient, prevSid);
|
|
398
|
+
} catch {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
316
401
|
}
|
|
317
402
|
browser.teardownRouteInterception(prevSid);
|
|
318
403
|
browser.stopRequestCapture(prevSid);
|
|
@@ -352,7 +437,10 @@ const openCommand: Command = {
|
|
|
352
437
|
// launched it) instead of overwriting with undefined.
|
|
353
438
|
const existing = await browser.loadActivePortInfo();
|
|
354
439
|
const preserved = existing && existing.port === _port ? existing : undefined;
|
|
355
|
-
await browser.saveActivePort(_port, {
|
|
440
|
+
await browser.saveActivePort(_port, {
|
|
441
|
+
pid: preserved?.pid,
|
|
442
|
+
userDataDir: preserved?.userDataDir,
|
|
443
|
+
});
|
|
356
444
|
}
|
|
357
445
|
ensureSignalCleanupHandlers();
|
|
358
446
|
const conn = await browser.connectToTarget(_port);
|
|
@@ -381,10 +469,14 @@ const openCommand: Command = {
|
|
|
381
469
|
if (!forceHeaded) {
|
|
382
470
|
const attentionType = await detectAttentionNeeded(_client, _sessionId, currentUrl);
|
|
383
471
|
if (attentionType) {
|
|
384
|
-
output.printWarning(
|
|
472
|
+
output.printWarning(
|
|
473
|
+
`${attentionType === 'captcha' ? 'CAPTCHA' : 'Login'} detected — switching to headed mode`,
|
|
474
|
+
);
|
|
385
475
|
await switchToHeaded(currentUrl, port);
|
|
386
476
|
await browser.openUrl(_client!, _sessionId, currentUrl);
|
|
387
|
-
output.printSuccess(
|
|
477
|
+
output.printSuccess(
|
|
478
|
+
`Resumed headless after ${attentionType === 'captcha' ? 'CAPTCHA' : 'login'}`,
|
|
479
|
+
);
|
|
388
480
|
}
|
|
389
481
|
}
|
|
390
482
|
|
|
@@ -400,15 +492,50 @@ const snapshotCommand: Command = {
|
|
|
400
492
|
name: 'snapshot',
|
|
401
493
|
description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
|
|
402
494
|
options: [
|
|
403
|
-
{
|
|
404
|
-
|
|
495
|
+
{
|
|
496
|
+
name: 'interactive',
|
|
497
|
+
short: 'i',
|
|
498
|
+
type: 'boolean',
|
|
499
|
+
description: 'Interactive elements only (93% token reduction)',
|
|
500
|
+
default: false,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: 'compact',
|
|
504
|
+
short: 'c',
|
|
505
|
+
type: 'boolean',
|
|
506
|
+
description: 'Compact output format',
|
|
507
|
+
default: false,
|
|
508
|
+
},
|
|
405
509
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
406
510
|
{ name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
|
|
407
|
-
{
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
511
|
+
{
|
|
512
|
+
name: 'selector',
|
|
513
|
+
short: 's',
|
|
514
|
+
type: 'string',
|
|
515
|
+
description: 'Scope snapshot to a CSS selector',
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'save',
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'Save snapshot text to file (baseline for --diff)',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'diff',
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Compare current snapshot against a saved baseline file',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'content-boundaries',
|
|
529
|
+
type: 'boolean',
|
|
530
|
+
description: 'Wrap output in sentinel markers to prevent page-content injection attacks',
|
|
531
|
+
default: false,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'max-output',
|
|
535
|
+
type: 'number',
|
|
536
|
+
description:
|
|
537
|
+
'Truncate output to N characters (prevents context window blowout on large pages)',
|
|
538
|
+
},
|
|
412
539
|
],
|
|
413
540
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
414
541
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -436,8 +563,8 @@ const snapshotCommand: Command = {
|
|
|
436
563
|
|
|
437
564
|
// --save: write snapshot text to baseline file
|
|
438
565
|
if (ctx.flags.save) {
|
|
439
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
440
|
-
const { dirname } = await import('path');
|
|
566
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
567
|
+
const { dirname } = await import('node:path');
|
|
441
568
|
const savePath = ctx.flags.save as string;
|
|
442
569
|
await mkdir(dirname(savePath), { recursive: true }).catch(() => {});
|
|
443
570
|
await writeFile(savePath, result.text, 'utf8');
|
|
@@ -447,35 +574,58 @@ const snapshotCommand: Command = {
|
|
|
447
574
|
|
|
448
575
|
// --diff: compare against baseline file
|
|
449
576
|
if (ctx.flags.diff) {
|
|
450
|
-
const { readFile } = await import('fs/promises');
|
|
577
|
+
const { readFile } = await import('node:fs/promises');
|
|
451
578
|
const baselinePath = ctx.flags.diff as string;
|
|
452
579
|
let baseline: string;
|
|
453
|
-
try {
|
|
454
|
-
|
|
580
|
+
try {
|
|
581
|
+
baseline = await readFile(baselinePath, 'utf8');
|
|
582
|
+
} catch {
|
|
583
|
+
throw new Error(`Baseline not found: ${baselinePath}. Run snapshot --save first.`);
|
|
584
|
+
}
|
|
455
585
|
const currentLines = result.text.split('\n');
|
|
456
586
|
const baselineLines = baseline.split('\n');
|
|
457
|
-
const added: string[] = [],
|
|
587
|
+
const added: string[] = [],
|
|
588
|
+
removed: string[] = [];
|
|
458
589
|
const baseSet = new Set(baselineLines);
|
|
459
590
|
const curSet = new Set(currentLines);
|
|
460
591
|
for (const l of currentLines) if (!baseSet.has(l)) added.push(l);
|
|
461
592
|
for (const l of baselineLines) if (!curSet.has(l)) removed.push(l);
|
|
462
593
|
const changed = added.length > 0 || removed.length > 0;
|
|
463
594
|
if (ctx.flags.json) {
|
|
464
|
-
print(
|
|
595
|
+
print(
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
changed,
|
|
598
|
+
additions: added.length,
|
|
599
|
+
removals: removed.length,
|
|
600
|
+
added,
|
|
601
|
+
removed,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
465
604
|
} else {
|
|
466
|
-
if (!changed) {
|
|
467
|
-
|
|
605
|
+
if (!changed) {
|
|
606
|
+
output.printSuccess('No snapshot changes detected');
|
|
607
|
+
} else {
|
|
468
608
|
output.printWarning(`Snapshot changed: +${added.length} lines, -${removed.length} lines`);
|
|
469
609
|
for (const l of added) print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
470
610
|
for (const l of removed) print(`\x1b[31m- ${l}\x1b[0m`);
|
|
471
611
|
}
|
|
472
612
|
}
|
|
473
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
success: true,
|
|
615
|
+
data: { changed, additions: added.length, removals: removed.length },
|
|
616
|
+
};
|
|
474
617
|
}
|
|
475
618
|
|
|
476
619
|
if (ctx.flags.json) {
|
|
477
620
|
const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
|
|
478
|
-
print(
|
|
621
|
+
print(
|
|
622
|
+
JSON.stringify({
|
|
623
|
+
url: result.url,
|
|
624
|
+
title: result.title,
|
|
625
|
+
refs: refsObj,
|
|
626
|
+
snapshot: result.text,
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
479
629
|
} else {
|
|
480
630
|
print(`[${result.title}] ${result.url}\n`);
|
|
481
631
|
print(applyOutputLimits(result.text));
|
|
@@ -563,11 +713,20 @@ const waitCommand: Command = {
|
|
|
563
713
|
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
564
714
|
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
565
715
|
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
566
|
-
{
|
|
716
|
+
{
|
|
717
|
+
name: 'load',
|
|
718
|
+
type: 'string',
|
|
719
|
+
description: 'Wait for load event: load|networkidle|domcontentloaded',
|
|
720
|
+
},
|
|
567
721
|
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
568
722
|
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
569
723
|
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
570
|
-
{
|
|
724
|
+
{
|
|
725
|
+
name: 'download',
|
|
726
|
+
type: 'string',
|
|
727
|
+
description:
|
|
728
|
+
'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
|
|
729
|
+
},
|
|
571
730
|
],
|
|
572
731
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
573
732
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -584,12 +743,17 @@ const waitCommand: Command = {
|
|
|
584
743
|
if (ctx.flags.fn) {
|
|
585
744
|
const expr = ctx.flags.fn as string;
|
|
586
745
|
const rawTimeout = (ctx.flags.timeout as number) ?? 30000;
|
|
587
|
-
const timeout = Number.isFinite(rawTimeout)
|
|
746
|
+
const timeout = Number.isFinite(rawTimeout)
|
|
747
|
+
? Math.max(100, Math.min(rawTimeout, 300_000))
|
|
748
|
+
: 30000; // cap at 5min
|
|
588
749
|
const interval = 200;
|
|
589
750
|
const deadline = Date.now() + timeout;
|
|
590
751
|
while (Date.now() < deadline) {
|
|
591
752
|
const result = await browser.evaluateJs(client, sessionId, expr);
|
|
592
|
-
if (result) {
|
|
753
|
+
if (result) {
|
|
754
|
+
output.printSuccess('Wait function returned truthy');
|
|
755
|
+
return { success: true };
|
|
756
|
+
}
|
|
593
757
|
await new Promise((r) => setTimeout(r, interval));
|
|
594
758
|
}
|
|
595
759
|
throw new Error(`Timeout waiting for --fn: ${expr}`);
|
|
@@ -601,8 +765,15 @@ const waitCommand: Command = {
|
|
|
601
765
|
const interval = 200;
|
|
602
766
|
const deadline = Date.now() + timeout;
|
|
603
767
|
while (Date.now() < deadline) {
|
|
604
|
-
const text = await browser.evaluateJs(
|
|
605
|
-
|
|
768
|
+
const text = (await browser.evaluateJs(
|
|
769
|
+
client,
|
|
770
|
+
sessionId,
|
|
771
|
+
'document.body?.innerText ?? ""',
|
|
772
|
+
)) as string;
|
|
773
|
+
if (!text.includes(target)) {
|
|
774
|
+
output.printSuccess('Text disappeared');
|
|
775
|
+
return { success: true };
|
|
776
|
+
}
|
|
606
777
|
await new Promise((r) => setTimeout(r, interval));
|
|
607
778
|
}
|
|
608
779
|
throw new Error(`Timeout waiting for text to disappear: "${target}"`);
|
|
@@ -610,28 +781,52 @@ const waitCommand: Command = {
|
|
|
610
781
|
|
|
611
782
|
if (ctx.flags.download) {
|
|
612
783
|
const savePath = ctx.flags.download as string;
|
|
613
|
-
const { mkdir } = await import('fs/promises');
|
|
614
|
-
const { dirname, join } = await import('path');
|
|
615
|
-
const { tmpdir } = await import('os');
|
|
784
|
+
const { mkdir } = await import('node:fs/promises');
|
|
785
|
+
const { dirname, join } = await import('node:path');
|
|
786
|
+
const { tmpdir } = await import('node:os');
|
|
616
787
|
const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
|
|
617
788
|
await mkdir(downloadDir, { recursive: true });
|
|
618
|
-
await client
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
789
|
+
await client
|
|
790
|
+
.send(
|
|
791
|
+
'Browser.setDownloadBehavior',
|
|
792
|
+
{
|
|
793
|
+
behavior: 'allow',
|
|
794
|
+
downloadPath: downloadDir,
|
|
795
|
+
eventsEnabled: true,
|
|
796
|
+
},
|
|
797
|
+
undefined,
|
|
798
|
+
)
|
|
799
|
+
.catch(() =>
|
|
800
|
+
client
|
|
801
|
+
.send(
|
|
802
|
+
'Page.setDownloadBehavior',
|
|
803
|
+
{ behavior: 'allow', downloadPath: downloadDir },
|
|
804
|
+
sessionId,
|
|
805
|
+
)
|
|
806
|
+
.catch(() => {}),
|
|
807
|
+
);
|
|
623
808
|
const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
|
|
624
809
|
const rawTimeout = Math.min((ctx.flags.timeout as number) ?? 30000, MAX_DOWNLOAD_TIMEOUT);
|
|
625
810
|
const finalPath = await new Promise<string>((resolve, reject) => {
|
|
626
811
|
let guid = '';
|
|
627
812
|
let settled = false;
|
|
628
813
|
// C2: capture off() functions to avoid listener leaks
|
|
629
|
-
const offBegin = client.on(
|
|
814
|
+
const offBegin = client.on(
|
|
815
|
+
'Browser.downloadWillBegin',
|
|
816
|
+
(params: Record<string, unknown>) => {
|
|
817
|
+
guid = params.guid as string;
|
|
818
|
+
},
|
|
819
|
+
);
|
|
630
820
|
let offProgress: (() => void) | undefined;
|
|
631
821
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
632
822
|
let tid: ReturnType<typeof setTimeout>;
|
|
633
823
|
let pollTid: ReturnType<typeof setInterval> | undefined;
|
|
634
|
-
const cleanup = () => {
|
|
824
|
+
const cleanup = () => {
|
|
825
|
+
clearTimeout(tid);
|
|
826
|
+
clearInterval(pollTid);
|
|
827
|
+
offBegin?.();
|
|
828
|
+
offProgress?.();
|
|
829
|
+
};
|
|
635
830
|
const finish = (path: string) => {
|
|
636
831
|
if (settled) return;
|
|
637
832
|
settled = true;
|
|
@@ -645,24 +840,27 @@ const waitCommand: Command = {
|
|
|
645
840
|
reject(err);
|
|
646
841
|
};
|
|
647
842
|
tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
|
|
648
|
-
offProgress = client.on(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
843
|
+
offProgress = client.on(
|
|
844
|
+
'Browser.downloadProgress',
|
|
845
|
+
async (params: Record<string, unknown>) => {
|
|
846
|
+
if (params.guid === guid && params.state === 'completed') {
|
|
847
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
848
|
+
const files = await readdir(downloadDir);
|
|
849
|
+
if (files.length > 0) {
|
|
850
|
+
const src = join(downloadDir, files[0]);
|
|
851
|
+
await mkdir(dirname(savePath), { recursive: true });
|
|
852
|
+
await rename(src, savePath);
|
|
853
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
854
|
+
finish(savePath);
|
|
855
|
+
} else {
|
|
856
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
857
|
+
fail(new Error('Download completed but no file found'));
|
|
858
|
+
}
|
|
859
|
+
} else if (params.guid === guid && params.state === 'canceled') {
|
|
860
|
+
fail(new Error('Download was canceled'));
|
|
661
861
|
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
});
|
|
862
|
+
},
|
|
863
|
+
);
|
|
666
864
|
|
|
667
865
|
// Fallback for the empty-guid race: this process's listener attaches
|
|
668
866
|
// AFTER a separate `click` process may have already started (and even
|
|
@@ -675,7 +873,7 @@ const waitCommand: Command = {
|
|
|
675
873
|
pollTid = setInterval(async () => {
|
|
676
874
|
if (settled || guid) return; // a real CDP event has taken over
|
|
677
875
|
try {
|
|
678
|
-
const { stat } = await import('fs/promises');
|
|
876
|
+
const { stat } = await import('node:fs/promises');
|
|
679
877
|
const st = await stat(savePath);
|
|
680
878
|
if (st.isFile() && st.size > 0 && st.size === lastSize) {
|
|
681
879
|
stableReads++;
|
|
@@ -708,13 +906,25 @@ const waitCommand: Command = {
|
|
|
708
906
|
|
|
709
907
|
const screenshotCommand: Command = {
|
|
710
908
|
name: 'screenshot',
|
|
711
|
-
description:
|
|
909
|
+
description:
|
|
910
|
+
'Capture a screenshot. Usage: monomind browse screenshot [path] [--annotate] [--hide-scrollbars]',
|
|
712
911
|
options: [
|
|
713
912
|
{ name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
|
|
714
913
|
{ name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
|
|
715
914
|
{ name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
|
|
716
|
-
{
|
|
717
|
-
|
|
915
|
+
{
|
|
916
|
+
name: 'annotate',
|
|
917
|
+
type: 'boolean',
|
|
918
|
+
description:
|
|
919
|
+
'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
|
|
920
|
+
default: false,
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: 'hide-scrollbars',
|
|
924
|
+
type: 'boolean',
|
|
925
|
+
description: 'Hide native scrollbars via CSS injection before capture',
|
|
926
|
+
default: false,
|
|
927
|
+
},
|
|
718
928
|
{ name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
|
|
719
929
|
],
|
|
720
930
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
@@ -723,10 +933,16 @@ const screenshotCommand: Command = {
|
|
|
723
933
|
|
|
724
934
|
const hideScrollbars = ctx.flags['hide-scrollbars'] as boolean;
|
|
725
935
|
if (hideScrollbars) {
|
|
726
|
-
await client
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
936
|
+
await client
|
|
937
|
+
.send(
|
|
938
|
+
'Runtime.evaluate',
|
|
939
|
+
{
|
|
940
|
+
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)return;var el=document.createElement('style');el.id='__mm_noscroll__';el.textContent='*::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important;-ms-overflow-style:none!important}';document.head.appendChild(el);})()`,
|
|
941
|
+
returnByValue: false,
|
|
942
|
+
},
|
|
943
|
+
sessionId,
|
|
944
|
+
)
|
|
945
|
+
.catch(() => {});
|
|
730
946
|
}
|
|
731
947
|
|
|
732
948
|
const annotate = ctx.flags.annotate as boolean;
|
|
@@ -743,10 +959,16 @@ const screenshotCommand: Command = {
|
|
|
743
959
|
});
|
|
744
960
|
} finally {
|
|
745
961
|
if (hideScrollbars) {
|
|
746
|
-
await client
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
962
|
+
await client
|
|
963
|
+
.send(
|
|
964
|
+
'Runtime.evaluate',
|
|
965
|
+
{
|
|
966
|
+
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
|
|
967
|
+
returnByValue: false,
|
|
968
|
+
},
|
|
969
|
+
sessionId,
|
|
970
|
+
)
|
|
971
|
+
.catch(() => {});
|
|
750
972
|
}
|
|
751
973
|
}
|
|
752
974
|
|
|
@@ -762,16 +984,16 @@ const screenshotCommand: Command = {
|
|
|
762
984
|
|
|
763
985
|
const getCommand: Command = {
|
|
764
986
|
name: 'get',
|
|
765
|
-
description:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
],
|
|
987
|
+
description:
|
|
988
|
+
'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
|
|
989
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
769
990
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
770
991
|
const { client, sessionId } = await ensureConnected(_port);
|
|
771
992
|
const browser = await getBrowser();
|
|
772
993
|
|
|
773
994
|
const what = ctx.args[0] as string;
|
|
774
|
-
if (!what)
|
|
995
|
+
if (!what)
|
|
996
|
+
throw new Error('Usage: monomind browse get url|title|text|html|value|attr|count|box|styles');
|
|
775
997
|
|
|
776
998
|
let value: unknown;
|
|
777
999
|
|
|
@@ -790,19 +1012,32 @@ const getCommand: Command = {
|
|
|
790
1012
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
791
1013
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
792
1014
|
if (!objectId) throw new Error('Element not in DOM');
|
|
793
|
-
const result = await client.send<{ result: { value?: string } }>(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1015
|
+
const result = await client.send<{ result: { value?: string } }>(
|
|
1016
|
+
'Runtime.callFunctionOn',
|
|
1017
|
+
{
|
|
1018
|
+
functionDeclaration:
|
|
1019
|
+
'function() { return this.innerText || this.textContent || ""; }',
|
|
1020
|
+
objectId,
|
|
1021
|
+
returnByValue: true,
|
|
1022
|
+
},
|
|
1023
|
+
sessionId,
|
|
1024
|
+
);
|
|
798
1025
|
value = result.result?.value ?? '';
|
|
799
1026
|
} else {
|
|
800
|
-
value = (await browser.evaluateJs(
|
|
1027
|
+
value = (await browser.evaluateJs(
|
|
1028
|
+
client,
|
|
1029
|
+
sessionId,
|
|
1030
|
+
'document.body?.innerText ?? ""',
|
|
1031
|
+
)) as string;
|
|
801
1032
|
}
|
|
802
1033
|
break;
|
|
803
1034
|
}
|
|
804
1035
|
case 'html':
|
|
805
|
-
value = (await browser.evaluateJs(
|
|
1036
|
+
value = (await browser.evaluateJs(
|
|
1037
|
+
client,
|
|
1038
|
+
sessionId,
|
|
1039
|
+
'document.documentElement.outerHTML',
|
|
1040
|
+
)) as string;
|
|
806
1041
|
break;
|
|
807
1042
|
case 'value': {
|
|
808
1043
|
const refArg = ctx.args[1] as string;
|
|
@@ -812,33 +1047,48 @@ const getCommand: Command = {
|
|
|
812
1047
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
813
1048
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
814
1049
|
if (!objectId) throw new Error('Element not in DOM');
|
|
815
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1050
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1051
|
+
'Runtime.callFunctionOn',
|
|
1052
|
+
{
|
|
1053
|
+
functionDeclaration: 'function() { return this.value ?? null; }',
|
|
1054
|
+
objectId,
|
|
1055
|
+
returnByValue: true,
|
|
1056
|
+
},
|
|
1057
|
+
sessionId,
|
|
1058
|
+
);
|
|
819
1059
|
value = r.result?.value ?? null;
|
|
820
1060
|
break;
|
|
821
1061
|
}
|
|
822
1062
|
case 'attr': {
|
|
823
1063
|
const refArg = ctx.args[1] as string;
|
|
824
1064
|
const attrName = ctx.args[2] as string;
|
|
825
|
-
if (!refArg || !attrName)
|
|
1065
|
+
if (!refArg || !attrName)
|
|
1066
|
+
throw new Error('Usage: monomind browse get attr @ref <attrName>');
|
|
826
1067
|
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
827
1068
|
const ref = _refs.get(refKey);
|
|
828
1069
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
829
1070
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
830
1071
|
if (!objectId) throw new Error('Element not in DOM');
|
|
831
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1072
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1073
|
+
'Runtime.callFunctionOn',
|
|
1074
|
+
{
|
|
1075
|
+
functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
|
|
1076
|
+
objectId,
|
|
1077
|
+
returnByValue: true,
|
|
1078
|
+
},
|
|
1079
|
+
sessionId,
|
|
1080
|
+
);
|
|
835
1081
|
value = r.result?.value ?? null;
|
|
836
1082
|
break;
|
|
837
1083
|
}
|
|
838
1084
|
case 'count': {
|
|
839
1085
|
const selector = ctx.args[1] as string;
|
|
840
1086
|
if (!selector) throw new Error('Usage: monomind browse get count <cssSelector>');
|
|
841
|
-
value = await browser.evaluateJs(
|
|
1087
|
+
value = await browser.evaluateJs(
|
|
1088
|
+
client,
|
|
1089
|
+
sessionId,
|
|
1090
|
+
`document.querySelectorAll(${JSON.stringify(selector)}).length`,
|
|
1091
|
+
);
|
|
842
1092
|
break;
|
|
843
1093
|
}
|
|
844
1094
|
case 'box': {
|
|
@@ -859,11 +1109,21 @@ const getCommand: Command = {
|
|
|
859
1109
|
if (!ref) throw new Error(`Ref @${refKey} not found`);
|
|
860
1110
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
861
1111
|
if (!objectId) throw new Error('Element not in DOM');
|
|
862
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1112
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
1113
|
+
'Runtime.callFunctionOn',
|
|
1114
|
+
{
|
|
1115
|
+
functionDeclaration:
|
|
1116
|
+
'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
|
|
1117
|
+
objectId,
|
|
1118
|
+
returnByValue: true,
|
|
1119
|
+
},
|
|
1120
|
+
sessionId,
|
|
1121
|
+
);
|
|
1122
|
+
try {
|
|
1123
|
+
value = JSON.parse(r.result?.value ?? '{}');
|
|
1124
|
+
} catch {
|
|
1125
|
+
value = {};
|
|
1126
|
+
}
|
|
867
1127
|
break;
|
|
868
1128
|
}
|
|
869
1129
|
default:
|
|
@@ -882,11 +1142,17 @@ const getCommand: Command = {
|
|
|
882
1142
|
|
|
883
1143
|
const scrollCommand: Command = {
|
|
884
1144
|
name: 'scroll',
|
|
885
|
-
description:
|
|
1145
|
+
description:
|
|
1146
|
+
'Scroll the page. Usage: monomind browse scroll up|down|left|right [amount] [--selector ".sidebar"]',
|
|
886
1147
|
options: [
|
|
887
1148
|
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
888
1149
|
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
889
|
-
{
|
|
1150
|
+
{
|
|
1151
|
+
name: 'selector',
|
|
1152
|
+
short: 's',
|
|
1153
|
+
type: 'string',
|
|
1154
|
+
description: 'CSS selector of element to scroll within',
|
|
1155
|
+
},
|
|
890
1156
|
],
|
|
891
1157
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
892
1158
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -896,18 +1162,29 @@ const scrollCommand: Command = {
|
|
|
896
1162
|
if (!direction) throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
|
|
897
1163
|
|
|
898
1164
|
// Support positional amount: scroll down 300
|
|
899
|
-
const positionalAmount =
|
|
900
|
-
|
|
1165
|
+
const positionalAmount =
|
|
1166
|
+
ctx.args[1] !== undefined ? parseInt(ctx.args[1] as string, 10) : undefined;
|
|
1167
|
+
const amount =
|
|
1168
|
+
positionalAmount && Number.isFinite(positionalAmount)
|
|
1169
|
+
? positionalAmount
|
|
1170
|
+
: ((ctx.flags.amount as number) ?? 300);
|
|
901
1171
|
|
|
902
1172
|
if (ctx.flags.selector) {
|
|
903
1173
|
const sel = ctx.flags.selector as string;
|
|
904
1174
|
const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
|
|
905
1175
|
const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
|
|
906
|
-
const posJson = await browser.evaluateJs(
|
|
907
|
-
|
|
1176
|
+
const posJson = (await browser.evaluateJs(
|
|
1177
|
+
client,
|
|
1178
|
+
sessionId,
|
|
1179
|
+
`(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`,
|
|
1180
|
+
)) as string | null;
|
|
908
1181
|
if (!posJson) throw new Error(`Selector not found: ${sel}`);
|
|
909
1182
|
const pos = JSON.parse(posJson) as { x: number; y: number };
|
|
910
|
-
await client.send(
|
|
1183
|
+
await client.send(
|
|
1184
|
+
'Input.dispatchMouseEvent',
|
|
1185
|
+
{ type: 'mouseWheel', x: pos.x, y: pos.y, deltaX: dx, deltaY: dy },
|
|
1186
|
+
sessionId,
|
|
1187
|
+
);
|
|
911
1188
|
output.printSuccess(`Scrolled ${direction} in ${sel}`);
|
|
912
1189
|
return { success: true };
|
|
913
1190
|
}
|
|
@@ -916,7 +1193,7 @@ const scrollCommand: Command = {
|
|
|
916
1193
|
if (ctx.flags.ref) {
|
|
917
1194
|
const refKey = (ctx.flags.ref as string).startsWith('@')
|
|
918
1195
|
? (ctx.flags.ref as string).slice(1)
|
|
919
|
-
: ctx.flags.ref as string;
|
|
1196
|
+
: (ctx.flags.ref as string);
|
|
920
1197
|
ref = _refs.get(refKey);
|
|
921
1198
|
}
|
|
922
1199
|
|
|
@@ -942,15 +1219,26 @@ const navigateCommand: Command = {
|
|
|
942
1219
|
let offFrameStarted: () => void = () => {};
|
|
943
1220
|
const frameStartedPromise = new Promise<void>((resolve) => {
|
|
944
1221
|
offFrameStarted = client.on('Page.frameStartedLoading', (_params, sid) => {
|
|
945
|
-
if (sid === sessionId) {
|
|
1222
|
+
if (sid === sessionId) {
|
|
1223
|
+
const off = offFrameStarted;
|
|
1224
|
+
offFrameStarted = () => {};
|
|
1225
|
+
off();
|
|
1226
|
+
resolve();
|
|
1227
|
+
}
|
|
946
1228
|
});
|
|
947
1229
|
});
|
|
948
1230
|
try {
|
|
949
|
-
await client.send(
|
|
950
|
-
|
|
951
|
-
|
|
1231
|
+
await client.send(
|
|
1232
|
+
'Runtime.evaluate',
|
|
1233
|
+
{
|
|
1234
|
+
expression: direction === 'back' ? 'history.back()' : 'history.forward()',
|
|
1235
|
+
},
|
|
1236
|
+
sessionId,
|
|
1237
|
+
);
|
|
952
1238
|
let fallbackHandle: ReturnType<typeof setTimeout> | undefined;
|
|
953
|
-
const fallbackPromise = new Promise<void>((r) => {
|
|
1239
|
+
const fallbackPromise = new Promise<void>((r) => {
|
|
1240
|
+
fallbackHandle = setTimeout(r, 2000);
|
|
1241
|
+
});
|
|
954
1242
|
await Promise.race([frameStartedPromise, fallbackPromise]);
|
|
955
1243
|
if (fallbackHandle !== undefined) clearTimeout(fallbackHandle);
|
|
956
1244
|
} finally {
|
|
@@ -975,29 +1263,44 @@ const navigateCommand: Command = {
|
|
|
975
1263
|
|
|
976
1264
|
const setCommand: Command = {
|
|
977
1265
|
name: 'set',
|
|
978
|
-
description:
|
|
1266
|
+
description:
|
|
1267
|
+
'Configure browser settings. Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
979
1268
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
980
1269
|
const { client, sessionId } = await ensureConnected(_port);
|
|
981
1270
|
const browser = await getBrowser();
|
|
982
1271
|
|
|
983
1272
|
const setting = ctx.args[0] as string;
|
|
984
|
-
if (!setting)
|
|
1273
|
+
if (!setting)
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
'Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
1276
|
+
);
|
|
985
1277
|
|
|
986
1278
|
switch (setting) {
|
|
987
1279
|
case 'viewport': {
|
|
988
1280
|
const width = parseInt(ctx.args[1] as string, 10);
|
|
989
1281
|
const height = parseInt(ctx.args[2] as string, 10);
|
|
990
1282
|
const dpr = parseFloat(ctx.args[3] as string) || undefined;
|
|
991
|
-
if (isNaN(width) || isNaN(height))
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1283
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
1284
|
+
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
1285
|
+
await client.send(
|
|
1286
|
+
'Emulation.setDeviceMetricsOverride',
|
|
1287
|
+
{
|
|
1288
|
+
width,
|
|
1289
|
+
height,
|
|
1290
|
+
deviceScaleFactor: dpr ?? 1,
|
|
1291
|
+
mobile: false,
|
|
1292
|
+
},
|
|
1293
|
+
sessionId,
|
|
1294
|
+
);
|
|
995
1295
|
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
996
1296
|
break;
|
|
997
1297
|
}
|
|
998
1298
|
case 'device': {
|
|
999
1299
|
const deviceName = ctx.args[1] as string;
|
|
1000
|
-
if (!deviceName)
|
|
1300
|
+
if (!deviceName)
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`Usage: set device <name>. Available: ${browser.listDevices().join(', ')}`,
|
|
1303
|
+
);
|
|
1001
1304
|
await browser.emulateDevice(client, sessionId, deviceName);
|
|
1002
1305
|
output.printSuccess(`Emulating device: ${deviceName}`);
|
|
1003
1306
|
break;
|
|
@@ -1006,7 +1309,8 @@ const setCommand: Command = {
|
|
|
1006
1309
|
const lat = parseFloat(ctx.args[1] as string);
|
|
1007
1310
|
const lon = parseFloat(ctx.args[2] as string);
|
|
1008
1311
|
const acc = parseFloat(ctx.args[3] as string) || 100;
|
|
1009
|
-
if (isNaN(lat) || isNaN(lon))
|
|
1312
|
+
if (Number.isNaN(lat) || Number.isNaN(lon))
|
|
1313
|
+
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
1010
1314
|
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
1011
1315
|
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
1012
1316
|
break;
|
|
@@ -1042,7 +1346,9 @@ const setCommand: Command = {
|
|
|
1042
1346
|
break;
|
|
1043
1347
|
}
|
|
1044
1348
|
default:
|
|
1045
|
-
throw new Error(
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`Unknown setting: ${setting}. Use: viewport|device|geo|offline|media|credentials|useragent`,
|
|
1351
|
+
);
|
|
1046
1352
|
}
|
|
1047
1353
|
|
|
1048
1354
|
return { success: true };
|
|
@@ -1051,9 +1357,14 @@ const setCommand: Command = {
|
|
|
1051
1357
|
|
|
1052
1358
|
const stateCommand: Command = {
|
|
1053
1359
|
name: 'state',
|
|
1054
|
-
description:
|
|
1360
|
+
description:
|
|
1361
|
+
'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
|
|
1055
1362
|
options: [
|
|
1056
|
-
{
|
|
1363
|
+
{
|
|
1364
|
+
name: 'older-than',
|
|
1365
|
+
type: 'number',
|
|
1366
|
+
description: 'For state clean: remove sessions older than N days',
|
|
1367
|
+
},
|
|
1057
1368
|
],
|
|
1058
1369
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1059
1370
|
const browser = await getBrowser();
|
|
@@ -1104,7 +1415,13 @@ const stateCommand: Command = {
|
|
|
1104
1415
|
const title = await browser.getCurrentTitle(c, sid);
|
|
1105
1416
|
const cookies = await browser.getCookies(c, sid);
|
|
1106
1417
|
const ls = await browser.getAllLocalStorage(c, sid);
|
|
1107
|
-
const info = {
|
|
1418
|
+
const info = {
|
|
1419
|
+
url,
|
|
1420
|
+
title,
|
|
1421
|
+
cookies: cookies.length,
|
|
1422
|
+
localStorage: Object.keys(ls).length,
|
|
1423
|
+
refs: _refs.size,
|
|
1424
|
+
};
|
|
1108
1425
|
print(JSON.stringify(info, null, 2));
|
|
1109
1426
|
return { success: true, data: info };
|
|
1110
1427
|
}
|
|
@@ -1121,18 +1438,26 @@ const stateCommand: Command = {
|
|
|
1121
1438
|
case 'rename': {
|
|
1122
1439
|
const oldName = ctx.args[1] as string;
|
|
1123
1440
|
const newName = ctx.args[2] as string;
|
|
1124
|
-
if (!oldName || !newName)
|
|
1441
|
+
if (!oldName || !newName)
|
|
1442
|
+
throw new Error('Usage: monomind browse state rename <old-name> <new-name>');
|
|
1125
1443
|
const sessions = await browser.listSessions();
|
|
1126
1444
|
if (!sessions.includes(oldName)) throw new Error(`Session not found: ${oldName}`);
|
|
1127
1445
|
// W1: validate newName to prevent path traversal
|
|
1128
|
-
const { basename: basenameFn } = await import('path');
|
|
1446
|
+
const { basename: basenameFn } = await import('node:path');
|
|
1129
1447
|
const safeName = basenameFn(newName);
|
|
1130
1448
|
if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
|
|
1131
|
-
throw new Error(
|
|
1449
|
+
throw new Error(
|
|
1450
|
+
'Invalid session name — must not contain path separators or start with "."',
|
|
1451
|
+
);
|
|
1132
1452
|
}
|
|
1133
|
-
const {
|
|
1134
|
-
|
|
1135
|
-
|
|
1453
|
+
const {
|
|
1454
|
+
unlink: unlinkRename,
|
|
1455
|
+
readFile,
|
|
1456
|
+
writeFile,
|
|
1457
|
+
mkdir: mkdirRename,
|
|
1458
|
+
} = await import('node:fs/promises');
|
|
1459
|
+
const { join: joinR } = await import('node:path');
|
|
1460
|
+
const { homedir } = await import('node:os');
|
|
1136
1461
|
const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
|
|
1137
1462
|
const oldPath = joinR(sessionDir, `${oldName}.json`);
|
|
1138
1463
|
const newPath = joinR(sessionDir, `${newName}.json`);
|
|
@@ -1146,9 +1471,9 @@ const stateCommand: Command = {
|
|
|
1146
1471
|
}
|
|
1147
1472
|
case 'clean': {
|
|
1148
1473
|
const days = (ctx.flags['older-than'] as number) ?? 7;
|
|
1149
|
-
const { unlink, stat } = await import('fs/promises');
|
|
1150
|
-
const { join: joinC } = await import('path');
|
|
1151
|
-
const { homedir: homedirC } = await import('os');
|
|
1474
|
+
const { unlink, stat } = await import('node:fs/promises');
|
|
1475
|
+
const { join: joinC } = await import('node:path');
|
|
1476
|
+
const { homedir: homedirC } = await import('node:os');
|
|
1152
1477
|
const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
|
|
1153
1478
|
const sessions = await browser.listSessions();
|
|
1154
1479
|
const cutoff = Date.now() - days * 86400 * 1000;
|
|
@@ -1180,30 +1505,52 @@ const networkCommand: Command = {
|
|
|
1180
1505
|
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
1181
1506
|
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
1182
1507
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1183
|
-
{
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1508
|
+
{
|
|
1509
|
+
name: 'filter',
|
|
1510
|
+
type: 'string',
|
|
1511
|
+
description: 'Filter requests by URL substring (for network requests)',
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: 'method',
|
|
1515
|
+
type: 'string',
|
|
1516
|
+
description: 'Filter by HTTP method, e.g. GET, POST (for network requests)',
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
name: 'status-code',
|
|
1520
|
+
type: 'number',
|
|
1521
|
+
description: 'Filter by HTTP status code (for network requests)',
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
name: 'type',
|
|
1525
|
+
type: 'string',
|
|
1526
|
+
description:
|
|
1527
|
+
'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)',
|
|
1528
|
+
},
|
|
1187
1529
|
],
|
|
1188
1530
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1189
1531
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1190
1532
|
const browser = await getBrowser();
|
|
1191
1533
|
const action = ctx.args[0] as string;
|
|
1192
|
-
if (!action)
|
|
1534
|
+
if (!action)
|
|
1535
|
+
throw new Error('Usage: monomind browse network route|unroute|cookies|headers|requests');
|
|
1193
1536
|
|
|
1194
1537
|
switch (action) {
|
|
1195
1538
|
case 'route': {
|
|
1196
1539
|
const pattern = ctx.flags.pattern as string;
|
|
1197
1540
|
if (!pattern) throw new Error('--pattern required for network route');
|
|
1198
|
-
const routes: NetworkRoute[] = [
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1541
|
+
const routes: NetworkRoute[] = [
|
|
1542
|
+
{
|
|
1543
|
+
pattern,
|
|
1544
|
+
action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
|
|
1545
|
+
response: ctx.flags.fulfill
|
|
1546
|
+
? {
|
|
1547
|
+
status: ctx.flags.status as number,
|
|
1548
|
+
body: ctx.flags.fulfill as string,
|
|
1549
|
+
headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers as string) : {},
|
|
1550
|
+
}
|
|
1551
|
+
: undefined,
|
|
1552
|
+
},
|
|
1553
|
+
];
|
|
1207
1554
|
await browser.setupRoutes(client, sessionId, routes);
|
|
1208
1555
|
output.printSuccess(`Network route set: ${pattern}`);
|
|
1209
1556
|
break;
|
|
@@ -1225,7 +1572,7 @@ const networkCommand: Command = {
|
|
|
1225
1572
|
break;
|
|
1226
1573
|
}
|
|
1227
1574
|
case 'capture': {
|
|
1228
|
-
const subAction = ctx.args[1] as string ?? 'start';
|
|
1575
|
+
const subAction = (ctx.args[1] as string) ?? 'start';
|
|
1229
1576
|
if (subAction === 'start') {
|
|
1230
1577
|
browser.startRequestCapture(client, sessionId);
|
|
1231
1578
|
output.printSuccess('Request capture started');
|
|
@@ -1245,13 +1592,22 @@ const networkCommand: Command = {
|
|
|
1245
1592
|
const filterStatus = ctx.flags['status-code'] as number | undefined;
|
|
1246
1593
|
const filterType = ctx.flags.type as string | undefined;
|
|
1247
1594
|
if (filterUrl) reqs = reqs.filter((r) => r.url.includes(filterUrl));
|
|
1248
|
-
if (filterMethod)
|
|
1595
|
+
if (filterMethod)
|
|
1596
|
+
reqs = reqs.filter(
|
|
1597
|
+
(r) => (r.method ?? 'GET').toUpperCase() === filterMethod.toUpperCase(),
|
|
1598
|
+
);
|
|
1249
1599
|
if (filterStatus) reqs = reqs.filter((r) => r.status === filterStatus);
|
|
1250
|
-
if (filterType)
|
|
1600
|
+
if (filterType)
|
|
1601
|
+
reqs = reqs.filter(
|
|
1602
|
+
(r) =>
|
|
1603
|
+
(r as Record<string, unknown>).resourceType === filterType ||
|
|
1604
|
+
(r as Record<string, unknown>).type === filterType,
|
|
1605
|
+
);
|
|
1251
1606
|
if (ctx.flags.json) print(JSON.stringify({ data: reqs }));
|
|
1252
1607
|
else {
|
|
1253
|
-
if (reqs.length === 0) {
|
|
1254
|
-
|
|
1608
|
+
if (reqs.length === 0) {
|
|
1609
|
+
output.printInfo('No captured requests. Run: network capture start');
|
|
1610
|
+
} else for (const r of reqs) print(` ${r.method ?? 'GET'} ${r.status ?? '-'} ${r.url}`);
|
|
1255
1611
|
}
|
|
1256
1612
|
return { success: true, data: { requests: reqs } };
|
|
1257
1613
|
}
|
|
@@ -1259,14 +1615,23 @@ const networkCommand: Command = {
|
|
|
1259
1615
|
const reqId = ctx.args[1] as string;
|
|
1260
1616
|
if (!reqId) throw new Error('Usage: monomind browse network request <requestId>');
|
|
1261
1617
|
const reqs = browser.getCapturedRequests(sessionId);
|
|
1262
|
-
const req = reqs.find(
|
|
1263
|
-
|
|
1618
|
+
const req = reqs.find(
|
|
1619
|
+
(r) =>
|
|
1620
|
+
(r as Record<string, unknown>).requestId === reqId ||
|
|
1621
|
+
(r as Record<string, unknown>).id === reqId,
|
|
1622
|
+
);
|
|
1623
|
+
if (!req) {
|
|
1624
|
+
output.printWarning(`Request not found: ${reqId}`);
|
|
1625
|
+
return { success: false };
|
|
1626
|
+
}
|
|
1264
1627
|
if (ctx.flags.json) print(JSON.stringify({ data: req }));
|
|
1265
1628
|
else print(JSON.stringify(req, null, 2));
|
|
1266
1629
|
return { success: true, data: { request: req } };
|
|
1267
1630
|
}
|
|
1268
1631
|
default:
|
|
1269
|
-
throw new Error(
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Unknown: ${action}. Use: route|unroute|cookies|headers|capture|requests|request`,
|
|
1634
|
+
);
|
|
1270
1635
|
}
|
|
1271
1636
|
|
|
1272
1637
|
return { success: true };
|
|
@@ -1282,7 +1647,7 @@ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
|
|
|
1282
1647
|
|
|
1283
1648
|
function truncateForOutput(text: string, maxOutput: number): string {
|
|
1284
1649
|
if (!(maxOutput > 0) || text.length <= maxOutput) return text;
|
|
1285
|
-
return text.slice(0, maxOutput)
|
|
1650
|
+
return `${text.slice(0, maxOutput)}\n[... truncated at ${maxOutput} chars]`;
|
|
1286
1651
|
}
|
|
1287
1652
|
|
|
1288
1653
|
const evalCommand: Command = {
|
|
@@ -1290,9 +1655,22 @@ const evalCommand: Command = {
|
|
|
1290
1655
|
description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
|
|
1291
1656
|
options: [
|
|
1292
1657
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1293
|
-
{
|
|
1294
|
-
|
|
1295
|
-
|
|
1658
|
+
{
|
|
1659
|
+
name: 'stdin',
|
|
1660
|
+
type: 'boolean',
|
|
1661
|
+
description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)',
|
|
1662
|
+
default: false,
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
name: 'max-output',
|
|
1666
|
+
type: 'number',
|
|
1667
|
+
description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)`,
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
name: 'timeout',
|
|
1671
|
+
type: 'number',
|
|
1672
|
+
description: 'Max ms to wait for evaluation to settle (default 30000)',
|
|
1673
|
+
},
|
|
1296
1674
|
],
|
|
1297
1675
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1298
1676
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1320,6 +1698,272 @@ const evalCommand: Command = {
|
|
|
1320
1698
|
},
|
|
1321
1699
|
};
|
|
1322
1700
|
|
|
1701
|
+
const reportCommand: Command = {
|
|
1702
|
+
name: 'report',
|
|
1703
|
+
description:
|
|
1704
|
+
'Test a page and write one self-contained HTML report + sibling JSON. Usage: monomind browse report <url>',
|
|
1705
|
+
options: [
|
|
1706
|
+
{ name: 'out', short: 'o', type: 'string', description: 'Output .html path, or a directory' },
|
|
1707
|
+
{
|
|
1708
|
+
name: 'budget',
|
|
1709
|
+
short: 'b',
|
|
1710
|
+
type: 'string',
|
|
1711
|
+
description: 'Budget JSON file path, or inline JSON',
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
name: 'devices',
|
|
1715
|
+
short: 'd',
|
|
1716
|
+
type: 'string',
|
|
1717
|
+
description: 'Comma-separated device names for the screenshot matrix',
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
name: 'wait',
|
|
1721
|
+
short: 'w',
|
|
1722
|
+
type: 'string',
|
|
1723
|
+
description: 'Extra settle step: a CSS selector to wait for, or milliseconds',
|
|
1724
|
+
},
|
|
1725
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1726
|
+
{
|
|
1727
|
+
name: 'full-page',
|
|
1728
|
+
type: 'boolean',
|
|
1729
|
+
description: 'Full-page screenshot (--no-full-page for viewport only)',
|
|
1730
|
+
default: true,
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
name: 'vitals-wait',
|
|
1734
|
+
type: 'number',
|
|
1735
|
+
description: 'Milliseconds to let the web-vitals observers run',
|
|
1736
|
+
default: 2500,
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
name: 'timeout',
|
|
1740
|
+
type: 'number',
|
|
1741
|
+
description: 'Milliseconds to wait for the page to go network-idle',
|
|
1742
|
+
default: 20000,
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
name: 'keep-open',
|
|
1746
|
+
type: 'boolean',
|
|
1747
|
+
description: 'Leave the browser running after the report',
|
|
1748
|
+
default: false,
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: 'repeat',
|
|
1752
|
+
type: 'number',
|
|
1753
|
+
description: 'Run the URL N times and report per-check flake rates (RIG-14)',
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: 'record',
|
|
1757
|
+
type: 'boolean',
|
|
1758
|
+
description: 'Record frames for the evidence timeline even when the run passes',
|
|
1759
|
+
default: false,
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
name: 'history',
|
|
1763
|
+
type: 'boolean',
|
|
1764
|
+
description: 'Print the stored run history for this URL instead of running it',
|
|
1765
|
+
default: false,
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
name: 'save',
|
|
1769
|
+
type: 'boolean',
|
|
1770
|
+
description: 'Save this run to the history store (--no-save to skip)',
|
|
1771
|
+
default: true,
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
name: 'history-max',
|
|
1775
|
+
type: 'number',
|
|
1776
|
+
description: 'Runs kept per URL before the oldest are pruned (default 20)',
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
name: 'trend-window',
|
|
1780
|
+
type: 'number',
|
|
1781
|
+
description: 'Prior runs charted in the trend section (default 10)',
|
|
1782
|
+
},
|
|
1783
|
+
],
|
|
1784
|
+
examples: [
|
|
1785
|
+
{ command: 'monomind browse report https://example.com', description: 'Report with defaults' },
|
|
1786
|
+
{
|
|
1787
|
+
command: 'monomind browse report https://example.com --repeat 5',
|
|
1788
|
+
description: 'Run five times and report flake rates',
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
command: 'monomind browse report https://example.com --history',
|
|
1792
|
+
description: 'Show the stored run history for a URL',
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
command: 'monomind browse report https://example.com --record',
|
|
1796
|
+
description: 'Attach a frame-by-frame evidence timeline',
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
command: 'monomind browse report https://example.com --out ./reports/home.html',
|
|
1800
|
+
description: 'Choose the output path',
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
command: 'monomind browse report https://example.com --budget \'{"lcp":4000}\'',
|
|
1804
|
+
description: 'Relax a budget inline',
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
command: 'monomind browse report https://example.com --devices "iPhone 14,iPad"',
|
|
1808
|
+
description: 'Add a device screenshot matrix',
|
|
1809
|
+
},
|
|
1810
|
+
],
|
|
1811
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1812
|
+
const url = ctx.args[0] as string;
|
|
1813
|
+
if (!url) throw new Error('URL required. Usage: monomind browse report <url>');
|
|
1814
|
+
|
|
1815
|
+
// Imported lazily for the same reason getBrowser() is: this module loads
|
|
1816
|
+
// on every CLI invocation, and nothing but `report` needs the renderer.
|
|
1817
|
+
const { runReport, runReportRepeated, readHistory } = await import('../report/index.js');
|
|
1818
|
+
|
|
1819
|
+
// --history is a read of the store, so it deliberately never launches a
|
|
1820
|
+
// browser — `report --history` must work offline and on a dead site.
|
|
1821
|
+
if (ctx.flags.history) {
|
|
1822
|
+
const { dir, runs } = await readHistory(url);
|
|
1823
|
+
if (ctx.flags.json) {
|
|
1824
|
+
print(JSON.stringify({ data: { dir, runs } }, null, 2));
|
|
1825
|
+
} else if (!runs.length) {
|
|
1826
|
+
output.printWarning(`No stored runs for ${url}`);
|
|
1827
|
+
print(`Looked in: ${dir}`);
|
|
1828
|
+
} else {
|
|
1829
|
+
print(`${runs.length} run(s) for ${url}`);
|
|
1830
|
+
print(`Stored in: ${dir}`);
|
|
1831
|
+
for (const run of runs) {
|
|
1832
|
+
const lcp = run.vitals.lcp === undefined ? '—' : `${Math.round(run.vitals.lcp)}ms`;
|
|
1833
|
+
print(
|
|
1834
|
+
` ${run.capturedAt} ${run.verdict.toUpperCase().padEnd(4)} ` +
|
|
1835
|
+
`LCP ${lcp.padStart(7)} ${run.counts.consoleErrors} console errors ` +
|
|
1836
|
+
`${run.counts.failedRequests} failed requests ${run.counts.a11yErrors} a11y errors`,
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return { success: true, data: { dir, runs } };
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const repeatRaw = ctx.flags.repeat as number | undefined;
|
|
1844
|
+
const repeat =
|
|
1845
|
+
typeof repeatRaw === 'number' && Number.isFinite(repeatRaw) && repeatRaw > 1
|
|
1846
|
+
? Math.floor(repeatRaw)
|
|
1847
|
+
: undefined;
|
|
1848
|
+
|
|
1849
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1850
|
+
const browser = await getBrowser();
|
|
1851
|
+
|
|
1852
|
+
const rawDevices = ctx.flags.devices;
|
|
1853
|
+
const devices =
|
|
1854
|
+
typeof rawDevices === 'string'
|
|
1855
|
+
? rawDevices
|
|
1856
|
+
.split(',')
|
|
1857
|
+
.map((d) => d.trim())
|
|
1858
|
+
.filter(Boolean)
|
|
1859
|
+
: undefined;
|
|
1860
|
+
|
|
1861
|
+
const runOptions = {
|
|
1862
|
+
url,
|
|
1863
|
+
out: ctx.flags.out as string | undefined,
|
|
1864
|
+
budget: ctx.flags.budget as string | undefined,
|
|
1865
|
+
devices,
|
|
1866
|
+
wait: ctx.flags.wait as string | undefined,
|
|
1867
|
+
vitalsWaitMs: ctx.flags['vitals-wait'] as number | undefined,
|
|
1868
|
+
loadTimeoutMs: ctx.flags.timeout as number | undefined,
|
|
1869
|
+
fullPage: ctx.flags['full-page'] !== false,
|
|
1870
|
+
cwd: ctx.cwd,
|
|
1871
|
+
// A failing run records its own evidence without being asked; --record
|
|
1872
|
+
// is for when you want the timeline from a run that passes.
|
|
1873
|
+
record: ctx.flags.record === true,
|
|
1874
|
+
history: ctx.flags.save !== false,
|
|
1875
|
+
historyMax: ctx.flags['history-max'] as number | undefined,
|
|
1876
|
+
trendWindow: ctx.flags['trend-window'] as number | undefined,
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
let result:
|
|
1880
|
+
| Awaited<ReturnType<typeof runReport>>
|
|
1881
|
+
| Awaited<ReturnType<typeof runReportRepeated>>;
|
|
1882
|
+
try {
|
|
1883
|
+
result = repeat
|
|
1884
|
+
? await runReportRepeated(client, sessionId, { ...runOptions, repeat })
|
|
1885
|
+
: await runReport(client, sessionId, runOptions);
|
|
1886
|
+
} finally {
|
|
1887
|
+
// A report is a one-shot command — CI should not be left with an
|
|
1888
|
+
// orphan Chrome. Only close a browser THIS process launched: an
|
|
1889
|
+
// attached one belongs to the user's own `open`/`connect` session.
|
|
1890
|
+
if (!ctx.flags['keep-open'] && browser.getLaunchedPid(_port) !== undefined) {
|
|
1891
|
+
browser.stopRequestCapture(sessionId);
|
|
1892
|
+
browser.teardownConsoleCapture(sessionId);
|
|
1893
|
+
try {
|
|
1894
|
+
await browser.closeBrowser(client, _port);
|
|
1895
|
+
} catch {
|
|
1896
|
+
/* best-effort */
|
|
1897
|
+
}
|
|
1898
|
+
_client = null;
|
|
1899
|
+
_sessionId = '';
|
|
1900
|
+
_targetId = '';
|
|
1901
|
+
_refs = new Map();
|
|
1902
|
+
await browser.clearActivePort();
|
|
1903
|
+
await browser.clearRefCache();
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const { report, htmlPath, jsonPath, summary, historyDir } = result;
|
|
1908
|
+
const { flake } = result;
|
|
1909
|
+
// With --repeat the flake verdict governs: a check that failed 2 of 5
|
|
1910
|
+
// runs must not exit 0 just because the last run happened to be green.
|
|
1911
|
+
const passed = flake ? flake.verdict === 'pass' : report.verdict === 'pass';
|
|
1912
|
+
|
|
1913
|
+
if (ctx.flags.json) {
|
|
1914
|
+
const { toJsonReport } = await import('../report/index.js');
|
|
1915
|
+
print(
|
|
1916
|
+
JSON.stringify(
|
|
1917
|
+
{ data: { ...toJsonReport(report), flake, htmlPath, jsonPath, historyDir } },
|
|
1918
|
+
null,
|
|
1919
|
+
2,
|
|
1920
|
+
),
|
|
1921
|
+
);
|
|
1922
|
+
} else {
|
|
1923
|
+
if (passed) output.printSuccess(summary);
|
|
1924
|
+
else output.printError(summary);
|
|
1925
|
+
if (flake) {
|
|
1926
|
+
print(` ${flake.confidenceNote}`);
|
|
1927
|
+
for (const check of flake.checks.filter((c) => !c.stable)) {
|
|
1928
|
+
print(` ${check.label}: failed ${check.failed} of ${check.runs} runs`);
|
|
1929
|
+
}
|
|
1930
|
+
for (const signal of flake.signals.slice(0, 5)) {
|
|
1931
|
+
print(
|
|
1932
|
+
` ${signal.kind} "${signal.signature}" in ${signal.runs} of ${signal.total} runs`,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
for (const failure of report.failures) {
|
|
1937
|
+
print(` ${failure.budget}: expected ${failure.expected}, got ${failure.actual}`);
|
|
1938
|
+
if (failure.detail) print(` ${failure.detail}`);
|
|
1939
|
+
}
|
|
1940
|
+
for (const headline of report.trend?.headlines ?? []) output.printWarning(headline);
|
|
1941
|
+
if (report.diff?.structure) {
|
|
1942
|
+
const d = report.diff.structure;
|
|
1943
|
+
print(
|
|
1944
|
+
` vs previous run: ${d.gained.length} gained, ${d.lost.length} lost, ${d.renamed.length} renamed`,
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
if (report.diff?.pixels?.comparable) {
|
|
1948
|
+
print(
|
|
1949
|
+
` vs previous run: ${report.diff.pixels.changedPercent.toFixed(2)}% of pixels changed`,
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
for (const note of report.notes) output.printWarning(note);
|
|
1953
|
+
print(`Report: ${htmlPath}`);
|
|
1954
|
+
print(`JSON: ${jsonPath}`);
|
|
1955
|
+
if (historyDir) print(`History: ${historyDir}`);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Non-zero exit is the point of RIG-06 — CI and agents gate on it.
|
|
1959
|
+
return {
|
|
1960
|
+
success: passed,
|
|
1961
|
+
exitCode: passed ? 0 : 1,
|
|
1962
|
+
data: { htmlPath, jsonPath, report, flake },
|
|
1963
|
+
};
|
|
1964
|
+
},
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1323
1967
|
const closeCommand: Command = {
|
|
1324
1968
|
name: 'close',
|
|
1325
1969
|
description: 'Close the active browser session',
|
|
@@ -1330,13 +1974,25 @@ const closeCommand: Command = {
|
|
|
1330
1974
|
const client = _client;
|
|
1331
1975
|
// Tear down per-session Maps and listeners before closing
|
|
1332
1976
|
if (browser.getHarStatus(sid).recording) {
|
|
1333
|
-
try {
|
|
1977
|
+
try {
|
|
1978
|
+
await browser.stopHarRecording(client, sid);
|
|
1979
|
+
} catch {
|
|
1980
|
+
/* ignore */
|
|
1981
|
+
}
|
|
1334
1982
|
}
|
|
1335
1983
|
if (browser.getTraceStatus(sid)) {
|
|
1336
|
-
try {
|
|
1984
|
+
try {
|
|
1985
|
+
await browser.stopTrace(client, sid);
|
|
1986
|
+
} catch {
|
|
1987
|
+
/* ignore */
|
|
1988
|
+
}
|
|
1337
1989
|
}
|
|
1338
1990
|
if (browser.isProfilingActive(sid)) {
|
|
1339
|
-
try {
|
|
1991
|
+
try {
|
|
1992
|
+
await browser.stopCpuProfile(client, sid);
|
|
1993
|
+
} catch {
|
|
1994
|
+
/* ignore */
|
|
1995
|
+
}
|
|
1340
1996
|
}
|
|
1341
1997
|
browser.teardownRouteInterception(sid);
|
|
1342
1998
|
browser.stopRequestCapture(sid);
|
|
@@ -1370,7 +2026,11 @@ const closeCommand: Command = {
|
|
|
1370
2026
|
} finally {
|
|
1371
2027
|
// Always drop our websocket — a hung Browser.close must not keep
|
|
1372
2028
|
// this CLI process's event loop alive.
|
|
1373
|
-
try {
|
|
2029
|
+
try {
|
|
2030
|
+
conn.client.close();
|
|
2031
|
+
} catch {
|
|
2032
|
+
/* already gone */
|
|
2033
|
+
}
|
|
1374
2034
|
}
|
|
1375
2035
|
// closeBrowser has no PID fallback in a fresh process (launchedPids
|
|
1376
2036
|
// is per-process) — re-probe so we report what actually happened.
|
|
@@ -1380,19 +2040,28 @@ const closeCommand: Command = {
|
|
|
1380
2040
|
const probeDeadline = Date.now() + 3000;
|
|
1381
2041
|
while (stillUp && Date.now() < probeDeadline) {
|
|
1382
2042
|
try {
|
|
1383
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1384
|
-
|
|
1385
|
-
|
|
2043
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
2044
|
+
signal: AbortSignal.timeout(800),
|
|
2045
|
+
});
|
|
2046
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2047
|
+
} catch {
|
|
2048
|
+
stillUp = false;
|
|
2049
|
+
}
|
|
1386
2050
|
}
|
|
1387
|
-
if (stillUp)
|
|
2051
|
+
if (stillUp)
|
|
2052
|
+
output.printWarning(
|
|
2053
|
+
`Browser on port ${persisted.port} did not exit — kill it manually if needed`,
|
|
2054
|
+
);
|
|
1388
2055
|
else output.printSuccess(`Closed browser on port ${persisted.port}`);
|
|
1389
2056
|
} catch {
|
|
1390
2057
|
output.printInfo(`No browser answering on port ${persisted.port} — nothing to close`);
|
|
1391
2058
|
}
|
|
1392
2059
|
} else {
|
|
1393
|
-
output.printInfo(
|
|
1394
|
-
|
|
1395
|
-
|
|
2060
|
+
output.printInfo(
|
|
2061
|
+
persisted
|
|
2062
|
+
? `Detached from browser on port ${persisted.port} (attached via connect — left running)`
|
|
2063
|
+
: 'No active browser session',
|
|
2064
|
+
);
|
|
1396
2065
|
}
|
|
1397
2066
|
await browser.clearActivePort();
|
|
1398
2067
|
await browser.clearRefCache();
|
|
@@ -1439,7 +2108,8 @@ const focusCommand: Command = {
|
|
|
1439
2108
|
|
|
1440
2109
|
const typeCommand: Command = {
|
|
1441
2110
|
name: 'type',
|
|
1442
|
-
description:
|
|
2111
|
+
description:
|
|
2112
|
+
'Type text into element (appends, does not clear). Usage: monomind browse type @e1 "text"',
|
|
1443
2113
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1444
2114
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1445
2115
|
const browser = await getBrowser();
|
|
@@ -1566,7 +2236,7 @@ async function resolveElementObjectId(
|
|
|
1566
2236
|
client: import('../index.js').CdpClient,
|
|
1567
2237
|
sessionId: string,
|
|
1568
2238
|
refs: Map<string, import('../index.js').ElementRef>,
|
|
1569
|
-
refOrSelector: string
|
|
2239
|
+
refOrSelector: string,
|
|
1570
2240
|
): Promise<string> {
|
|
1571
2241
|
const browser = await getBrowser();
|
|
1572
2242
|
if (refOrSelector.startsWith('@') || /^e\d+$/.test(refOrSelector)) {
|
|
@@ -1577,11 +2247,16 @@ async function resolveElementObjectId(
|
|
|
1577
2247
|
return objectId;
|
|
1578
2248
|
}
|
|
1579
2249
|
// CSS selector path
|
|
1580
|
-
const res = await client.send<{ result: { objectId?: string; subtype?: string } }>(
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
2250
|
+
const res = await client.send<{ result: { objectId?: string; subtype?: string } }>(
|
|
2251
|
+
'Runtime.evaluate',
|
|
2252
|
+
{
|
|
2253
|
+
expression: `document.querySelector(${JSON.stringify(refOrSelector)})`,
|
|
2254
|
+
returnByValue: false,
|
|
2255
|
+
},
|
|
2256
|
+
sessionId,
|
|
2257
|
+
);
|
|
2258
|
+
if (!res.result?.objectId || res.result?.subtype === 'null')
|
|
2259
|
+
throw new Error(`Selector not found: ${refOrSelector}`);
|
|
1585
2260
|
return res.result.objectId;
|
|
1586
2261
|
}
|
|
1587
2262
|
|
|
@@ -1594,63 +2269,87 @@ const isvisibleCommand: Command = {
|
|
|
1594
2269
|
const arg = ctx.args[0] as string;
|
|
1595
2270
|
if (!arg) throw new Error('Usage: monomind browse isvisible @e1|".selector"');
|
|
1596
2271
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1597
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2272
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2273
|
+
'Runtime.callFunctionOn',
|
|
2274
|
+
{
|
|
2275
|
+
functionDeclaration: `function(){var r=this.getBoundingClientRect(),s=window.getComputedStyle(this);return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none'&&parseFloat(s.opacity)>0;}`,
|
|
2276
|
+
objectId,
|
|
2277
|
+
returnByValue: true,
|
|
2278
|
+
},
|
|
2279
|
+
sessionId,
|
|
2280
|
+
);
|
|
1602
2281
|
const visible = r.result?.value ?? false;
|
|
1603
|
-
if (ctx.flags.json) {
|
|
1604
|
-
|
|
2282
|
+
if (ctx.flags.json) {
|
|
2283
|
+
print(JSON.stringify({ visible }));
|
|
2284
|
+
} else {
|
|
2285
|
+
output.printSuccess(`isvisible: ${visible}`);
|
|
2286
|
+
}
|
|
1605
2287
|
return { success: true, data: { visible } };
|
|
1606
2288
|
},
|
|
1607
2289
|
};
|
|
1608
2290
|
|
|
1609
2291
|
const isenabledCommand: Command = {
|
|
1610
2292
|
name: 'isenabled',
|
|
1611
|
-
description:
|
|
2293
|
+
description:
|
|
2294
|
+
'Check if element is enabled (not disabled). Usage: monomind browse isenabled @e1|"selector"',
|
|
1612
2295
|
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
1613
2296
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1614
2297
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1615
2298
|
const arg = ctx.args[0] as string;
|
|
1616
2299
|
if (!arg) throw new Error('Usage: monomind browse isenabled @e1|".selector"');
|
|
1617
2300
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1618
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2301
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2302
|
+
'Runtime.callFunctionOn',
|
|
2303
|
+
{
|
|
2304
|
+
functionDeclaration: `function(){return !this.disabled;}`,
|
|
2305
|
+
objectId,
|
|
2306
|
+
returnByValue: true,
|
|
2307
|
+
},
|
|
2308
|
+
sessionId,
|
|
2309
|
+
);
|
|
1623
2310
|
const enabled = r.result?.value ?? true;
|
|
1624
|
-
if (ctx.flags.json) {
|
|
1625
|
-
|
|
2311
|
+
if (ctx.flags.json) {
|
|
2312
|
+
print(JSON.stringify({ enabled }));
|
|
2313
|
+
} else {
|
|
2314
|
+
output.printSuccess(`isenabled: ${enabled}`);
|
|
2315
|
+
}
|
|
1626
2316
|
return { success: true, data: { enabled } };
|
|
1627
2317
|
},
|
|
1628
2318
|
};
|
|
1629
2319
|
|
|
1630
2320
|
const ischeckedCommand: Command = {
|
|
1631
2321
|
name: 'ischecked',
|
|
1632
|
-
description:
|
|
2322
|
+
description:
|
|
2323
|
+
'Check if checkbox/radio is checked. Usage: monomind browse ischecked @e1|"selector"',
|
|
1633
2324
|
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
1634
2325
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1635
2326
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1636
2327
|
const arg = ctx.args[0] as string;
|
|
1637
2328
|
if (!arg) throw new Error('Usage: monomind browse ischecked @e1|".selector"');
|
|
1638
2329
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, arg);
|
|
1639
|
-
const r = await client.send<{ result: { value?: boolean } }>(
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2330
|
+
const r = await client.send<{ result: { value?: boolean } }>(
|
|
2331
|
+
'Runtime.callFunctionOn',
|
|
2332
|
+
{
|
|
2333
|
+
functionDeclaration: `function(){var el=this,tag=el.tagName&&el.tagName.toUpperCase();if(tag==='INPUT'&&(el.type==='checkbox'||el.type==='radio'))return el.checked;var role=el.getAttribute&&el.getAttribute('role');if(role&&['checkbox','radio','switch','menuitemcheckbox','menuitemradio','option','treeitem'].indexOf(role)!==-1)return el.getAttribute('aria-checked')==='true';var label=tag!=='LABEL'?el.closest&&el.closest('label'):el;if(label&&label.control&&(label.control.type==='checkbox'||label.control.type==='radio'))return label.control.checked;var inp=el.querySelector&&el.querySelector('input[type="checkbox"],input[type="radio"]');return inp?inp.checked:false;}`,
|
|
2334
|
+
objectId,
|
|
2335
|
+
returnByValue: true,
|
|
2336
|
+
},
|
|
2337
|
+
sessionId,
|
|
2338
|
+
);
|
|
1644
2339
|
const checked = r.result?.value ?? false;
|
|
1645
|
-
if (ctx.flags.json) {
|
|
1646
|
-
|
|
2340
|
+
if (ctx.flags.json) {
|
|
2341
|
+
print(JSON.stringify({ checked }));
|
|
2342
|
+
} else {
|
|
2343
|
+
output.printSuccess(`ischecked: ${checked}`);
|
|
2344
|
+
}
|
|
1647
2345
|
return { success: true, data: { checked } };
|
|
1648
2346
|
},
|
|
1649
2347
|
};
|
|
1650
2348
|
|
|
1651
2349
|
const tapCommand: Command = {
|
|
1652
2350
|
name: 'tap',
|
|
1653
|
-
description:
|
|
2351
|
+
description:
|
|
2352
|
+
'Tap element with a touch event (mobile testing). Usage: monomind browse tap @e1|"selector"',
|
|
1654
2353
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1655
2354
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1656
2355
|
const browser = await getBrowser();
|
|
@@ -1669,13 +2368,21 @@ const tapCommand: Command = {
|
|
|
1669
2368
|
x = Math.round(box.x);
|
|
1670
2369
|
y = Math.round(box.y);
|
|
1671
2370
|
} else {
|
|
1672
|
-
const posJson = await browser.evaluateJs(
|
|
1673
|
-
|
|
2371
|
+
const posJson = (await browser.evaluateJs(
|
|
2372
|
+
client,
|
|
2373
|
+
sessionId,
|
|
2374
|
+
`(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`,
|
|
2375
|
+
)) as string | null;
|
|
1674
2376
|
if (!posJson) throw new Error(`Selector not found: ${arg}`);
|
|
1675
2377
|
const pos = JSON.parse(posJson) as { x: number; y: number };
|
|
1676
|
-
x = Math.round(pos.x);
|
|
2378
|
+
x = Math.round(pos.x);
|
|
2379
|
+
y = Math.round(pos.y);
|
|
1677
2380
|
}
|
|
1678
|
-
await client.send(
|
|
2381
|
+
await client.send(
|
|
2382
|
+
'Input.dispatchTouchEvent',
|
|
2383
|
+
{ type: 'touchStart', touchPoints: [{ x, y }] },
|
|
2384
|
+
sessionId,
|
|
2385
|
+
);
|
|
1679
2386
|
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
|
|
1680
2387
|
output.printSuccess(`Tapped at (${x}, ${y})`);
|
|
1681
2388
|
return { success: true, data: { x, y } };
|
|
@@ -1684,31 +2391,62 @@ const tapCommand: Command = {
|
|
|
1684
2391
|
|
|
1685
2392
|
const swipeCommand: Command = {
|
|
1686
2393
|
name: 'swipe',
|
|
1687
|
-
description:
|
|
2394
|
+
description:
|
|
2395
|
+
'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
|
|
1688
2396
|
options: [
|
|
1689
|
-
{
|
|
1690
|
-
|
|
1691
|
-
|
|
2397
|
+
{
|
|
2398
|
+
name: 'x',
|
|
2399
|
+
type: 'number',
|
|
2400
|
+
description: 'Start X coordinate (default: center)',
|
|
2401
|
+
default: 200,
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: 'y',
|
|
2405
|
+
type: 'number',
|
|
2406
|
+
description: 'Start Y coordinate (default: center)',
|
|
2407
|
+
default: 400,
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
name: 'distance',
|
|
2411
|
+
short: 'd',
|
|
2412
|
+
type: 'number',
|
|
2413
|
+
description: 'Swipe distance in pixels',
|
|
2414
|
+
default: 300,
|
|
2415
|
+
},
|
|
1692
2416
|
],
|
|
1693
2417
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1694
2418
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1695
|
-
const direction =
|
|
2419
|
+
const direction = ctx.args[0] as string as 'up' | 'down' | 'left' | 'right';
|
|
1696
2420
|
if (!['up', 'down', 'left', 'right'].includes(direction)) {
|
|
1697
|
-
throw new Error(
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
'Usage: monomind browse swipe up|down|left|right [--x N] [--y N] [--distance N]',
|
|
2423
|
+
);
|
|
1698
2424
|
}
|
|
1699
2425
|
const startX = (ctx.flags.x as number) ?? 200;
|
|
1700
2426
|
const startY = (ctx.flags.y as number) ?? 400;
|
|
1701
|
-
const positionalDistance =
|
|
1702
|
-
|
|
2427
|
+
const positionalDistance =
|
|
2428
|
+
ctx.args[1] !== undefined ? parseInt(ctx.args[1] as string, 10) : undefined;
|
|
2429
|
+
const distance =
|
|
2430
|
+
positionalDistance && Number.isFinite(positionalDistance)
|
|
2431
|
+
? positionalDistance
|
|
2432
|
+
: ((ctx.flags.distance as number) ?? 300);
|
|
1703
2433
|
const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
|
|
1704
2434
|
const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
|
|
1705
2435
|
|
|
1706
|
-
await client.send(
|
|
2436
|
+
await client.send(
|
|
2437
|
+
'Input.dispatchTouchEvent',
|
|
2438
|
+
{ type: 'touchStart', touchPoints: [{ x: startX, y: startY }] },
|
|
2439
|
+
sessionId,
|
|
2440
|
+
);
|
|
1707
2441
|
const steps = 10;
|
|
1708
2442
|
for (let i = 1; i <= steps; i++) {
|
|
1709
|
-
const x = Math.round(startX + dx * i / steps);
|
|
1710
|
-
const y = Math.round(startY + dy * i / steps);
|
|
1711
|
-
await client.send(
|
|
2443
|
+
const x = Math.round(startX + (dx * i) / steps);
|
|
2444
|
+
const y = Math.round(startY + (dy * i) / steps);
|
|
2445
|
+
await client.send(
|
|
2446
|
+
'Input.dispatchTouchEvent',
|
|
2447
|
+
{ type: 'touchMove', touchPoints: [{ x, y }] },
|
|
2448
|
+
sessionId,
|
|
2449
|
+
);
|
|
1712
2450
|
await new Promise((r) => setTimeout(r, 16));
|
|
1713
2451
|
}
|
|
1714
2452
|
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }, sessionId);
|
|
@@ -1761,7 +2499,8 @@ const uploadCommand: Command = {
|
|
|
1761
2499
|
const browser = await getBrowser();
|
|
1762
2500
|
const refArg = ctx.args[0] as string;
|
|
1763
2501
|
const files = ctx.args.slice(1) as string[];
|
|
1764
|
-
if (!refArg || files.length === 0)
|
|
2502
|
+
if (!refArg || files.length === 0)
|
|
2503
|
+
throw new Error('Usage: monomind browse upload @e1 <file1> [file2...]');
|
|
1765
2504
|
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
1766
2505
|
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
1767
2506
|
await browser.uploadFile(client, sessionId, ref, files);
|
|
@@ -1772,37 +2511,57 @@ const uploadCommand: Command = {
|
|
|
1772
2511
|
|
|
1773
2512
|
const downloadCommand: Command = {
|
|
1774
2513
|
name: 'download',
|
|
1775
|
-
description:
|
|
2514
|
+
description:
|
|
2515
|
+
'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
|
|
1776
2516
|
options: [
|
|
1777
|
-
{
|
|
2517
|
+
{
|
|
2518
|
+
name: 'timeout',
|
|
2519
|
+
short: 't',
|
|
2520
|
+
type: 'number',
|
|
2521
|
+
description: 'Max wait for download in ms',
|
|
2522
|
+
default: 30000,
|
|
2523
|
+
},
|
|
1778
2524
|
{ name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
|
|
1779
2525
|
],
|
|
1780
2526
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1781
2527
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1782
|
-
const
|
|
2528
|
+
const _browser = await getBrowser();
|
|
1783
2529
|
const refOrSel = ctx.args[0] as string;
|
|
1784
2530
|
const savePath = ctx.args[1] as string;
|
|
1785
|
-
if (!refOrSel || !savePath)
|
|
2531
|
+
if (!refOrSel || !savePath)
|
|
2532
|
+
throw new Error('Usage: monomind browse download @e1|selector <save-path>');
|
|
1786
2533
|
|
|
1787
|
-
const { mkdir } = await import('fs/promises');
|
|
1788
|
-
const { dirname } = await import('path');
|
|
1789
|
-
const { tmpdir } = await import('os');
|
|
1790
|
-
const { join } = await import('path');
|
|
2534
|
+
const { mkdir } = await import('node:fs/promises');
|
|
2535
|
+
const { dirname } = await import('node:path');
|
|
2536
|
+
const { tmpdir } = await import('node:os');
|
|
2537
|
+
const { join } = await import('node:path');
|
|
1791
2538
|
const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
|
|
1792
2539
|
await mkdir(downloadDir, { recursive: true });
|
|
1793
2540
|
|
|
1794
2541
|
// Enable Page.downloadWillBegin / Page.downloadProgress events
|
|
1795
|
-
await client
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2542
|
+
await client
|
|
2543
|
+
.send(
|
|
2544
|
+
'Browser.setDownloadBehavior',
|
|
2545
|
+
{
|
|
2546
|
+
behavior: 'allow',
|
|
2547
|
+
downloadPath: downloadDir,
|
|
2548
|
+
eventsEnabled: true,
|
|
2549
|
+
},
|
|
2550
|
+
undefined,
|
|
2551
|
+
)
|
|
2552
|
+
.catch(() => {
|
|
2553
|
+
// Fallback: older Chrome API (session-scoped)
|
|
2554
|
+
return client
|
|
2555
|
+
.send(
|
|
2556
|
+
'Page.setDownloadBehavior',
|
|
2557
|
+
{
|
|
2558
|
+
behavior: 'allow',
|
|
2559
|
+
downloadPath: downloadDir,
|
|
2560
|
+
},
|
|
2561
|
+
sessionId,
|
|
2562
|
+
)
|
|
2563
|
+
.catch(() => {});
|
|
2564
|
+
});
|
|
1806
2565
|
|
|
1807
2566
|
// Track when download completes
|
|
1808
2567
|
const downloadPromise = new Promise<string>((resolve, reject) => {
|
|
@@ -1814,38 +2573,52 @@ const downloadCommand: Command = {
|
|
|
1814
2573
|
let offProgress: (() => void) | undefined;
|
|
1815
2574
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
1816
2575
|
let timeout: ReturnType<typeof setTimeout>;
|
|
1817
|
-
const cleanup = () => {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2576
|
+
const cleanup = () => {
|
|
2577
|
+
clearTimeout(timeout);
|
|
2578
|
+
offBegin?.();
|
|
2579
|
+
offProgress?.();
|
|
2580
|
+
};
|
|
2581
|
+
timeout = setTimeout(() => {
|
|
2582
|
+
cleanup();
|
|
2583
|
+
reject(new Error('Download timed out'));
|
|
2584
|
+
}, ctx.flags.timeout as number);
|
|
2585
|
+
offProgress = client.on(
|
|
2586
|
+
'Browser.downloadProgress',
|
|
2587
|
+
async (params: Record<string, unknown>) => {
|
|
2588
|
+
if (params.guid === guid && params.state === 'completed') {
|
|
2589
|
+
cleanup();
|
|
2590
|
+
// Find the downloaded file in downloadDir
|
|
2591
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
2592
|
+
const files = await readdir(downloadDir);
|
|
2593
|
+
if (files.length > 0) {
|
|
2594
|
+
const src = join(downloadDir, files[0]);
|
|
2595
|
+
await mkdir(dirname(savePath), { recursive: true });
|
|
2596
|
+
await rename(src, savePath);
|
|
2597
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
2598
|
+
resolve(savePath);
|
|
2599
|
+
} else {
|
|
2600
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
2601
|
+
reject(new Error('Download completed but no file found'));
|
|
2602
|
+
}
|
|
2603
|
+
} else if (params.guid === guid && params.state === 'canceled') {
|
|
2604
|
+
cleanup();
|
|
2605
|
+
reject(new Error('Download was canceled'));
|
|
1834
2606
|
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
reject(new Error('Download was canceled'));
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
2607
|
+
},
|
|
2608
|
+
);
|
|
1840
2609
|
});
|
|
1841
2610
|
|
|
1842
2611
|
// Click the element to trigger download
|
|
1843
2612
|
const objectId = await resolveElementObjectId(client, sessionId, _refs, refOrSel);
|
|
1844
|
-
await client.send<{ result: unknown }>(
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2613
|
+
await client.send<{ result: unknown }>(
|
|
2614
|
+
'Runtime.callFunctionOn',
|
|
2615
|
+
{
|
|
2616
|
+
functionDeclaration: 'function(){ this.click(); }',
|
|
2617
|
+
objectId,
|
|
2618
|
+
returnByValue: true,
|
|
2619
|
+
},
|
|
2620
|
+
sessionId,
|
|
2621
|
+
);
|
|
1849
2622
|
|
|
1850
2623
|
const finalPath = await downloadPromise;
|
|
1851
2624
|
if (ctx.flags.json) print(JSON.stringify({ data: { path: finalPath } }));
|
|
@@ -1982,7 +2755,8 @@ const dialogCommand: Command = {
|
|
|
1982
2755
|
|
|
1983
2756
|
const frameCommand: Command = {
|
|
1984
2757
|
name: 'frame',
|
|
1985
|
-
description:
|
|
2758
|
+
description:
|
|
2759
|
+
'Switch to iframe or back to main. Usage: monomind browse frame "#frame-id" | frame main',
|
|
1986
2760
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
1987
2761
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1988
2762
|
const browser = await getBrowser();
|
|
@@ -2015,9 +2789,7 @@ const frameCommand: Command = {
|
|
|
2015
2789
|
const tabCommand: Command = {
|
|
2016
2790
|
name: 'tab',
|
|
2017
2791
|
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
2018
|
-
options: [
|
|
2019
|
-
{ name: 'label', type: 'string', description: 'Label for new tab' },
|
|
2020
|
-
],
|
|
2792
|
+
options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
|
|
2021
2793
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2022
2794
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2023
2795
|
const browser = await getBrowser();
|
|
@@ -2042,13 +2814,25 @@ const tabCommand: Command = {
|
|
|
2042
2814
|
const sid = _sessionId;
|
|
2043
2815
|
// Stop profiling before closing the tab so CDP commands still reach the live session
|
|
2044
2816
|
if (browser.getHarStatus(sid).recording) {
|
|
2045
|
-
try {
|
|
2817
|
+
try {
|
|
2818
|
+
await browser.stopHarRecording(client, sid);
|
|
2819
|
+
} catch {
|
|
2820
|
+
/* ignore */
|
|
2821
|
+
}
|
|
2046
2822
|
}
|
|
2047
2823
|
if (browser.getTraceStatus(sid)) {
|
|
2048
|
-
try {
|
|
2824
|
+
try {
|
|
2825
|
+
await browser.stopTrace(client, sid);
|
|
2826
|
+
} catch {
|
|
2827
|
+
/* ignore */
|
|
2828
|
+
}
|
|
2049
2829
|
}
|
|
2050
2830
|
if (browser.isProfilingActive(sid)) {
|
|
2051
|
-
try {
|
|
2831
|
+
try {
|
|
2832
|
+
await browser.stopCpuProfile(client, sid);
|
|
2833
|
+
} catch {
|
|
2834
|
+
/* ignore */
|
|
2835
|
+
}
|
|
2052
2836
|
}
|
|
2053
2837
|
browser.teardownRouteInterception(sid);
|
|
2054
2838
|
browser.stopRequestCapture(sid);
|
|
@@ -2072,13 +2856,25 @@ const tabCommand: Command = {
|
|
|
2072
2856
|
const newSid = await browser.activateTab(client, sessionId, action);
|
|
2073
2857
|
const oldSid = _sessionId;
|
|
2074
2858
|
if (browser.getHarStatus(oldSid).recording) {
|
|
2075
|
-
try {
|
|
2859
|
+
try {
|
|
2860
|
+
await browser.stopHarRecording(client, oldSid);
|
|
2861
|
+
} catch {
|
|
2862
|
+
/* ignore */
|
|
2863
|
+
}
|
|
2076
2864
|
}
|
|
2077
2865
|
if (browser.getTraceStatus(oldSid)) {
|
|
2078
|
-
try {
|
|
2866
|
+
try {
|
|
2867
|
+
await browser.stopTrace(client, oldSid);
|
|
2868
|
+
} catch {
|
|
2869
|
+
/* ignore */
|
|
2870
|
+
}
|
|
2079
2871
|
}
|
|
2080
2872
|
if (browser.isProfilingActive(oldSid)) {
|
|
2081
|
-
try {
|
|
2873
|
+
try {
|
|
2874
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
2875
|
+
} catch {
|
|
2876
|
+
/* ignore */
|
|
2877
|
+
}
|
|
2082
2878
|
}
|
|
2083
2879
|
await browser.disableInterception(client, oldSid).catch(() => {});
|
|
2084
2880
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2105,23 +2901,47 @@ const windowCommand: Command = {
|
|
|
2105
2901
|
|
|
2106
2902
|
if (!action || action === 'new') {
|
|
2107
2903
|
// Create isolated browser context (incognito-like) with a fresh page
|
|
2108
|
-
const ctxResult = await client.send<{ browserContextId: string }>(
|
|
2904
|
+
const ctxResult = await client.send<{ browserContextId: string }>(
|
|
2905
|
+
'Target.createBrowserContext',
|
|
2906
|
+
{},
|
|
2907
|
+
undefined,
|
|
2908
|
+
);
|
|
2109
2909
|
const browserContextId = ctxResult.browserContextId;
|
|
2110
2910
|
const url = (ctx.args[1] as string) || 'about:blank';
|
|
2111
|
-
const targetResult = await client.send<{ targetId: string }>(
|
|
2911
|
+
const targetResult = await client.send<{ targetId: string }>(
|
|
2912
|
+
'Target.createTarget',
|
|
2913
|
+
{ url, browserContextId },
|
|
2914
|
+
undefined,
|
|
2915
|
+
);
|
|
2112
2916
|
const targetId = targetResult.targetId;
|
|
2113
|
-
const attachResult = await client.send<{ sessionId: string }>(
|
|
2917
|
+
const attachResult = await client.send<{ sessionId: string }>(
|
|
2918
|
+
'Target.attachToTarget',
|
|
2919
|
+
{ targetId, flatten: true },
|
|
2920
|
+
undefined,
|
|
2921
|
+
);
|
|
2114
2922
|
const newSessionId = attachResult.sessionId;
|
|
2115
2923
|
// W3: fully tear down old session before switching
|
|
2116
2924
|
const oldSid = _sessionId;
|
|
2117
2925
|
if (browser.getHarStatus(oldSid).recording) {
|
|
2118
|
-
try {
|
|
2926
|
+
try {
|
|
2927
|
+
await browser.stopHarRecording(client, oldSid);
|
|
2928
|
+
} catch {
|
|
2929
|
+
/* ignore */
|
|
2930
|
+
}
|
|
2119
2931
|
}
|
|
2120
2932
|
if (browser.getTraceStatus(oldSid)) {
|
|
2121
|
-
try {
|
|
2933
|
+
try {
|
|
2934
|
+
await browser.stopTrace(client, oldSid);
|
|
2935
|
+
} catch {
|
|
2936
|
+
/* ignore */
|
|
2937
|
+
}
|
|
2122
2938
|
}
|
|
2123
2939
|
if (browser.isProfilingActive(oldSid)) {
|
|
2124
|
-
try {
|
|
2940
|
+
try {
|
|
2941
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
2942
|
+
} catch {
|
|
2943
|
+
/* ignore */
|
|
2944
|
+
}
|
|
2125
2945
|
}
|
|
2126
2946
|
browser.teardownRouteInterception(oldSid);
|
|
2127
2947
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2196,7 +3016,8 @@ const errorsCommand: Command = {
|
|
|
2196
3016
|
|
|
2197
3017
|
const storageCommand: Command = {
|
|
2198
3018
|
name: 'storage',
|
|
2199
|
-
description:
|
|
3019
|
+
description:
|
|
3020
|
+
'localStorage/sessionStorage management. Usage: monomind browse storage local|session [key] [--set val] [--clear]',
|
|
2200
3021
|
options: [
|
|
2201
3022
|
{ name: 'set', type: 'string', description: 'Value to set for key' },
|
|
2202
3023
|
{ name: 'clear', type: 'boolean', description: 'Clear all storage', default: false },
|
|
@@ -2221,7 +3042,8 @@ const storageCommand: Command = {
|
|
|
2221
3042
|
}
|
|
2222
3043
|
|
|
2223
3044
|
if (key && ctx.flags.set !== undefined) {
|
|
2224
|
-
if (isLocal)
|
|
3045
|
+
if (isLocal)
|
|
3046
|
+
await browser.setLocalStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
2225
3047
|
else await browser.setSessionStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
2226
3048
|
output.printSuccess(`Set ${key}`);
|
|
2227
3049
|
return { success: true };
|
|
@@ -2281,11 +3103,13 @@ const cookiesCommand: Command = {
|
|
|
2281
3103
|
if (!name || value === undefined) {
|
|
2282
3104
|
throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
|
|
2283
3105
|
}
|
|
2284
|
-
await browser.setCookies(client, sessionId, [
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
3106
|
+
await browser.setCookies(client, sessionId, [
|
|
3107
|
+
{
|
|
3108
|
+
name,
|
|
3109
|
+
value,
|
|
3110
|
+
domain: ctx.flags.domain as string,
|
|
3111
|
+
},
|
|
3112
|
+
]);
|
|
2289
3113
|
output.printSuccess(`Cookie set: ${name}`);
|
|
2290
3114
|
break;
|
|
2291
3115
|
}
|
|
@@ -2323,9 +3147,7 @@ const pdfCommand: Command = {
|
|
|
2323
3147
|
const isCommand: Command = {
|
|
2324
3148
|
name: 'is',
|
|
2325
3149
|
description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
|
|
2326
|
-
options: [
|
|
2327
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2328
|
-
],
|
|
3150
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
2329
3151
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2330
3152
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2331
3153
|
const browser = await getBrowser();
|
|
@@ -2338,10 +3160,17 @@ const isCommand: Command = {
|
|
|
2338
3160
|
|
|
2339
3161
|
let result: boolean;
|
|
2340
3162
|
switch (check) {
|
|
2341
|
-
case 'visible':
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
3163
|
+
case 'visible':
|
|
3164
|
+
result = await browser.isVisible(client, sessionId, ref);
|
|
3165
|
+
break;
|
|
3166
|
+
case 'enabled':
|
|
3167
|
+
result = await browser.isEnabled(client, sessionId, ref);
|
|
3168
|
+
break;
|
|
3169
|
+
case 'checked':
|
|
3170
|
+
result = await browser.isChecked(client, sessionId, ref);
|
|
3171
|
+
break;
|
|
3172
|
+
default:
|
|
3173
|
+
throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
|
|
2345
3174
|
}
|
|
2346
3175
|
|
|
2347
3176
|
if (ctx.flags.json) {
|
|
@@ -2355,7 +3184,8 @@ const isCommand: Command = {
|
|
|
2355
3184
|
|
|
2356
3185
|
const findCommand: Command = {
|
|
2357
3186
|
name: 'find',
|
|
2358
|
-
description:
|
|
3187
|
+
description:
|
|
3188
|
+
'Find elements by semantic locators. Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
|
|
2359
3189
|
options: [
|
|
2360
3190
|
{ name: 'name', type: 'string', description: 'Filter by accessible name' },
|
|
2361
3191
|
{ name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
|
|
@@ -2369,7 +3199,10 @@ const findCommand: Command = {
|
|
|
2369
3199
|
const value = ctx.args[1] as string;
|
|
2370
3200
|
const action = ctx.args[2] as FindAction | undefined;
|
|
2371
3201
|
|
|
2372
|
-
if (!locator || !value)
|
|
3202
|
+
if (!locator || !value)
|
|
3203
|
+
throw new Error(
|
|
3204
|
+
'Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
|
|
3205
|
+
);
|
|
2373
3206
|
|
|
2374
3207
|
const opts = {
|
|
2375
3208
|
name: ctx.flags.name as string,
|
|
@@ -2382,9 +3215,15 @@ const findCommand: Command = {
|
|
|
2382
3215
|
if (locator === 'alttext') {
|
|
2383
3216
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2384
3217
|
const valJson = JSON.stringify(value);
|
|
2385
|
-
const found = await browser.evaluateJs(
|
|
2386
|
-
|
|
2387
|
-
|
|
3218
|
+
const found = (await browser.evaluateJs(
|
|
3219
|
+
client,
|
|
3220
|
+
sessionId,
|
|
3221
|
+
`(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`,
|
|
3222
|
+
)) as boolean;
|
|
3223
|
+
if (!found) {
|
|
3224
|
+
output.printWarning(`alttext not found: ${value}`);
|
|
3225
|
+
return { success: false };
|
|
3226
|
+
}
|
|
2388
3227
|
output.printSuccess(`Found element with alt="${value}"`);
|
|
2389
3228
|
return { success: true, data: { alttext: value } };
|
|
2390
3229
|
}
|
|
@@ -2393,28 +3232,49 @@ const findCommand: Command = {
|
|
|
2393
3232
|
if (locator === 'title') {
|
|
2394
3233
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2395
3234
|
const valJson = JSON.stringify(value);
|
|
2396
|
-
const found = await browser.evaluateJs(
|
|
2397
|
-
|
|
2398
|
-
|
|
3235
|
+
const found = (await browser.evaluateJs(
|
|
3236
|
+
client,
|
|
3237
|
+
sessionId,
|
|
3238
|
+
`(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`,
|
|
3239
|
+
)) as boolean;
|
|
3240
|
+
if (!found) {
|
|
3241
|
+
output.printWarning(`title not found: ${value}`);
|
|
3242
|
+
return { success: false };
|
|
3243
|
+
}
|
|
2399
3244
|
output.printSuccess(`Found element with title="${value}"`);
|
|
2400
3245
|
return { success: true, data: { title: value } };
|
|
2401
3246
|
}
|
|
2402
3247
|
|
|
2403
3248
|
let ref: ElementRef | null = null;
|
|
2404
3249
|
switch (locator) {
|
|
2405
|
-
case 'role':
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
case '
|
|
2409
|
-
|
|
3250
|
+
case 'role':
|
|
3251
|
+
ref = await browser.findByRole(client, sessionId, _refs, value, opts);
|
|
3252
|
+
break;
|
|
3253
|
+
case 'text':
|
|
3254
|
+
ref = await browser.findByText(client, sessionId, _refs, value, opts);
|
|
3255
|
+
break;
|
|
3256
|
+
case 'label':
|
|
3257
|
+
ref = await browser.findByLabel(client, sessionId, _refs, value, opts);
|
|
3258
|
+
break;
|
|
3259
|
+
case 'placeholder':
|
|
3260
|
+
ref = await browser.findByPlaceholder(client, sessionId, _refs, value, opts);
|
|
3261
|
+
break;
|
|
3262
|
+
case 'selector':
|
|
3263
|
+
ref = await browser.findBySelector(client, sessionId, _refs, value, opts);
|
|
3264
|
+
break;
|
|
2410
3265
|
case 'testid': {
|
|
2411
3266
|
const sel = await browser.findByTestId(client, sessionId, value);
|
|
2412
|
-
if (!sel) {
|
|
3267
|
+
if (!sel) {
|
|
3268
|
+
output.printWarning(`testid not found: ${value}`);
|
|
3269
|
+
return { success: false };
|
|
3270
|
+
}
|
|
2413
3271
|
output.printSuccess(`Found testid selector: ${sel}`);
|
|
2414
3272
|
return { success: true, data: { selector: sel } };
|
|
2415
3273
|
}
|
|
2416
3274
|
default:
|
|
2417
|
-
throw new Error(
|
|
3275
|
+
throw new Error(
|
|
3276
|
+
`Unknown locator: ${locator}. Use: role|text|label|placeholder|testid|alttext|title|selector`,
|
|
3277
|
+
);
|
|
2418
3278
|
}
|
|
2419
3279
|
|
|
2420
3280
|
if (!ref) {
|
|
@@ -2426,7 +3286,9 @@ const findCommand: Command = {
|
|
|
2426
3286
|
|
|
2427
3287
|
if (action) {
|
|
2428
3288
|
switch (action) {
|
|
2429
|
-
case 'click':
|
|
3289
|
+
case 'click':
|
|
3290
|
+
await browser.clickElement(client, sessionId, ref);
|
|
3291
|
+
break;
|
|
2430
3292
|
case 'fill': {
|
|
2431
3293
|
const fillValue = ctx.args[3] as string;
|
|
2432
3294
|
await browser.fillElement(client, sessionId, ref, fillValue ?? '');
|
|
@@ -2437,18 +3299,31 @@ const findCommand: Command = {
|
|
|
2437
3299
|
await browser.typeIntoElement(client, sessionId, ref, typeValue ?? '');
|
|
2438
3300
|
break;
|
|
2439
3301
|
}
|
|
2440
|
-
case 'hover':
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
case '
|
|
3302
|
+
case 'hover':
|
|
3303
|
+
await browser.hoverElement(client, sessionId, ref);
|
|
3304
|
+
break;
|
|
3305
|
+
case 'focus':
|
|
3306
|
+
await browser.focusElement(client, sessionId, ref);
|
|
3307
|
+
break;
|
|
3308
|
+
case 'check':
|
|
3309
|
+
await browser.checkElement(client, sessionId, ref, true);
|
|
3310
|
+
break;
|
|
3311
|
+
case 'uncheck':
|
|
3312
|
+
await browser.checkElement(client, sessionId, ref, false);
|
|
3313
|
+
break;
|
|
2444
3314
|
case 'text': {
|
|
2445
3315
|
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
2446
3316
|
if (objectId) {
|
|
2447
|
-
const r = await client.send<{ result: { value?: string } }>(
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
3317
|
+
const r = await client.send<{ result: { value?: string } }>(
|
|
3318
|
+
'Runtime.callFunctionOn',
|
|
3319
|
+
{
|
|
3320
|
+
functionDeclaration:
|
|
3321
|
+
'function() { return this.innerText || this.textContent || ""; }',
|
|
3322
|
+
objectId,
|
|
3323
|
+
returnByValue: true,
|
|
3324
|
+
},
|
|
3325
|
+
sessionId,
|
|
3326
|
+
);
|
|
2452
3327
|
print(r.result?.value ?? '');
|
|
2453
3328
|
}
|
|
2454
3329
|
break;
|
|
@@ -2478,9 +3353,16 @@ const highlightCommand: Command = {
|
|
|
2478
3353
|
|
|
2479
3354
|
const diffCommand: Command = {
|
|
2480
3355
|
name: 'diff',
|
|
2481
|
-
description:
|
|
3356
|
+
description:
|
|
3357
|
+
'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
|
|
2482
3358
|
options: [
|
|
2483
|
-
{
|
|
3359
|
+
{
|
|
3360
|
+
name: 'interactive',
|
|
3361
|
+
short: 'i',
|
|
3362
|
+
type: 'boolean',
|
|
3363
|
+
description: 'Snapshot interactive elements only',
|
|
3364
|
+
default: false,
|
|
3365
|
+
},
|
|
2484
3366
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2485
3367
|
],
|
|
2486
3368
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
@@ -2496,12 +3378,16 @@ const diffCommand: Command = {
|
|
|
2496
3378
|
// Capture snapshot at url1
|
|
2497
3379
|
await browser.openUrl(client, sessionId, url1);
|
|
2498
3380
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2499
|
-
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3381
|
+
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3382
|
+
interactiveOnly: ctx.flags.interactive as boolean,
|
|
3383
|
+
});
|
|
2500
3384
|
|
|
2501
3385
|
// Capture snapshot at url2
|
|
2502
3386
|
await browser.openUrl(client, sessionId, url2);
|
|
2503
3387
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2504
|
-
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3388
|
+
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3389
|
+
interactiveOnly: ctx.flags.interactive as boolean,
|
|
3390
|
+
});
|
|
2505
3391
|
|
|
2506
3392
|
_refs = snap2.refs;
|
|
2507
3393
|
await browser.saveRefCache(_targetId, snap2.url, _refs);
|
|
@@ -2516,17 +3402,32 @@ const diffCommand: Command = {
|
|
|
2516
3402
|
const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
|
|
2517
3403
|
|
|
2518
3404
|
if (ctx.flags.json) {
|
|
2519
|
-
print(
|
|
3405
|
+
print(
|
|
3406
|
+
JSON.stringify({
|
|
3407
|
+
changed,
|
|
3408
|
+
url1,
|
|
3409
|
+
url2,
|
|
3410
|
+
onlyIn1,
|
|
3411
|
+
onlyIn2,
|
|
3412
|
+
additions: onlyIn2.length,
|
|
3413
|
+
removals: onlyIn1.length,
|
|
3414
|
+
}),
|
|
3415
|
+
);
|
|
2520
3416
|
} else {
|
|
2521
3417
|
if (!changed) {
|
|
2522
3418
|
output.printSuccess(`No differences between ${url1} and ${url2}`);
|
|
2523
3419
|
} else {
|
|
2524
|
-
output.printWarning(
|
|
3420
|
+
output.printWarning(
|
|
3421
|
+
`Diff: ${url1} vs ${url2} — +${onlyIn2.length} lines, -${onlyIn1.length} lines`,
|
|
3422
|
+
);
|
|
2525
3423
|
for (const l of onlyIn1) print(`\x1b[31m- ${l}\x1b[0m`);
|
|
2526
3424
|
for (const l of onlyIn2) print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
2527
3425
|
}
|
|
2528
3426
|
}
|
|
2529
|
-
return {
|
|
3427
|
+
return {
|
|
3428
|
+
success: true,
|
|
3429
|
+
data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length },
|
|
3430
|
+
};
|
|
2530
3431
|
}
|
|
2531
3432
|
|
|
2532
3433
|
throw new Error('Usage: monomind browse diff url <url1> <url2>');
|
|
@@ -2557,12 +3458,18 @@ function tokenizeBatchCommand(input: string): string[] {
|
|
|
2557
3458
|
let inQuote: '"' | "'" | null = null;
|
|
2558
3459
|
for (const ch of input.trim()) {
|
|
2559
3460
|
if (inQuote) {
|
|
2560
|
-
if (ch === inQuote) {
|
|
2561
|
-
|
|
3461
|
+
if (ch === inQuote) {
|
|
3462
|
+
inQuote = null;
|
|
3463
|
+
} else {
|
|
3464
|
+
current += ch;
|
|
3465
|
+
}
|
|
2562
3466
|
} else if (ch === '"' || ch === "'") {
|
|
2563
3467
|
inQuote = ch;
|
|
2564
3468
|
} else if (/\s/.test(ch)) {
|
|
2565
|
-
if (current) {
|
|
3469
|
+
if (current) {
|
|
3470
|
+
tokens.push(current);
|
|
3471
|
+
current = '';
|
|
3472
|
+
}
|
|
2566
3473
|
} else {
|
|
2567
3474
|
current += ch;
|
|
2568
3475
|
}
|
|
@@ -2597,8 +3504,15 @@ function tokenizeBatchCommand(input: string): string[] {
|
|
|
2597
3504
|
* Exported for direct unit testing — not part of the CLI's public API.
|
|
2598
3505
|
*/
|
|
2599
3506
|
export function deriveBoxOutput(
|
|
2600
|
-
center: { x: number; y: number; width: number; height: number } | null
|
|
2601
|
-
): {
|
|
3507
|
+
center: { x: number; y: number; width: number; height: number } | null,
|
|
3508
|
+
): {
|
|
3509
|
+
x: number;
|
|
3510
|
+
y: number;
|
|
3511
|
+
width: number;
|
|
3512
|
+
height: number;
|
|
3513
|
+
centerX: number;
|
|
3514
|
+
centerY: number;
|
|
3515
|
+
} | null {
|
|
2602
3516
|
if (!center) return null;
|
|
2603
3517
|
return {
|
|
2604
3518
|
x: center.x - center.width / 2,
|
|
@@ -2610,7 +3524,11 @@ export function deriveBoxOutput(
|
|
|
2610
3524
|
};
|
|
2611
3525
|
}
|
|
2612
3526
|
|
|
2613
|
-
export function parseBatchCommandLine(cmdStr: string): {
|
|
3527
|
+
export function parseBatchCommandLine(cmdStr: string): {
|
|
3528
|
+
subName: string;
|
|
3529
|
+
subArgs: string[];
|
|
3530
|
+
flags: Record<string, unknown>;
|
|
3531
|
+
} {
|
|
2614
3532
|
const trimmed = cmdStr.trim();
|
|
2615
3533
|
const evalMatch = trimmed.match(/^eval\b\s*/);
|
|
2616
3534
|
if (!evalMatch) {
|
|
@@ -2635,7 +3553,6 @@ export function parseBatchCommandLine(cmdStr: string): { subName: string; subArg
|
|
|
2635
3553
|
flags[boolFlag[1]] = true;
|
|
2636
3554
|
rest = rest.slice(boolFlag[0].length);
|
|
2637
3555
|
consumedAnother = true;
|
|
2638
|
-
continue;
|
|
2639
3556
|
}
|
|
2640
3557
|
}
|
|
2641
3558
|
return { subName: 'eval', subArgs: [rest], flags };
|
|
@@ -2643,7 +3560,8 @@ export function parseBatchCommandLine(cmdStr: string): { subName: string; subArg
|
|
|
2643
3560
|
|
|
2644
3561
|
const batchCommand: Command = {
|
|
2645
3562
|
name: 'batch',
|
|
2646
|
-
description:
|
|
3563
|
+
description:
|
|
3564
|
+
'Execute multiple commands. Usage: monomind browse batch "open url" "snapshot -i" "click @e1"',
|
|
2647
3565
|
options: [
|
|
2648
3566
|
{ name: 'bail', type: 'boolean', description: 'Stop on first error', default: false },
|
|
2649
3567
|
{ name: 'json', type: 'boolean', description: 'Input from JSON stdin', default: false },
|
|
@@ -2692,7 +3610,11 @@ const batchCommand: Command = {
|
|
|
2692
3610
|
} else {
|
|
2693
3611
|
parsedFlags[key] = true;
|
|
2694
3612
|
}
|
|
2695
|
-
} else if (
|
|
3613
|
+
} else if (
|
|
3614
|
+
subArgs[i].startsWith('-') &&
|
|
3615
|
+
subArgs[i].length === 2 &&
|
|
3616
|
+
/[a-zA-Z]/.test(subArgs[i][1])
|
|
3617
|
+
) {
|
|
2696
3618
|
consumedIndices.add(i);
|
|
2697
3619
|
const shortKey = subArgs[i][1];
|
|
2698
3620
|
const optDef = subCmd.options?.find((o) => o.short === shortKey);
|
|
@@ -2723,7 +3645,11 @@ const batchCommand: Command = {
|
|
|
2723
3645
|
|
|
2724
3646
|
const cmdResult = await subCmd.action(fakeCtx);
|
|
2725
3647
|
const succeeded = cmdResult?.success !== false;
|
|
2726
|
-
results.push({
|
|
3648
|
+
results.push({
|
|
3649
|
+
command: cmdStr,
|
|
3650
|
+
success: succeeded,
|
|
3651
|
+
error: succeeded ? undefined : 'Command returned failure',
|
|
3652
|
+
});
|
|
2727
3653
|
if (!succeeded && ctx.flags.bail) break;
|
|
2728
3654
|
} catch (e) {
|
|
2729
3655
|
const err = e instanceof Error ? e.message : String(e);
|
|
@@ -2741,7 +3667,8 @@ const batchCommand: Command = {
|
|
|
2741
3667
|
|
|
2742
3668
|
const addinitscriptCommand: Command = {
|
|
2743
3669
|
name: 'addinitscript',
|
|
2744
|
-
description:
|
|
3670
|
+
description:
|
|
3671
|
+
'Add script to run before page navigation. Usage: monomind browse addinitscript "window.x=1"',
|
|
2745
3672
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2746
3673
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2747
3674
|
const browser = await getBrowser();
|
|
@@ -2755,7 +3682,8 @@ const addinitscriptCommand: Command = {
|
|
|
2755
3682
|
|
|
2756
3683
|
const removeinitscriptCommand: Command = {
|
|
2757
3684
|
name: 'removeinitscript',
|
|
2758
|
-
description:
|
|
3685
|
+
description:
|
|
3686
|
+
'Remove a previously added init script. Usage: monomind browse removeinitscript <id>',
|
|
2759
3687
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2760
3688
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2761
3689
|
const browser = await getBrowser();
|
|
@@ -2769,11 +3697,17 @@ const removeinitscriptCommand: Command = {
|
|
|
2769
3697
|
|
|
2770
3698
|
const connectCommand: Command = {
|
|
2771
3699
|
name: 'connect',
|
|
2772
|
-
description:
|
|
3700
|
+
description:
|
|
3701
|
+
'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port 9222] [--target <id>] [--auto-connect]',
|
|
2773
3702
|
options: [
|
|
2774
3703
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
2775
3704
|
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
2776
|
-
{
|
|
3705
|
+
{
|
|
3706
|
+
name: 'auto-connect',
|
|
3707
|
+
type: 'boolean',
|
|
3708
|
+
description: 'Auto-discover running Chrome on ports 9222 and 9229',
|
|
3709
|
+
default: false,
|
|
3710
|
+
},
|
|
2777
3711
|
],
|
|
2778
3712
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
2779
3713
|
let port = (ctx.flags.port as number) ?? 9222;
|
|
@@ -2784,10 +3718,19 @@ const connectCommand: Command = {
|
|
|
2784
3718
|
for (const p of probePorts) {
|
|
2785
3719
|
try {
|
|
2786
3720
|
const r = await fetch(`http://127.0.0.1:${p}/json/version`);
|
|
2787
|
-
if (r.ok) {
|
|
2788
|
-
|
|
3721
|
+
if (r.ok) {
|
|
3722
|
+
port = p;
|
|
3723
|
+
found = true;
|
|
3724
|
+
break;
|
|
3725
|
+
}
|
|
3726
|
+
} catch {
|
|
3727
|
+
/* port not open */
|
|
3728
|
+
}
|
|
2789
3729
|
}
|
|
2790
|
-
if (!found)
|
|
3730
|
+
if (!found)
|
|
3731
|
+
throw new Error(
|
|
3732
|
+
'No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.',
|
|
3733
|
+
);
|
|
2791
3734
|
}
|
|
2792
3735
|
|
|
2793
3736
|
const browser = await getBrowser();
|
|
@@ -2796,13 +3739,25 @@ const connectCommand: Command = {
|
|
|
2796
3739
|
const prevSid = _sessionId;
|
|
2797
3740
|
const prevClient = _client;
|
|
2798
3741
|
if (browser.getHarStatus(prevSid).recording) {
|
|
2799
|
-
try {
|
|
3742
|
+
try {
|
|
3743
|
+
await browser.stopHarRecording(prevClient, prevSid);
|
|
3744
|
+
} catch {
|
|
3745
|
+
/* ignore */
|
|
3746
|
+
}
|
|
2800
3747
|
}
|
|
2801
3748
|
if (browser.getTraceStatus(prevSid)) {
|
|
2802
|
-
try {
|
|
3749
|
+
try {
|
|
3750
|
+
await browser.stopTrace(prevClient, prevSid);
|
|
3751
|
+
} catch {
|
|
3752
|
+
/* ignore */
|
|
3753
|
+
}
|
|
2803
3754
|
}
|
|
2804
3755
|
if (browser.isProfilingActive(prevSid)) {
|
|
2805
|
-
try {
|
|
3756
|
+
try {
|
|
3757
|
+
await browser.stopCpuProfile(prevClient, prevSid);
|
|
3758
|
+
} catch {
|
|
3759
|
+
/* ignore */
|
|
3760
|
+
}
|
|
2806
3761
|
}
|
|
2807
3762
|
browser.teardownRouteInterception(prevSid);
|
|
2808
3763
|
browser.stopRequestCapture(prevSid);
|
|
@@ -2880,7 +3835,10 @@ const recordCommand: Command = {
|
|
|
2880
3835
|
case 'status': {
|
|
2881
3836
|
const status = browser.getRecordingStatus(sessionId);
|
|
2882
3837
|
if (ctx.flags.json) print(JSON.stringify({ data: status }));
|
|
2883
|
-
else
|
|
3838
|
+
else
|
|
3839
|
+
print(
|
|
3840
|
+
`Recording: ${status.recording} | Frames: ${status.frames}${status.autoStopped ? ' (auto-stopped: buffer limit reached — run "record stop" to save)' : ''}`,
|
|
3841
|
+
);
|
|
2884
3842
|
return { success: true, data: status };
|
|
2885
3843
|
}
|
|
2886
3844
|
default:
|
|
@@ -2904,7 +3862,9 @@ const traceCommand: Command = {
|
|
|
2904
3862
|
|
|
2905
3863
|
switch (action) {
|
|
2906
3864
|
case 'start':
|
|
2907
|
-
await browser.startTrace(client, sessionId, {
|
|
3865
|
+
await browser.startTrace(client, sessionId, {
|
|
3866
|
+
screenshots: ctx.flags.screenshots as boolean,
|
|
3867
|
+
});
|
|
2908
3868
|
output.printSuccess('Trace started');
|
|
2909
3869
|
return { success: true };
|
|
2910
3870
|
case 'stop': {
|
|
@@ -2937,7 +3897,9 @@ const profilerCommand: Command = {
|
|
|
2937
3897
|
|
|
2938
3898
|
switch (action) {
|
|
2939
3899
|
case 'start':
|
|
2940
|
-
await browser.startCpuProfile(client, sessionId, {
|
|
3900
|
+
await browser.startCpuProfile(client, sessionId, {
|
|
3901
|
+
samplingInterval: ctx.flags.interval as number,
|
|
3902
|
+
});
|
|
2941
3903
|
output.printSuccess('CPU profiler started');
|
|
2942
3904
|
return { success: true };
|
|
2943
3905
|
case 'stop': {
|
|
@@ -2997,7 +3959,12 @@ const harCommand: Command = {
|
|
|
2997
3959
|
output.printSuccess('HAR recording started');
|
|
2998
3960
|
return { success: true };
|
|
2999
3961
|
case 'stop': {
|
|
3000
|
-
const path = await browser.stopHarRecording(
|
|
3962
|
+
const path = await browser.stopHarRecording(
|
|
3963
|
+
client,
|
|
3964
|
+
sessionId,
|
|
3965
|
+
ctx.args[1] as string,
|
|
3966
|
+
ctx.flags.bodies as boolean,
|
|
3967
|
+
);
|
|
3001
3968
|
if (ctx.flags.json) print(JSON.stringify({ data: { path } }));
|
|
3002
3969
|
else output.printSuccess(`HAR saved: ${path}`);
|
|
3003
3970
|
return { success: true, data: { path } };
|
|
@@ -3022,7 +3989,8 @@ const resizeCommand: Command = {
|
|
|
3022
3989
|
const browser = await getBrowser();
|
|
3023
3990
|
const width = parseInt(ctx.args[0] as string, 10);
|
|
3024
3991
|
const height = parseInt(ctx.args[1] as string, 10);
|
|
3025
|
-
if (isNaN(width) || isNaN(height))
|
|
3992
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
3993
|
+
throw new Error('Usage: monomind browse resize <width> <height>');
|
|
3026
3994
|
await browser.setViewport(client, sessionId, width, height);
|
|
3027
3995
|
output.printSuccess(`Resized to ${width}x${height}`);
|
|
3028
3996
|
return { success: true, data: { width, height } };
|
|
@@ -3157,6 +4125,7 @@ const browseCommand: Command = {
|
|
|
3157
4125
|
traceCommand,
|
|
3158
4126
|
profilerCommand,
|
|
3159
4127
|
vitalsCommand,
|
|
4128
|
+
reportCommand,
|
|
3160
4129
|
harCommand,
|
|
3161
4130
|
resizeCommand,
|
|
3162
4131
|
closeCommand,
|
|
@@ -3169,7 +4138,10 @@ const browseCommand: Command = {
|
|
|
3169
4138
|
],
|
|
3170
4139
|
examples: [
|
|
3171
4140
|
{ command: 'monomind browse open https://example.com', description: 'Open a URL' },
|
|
3172
|
-
{
|
|
4141
|
+
{
|
|
4142
|
+
command: 'monomind browse snapshot -i',
|
|
4143
|
+
description: 'Interactive-only snapshot (93% token reduction)',
|
|
4144
|
+
},
|
|
3173
4145
|
{ command: 'monomind browse click @e3', description: 'Click element by ref' },
|
|
3174
4146
|
{ command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
|
|
3175
4147
|
{ command: 'monomind browse press Enter', description: 'Press Enter key' },
|
|
@@ -3183,7 +4155,14 @@ const browseCommand: Command = {
|
|
|
3183
4155
|
{ command: 'monomind browse state save my-session', description: 'Save session state' },
|
|
3184
4156
|
{ command: 'monomind browse navigate back', description: 'Navigate back' },
|
|
3185
4157
|
{ command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
|
|
3186
|
-
{
|
|
4158
|
+
{
|
|
4159
|
+
command: 'monomind browse network route --pattern "https://api.*" --abort',
|
|
4160
|
+
description: 'Abort API calls',
|
|
4161
|
+
},
|
|
4162
|
+
{
|
|
4163
|
+
command: 'monomind browse report https://example.com',
|
|
4164
|
+
description: 'One-command page report with a pass/fail verdict',
|
|
4165
|
+
},
|
|
3187
4166
|
{ command: 'monomind browse close', description: 'Close browser session' },
|
|
3188
4167
|
],
|
|
3189
4168
|
action: async (_ctx: CommandContext): Promise<CommandResult> => {
|
|
@@ -3240,6 +4219,7 @@ const browseCommand: Command = {
|
|
|
3240
4219
|
output.printInfo(' batch Execute multiple commands');
|
|
3241
4220
|
output.printInfo(' addinitscript Add script to run before page navigation');
|
|
3242
4221
|
output.printInfo(' removeinitscript Remove a previously added init script');
|
|
4222
|
+
output.printInfo(' report Test a page and write an HTML report');
|
|
3243
4223
|
output.printInfo(' close Close the browser session');
|
|
3244
4224
|
return { success: true };
|
|
3245
4225
|
},
|