talos-code 0.0.1 → 0.2.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 +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-vivo.mjs — M1 (07/09/2026): il MOTORE del browser vivo, cioè far
|
|
3
|
+
* vivere un Chromium che è NOSTRO.
|
|
4
|
+
*
|
|
5
|
+
* Perché esiste. La vista Browser incornicia le pagine con un `<iframe>`, e su
|
|
6
|
+
* un sito che vieta la cornice resta un rettangolo grigio: misurato oggi, il
|
|
7
|
+
* ripiego non scattava nemmeno, perché Chrome carica DENTRO la cornice la
|
|
8
|
+
* propria pagina d'errore e spara un `load` regolare — la cornice non sa di
|
|
9
|
+
* aver fallito. Il ripiego B è quindi un browser VERO: il Chromium già
|
|
10
|
+
* installato sul computer (Chrome o Edge — su Windows 11 Edge c'è sempre),
|
|
11
|
+
* avviato dal nostro server con un PROFILO SEPARATO e pilotato via CDP.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ SICUREZZA, non negoziabile. La pagina di terzi è SEMPRE contenuto non
|
|
14
|
+
* affidabile. Perciò: processo separato che non vede le nostre API; profilo in
|
|
15
|
+
* una cartella nostra, MAI quello personale della persona (i suoi cookie e le
|
|
16
|
+
* sue sessioni non entrano qui); porta di debug solo su loopback; e la sandbox
|
|
17
|
+
* di Chrome resta ACCESA — vedi `argomentiChromium()`.
|
|
18
|
+
*
|
|
19
|
+
* Fonti lette il 07/09/2026, PRIMA di scrivere:
|
|
20
|
+
* · chrome-launcher, `docs/chrome-flags-for-tools.md` — la lista dei flag per
|
|
21
|
+
* un browser pilotato e, testuale, che `--no-sandbox` «is not recommended»;
|
|
22
|
+
* lì stanno anche i flag RIMOSSI da Chrome (`--disable-infobars` via nel
|
|
23
|
+
* 2019, `--headless=new` inutile dal 132 di gennaio 2025) che qui NON si
|
|
24
|
+
* scrivono.
|
|
25
|
+
* · ChromeDevTools/devtools-protocol issue #55 (FAQ) — con
|
|
26
|
+
* `--remote-debugging-port=0` Chrome sceglie una porta libera e scrive
|
|
27
|
+
* l'indirizzo del browser SIA su stderr SIA nel file `DevToolsActivePort`
|
|
28
|
+
* dentro il profilo.
|
|
29
|
+
* · Puppeteer, `waitForWSEndpoint` — la riga si riconosce con
|
|
30
|
+
* `/^DevTools listening on (ws:\/\/.*?)\r?$/`, letta A RIGHE da stderr.
|
|
31
|
+
* · CDP, dominio Target — `flatten` «Enables "flat" access to the session via
|
|
32
|
+
* specifying sessionId attribute in the commands», e «We plan to make this
|
|
33
|
+
* the default, deprecate non-flattened mode, and eventually retire it»
|
|
34
|
+
* (crbug 991325).
|
|
35
|
+
* · chromium issue 40096993 — dal 111 il server CDP risponde **403** a una
|
|
36
|
+
* stretta di mano WebSocket che porta un'intestazione `Origin` non
|
|
37
|
+
* dichiarata in `--remote-allow-origins`. Vedi `argomentiChromium()`: qui
|
|
38
|
+
* quel flag NON si passa, ed è una scelta motivata.
|
|
39
|
+
*
|
|
40
|
+
* ⛔ Nessuna dipendenza nuova: `node:child_process`, `node:fs`, `node:readline`
|
|
41
|
+
* e il `ws` che il backend ha già. Il browser NON si avvia mai nei test —
|
|
42
|
+
* `avvia` ed `esiste` stanno nel contratto proprio perché si possano fingere.
|
|
43
|
+
*/
|
|
44
|
+
import { spawn, execFile } from 'node:child_process';
|
|
45
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
46
|
+
import { createInterface } from 'node:readline';
|
|
47
|
+
|
|
48
|
+
import { ambienteSenzaVariabiliDelServer } from './ambiente-solo-server.mjs';
|
|
49
|
+
import { urlAmmesso } from './browser-frame.mjs';
|
|
50
|
+
|
|
51
|
+
/** Tetto d'attesa della riga «DevTools listening on …»: oltre, errore parlante. */
|
|
52
|
+
export const ATTESA_AVVIO_MS = 20_000;
|
|
53
|
+
/** Tetto d'attesa di UNA risposta CDP: un comando che non torna non resta appeso per sempre. */
|
|
54
|
+
export const ATTESA_CDP_MS = 30_000;
|
|
55
|
+
/** Tetto d'attesa del caricamento di una pagina in `vaiA`. */
|
|
56
|
+
export const ATTESA_CARICAMENTO_MS = 20_000;
|
|
57
|
+
/** Quanto si concede al processo per uscire da solo prima di ucciderlo davvero. */
|
|
58
|
+
export const GRAZIA_CHIUSURA_MS = 3_000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* La riga che Chrome stampa su stderr quando il server CDP è in piedi.
|
|
62
|
+
* ⛔ Il `\r` finale è obbligatorio nel modello: su Windows la riga arriva con
|
|
63
|
+
* il ritorno a capo dentro, e senza questo il ws finirebbe con un carattere
|
|
64
|
+
* invisibile che poi rompe la connessione (è la stessa cura che sta nel
|
|
65
|
+
* `waitForWSEndpoint` di Puppeteer, letto il 07/09/2026).
|
|
66
|
+
*/
|
|
67
|
+
const RIGA_DEVTOOLS = /^\s*DevTools listening on (ws:\/\/\S+?)\s*$/;
|
|
68
|
+
|
|
69
|
+
function errore(codice, messaggio, causa) {
|
|
70
|
+
const e = new Error(messaggio);
|
|
71
|
+
e.codice = codice;
|
|
72
|
+
if (causa !== undefined) e.causa = causa;
|
|
73
|
+
return e;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Toglie i doppioni tenendo il PRIMO: l'ordine dei candidati È la preferenza. */
|
|
77
|
+
function senzaDoppioni(elenco) {
|
|
78
|
+
const visti = new Set();
|
|
79
|
+
const fuori = [];
|
|
80
|
+
for (const voce of elenco) {
|
|
81
|
+
const v = String(voce || '').trim();
|
|
82
|
+
if (!v || visti.has(v)) continue;
|
|
83
|
+
visti.add(v);
|
|
84
|
+
fuori.push(v);
|
|
85
|
+
}
|
|
86
|
+
return fuori;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Il canale si legge dal PERCORSO. Edge si controlla PER PRIMO: il suo percorso
|
|
91
|
+
* su macOS è «Microsoft Edge.app/Contents/MacOS/Microsoft Edge» e non contiene
|
|
92
|
+
* «chrome», ma un domani un canale Dev potrebbe contenerlo — l'ordine dei
|
|
93
|
+
* controlli è la difesa.
|
|
94
|
+
* @param {string} percorso
|
|
95
|
+
* @returns {'chrome'|'edge'|'chromium'}
|
|
96
|
+
*/
|
|
97
|
+
export function canaleDaPercorso(percorso) {
|
|
98
|
+
const p = String(percorso || '').toLowerCase();
|
|
99
|
+
if (p.includes('msedge') || p.includes('microsoft edge') || p.includes('microsoft-edge')) return 'edge';
|
|
100
|
+
if (p.includes('chromium')) return 'chromium';
|
|
101
|
+
if (p.includes('chrome')) return 'chrome';
|
|
102
|
+
return 'chromium';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* I percorsi da provare, IN ORDINE di preferenza: prima quello dichiarato
|
|
107
|
+
* dall'ambiente (se l'owner ha un binario suo, comanda lui), poi Chrome, poi
|
|
108
|
+
* Edge, poi Chromium. ⛔ Qui non si tocca il disco: questa funzione dice solo
|
|
109
|
+
* DOVE guardare — chi guarda è `trovaChromium`.
|
|
110
|
+
* @param {string} piattaforma `process.platform`
|
|
111
|
+
* @param {Record<string,string|undefined>} ambiente `process.env`
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
export function candidatiChromium(piattaforma = process.platform, ambiente = process.env) {
|
|
115
|
+
const amb = ambiente || {};
|
|
116
|
+
const dichiarato = amb.TALOS_CHROMIUM || amb.CHROME_PATH || '';
|
|
117
|
+
const fuori = dichiarato ? [String(dichiarato)] : [];
|
|
118
|
+
|
|
119
|
+
if (piattaforma === 'win32') {
|
|
120
|
+
const radici = [amb.PROGRAMFILES, amb['PROGRAMFILES(X86)'], amb.LOCALAPPDATA].filter(Boolean);
|
|
121
|
+
/* ⛔ Edge è un binario a 64 bit e sta comunque, per default, sotto
|
|
122
|
+
«Program Files (x86)» (Microsoft Learn, 07/09/2026): la radice si prova
|
|
123
|
+
tutte e tre le volte invece di scommettere su una sola. */
|
|
124
|
+
for (const r of radici) fuori.push(`${r}\\Google\\Chrome\\Application\\chrome.exe`);
|
|
125
|
+
for (const r of radici) fuori.push(`${r}\\Microsoft\\Edge\\Application\\msedge.exe`);
|
|
126
|
+
for (const r of radici) fuori.push(`${r}\\Chromium\\Application\\chrome.exe`);
|
|
127
|
+
return senzaDoppioni(fuori);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (piattaforma === 'darwin') {
|
|
131
|
+
const casa = amb.HOME || '';
|
|
132
|
+
fuori.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
|
|
133
|
+
if (casa) fuori.push(`${casa}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`);
|
|
134
|
+
fuori.push('/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
|
|
135
|
+
if (casa) fuori.push(`${casa}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`);
|
|
136
|
+
fuori.push('/Applications/Chromium.app/Contents/MacOS/Chromium');
|
|
137
|
+
return senzaDoppioni(fuori);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (piattaforma === 'linux') {
|
|
141
|
+
fuori.push('/usr/bin/google-chrome-stable', '/usr/bin/google-chrome', '/opt/google/chrome/chrome');
|
|
142
|
+
fuori.push('/usr/bin/microsoft-edge-stable', '/usr/bin/microsoft-edge');
|
|
143
|
+
fuori.push('/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
|
|
144
|
+
return senzaDoppioni(fuori);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Una piattaforma che non conosciamo non riceve percorsi INVENTATI: resta il
|
|
148
|
+
solo binario dichiarato dall'ambiente, se c'è. Meglio zero candidati che
|
|
149
|
+
una lista che finge di sapere. */
|
|
150
|
+
return senzaDoppioni(fuori);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Il primo candidato che esiste DAVVERO sul disco.
|
|
155
|
+
* @param {{piattaforma?:string, ambiente?:Record<string,string|undefined>, esiste?:(percorso:string)=>boolean}} deps
|
|
156
|
+
* @returns {{percorso:string, canale:'chrome'|'edge'|'chromium'}|null}
|
|
157
|
+
*/
|
|
158
|
+
export function trovaChromium({ piattaforma = process.platform, ambiente = process.env, esiste } = {}) {
|
|
159
|
+
/*
|
|
160
|
+
* ⛔⛔ 07/9 — qui il ripiego era `() => false`: senza il parametro `esiste` la funzione
|
|
161
|
+
* rispondeva SEMPRE «nessun browser», cioè era INERTE PER COSTRUZIONE fuori dai test. Trovato
|
|
162
|
+
* dal vivo al primo giro vero: Chrome era al suo posto
|
|
163
|
+
* (`C:\Program Files\Google\Chrome\Application\chrome.exe`) e la app diceva di non trovarlo.
|
|
164
|
+
* È la stessa famiglia del cancello semantico spento da sempre: un ripiego che non guarda
|
|
165
|
+
* niente supera ogni prova finché nessuno gli chiede la verità.
|
|
166
|
+
* ⇒ Il ripiego è la domanda VERA al disco; l'iniezione resta, e serve ai test.
|
|
167
|
+
*/
|
|
168
|
+
const guarda = typeof esiste === 'function' ? esiste : ((dove) => existsSync(dove));
|
|
169
|
+
for (const percorso of candidatiChromium(piattaforma, ambiente)) {
|
|
170
|
+
let c = false;
|
|
171
|
+
try { c = Boolean(guarda(percorso)); } catch { c = false; }
|
|
172
|
+
if (c) return { percorso, canale: canaleDaPercorso(percorso) };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Gli argomenti di un Chromium PILOTATO nel 2026 (fonte: chrome-launcher,
|
|
179
|
+
* `chrome-flags-for-tools.md`, letto il 07/09/2026).
|
|
180
|
+
*
|
|
181
|
+
* ⛔ Quello che NON c'è, e perché:
|
|
182
|
+
* · `--no-sandbox` — la documentazione di chrome-launcher lo dichiara «not
|
|
183
|
+
* recommended», e qui la pagina caricata è contenuto ostile per definizione:
|
|
184
|
+
* la sandbox è esattamente il muro che ci serve. Non si spegne, mai.
|
|
185
|
+
* · `--remote-allow-origins=*` — servirebbe solo a un client che manda
|
|
186
|
+
* l'intestazione `Origin` nella stretta di mano (chromium 40096993, il 403
|
|
187
|
+
* dal Chrome 111). Il nostro client è `ws` da Node e NON manda `Origin`;
|
|
188
|
+
* metterlo a `*` aprirebbe l'endpoint di debug a QUALUNQUE pagina web aperta
|
|
189
|
+
* in quel browser — cioè al contenuto non affidabile che stiamo isolando.
|
|
190
|
+
* · `--disable-dev-shm-usage` — è una cura per `/dev/shm` piccola dentro
|
|
191
|
+
* Docker; qui il browser gira sul computer della persona.
|
|
192
|
+
* · `--headless=new` — ⛔ 08/09/2026, owner: «non si devono aprire schede chrome in bg». Fino a
|
|
193
|
+
* oggi qui c'era scritto il contrario («la finestra serve viva») ed era una premessa mai
|
|
194
|
+
* misurata: una finestra vera spuntava sullo schermo della persona, e restava li' mentre lei
|
|
195
|
+
* lavorava. MISURATO prima di cambiarla (C35 nello scratchpad): stessa pagina animata, 6 secondi,
|
|
196
|
+
* **599 fotogrammi** con la finestra davanti, **599** con la finestra coperta da un'altra e
|
|
197
|
+
* **599** in headless. Il compositore produce lo stesso, e lo screencast li prende lo stesso.
|
|
198
|
+
* ⇒ la finestra non serviva a vedere: serviva solo a disturbare.
|
|
199
|
+
*
|
|
200
|
+
* @param {{cartellaProfilo:string, porta?:number}} opzioni
|
|
201
|
+
* @returns {string[]}
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* Lo user agent di Chrome senza la parola «Headless»: in headless Chrome la aggiunge da se', e
|
|
205
|
+
* alcuni siti servono una pagina diversa (o un blocco) a chi la porta. Non si finge un altro
|
|
206
|
+
* browser — versione, sistema e motore restano quelli veri: si toglie solo l'etichetta.
|
|
207
|
+
*/
|
|
208
|
+
export function userAgentSenzaHeadless(agente = AGENTE_PREDEFINITO) {
|
|
209
|
+
return String(agente).replace(/HeadlessChrome/gi, 'Chrome');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const AGENTE_PREDEFINITO = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
|
|
213
|
+
|
|
214
|
+
export function argomentiChromium({ cartellaProfilo, porta = 0 } = {}) {
|
|
215
|
+
if (!cartellaProfilo) throw errore('BROWSER_VIVO_PROFILO_MANCANTE', 'Serve la cartella del profilo: il browser pilotato non usa mai quello personale');
|
|
216
|
+
return [
|
|
217
|
+
/* ⛔ Porta 0: la sceglie Chrome e la leggiamo dalla riga su stderr. Una
|
|
218
|
+
porta fissa è un conflitto che prima o poi capita — due sessioni, o un
|
|
219
|
+
altro strumento che l'ha già presa. */
|
|
220
|
+
`--remote-debugging-port=${porta}`,
|
|
221
|
+
/* Il profilo è NOSTRO. È questo flag, non una promessa, a tenere fuori i
|
|
222
|
+
cookie e le sessioni della persona. */
|
|
223
|
+
`--user-data-dir=${cartellaProfilo}`,
|
|
224
|
+
/* Niente primo avvio, niente «vuoi Chrome come predefinito», niente app di
|
|
225
|
+
default installate al volo. */
|
|
226
|
+
'--no-first-run',
|
|
227
|
+
'--no-default-browser-check',
|
|
228
|
+
'--disable-default-apps',
|
|
229
|
+
/* Niente finestre di aggiornamento né traffico di componenti. */
|
|
230
|
+
'--disable-component-update',
|
|
231
|
+
'--disable-sync',
|
|
232
|
+
/* Niente ripristino di sessione: un profilo pilotato che si riapre con le
|
|
233
|
+
schede di ieri è uno stato che nessuno ha chiesto. */
|
|
234
|
+
'--disable-session-crashed-bubble',
|
|
235
|
+
'--hide-crash-restore-bubble',
|
|
236
|
+
/* ⛔ Senza questi tre, una finestra coperta o in secondo piano viene
|
|
237
|
+
strozzata dal browser e ogni misura di fluidità diventa falsa (lezione
|
|
238
|
+
già pagata: `requestAnimationFrame` a ~1/s su una finestra occlusa). */
|
|
239
|
+
'--disable-background-timer-throttling',
|
|
240
|
+
'--disable-backgrounding-occluded-windows',
|
|
241
|
+
'--disable-renderer-backgrounding',
|
|
242
|
+
/* Niente estensioni (il profilo è nuovo, ma un profilo aziendale con
|
|
243
|
+
policy potrebbe iniettarle) e niente richieste di permesso che bloccano
|
|
244
|
+
l'agente aspettando un clic umano. */
|
|
245
|
+
'--disable-extensions',
|
|
246
|
+
'--deny-permission-prompts',
|
|
247
|
+
'--disable-notifications',
|
|
248
|
+
'--disable-features=Translate,MediaRouter,OptimizationHints',
|
|
249
|
+
/* ⛔ Nessuna finestra sullo schermo di chi lavora. `--headless=new` e' lo stesso browser
|
|
250
|
+
completo (stesso rendering, stesse estensioni possibili, stesso compositore): dal 132 il
|
|
251
|
+
vecchio headless non esiste piu' e `--headless` da solo significa gia' questo, ma il nome
|
|
252
|
+
esplicito dice a chi legge QUALE dei due si intende. */
|
|
253
|
+
'--headless=new',
|
|
254
|
+
/* ⛔ In headless lo user agent contiene «HeadlessChrome» e alcuni siti lo rifiutano o servono
|
|
255
|
+
una pagina diversa: la lettura sarebbe di un'altra pagina rispetto a quella che la persona
|
|
256
|
+
vede nel suo browser. Si toglie la parola, non si finge un altro browser. */
|
|
257
|
+
`--user-agent=${userAgentSenzaHeadless()}`,
|
|
258
|
+
/* Si parte da una pagina vuota: la «nuova scheda» di Chrome fa rete e
|
|
259
|
+
mostra contenuti che non c'entrano niente con noi. */
|
|
260
|
+
'about:blank',
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Avvia il browser e risolve quando il server CDP RISPONDE DAVVERO — cioè
|
|
266
|
+
* quando la riga «DevTools listening on ws://…» è arrivata su stderr. Prima di
|
|
267
|
+
* quella riga il processo esiste ma non è pilotabile: dire «avviato» lì
|
|
268
|
+
* sarebbe dire una cosa che non è vera.
|
|
269
|
+
*
|
|
270
|
+
* @param {{percorso:string, cartellaProfilo:string, avvia?:Function, attesaMs?:number, creaCartella?:Function, terminaAlbero?:Function}} opzioni
|
|
271
|
+
* @returns {Promise<{pid:number|null, wsUrl:string, percorso:string, chiudi:()=>Promise<{chiuso:boolean, modo:string}>, processo:object}>}
|
|
272
|
+
*/
|
|
273
|
+
export async function avviaBrowserVivo({
|
|
274
|
+
percorso,
|
|
275
|
+
cartellaProfilo,
|
|
276
|
+
avvia,
|
|
277
|
+
attesaMs = ATTESA_AVVIO_MS,
|
|
278
|
+
creaCartella = (dove) => mkdirSync(dove, { recursive: true }),
|
|
279
|
+
terminaAlbero = terminaAlberoDiSistema,
|
|
280
|
+
graziaMs = GRAZIA_CHIUSURA_MS,
|
|
281
|
+
} = {}) {
|
|
282
|
+
if (!percorso) throw errore('BROWSER_VIVO_SENZA_BINARIO', 'Nessun Chromium da avviare: manca il percorso del binario');
|
|
283
|
+
if (!cartellaProfilo) throw errore('BROWSER_VIVO_PROFILO_MANCANTE', 'Serve la cartella del profilo: il browser pilotato non usa mai quello personale');
|
|
284
|
+
|
|
285
|
+
/* La cartella del profilo si crea se manca: Chrome la creerebbe da sé, ma
|
|
286
|
+
allora un errore di permessi arriverebbe come «il browser non è partito»
|
|
287
|
+
invece che come quello che è. */
|
|
288
|
+
try { creaCartella(cartellaProfilo); } catch (causa) {
|
|
289
|
+
throw errore('BROWSER_VIVO_PROFILO_NON_CREABILE', `Non riesco a creare la cartella del profilo (${cartellaProfilo})`, causa);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const argomenti = argomentiChromium({ cartellaProfilo });
|
|
293
|
+
const lancia = typeof avvia === 'function' ? avvia : avviaDiSistema;
|
|
294
|
+
/* ⛔ D1: l'ambiente del browser NON è quello del server — vedi `opzioniAvvioBrowser`. */
|
|
295
|
+
const opzioni = opzioniAvvioBrowser();
|
|
296
|
+
let processo;
|
|
297
|
+
try { processo = lancia(percorso, argomenti, opzioni); } catch (causa) {
|
|
298
|
+
throw errore('BROWSER_VIVO_NON_PARTE', `Non riesco ad avviare il browser (${percorso})`, causa);
|
|
299
|
+
}
|
|
300
|
+
if (!processo || !processo.stderr) {
|
|
301
|
+
throw errore('BROWSER_VIVO_SENZA_STDERR', 'Il browser è partito senza stderr: la porta di debug si legge solo da lì');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const chiudi = creaChiusura(processo, terminaAlbero, graziaMs);
|
|
305
|
+
let wsUrl;
|
|
306
|
+
try {
|
|
307
|
+
wsUrl = await attendiWsUrl(processo, attesaMs);
|
|
308
|
+
} catch (causa) {
|
|
309
|
+
/* ⛔ Se non è arrivato l'indirizzo, il processo NON si lascia in giro: un
|
|
310
|
+
Chromium orfano con un profilo nostro è un consumo che nessuno vedrà. */
|
|
311
|
+
await chiudi().catch(() => {});
|
|
312
|
+
throw causa;
|
|
313
|
+
}
|
|
314
|
+
return { pid: processo.pid ?? null, wsUrl, percorso, processo, chiudi };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* ⛔⛔⛔ D1 del secondo giro CLI-REQ (17/09/2026), autorizzato dall'owner: «la stessa cura del
|
|
319
|
+
* terminale». Fino a oggi questo `spawn` non passava `env`, e il valore predefinito di
|
|
320
|
+
* `node:child_process` è `process.env` INTERO (nodejs.org/api/child_process.html, letto il
|
|
321
|
+
* 17/09/2026) ⇒ il Chromium che TALOS avvia PER LA PERSONA nasceva col token di loopback che
|
|
322
|
+
* protegge tutta la nostra API locale, con la chiave privata che firma le ricevute e con la
|
|
323
|
+
* chiave della ricerca web. Un'estensione, o un processo figlio del browser, li leggeva.
|
|
324
|
+
*
|
|
325
|
+
* ⛔ Stesso elenco del terminale, IMPORTATO e non ricopiato: due copie divergerebbero al primo
|
|
326
|
+
* segreto nuovo, e nessuno se ne accorgerebbe (il risultato sbagliato ha lo stesso aspetto di
|
|
327
|
+
* quello giusto). La fonte unica è `ambiente-solo-server.mjs`, che sta in un file suo proprio
|
|
328
|
+
* perché `pty-terminal.mjs` importa `node-pty` — un modulo nativo che qui non serve.
|
|
329
|
+
*
|
|
330
|
+
* ⛔ Le opzioni vivono in una funzione con un nome perché la prova possa LEGGERLE: l'iniezione
|
|
331
|
+
* `avvia` riceve anche loro, quindi «quale ambiente riceve il browser» è una cosa misurabile e
|
|
332
|
+
* non una cosa che si legge nel sorgente e si spera.
|
|
333
|
+
*/
|
|
334
|
+
export function opzioniAvvioBrowser(ambienteFn = ambienteSenzaVariabiliDelServer) {
|
|
335
|
+
/* `stdio` con stderr a tubo: è la nostra unica fonte per la porta. */
|
|
336
|
+
return { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: false, env: ambienteFn() };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Il lancio vero. */
|
|
340
|
+
function avviaDiSistema(percorso, argomenti, opzioni) {
|
|
341
|
+
return spawn(percorso, argomenti, opzioni);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Legge stderr A RIGHE finché non trova l'indirizzo. ⛔ A pezzi (`on('data')`)
|
|
346
|
+
* non funzionerebbe: la riga può arrivare spezzata in due chunk.
|
|
347
|
+
*/
|
|
348
|
+
function attendiWsUrl(processo, attesaMs) {
|
|
349
|
+
return new Promise((risolvi, rifiuta) => {
|
|
350
|
+
const lettore = createInterface({ input: processo.stderr });
|
|
351
|
+
const coda = [];
|
|
352
|
+
let finito = false;
|
|
353
|
+
const chiudiLettore = () => { try { lettore.close(); } catch { /* già chiuso */ } };
|
|
354
|
+
const finisci = (fn, valore) => {
|
|
355
|
+
if (finito) return;
|
|
356
|
+
finito = true;
|
|
357
|
+
clearTimeout(scadenza);
|
|
358
|
+
chiudiLettore();
|
|
359
|
+
processo.off?.('exit', suUscita);
|
|
360
|
+
processo.off?.('error', suErrore);
|
|
361
|
+
fn(valore);
|
|
362
|
+
};
|
|
363
|
+
const scadenza = setTimeout(() => {
|
|
364
|
+
finisci(rifiuta, errore(
|
|
365
|
+
'BROWSER_VIVO_ATTESA_SCADUTA',
|
|
366
|
+
`Il browser non ha annunciato la porta di debug entro ${Math.round(attesaMs / 1000)} secondi. Ultime righe: ${coda.join(' | ') || '(nessuna)'}`,
|
|
367
|
+
));
|
|
368
|
+
}, attesaMs);
|
|
369
|
+
scadenza.unref?.();
|
|
370
|
+
|
|
371
|
+
const suUscita = (codice) => finisci(rifiuta, errore(
|
|
372
|
+
'BROWSER_VIVO_USCITO_SUBITO',
|
|
373
|
+
`Il browser è uscito (codice ${codice}) prima di annunciare la porta di debug. Ultime righe: ${coda.join(' | ') || '(nessuna)'}`,
|
|
374
|
+
));
|
|
375
|
+
const suErrore = (causa) => finisci(rifiuta, errore('BROWSER_VIVO_NON_PARTE', 'Il browser non è partito', causa));
|
|
376
|
+
processo.on?.('exit', suUscita);
|
|
377
|
+
processo.on?.('error', suErrore);
|
|
378
|
+
|
|
379
|
+
lettore.on('line', (riga) => {
|
|
380
|
+
/* Le ultime righe si tengono: se scade l'attesa, l'errore deve DIRE cosa
|
|
381
|
+
il browser stava dicendo, non solo che il tempo è finito. */
|
|
382
|
+
coda.push(String(riga).trim());
|
|
383
|
+
if (coda.length > 5) coda.shift();
|
|
384
|
+
const trovato = RIGA_DEVTOOLS.exec(String(riga));
|
|
385
|
+
if (trovato) finisci(risolvi, trovato[1]);
|
|
386
|
+
});
|
|
387
|
+
lettore.on('close', () => {
|
|
388
|
+
if (!finito) finisci(rifiuta, errore('BROWSER_VIVO_STDERR_CHIUSO', 'Lo stderr del browser si è chiuso senza annunciare la porta di debug'));
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Chiude DAVVERO. Non si chiede al browser via CDP di chiudersi: se il
|
|
395
|
+
* problema è proprio che CDP non risponde, quella strada è chiusa per
|
|
396
|
+
* definizione. Si va al processo.
|
|
397
|
+
*/
|
|
398
|
+
function creaChiusura(processo, terminaAlbero, graziaMs = GRAZIA_CHIUSURA_MS) {
|
|
399
|
+
let gia = false;
|
|
400
|
+
return async function chiudi() {
|
|
401
|
+
if (gia) return { chiuso: true, modo: 'già chiuso' };
|
|
402
|
+
gia = true;
|
|
403
|
+
if (processo.exitCode !== null && processo.exitCode !== undefined) return { chiuso: true, modo: 'era già uscito' };
|
|
404
|
+
|
|
405
|
+
const uscita = new Promise((r) => {
|
|
406
|
+
if (typeof processo.once === 'function') processo.once('exit', () => r(true));
|
|
407
|
+
else r(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
let modo = 'segnale';
|
|
411
|
+
try {
|
|
412
|
+
const fatto = await Promise.resolve(terminaAlbero(processo));
|
|
413
|
+
if (fatto) modo = 'albero';
|
|
414
|
+
} catch { /* si prosegue col segnale: un fallimento qui non è una scusa */ }
|
|
415
|
+
|
|
416
|
+
const scaduto = new Promise((r) => { const t = setTimeout(() => r(false), graziaMs); t.unref?.(); });
|
|
417
|
+
const uscitoInTempo = await Promise.race([uscita, scaduto]);
|
|
418
|
+
if (uscitoInTempo) return { chiuso: true, modo };
|
|
419
|
+
|
|
420
|
+
/* ⛔ Ultima parola: SIGKILL. Un `taskkill` che fallisce o un processo che
|
|
421
|
+
ignora il segnale non devono lasciare un browser vivo. */
|
|
422
|
+
try { processo.kill?.('SIGKILL'); } catch { /* già morto */ }
|
|
423
|
+
return { chiuso: true, modo: 'forzata' };
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Su Windows i figli di Chrome (renderer, GPU, utility) NON muoiono col padre:
|
|
429
|
+
* si uccide l'albero con `taskkill /T /F`. Altrove basta il segnale al gruppo.
|
|
430
|
+
*/
|
|
431
|
+
function terminaAlberoDiSistema(processo) {
|
|
432
|
+
const pid = processo?.pid;
|
|
433
|
+
if (!pid) return false;
|
|
434
|
+
if (process.platform === 'win32') {
|
|
435
|
+
return new Promise((r) => {
|
|
436
|
+
execFile('taskkill', ['/PID', String(pid), '/T', '/F'], () => r(true));
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
try { processo.kill('SIGTERM'); } catch { /* già morto */ }
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Il client CDP: un contatore di id, una mappa delle richieste in volo, e gli
|
|
445
|
+
* eventi smistati per nome.
|
|
446
|
+
*
|
|
447
|
+
* `socket` è un WebSocket alla maniera di `ws` (già dipendenza del backend):
|
|
448
|
+
* `send`, `close`, e `on('message'|'close'|'error')`. Nei test è un finto con
|
|
449
|
+
* la stessa forma — per questo il socket lo passa il chiamante e non lo apre
|
|
450
|
+
* questo modulo.
|
|
451
|
+
*
|
|
452
|
+
* @param {{send:Function, close?:Function, on?:Function, addEventListener?:Function}} socket
|
|
453
|
+
* @param {{attesaMs?:number}} opzioni
|
|
454
|
+
*/
|
|
455
|
+
export function creaClientCdp(socket, { attesaMs = ATTESA_CDP_MS } = {}) {
|
|
456
|
+
if (!socket || typeof socket.send !== 'function') throw errore('CDP_SOCKET_INVALIDO', 'Serve un WebSocket con «send» per parlare col browser');
|
|
457
|
+
|
|
458
|
+
let prossimoId = 1;
|
|
459
|
+
let chiuso = false;
|
|
460
|
+
const inVolo = new Map();
|
|
461
|
+
const ascoltatori = new Map();
|
|
462
|
+
|
|
463
|
+
const ascolta = (evento, cb) => {
|
|
464
|
+
if (typeof socket.on === 'function') socket.on(evento, cb);
|
|
465
|
+
else if (typeof socket.addEventListener === 'function') socket.addEventListener(evento, (e) => cb(evento === 'message' ? e.data : e));
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
function smista(messaggio) {
|
|
469
|
+
let m;
|
|
470
|
+
try { m = JSON.parse(typeof messaggio === 'string' ? messaggio : String(messaggio)); } catch { return; }
|
|
471
|
+
if (m && m.id !== undefined && inVolo.has(m.id)) {
|
|
472
|
+
const attesa = inVolo.get(m.id);
|
|
473
|
+
inVolo.delete(m.id);
|
|
474
|
+
clearTimeout(attesa.scadenza);
|
|
475
|
+
if (m.error) {
|
|
476
|
+
/* ⛔ L'errore del browser si riporta INTERO: il codice e il messaggio
|
|
477
|
+
veri. Un «comando fallito» generico costringe chi legge a indovinare. */
|
|
478
|
+
attesa.rifiuta(errore('CDP_ERRORE', `${attesa.metodo}: ${m.error.message || 'errore senza messaggio'} (codice ${m.error.code ?? 'ignoto'})`, m.error));
|
|
479
|
+
} else {
|
|
480
|
+
attesa.risolvi(m.result ?? {});
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (m && m.method) {
|
|
485
|
+
const contesto = { sessionId: m.sessionId ?? null, metodo: m.method };
|
|
486
|
+
for (const cb of ascoltatori.get(m.method) || []) { try { cb(m.params ?? {}, contesto); } catch { /* un ascoltatore rotto non ferma gli altri */ } }
|
|
487
|
+
for (const cb of ascoltatori.get('*') || []) { try { cb(m.params ?? {}, contesto); } catch { /* idem */ } }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function abbandonaTutti(motivo) {
|
|
492
|
+
for (const [, attesa] of inVolo) {
|
|
493
|
+
clearTimeout(attesa.scadenza);
|
|
494
|
+
attesa.rifiuta(errore('CDP_SOCKET_CHIUSO', `${attesa.metodo}: ${motivo}`));
|
|
495
|
+
}
|
|
496
|
+
inVolo.clear();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
ascolta('message', smista);
|
|
500
|
+
ascolta('close', () => { chiuso = true; abbandonaTutti('la connessione col browser si è chiusa'); });
|
|
501
|
+
ascolta('error', () => { chiuso = true; abbandonaTutti('la connessione col browser è andata in errore'); });
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @param {string} metodo es. `Page.navigate`
|
|
505
|
+
* @param {object} parametri
|
|
506
|
+
* @param {string|null} sessionId ⛔ in modo «flat» il sessionId viaggia in
|
|
507
|
+
* OGNI messaggio destinato a una scheda: senza, il comando finisce al
|
|
508
|
+
* browser invece che alla pagina.
|
|
509
|
+
*/
|
|
510
|
+
function invia(metodo, parametri = {}, sessionId = null) {
|
|
511
|
+
if (chiuso) return Promise.reject(errore('CDP_SOCKET_CHIUSO', `${metodo}: la connessione col browser è già chiusa`));
|
|
512
|
+
const id = prossimoId++;
|
|
513
|
+
const messaggio = { id, method: metodo, params: parametri || {} };
|
|
514
|
+
if (sessionId) messaggio.sessionId = sessionId;
|
|
515
|
+
return new Promise((risolvi, rifiuta) => {
|
|
516
|
+
const scadenza = setTimeout(() => {
|
|
517
|
+
inVolo.delete(id);
|
|
518
|
+
rifiuta(errore('CDP_ATTESA_SCADUTA', `${metodo}: nessuna risposta dal browser entro ${Math.round(attesaMs / 1000)} secondi`));
|
|
519
|
+
}, attesaMs);
|
|
520
|
+
scadenza.unref?.();
|
|
521
|
+
inVolo.set(id, { risolvi, rifiuta, scadenza, metodo });
|
|
522
|
+
try {
|
|
523
|
+
socket.send(JSON.stringify(messaggio));
|
|
524
|
+
} catch (causa) {
|
|
525
|
+
inVolo.delete(id);
|
|
526
|
+
clearTimeout(scadenza);
|
|
527
|
+
rifiuta(errore('CDP_INVIO_FALLITO', `${metodo}: non riesco a scrivere sulla connessione col browser`, causa));
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Ritorna la funzione per STACCARSI: un ascoltatore che nessuno può togliere è una perdita. */
|
|
533
|
+
function su(evento, cb) {
|
|
534
|
+
if (!ascoltatori.has(evento)) ascoltatori.set(evento, new Set());
|
|
535
|
+
ascoltatori.get(evento).add(cb);
|
|
536
|
+
return () => { ascoltatori.get(evento)?.delete(cb); };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function chiudiClient() {
|
|
540
|
+
chiuso = true;
|
|
541
|
+
abbandonaTutti('il client è stato chiuso');
|
|
542
|
+
ascoltatori.clear();
|
|
543
|
+
try { socket.close?.(); } catch { /* un socket già morto non si richiude */ }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { invia, su, chiudi: chiudiClient };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Apre una scheda vuota e ci si aggancia.
|
|
551
|
+
*
|
|
552
|
+
* ⛔ `flatten: true` è obbligatorio, non un'opzione. La documentazione del
|
|
553
|
+
* dominio Target (letta il 07/09/2026) dice testualmente: «Enables "flat"
|
|
554
|
+
* access to the session via specifying sessionId attribute in the commands.
|
|
555
|
+
* We plan to make this the default, deprecate non-flattened mode, and
|
|
556
|
+
* eventually retire it» (crbug 991325). Nel modo vecchio i messaggi della
|
|
557
|
+
* scheda viaggiavano incapsulati dentro `Target.sendMessageToTarget` e gli
|
|
558
|
+
* eventi dentro `Target.receivedMessageFromTarget`: una busta dentro la busta.
|
|
559
|
+
* Nel modo flat il `sessionId` sta in cima a ogni messaggio — ed è così che il
|
|
560
|
+
* resto di questo modulo (e i quattro moduli che ci si appoggiano) parla con
|
|
561
|
+
* la pagina.
|
|
562
|
+
*
|
|
563
|
+
* @param {{invia:Function}} cdp
|
|
564
|
+
* @returns {Promise<{targetId:string, sessionId:string}>}
|
|
565
|
+
*/
|
|
566
|
+
export async function apriSchedaVuota(cdp, { url = 'about:blank', browserContextId = null } = {}) {
|
|
567
|
+
/*
|
|
568
|
+
* ⛔ 07/9 — `browserContextId` non è un dettaglio: le schede aperte nel contesto PREDEFINITO si
|
|
569
|
+
* scambiano cookie, localStorage e sessionStorage, quindi due sessioni di TALOS che aprono due
|
|
570
|
+
* pagine si vedrebbero i dati a vicenda. Il contesto è un confine di STORAGE (non di finestra:
|
|
571
|
+
* schede di contesti diversi convivono nella stessa finestra). Chi apre la scheda decide se
|
|
572
|
+
* isolarla; qui si passa solo ciò che il chiamante ha chiesto.
|
|
573
|
+
* Ricerca 07/09/2026: vercel-labs/agent-browser #1068, guida CDP di browser-use.
|
|
574
|
+
*/
|
|
575
|
+
const creata = await cdp.invia('Target.createTarget', browserContextId ? { url, browserContextId } : { url });
|
|
576
|
+
const targetId = creata?.targetId;
|
|
577
|
+
if (!targetId) throw errore('BROWSER_VIVO_SCHEDA_SENZA_ID', 'Il browser ha aperto una scheda senza dirci il suo identificativo');
|
|
578
|
+
const agganciata = await cdp.invia('Target.attachToTarget', { targetId, flatten: true });
|
|
579
|
+
const sessionId = agganciata?.sessionId;
|
|
580
|
+
if (!sessionId) throw errore('BROWSER_VIVO_SESSIONE_MANCANTE', 'Il browser non ha assegnato una sessione alla scheda: senza sessionId non si può pilotare');
|
|
581
|
+
return { targetId, sessionId };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Traduce i `net::ERR_…` di Chrome in una frase che una persona può leggere. */
|
|
585
|
+
function motivoDiRete(testo) {
|
|
586
|
+
const t = String(testo || '');
|
|
587
|
+
if (/ERR_NAME_NOT_RESOLVED/.test(t)) return 'Il nome del sito non esiste';
|
|
588
|
+
if (/ERR_CONNECTION_REFUSED/.test(t)) return 'Nessuno risponde a quell\'indirizzo';
|
|
589
|
+
if (/ERR_CONNECTION_TIMED_OUT|ERR_TIMED_OUT/.test(t)) return 'Il sito non ha risposto in tempo';
|
|
590
|
+
if (/ERR_CERT|ERR_SSL/.test(t)) return 'Il certificato del sito non è valido';
|
|
591
|
+
if (/ERR_ABORTED/.test(t)) return 'Il caricamento è stato interrotto';
|
|
592
|
+
if (/ERR_BLOCKED_BY/.test(t)) return 'Il browser ha bloccato la pagina';
|
|
593
|
+
return t || 'La pagina non si è caricata';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Porta la scheda a un indirizzo e dice com'è andata DAVVERO.
|
|
598
|
+
*
|
|
599
|
+
* ⛔ `Page.navigate` non riporta lo stato HTTP: da solo direbbe «ok» anche su
|
|
600
|
+
* un 404 o un 500 (la pagina d'errore del sito È una pagina che si carica —
|
|
601
|
+
* è lo stesso inganno per cui la cornice `<iframe>` non si accorgeva del
|
|
602
|
+
* fallimento). Perciò si accende anche `Network`, si prende lo stato dalla
|
|
603
|
+
* risposta del documento principale e si aspetta che il frame smetta di
|
|
604
|
+
* caricare.
|
|
605
|
+
*
|
|
606
|
+
* Il filtro sugli indirizzi è quello che il repo ha già, `urlAmmesso` di
|
|
607
|
+
* `browser-frame.mjs`: solo `http:`/`https:` (fuori `file:`, `chrome:`,
|
|
608
|
+
* `devtools:`, `view-source:` — con cui una pagina ostile leggerebbe il disco
|
|
609
|
+
* o si prenderebbe il browser) e mai credenziali nell'indirizzo. ⛔ Loopback e
|
|
610
|
+
* indirizzi privati restano AMMESSI di proposito: `localhost:5173` è il motivo
|
|
611
|
+
* per cui questo browser esiste — sta dove sta l'agente, ed è il vantaggio che
|
|
612
|
+
* un gateway in cloud non può avere.
|
|
613
|
+
*
|
|
614
|
+
* @param {{invia:Function, su:Function}} cdp
|
|
615
|
+
* @param {string} sessionId
|
|
616
|
+
* @param {string} url
|
|
617
|
+
* @returns {Promise<{ok:boolean, stato:number|null, errore:string|null, url:string}>}
|
|
618
|
+
*/
|
|
619
|
+
export async function vaiA(cdp, sessionId, url, { attesaMs = ATTESA_CARICAMENTO_MS } = {}) {
|
|
620
|
+
let indirizzo;
|
|
621
|
+
try { indirizzo = new URL(String(url)); } catch {
|
|
622
|
+
return { ok: false, stato: null, errore: 'URL non valido', url: String(url) };
|
|
623
|
+
}
|
|
624
|
+
const ammesso = urlAmmesso(indirizzo);
|
|
625
|
+
if (!ammesso.ok) return { ok: false, stato: null, errore: ammesso.motivo, url: indirizzo.href };
|
|
626
|
+
|
|
627
|
+
await cdp.invia('Page.enable', {}, sessionId);
|
|
628
|
+
await cdp.invia('Network.enable', {}, sessionId);
|
|
629
|
+
|
|
630
|
+
/* Gli stati dei documenti si raccolgono PRIMA di navigare: il frameId lo
|
|
631
|
+
conosciamo solo dopo, e la risposta può arrivare prima che `Page.navigate`
|
|
632
|
+
abbia risolto. */
|
|
633
|
+
const statiPerFrame = new Map();
|
|
634
|
+
const staccaRisposta = cdp.su('Network.responseReceived', (p) => {
|
|
635
|
+
if (p?.type === 'Document' && p?.frameId) statiPerFrame.set(p.frameId, p?.response?.status ?? null);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
let fine;
|
|
639
|
+
const caricata = new Promise((r) => { fine = r; });
|
|
640
|
+
let frameAtteso = null;
|
|
641
|
+
/* ⛔ La fine del caricamento può arrivare PRIMA che `Page.navigate` ci abbia
|
|
642
|
+
detto quale riquadro guardare: senza memoria, quel caso resterebbe in
|
|
643
|
+
attesa fino allo scadere del tetto e verrebbe raccontato come «lenta». */
|
|
644
|
+
const fermiVisti = new Set();
|
|
645
|
+
let loadVisto = false;
|
|
646
|
+
const controlla = (p) => {
|
|
647
|
+
if (p?.frameId) fermiVisti.add(p.frameId);
|
|
648
|
+
if (frameAtteso && p?.frameId === frameAtteso) fine('caricata');
|
|
649
|
+
};
|
|
650
|
+
const staccaFermo = cdp.su('Page.frameStoppedLoading', controlla);
|
|
651
|
+
const staccaLoad = cdp.su('Page.loadEventFired', () => { loadVisto = true; if (frameAtteso) fine('caricata'); });
|
|
652
|
+
|
|
653
|
+
const smonta = () => { staccaRisposta(); staccaFermo(); staccaLoad(); };
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const esito = await cdp.invia('Page.navigate', { url: indirizzo.href }, sessionId);
|
|
657
|
+
if (esito?.errorText) {
|
|
658
|
+
return { ok: false, stato: null, errore: motivoDiRete(esito.errorText), url: indirizzo.href };
|
|
659
|
+
}
|
|
660
|
+
frameAtteso = esito?.frameId || null;
|
|
661
|
+
if (!frameAtteso) return { ok: false, stato: null, errore: 'Il browser non ha detto quale riquadro sta caricando', url: indirizzo.href };
|
|
662
|
+
if (fermiVisti.has(frameAtteso) || loadVisto) fine('caricata');
|
|
663
|
+
/* Se la risposta era già arrivata mentre navigavamo, non si aspetta a vuoto. */
|
|
664
|
+
const scaduta = new Promise((r) => { const t = setTimeout(() => r('scaduta'), attesaMs); t.unref?.(); });
|
|
665
|
+
const come = await Promise.race([caricata, scaduta]);
|
|
666
|
+
const stato = statiPerFrame.has(frameAtteso) ? statiPerFrame.get(frameAtteso) : null;
|
|
667
|
+
if (come === 'scaduta') {
|
|
668
|
+
return { ok: false, stato, errore: `La pagina non ha finito di caricare entro ${Math.round(attesaMs / 1000)} secondi`, url: indirizzo.href };
|
|
669
|
+
}
|
|
670
|
+
/* ⛔ Uno stato HTTP di errore È un fallimento, anche se il documento si è
|
|
671
|
+
caricato: è esattamente il caso che la cornice non sapeva vedere. */
|
|
672
|
+
if (typeof stato === 'number' && stato >= 400) {
|
|
673
|
+
return { ok: false, stato, errore: `Il sito ha risposto ${stato}`, url: indirizzo.href };
|
|
674
|
+
}
|
|
675
|
+
return { ok: true, stato, errore: null, url: indirizzo.href };
|
|
676
|
+
} catch (causa) {
|
|
677
|
+
return { ok: false, stato: null, errore: causa?.message || 'Il browser non ha eseguito la navigazione', url: indirizzo.href };
|
|
678
|
+
} finally {
|
|
679
|
+
smonta();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* La riga che Chrome stampa su stderr → l'indirizzo del server CDP, o `null`.
|
|
685
|
+
* ⛔ Solo `ws://`: una riga che parla d'altro non è un indirizzo, e restituire
|
|
686
|
+
* qualcosa «che ci somiglia» qui vorrebbe dire fallire più avanti, dove è più
|
|
687
|
+
* difficile capire perché.
|
|
688
|
+
* @param {string} riga
|
|
689
|
+
* @returns {string|null}
|
|
690
|
+
*/
|
|
691
|
+
export function wsUrlDaRiga(riga) {
|
|
692
|
+
const trovato = RIGA_DEVTOOLS.exec(String(riga ?? ''));
|
|
693
|
+
return trovato ? trovato[1] : null;
|
|
694
|
+
}
|