talos-code 0.0.1 → 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/LICENSE +661 -0
- package/README.md +111 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +97 -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 +185 -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 +43 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +82 -0
- package/dist/i18n/en/errors.js +246 -0
- package/dist/i18n/en/firstrun.js +89 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +736 -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 +330 -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/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-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 +262 -0
- package/dist/runtime/talos-composition.js +1006 -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/app.js +3761 -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 +559 -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 +32 -0
- package/dist/tui/components/terminal-shell.js +205 -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/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 +139 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +45 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +39 -0
- package/dist/tui/overlays/approval-dialog.js +640 -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 +72 -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 +145 -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 +287 -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 +778 -0
- package/dist/tui/session-export.js +54 -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 +80 -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 +25 -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 +63 -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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accesso locale minimale al filesystem di un workspace.
|
|
3
|
+
*
|
|
4
|
+
* Questo adapter sostituisce l'import da repository esterni usato in
|
|
5
|
+
* precedenza dall'albero file. Conosce soltanto l'elenco di un livello:
|
|
6
|
+
* nessuna ricorsione, nessun comando di sistema e nessuna trasformazione del
|
|
7
|
+
* contenuto. Le policy di percorso restano esplicite e verificabili qui.
|
|
8
|
+
*/
|
|
9
|
+
import { readdir } from 'node:fs/promises';
|
|
10
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export class WorkspaceDiskError extends Error {
|
|
13
|
+
constructor(message, code = 'DIRECTORY_UNREADABLE') {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'WorkspaceDiskError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function percorsoRelativoValido(percorso) {
|
|
21
|
+
return typeof percorso === 'string'
|
|
22
|
+
&& !percorso.includes('\0')
|
|
23
|
+
&& !isAbsolute(percorso);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dentroRadice(radice, candidato) {
|
|
27
|
+
const differenza = relative(radice, candidato);
|
|
28
|
+
return differenza === ''
|
|
29
|
+
|| (differenza !== '..' && !differenza.startsWith(`..${sep}`) && !isAbsolute(differenza));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{rootDir?:string, root?:string, fsImpl?:{readdir?:typeof readdir}, readdirFn?:typeof readdir}} input
|
|
34
|
+
* @returns {{elenca:(percorso?:string)=>Promise<Array<{nome:string, cartella:boolean}>>}}
|
|
35
|
+
*/
|
|
36
|
+
export function createWorkspaceDisk({ rootDir, root = rootDir, fsImpl = {}, readdirFn = fsImpl.readdir ?? readdir } = {}) {
|
|
37
|
+
if (typeof root !== 'string' || root.length === 0 || root.includes('\0') || !isAbsolute(root)) {
|
|
38
|
+
throw new WorkspaceDiskError('Cartella workspace non valida', 'QUERY_INVALID');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const radice = resolve(root);
|
|
42
|
+
return {
|
|
43
|
+
async elenca(percorso = '') {
|
|
44
|
+
if (!percorsoRelativoValido(percorso)) {
|
|
45
|
+
throw new WorkspaceDiskError('Percorso non valido', 'QUERY_INVALID');
|
|
46
|
+
}
|
|
47
|
+
const candidato = resolve(radice, percorso);
|
|
48
|
+
if (!dentroRadice(radice, candidato)) {
|
|
49
|
+
throw new WorkspaceDiskError('Percorso fuori dalla cartella della sessione', 'QUERY_INVALID');
|
|
50
|
+
}
|
|
51
|
+
let voci;
|
|
52
|
+
try {
|
|
53
|
+
voci = await readdirFn(candidato, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
throw new WorkspaceDiskError('Cartella non leggibile');
|
|
56
|
+
}
|
|
57
|
+
return voci
|
|
58
|
+
.map((voce) => ({ nome: voce.name, cartella: voce.isDirectory() }))
|
|
59
|
+
.sort((a, b) => (a.cartella === b.cartella ? a.nome.localeCompare(b.nome, 'en') : a.cartella ? -1 : 1));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-files.mjs — le azioni sul singolo file dell'albero workspace
|
|
3
|
+
* che oggi mancano (piano `elegant-spinning-dongarra.md`, owner 27/8:
|
|
4
|
+
* "Non ha nessun'opzione per rinominare i file, per aprire i file, per
|
|
5
|
+
* aprirli nel visualizza file explorer di Windows. Non ha opzioni per
|
|
6
|
+
* eliminarlo, per allegarlo nella chat").
|
|
7
|
+
*
|
|
8
|
+
* ⛔ Deliberatamente FUORI da `talosHarness.mjs`: queste sono azioni
|
|
9
|
+
* dell'OWNER sull'albero (mai un tool-call del modello), quindi non
|
|
10
|
+
* hanno bisogno di passare dal kernel benchmarkato — `discoNode` espone
|
|
11
|
+
* solo `elenca`/`leggi`/`scrivi` (verificato leggendo il sorgente prima
|
|
12
|
+
* di scrivere questo file, non presunto), niente rinomina/elimina.
|
|
13
|
+
* Aggiungerle lì avrebbe richiesto un'altra ri-misura TALOS-BANCO per
|
|
14
|
+
* un bisogno che non tocca affatto il banco.
|
|
15
|
+
*
|
|
16
|
+
* ⛔⛔ Stessa disciplina di path-policy.mjs/workspace-tree.mjs: ogni
|
|
17
|
+
* percorso arriva da una richiesta HTTP, non da un modello cooperativo
|
|
18
|
+
* — `isPathInside` (path-policy.mjs, riusato non duplicato) valida
|
|
19
|
+
* SEMPRE il percorso REALE (dopo aver risolto eventuali symlink) contro
|
|
20
|
+
* la radice reale della sessione, mai la stringa grezza.
|
|
21
|
+
*/
|
|
22
|
+
import { promises as fsp, realpathSync } from 'node:fs';
|
|
23
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { isPathInside } from './path-policy.mjs';
|
|
26
|
+
import { createProcessPolicy } from './process-policy.mjs';
|
|
27
|
+
|
|
28
|
+
/* BC-64 (17/09): Esplora file è l'UNICO programma che lanciamo perché la persona lo VEDA — con la finestra
|
|
29
|
+
nascosta «Rivela in Esplora file» riusciva e non mostrava niente (misura in `process-policy.mjs`). */
|
|
30
|
+
export const EXPLORER_PROCESS_POLICY = createProcessPolicy({ allowedExecutables: ['explorer.exe', 'explorer'], finestreVisibili: true });
|
|
31
|
+
|
|
32
|
+
export class WorkspaceFileError extends Error {
|
|
33
|
+
constructor(message, code = 'QUERY_INVALID') {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'WorkspaceFileError';
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** ⛔ 512 KB: un'anteprima, non un editor — file più grandi si dichiarano troppo grandi invece di essere troncati in silenzio. */
|
|
41
|
+
const DIMENSIONE_MASSIMA_ANTEPRIMA = 512 * 1024;
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* ⛔⛔ 11/09/2026 — `ammettiRadice` nasce dall'unica azione che sulla RADICE ha senso: «Apri in
|
|
45
|
+
* Esplora file» (owner, dal vivo: «se faccio tasto destro sulla ROOT deve poter spuntare "Apri",
|
|
46
|
+
* cioè devo poterla aprire su Windows»). Tutte le altre — rinomina, elimina, copia, sposta —
|
|
47
|
+
* restano vietate sulla radice, ed è per questo che il divieto NON è stato tolto ma reso una
|
|
48
|
+
* scelta di chi chiama: il valore di default è ancora `false`, quindi nessuna chiamata già
|
|
49
|
+
* scritta cambia comportamento.
|
|
50
|
+
* ⛔ Quello che NON si allenta mai: `realpath` prima del confronto (un collegamento simbolico che
|
|
51
|
+
* punta fuori viene canonicalizzato e respinto da `isPathInside`), niente percorsi assoluti,
|
|
52
|
+
* niente `\0`. Un `..` che risale sopra la radice cade su `isPathInside`, esattamente come prima.
|
|
53
|
+
*/
|
|
54
|
+
function risolviPercorsoEsistente(cartella, percorso, { realpathSyncFn = realpathSync } = {}, ammettiRadice = false) {
|
|
55
|
+
const vuoto = percorso === '';
|
|
56
|
+
if (typeof percorso !== 'string' || percorso.includes('\0') || isAbsolute(percorso) || (vuoto && !ammettiRadice)) {
|
|
57
|
+
throw new WorkspaceFileError('Percorso non valido');
|
|
58
|
+
}
|
|
59
|
+
const radiceReale = realpathSyncFn(cartella);
|
|
60
|
+
let reale;
|
|
61
|
+
try {
|
|
62
|
+
reale = vuoto ? radiceReale : realpathSyncFn(join(cartella, percorso));
|
|
63
|
+
} catch {
|
|
64
|
+
throw new WorkspaceFileError('File non trovato', 'FILE_NOT_FOUND');
|
|
65
|
+
}
|
|
66
|
+
// ⛔ `isPathInside` dice `true` anche quando i due percorsi COINCIDONO (`relative()` torna ''): il confine vale comunque, ed è il controllo sotto a decidere se la radice stessa è ammessa.
|
|
67
|
+
if (!isPathInside(radiceReale, reale)) {
|
|
68
|
+
throw new WorkspaceFileError('Percorso fuori dalla cartella della sessione');
|
|
69
|
+
}
|
|
70
|
+
if (reale === radiceReale && !ammettiRadice) {
|
|
71
|
+
// ⛔ nessuna delle azioni che passano di qui con `ammettiRadice=false` ha senso sulla RADICE della sessione stessa (rinominarla/eliminarla è un disastro diverso, fuori scope qui)
|
|
72
|
+
throw new WorkspaceFileError('Percorso fuori dalla cartella della sessione, o è la radice stessa');
|
|
73
|
+
}
|
|
74
|
+
return { radiceReale, reale };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* "Apri" — il contenuto di un file, in sola lettura. Non passa da
|
|
79
|
+
* `disco.leggi` del kernel (che tronca a MAX_BYTE_LETTI per un motivo
|
|
80
|
+
* diverso, il contesto del modello): qui l'anteprima ha il suo proprio
|
|
81
|
+
* tetto, dichiarato esplicitamente se superato.
|
|
82
|
+
*/
|
|
83
|
+
export async function leggiContenutoFile({ cartella, percorso }, deps = {}) {
|
|
84
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
85
|
+
const stat = await (deps.statFn ?? fsp.stat)(reale);
|
|
86
|
+
if (!stat.isFile()) throw new WorkspaceFileError('Non è un file — apri una cartella dall\'albero, non da qui');
|
|
87
|
+
if (stat.size > DIMENSIONE_MASSIMA_ANTEPRIMA) {
|
|
88
|
+
throw new WorkspaceFileError(`File troppo grande per l'anteprima (${Math.round(stat.size / 1024)} KB, tetto ${DIMENSIONE_MASSIMA_ANTEPRIMA / 1024} KB)`, 'FILE_TOO_LARGE');
|
|
89
|
+
}
|
|
90
|
+
const contenuto = await (deps.readFileFn ?? fsp.readFile)(reale, 'utf8');
|
|
91
|
+
return { contenuto, dimensione: stat.size };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* ⛔⛔ PO-05, owner: «ogni file generato deve avere un collegamento diretto per scaricarlo con un
|
|
96
|
+
* clic; nome, formato, dimensione e disponibilità REALI». Fino a oggi un documento generato finiva nel
|
|
97
|
+
* workspace e in Libreria, e in chat arrivava la riga `[binary docx file, 7714 bytes]`: vera, ma non
|
|
98
|
+
* cliccabile. Questa è la metà del server.
|
|
99
|
+
*
|
|
100
|
+
* ⛔ Perché non basta `leggiContenutoFile`, che è qui sopra: quella legge in **utf8** — un `.docx` è
|
|
101
|
+
* uno zip, e passarlo da `readFile(…, 'utf8')` lo corrompe irreparabilmente (ogni byte non valido
|
|
102
|
+
* diventa U+FFFD e non torna più indietro) — e ha il tetto dell'ANTEPRIMA (512 KB), che per un
|
|
103
|
+
* allegato è la misura sbagliata. Qui i byte restano byte.
|
|
104
|
+
* ⛔ La difesa NON si riscrive: è la stessa `risolviPercorsoEsistente` delle sorelle (canonicalizza
|
|
105
|
+
* con `realpath` e confina dentro la cartella della sessione, quindi né `..` né un collegamento
|
|
106
|
+
* simbolico portano fuori). Una difesa copiata è una difesa che un giorno diverge.
|
|
107
|
+
*
|
|
108
|
+
* Ricerca 09-10/09/2026, prima di scrivere:
|
|
109
|
+
* · IETF RFC 6266 (`Use of the Content-Disposition Header Field in HTTP`): «attachment» dice al
|
|
110
|
+
* destinatario di proporre il salvataggio; il parametro `filename*` estende `filename` alle
|
|
111
|
+
* codifiche oltre l'ASCII e, quando ci sono entrambi, **`filename*` ha la precedenza** ⇒ si
|
|
112
|
+
* mandano TUTTI E DUE: `filename` come ripiego ASCII e `filename*` in UTF-8. UTF-8 è la codifica
|
|
113
|
+
* raccomandata perché almeno un'implementazione diffusa non ne conosce altre.
|
|
114
|
+
* · MDN, `Content-Disposition` (letto 10/09/2026): stessa regola, e l'avvertenza che un agente che
|
|
115
|
+
* non conosce RFC 5987 mostrerebbe la sequenza percent-encoded — da cui il doppio parametro.
|
|
116
|
+
* ⛔ Il nome che finisce nell'intestazione è il **nome del file sul disco**, non un testo scelto da
|
|
117
|
+
* chi chiama: niente a capo né virgolette che possano spezzare l'intestazione (una risposta HTTP
|
|
118
|
+
* con un'intestazione spezzata è una vulnerabilità, non un nome brutto).
|
|
119
|
+
*/
|
|
120
|
+
/* ⛔ Esportato il 10/09/2026: lo scarico di una voce di Libreria (`library-store.mjs`) ha lo
|
|
121
|
+
STESSO tetto, e due numeri scritti in due file un giorno dicono due cose diverse. */
|
|
122
|
+
export const DIMENSIONE_MASSIMA_SCARICO = 64 * 1024 * 1024;
|
|
123
|
+
|
|
124
|
+
/** Il nome, ridotto a ciò che può stare in un'intestazione HTTP senza spezzarla. Puro. */
|
|
125
|
+
export function nomeSicuroPerIntestazione(nome) {
|
|
126
|
+
const pulito = String(nome ?? '').replace(/[\r\n\0]/g, '').trim();
|
|
127
|
+
return pulito.length > 0 ? pulito : 'file';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Le due forme del nome per `Content-Disposition`, secondo RFC 6266: `ascii` è il ripiego (i
|
|
132
|
+
* caratteri fuori ASCII diventano `_`, le virgolette e la barra rovescia spariscono), `utf8` è il
|
|
133
|
+
* valore percent-encoded per `filename*`. Puro, così si prova senza un server.
|
|
134
|
+
*/
|
|
135
|
+
export function nomiPerContentDisposition(nome) {
|
|
136
|
+
const sicuro = nomeSicuroPerIntestazione(nome);
|
|
137
|
+
const ascii = sicuro.replace(/["\\]/g, '').replace(/[^\x20-\x7E]/g, '_');
|
|
138
|
+
return { ascii: ascii.trim() || 'file', utf8: encodeURIComponent(sicuro) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Un file del workspace, in BYTE, per essere scaricato. Stessa difesa delle sorelle.
|
|
143
|
+
* @returns {Promise<{bytes:Buffer, dimensione:number, nome:string}>}
|
|
144
|
+
*/
|
|
145
|
+
export async function leggiFilePerScarico({ cartella, percorso }, deps = {}) {
|
|
146
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
147
|
+
const stat = await (deps.statFn ?? fsp.stat)(reale);
|
|
148
|
+
if (!stat.isFile()) throw new WorkspaceFileError('Non è un file: una cartella non si scarica');
|
|
149
|
+
if (stat.size > DIMENSIONE_MASSIMA_SCARICO) {
|
|
150
|
+
throw new WorkspaceFileError(
|
|
151
|
+
`File troppo grande da scaricare (${Math.round(stat.size / 1024 / 1024)} MB, tetto ${DIMENSIONE_MASSIMA_SCARICO / 1024 / 1024} MB)`,
|
|
152
|
+
'FILE_TOO_LARGE',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const bytes = await (deps.readFileFn ?? fsp.readFile)(reale);
|
|
156
|
+
return { bytes, dimensione: stat.size, nome: reale.split(/[\\/]/).pop() };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* "Rinomina" — `nuovoNome` è un NOME, non un percorso: niente `/`, `\`,
|
|
161
|
+
* `..` — sposta il file nella STESSA cartella, non altrove (rinominare
|
|
162
|
+
* ≠ spostare, stessa distinzione che fa ogni file manager reale).
|
|
163
|
+
*/
|
|
164
|
+
export async function rinominaFile({ cartella, percorso, nuovoNome }, deps = {}) {
|
|
165
|
+
const { radiceReale, reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
166
|
+
if (
|
|
167
|
+
typeof nuovoNome !== 'string' || nuovoNome.length === 0 || nuovoNome.length > 255
|
|
168
|
+
|| nuovoNome.includes('/') || nuovoNome.includes('\\') || nuovoNome.includes('\0')
|
|
169
|
+
|| nuovoNome === '.' || nuovoNome === '..'
|
|
170
|
+
) {
|
|
171
|
+
throw new WorkspaceFileError('Nuovo nome non valido — un nome di file, non un percorso');
|
|
172
|
+
}
|
|
173
|
+
const destinazione = join(dirname(reale), nuovoNome);
|
|
174
|
+
if (!isPathInside(radiceReale, destinazione)) throw new WorkspaceFileError('Destinazione fuori dalla cartella della sessione');
|
|
175
|
+
const accessFn = deps.accessFn ?? fsp.access;
|
|
176
|
+
const esisteGia = await accessFn(destinazione).then(() => true, () => false);
|
|
177
|
+
if (esisteGia) throw new WorkspaceFileError('Esiste già un file con questo nome', 'FILE_EXISTS');
|
|
178
|
+
await (deps.renameFn ?? fsp.rename)(reale, destinazione);
|
|
179
|
+
const nuovoPercorso = relative(radiceReale, destinazione).split(sep).join('/');
|
|
180
|
+
return { nuovoPercorso };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* "Elimina" — DISTRUTTIVA. La conferma vive nel frontend (un dialogo
|
|
185
|
+
* vero, non un `confirm()` del browser — stessa disciplina "hard to
|
|
186
|
+
* reverse actions get confirmed" del resto del prodotto): questa
|
|
187
|
+
* funzione esegue e basta, non chiede nulla lei stessa.
|
|
188
|
+
*/
|
|
189
|
+
export async function eliminaFile({ cartella, percorso }, deps = {}) {
|
|
190
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
191
|
+
const stat = await (deps.statFn ?? fsp.stat)(reale);
|
|
192
|
+
await (deps.rmFn ?? fsp.rm)(reale, { recursive: stat.isDirectory(), force: false });
|
|
193
|
+
return { eliminato: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* ⛔⛔⛔ BC-11, 11/09/2026 — IL TETTO DI UNA SCRITTURA SI DICHIARA, non lo si scopre sbattendoci.
|
|
198
|
+
*
|
|
199
|
+
* Misurato sulla sessione `8dde6bff` dell'owner («genera un file html di almeno 1000 righe»):
|
|
200
|
+
* `creaFileWorkspace` accettava **41.943.040 byte in 12 ms senza un solo controllo**, e con
|
|
201
|
+
* `bytes: undefined` non rispondeva niente di utile — rilanciava il `TypeError` di Node
|
|
202
|
+
* (`ERR_INVALID_ARG_TYPE: The "data" argument must be of type string or an instance of Buffer…`),
|
|
203
|
+
* cioè un errore di sistema operativo consegnato a un modello che non ha modo di agirci sopra.
|
|
204
|
+
* ⇒ Regola dell'owner per BC-11: «ogni attrezzo deve DIRE al modello i suoi limiti quando li
|
|
205
|
+
* supera, invece di restituire un errore di sistema operativo».
|
|
206
|
+
*
|
|
207
|
+
* ⛔ Perché 32 MB e non di più: è la stessa taglia d'ordine dello scarico (64 MB, qui sopra) ma
|
|
208
|
+
* dimezzata, perché QUESTI byte hanno attraversato una risposta del modello — nessun documento
|
|
209
|
+
* generato da un giro è legittimamente più grande, e un numero più alto non proteggerebbe da
|
|
210
|
+
* niente. È un tetto DICHIARATO nel messaggio, non una soglia muta.
|
|
211
|
+
*
|
|
212
|
+
* Ricerca 11/09/2026, prima di scrivere (regola owner: si cerca PRIMA, soprattutto quando sembra
|
|
213
|
+
* ovvio — quello che manca non è la soluzione, sono i vincoli):
|
|
214
|
+
* · Anthropic, «Text editor tool» (platform.claude.com, letto 11/09/2026): i comandi sono
|
|
215
|
+
* `view`/`create`/`str_replace`/`insert`/`undo_edit` — `insert` esiste APPOSTA per aggiungere
|
|
216
|
+
* testo dopo una riga, cioè per non dover riscrivere un file intero a ogni aggiunta.
|
|
217
|
+
* · anthropics/claude-quickstarts#348 (letto 11/09/2026): «The `EditTool20250728` class expects
|
|
218
|
+
* `new_str` for the insert command, but Claude actually outputs `insert_text`. This causes
|
|
219
|
+
* insert commands to fail» ⇒ il nome sbagliato di un argomento è una classe di errore REALE
|
|
220
|
+
* anche nell'implementazione di riferimento del fornitore, non una stranezza del nostro
|
|
221
|
+
* modello: un attrezzo che accetta un solo nome trasforma un errore di forma in un guasto.
|
|
222
|
+
* · Nous Research, hermes-agent v0.21, `tools/file_tools.py:2729` (WRITE_FILE_SCHEMA): «Use this
|
|
223
|
+
* instead of echo/cat heredoc in terminal… OVERWRITES the entire file — use 'patch' for
|
|
224
|
+
* targeted edits», e `registry.register(…, max_result_size_chars=100_000)` — il tetto è un dato
|
|
225
|
+
* dichiarato accanto all'attrezzo, non un comportamento scoperto a valle.
|
|
226
|
+
* · cline, `apps/vscode/src/sdk/sdk-diff-edit-coordinator.ts:401` — l'errore stesso insegna il
|
|
227
|
+
* valore giusto: «insert_line must be a positive one-based boundary line in the range 1-N.
|
|
228
|
+
* **Use N to append at EOF.**»
|
|
229
|
+
* · Node.js `fs` (nodejs.org, letto 11/09/2026): `appendFile` usa il flag `'a'`; su Windows
|
|
230
|
+
* `flock` non c'è, e `writeFile` con `'w'` / `appendFile` con `'a'` sono la via per una
|
|
231
|
+
* scrittura o un'aggiunta atomica per singola chiamata. ⇒ qui si accoda con `appendFile`, MAI
|
|
232
|
+
* leggendo-concatenando-riscrivendo (che perderebbe la scrittura di chiunque altro in mezzo).
|
|
233
|
+
*/
|
|
234
|
+
export const DIMENSIONE_MASSIMA_CREAZIONE = 32 * 1024 * 1024;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* "Crea" — scrive BYTE nuovi alla radice del workspace (`document_create`,
|
|
238
|
+
* piano elegant-spinning-dongarra.md, 28/8). Diversa dalle altre azioni
|
|
239
|
+
* di questo file: qui il file NON esiste ancora, quindi
|
|
240
|
+
* `risolviPercorsoEsistente` (che richiede `realpathSync` sul
|
|
241
|
+
* bersaglio) non si applica — si valida solo che `nome` sia un NOME
|
|
242
|
+
* piatto (stessa grammatica di `nuovoNome` in `rinominaFile`: niente
|
|
243
|
+
* `/`, `\`, `..`), mai un percorso con sottocartelle. Scope
|
|
244
|
+
* deliberatamente stretto (radice del workspace, non un percorso
|
|
245
|
+
* arbitrario) — un generatore di documenti scrive dove l'utente lo
|
|
246
|
+
* vede subito nell'albero, non in una sottocartella indovinata.
|
|
247
|
+
*
|
|
248
|
+
* ⛔⛔⛔ BC-11 — `modalita` è la seconda metà della cura, e nasce da un numero: nella sessione
|
|
249
|
+
* `8dde6bff` il modello ha speso **119 chiamate `shell` contro 23 `scrivi`** (e nella `37e10d21`
|
|
250
|
+
* **99 contro 6**) per scrivere UN file, perché nessun attrezzo sa AGGIUNGERE: un file più lungo
|
|
251
|
+
* di una risposta obbligava a inventarsi `_p2.html`, `_p3.html`, `_p4.html`, `_p5.html` e poi un
|
|
252
|
+
* passo di «assemblaggio» — cioè almeno due giri in più per ogni pezzo.
|
|
253
|
+
* · `'nuovo'` (default): ESATTAMENTE il comportamento di sempre — un nome già preso viene
|
|
254
|
+
* rifiutato, mai sovrascritto in silenzio (prova `workspace-files.test.mjs:240`, che resta vera).
|
|
255
|
+
* · `'accoda'`: aggiunge in coda se il file c'è, lo crea se non c'è. È un'OPZIONE ESPLICITA di chi
|
|
256
|
+
* chiama, non un ripiego automatico: una sovrascrittura involontaria e un'aggiunta involontaria
|
|
257
|
+
* sono due danni diversi, e nessuno dei due deve poter succedere per distrazione.
|
|
258
|
+
* ⛔ Accodare a una CARTELLA è esattamente il guasto che BC-11 è venuto a curare (`EISDIR:
|
|
259
|
+
* illegal operation on a directory`): qui si guarda cosa c'è PRIMA e si risponde a parole.
|
|
260
|
+
*
|
|
261
|
+
* @param {{cartella:string, nome:string, bytes:Uint8Array|Buffer|string, modalita?:'nuovo'|'accoda'}} input
|
|
262
|
+
*/
|
|
263
|
+
export async function creaFileWorkspace({ cartella, nome, bytes, modalita = 'nuovo' }, deps = {}) {
|
|
264
|
+
if (modalita !== 'nuovo' && modalita !== 'accoda') {
|
|
265
|
+
throw new WorkspaceFileError('Modalità non valida: "nuovo" (rifiuta un nome già preso) o "accoda" (aggiunge in coda)');
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
typeof nome !== 'string' || nome.length === 0 || nome.length > 255
|
|
269
|
+
|| nome.includes('/') || nome.includes('\\') || nome.includes('\0')
|
|
270
|
+
|| nome === '.' || nome === '..'
|
|
271
|
+
) {
|
|
272
|
+
throw new WorkspaceFileError('Nome file non valido — un nome, non un percorso');
|
|
273
|
+
}
|
|
274
|
+
/*
|
|
275
|
+
* ⛔ BC-11 — il TIPO prima del tetto, e detto a parole. Senza questo controllo `fsp.writeFile`
|
|
276
|
+
* lancia `ERR_INVALID_ARG_TYPE` (riprodotto l'11/09/2026), che per chi legge il risultato
|
|
277
|
+
* dell'attrezzo è indistinguibile da un guasto del disco.
|
|
278
|
+
*/
|
|
279
|
+
const byteValidi = typeof bytes === 'string' || ArrayBuffer.isView(bytes) || bytes instanceof ArrayBuffer;
|
|
280
|
+
if (!byteValidi) {
|
|
281
|
+
throw new WorkspaceFileError(
|
|
282
|
+
'Contenuto mancante o non valido: servono byte o testo, e ne è arrivato '
|
|
283
|
+
+ `${bytes === undefined ? 'nessuno' : typeof bytes}. Rimanda la chiamata con il contenuto.`,
|
|
284
|
+
'CONTENT_INVALID',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const dimensione = typeof bytes === 'string' ? Buffer.byteLength(bytes, 'utf8') : bytes.byteLength;
|
|
288
|
+
if (dimensione > DIMENSIONE_MASSIMA_CREAZIONE) {
|
|
289
|
+
throw new WorkspaceFileError(
|
|
290
|
+
`Contenuto troppo grande (${Math.round(dimensione / 1024 / 1024)} MB, tetto `
|
|
291
|
+
+ `${DIMENSIONE_MASSIMA_CREAZIONE / 1024 / 1024} MB). Scrivilo in più pezzi, aggiungendo ogni pezzo in coda.`,
|
|
292
|
+
'CONTENT_TOO_LARGE',
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const radiceReale = (deps.realpathSyncFn ?? realpathSync)(cartella);
|
|
296
|
+
const destinazione = join(radiceReale, nome);
|
|
297
|
+
if (!isPathInside(radiceReale, destinazione)) throw new WorkspaceFileError('Destinazione fuori dalla cartella del workspace');
|
|
298
|
+
const accessFn = deps.accessFn ?? fsp.access;
|
|
299
|
+
const esisteGia = await accessFn(destinazione).then(() => true, () => false);
|
|
300
|
+
if (esisteGia && modalita === 'nuovo') throw new WorkspaceFileError('Esiste già un file con questo nome', 'FILE_EXISTS');
|
|
301
|
+
if (esisteGia) {
|
|
302
|
+
/*
|
|
303
|
+
* ⛔ La domanda «è un file?» si fa PRIMA di aprire: `appendFile` su una cartella darebbe
|
|
304
|
+
* `EISDIR` — lo stesso errore di sistema operativo che l'owner ha visto cinque volte nella
|
|
305
|
+
* sessione `8dde6bff` e che nessuno poteva leggere.
|
|
306
|
+
*/
|
|
307
|
+
const stat = await (deps.statFn ?? fsp.stat)(destinazione);
|
|
308
|
+
if (!stat.isFile()) throw new WorkspaceFileError('Esiste già una cartella con questo nome: non ci si può accodare', 'NOT_A_FILE');
|
|
309
|
+
if (stat.size + dimensione > DIMENSIONE_MASSIMA_CREAZIONE) {
|
|
310
|
+
throw new WorkspaceFileError(
|
|
311
|
+
`Il file arriverebbe a ${Math.round((stat.size + dimensione) / 1024 / 1024)} MB, oltre il tetto di `
|
|
312
|
+
+ `${DIMENSIONE_MASSIMA_CREAZIONE / 1024 / 1024} MB.`,
|
|
313
|
+
'CONTENT_TOO_LARGE',
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
// ⛔ `appendFile` (flag 'a'), MAI leggi-concatena-riscrivi: quest'ultima perderebbe in silenzio
|
|
317
|
+
// ciò che qualcun altro ha scritto fra la lettura e la riscrittura.
|
|
318
|
+
await (deps.appendFileFn ?? fsp.appendFile)(destinazione, bytes);
|
|
319
|
+
return { percorso: nome, accodato: true, byteTotali: stat.size + dimensione };
|
|
320
|
+
}
|
|
321
|
+
await (deps.writeFileFn ?? fsp.writeFile)(destinazione, bytes);
|
|
322
|
+
return { percorso: nome, ...(modalita === 'accoda' ? { accodato: false, byteTotali: dimensione } : {}) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* "Sposta" — drag&drop (piano `elegant-spinning-dongarra.md`, owner:
|
|
327
|
+
* "nella lista files devo poter draggare i file"). Sposta un file/
|
|
328
|
+
* cartella in un'ALTRA cartella dello stesso workspace, mantenendo il
|
|
329
|
+
* nome — `cartellaDestinazione` è un percorso relativo ('' = radice)
|
|
330
|
+
* che deve esistere ED essere una cartella. Non sovrascrive mai un
|
|
331
|
+
* nome già occupato nella destinazione. `isPathInside(reale,
|
|
332
|
+
* destinazioneCartellaReale)` copre da sola sia "dentro se stessa" sia
|
|
333
|
+
* "dentro un proprio discendente" (torna true anche a parità di
|
|
334
|
+
* percorso — verificato leggendo path-policy.mjs, non presunto): senza
|
|
335
|
+
* questa guardia una cartella spostata dentro se stessa lascerebbe
|
|
336
|
+
* `fs.rename` fallire a metà con un errore di sistema operativo
|
|
337
|
+
* travestito da azione riuscita.
|
|
338
|
+
*/
|
|
339
|
+
export async function spostaFile({ cartella, percorso, cartellaDestinazione }, deps = {}) {
|
|
340
|
+
const { radiceReale, reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
341
|
+
const realpathSyncFn = deps.realpathSyncFn ?? realpathSync;
|
|
342
|
+
let destinazioneCartellaReale;
|
|
343
|
+
if (cartellaDestinazione === '') {
|
|
344
|
+
destinazioneCartellaReale = radiceReale;
|
|
345
|
+
} else {
|
|
346
|
+
if (typeof cartellaDestinazione !== 'string' || isAbsolute(cartellaDestinazione)) {
|
|
347
|
+
throw new WorkspaceFileError('Cartella di destinazione non valida');
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
destinazioneCartellaReale = realpathSyncFn(join(cartella, cartellaDestinazione));
|
|
351
|
+
} catch {
|
|
352
|
+
throw new WorkspaceFileError('Cartella di destinazione non trovata', 'FILE_NOT_FOUND');
|
|
353
|
+
}
|
|
354
|
+
if (!isPathInside(radiceReale, destinazioneCartellaReale)) {
|
|
355
|
+
throw new WorkspaceFileError('Destinazione fuori dalla cartella della sessione');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const statDestinazione = await (deps.statFn ?? fsp.stat)(destinazioneCartellaReale).catch(() => null);
|
|
359
|
+
if (!statDestinazione || !statDestinazione.isDirectory()) {
|
|
360
|
+
throw new WorkspaceFileError('La destinazione non è una cartella');
|
|
361
|
+
}
|
|
362
|
+
if (isPathInside(reale, destinazioneCartellaReale)) {
|
|
363
|
+
throw new WorkspaceFileError('Non puoi spostare un elemento dentro se stesso o un suo discendente');
|
|
364
|
+
}
|
|
365
|
+
const nome = reale.split(sep).pop();
|
|
366
|
+
const destinazione = join(destinazioneCartellaReale, nome);
|
|
367
|
+
const accessFn = deps.accessFn ?? fsp.access;
|
|
368
|
+
const esisteGia = await accessFn(destinazione).then(() => true, () => false);
|
|
369
|
+
if (esisteGia) throw new WorkspaceFileError('Esiste già un elemento con questo nome nella destinazione', 'FILE_EXISTS');
|
|
370
|
+
await (deps.renameFn ?? fsp.rename)(reale, destinazione);
|
|
371
|
+
const nuovoPercorso = relative(radiceReale, destinazione).split(sep).join('/');
|
|
372
|
+
return { nuovoPercorso };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* "Copia" — owner: "non esiste il comando copia". Duplica un file/
|
|
377
|
+
* cartella nella STESSA posizione, con un nome tipo "nome (copia).ext"
|
|
378
|
+
* (pattern Explorer/Finder — verificato via ricerca web, non inventato:
|
|
379
|
+
* è la convenzione standard di ogni file manager desktop). Se anche
|
|
380
|
+
* quello esiste già, prova "nome (copia 2).ext" e così via, fino a un
|
|
381
|
+
* tetto dichiarato — mai un ciclo infinito su un caso patologico.
|
|
382
|
+
*/
|
|
383
|
+
export async function copiaFile({ cartella, percorso }, deps = {}) {
|
|
384
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
385
|
+
const accessFn = deps.accessFn ?? fsp.access;
|
|
386
|
+
const cartellaGenitore = dirname(reale);
|
|
387
|
+
const nomeOriginale = reale.split(sep).pop();
|
|
388
|
+
const stat = await (deps.statFn ?? fsp.stat)(reale);
|
|
389
|
+
const puntoEstensione = nomeOriginale.lastIndexOf('.');
|
|
390
|
+
// niente estensione separata per una cartella, o per un file che INIZIA con un punto (es. ".gitignore" resta intero in "base")
|
|
391
|
+
const haEstensione = !stat.isDirectory() && puntoEstensione > 0;
|
|
392
|
+
const base = haEstensione ? nomeOriginale.slice(0, puntoEstensione) : nomeOriginale;
|
|
393
|
+
const estensione = haEstensione ? nomeOriginale.slice(puntoEstensione) : '';
|
|
394
|
+
|
|
395
|
+
const TETTO_TENTATIVI = 1000;
|
|
396
|
+
let destinazione = null;
|
|
397
|
+
for (let n = 1; n <= TETTO_TENTATIVI; n += 1) {
|
|
398
|
+
const suffisso = n === 1 ? ' (copia)' : ` (copia ${n})`;
|
|
399
|
+
const candidato = join(cartellaGenitore, `${base}${suffisso}${estensione}`);
|
|
400
|
+
const esisteGia = await accessFn(candidato).then(() => true, () => false);
|
|
401
|
+
if (!esisteGia) { destinazione = candidato; break; }
|
|
402
|
+
}
|
|
403
|
+
if (!destinazione) throw new WorkspaceFileError('Troppe copie già esistenti con questo nome');
|
|
404
|
+
|
|
405
|
+
await (deps.cpFn ?? fsp.cp)(reale, destinazione, { recursive: true, errorOnExist: true });
|
|
406
|
+
const radiceReale = realpathSync(cartella);
|
|
407
|
+
const nuovoPercorso = relative(radiceReale, destinazione).split(sep).join('/');
|
|
408
|
+
return { nuovoPercorso };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* "Nuovo file"/"Nuova cartella" — CRUD manuale dell'owner ("comandi
|
|
413
|
+
* crud in generale"), in QUALSIASI punto dell'albero — a differenza di
|
|
414
|
+
* `creaFileWorkspace` sopra, che resta vincolata alla radice per
|
|
415
|
+
* `document_create` (un bisogno diverso, dell'AGENTE, non toccato qui).
|
|
416
|
+
* `percorsoBase` è la cartella dove creare ('' = radice); deve
|
|
417
|
+
* esistere ED essere una cartella se non vuoto.
|
|
418
|
+
*/
|
|
419
|
+
export async function creaVoceWorkspace({ cartella, percorsoBase, nome, tipo }, deps = {}) {
|
|
420
|
+
if (tipo !== 'file' && tipo !== 'cartella') {
|
|
421
|
+
throw new WorkspaceFileError('Tipo non valido: "file" o "cartella"');
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
typeof nome !== 'string' || nome.length === 0 || nome.length > 255
|
|
425
|
+
|| nome.includes('/') || nome.includes('\\') || nome.includes('\0')
|
|
426
|
+
|| nome === '.' || nome === '..'
|
|
427
|
+
) {
|
|
428
|
+
throw new WorkspaceFileError('Nome non valido — un nome, non un percorso');
|
|
429
|
+
}
|
|
430
|
+
const realpathSyncFn = deps.realpathSyncFn ?? realpathSync;
|
|
431
|
+
const radiceReale = realpathSyncFn(cartella);
|
|
432
|
+
let cartellaBaseReale = radiceReale;
|
|
433
|
+
if (percorsoBase) {
|
|
434
|
+
if (typeof percorsoBase !== 'string' || isAbsolute(percorsoBase)) {
|
|
435
|
+
throw new WorkspaceFileError('Cartella base non valida');
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
cartellaBaseReale = realpathSyncFn(join(cartella, percorsoBase));
|
|
439
|
+
} catch {
|
|
440
|
+
throw new WorkspaceFileError('Cartella base non trovata', 'FILE_NOT_FOUND');
|
|
441
|
+
}
|
|
442
|
+
if (!isPathInside(radiceReale, cartellaBaseReale)) {
|
|
443
|
+
throw new WorkspaceFileError('Cartella base fuori dal workspace della sessione');
|
|
444
|
+
}
|
|
445
|
+
const statBase = await (deps.statFn ?? fsp.stat)(cartellaBaseReale).catch(() => null);
|
|
446
|
+
if (!statBase || !statBase.isDirectory()) throw new WorkspaceFileError('La cartella base non è una cartella');
|
|
447
|
+
}
|
|
448
|
+
const destinazione = join(cartellaBaseReale, nome);
|
|
449
|
+
if (!isPathInside(radiceReale, destinazione)) throw new WorkspaceFileError('Destinazione fuori dal workspace della sessione');
|
|
450
|
+
const accessFn = deps.accessFn ?? fsp.access;
|
|
451
|
+
const esisteGia = await accessFn(destinazione).then(() => true, () => false);
|
|
452
|
+
if (esisteGia) throw new WorkspaceFileError('Esiste già un elemento con questo nome', 'FILE_EXISTS');
|
|
453
|
+
if (tipo === 'cartella') await (deps.mkdirFn ?? fsp.mkdir)(destinazione);
|
|
454
|
+
else await (deps.writeFileFn ?? fsp.writeFile)(destinazione, '');
|
|
455
|
+
const percorso = relative(radiceReale, destinazione).split(sep).join('/');
|
|
456
|
+
return { percorso };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* "Rivela in Esplora File" — SOLO Windows (`explorer.exe`), dichiarato
|
|
461
|
+
* non simulato altrove. Un SOLO argomento argv (`/select,<percorso>`,
|
|
462
|
+
* verificato via ricerca web la sintassi esatta — niente spazio dopo la
|
|
463
|
+
* virgola): `execFile` non passa da una shell, quindi zero rischio di
|
|
464
|
+
* injection anche con un percorso pieno di caratteri strani.
|
|
465
|
+
*
|
|
466
|
+
* ⛔ `explorer.exe` torna quasi sempre un codice di uscita diverso da
|
|
467
|
+
* zero ANCHE quando ha aperto la finestra correttamente (comportamento
|
|
468
|
+
* noto, non un guasto di questo codice) — l'unico fallimento vero da
|
|
469
|
+
* segnalare è `execFile` che non trova l'eseguibile affatto.
|
|
470
|
+
*/
|
|
471
|
+
/*
|
|
472
|
+
* ⛔⛔ 10/09/2026 — «APRI», l'azione Windows che mancava. Owner: «con CRUD completo e AZIONI
|
|
473
|
+
* WINDOWS», e poi «fai in modo che le azioni e i pulsanti della libreria funzionino a schermo».
|
|
474
|
+
*
|
|
475
|
+
* Il problema, misurato: la rotta che serve i byte manda `Content-Disposition: attachment` e
|
|
476
|
+
* `application/octet-stream` — giusto per uno SCARICO, ma vuol dire che un pulsante «Apri» agganciato
|
|
477
|
+
* lì farebbe scaricare il file una seconda volta invece di aprirlo. Su Windows «aprire» ha un
|
|
478
|
+
* significato preciso e diverso: lo apre il programma associato all'estensione (Word per un .docx,
|
|
479
|
+
* il visualizzatore foto per un .jpg).
|
|
480
|
+
*
|
|
481
|
+
* ⛔ `explorer.exe <percorso>` è la stessa porta già usata da `rivelaInEsploraFile` qui sotto, senza
|
|
482
|
+
* `/select`: con il percorso di un FILE, Explorer lo apre col programma associato. Riusa la stessa
|
|
483
|
+
* politica di processo (`EXPLORER_PROCESS_POLICY`), che ammette solo `explorer.exe`: nessun
|
|
484
|
+
* eseguibile nuovo, nessuna shell, e il percorso resta un ARGOMENTO — mai una stringa di comando,
|
|
485
|
+
* che con un nome contenente `&` o `"` sarebbe un'iniezione.
|
|
486
|
+
* ⛔ E come la sorella: fuori da Windows non finge, dichiara che non è disponibile.
|
|
487
|
+
*/
|
|
488
|
+
/*
|
|
489
|
+
* ⛔⛔ 11/09/2026 — L'UNICA PORTA verso `explorer.exe`, estratta dalle tre azioni che la usavano con
|
|
490
|
+
* lo stesso identico preambolo copiato tre volte. Non è un riordino estetico: il difetto del
|
|
491
|
+
* 10/09 (il richiamo finito nel posto delle OPZIONI, la promessa mai risolta, la richiesta HTTP
|
|
492
|
+
* appesa per sempre) viveva in DUE copie e andava curato in due posti. «Una difesa copiata è una
|
|
493
|
+
* difesa che un giorno diverge» — qui la forma del richiamo, la politica di processo e la regola
|
|
494
|
+
* «un codice d'uscita non-zero non è un guasto» stanno scritte una volta sola.
|
|
495
|
+
* ⛔ Gli argomenti restano ARGOMENTI (`execFile`, mai una shell): un nome con `&`, `"` o uno spazio
|
|
496
|
+
* non può diventare un comando.
|
|
497
|
+
*/
|
|
498
|
+
function lanciaExplorer(argomenti, cartella, deps) {
|
|
499
|
+
const politica = (comando, args, opzioni, callback) => EXPLORER_PROCESS_POLICY.execFile(comando, args, {
|
|
500
|
+
...opzioni,
|
|
501
|
+
cwd: opzioni?.cwd ?? cartella,
|
|
502
|
+
}, callback);
|
|
503
|
+
const grezza = deps.execFileFn ?? politica;
|
|
504
|
+
/* Chi inietta un finto a tre parametri continua a funzionare: l'ultimo argomento è il richiamo. */
|
|
505
|
+
const execFileFn = (comando, args, opzioni, callback) => (
|
|
506
|
+
grezza.length <= 3
|
|
507
|
+
? grezza(comando, args, callback)
|
|
508
|
+
: grezza(comando, args, opzioni, callback)
|
|
509
|
+
);
|
|
510
|
+
return new Promise((ok, no) => {
|
|
511
|
+
execFileFn('explorer.exe', argomenti, {}, (errore) => {
|
|
512
|
+
if (errore && errore.code === 'ENOENT') { no(errore); return; }
|
|
513
|
+
ok(); // ⛔ explorer.exe esce con codici non-zero anche quando ha funzionato: è noto, e non è un guasto
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/*
|
|
519
|
+
* ⛔⛔⛔ 11/09/2026 — «APRI IN ESPLORA FILE», owner dal vivo sulla foto dell'albero: «se faccio tasto
|
|
520
|
+
* destro sulla ROOT deve poter spuntare "Apri", cioè devo poterla aprire su Windows».
|
|
521
|
+
*
|
|
522
|
+
* Questa è la sorella di `apriFileConProgrammaPredefinito` che sa dire di sì anche a una CARTELLA e
|
|
523
|
+
* alla RADICE della sessione (`percorso: ''`). Perché non si è semplicemente allargata quella:
|
|
524
|
+
* · la Libreria la usa per i FILE e il suo rifiuto su una cartella è un contratto provato
|
|
525
|
+
* («⛔ APRI, AL CONTRARIO: una cartella non si apre col programma») — un rifiuto che passa a un sì
|
|
526
|
+
* non è un allargamento, è un'altra funzione;
|
|
527
|
+
* · qui invece «aprire» una cartella ha un significato Windows preciso e diverso.
|
|
528
|
+
* ⛔ Ma la PORTA è la stessa (`lanciaExplorer`) e la DIFESA è la stessa (`risolviPercorsoEsistente`,
|
|
529
|
+
* con l'unica deroga dichiarata: la radice è ammessa) — mai una seconda validazione da tenere
|
|
530
|
+
* allineata alla prima.
|
|
531
|
+
*
|
|
532
|
+
* Ricerca 11/09/2026, PRIMA di scrivere (WebSearch esaurito per la sessione ⇒ fonti primarie via
|
|
533
|
+
* WebFetch, dichiarato nel rapporto):
|
|
534
|
+
* · Microsoft Learn, «Launching Applications (ShellExecute, ShellExecuteEx, SHELLEXECUTEINFO)»
|
|
535
|
+
* (learn.microsoft.com/en-us/windows/win32/shell/launch, pagina datata 2025-07-02, letta
|
|
536
|
+
* 11/09/2026): il verbo predefinito di un oggetto della Shell è `open`, e vale per «file or
|
|
537
|
+
* folder objects» — cioè aprire una CARTELLA è la stessa operazione di aprire un file, con un
|
|
538
|
+
* gestore diverso (la finestra di Esplora file invece del programma associato all'estensione).
|
|
539
|
+
* ⇒ `explorer.exe <cartella>` è la stessa porta già in uso per i file, senza `/select`.
|
|
540
|
+
* · Stessa pagina: `/select` non compare fra i verbi — è lo switch di Explorer che apre la
|
|
541
|
+
* cartella GENITORE con l'elemento evidenziato. ⇒ sulla radice sarebbe la cosa sbagliata: aprirebbe
|
|
542
|
+
* il genitore del workspace, cioè un posto FUORI dal workspace. «Apri» e «Rivela» restano due
|
|
543
|
+
* azioni diverse, e questa è quella che l'owner ha chiesto.
|
|
544
|
+
*/
|
|
545
|
+
export async function apriInEsploraFile({ cartella, percorso }, deps = {}) {
|
|
546
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps, true);
|
|
547
|
+
if ((deps.platform ?? process.platform) !== 'win32') {
|
|
548
|
+
throw new WorkspaceFileError('Disponibile solo su Windows', 'PLATFORM_UNSUPPORTED');
|
|
549
|
+
}
|
|
550
|
+
await lanciaExplorer([reale], cartella, deps);
|
|
551
|
+
return { aperto: true };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function apriFileConProgrammaPredefinito({ cartella, percorso }, deps = {}) {
|
|
555
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
556
|
+
if ((deps.platform ?? process.platform) !== 'win32') {
|
|
557
|
+
throw new WorkspaceFileError('Disponibile solo su Windows', 'PLATFORM_UNSUPPORTED');
|
|
558
|
+
}
|
|
559
|
+
const stat = await (deps.statFn ?? fsp.stat)(reale);
|
|
560
|
+
/* ⛔ Una cartella si «rivela», non si «apre col programma»: due azioni diverse, due bottoni diversi. */
|
|
561
|
+
if (!stat.isFile()) throw new WorkspaceFileError('Non è un file: usa «Mostra nella cartella»');
|
|
562
|
+
/*
|
|
563
|
+
* ⛔⛔ 10/09/2026 — QUESTA RIGA NON HA MAI FUNZIONATO CON LA POLITICA VERA, e nessun test se n'era
|
|
564
|
+
* accorto. Il wrapper dichiarava `(comando, argomenti, opzioni, callback)` ma qui sotto veniva
|
|
565
|
+
* chiamato con TRE argomenti: la funzione di richiamo finiva nel posto delle OPZIONI, e
|
|
566
|
+
* `callback` restava `undefined` ⇒ `execFile` non richiamava nessuno, la promessa non si
|
|
567
|
+
* risolveva mai, e la richiesta HTTP restava appesa per sempre.
|
|
568
|
+
* ⛔ Misurato dal vivo il 10/09 premendo il bottone sul 4174: la rotta non rispondeva entro 15
|
|
569
|
+
* secondi, e `curl` chiudeva con «0 bytes received». Fuori dal server, la funzione restava appesa
|
|
570
|
+
* oltre gli 8 secondi. Con `execFile` diretto, invece, il richiamo arriva in **126 ms**.
|
|
571
|
+
* ⛔ Perché i test erano verdi: iniettavano un finto a TRE parametri `(comando, argomenti, cb)`,
|
|
572
|
+
* cioè con la forma sbagliata — un finto che non imita il vero misura il finto. Ora il wrapper
|
|
573
|
+
* accetta entrambe le forme, e una prova nuova chiama con QUATTRO argomenti come fa il codice.
|
|
574
|
+
*/
|
|
575
|
+
await lanciaExplorer([reale], cartella, deps);
|
|
576
|
+
return { aperto: true };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export async function rivelaInEsploraFile({ cartella, percorso }, deps = {}) {
|
|
580
|
+
const { reale } = risolviPercorsoEsistente(cartella, percorso, deps);
|
|
581
|
+
if ((deps.platform ?? process.platform) !== 'win32') {
|
|
582
|
+
throw new WorkspaceFileError('Disponibile solo su Windows', 'PLATFORM_UNSUPPORTED');
|
|
583
|
+
}
|
|
584
|
+
/* ⛔ Stessa porta della sorella qui sopra (`lanciaExplorer`), e per lo stesso motivo: chiamato con
|
|
585
|
+
tre argomenti il richiamo finiva nel posto delle opzioni e la promessa non si risolveva mai.
|
|
586
|
+
Vedi là la storia — e ora quella cura vive in UN posto solo, non in tre copie. */
|
|
587
|
+
await lanciaExplorer([`/select,${reale}`], cartella, deps);
|
|
588
|
+
return { rivelato: true };
|
|
589
|
+
}
|