@workerstack/repo 0.0.0-stage → 0.1.0
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Session browser rail: nav|snap|click|fill|scroll|wait|hover|select|shot|pdf|stop|doctor|ensure. Lightpanda default. */
|
|
3
|
+
import { spawn, spawnSync, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { resolveRoots } from "./workspace-root.ts";
|
|
10
|
+
import { writeStash } from "./job.ts";
|
|
11
|
+
import { isRecord } from "./hook-input.ts";
|
|
12
|
+
import {
|
|
13
|
+
CHROMIUM_BINS,
|
|
14
|
+
LIGHTPANDA_REPO as DOCTOR_LP_REPO,
|
|
15
|
+
globalsRow,
|
|
16
|
+
installHints,
|
|
17
|
+
runDoctor,
|
|
18
|
+
whichBin,
|
|
19
|
+
} from "./doctor.ts";
|
|
20
|
+
import { childEnv } from "../env.ts";
|
|
21
|
+
|
|
22
|
+
export { CHROMIUM_BINS, whichBin };
|
|
23
|
+
|
|
24
|
+
type Rec = Record<string, unknown>;
|
|
25
|
+
export type Engine = "lightpanda" | "chromium";
|
|
26
|
+
|
|
27
|
+
export type Bins = {
|
|
28
|
+
lightpanda: string;
|
|
29
|
+
chromium: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** One remembered snapshot ref. */
|
|
33
|
+
export interface RefEntry {
|
|
34
|
+
role: string;
|
|
35
|
+
name: string;
|
|
36
|
+
backendNodeId: unknown;
|
|
37
|
+
nodeId: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The live browser session stored in session.json. */
|
|
41
|
+
export interface Session {
|
|
42
|
+
engine: Engine;
|
|
43
|
+
pid?: number | undefined;
|
|
44
|
+
port: number;
|
|
45
|
+
lastURL: string;
|
|
46
|
+
lastRefs: Record<string, RefEntry>;
|
|
47
|
+
escalated: boolean;
|
|
48
|
+
swapCount: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BrowserResult {
|
|
52
|
+
code: number;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Cdp {
|
|
58
|
+
send(method: string, params?: Rec): Promise<Rec>;
|
|
59
|
+
close(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FakeCdpState {
|
|
63
|
+
url: string;
|
|
64
|
+
markdown: string;
|
|
65
|
+
failProbe: boolean;
|
|
66
|
+
thinSnap: boolean;
|
|
67
|
+
engine: string;
|
|
68
|
+
axNodes?: unknown[];
|
|
69
|
+
/** Per CDP method: a record is the reply, a string is the error message. */
|
|
70
|
+
replies?: Record<string, Rec | string>;
|
|
71
|
+
/** Every CDP request the fake receives, in order. */
|
|
72
|
+
calls?: { method: string; params: Rec }[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Ctx {
|
|
76
|
+
cwd: string;
|
|
77
|
+
env: NodeJS.ProcessEnv;
|
|
78
|
+
platform: NodeJS.Platform;
|
|
79
|
+
bins: Bins;
|
|
80
|
+
readyMs: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function asRec(v: unknown): Rec {
|
|
84
|
+
return isRecord(v) ? v : {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function errMessage(e: unknown): string {
|
|
88
|
+
return String(asRec(e).message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toSession(v: unknown): Session | null {
|
|
92
|
+
if (!isRecord(v)) return null;
|
|
93
|
+
const engine = v.engine === "lightpanda" || v.engine === "chromium" ? v.engine : null;
|
|
94
|
+
const port = Number(v.port);
|
|
95
|
+
if (!engine || !Number.isFinite(port)) return null;
|
|
96
|
+
const lastRefs: Record<string, RefEntry> = {};
|
|
97
|
+
for (const [id, e] of Object.entries(asRec(v.lastRefs))) {
|
|
98
|
+
if (!isRecord(e)) continue;
|
|
99
|
+
lastRefs[id] = {
|
|
100
|
+
role: String(e.role ?? ""),
|
|
101
|
+
name: String(e.name ?? ""),
|
|
102
|
+
backendNodeId: e.backendNodeId,
|
|
103
|
+
nodeId: e.nodeId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
engine,
|
|
108
|
+
pid: typeof v.pid === "number" ? v.pid : undefined,
|
|
109
|
+
port,
|
|
110
|
+
lastURL: String(v.lastURL ?? ""),
|
|
111
|
+
lastRefs,
|
|
112
|
+
escalated: v.escalated === true,
|
|
113
|
+
swapCount: Number(v.swapCount) || 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export const SESSION_REL = ".tmp/repo/browser";
|
|
117
|
+
export const LIGHTPANDA_REPO: "https://github.com/lightpanda-io/browser" = DOCTOR_LP_REPO;
|
|
118
|
+
const INSTALL_HINT = `no browser engine; install lightpanda (${LIGHTPANDA_REPO}) or Chromium`;
|
|
119
|
+
const CHROMIUM_VERBS = new Set(["shot", "pdf", "trace"]);
|
|
120
|
+
const REF_ROLES = new Set([
|
|
121
|
+
"link",
|
|
122
|
+
"button",
|
|
123
|
+
"textbox",
|
|
124
|
+
"searchbox",
|
|
125
|
+
"combobox",
|
|
126
|
+
"checkbox",
|
|
127
|
+
"radio",
|
|
128
|
+
"tab",
|
|
129
|
+
"menuitem",
|
|
130
|
+
"option",
|
|
131
|
+
]);
|
|
132
|
+
const PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
133
|
+
const PDF_B64 =
|
|
134
|
+
"JVBERi0xLjAKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3hbMCAwIDMgM10vUGFyZW50IDIgMCBSL1Jlc291cmNlczw+Pj4KZW5kb2JqCnhyZWYKMCA0CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAwOSAwMDAwMCBuIAowMDAwMDAwMDU4IDAwMDAwIG4gCjAwMDAwMDAxMTUgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDQvUm9vdCAxIDAgUj4+CnN0YXJ0eHJlZgoxOTAKJSVFT0Y=";
|
|
135
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
136
|
+
|
|
137
|
+
export function sessionDir(cwd?: string): string {
|
|
138
|
+
return path.join(resolveRoots({}, { cwd: cwd || process.cwd() }).root, SESSION_REL);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sessionFile(cwd?: string): string {
|
|
142
|
+
return path.join(sessionDir(cwd), "session.json");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isPaintHost(url: unknown): boolean {
|
|
146
|
+
let u: URL;
|
|
147
|
+
try {
|
|
148
|
+
u = new URL(String(url || ""));
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const h = u.hostname.toLowerCase();
|
|
153
|
+
if (h === "figma.com" || h.endsWith(".figma.com")) return true;
|
|
154
|
+
if (h === "docs.google.com" || h.endsWith(".docs.google.com")) return true;
|
|
155
|
+
if (h === "sheets.google.com" || h.endsWith(".sheets.google.com")) return true;
|
|
156
|
+
if (h === "slides.google.com" || h.endsWith(".slides.google.com")) return true;
|
|
157
|
+
if (h === "mail.google.com" || h.endsWith(".mail.google.com")) return true;
|
|
158
|
+
if (h === "earth.google.com" || h.endsWith(".earth.google.com")) return true;
|
|
159
|
+
if ((h === "google.com" || h.endsWith(".google.com")) && u.pathname.startsWith("/earth")) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (/(^|\.)amazon\./.test(h)) return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const BLOCKED_SNAP_RE =
|
|
167
|
+
/enable javascript|javascript (is )?(required|disabled)|checking your browser|just a moment|captcha|access denied|sorry.*robot|continue shopping|attention required|cf-browser-verification|verify you are a human/i;
|
|
168
|
+
|
|
169
|
+
export function isBlockedSnap(markdown: unknown, lines: unknown): boolean {
|
|
170
|
+
const refs = Array.isArray(lines) ? lines.length : 0;
|
|
171
|
+
const md = String(markdown || "").trim();
|
|
172
|
+
if (BLOCKED_SNAP_RE.test(md)) return true;
|
|
173
|
+
const body = md.replace(/^#\s+\S+\s*$/m, "").trim();
|
|
174
|
+
return refs === 0 && body.length < 80;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function lookupBins(pathEnv: string | undefined, platform: NodeJS.Platform = process.platform): Bins {
|
|
178
|
+
const lightpanda = whichBin("lightpanda", pathEnv, platform);
|
|
179
|
+
let chromium = "";
|
|
180
|
+
for (const n of CHROMIUM_BINS) {
|
|
181
|
+
chromium = whichBin(n, pathEnv, platform);
|
|
182
|
+
if (chromium) break;
|
|
183
|
+
}
|
|
184
|
+
return { lightpanda, chromium };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function doctorHints(platform: NodeJS.Platform = process.platform): string[] {
|
|
188
|
+
return [...installHints("lightpanda", platform), ...installHints("chromium", platform)];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function installHint(platform: NodeJS.Platform = process.platform): string {
|
|
192
|
+
return [INSTALL_HINT, ...doctorHints(platform)].join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function pickEngine({
|
|
196
|
+
platform = process.platform,
|
|
197
|
+
verb = "nav",
|
|
198
|
+
url = "",
|
|
199
|
+
bins = {},
|
|
200
|
+
forceChromium = false,
|
|
201
|
+
}: {
|
|
202
|
+
platform?: NodeJS.Platform;
|
|
203
|
+
verb?: string;
|
|
204
|
+
url?: string;
|
|
205
|
+
bins?: Partial<Bins> & Record<string, string | undefined>;
|
|
206
|
+
forceChromium?: boolean;
|
|
207
|
+
} = {}): { engine: Engine; error?: undefined } | { engine: null; error: string } {
|
|
208
|
+
const hasC = Boolean(bins.chromium);
|
|
209
|
+
const hasL = Boolean(bins.lightpanda);
|
|
210
|
+
const wantC = forceChromium || platform === "win32" || CHROMIUM_VERBS.has(String(verb)) || isPaintHost(url);
|
|
211
|
+
if (wantC) {
|
|
212
|
+
if (!hasC) return { engine: null, error: installHint(platform) };
|
|
213
|
+
return { engine: "chromium" };
|
|
214
|
+
}
|
|
215
|
+
if (hasL) return { engine: "lightpanda" };
|
|
216
|
+
if (hasC) return { engine: "chromium" };
|
|
217
|
+
return { engine: null, error: installHint(platform) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sleep(ms: number): Promise<void> {
|
|
221
|
+
return new Promise<void>((r) => setTimeout(r, ms));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function alive(pid: number | undefined): boolean {
|
|
225
|
+
if (!pid) return false;
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Whether a spawn or kill here goes through Windows: the platform asked for, or the host, whose process model decides. */
|
|
235
|
+
const onWindows = (platform: NodeJS.Platform): boolean => platform === "win32" || process.platform === "win32";
|
|
236
|
+
|
|
237
|
+
function reap(pid: number | undefined, platform: NodeJS.Platform): void {
|
|
238
|
+
if (!pid) return;
|
|
239
|
+
if (onWindows(platform)) {
|
|
240
|
+
// The tree first: a shell shim's child outlives the shell once that is killed, and holds the profile dir open.
|
|
241
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 1500, windowsHide: true });
|
|
242
|
+
try {
|
|
243
|
+
process.kill(pid);
|
|
244
|
+
} catch {
|
|
245
|
+
/* ignore */
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
process.kill(-pid, "SIGTERM");
|
|
251
|
+
} catch {
|
|
252
|
+
/* ignore */
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
process.kill(pid, "SIGTERM");
|
|
256
|
+
} catch {
|
|
257
|
+
/* ignore */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readSession(cwd?: string): Session | null {
|
|
262
|
+
const f = sessionFile(cwd);
|
|
263
|
+
if (!fs.existsSync(f)) return null;
|
|
264
|
+
try {
|
|
265
|
+
const raw: unknown = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
266
|
+
return toSession(raw);
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function writeSession(cwd: string, session: Session): void {
|
|
273
|
+
const dir = sessionDir(cwd);
|
|
274
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
275
|
+
fs.writeFileSync(sessionFile(cwd), JSON.stringify(session, null, 2) + "\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function clearSession(cwd: string): void {
|
|
279
|
+
try {
|
|
280
|
+
fs.unlinkSync(sessionFile(cwd));
|
|
281
|
+
} catch {
|
|
282
|
+
/* ignore */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ok(stdout: string): BrowserResult {
|
|
287
|
+
return { code: 0, stdout: stdout.endsWith("\n") ? stdout : stdout + "\n", stderr: "" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function fail(stderr: string): BrowserResult {
|
|
291
|
+
const msg = stderr.replace(/^Error:\s*/, "");
|
|
292
|
+
return { code: 1, stdout: "", stderr: msg.endsWith("\n") ? msg : msg + "\n" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function freePort(): Promise<number> {
|
|
296
|
+
return new Promise<number>((resolve, reject) => {
|
|
297
|
+
const s = net.createServer();
|
|
298
|
+
s.listen(0, "127.0.0.1", () => {
|
|
299
|
+
const p = Number(asRec(s.address()).port);
|
|
300
|
+
s.close(() => resolve(p));
|
|
301
|
+
});
|
|
302
|
+
s.on("error", reject);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function waitVersion(port: number, timeoutMs: number, failed?: () => Error | undefined): Promise<Rec> {
|
|
307
|
+
const t0 = Date.now();
|
|
308
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
309
|
+
const dead = failed?.();
|
|
310
|
+
if (dead) throw dead;
|
|
311
|
+
try {
|
|
312
|
+
const r = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
313
|
+
if (r.ok) {
|
|
314
|
+
const v: unknown = await r.json();
|
|
315
|
+
return asRec(v);
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
/* retry */
|
|
319
|
+
}
|
|
320
|
+
await sleep(40);
|
|
321
|
+
}
|
|
322
|
+
throw new Error("engine not ready");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function connectCdp(port: number, timeoutMs = 4000): Promise<Cdp> {
|
|
326
|
+
const version = await waitVersion(port, timeoutMs);
|
|
327
|
+
const wsUrl = version.webSocketDebuggerUrl;
|
|
328
|
+
if (typeof wsUrl !== "string" || !wsUrl) throw new Error("engine not ready");
|
|
329
|
+
const ws = new WebSocket(wsUrl);
|
|
330
|
+
await new Promise<void>((resolve, reject) => {
|
|
331
|
+
const t = setTimeout(() => reject(new Error("engine not ready")), timeoutMs);
|
|
332
|
+
ws.addEventListener("open", () => {
|
|
333
|
+
clearTimeout(t);
|
|
334
|
+
resolve();
|
|
335
|
+
});
|
|
336
|
+
ws.addEventListener("error", () => {
|
|
337
|
+
clearTimeout(t);
|
|
338
|
+
reject(new Error("engine not ready"));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
let nextId = 1;
|
|
342
|
+
const pending = new Map<number, { resolve: (v: Rec) => void; reject: (e: Error) => void }>();
|
|
343
|
+
ws.addEventListener("message", (ev) => {
|
|
344
|
+
let parsed: unknown;
|
|
345
|
+
try {
|
|
346
|
+
parsed = JSON.parse(String(ev.data));
|
|
347
|
+
} catch {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const msg = asRec(parsed);
|
|
351
|
+
if (typeof msg.id !== "number") return;
|
|
352
|
+
const p = pending.get(msg.id);
|
|
353
|
+
if (!p) return;
|
|
354
|
+
pending.delete(msg.id);
|
|
355
|
+
if (msg.error) p.reject(new Error(String(asRec(msg.error).message || "cdp")));
|
|
356
|
+
else p.resolve(asRec(msg.result));
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
send(method: string, params: Rec = {}): Promise<Rec> {
|
|
360
|
+
const id = nextId++;
|
|
361
|
+
return new Promise<Rec>((resolve, reject) => {
|
|
362
|
+
pending.set(id, { resolve, reject });
|
|
363
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
close() {
|
|
367
|
+
ws.close();
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function defaultAx(url: string): Rec[] {
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
nodeId: "1",
|
|
376
|
+
role: { value: "WebArea" },
|
|
377
|
+
name: { value: url || "page" },
|
|
378
|
+
childIds: ["2", "3"],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
nodeId: "2",
|
|
382
|
+
role: { value: "link" },
|
|
383
|
+
name: { value: "Home" },
|
|
384
|
+
backendDOMNodeId: 10,
|
|
385
|
+
childIds: [],
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
nodeId: "3",
|
|
389
|
+
role: { value: "textbox" },
|
|
390
|
+
name: { value: "Search" },
|
|
391
|
+
backendDOMNodeId: 11,
|
|
392
|
+
childIds: [],
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function encodeWsFrame(data: string): Buffer {
|
|
398
|
+
const payload = Buffer.from(data);
|
|
399
|
+
const len = payload.length;
|
|
400
|
+
let header: Buffer;
|
|
401
|
+
if (len < 126) {
|
|
402
|
+
header = Buffer.alloc(2);
|
|
403
|
+
header[0] = 0x81;
|
|
404
|
+
header[1] = len;
|
|
405
|
+
} else if (len < 65536) {
|
|
406
|
+
header = Buffer.alloc(4);
|
|
407
|
+
header[0] = 0x81;
|
|
408
|
+
header[1] = 126;
|
|
409
|
+
header.writeUInt16BE(len, 2);
|
|
410
|
+
} else {
|
|
411
|
+
header = Buffer.alloc(10);
|
|
412
|
+
header[0] = 0x81;
|
|
413
|
+
header[1] = 127;
|
|
414
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
415
|
+
}
|
|
416
|
+
return Buffer.concat([header, payload]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function decodeWsFrames(buf: Buffer): {
|
|
420
|
+
messages: ({ type: "close" } | { type: "msg"; data: string })[];
|
|
421
|
+
rest: Buffer;
|
|
422
|
+
} {
|
|
423
|
+
const messages: ({ type: "close" } | { type: "msg"; data: string })[] = [];
|
|
424
|
+
let offset = 0;
|
|
425
|
+
while (buf.length - offset >= 2) {
|
|
426
|
+
const b1 = buf[offset + 1]!; // the loop guard keeps offset + 1 in range
|
|
427
|
+
const masked = (b1 & 0x80) !== 0;
|
|
428
|
+
let len = b1 & 0x7f;
|
|
429
|
+
let start = offset + 2;
|
|
430
|
+
if (len === 126) {
|
|
431
|
+
if (buf.length - offset < 4) break;
|
|
432
|
+
len = buf.readUInt16BE(offset + 2);
|
|
433
|
+
start = offset + 4;
|
|
434
|
+
} else if (len === 127) {
|
|
435
|
+
if (buf.length - offset < 10) break;
|
|
436
|
+
len = Number(buf.readBigUInt64BE(offset + 2));
|
|
437
|
+
start = offset + 10;
|
|
438
|
+
}
|
|
439
|
+
const maskStart = start;
|
|
440
|
+
if (masked) start += 4;
|
|
441
|
+
if (buf.length < start + len) break;
|
|
442
|
+
let payload = buf.subarray(start, start + len);
|
|
443
|
+
if (masked) {
|
|
444
|
+
const mask = buf.subarray(maskStart, maskStart + 4);
|
|
445
|
+
payload = Buffer.from(payload);
|
|
446
|
+
for (let i = 0; i < payload.length; i++) payload[i] = payload[i]! ^ mask[i % 4]!; // both indexes are in range
|
|
447
|
+
}
|
|
448
|
+
const opcode = buf[offset]! & 0x0f; // offset < buf.length
|
|
449
|
+
offset = start + len;
|
|
450
|
+
if (opcode === 0x8) messages.push({ type: "close" });
|
|
451
|
+
else if (opcode === 0x1 || opcode === 0x2) {
|
|
452
|
+
messages.push({ type: "msg", data: payload.toString("utf8") });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { messages, rest: buf.subarray(offset) };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function handleCdp(state: FakeCdpState, method: string, params: Rec): Promise<Rec> {
|
|
459
|
+
state.calls?.push({ method, params });
|
|
460
|
+
const scripted = state.replies?.[method];
|
|
461
|
+
if (typeof scripted === "string") throw new Error(scripted);
|
|
462
|
+
if (scripted) return scripted;
|
|
463
|
+
if (state.failProbe && (method === "Browser.getVersion" || method === "Target.getTargets")) {
|
|
464
|
+
throw new Error("probe failed");
|
|
465
|
+
}
|
|
466
|
+
switch (method) {
|
|
467
|
+
case "Browser.getVersion":
|
|
468
|
+
return { protocolVersion: "1.3", product: state.engine || "lightpanda" };
|
|
469
|
+
case "Target.getTargets":
|
|
470
|
+
return {
|
|
471
|
+
targetInfos: [{ targetId: "t1", type: "page", url: state.url || "about:blank", attached: true }],
|
|
472
|
+
};
|
|
473
|
+
case "Page.enable":
|
|
474
|
+
case "Accessibility.enable":
|
|
475
|
+
case "Runtime.enable":
|
|
476
|
+
case "DOM.enable":
|
|
477
|
+
return {};
|
|
478
|
+
case "Page.navigate":
|
|
479
|
+
state.url = typeof params.url === "string" && params.url ? params.url : state.url;
|
|
480
|
+
state.markdown = `# ${state.url}\n\nhello`;
|
|
481
|
+
return { frameId: "f1" };
|
|
482
|
+
case "LP.getMarkdown":
|
|
483
|
+
if (state.thinSnap) return { markdown: `# ${state.url || ""}\n` };
|
|
484
|
+
return { markdown: state.markdown || `# ${state.url || ""}\n\nhello` };
|
|
485
|
+
case "Accessibility.getFullAXTree":
|
|
486
|
+
if (state.thinSnap) return { nodes: [] };
|
|
487
|
+
return { nodes: state.axNodes || defaultAx(state.url) };
|
|
488
|
+
case "DOM.resolveNode": {
|
|
489
|
+
const id = params.backendNodeId;
|
|
490
|
+
const known = new Set([10, 11]);
|
|
491
|
+
if (typeof id !== "number" || !known.has(id)) throw new Error("stale");
|
|
492
|
+
return { object: { objectId: "oid-" + id } };
|
|
493
|
+
}
|
|
494
|
+
case "Runtime.callFunctionOn":
|
|
495
|
+
return { result: { type: "undefined" } };
|
|
496
|
+
case "Input.insertText":
|
|
497
|
+
return {};
|
|
498
|
+
case "Page.captureScreenshot":
|
|
499
|
+
return { data: PNG_B64 };
|
|
500
|
+
case "Page.printToPDF":
|
|
501
|
+
return { data: PDF_B64 };
|
|
502
|
+
case "Runtime.evaluate":
|
|
503
|
+
return { result: { type: "string", value: state.markdown || "" } };
|
|
504
|
+
default:
|
|
505
|
+
return {};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Undocumented test helper: fake Lightpanda/Chromium CDP. */
|
|
510
|
+
export async function serveCdp(
|
|
511
|
+
state: Partial<FakeCdpState> = {},
|
|
512
|
+
{ port = 0 }: { port?: number } = {},
|
|
513
|
+
): Promise<{ port: number; close(): void; state: FakeCdpState }> {
|
|
514
|
+
const st: FakeCdpState = {
|
|
515
|
+
url: "about:blank",
|
|
516
|
+
markdown: "",
|
|
517
|
+
failProbe: false,
|
|
518
|
+
thinSnap: false,
|
|
519
|
+
engine: "lightpanda",
|
|
520
|
+
...state,
|
|
521
|
+
};
|
|
522
|
+
let bound = 0;
|
|
523
|
+
const server = http.createServer((req, res) => {
|
|
524
|
+
if (req.url?.startsWith("/json/version")) {
|
|
525
|
+
res.setHeader("content-type", "application/json");
|
|
526
|
+
res.end(
|
|
527
|
+
JSON.stringify({
|
|
528
|
+
Browser: st.engine || "Lightpanda",
|
|
529
|
+
"Protocol-Version": "1.3",
|
|
530
|
+
webSocketDebuggerUrl: `ws://127.0.0.1:${bound}/devtools/browser/repo`,
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
res.statusCode = 404;
|
|
536
|
+
res.end();
|
|
537
|
+
});
|
|
538
|
+
server.on("upgrade", (req, socket) => {
|
|
539
|
+
const key = req.headers["sec-websocket-key"];
|
|
540
|
+
if (typeof key !== "string" || !key) {
|
|
541
|
+
socket.destroy();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const accept = crypto
|
|
545
|
+
.createHash("sha1")
|
|
546
|
+
.update(key + WS_GUID)
|
|
547
|
+
.digest("base64");
|
|
548
|
+
socket.write(
|
|
549
|
+
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " +
|
|
550
|
+
accept +
|
|
551
|
+
"\r\n\r\n",
|
|
552
|
+
);
|
|
553
|
+
let buf = Buffer.alloc(0);
|
|
554
|
+
socket.on("data", (chunk) => {
|
|
555
|
+
buf = Buffer.concat([buf, chunk]);
|
|
556
|
+
const decoded = decodeWsFrames(buf);
|
|
557
|
+
buf = Buffer.from(decoded.rest);
|
|
558
|
+
for (const m of decoded.messages) {
|
|
559
|
+
if (m.type === "close") {
|
|
560
|
+
socket.end();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
let parsed: unknown;
|
|
564
|
+
try {
|
|
565
|
+
parsed = JSON.parse(m.data);
|
|
566
|
+
} catch {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const msg = asRec(parsed);
|
|
570
|
+
Promise.resolve()
|
|
571
|
+
.then(() => handleCdp(st, String(msg.method), asRec(msg.params)))
|
|
572
|
+
.then((result) => {
|
|
573
|
+
socket.write(encodeWsFrame(JSON.stringify({ id: msg.id, result })));
|
|
574
|
+
})
|
|
575
|
+
.catch((err) => {
|
|
576
|
+
socket.write(
|
|
577
|
+
encodeWsFrame(
|
|
578
|
+
JSON.stringify({
|
|
579
|
+
id: msg.id,
|
|
580
|
+
error: { code: -32000, message: errMessage(err) },
|
|
581
|
+
}),
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
await new Promise<void>((resolve, reject) => {
|
|
589
|
+
server.once("error", reject);
|
|
590
|
+
server.listen(port, "127.0.0.1", resolve);
|
|
591
|
+
});
|
|
592
|
+
bound = Number(asRec(server.address()).port);
|
|
593
|
+
return {
|
|
594
|
+
port: bound,
|
|
595
|
+
close() {
|
|
596
|
+
server.close();
|
|
597
|
+
},
|
|
598
|
+
state: st,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function engineBin(engine: Engine, bins: Bins): string {
|
|
603
|
+
if (engine === "lightpanda") return bins.lightpanda;
|
|
604
|
+
return bins.chromium;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function spawnEngine(bin: string, args: string[], ctx: Ctx): ChildProcess {
|
|
608
|
+
const opts: SpawnOptions = { detached: true, stdio: ["ignore", "pipe", "pipe"], env: ctx.env, cwd: ctx.cwd };
|
|
609
|
+
if (onWindows(ctx.platform) && /\.(cmd|bat)$/i.test(bin)) {
|
|
610
|
+
const mjs = `${String(bin).replace(/\.(cmd|bat)$/i, "")}.mjs`;
|
|
611
|
+
try {
|
|
612
|
+
fs.accessSync(mjs, fs.constants.F_OK);
|
|
613
|
+
return spawn(process.execPath, [mjs, ...args], opts);
|
|
614
|
+
} catch {
|
|
615
|
+
return spawn(bin, args, { ...opts, shell: true });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return spawn(bin, args, opts);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function spawnArgs(engine: Engine, port: number, profile: string): string[] {
|
|
622
|
+
if (engine === "lightpanda") return ["serve", "--host", "127.0.0.1", "--port", String(port)];
|
|
623
|
+
return [
|
|
624
|
+
"--headless=new",
|
|
625
|
+
"--disable-gpu",
|
|
626
|
+
"--remote-debugging-port",
|
|
627
|
+
String(port),
|
|
628
|
+
"--user-data-dir",
|
|
629
|
+
profile,
|
|
630
|
+
"about:blank",
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function launchEngine(engine: Engine, ctx: Ctx): Promise<Session> {
|
|
635
|
+
const bin = engineBin(engine, ctx.bins);
|
|
636
|
+
const dir = sessionDir(ctx.cwd);
|
|
637
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
638
|
+
const profile = path.join(dir, "profile");
|
|
639
|
+
fs.mkdirSync(profile, { recursive: true });
|
|
640
|
+
const port = await freePort();
|
|
641
|
+
const child = spawnEngine(bin, spawnArgs(engine, port, profile), ctx);
|
|
642
|
+
child.unref();
|
|
643
|
+
// An engine that cannot start (missing, not executable) reports through 'error'; unhandled it would crash the process.
|
|
644
|
+
let spawnError: Error | undefined;
|
|
645
|
+
child.once("error", (err) => {
|
|
646
|
+
spawnError = new Error(`engine failed to start: ${err.message}`);
|
|
647
|
+
});
|
|
648
|
+
try {
|
|
649
|
+
await waitVersion(port, ctx.readyMs, () => spawnError);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
reap(child.pid, ctx.platform);
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
engine,
|
|
656
|
+
pid: child.pid,
|
|
657
|
+
port,
|
|
658
|
+
lastURL: "",
|
|
659
|
+
lastRefs: {},
|
|
660
|
+
escalated: false,
|
|
661
|
+
swapCount: 0,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function probe(session: Session): Promise<void> {
|
|
666
|
+
const cdp = await connectCdp(session.port);
|
|
667
|
+
try {
|
|
668
|
+
await cdp.send("Browser.getVersion");
|
|
669
|
+
} finally {
|
|
670
|
+
cdp.close();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function navigate(session: Session, url: string): Promise<void> {
|
|
675
|
+
const cdp = await connectCdp(session.port);
|
|
676
|
+
try {
|
|
677
|
+
await cdp.send("Page.enable").catch(() => {});
|
|
678
|
+
await cdp.send("Page.navigate", { url });
|
|
679
|
+
session.lastURL = url;
|
|
680
|
+
} finally {
|
|
681
|
+
cdp.close();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function axText(v: unknown): string {
|
|
686
|
+
return String(isRecord(v) ? (v.value ?? "") : (v ?? ""));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function formatRefs(nodes: unknown[]): { lastRefs: Record<string, RefEntry>; lines: string[] } {
|
|
690
|
+
const lastRefs: Record<string, RefEntry> = {};
|
|
691
|
+
const lines: string[] = [];
|
|
692
|
+
let n = 1;
|
|
693
|
+
for (const raw of nodes) {
|
|
694
|
+
const node = asRec(raw);
|
|
695
|
+
const role = axText(node.role).toLowerCase();
|
|
696
|
+
if (!REF_ROLES.has(role)) continue;
|
|
697
|
+
const name = axText(node.name);
|
|
698
|
+
const id = String(n++);
|
|
699
|
+
lastRefs[id] = {
|
|
700
|
+
role,
|
|
701
|
+
name,
|
|
702
|
+
backendNodeId: node.backendDOMNodeId ?? node.backendNodeId,
|
|
703
|
+
nodeId: node.nodeId,
|
|
704
|
+
};
|
|
705
|
+
lines.push(`[${id}] ${role} ${name}`.trim());
|
|
706
|
+
}
|
|
707
|
+
return { lastRefs, lines };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function snapshot(session: Session): Promise<{ markdown: string; lines: string[] }> {
|
|
711
|
+
const cdp = await connectCdp(session.port);
|
|
712
|
+
try {
|
|
713
|
+
let markdown = "";
|
|
714
|
+
if (session.engine === "lightpanda") {
|
|
715
|
+
try {
|
|
716
|
+
const r = await cdp.send("LP.getMarkdown");
|
|
717
|
+
const v = r.markdown || r.result || "";
|
|
718
|
+
markdown = typeof v === "string" ? v : "";
|
|
719
|
+
} catch {
|
|
720
|
+
markdown = "";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!markdown) {
|
|
724
|
+
markdown = `# ${session.lastURL || ""}\n`;
|
|
725
|
+
}
|
|
726
|
+
await cdp.send("Accessibility.enable").catch(() => {});
|
|
727
|
+
const ax = await cdp.send("Accessibility.getFullAXTree");
|
|
728
|
+
const { lastRefs, lines } = formatRefs(Array.isArray(ax.nodes) ? ax.nodes : []);
|
|
729
|
+
session.lastRefs = lastRefs;
|
|
730
|
+
return { markdown, lines };
|
|
731
|
+
} finally {
|
|
732
|
+
cdp.close();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function swapToChromium(session: Session, ctx: Ctx): Promise<Session> {
|
|
737
|
+
const url = session.lastURL;
|
|
738
|
+
reap(session.pid, ctx.platform);
|
|
739
|
+
const next = await launchEngine("chromium", ctx);
|
|
740
|
+
next.lastURL = url;
|
|
741
|
+
next.lastRefs = session.lastRefs;
|
|
742
|
+
next.escalated = true;
|
|
743
|
+
next.swapCount = session.swapCount + 1;
|
|
744
|
+
if (url) await navigate(next, url);
|
|
745
|
+
return next;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function withCdp<T>(session: Session, fn: (cdp: Cdp) => Promise<T>): Promise<T> {
|
|
749
|
+
const cdp = await connectCdp(session.port);
|
|
750
|
+
try {
|
|
751
|
+
return await fn(cdp);
|
|
752
|
+
} finally {
|
|
753
|
+
cdp.close();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export async function runBrowser(
|
|
758
|
+
argv: string[],
|
|
759
|
+
opts: {
|
|
760
|
+
cwd?: string;
|
|
761
|
+
env?: NodeJS.ProcessEnv;
|
|
762
|
+
platform?: NodeJS.Platform;
|
|
763
|
+
/** Engine binaries; found on `env.PATH` when absent. */
|
|
764
|
+
bins?: Bins;
|
|
765
|
+
/** How long a launched engine gets to answer /json/version. */
|
|
766
|
+
readyMs?: number;
|
|
767
|
+
} = {},
|
|
768
|
+
): Promise<BrowserResult> {
|
|
769
|
+
const cwd = opts.cwd || process.cwd();
|
|
770
|
+
const env = opts.env || childEnv();
|
|
771
|
+
const platform = opts.platform || process.platform;
|
|
772
|
+
const bins = opts.bins || lookupBins(env.PATH, platform);
|
|
773
|
+
const ctx: Ctx = { cwd, env, platform, bins, readyMs: opts.readyMs ?? 4000 };
|
|
774
|
+
const verb = String(argv[0] || "").toLowerCase();
|
|
775
|
+
try {
|
|
776
|
+
if (!verb || verb === "help" || verb === "-h" || verb === "--help") {
|
|
777
|
+
return ok("nav|snap|click|fill|scroll|wait|hover|select|shot|pdf|stop|doctor|ensure");
|
|
778
|
+
}
|
|
779
|
+
if (verb === "doctor" || verb === "ensure") {
|
|
780
|
+
return runDoctor({ pathEnv: env.PATH, platform, cwd, globals: await globalsRow(cwd, env.HOME) });
|
|
781
|
+
}
|
|
782
|
+
if (verb === "stop") {
|
|
783
|
+
const s = readSession(cwd);
|
|
784
|
+
if (s && s.pid) reap(s.pid, platform);
|
|
785
|
+
clearSession(cwd);
|
|
786
|
+
await sleep(30);
|
|
787
|
+
return ok("stopped");
|
|
788
|
+
}
|
|
789
|
+
if (verb === "nav") {
|
|
790
|
+
const url = String(argv[1] || "").trim();
|
|
791
|
+
if (!url) return fail("nav <url>");
|
|
792
|
+
const picked = pickEngine({ platform, verb: "nav", url, bins });
|
|
793
|
+
if (!picked.engine) return fail(picked.error);
|
|
794
|
+
const prev = readSession(cwd);
|
|
795
|
+
if (prev && prev.pid) reap(prev.pid, platform);
|
|
796
|
+
let session: Session;
|
|
797
|
+
try {
|
|
798
|
+
session = await launchEngine(picked.engine, ctx);
|
|
799
|
+
} catch {
|
|
800
|
+
return fail(INSTALL_HINT);
|
|
801
|
+
}
|
|
802
|
+
if (picked.engine === "lightpanda") {
|
|
803
|
+
try {
|
|
804
|
+
await probe(session);
|
|
805
|
+
} catch {
|
|
806
|
+
reap(session.pid, platform);
|
|
807
|
+
if (!bins.chromium) return fail(INSTALL_HINT);
|
|
808
|
+
session = await launchEngine("chromium", ctx);
|
|
809
|
+
session.escalated = true;
|
|
810
|
+
session.swapCount = 1;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
await navigate(session, url);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
reap(session.pid, platform); // no session is written, so nothing else would ever stop this engine
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
writeSession(cwd, session);
|
|
820
|
+
return ok(`engine: ${session.engine}\nurl: ${session.lastURL}`);
|
|
821
|
+
}
|
|
822
|
+
if (verb === "snap") {
|
|
823
|
+
let session = readSession(cwd);
|
|
824
|
+
if (!session || !alive(session.pid)) return fail("no browser session; run nav");
|
|
825
|
+
let { markdown, lines } = await snapshot(session);
|
|
826
|
+
if (session.engine === "lightpanda" && session.swapCount < 1 && bins.chromium && isBlockedSnap(markdown, lines)) {
|
|
827
|
+
session = await swapToChromium(session, ctx);
|
|
828
|
+
({ markdown, lines } = await snapshot(session));
|
|
829
|
+
}
|
|
830
|
+
writeSession(cwd, session);
|
|
831
|
+
const raw = [markdown.trim(), "", ...lines].join("\n");
|
|
832
|
+
const headings = markdown.split(/\n/).filter((l) => /^#{1,6}\s/.test(l));
|
|
833
|
+
const shown = [...headings.slice(0, 6), ...lines.slice(0, 24)];
|
|
834
|
+
if (raw.length > 32 * 1024 || lines.length > 6) {
|
|
835
|
+
const stash = writeStash({ cwd }, "snap", raw);
|
|
836
|
+
return ok(
|
|
837
|
+
`engine: ${session.engine}\nurl: ${session.lastURL}\n${shown.length} shown. stash=${stash}\n${shown.join("\n")}`,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
const body = [markdown.trim(), "", ...lines].filter((x, i, a) => x !== "" || a[i - 1] !== "").join("\n");
|
|
841
|
+
return ok(`engine: ${session.engine}\nurl: ${session.lastURL}\n\n${body}`);
|
|
842
|
+
}
|
|
843
|
+
if (verb === "wait") {
|
|
844
|
+
const session = readSession(cwd);
|
|
845
|
+
if (!session || !alive(session.pid)) return fail("no browser session; run nav");
|
|
846
|
+
const raw = String(argv[1] || "1000");
|
|
847
|
+
const ms = Math.min(15000, Math.max(0, Number(raw) || 0));
|
|
848
|
+
await sleep(ms);
|
|
849
|
+
return ok(`wait: ${ms}`);
|
|
850
|
+
}
|
|
851
|
+
if (verb === "scroll") {
|
|
852
|
+
const session = readSession(cwd);
|
|
853
|
+
if (!session || !alive(session.pid)) return fail("no browser session; run nav");
|
|
854
|
+
const arg = String(argv[1] || "down");
|
|
855
|
+
const rec = session.lastRefs[arg];
|
|
856
|
+
const act = async (sess: Session) =>
|
|
857
|
+
withCdp(sess, async (cdp) => {
|
|
858
|
+
if (rec) {
|
|
859
|
+
const resolved = await cdp.send("DOM.resolveNode", { backendNodeId: rec.backendNodeId });
|
|
860
|
+
const objectId = asRec(resolved.object).objectId;
|
|
861
|
+
if (!objectId) throw new Error("stale");
|
|
862
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
863
|
+
objectId,
|
|
864
|
+
functionDeclaration: "function() { this.scrollIntoView({ block: 'center' }); }",
|
|
865
|
+
});
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
let dy = 600;
|
|
869
|
+
if (arg === "up") dy = -600;
|
|
870
|
+
else if (/^-?\d+$/.test(arg)) dy = Number(arg);
|
|
871
|
+
await cdp.send("Runtime.evaluate", {
|
|
872
|
+
expression: `window.scrollBy(0, ${dy})`,
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
try {
|
|
876
|
+
await act(session);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
const msg = errMessage(err);
|
|
879
|
+
if (/stale|missing/i.test(msg)) return fail(`stale ref: ${arg}`);
|
|
880
|
+
return fail(msg);
|
|
881
|
+
}
|
|
882
|
+
writeSession(cwd, session);
|
|
883
|
+
return ok(`scroll: ${arg}`);
|
|
884
|
+
}
|
|
885
|
+
if (verb === "click" || verb === "fill" || verb === "hover" || verb === "select") {
|
|
886
|
+
let session = readSession(cwd);
|
|
887
|
+
if (!session || !alive(session.pid)) return fail("no browser session; run nav");
|
|
888
|
+
const refs = session.lastRefs;
|
|
889
|
+
if (!Object.keys(refs).length) return fail("no snap refs; run snap");
|
|
890
|
+
const refId = String(argv[1] || "");
|
|
891
|
+
const rec = refs[refId];
|
|
892
|
+
if (!rec) return fail(`missing ref: ${refId}`);
|
|
893
|
+
const text = argv.slice(2).join(" ");
|
|
894
|
+
if (verb === "fill" && !text) return fail("fill <ref> <text>");
|
|
895
|
+
if (verb === "select" && !text) return fail("select <ref> <value>");
|
|
896
|
+
const act = async (sess: Session) =>
|
|
897
|
+
withCdp(sess, async (cdp) => {
|
|
898
|
+
const resolved = await cdp.send("DOM.resolveNode", { backendNodeId: rec.backendNodeId });
|
|
899
|
+
const objectId = asRec(resolved.object).objectId;
|
|
900
|
+
if (!objectId) throw new Error("stale");
|
|
901
|
+
if (verb === "click") {
|
|
902
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
903
|
+
objectId,
|
|
904
|
+
functionDeclaration: "function() { this.click(); }",
|
|
905
|
+
});
|
|
906
|
+
} else if (verb === "hover") {
|
|
907
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
908
|
+
objectId,
|
|
909
|
+
functionDeclaration: "function() { this.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); }",
|
|
910
|
+
});
|
|
911
|
+
} else if (verb === "select") {
|
|
912
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
913
|
+
objectId,
|
|
914
|
+
arguments: [{ value: text }],
|
|
915
|
+
functionDeclaration:
|
|
916
|
+
"function(v) { this.value = v; this.dispatchEvent(new Event('change', { bubbles: true })); }",
|
|
917
|
+
});
|
|
918
|
+
} else {
|
|
919
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
920
|
+
objectId,
|
|
921
|
+
functionDeclaration: "function() { this.focus(); this.value = ''; }",
|
|
922
|
+
});
|
|
923
|
+
await cdp.send("Input.insertText", { text });
|
|
924
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
925
|
+
objectId,
|
|
926
|
+
functionDeclaration: "function() { this.dispatchEvent(new Event('input', { bubbles: true })); }",
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
try {
|
|
931
|
+
await act(session);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
const msg = errMessage(err);
|
|
934
|
+
if (/stale|missing/i.test(msg)) return fail(`stale ref: ${refId}`);
|
|
935
|
+
if (session.engine !== "chromium" && session.swapCount < 1 && bins.chromium) {
|
|
936
|
+
session = await swapToChromium(session, ctx);
|
|
937
|
+
writeSession(cwd, session);
|
|
938
|
+
try {
|
|
939
|
+
await act(session);
|
|
940
|
+
} catch (err2) {
|
|
941
|
+
const msg2 = errMessage(err2);
|
|
942
|
+
if (/stale|missing/i.test(msg2)) return fail(`stale ref: ${refId}`);
|
|
943
|
+
return fail(msg2);
|
|
944
|
+
}
|
|
945
|
+
} else {
|
|
946
|
+
return fail(msg);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
writeSession(cwd, session);
|
|
950
|
+
return ok(`${verb}: ${refId}`);
|
|
951
|
+
}
|
|
952
|
+
if (verb === "shot" || verb === "pdf") {
|
|
953
|
+
let session = readSession(cwd);
|
|
954
|
+
if (!session || !session.lastURL) return fail("no browser session; run nav");
|
|
955
|
+
const picked = pickEngine({ platform, verb, url: session.lastURL, bins, forceChromium: true });
|
|
956
|
+
if (!picked.engine) return fail(picked.error);
|
|
957
|
+
if (session.engine !== "chromium") {
|
|
958
|
+
session = await swapToChromium(session, ctx);
|
|
959
|
+
} else if (!alive(session.pid)) {
|
|
960
|
+
return fail("no browser session; run nav");
|
|
961
|
+
}
|
|
962
|
+
const dir = sessionDir(cwd);
|
|
963
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
964
|
+
const destName = verb === "shot" ? "shot.png" : "page.pdf";
|
|
965
|
+
const dest = path.join(dir, destName);
|
|
966
|
+
await withCdp(session, async (cdp) => {
|
|
967
|
+
if (verb === "shot") {
|
|
968
|
+
const r = await cdp.send("Page.captureScreenshot", { format: "png" });
|
|
969
|
+
fs.writeFileSync(dest, Buffer.from(String(r.data), "base64"));
|
|
970
|
+
} else {
|
|
971
|
+
const r = await cdp.send("Page.printToPDF", {});
|
|
972
|
+
fs.writeFileSync(dest, Buffer.from(String(r.data), "base64"));
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
writeSession(cwd, session);
|
|
976
|
+
return ok(`engine: ${session.engine}\nfile: ${path.join(SESSION_REL, destName)}`);
|
|
977
|
+
}
|
|
978
|
+
return fail("unknown verb");
|
|
979
|
+
} catch (err) {
|
|
980
|
+
const msg = errMessage(err).replace(/^Error:\s*/, "");
|
|
981
|
+
return fail(msg);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export async function main(argv: string[]): Promise<number> {
|
|
986
|
+
const r = await runBrowser(argv, {
|
|
987
|
+
cwd: process.cwd(),
|
|
988
|
+
env: childEnv(),
|
|
989
|
+
platform: process.platform,
|
|
990
|
+
});
|
|
991
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
992
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
993
|
+
return r.code;
|
|
994
|
+
}
|