@agentrhq/webcmd 0.1.1 → 0.1.2
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/README.md +317 -24
- package/dist/src/adapter-shadow.d.ts +11 -0
- package/dist/src/adapter-shadow.js +72 -0
- package/dist/src/adapter-shadow.test.d.ts +1 -0
- package/dist/src/adapter-shadow.test.js +49 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.d.ts +1 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/brand.d.ts +9 -0
- package/dist/src/brand.js +9 -0
- package/dist/src/brand.test.d.ts +1 -0
- package/dist/src/brand.test.js +15 -0
- package/dist/src/browser/analyze.d.ts +118 -0
- package/dist/src/browser/analyze.js +405 -0
- package/dist/src/browser/analyze.test.d.ts +1 -0
- package/dist/src/browser/analyze.test.js +209 -0
- package/dist/src/browser/article-extract.d.ts +57 -0
- package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
- package/dist/src/browser/article-extract.e2e.test.js +105 -0
- package/dist/src/browser/article-extract.js +169 -0
- package/dist/src/browser/article-extract.test.d.ts +1 -0
- package/dist/src/browser/article-extract.test.js +94 -0
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +151 -0
- package/dist/src/browser/base-page.js +1060 -0
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +803 -0
- package/dist/src/browser/bridge-readiness.d.ts +14 -0
- package/dist/src/browser/bridge-readiness.js +24 -0
- package/dist/src/browser/bridge-readiness.test.d.ts +1 -0
- package/dist/src/browser/bridge-readiness.test.js +58 -0
- package/dist/src/browser/bridge.d.ts +26 -0
- package/dist/src/browser/bridge.js +58 -0
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +46 -0
- package/dist/src/browser/cdp.js +536 -0
- package/dist/src/browser/cdp.test.d.ts +1 -0
- package/dist/src/browser/cdp.test.js +79 -0
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.d.ts +1 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/config.d.ts +2 -0
- package/dist/src/browser/config.js +14 -0
- package/dist/src/browser/daemon-client.d.ts +32 -0
- package/dist/src/browser/daemon-client.js +135 -0
- package/dist/src/browser/daemon-client.test.d.ts +1 -0
- package/dist/src/browser/daemon-client.test.js +326 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +37 -0
- package/dist/src/browser/daemon-lifecycle.js +147 -0
- package/dist/src/browser/daemon-transport.d.ts +54 -0
- package/dist/src/browser/daemon-transport.js +68 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/dom-helpers.d.ts +41 -0
- package/dist/src/browser/dom-helpers.js +239 -0
- package/dist/src/browser/dom-helpers.test.d.ts +1 -0
- package/dist/src/browser/dom-helpers.test.js +106 -0
- package/dist/src/browser/dom-snapshot.d.ts +93 -0
- package/dist/src/browser/dom-snapshot.js +894 -0
- package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
- package/dist/src/browser/dom-snapshot.test.js +313 -0
- package/dist/src/browser/errors.d.ts +38 -0
- package/dist/src/browser/errors.js +79 -0
- package/dist/src/browser/errors.test.d.ts +1 -0
- package/dist/src/browser/errors.test.js +60 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.d.ts +1 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +84 -0
- package/dist/src/browser/find.js +398 -0
- package/dist/src/browser/find.test.d.ts +1 -0
- package/dist/src/browser/find.test.js +180 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/index.d.ts +14 -0
- package/dist/src/browser/index.js +12 -0
- package/dist/src/browser/network-cache.d.ts +49 -0
- package/dist/src/browser/network-cache.js +78 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +75 -0
- package/dist/src/browser/network-interceptor.d.ts +11 -0
- package/dist/src/browser/network-interceptor.js +11 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/page.d.ts +83 -0
- package/dist/src/browser/page.js +453 -0
- package/dist/src/browser/page.test.d.ts +1 -0
- package/dist/src/browser/page.test.js +406 -0
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +86 -0
- package/dist/src/browser/protocol.d.ts +63 -0
- package/dist/src/browser/protocol.js +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +240 -0
- package/dist/src/browser/runtime/local-cloak/downloads.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/downloads.js +28 -0
- package/dist/src/browser/runtime/local-cloak/network.d.ts +28 -0
- package/dist/src/browser/runtime/local-cloak/network.js +120 -0
- package/dist/src/browser/runtime/local-cloak/network.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/network.test.js +124 -0
- package/dist/src/browser/runtime/local-cloak/profiles.d.ts +6 -0
- package/dist/src/browser/runtime/local-cloak/profiles.js +17 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.js +19 -0
- package/dist/src/browser/runtime/local-cloak/provider.d.ts +15 -0
- package/dist/src/browser/runtime/local-cloak/provider.js +27 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.js +244 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +77 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +280 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +92 -0
- package/dist/src/browser/runtime/provider.d.ts +9 -0
- package/dist/src/browser/runtime/provider.js +1 -0
- package/dist/src/browser/runtime/provider.test.d.ts +2 -0
- package/dist/src/browser/runtime/provider.test.js +49 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/stealth.d.ts +11 -0
- package/dist/src/browser/stealth.js +359 -0
- package/dist/src/browser/stealth.test.d.ts +1 -0
- package/dist/src/browser/stealth.test.js +134 -0
- package/dist/src/browser/tabs.d.ts +13 -0
- package/dist/src/browser/tabs.js +70 -0
- package/dist/src/browser/target-errors.d.ts +39 -0
- package/dist/src/browser/target-errors.js +45 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +94 -0
- package/dist/src/browser/target-resolver.d.ts +158 -0
- package/dist/src/browser/target-resolver.js +570 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +118 -0
- package/dist/src/browser/utils.d.ts +20 -0
- package/dist/src/browser/utils.js +64 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/browser/verify-fixture.d.ts +65 -0
- package/dist/src/browser/verify-fixture.js +318 -0
- package/dist/src/browser/verify-fixture.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.test.js +219 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/browser.test.d.ts +1 -0
- package/dist/src/browser.test.js +297 -0
- package/dist/src/build-manifest.d.ts +103 -0
- package/dist/src/build-manifest.js +363 -0
- package/dist/src/build-manifest.test.d.ts +1 -0
- package/dist/src/build-manifest.test.js +341 -0
- package/dist/src/capabilityRouting.d.ts +19 -0
- package/dist/src/capabilityRouting.js +56 -0
- package/dist/src/capabilityRouting.test.d.ts +1 -0
- package/dist/src/capabilityRouting.test.js +63 -0
- package/dist/src/cli-argv-preprocess.d.ts +63 -0
- package/dist/src/cli-argv-preprocess.js +303 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +304 -0
- package/dist/src/cli.d.ts +79 -0
- package/dist/src/cli.js +3350 -0
- package/dist/src/cli.test.d.ts +1 -0
- package/dist/src/cli.test.js +3041 -0
- package/dist/src/commanderAdapter.d.ts +21 -0
- package/dist/src/commanderAdapter.js +208 -0
- package/dist/src/commanderAdapter.test.d.ts +1 -0
- package/dist/src/commanderAdapter.test.js +352 -0
- package/dist/src/commands/auth.d.ts +40 -0
- package/dist/src/commands/auth.js +434 -0
- package/dist/src/commands/auth.test.d.ts +1 -0
- package/dist/src/commands/auth.test.js +252 -0
- package/dist/src/commands/daemon.d.ts +9 -0
- package/dist/src/commands/daemon.js +91 -0
- package/dist/src/commands/daemon.test.d.ts +1 -0
- package/dist/src/commands/daemon.test.js +293 -0
- package/dist/src/completion-fast.d.ts +22 -0
- package/dist/src/completion-fast.js +88 -0
- package/dist/src/completion-shared.d.ts +13 -0
- package/dist/src/completion-shared.js +61 -0
- package/dist/src/completion.d.ts +21 -0
- package/dist/src/completion.js +68 -0
- package/dist/src/completion.test.d.ts +1 -0
- package/dist/src/completion.test.js +24 -0
- package/dist/src/constants.d.ts +18 -0
- package/dist/src/constants.js +41 -0
- package/dist/src/convention-audit.d.ts +50 -0
- package/dist/src/convention-audit.js +553 -0
- package/dist/src/convention-audit.test.d.ts +1 -0
- package/dist/src/convention-audit.test.js +255 -0
- package/dist/src/daemon/server.d.ts +13 -0
- package/dist/src/daemon/server.js +185 -0
- package/dist/src/daemon/server.test.d.ts +1 -0
- package/dist/src/daemon/server.test.js +132 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -0
- package/dist/src/daemon.js +27 -0
- package/dist/src/daemon.test.d.ts +1 -0
- package/dist/src/daemon.test.js +60 -0
- package/dist/src/discovery.d.ts +46 -0
- package/dist/src/discovery.js +341 -0
- package/dist/src/doctor.d.ts +39 -0
- package/dist/src/doctor.js +168 -0
- package/dist/src/doctor.test.d.ts +1 -0
- package/dist/src/doctor.test.js +302 -0
- package/dist/src/download/article-download.d.ts +72 -0
- package/dist/src/download/article-download.js +305 -0
- package/dist/src/download/article-download.test.d.ts +1 -0
- package/dist/src/download/article-download.test.js +235 -0
- package/dist/src/download/index.d.ts +71 -0
- package/dist/src/download/index.js +363 -0
- package/dist/src/download/index.test.d.ts +1 -0
- package/dist/src/download/index.test.js +174 -0
- package/dist/src/download/media-download.d.ts +49 -0
- package/dist/src/download/media-download.js +127 -0
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.d.ts +36 -0
- package/dist/src/download/progress.js +120 -0
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +36 -0
- package/dist/src/electron-apps.d.ts +31 -0
- package/dist/src/electron-apps.js +91 -0
- package/dist/src/electron-apps.test.d.ts +1 -0
- package/dist/src/electron-apps.test.js +76 -0
- package/dist/src/engine.test.d.ts +1 -0
- package/dist/src/engine.test.js +312 -0
- package/dist/src/errors.d.ts +114 -0
- package/dist/src/errors.js +174 -0
- package/dist/src/errors.test.d.ts +1 -0
- package/dist/src/errors.test.js +109 -0
- package/dist/src/execution.d.ts +24 -0
- package/dist/src/execution.js +509 -0
- package/dist/src/execution.test.d.ts +1 -0
- package/dist/src/execution.test.js +645 -0
- package/dist/src/external.d.ts +49 -0
- package/dist/src/external.js +211 -0
- package/dist/src/external.test.d.ts +1 -0
- package/dist/src/external.test.js +110 -0
- package/dist/src/help.d.ts +88 -0
- package/dist/src/help.js +556 -0
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +58 -0
- package/dist/src/hooks.d.ts +46 -0
- package/dist/src/hooks.js +58 -0
- package/dist/src/hooks.test.d.ts +4 -0
- package/dist/src/hooks.test.js +92 -0
- package/dist/src/interceptor.d.ts +44 -0
- package/dist/src/interceptor.js +183 -0
- package/dist/src/interceptor.test.d.ts +4 -0
- package/dist/src/interceptor.test.js +81 -0
- package/dist/src/launcher.d.ts +41 -0
- package/dist/src/launcher.js +226 -0
- package/dist/src/launcher.test.d.ts +1 -0
- package/dist/src/launcher.test.js +153 -0
- package/dist/src/logger.d.ts +26 -0
- package/dist/src/logger.js +49 -0
- package/dist/src/main.d.ts +5 -0
- package/dist/src/main.js +177 -0
- package/dist/src/manifest-types.d.ts +43 -0
- package/dist/src/manifest-types.js +9 -0
- package/dist/src/node-network.d.ts +10 -0
- package/dist/src/node-network.js +174 -0
- package/dist/src/node-network.test.d.ts +1 -0
- package/dist/src/node-network.test.js +55 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/output.d.ts +14 -0
- package/dist/src/output.js +152 -0
- package/dist/src/output.test.d.ts +1 -0
- package/dist/src/output.test.js +53 -0
- package/dist/src/package-exports.test.d.ts +1 -0
- package/dist/src/package-exports.test.js +112 -0
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/pipeline/executor.d.ts +11 -0
- package/dist/src/pipeline/executor.js +93 -0
- package/dist/src/pipeline/executor.test.d.ts +4 -0
- package/dist/src/pipeline/executor.test.js +182 -0
- package/dist/src/pipeline/index.d.ts +5 -0
- package/dist/src/pipeline/index.js +5 -0
- package/dist/src/pipeline/registry.d.ts +28 -0
- package/dist/src/pipeline/registry.js +55 -0
- package/dist/src/pipeline/steps/browser.d.ts +13 -0
- package/dist/src/pipeline/steps/browser.js +86 -0
- package/dist/src/pipeline/steps/download.d.ts +18 -0
- package/dist/src/pipeline/steps/download.js +252 -0
- package/dist/src/pipeline/steps/download.test.d.ts +1 -0
- package/dist/src/pipeline/steps/download.test.js +102 -0
- package/dist/src/pipeline/steps/fetch.d.ts +5 -0
- package/dist/src/pipeline/steps/fetch.js +122 -0
- package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/src/pipeline/steps/fetch.test.js +117 -0
- package/dist/src/pipeline/steps/intercept.d.ts +5 -0
- package/dist/src/pipeline/steps/intercept.js +50 -0
- package/dist/src/pipeline/steps/tap.d.ts +12 -0
- package/dist/src/pipeline/steps/tap.js +93 -0
- package/dist/src/pipeline/steps/transform.d.ts +9 -0
- package/dist/src/pipeline/steps/transform.js +70 -0
- package/dist/src/pipeline/template.d.ts +17 -0
- package/dist/src/pipeline/template.js +344 -0
- package/dist/src/pipeline/template.test.d.ts +4 -0
- package/dist/src/pipeline/template.test.js +197 -0
- package/dist/src/pipeline/transform.test.d.ts +4 -0
- package/dist/src/pipeline/transform.test.js +133 -0
- package/dist/src/plugin-manifest.d.ts +70 -0
- package/dist/src/plugin-manifest.js +160 -0
- package/dist/src/plugin-manifest.test.d.ts +4 -0
- package/dist/src/plugin-manifest.test.js +179 -0
- package/dist/src/plugin-scaffold.d.ts +28 -0
- package/dist/src/plugin-scaffold.js +143 -0
- package/dist/src/plugin-scaffold.test.d.ts +4 -0
- package/dist/src/plugin-scaffold.test.js +83 -0
- package/dist/src/plugin.d.ts +146 -0
- package/dist/src/plugin.js +1303 -0
- package/dist/src/plugin.test.d.ts +4 -0
- package/dist/src/plugin.test.js +1363 -0
- package/dist/src/registry-api.d.ts +13 -0
- package/dist/src/registry-api.js +10 -0
- package/dist/src/registry.d.ts +123 -0
- package/dist/src/registry.js +125 -0
- package/dist/src/registry.test.d.ts +4 -0
- package/dist/src/registry.test.js +209 -0
- package/dist/src/runtime-copy.test.d.ts +1 -0
- package/dist/src/runtime-copy.test.js +29 -0
- package/dist/src/runtime-detect.d.ts +31 -0
- package/dist/src/runtime-detect.js +47 -0
- package/dist/src/runtime-detect.test.d.ts +1 -0
- package/dist/src/runtime-detect.test.js +39 -0
- package/dist/src/runtime-identity.test.d.ts +1 -0
- package/dist/src/runtime-identity.test.js +18 -0
- package/dist/src/runtime.d.ts +47 -0
- package/dist/src/runtime.js +54 -0
- package/dist/src/scripts/framework.d.ts +7 -0
- package/dist/src/scripts/framework.js +25 -0
- package/dist/src/scripts/interact.d.ts +4 -0
- package/dist/src/scripts/interact.js +20 -0
- package/dist/src/scripts/store.d.ts +13 -0
- package/dist/src/scripts/store.js +48 -0
- package/dist/src/serialization.d.ts +42 -0
- package/dist/src/serialization.js +103 -0
- package/dist/src/serialization.test.d.ts +1 -0
- package/dist/src/serialization.test.js +78 -0
- package/dist/src/skills.d.ts +14 -0
- package/dist/src/skills.js +127 -0
- package/dist/src/skills.test.d.ts +1 -0
- package/dist/src/skills.test.js +71 -0
- package/dist/src/snapshotFormatter.d.ts +11 -0
- package/dist/src/snapshotFormatter.js +338 -0
- package/dist/src/snapshotFormatter.test.d.ts +7 -0
- package/dist/src/snapshotFormatter.test.js +521 -0
- package/dist/src/tui.d.ts +28 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/tui.test.d.ts +1 -0
- package/dist/src/tui.test.js +19 -0
- package/dist/src/types.d.ts +236 -0
- package/dist/src/types.js +7 -0
- package/dist/src/update-check.d.ts +66 -0
- package/dist/src/update-check.js +206 -0
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +117 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +152 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/dist/src/validate.d.ts +21 -0
- package/dist/src/validate.js +130 -0
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/dist/src/verify.d.ts +26 -0
- package/dist/src/verify.js +67 -0
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.js +22 -0
- package/dist/src/weixin-download.test.d.ts +1 -0
- package/dist/src/weixin-download.test.js +45 -0
- package/package.json +4 -3
- package/scripts/check-package-bin.mjs +95 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: download — file download with concurrency and progress.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Direct HTTP downloads (images, documents)
|
|
6
|
+
* - yt-dlp integration for video platforms
|
|
7
|
+
* - Browser cookie forwarding for authenticated downloads
|
|
8
|
+
* - Filename templating and deduplication
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { render } from '../template.js';
|
|
13
|
+
import { getErrorMessage } from '../../errors.js';
|
|
14
|
+
import { httpDownload, ytdlpDownload, saveDocument, detectContentType, requiresYtdlp, sanitizeFilename, generateFilename, exportCookiesToNetscape, getTempDir, formatCookieHeader, } from '../../download/index.js';
|
|
15
|
+
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
16
|
+
import { mapConcurrent } from '../../utils.js';
|
|
17
|
+
/**
|
|
18
|
+
* Extract cookies from browser page.
|
|
19
|
+
*/
|
|
20
|
+
async function extractBrowserCookies(page, domain) {
|
|
21
|
+
try {
|
|
22
|
+
const cookies = await page.getCookies({ domain });
|
|
23
|
+
return formatCookieHeader(cookies);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract cookies as array for yt-dlp Netscape format.
|
|
31
|
+
*/
|
|
32
|
+
async function extractCookiesArray(page, domain) {
|
|
33
|
+
try {
|
|
34
|
+
const cookies = await page.getCookies({ domain });
|
|
35
|
+
return cookies
|
|
36
|
+
.filter((cookie) => cookie.name)
|
|
37
|
+
.map((cookie) => ({
|
|
38
|
+
name: cookie.name,
|
|
39
|
+
value: cookie.value,
|
|
40
|
+
domain: cookie.domain,
|
|
41
|
+
path: cookie.path ?? '/',
|
|
42
|
+
secure: cookie.secure ?? false,
|
|
43
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function dedupeCookies(cookies) {
|
|
51
|
+
const deduped = new Map();
|
|
52
|
+
for (const cookie of cookies) {
|
|
53
|
+
deduped.set(`${cookie.domain}\t${cookie.path}\t${cookie.name}`, cookie);
|
|
54
|
+
}
|
|
55
|
+
return [...deduped.values()];
|
|
56
|
+
}
|
|
57
|
+
export async function stepDownload(page, params, data, args) {
|
|
58
|
+
// Parse parameters with defaults
|
|
59
|
+
const p = typeof params === 'object' && params !== null ? params : {};
|
|
60
|
+
const urlTemplate = typeof params === 'string' ? params : (p.url ?? '');
|
|
61
|
+
const dirTemplate = p.dir ?? './downloads';
|
|
62
|
+
const filenameTemplate = p.filename ?? '';
|
|
63
|
+
const concurrency = typeof p.concurrency === 'number' ? p.concurrency : 3;
|
|
64
|
+
const skipExisting = p.skip_existing !== false;
|
|
65
|
+
const timeout = typeof p.timeout === 'number' ? p.timeout * 1000 : 30000;
|
|
66
|
+
const useYtdlp = p.use_ytdlp ?? false;
|
|
67
|
+
const ytdlpArgs = Array.isArray(p.ytdlp_args)
|
|
68
|
+
? p.ytdlp_args.map((v) => String(v))
|
|
69
|
+
: [];
|
|
70
|
+
const contentType = p.type ?? 'auto';
|
|
71
|
+
const showProgress = p.progress !== false;
|
|
72
|
+
const contentTemplate = p.content;
|
|
73
|
+
const metadataTemplate = p.metadata;
|
|
74
|
+
// Resolve output directory
|
|
75
|
+
const dir = String(render(dirTemplate, { args, data }));
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
// Normalize data to array. Items are row records (string-keyed) produced by
|
|
78
|
+
// upstream steps; we treat them as Record<string, unknown> and narrow per-use.
|
|
79
|
+
const items = Array.isArray(data) ? data
|
|
80
|
+
: data ? [data]
|
|
81
|
+
: [];
|
|
82
|
+
if (items.length === 0) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
// Create progress tracker
|
|
86
|
+
const tracker = new DownloadProgressTracker(items.length, showProgress);
|
|
87
|
+
// Cache cookie lookups per domain so mixed-domain batches stay isolated without repeated browser calls.
|
|
88
|
+
const cookieHeaderCache = new Map();
|
|
89
|
+
let cookiesFile;
|
|
90
|
+
if (page) {
|
|
91
|
+
// For yt-dlp, we need to export cookies to Netscape format
|
|
92
|
+
if (useYtdlp || items.some((item, index) => {
|
|
93
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
94
|
+
return requiresYtdlp(url);
|
|
95
|
+
})) {
|
|
96
|
+
try {
|
|
97
|
+
const ytdlpDomains = [...new Set(items.flatMap((item, index) => {
|
|
98
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
99
|
+
if (!useYtdlp && !requiresYtdlp(url))
|
|
100
|
+
return [];
|
|
101
|
+
try {
|
|
102
|
+
return [new URL(url).hostname];
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}))];
|
|
108
|
+
const cookiesArray = dedupeCookies((await Promise.all(ytdlpDomains.map((domain) => extractCookiesArray(page, domain)))).flat());
|
|
109
|
+
if (cookiesArray.length > 0) {
|
|
110
|
+
const tempDir = getTempDir();
|
|
111
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
112
|
+
cookiesFile = path.join(tempDir, `cookies_${Date.now()}.txt`);
|
|
113
|
+
exportCookiesToNetscape(cookiesArray, cookiesFile);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore cookie extraction errors
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const results = await mapConcurrent(items, concurrency, async (item, index) => {
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
// Render URL
|
|
124
|
+
const url = String(render(urlTemplate, { args, data, item, index }));
|
|
125
|
+
if (!url) {
|
|
126
|
+
tracker.onFileComplete(false);
|
|
127
|
+
return {
|
|
128
|
+
...item,
|
|
129
|
+
_download: { status: 'failed', error: 'Empty URL' },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Render filename
|
|
133
|
+
let filename;
|
|
134
|
+
if (filenameTemplate) {
|
|
135
|
+
filename = String(render(filenameTemplate, { args, data, item, index }));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
filename = generateFilename(url, index);
|
|
139
|
+
}
|
|
140
|
+
filename = sanitizeFilename(filename);
|
|
141
|
+
const destPath = path.join(dir, filename);
|
|
142
|
+
// Check if file exists and skip_existing is true
|
|
143
|
+
if (skipExisting && fs.existsSync(destPath)) {
|
|
144
|
+
tracker.onFileComplete(true, true);
|
|
145
|
+
return {
|
|
146
|
+
...item,
|
|
147
|
+
_download: {
|
|
148
|
+
status: 'skipped',
|
|
149
|
+
path: destPath,
|
|
150
|
+
size: fs.statSync(destPath).size,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Create progress bar for this file
|
|
155
|
+
const progressBar = tracker.onFileStart(filename, index);
|
|
156
|
+
// Determine download method
|
|
157
|
+
const detectedType = contentType === 'auto' ? detectContentType(url) : contentType;
|
|
158
|
+
const shouldUseYtdlp = useYtdlp || (detectedType === 'video' && requiresYtdlp(url));
|
|
159
|
+
let result;
|
|
160
|
+
try {
|
|
161
|
+
if (detectedType === 'document' && contentTemplate) {
|
|
162
|
+
// Save extracted content as document
|
|
163
|
+
const content = String(render(contentTemplate, { args, data, item, index }));
|
|
164
|
+
const metadata = metadataTemplate
|
|
165
|
+
? Object.fromEntries(Object.entries(metadataTemplate).map(([k, v]) => [k, render(v, { args, data, item, index })]))
|
|
166
|
+
: undefined;
|
|
167
|
+
const ext = path.extname(filename).toLowerCase();
|
|
168
|
+
const format = ext === '.json' ? 'json' : ext === '.html' ? 'html' : 'markdown';
|
|
169
|
+
result = await saveDocument(content, destPath, format, metadata);
|
|
170
|
+
if (progressBar) {
|
|
171
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (shouldUseYtdlp) {
|
|
175
|
+
// Use yt-dlp for video downloads
|
|
176
|
+
result = await ytdlpDownload(url, destPath, {
|
|
177
|
+
cookiesFile,
|
|
178
|
+
extraArgs: ytdlpArgs,
|
|
179
|
+
onProgress: (percent) => {
|
|
180
|
+
if (progressBar) {
|
|
181
|
+
progressBar.update(percent, 100);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
if (progressBar) {
|
|
186
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Direct HTTP download
|
|
191
|
+
let cookies = '';
|
|
192
|
+
if (page) {
|
|
193
|
+
try {
|
|
194
|
+
const targetDomain = new URL(url).hostname;
|
|
195
|
+
let cookiePromise = cookieHeaderCache.get(targetDomain);
|
|
196
|
+
if (!cookiePromise) {
|
|
197
|
+
cookiePromise = extractBrowserCookies(page, targetDomain);
|
|
198
|
+
cookieHeaderCache.set(targetDomain, cookiePromise);
|
|
199
|
+
}
|
|
200
|
+
cookies = await cookiePromise;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
cookies = '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
result = await httpDownload(url, destPath, {
|
|
207
|
+
cookies,
|
|
208
|
+
timeout,
|
|
209
|
+
onProgress: (received, total) => {
|
|
210
|
+
if (progressBar) {
|
|
211
|
+
progressBar.update(received, total);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
if (progressBar) {
|
|
216
|
+
progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
const msg = getErrorMessage(err);
|
|
222
|
+
result = { success: false, size: 0, error: msg };
|
|
223
|
+
if (progressBar) {
|
|
224
|
+
progressBar.fail(msg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
tracker.onFileComplete(result.success);
|
|
228
|
+
const duration = Date.now() - startTime;
|
|
229
|
+
return {
|
|
230
|
+
...item,
|
|
231
|
+
_download: {
|
|
232
|
+
status: result.success ? 'success' : 'failed',
|
|
233
|
+
path: result.success ? destPath : undefined,
|
|
234
|
+
size: result.size,
|
|
235
|
+
error: result.error,
|
|
236
|
+
duration,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
// Cleanup temp cookie file
|
|
241
|
+
if (cookiesFile && fs.existsSync(cookiesFile)) {
|
|
242
|
+
try {
|
|
243
|
+
fs.unlinkSync(cookiesFile);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Ignore cleanup errors
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Show summary
|
|
250
|
+
tracker.finish();
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const { mockHttpDownload, mockYtdlpDownload, mockExportCookiesToNetscape } = vi.hoisted(() => ({
|
|
5
|
+
mockHttpDownload: vi.fn(),
|
|
6
|
+
mockYtdlpDownload: vi.fn(),
|
|
7
|
+
mockExportCookiesToNetscape: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../download/index.js', async () => {
|
|
10
|
+
const actual = await vi.importActual('../../download/index.js');
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
httpDownload: mockHttpDownload,
|
|
14
|
+
ytdlpDownload: mockYtdlpDownload,
|
|
15
|
+
exportCookiesToNetscape: mockExportCookiesToNetscape,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
import { stepDownload } from './download.js';
|
|
19
|
+
function createMockPage(getCookies) {
|
|
20
|
+
return {
|
|
21
|
+
goto: vi.fn(),
|
|
22
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
23
|
+
fetchJson: vi.fn().mockResolvedValue(null),
|
|
24
|
+
getCookies,
|
|
25
|
+
snapshot: vi.fn().mockResolvedValue(''),
|
|
26
|
+
click: vi.fn(),
|
|
27
|
+
typeText: vi.fn(),
|
|
28
|
+
fillText: vi.fn(),
|
|
29
|
+
pressKey: vi.fn(),
|
|
30
|
+
scrollTo: vi.fn(),
|
|
31
|
+
getFormState: vi.fn().mockResolvedValue({}),
|
|
32
|
+
wait: vi.fn(),
|
|
33
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
34
|
+
selectTab: vi.fn(),
|
|
35
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
36
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
37
|
+
scroll: vi.fn(),
|
|
38
|
+
autoScroll: vi.fn(),
|
|
39
|
+
installInterceptor: vi.fn(),
|
|
40
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
41
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
42
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
describe('stepDownload', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mockHttpDownload.mockReset();
|
|
48
|
+
mockHttpDownload.mockResolvedValue({ success: true, size: 2 });
|
|
49
|
+
mockYtdlpDownload.mockReset();
|
|
50
|
+
mockYtdlpDownload.mockResolvedValue({ success: true, size: 2 });
|
|
51
|
+
mockExportCookiesToNetscape.mockReset();
|
|
52
|
+
});
|
|
53
|
+
it('scopes browser cookies to each direct-download target domain', async () => {
|
|
54
|
+
const page = createMockPage(vi.fn().mockImplementation(async (opts) => {
|
|
55
|
+
const domain = opts?.domain ?? 'unknown';
|
|
56
|
+
return [{ name: 'sid', value: domain, domain }];
|
|
57
|
+
}));
|
|
58
|
+
await stepDownload(page, {
|
|
59
|
+
url: '${{ item.url }}',
|
|
60
|
+
dir: path.join(os.tmpdir(), 'webcmd-download-test'),
|
|
61
|
+
filename: '${{ index }}.txt',
|
|
62
|
+
progress: false,
|
|
63
|
+
concurrency: 1,
|
|
64
|
+
}, [
|
|
65
|
+
{ url: 'https://a.example/file-1.txt' },
|
|
66
|
+
{ url: 'https://b.example/file-2.txt' },
|
|
67
|
+
], {});
|
|
68
|
+
expect(mockHttpDownload).toHaveBeenNthCalledWith(1, 'https://a.example/file-1.txt', path.join(os.tmpdir(), 'webcmd-download-test', '0.txt'), expect.objectContaining({ cookies: 'sid=a.example' }));
|
|
69
|
+
expect(mockHttpDownload).toHaveBeenNthCalledWith(2, 'https://b.example/file-2.txt', path.join(os.tmpdir(), 'webcmd-download-test', '1.txt'), expect.objectContaining({ cookies: 'sid=b.example' }));
|
|
70
|
+
});
|
|
71
|
+
it('builds yt-dlp cookies from all target domains instead of only the first item', async () => {
|
|
72
|
+
const getCookies = vi.fn().mockImplementation(async (opts) => {
|
|
73
|
+
const domain = opts?.domain ?? 'unknown';
|
|
74
|
+
return [{
|
|
75
|
+
name: `sid-${domain}`,
|
|
76
|
+
value: domain,
|
|
77
|
+
domain,
|
|
78
|
+
path: '/',
|
|
79
|
+
secure: false,
|
|
80
|
+
httpOnly: false,
|
|
81
|
+
}];
|
|
82
|
+
});
|
|
83
|
+
const page = createMockPage(getCookies);
|
|
84
|
+
await stepDownload(page, {
|
|
85
|
+
url: '${{ item.url }}',
|
|
86
|
+
dir: '/tmp/webcmd-download-test',
|
|
87
|
+
filename: '${{ index }}.mp4',
|
|
88
|
+
progress: false,
|
|
89
|
+
concurrency: 1,
|
|
90
|
+
}, [
|
|
91
|
+
{ url: 'https://www.youtube.com/watch?v=one' },
|
|
92
|
+
{ url: 'https://www.bilibili.com/video/BV1xx411c7mD' },
|
|
93
|
+
], {});
|
|
94
|
+
expect(getCookies).toHaveBeenCalledWith({ domain: 'www.youtube.com' });
|
|
95
|
+
expect(getCookies).toHaveBeenCalledWith({ domain: 'www.bilibili.com' });
|
|
96
|
+
expect(mockExportCookiesToNetscape).toHaveBeenCalledWith(expect.arrayContaining([
|
|
97
|
+
expect.objectContaining({ name: 'sid-www.youtube.com', domain: 'www.youtube.com' }),
|
|
98
|
+
expect.objectContaining({ name: 'sid-www.bilibili.com', domain: 'www.bilibili.com' }),
|
|
99
|
+
]), expect.any(String));
|
|
100
|
+
expect(mockYtdlpDownload).toHaveBeenCalledTimes(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: fetch — HTTP API requests.
|
|
3
|
+
*/
|
|
4
|
+
import { CliError, getErrorMessage } from '../../errors.js';
|
|
5
|
+
import { log } from '../../logger.js';
|
|
6
|
+
import { render } from '../template.js';
|
|
7
|
+
import { isRecord, mapConcurrent } from '../../utils.js';
|
|
8
|
+
/** Single URL fetch helper */
|
|
9
|
+
async function fetchSingle(page, url, method, queryParams, headers, args, data) {
|
|
10
|
+
const renderedParams = {};
|
|
11
|
+
for (const [k, v] of Object.entries(queryParams))
|
|
12
|
+
renderedParams[k] = String(render(v, { args, data }));
|
|
13
|
+
const renderedHeaders = {};
|
|
14
|
+
for (const [k, v] of Object.entries(headers))
|
|
15
|
+
renderedHeaders[k] = String(render(v, { args, data }));
|
|
16
|
+
let finalUrl = url;
|
|
17
|
+
if (Object.keys(renderedParams).length > 0) {
|
|
18
|
+
const qs = new URLSearchParams(renderedParams).toString();
|
|
19
|
+
finalUrl = `${finalUrl}${finalUrl.includes('?') ? '&' : '?'}${qs}`;
|
|
20
|
+
}
|
|
21
|
+
if (page === null) {
|
|
22
|
+
const resp = await fetch(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });
|
|
23
|
+
if (!resp.ok) {
|
|
24
|
+
throw new CliError('FETCH_ERROR', `HTTP ${resp.status} ${resp.statusText} from ${finalUrl}`);
|
|
25
|
+
}
|
|
26
|
+
return resp.json();
|
|
27
|
+
}
|
|
28
|
+
return page.fetchJson(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Batch fetch: send all URLs into the browser as a single evaluate() call.
|
|
32
|
+
* This eliminates N-1 cross-process IPC round trips, performing all fetches
|
|
33
|
+
* inside the V8 engine and returning results as one JSON array.
|
|
34
|
+
*/
|
|
35
|
+
async function fetchBatchInBrowser(page, urls, method, headers, concurrency) {
|
|
36
|
+
const headersJs = JSON.stringify(headers);
|
|
37
|
+
const urlsJs = JSON.stringify(urls);
|
|
38
|
+
const methodJs = JSON.stringify(method);
|
|
39
|
+
return (await page.evaluate(`
|
|
40
|
+
async () => {
|
|
41
|
+
const urls = ${urlsJs};
|
|
42
|
+
const method = ${methodJs};
|
|
43
|
+
const headers = ${headersJs};
|
|
44
|
+
const concurrency = ${concurrency};
|
|
45
|
+
|
|
46
|
+
const results = new Array(urls.length);
|
|
47
|
+
let idx = 0;
|
|
48
|
+
|
|
49
|
+
async function worker() {
|
|
50
|
+
while (idx < urls.length) {
|
|
51
|
+
const i = idx++;
|
|
52
|
+
try {
|
|
53
|
+
const resp = await fetch(urls[i], { method, headers, credentials: "include" });
|
|
54
|
+
if (!resp.ok) {
|
|
55
|
+
throw new Error('HTTP ' + resp.status + ' ' + resp.statusText + ' from ' + urls[i]);
|
|
56
|
+
}
|
|
57
|
+
results[i] = await resp.json();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
results[i] = { error: e instanceof Error ? e.message : String(e) };
|
|
60
|
+
// Note: getErrorMessage() is a Node.js utility — can't use it inside evaluate()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => worker());
|
|
66
|
+
await Promise.all(workers);
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
`));
|
|
70
|
+
}
|
|
71
|
+
export async function stepFetch(page, params, data, args) {
|
|
72
|
+
const paramObject = isRecord(params) ? params : {};
|
|
73
|
+
const urlOrObj = typeof params === 'string' ? params : (paramObject.url ?? '');
|
|
74
|
+
const method = typeof paramObject.method === 'string' ? paramObject.method : 'GET';
|
|
75
|
+
const queryParams = isRecord(paramObject.params) ? paramObject.params : {};
|
|
76
|
+
const headers = isRecord(paramObject.headers) ? paramObject.headers : {};
|
|
77
|
+
const urlTemplate = String(urlOrObj);
|
|
78
|
+
// Per-item fetch when data is array and URL references item
|
|
79
|
+
if (Array.isArray(data) && urlTemplate.includes('item')) {
|
|
80
|
+
const concurrency = typeof paramObject.concurrency === 'number' ? paramObject.concurrency : 5;
|
|
81
|
+
// Render all URLs upfront
|
|
82
|
+
const renderedHeaders = {};
|
|
83
|
+
for (const [k, v] of Object.entries(headers))
|
|
84
|
+
renderedHeaders[k] = String(render(v, { args, data }));
|
|
85
|
+
const renderedParams = {};
|
|
86
|
+
for (const [k, v] of Object.entries(queryParams))
|
|
87
|
+
renderedParams[k] = String(render(v, { args, data }));
|
|
88
|
+
const urls = data.map((item, index) => {
|
|
89
|
+
let url = String(render(urlTemplate, { args, data, item, index }));
|
|
90
|
+
if (Object.keys(renderedParams).length > 0) {
|
|
91
|
+
const qs = new URLSearchParams(renderedParams).toString();
|
|
92
|
+
url = `${url}${url.includes('?') ? '&' : '?'}${qs}`;
|
|
93
|
+
}
|
|
94
|
+
return url;
|
|
95
|
+
});
|
|
96
|
+
// BATCH IPC: if browser is available, batch all fetches into a single evaluate() call
|
|
97
|
+
if (page !== null) {
|
|
98
|
+
const results = await fetchBatchInBrowser(page, urls, method.toUpperCase(), renderedHeaders, concurrency);
|
|
99
|
+
for (let i = 0; i < results.length; i++) {
|
|
100
|
+
const r = results[i];
|
|
101
|
+
if (r && typeof r === 'object' && 'error' in r) {
|
|
102
|
+
log.warn(`Batch fetch failed for ${urls[i]}: ${r.error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
// Non-browser: use concurrent pool (already optimized)
|
|
108
|
+
return mapConcurrent(data, concurrency, async (item, index) => {
|
|
109
|
+
const itemUrl = String(render(urlTemplate, { args, data, item, index }));
|
|
110
|
+
try {
|
|
111
|
+
return await fetchSingle(null, itemUrl, method, queryParams, headers, args, data);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const message = getErrorMessage(error);
|
|
115
|
+
log.warn(`Batch fetch failed for ${itemUrl}: ${message}`);
|
|
116
|
+
return { error: message };
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const url = render(urlOrObj, { args, data });
|
|
121
|
+
return fetchSingle(page, String(url), method, queryParams, headers, args, data);
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
import { stepFetch } from './fetch.js';
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
vi.unstubAllGlobals();
|
|
7
|
+
});
|
|
8
|
+
describe('stepFetch', () => {
|
|
9
|
+
// W1 + W4: non-browser single fetch throws CliError with FETCH_ERROR code and full message
|
|
10
|
+
it('throws CliError with FETCH_ERROR code on non-ok responses without a browser session', async () => {
|
|
11
|
+
const jsonMock = vi.fn().mockResolvedValue({ error: 'rate limited' });
|
|
12
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
13
|
+
ok: false,
|
|
14
|
+
status: 429,
|
|
15
|
+
statusText: 'Too Many Requests',
|
|
16
|
+
json: jsonMock,
|
|
17
|
+
});
|
|
18
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
19
|
+
const err = await stepFetch(null, { url: 'https://api.example.com/items' }, null, {}).catch((e) => e);
|
|
20
|
+
expect(err).toBeInstanceOf(CliError);
|
|
21
|
+
expect(err.code).toBe('FETCH_ERROR');
|
|
22
|
+
expect(err.message).toBe('HTTP 429 Too Many Requests from https://api.example.com/items');
|
|
23
|
+
expect(jsonMock).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
// W1 + W3: browser single fetch delegates to page.fetchJson, which owns browser-context fetch errors
|
|
26
|
+
it('throws CliError with FETCH_ERROR code on non-ok responses inside the browser session', async () => {
|
|
27
|
+
const page = {
|
|
28
|
+
fetchJson: vi.fn().mockRejectedValue(new CliError('FETCH_ERROR', 'HTTP 401 Unauthorized from https://api.example.com/items')),
|
|
29
|
+
};
|
|
30
|
+
const err = await stepFetch(page, { url: 'https://api.example.com/items' }, null, {}).catch((e) => e);
|
|
31
|
+
expect(err).toBeInstanceOf(CliError);
|
|
32
|
+
expect(err.code).toBe('FETCH_ERROR');
|
|
33
|
+
expect(err.message).toBe('HTTP 401 Unauthorized from https://api.example.com/items');
|
|
34
|
+
expect(page.fetchJson).toHaveBeenCalledWith('https://api.example.com/items', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('returns per-item HTTP errors for batch fetches without a browser session', async () => {
|
|
40
|
+
const jsonMock = vi.fn().mockResolvedValue({ error: 'upstream unavailable' });
|
|
41
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 503,
|
|
44
|
+
statusText: 'Service Unavailable',
|
|
45
|
+
json: jsonMock,
|
|
46
|
+
});
|
|
47
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
48
|
+
await expect(stepFetch(null, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }], {})).resolves.toEqual([
|
|
49
|
+
{ error: 'HTTP 503 Service Unavailable from https://api.example.com/items/1' },
|
|
50
|
+
]);
|
|
51
|
+
expect(jsonMock).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it('returns per-item HTTP errors for batch browser fetches', async () => {
|
|
54
|
+
const jsonMock = vi.fn().mockResolvedValue({ error: 'upstream unavailable' });
|
|
55
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 503,
|
|
58
|
+
statusText: 'Service Unavailable',
|
|
59
|
+
json: jsonMock,
|
|
60
|
+
});
|
|
61
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
62
|
+
const page = {
|
|
63
|
+
evaluate: vi.fn(async (js) => Function(`return (${js})`)()()),
|
|
64
|
+
};
|
|
65
|
+
await expect(stepFetch(page, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }], {})).resolves.toEqual([
|
|
66
|
+
{ error: 'HTTP 503 Service Unavailable from https://api.example.com/items/1' },
|
|
67
|
+
]);
|
|
68
|
+
expect(jsonMock).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('stringifies non-Error batch browser failures consistently', async () => {
|
|
71
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue('socket hang up'));
|
|
72
|
+
const page = {
|
|
73
|
+
evaluate: vi.fn(async (js) => Function(`return (${js})`)()()),
|
|
74
|
+
};
|
|
75
|
+
await expect(stepFetch(page, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }], {})).resolves.toEqual([
|
|
76
|
+
{ error: 'socket hang up' },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
it('stringifies non-Error batch non-browser failures consistently', async () => {
|
|
80
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue('socket hang up'));
|
|
81
|
+
await expect(stepFetch(null, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }], {})).resolves.toEqual([
|
|
82
|
+
{ error: 'socket hang up' },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
// W2: batch item failures emit a warning log
|
|
86
|
+
it('logs a warning for each failed batch item in non-browser mode', async () => {
|
|
87
|
+
const { log } = await import('../../logger.js');
|
|
88
|
+
const warnSpy = vi.spyOn(log, 'warn');
|
|
89
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
90
|
+
ok: false,
|
|
91
|
+
status: 503,
|
|
92
|
+
statusText: 'Service Unavailable',
|
|
93
|
+
json: vi.fn(),
|
|
94
|
+
}));
|
|
95
|
+
await stepFetch(null, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }, { id: 2 }], {});
|
|
96
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
97
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('https://api.example.com/items/1'));
|
|
98
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('https://api.example.com/items/2'));
|
|
99
|
+
});
|
|
100
|
+
it('logs a warning for each failed batch item in browser mode', async () => {
|
|
101
|
+
const { log } = await import('../../logger.js');
|
|
102
|
+
const warnSpy = vi.spyOn(log, 'warn');
|
|
103
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 502,
|
|
106
|
+
statusText: 'Bad Gateway',
|
|
107
|
+
json: vi.fn(),
|
|
108
|
+
}));
|
|
109
|
+
const page = {
|
|
110
|
+
evaluate: vi.fn(async (js) => Function(`return (${js})`)()()),
|
|
111
|
+
};
|
|
112
|
+
await stepFetch(page, { url: 'https://api.example.com/items/${{ item.id }}' }, [{ id: 1 }, { id: 2 }], {});
|
|
113
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('https://api.example.com/items/1'));
|
|
115
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('https://api.example.com/items/2'));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: intercept — declarative XHR interception.
|
|
3
|
+
*/
|
|
4
|
+
import { render, normalizeEvaluateSource } from '../template.js';
|
|
5
|
+
export async function stepIntercept(page, params, data, args) {
|
|
6
|
+
const cfg = typeof params === 'object' && params !== null ? params : {};
|
|
7
|
+
const trigger = cfg.trigger ?? '';
|
|
8
|
+
const capturePattern = cfg.capture ?? '';
|
|
9
|
+
const timeout = cfg.timeout ?? 8;
|
|
10
|
+
const selectPath = cfg.select ?? null;
|
|
11
|
+
if (!capturePattern)
|
|
12
|
+
return data;
|
|
13
|
+
// Step 1: Inject fetch/XHR interceptor BEFORE trigger
|
|
14
|
+
await page.installInterceptor(capturePattern);
|
|
15
|
+
// Step 2: Execute the trigger action
|
|
16
|
+
if (trigger.startsWith('navigate:')) {
|
|
17
|
+
const url = render(trigger.slice('navigate:'.length), { args, data });
|
|
18
|
+
await page.goto(String(url));
|
|
19
|
+
}
|
|
20
|
+
else if (trigger.startsWith('evaluate:')) {
|
|
21
|
+
const js = trigger.slice('evaluate:'.length);
|
|
22
|
+
await page.evaluate(normalizeEvaluateSource(render(js, { args, data })));
|
|
23
|
+
}
|
|
24
|
+
else if (trigger.startsWith('click:')) {
|
|
25
|
+
const ref = render(trigger.slice('click:'.length), { args, data });
|
|
26
|
+
await page.click(String(ref).replace(/^@/, ''));
|
|
27
|
+
}
|
|
28
|
+
else if (trigger === 'scroll') {
|
|
29
|
+
await page.scroll('down');
|
|
30
|
+
}
|
|
31
|
+
// Step 3: Wait for network capture (event-driven, not fixed sleep)
|
|
32
|
+
await page.waitForCapture(timeout);
|
|
33
|
+
// Step 4: Retrieve captured data
|
|
34
|
+
const matchingResponses = await page.getInterceptedRequests();
|
|
35
|
+
// Step 5: Select from response if specified
|
|
36
|
+
let result = matchingResponses.length === 1 ? matchingResponses[0] :
|
|
37
|
+
matchingResponses.length > 1 ? matchingResponses : data;
|
|
38
|
+
if (selectPath && result) {
|
|
39
|
+
let current = result;
|
|
40
|
+
for (const part of String(selectPath).split('.')) {
|
|
41
|
+
if (current && typeof current === 'object' && !Array.isArray(current)) {
|
|
42
|
+
current = current[part];
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
result = current ?? result;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step: tap — declarative Store Action Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained IIFE that:
|
|
5
|
+
* 1. Injects fetch + XHR dual interception proxy
|
|
6
|
+
* 2. Finds the Pinia/Vuex store and calls the action
|
|
7
|
+
* 3. Captures the response matching the URL pattern
|
|
8
|
+
* 4. Auto-cleans up interception in finally block
|
|
9
|
+
* 5. Returns the captured data (optionally sub-selected)
|
|
10
|
+
*/
|
|
11
|
+
import type { IPage } from '../../types.js';
|
|
12
|
+
export declare function stepTap(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown>;
|