gentle-pi 3.7.0 → 4.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/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, parse, resolve, win32 } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { gunzipSync } from "node:zlib";
|
|
5
|
+
import { artifactFor, verifiedDownload, compatibleEngine, launchWizard, pinnedPnpmCompatible } from "./installer-downloads.mjs";
|
|
6
|
+
|
|
7
|
+
export const windowsNodeFloor = ">=24.3.0";
|
|
8
|
+
const maxExpandedBytes = 128 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
/** Validate the whole namespace, including implicit parents, before any writes.
|
|
11
|
+
* Windows paths are case-insensitive here even on portable fixture hosts.
|
|
12
|
+
*/
|
|
13
|
+
export function validateWindowsEntries(entries) {
|
|
14
|
+
if (!Array.isArray(entries) || entries.length === 0 || entries.length > 50000) throw new Error("Unsafe archive entry count");
|
|
15
|
+
const explicit = new Set();
|
|
16
|
+
const namespace = new Map();
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (typeof entry.name !== "string" || entry.link || entry.name.includes("\\")) throw new Error("Unsafe archive entry");
|
|
19
|
+
const name = entry.directory && entry.name.endsWith("/") ? entry.name.slice(0, -1) : entry.name;
|
|
20
|
+
const parts = name.split("/");
|
|
21
|
+
if (parts.some((part) => !part || part === "." || part === ".." || /[<>:"|?*\x00-\x1f\x7f]/.test(part) || /[. ]$/.test(part) || /^(?:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\.|$)/i.test(part))) {
|
|
22
|
+
throw new Error("Unsafe archive path");
|
|
23
|
+
}
|
|
24
|
+
const key = name.toLowerCase();
|
|
25
|
+
if (explicit.has(key)) throw new Error("Unsafe duplicate archive path");
|
|
26
|
+
explicit.add(key);
|
|
27
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
28
|
+
const path = parts.slice(0, index).join("/");
|
|
29
|
+
const directory = index < parts.length || Boolean(entry.directory);
|
|
30
|
+
const previous = namespace.get(path.toLowerCase());
|
|
31
|
+
if (previous && (previous.path !== path || previous.directory !== directory)) throw new Error("Unsafe archive alias");
|
|
32
|
+
namespace.set(path.toLowerCase(), { path, directory });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Complete Windows command shims, exactly as their generators write them (CRLF
|
|
38
|
+
// read as LF; `\0` marks the one target, a path without quotes or `%`):
|
|
39
|
+
// - npm cmd-shim 4.1-8 and 9 (npm global bins, such as `npm install -g pnpm`;
|
|
40
|
+
// github.com/npm/cmd-shim tap snapshots): a JS target run by the shim's
|
|
41
|
+
// sibling node.exe, else `node` from PATH, or a native target run as it is.
|
|
42
|
+
// EndLocal restores PATH/PATHEXT before the child starts.
|
|
43
|
+
// - pnpm's @zkochan/cmd-shim 9 (pnpm 10-12 global bins from `pnpm add -g`,
|
|
44
|
+
// `pnpm setup` and `pnpm self-update`): the same two kinds, or a JS target run
|
|
45
|
+
// by the absolute node.exe pnpm names. pnpm's optional NODE_PATH block only
|
|
46
|
+
// extends module lookup and is not reproduced.
|
|
47
|
+
// - npm's own npm.cmd that Node.js ships (npm 6-9, and 10-11 with npm-prefix.js):
|
|
48
|
+
// node as above, then the npm-cli.js under npm's global prefix when one is there.
|
|
49
|
+
// No Node flags, extra environment assignments or other commands are accepted.
|
|
50
|
+
const npmHead = "@ECHO off\nGOTO start\n:find_dp0\nSET dp0=%~dp0\nEXIT /b\n:start\nSETLOCAL\nCALL :find_dp0\n";
|
|
51
|
+
const npmProgram = `${npmHead}\nIF EXIST "%dp0%\\node.exe" (\n SET "_prog=%dp0%\\node.exe"\n) ELSE (\n SET "_prog=node"\n`;
|
|
52
|
+
const pnpmJs = "@IF EXIST \"%~dp0\\node.exe\" (\n \"%~dp0\\node.exe\" \"\0\" %*\n) ELSE (\n @SET PATHEXT=%PATHEXT:;.JS;=;%\n node \"\0\" %*\n)\n";
|
|
53
|
+
const nodeNpm = (prefix) => `:: Created by npm, please don't edit manually.\n@ECHO OFF\n\nSETLOCAL\n\nSET "NODE_EXE=%~dp0\\node.exe"\nIF NOT EXIST "%NODE_EXE%" (\n SET "NODE_EXE=node"\n)\n\n${prefix === "prefix-js" ? "SET \"NPM_PREFIX_JS=%~dp0\\node_modules\\npm\\bin\\npm-prefix.js\"\n" : ""}` +
|
|
54
|
+
`SET "NPM_CLI_JS=%~dp0\\node_modules\\npm\\bin\\npm-cli.js"\nFOR /F "delims=" %%F IN ('CALL "%NODE_EXE%" ${prefix === "prefix-js" ? "\"%NPM_PREFIX_JS%\"" : "\"%NPM_CLI_JS%\" prefix -g"}') DO (\n` +
|
|
55
|
+
" SET \"NPM_PREFIX_NPM_CLI_JS=%%F\\node_modules\\npm\\bin\\npm-cli.js\"\n)\nIF EXIST \"%NPM_PREFIX_NPM_CLI_JS%\" (\n SET \"NPM_CLI_JS=%NPM_PREFIX_NPM_CLI_JS%\"\n)\n\n\"%NODE_EXE%\" \"%NPM_CLI_JS%\" %*\n";
|
|
56
|
+
const npmTemplates = [
|
|
57
|
+
["entry", `${npmProgram} SET PATHEXT=%PATHEXT:;.JS;=;%\n)\n\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\\0" %*\n`],
|
|
58
|
+
["entry", `${npmProgram})\n\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & set PATHEXT=%PATHEXT:;.JS;=;% & "%_prog%" "%dp0%\\\0" %*\n`],
|
|
59
|
+
["exe", `${npmHead}"%dp0%\\\0" %*\n`],
|
|
60
|
+
];
|
|
61
|
+
const pnpmHeader = /^@SETLOCAL\n(?:@IF NOT DEFINED NODE_PATH \(\n {2}@SET "NODE_PATH=([^"\n]*)"\n\) ELSE \(\n {2}@SET "NODE_PATH=\1;%NODE_PATH%"\n\)\n)?/;
|
|
62
|
+
/** The value at every `\0` of template, when text is exactly that template. */
|
|
63
|
+
function filled(text, template) {
|
|
64
|
+
const [head, next] = template.split("\0");
|
|
65
|
+
if (!text.startsWith(head)) return null;
|
|
66
|
+
const value = text.slice(head.length, text.indexOf(next, head.length));
|
|
67
|
+
return /^[^"%\x00-\x1f]+$/.test(value) && template.replaceAll("\0", value) === text ? value : null;
|
|
68
|
+
}
|
|
69
|
+
/** A shim target: relative to the shim's directory, or absolute on a local drive. */
|
|
70
|
+
function shimPath(value, relative) {
|
|
71
|
+
if (value === null) return null;
|
|
72
|
+
if (!relative) return /^[A-Za-z]:\\/.test(value) ? value : null;
|
|
73
|
+
return value.startsWith("\\") || /^[A-Za-z]:/.test(value) ? null : value;
|
|
74
|
+
}
|
|
75
|
+
/** What a complete known Windows command shim runs, read from its text and never
|
|
76
|
+
* executed: { exe } a native target; { entry, node } a JS entry run by node (an
|
|
77
|
+
* absolute node.exe, or null: the shim's sibling node.exe, else `node` from
|
|
78
|
+
* PATH); or { npm } for Node.js's own npm.cmd ("prefix-js" or "prefix-g": how it
|
|
79
|
+
* asks for npm's global prefix). Targets are relative to the shim's directory
|
|
80
|
+
* (backslashes) or absolute. Anything else is null.
|
|
81
|
+
*/
|
|
82
|
+
export function windowsShim(text) {
|
|
83
|
+
if (typeof text !== "string" || /\r(?!\n)/.test(text)) return null;
|
|
84
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
85
|
+
for (const prefix of ["prefix-js", "prefix-g"]) if (normalized === nodeNpm(prefix)) return { npm: prefix };
|
|
86
|
+
for (const [kind, template] of npmTemplates) {
|
|
87
|
+
const target = shimPath(filled(normalized, template), true);
|
|
88
|
+
if (target) return kind === "exe" ? { exe: target } : { entry: target, node: null };
|
|
89
|
+
}
|
|
90
|
+
const header = pnpmHeader.exec(normalized);
|
|
91
|
+
if (!header) return null;
|
|
92
|
+
const rest = normalized.slice(header[0].length);
|
|
93
|
+
const target = (quoted) => (quoted?.startsWith("%~dp0\\") ? shimPath(quoted.slice(6), true) : shimPath(quoted, false));
|
|
94
|
+
const javascript = /^@IF EXIST "%~dp0\\node\.exe" \(\n {2}"%~dp0\\node\.exe" {2}"(%~dp0\\[^"%\x00-\x1f]+|[^"%\x00-\x1f]+)" %\*\n/.exec(rest);
|
|
95
|
+
if (javascript && rest === pnpmJs.replaceAll("\0", javascript[1]) && target(javascript[1])) return { entry: target(javascript[1]), node: null };
|
|
96
|
+
const pinned = /^@"([A-Za-z]:\\[^"%\x00-\x1f]*\\node\.exe)" {2}"(%~dp0\\[^"%\x00-\x1f]+|[^"%\x00-\x1f]+)" %\*\n$/i.exec(rest);
|
|
97
|
+
if (pinned && target(pinned[2])) return { entry: target(pinned[2]), node: pinned[1] };
|
|
98
|
+
const native = /^@"(%~dp0\\[^"%\x00-\x1f]+|[^"%\x00-\x1f]+)" {3}%\*\n$/.exec(rest);
|
|
99
|
+
return native && target(native[1]) ? { exe: target(native[1]) } : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function regular(path) {
|
|
103
|
+
try { const info = lstatSync(path); return info.isFile() && !info.isSymbolicLink(); }
|
|
104
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
105
|
+
}
|
|
106
|
+
function exists(path) {
|
|
107
|
+
try { lstatSync(path); return true; }
|
|
108
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
109
|
+
}
|
|
110
|
+
export function windowsProcessCheck(command, args, env) {
|
|
111
|
+
// Owned direct children only, not a tree/global kill. Descendant-held stdio
|
|
112
|
+
// is outside this guarantee. Never execute .cmd through a command interpreter.
|
|
113
|
+
if (/\.(?:cmd|bat)$/i.test(command)) throw new Error("Unproven command interpreter target");
|
|
114
|
+
// A neutral cwd: inside a project (the bundle itself), pnpm 11 reports, and may
|
|
115
|
+
// fetch, the version that project's packageManager pins instead of its own.
|
|
116
|
+
const result = spawnSync(command, args, { cwd: parse(command).root || undefined, env, shell: false, encoding: "utf8", timeout: 15000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, windowsHide: true });
|
|
117
|
+
if (result.error || result.status !== 0) throw new Error("Windows prerequisite process failed");
|
|
118
|
+
return result.stdout.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Bounded fixture model of the fixed native predicate, not an ACL evaluator.
|
|
122
|
+
* ReadAndExecute + Synchronize are harmless for this mutation boundary. Only
|
|
123
|
+
* distant existing directories may also allow sibling CreateDirectories (4).
|
|
124
|
+
* Effective inherited ACEs are checked again on every actual path component.
|
|
125
|
+
*/
|
|
126
|
+
export function windowsAclRuleUnsafe({ rights, depth, allow = true, inheritOnly = false, trusted = false }) {
|
|
127
|
+
if (!Number.isInteger(rights) || rights < 0 || rights > 0xffffffff || !Number.isInteger(depth) || depth < 0) throw new Error("Unknown Windows ACL fixture");
|
|
128
|
+
if (!allow || inheritOnly || trusted) return false;
|
|
129
|
+
const allowedRights = depth >= 2 ? 0x1200ad : 0x1200a9;
|
|
130
|
+
return (rights & ~allowedRights) !== 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fixed stock PowerShell intrinsics, not a loaded/evaluated PS script. Paths
|
|
134
|
+
// travel only as environment data. Managed constraints/denials fail closed.
|
|
135
|
+
// ACLs are read through .NET, never Get-Acl: a PowerShell 7 parent's PSModulePath
|
|
136
|
+
// makes Windows PowerShell 5.1 fail to autoload Microsoft.PowerShell.Security.
|
|
137
|
+
// An intentional rejection prints one fixed `unsafe:<role>-<check>` code (the role
|
|
138
|
+
// of the component, never its path); any other exception still exits nonzero.
|
|
139
|
+
// A walk rejection appends `|` and the base64 of the UTF-8 text
|
|
140
|
+
// `<component>|<SID>|<account>|<rights>` (fields after the path may be empty):
|
|
141
|
+
// the folder, the principal and the rights that failed, for user-facing guidance.
|
|
142
|
+
const aclCheck = String.raw`
|
|
143
|
+
$ErrorActionPreference = 'Stop';
|
|
144
|
+
$detail = '';
|
|
145
|
+
try {
|
|
146
|
+
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'policy' };
|
|
147
|
+
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
|
|
148
|
+
$trusted = @($me, 'S-1-5-18', 'S-1-5-32-544', 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464');
|
|
149
|
+
$account = { param($sid) try { $sid.Translate([Security.Principal.NTAccount]).Value } catch { '' } };
|
|
150
|
+
$path = [IO.Path]::GetFullPath($env:GENTLE_WINDOWS_CHECK);
|
|
151
|
+
$depth = 0;
|
|
152
|
+
while ($path) {
|
|
153
|
+
$role = 'ancestor'; if ($depth -eq 0) { $role = 'target' } elseif ($depth -eq 1) { $role = 'parent' };
|
|
154
|
+
$item = Get-Item -LiteralPath $path -Force;
|
|
155
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
|
|
156
|
+
if ($item.PSIsContainer) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
|
|
157
|
+
$owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]);
|
|
158
|
+
if ($trusted -notcontains $owner.Value) { $detail = $path + '|' + $owner.Value + '|' + (& $account $owner) + '|'; throw ($role + '-owner') };
|
|
159
|
+
$allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };
|
|
160
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
|
|
161
|
+
if ($rule.AccessControlType -eq 'Allow' -and -not ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -and ([long]$rule.FileSystemRights -band (-bnot [long]$allowedRights)) -and $trusted -notcontains $rule.IdentityReference.Value) {
|
|
162
|
+
$detail = $path + '|' + $rule.IdentityReference.Value + '|' + (& $account $rule.IdentityReference) + '|0x' + ([int]$rule.FileSystemRights).ToString('X8'); throw ($role + '-acl-mask') };
|
|
163
|
+
};
|
|
164
|
+
$parent = [IO.Directory]::GetParent($path);
|
|
165
|
+
if ($null -eq $parent) { break }; $path = $parent.FullName; $depth++;
|
|
166
|
+
};
|
|
167
|
+
'safe'
|
|
168
|
+
} catch { if ($_.Exception.Message -cmatch '^(policy|(target|parent|ancestor)-(reparse|owner|acl-mask))$') { 'unsafe:' + $_.Exception.Message + $(if ($detail) { '|' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($detail)) } else { '' }) } else { throw } }
|
|
169
|
+
`;
|
|
170
|
+
// S6 notice: the same walk over many paths in one launch, for the plan's notice
|
|
171
|
+
// about reused tools in folders another account can change. The paths travel as
|
|
172
|
+
// one environment value joined by `|`, which no Windows path holds. One line per
|
|
173
|
+
// path, in order: `safe`, a walk rejection exactly as above, or `unknown` for any
|
|
174
|
+
// other error on that path (such as an owner that denies READ_CONTROL). A policy
|
|
175
|
+
// denial prints only `unsafe:policy`.
|
|
176
|
+
const aclCheckMany = String.raw`
|
|
177
|
+
$ErrorActionPreference = 'Stop';
|
|
178
|
+
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { 'unsafe:policy'; exit 0 };
|
|
179
|
+
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
|
|
180
|
+
$trusted = @($me, 'S-1-5-18', 'S-1-5-32-544', 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464');
|
|
181
|
+
$account = { param($sid) try { $sid.Translate([Security.Principal.NTAccount]).Value } catch { '' } };
|
|
182
|
+
foreach ($start in $env:GENTLE_WINDOWS_CHECKS.Split('|')) {
|
|
183
|
+
$detail = '';
|
|
184
|
+
try {
|
|
185
|
+
$path = [IO.Path]::GetFullPath($start);
|
|
186
|
+
$depth = 0;
|
|
187
|
+
while ($path) {
|
|
188
|
+
$role = 'ancestor'; if ($depth -eq 0) { $role = 'target' } elseif ($depth -eq 1) { $role = 'parent' };
|
|
189
|
+
$item = Get-Item -LiteralPath $path -Force;
|
|
190
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
|
|
191
|
+
if ($item.PSIsContainer) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
|
|
192
|
+
$owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]);
|
|
193
|
+
if ($trusted -notcontains $owner.Value) { $detail = $path + '|' + $owner.Value + '|' + (& $account $owner) + '|'; throw ($role + '-owner') };
|
|
194
|
+
$allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };
|
|
195
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
|
|
196
|
+
if ($rule.AccessControlType -eq 'Allow' -and -not ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -and ([long]$rule.FileSystemRights -band (-bnot [long]$allowedRights)) -and $trusted -notcontains $rule.IdentityReference.Value) {
|
|
197
|
+
$detail = $path + '|' + $rule.IdentityReference.Value + '|' + (& $account $rule.IdentityReference) + '|0x' + ([int]$rule.FileSystemRights).ToString('X8'); throw ($role + '-acl-mask') };
|
|
198
|
+
};
|
|
199
|
+
$parent = [IO.Directory]::GetParent($path);
|
|
200
|
+
if ($null -eq $parent) { break }; $path = $parent.FullName; $depth++;
|
|
201
|
+
};
|
|
202
|
+
'safe'
|
|
203
|
+
} catch { if ($_.Exception.Message -cmatch '^(target|parent|ancestor)-(reparse|owner|acl-mask)$') { 'unsafe:' + $_.Exception.Message + $(if ($detail) { '|' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($detail)) } else { '' }) } else { 'unknown' } }
|
|
204
|
+
};
|
|
205
|
+
`;
|
|
206
|
+
const storageChecks = /^(?:policy|(?:target|parent|ancestor)-(?:reparse|owner|acl-mask))$/;
|
|
207
|
+
const untrustedStorage = /^(?:target|parent|ancestor)-(?:reparse|owner|acl-mask)$/;
|
|
208
|
+
const storageOutput = /^unsafe:([a-z-]+)(?:\|([A-Za-z0-9+/]+={0,2}))?$/;
|
|
209
|
+
/** The decoded walk detail, or null when it is not exactly that shape. */
|
|
210
|
+
function storageDetail(encoded) {
|
|
211
|
+
const text = Buffer.from(encoded, "base64").toString("utf8");
|
|
212
|
+
if (Buffer.from(text, "utf8").toString("base64") !== encoded || /[\x00-\x1f\x7f]/.test(text)) return null;
|
|
213
|
+
const [path, sid, account, rights, ...rest] = text.split("|");
|
|
214
|
+
if (rest.length > 0 || rights === undefined || !win32.isAbsolute(path) || (sid !== "" && !/^S-1-\d+(?:-\d+)*$/.test(sid)) ||
|
|
215
|
+
(rights !== "" && !/^0x[0-9A-F]{8}$/.test(rights))) return null;
|
|
216
|
+
return { at: path, ...(sid ? { sid } : {}), ...(account ? { account } : {}), ...(rights ? { rights } : {}) };
|
|
217
|
+
}
|
|
218
|
+
/** Only exact `safe` passes. A fixed rejection code is kept as `check`, and a
|
|
219
|
+
* well-formed walk detail as `detail` ({ at, sid?, account?, rights? }); any other
|
|
220
|
+
* output is rejected without carrying its text.
|
|
221
|
+
*/
|
|
222
|
+
export function windowsStorageEvidence(output) {
|
|
223
|
+
if (output === "safe") return;
|
|
224
|
+
const match = typeof output === "string" ? storageOutput.exec(output) : null;
|
|
225
|
+
const detail = match?.[2] === undefined ? undefined : storageDetail(match[2]);
|
|
226
|
+
const known = match !== null && storageChecks.test(match[1]) && detail !== null && (detail === undefined || untrustedStorage.test(match[1]));
|
|
227
|
+
throw Object.assign(new Error("Windows ACL evidence rejected"), known ? { check: match[1], ...(detail ? { detail } : {}) } : {});
|
|
228
|
+
}
|
|
229
|
+
function windowsPowerShell(env, script, data, processAdapter) {
|
|
230
|
+
// Windows process.env is case-insensitive, but a copied environment is not.
|
|
231
|
+
const systemRoot = env.SystemRoot ?? Object.entries(env).find(([key]) => key.toLowerCase() === "systemroot")?.[1];
|
|
232
|
+
return processAdapter(join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], { ...env, ...data });
|
|
233
|
+
}
|
|
234
|
+
export function verifyWindowsStorage(path, env, processAdapter = windowsProcessCheck) {
|
|
235
|
+
if (process.platform !== "win32") throw new Error("Native Windows storage verification unavailable");
|
|
236
|
+
if (!win32.isAbsolute(path) || path.startsWith("\\\\")) throw new Error("Unsafe Windows storage path");
|
|
237
|
+
windowsStorageEvidence(windowsPowerShell(env, aclCheck, { GENTLE_WINDOWS_CHECK: path }, processAdapter));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Walks every path (local drive paths, never holding `|`) in one Windows
|
|
241
|
+
* PowerShell launch (aclCheckMany). Returns one entry per path, in order: null
|
|
242
|
+
* when it passes, { check: "unchecked", at } when its walk failed for another
|
|
243
|
+
* reason (it could not be checked), else what failed,
|
|
244
|
+
* { check, at?, sid?, account?, rights? }. A policy denial, a result count that
|
|
245
|
+
* does not match, or any other line rejects the whole result; nothing to walk
|
|
246
|
+
* launches nothing.
|
|
247
|
+
*/
|
|
248
|
+
export function verifyWindowsStorageMany(paths, env, { processAdapter = windowsProcessCheck, platform = process.platform } = {}) {
|
|
249
|
+
if (platform !== "win32") throw new Error("Native Windows storage verification unavailable");
|
|
250
|
+
if (paths.some((path) => typeof path !== "string" || !/^[A-Za-z]:\\/.test(path) || /[|\x00-\x1f]/.test(path))) throw new Error("Unsafe Windows storage path");
|
|
251
|
+
if (paths.length === 0) return [];
|
|
252
|
+
const rejected = () => new Error("Windows ACL evidence rejected");
|
|
253
|
+
const lines = windowsPowerShell(env, aclCheckMany, { GENTLE_WINDOWS_CHECKS: paths.join("|") }, processAdapter).split(/\r?\n/);
|
|
254
|
+
if (lines.length !== paths.length) throw rejected();
|
|
255
|
+
return lines.map((line, index) => {
|
|
256
|
+
if (line === "safe") return null;
|
|
257
|
+
if (line === "unknown") return { check: "unchecked", at: paths[index] };
|
|
258
|
+
const match = storageOutput.exec(line);
|
|
259
|
+
const detail = match?.[2] === undefined ? undefined : storageDetail(match[2]);
|
|
260
|
+
if (!match || !untrustedStorage.test(match[1]) || detail === null) throw rejected();
|
|
261
|
+
return { check: match[1], ...(detail ?? {}) };
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// S6: the wizard's PNPM_HOME on Windows. pnpm runs and persists binaries there,
|
|
266
|
+
// so it gets the same walk as the bootstrap's tools directory before consent.
|
|
267
|
+
export const privatePnpmHome = Object.freeze({ directory: ".pnpm", marker: ".gentle-shell-pnpm-home", text: "gentle-pi private pnpm home", temp: "tmp" });
|
|
268
|
+
const hostHomeFs = Object.freeze({
|
|
269
|
+
/** "missing", "directory" or "file" (neither a link), or "other". */
|
|
270
|
+
kind(path) {
|
|
271
|
+
let info;
|
|
272
|
+
try { info = lstatSync(path); }
|
|
273
|
+
catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") return "missing"; throw error; }
|
|
274
|
+
if (info.isSymbolicLink()) return "other";
|
|
275
|
+
return info.isDirectory() ? "directory" : info.isFile() ? "file" : "other";
|
|
276
|
+
},
|
|
277
|
+
entries: (path) => readdirSync(path),
|
|
278
|
+
readText: (path) => readFileSync(path, "utf8"),
|
|
279
|
+
});
|
|
280
|
+
function windowsEnv(env, name) {
|
|
281
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
282
|
+
return key === undefined ? undefined : env[key];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Which PNPM_HOME the wizard may use, decided before any probe runs:
|
|
286
|
+
* - a PNPM_HOME the user set, or pnpm's default `%LOCALAPPDATA%\pnpm`, that passes
|
|
287
|
+
* the walk: { available: true, path, source: "user" | "default" };
|
|
288
|
+
* - a default that fails it while holding nothing (absent or an empty directory):
|
|
289
|
+
* the private `%USERPROFILE%\.pnpm`, which must pass the walk too and be absent,
|
|
290
|
+
* empty or marked as created by this flow:
|
|
291
|
+
* { available: true, path, source: "private", rejected: { path, check, at, sid?, account?, rights? } };
|
|
292
|
+
* - otherwise a blocker: { available: true, path, source, untrusted, installed?, private? }.
|
|
293
|
+
* The walk covers the folder (or its nearest existing ancestor when it is absent)
|
|
294
|
+
* and its `bin` when present. Null when PNPM_HOME is unknowable, as for
|
|
295
|
+
* pnpmGlobalBin. Policy denials and unexpected errors throw.
|
|
296
|
+
*/
|
|
297
|
+
export function windowsPnpmHome({ env, storage = verifyWindowsStorage, fs = hostHomeFs }) {
|
|
298
|
+
const absolute = (value) => typeof value === "string" && win32.isAbsolute(value) && !value.startsWith("\\\\");
|
|
299
|
+
const normal = (value) => win32.normalize(value).replace(/(.)[\\/]+$/, "$1");
|
|
300
|
+
const walk = (home) => {
|
|
301
|
+
let target = home;
|
|
302
|
+
while (fs.kind(target) === "missing") {
|
|
303
|
+
const parent = win32.dirname(target);
|
|
304
|
+
if (parent === target) throw new Error("Unknown Windows PNPM_HOME");
|
|
305
|
+
target = parent;
|
|
306
|
+
}
|
|
307
|
+
const bin = win32.join(home, "bin");
|
|
308
|
+
for (const path of target === home && fs.kind(bin) === "directory" ? [home, bin] : [target]) {
|
|
309
|
+
try {
|
|
310
|
+
storage(path, env);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (!untrustedStorage.test(error?.check ?? "")) throw error;
|
|
313
|
+
return { check: error.check, at: path, ...(error.detail ?? {}) };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
// "absent", "empty", "marked" (this flow created it) or "used" (anything else).
|
|
319
|
+
const contents = (home) => {
|
|
320
|
+
const kind = fs.kind(home);
|
|
321
|
+
if (kind === "missing") return "absent";
|
|
322
|
+
if (kind !== "directory") return "used";
|
|
323
|
+
if (fs.entries(home).length === 0) return "empty";
|
|
324
|
+
const marker = win32.join(home, privatePnpmHome.marker);
|
|
325
|
+
return fs.kind(marker) === "file" && fs.readText(marker) === privatePnpmHome.text ? "marked" : "used";
|
|
326
|
+
};
|
|
327
|
+
const set = windowsEnv(env, "PNPM_HOME");
|
|
328
|
+
if (set !== undefined) {
|
|
329
|
+
if (!absolute(set)) return null;
|
|
330
|
+
const path = normal(set);
|
|
331
|
+
const untrusted = walk(path);
|
|
332
|
+
return { available: true, path, source: "user", ...(untrusted ? { untrusted } : {}) };
|
|
333
|
+
}
|
|
334
|
+
const local = windowsEnv(env, "LOCALAPPDATA");
|
|
335
|
+
if (!absolute(local)) return null;
|
|
336
|
+
const path = normal(win32.join(local, "pnpm"));
|
|
337
|
+
const untrusted = walk(path);
|
|
338
|
+
if (!untrusted) return { available: true, path, source: "default" };
|
|
339
|
+
const blocked = { available: true, path, source: "default", untrusted };
|
|
340
|
+
// Never moved, deleted or adopted: anything already there is an installation.
|
|
341
|
+
if (!["absent", "empty"].includes(contents(path))) return { ...blocked, installed: true };
|
|
342
|
+
const profile = windowsEnv(env, "USERPROFILE");
|
|
343
|
+
if (!absolute(profile)) return blocked;
|
|
344
|
+
const own = normal(win32.join(profile, privatePnpmHome.directory));
|
|
345
|
+
if (contents(own) === "used") return { ...blocked, private: { path: own, foreign: true } };
|
|
346
|
+
const ownUntrusted = walk(own);
|
|
347
|
+
if (ownUntrusted) return { ...blocked, private: { path: own, untrusted: ownUntrusted } };
|
|
348
|
+
return { available: true, path: own, source: "private", rejected: { path, ...untrusted } };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The wizard's environment and PNPM_HOME decision (windowsPnpmHome): a private
|
|
352
|
+
* PNPM_HOME is set for this process's children only (childEnvironment adds pnpm's
|
|
353
|
+
* own folders there). A walk that cannot finish is { available: null, failed: true }:
|
|
354
|
+
* preflight then blocks.
|
|
355
|
+
*/
|
|
356
|
+
export function windowsWizardEnvironment({ env, ...adapters }) {
|
|
357
|
+
let pnpmHome;
|
|
358
|
+
try {
|
|
359
|
+
pnpmHome = windowsPnpmHome({ env, ...adapters });
|
|
360
|
+
} catch {
|
|
361
|
+
return { env, pnpmHome: { available: null, failed: true } };
|
|
362
|
+
}
|
|
363
|
+
if (pnpmHome?.source !== "private") return { env, pnpmHome };
|
|
364
|
+
return { env: { ...env, PNPM_HOME: pnpmHome.path }, pnpmHome };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The private PNPM_HOME, after consent: created, or an empty one adopted, with the
|
|
368
|
+
// bootstrap claim's protected DACL (owner and FullControl for the invoking SID,
|
|
369
|
+
// SYSTEM and Administrators only), read back, then marked. A marked one is kept
|
|
370
|
+
// as it is. Anything else is `foreign` and never changed. Either way its `tmp`
|
|
371
|
+
// folder (the children's TEMP/TMP) is created inside it, inheriting that DACL.
|
|
372
|
+
const pnpmHomeClaim = String.raw`
|
|
373
|
+
$ErrorActionPreference = 'Stop';
|
|
374
|
+
$target = $null; $created = $false;
|
|
375
|
+
try {
|
|
376
|
+
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'policy' };
|
|
377
|
+
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User;
|
|
378
|
+
$target = [IO.Path]::GetFullPath($env:GENTLE_WINDOWS_PNPM_HOME); if ($target.StartsWith('\')) { throw 'foreign' };
|
|
379
|
+
$marker = Join-Path $target '.gentle-shell-pnpm-home'; $result = 'claimed';
|
|
380
|
+
if (Test-Path -LiteralPath $target) {
|
|
381
|
+
$item = Get-Item -LiteralPath $target -Force; if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'foreign' };
|
|
382
|
+
if (@(Get-ChildItem -LiteralPath $target -Force).Count -ne 0) {
|
|
383
|
+
if (-not (Test-Path -LiteralPath $marker)) { throw 'foreign' };
|
|
384
|
+
$file = Get-Item -LiteralPath $marker -Force;
|
|
385
|
+
if ($file.PSIsContainer -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Get-Content -LiteralPath $marker -Raw) -ne 'gentle-pi private pnpm home') { throw 'foreign' };
|
|
386
|
+
$result = 'kept';
|
|
387
|
+
};
|
|
388
|
+
} else { $null = New-Item -ItemType Directory -Path $target; $created = $true };
|
|
389
|
+
if ($result -eq 'claimed') {
|
|
390
|
+
$acl = New-Object Security.AccessControl.DirectorySecurity; $acl.SetOwner($me); $acl.SetAccessRuleProtection($true,$false);
|
|
391
|
+
foreach ($sid in @($me.Value,'S-1-5-18','S-1-5-32-544')) {
|
|
392
|
+
$identity = New-Object Security.Principal.SecurityIdentifier($sid);
|
|
393
|
+
$rule = New-Object Security.AccessControl.FileSystemAccessRule($identity,'FullControl','ContainerInherit,ObjectInherit','None','Allow'); $acl.AddAccessRule($rule);
|
|
394
|
+
}; [IO.Directory]::SetAccessControl($target,$acl);
|
|
395
|
+
$verified = [IO.Directory]::GetAccessControl($target); if (-not $verified.AreAccessRulesProtected) { throw 'protected-dacl' };
|
|
396
|
+
if ($verified.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $me.Value) { throw 'private-owner' };
|
|
397
|
+
foreach ($rule in $verified.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) { if ($rule.AccessControlType -ne 'Allow' -or @($me.Value,'S-1-5-18','S-1-5-32-544') -notcontains $rule.IdentityReference.Value) { throw 'private-ace' } };
|
|
398
|
+
$null = New-Item -ItemType File -Path $marker -Value 'gentle-pi private pnpm home';
|
|
399
|
+
};
|
|
400
|
+
$temp = Join-Path $target 'tmp';
|
|
401
|
+
if (Test-Path -LiteralPath $temp) { $folder = Get-Item -LiteralPath $temp -Force; if (-not $folder.PSIsContainer -or ($folder.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'foreign' } } else { $null = New-Item -ItemType Directory -Path $temp };
|
|
402
|
+
$result
|
|
403
|
+
} catch {
|
|
404
|
+
if ($created) { Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue };
|
|
405
|
+
if ($_.Exception.Message -cmatch '^(policy|foreign|protected-dacl|private-owner|private-ace)$') { 'unsafe:' + $_.Exception.Message } else { throw }
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
/** Claims the private PNPM_HOME (pnpmHomeClaim), then walks it and its `tmp`
|
|
409
|
+
* folder like any storage. Returns "claimed" or "kept"; anything else throws.
|
|
410
|
+
*/
|
|
411
|
+
export function ensureWindowsPnpmHome(home, env, { processAdapter = windowsProcessCheck, storage = verifyWindowsStorage, platform = process.platform } = {}) {
|
|
412
|
+
if (platform !== "win32") throw new Error("Native Windows storage verification unavailable");
|
|
413
|
+
if (!win32.isAbsolute(home) || home.startsWith("\\\\")) throw new Error("Unsafe Windows storage path");
|
|
414
|
+
const output = windowsPowerShell(env, pnpmHomeClaim, { GENTLE_WINDOWS_PNPM_HOME: home }, processAdapter);
|
|
415
|
+
if (output !== "claimed" && output !== "kept") {
|
|
416
|
+
const check = /^unsafe:(policy|foreign|protected-dacl|private-owner|private-ace)$/.exec(output)?.[1];
|
|
417
|
+
throw Object.assign(new Error("Windows PNPM_HOME claim rejected"), check ? { check } : {});
|
|
418
|
+
}
|
|
419
|
+
storage(home, env);
|
|
420
|
+
storage(win32.join(home, privatePnpmHome.temp), env);
|
|
421
|
+
return output;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The same claim for another private folder (the bundled install's prefix): only
|
|
425
|
+
// the folder, its marker file and the marker text differ, and they travel as
|
|
426
|
+
// environment data like every other path.
|
|
427
|
+
const privateFolderClaim = [["$env:GENTLE_WINDOWS_PNPM_HOME", "$env:GENTLE_WINDOWS_PRIVATE_FOLDER"],
|
|
428
|
+
["'.gentle-shell-pnpm-home'", "$env:GENTLE_WINDOWS_PRIVATE_MARKER"], ["'gentle-pi private pnpm home'", "$env:GENTLE_WINDOWS_PRIVATE_TEXT"]]
|
|
429
|
+
.reduce((script, [from, to]) => {
|
|
430
|
+
if (!script.includes(from)) throw new Error("Windows private folder claim template changed");
|
|
431
|
+
return script.replaceAll(from, to);
|
|
432
|
+
}, pnpmHomeClaim);
|
|
433
|
+
/** ensureWindowsPnpmHome for another private folder, marked with `marker`
|
|
434
|
+
* (a `.name` file) holding `text`. Returns "claimed" or "kept"; anything else,
|
|
435
|
+
* including a non-empty unmarked folder, throws.
|
|
436
|
+
*/
|
|
437
|
+
export function ensureWindowsPrivateFolder(folder, env, { marker, text, processAdapter = windowsProcessCheck, storage = verifyWindowsStorage, platform = process.platform }) {
|
|
438
|
+
if (platform !== "win32") throw new Error("Native Windows storage verification unavailable");
|
|
439
|
+
if (!win32.isAbsolute(folder) || folder.startsWith("\\\\")) throw new Error("Unsafe Windows storage path");
|
|
440
|
+
if (!/^\.[a-z][a-z-]*$/.test(marker) || !/^[a-z][a-z -]*$/.test(text)) throw new Error("Unsafe Windows private folder marker");
|
|
441
|
+
const output = windowsPowerShell(env, privateFolderClaim, { GENTLE_WINDOWS_PRIVATE_FOLDER: folder, GENTLE_WINDOWS_PRIVATE_MARKER: marker, GENTLE_WINDOWS_PRIVATE_TEXT: text }, processAdapter);
|
|
442
|
+
if (output !== "claimed" && output !== "kept") {
|
|
443
|
+
const check = /^unsafe:(policy|foreign|protected-dacl|private-owner|private-ace)$/.exec(output)?.[1];
|
|
444
|
+
throw Object.assign(new Error("Windows private folder claim rejected"), check ? { check } : {});
|
|
445
|
+
}
|
|
446
|
+
storage(folder, env);
|
|
447
|
+
storage(win32.join(folder, privatePnpmHome.temp), env);
|
|
448
|
+
return output;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Strict bounded tar reader: no system tar, archive links or archive execution.
|
|
452
|
+
* Unsupported GNU/PAX extensions stop rather than extract using guessed semantics.
|
|
453
|
+
*/
|
|
454
|
+
export function readWindowsPnpmArchive(bytes) {
|
|
455
|
+
const tar = gunzipSync(bytes, { maxOutputLength: maxExpandedBytes });
|
|
456
|
+
const entries = [];
|
|
457
|
+
let offset = 0;
|
|
458
|
+
let ended = false;
|
|
459
|
+
const text = (block, start, length) => {
|
|
460
|
+
const value = block.subarray(start, start + length);
|
|
461
|
+
const zero = value.indexOf(0);
|
|
462
|
+
const data = zero === -1 ? value : value.subarray(0, zero);
|
|
463
|
+
const result = data.toString("utf8");
|
|
464
|
+
if (!Buffer.from(result).equals(data)) throw new Error("Unsafe tar text encoding");
|
|
465
|
+
return result;
|
|
466
|
+
};
|
|
467
|
+
const octal = (value) => {
|
|
468
|
+
if (!/^[0-7]+$/.test(value.trim())) throw new Error("Unsafe tar number");
|
|
469
|
+
return Number.parseInt(value.trim(), 8);
|
|
470
|
+
};
|
|
471
|
+
while (offset + 512 <= tar.length) {
|
|
472
|
+
const block = tar.subarray(offset, offset + 512);
|
|
473
|
+
if (block.every((value) => value === 0)) {
|
|
474
|
+
if (tar.length < offset + 1024 || !tar.subarray(offset).every((value) => value === 0)) throw new Error("Unsafe tar termination");
|
|
475
|
+
ended = true;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
const expected = octal(text(block, 148, 8));
|
|
479
|
+
let checksum = 0;
|
|
480
|
+
for (let index = 0; index < 512; index += 1) checksum += index >= 148 && index < 156 ? 32 : block[index];
|
|
481
|
+
if (checksum !== expected || text(block, 257, 6) !== "ustar") throw new Error("Unsafe tar header");
|
|
482
|
+
const size = octal(text(block, 124, 12));
|
|
483
|
+
const prefix = text(block, 345, 155);
|
|
484
|
+
const name = `${prefix ? `${prefix}/` : ""}${text(block, 0, 100)}`;
|
|
485
|
+
const type = block[156];
|
|
486
|
+
if (![0, 48, 53].includes(type) || text(block, 157, 100) || (type === 53 && size !== 0)) throw new Error("Unsafe tar entry type");
|
|
487
|
+
const next = offset + 512 + Math.ceil(size / 512) * 512;
|
|
488
|
+
if (next > tar.length) throw new Error("Unsafe truncated tar");
|
|
489
|
+
entries.push({ name, directory: type === 53, bytes: tar.subarray(offset + 512, offset + 512 + size) });
|
|
490
|
+
if (entries.length > 50000) throw new Error("Unsafe tar entry count");
|
|
491
|
+
offset = next;
|
|
492
|
+
}
|
|
493
|
+
if (!ended) throw new Error("Unsafe tar termination");
|
|
494
|
+
validateWindowsEntries(entries);
|
|
495
|
+
if (entries.some((entry) => !entry.name.startsWith("package/") && entry.name !== "package")) throw new Error("Unsafe pnpm package root");
|
|
496
|
+
return entries;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The inherited PATHEXT, validated and lowercased, in order. */
|
|
500
|
+
function pathExtensions(env) {
|
|
501
|
+
const pathExt = Object.entries(env).find(([key]) => key.toLowerCase() === "pathext")?.[1];
|
|
502
|
+
if (typeof pathExt !== "string") throw new Error("Unknown Windows PATH/PATHEXT");
|
|
503
|
+
// Validate before lowercasing: case folding can map non-ASCII (U+212A KELVIN SIGN)
|
|
504
|
+
// onto ASCII and make a different name look like a known extension.
|
|
505
|
+
const entries = pathExt.split(";");
|
|
506
|
+
if (entries.some((extension) => !/^\.[A-Za-z0-9]+$/.test(extension))) throw new Error("Unknown Windows PATHEXT semantics");
|
|
507
|
+
const extensions = entries.map((extension) => extension.toLowerCase());
|
|
508
|
+
// Any well-formed extension is resolved in its PATHEXT place, like CMD does:
|
|
509
|
+
// Windows PowerShell 5.1 appends .CPL, Python adds .PY and .PYW (#1978), other
|
|
510
|
+
// runtimes add their own. Resolved is not accepted: a .cpl, .py (or any
|
|
511
|
+
// non-.cmd wrapper / non-.exe Node) found first still fails closed below.
|
|
512
|
+
if (!extensions.length || new Set(extensions).size !== extensions.length || extensions.some((extension) => !/^\.[a-z0-9]+$/.test(extension))) throw new Error("Unknown Windows PATHEXT semantics");
|
|
513
|
+
return extensions;
|
|
514
|
+
}
|
|
515
|
+
function findWindowsCommand(env, name = "pnpm") {
|
|
516
|
+
// Alternate CMD cwd-search policy is not silently flattened into our model.
|
|
517
|
+
if (Object.keys(env).some((key) => key.toLowerCase() === "nodefaultcurrentdirectoryinexepath")) throw new Error("Unknown Windows cwd-search semantics");
|
|
518
|
+
const path = Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1];
|
|
519
|
+
if (typeof path !== "string") throw new Error("Unknown Windows PATH/PATHEXT");
|
|
520
|
+
const extensions = pathExtensions(env);
|
|
521
|
+
// CMD ignores empty entries (a trailing `;` is the Windows default); every
|
|
522
|
+
// other non-absolute, quoted or UNC entry still fails closed.
|
|
523
|
+
for (const directory of [process.cwd(), ...path.split(";").filter((entry) => entry !== "")]) {
|
|
524
|
+
if (!win32.isAbsolute(directory) || directory.startsWith("\\\\") || directory.includes('"')) throw new Error("Unknown Windows PATH");
|
|
525
|
+
// CMD searches cwd too; honor the wrapper's inherited PATHEXT order.
|
|
526
|
+
for (const extension of extensions) {
|
|
527
|
+
const command = join(directory, `${name}${extension}`);
|
|
528
|
+
if (exists(command)) return command;
|
|
529
|
+
}
|
|
530
|
+
// npm, pnpm, Volta and mise write an extensionless Git Bash script beside
|
|
531
|
+
// each .cmd shim, and CMD runs the PATHEXT match in that directory. An
|
|
532
|
+
// extensionless/other target alone must block rather than be silently skipped.
|
|
533
|
+
if (exists(join(directory, name))) throw new Error("Unknown extensionless Windows prerequisite");
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
538
|
+
// Not pnpm: Corepack's pnpm.js behind npm's or pnpm's cmd-shim, and mise's file-mode
|
|
539
|
+
// shim (`mise x -- pnpm %*`, or the older `mise x -- %*`; github.com/jdx/mise src/shims.rs).
|
|
540
|
+
const corepackEntry = /(?:^|\\)node_modules\\corepack\\dist\\pnpm\.js$/i;
|
|
541
|
+
const miseFileShim = /^@echo off(\r?\n)setlocal\1mise x -- (?:pnpm )?%\*\1$/;
|
|
542
|
+
function proveGlobal(command, prefix, env, processAdapter) {
|
|
543
|
+
for (const capability of ["add", "bin"]) {
|
|
544
|
+
if (!/(?:^|[\s,])--global(?:[\s,=]|$)/.test(processAdapter(command, [...prefix, "help", capability], env))) throw new Error("Windows pnpm global capability rejected");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function proveCli(node, entry, metadata, env, processAdapter) {
|
|
548
|
+
const version = processAdapter(node, ["--version"], env);
|
|
549
|
+
if (metadata.name !== "pnpm" || !STABLE.test(metadata.version) || !compatibleEngine(windowsNodeFloor, version) || !compatibleEngine(metadata.engines?.node, version)) {
|
|
550
|
+
throw new Error("Windows pnpm engine or identity rejected");
|
|
551
|
+
}
|
|
552
|
+
if (processAdapter(node, [entry, "--version"], env) !== metadata.version) throw new Error("Windows pnpm version rejected");
|
|
553
|
+
proveGlobal(node, [entry], env, processAdapter);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Returns a direct invocation, NOT a fabricated npm/pnpm executable shim.
|
|
557
|
+
* T4 must call command + prefix with shell:false and retain this child env.
|
|
558
|
+
* `onStep` receives fixed phase names for diagnostics only; it never alters checks.
|
|
559
|
+
* The pnpm found is a known shim (windowsShim) of pnpm's own JS entry, run by
|
|
560
|
+
* the Node that shim selects, or a native pnpm.exe (pnpm's installer and
|
|
561
|
+
* `pnpm setup`, @pnpm/exe, pnpm 12, a Volta or mise shim), directly or as a
|
|
562
|
+
* shim's target (an extensionless target resolves through PATHEXT, and only an
|
|
563
|
+
* .exe first match counts); the package.json beside that exe must name pnpm and
|
|
564
|
+
* its version must be what the exe reports.
|
|
565
|
+
* One whose stable version is of another major or older than the pin is left as
|
|
566
|
+
* it is: the verified pnpm is acquired instead. So is one whose wrapper, Node,
|
|
567
|
+
* entry, metadata or exe fails the reparse/owner/ACL walk (a %LOCALAPPDATA%
|
|
568
|
+
* another principal may write): it never runs, like an untrusted user Node in
|
|
569
|
+
* bootstrap.cmd. A pnpm.exe without pnpm's package.json beside it (a standalone
|
|
570
|
+
* or Volta/mise shim exe), Corepack's pnpm.cmd and mise's file shim are not
|
|
571
|
+
* proven pnpm and count as no pnpm at all: never run, the verified pnpm is
|
|
572
|
+
* acquired. Policy denials and unknown evidence still refuse.
|
|
573
|
+
*/
|
|
574
|
+
export async function ensureWindowsPnpm({ tools, env, node = process.execPath, adapters = {}, onStep = () => {} }) {
|
|
575
|
+
const processAdapter = adapters.process ?? windowsProcessCheck;
|
|
576
|
+
const storage = adapters.storage ?? verifyWindowsStorage;
|
|
577
|
+
onStep("pnpm-discovery");
|
|
578
|
+
const existing = (adapters.findCommand ?? findWindowsCommand)(env);
|
|
579
|
+
// Thrown only to fall through to acquisition; never escapes.
|
|
580
|
+
const notPnpm = new Error("Not a proven pnpm");
|
|
581
|
+
// Only storage checks carry a role code (and notPnpm); everything else keeps stopping.
|
|
582
|
+
if (existing) try {
|
|
583
|
+
onStep("wrapper-storage");
|
|
584
|
+
storage(existing, env);
|
|
585
|
+
onStep("wrapper");
|
|
586
|
+
const extension = extname(existing).toLowerCase();
|
|
587
|
+
if (![".cmd", ".exe"].includes(extension) || !regular(existing)) throw new Error("Unknown pnpm wrapper; refusing replacement");
|
|
588
|
+
const text = extension === ".cmd" ? readFileSync(existing, "utf8") : null;
|
|
589
|
+
if (text !== null && miseFileShim.test(text)) throw notPnpm;
|
|
590
|
+
const shim = extension === ".exe" ? { exe: parse(existing).base } : windowsShim(text);
|
|
591
|
+
if (shim?.entry !== undefined && corepackEntry.test(shim.entry)) throw notPnpm;
|
|
592
|
+
const directory = dirname(existing);
|
|
593
|
+
const target = (path) => (win32.isAbsolute(path) ? path : join(directory, ...path.split("\\")));
|
|
594
|
+
const entry = shim?.entry === undefined ? null : target(shim.entry);
|
|
595
|
+
const file = entry && parse(entry).base;
|
|
596
|
+
const packageDirectory = entry && dirname(dirname(entry));
|
|
597
|
+
// Only pnpm's own bin entry, <...>\node_modules\pnpm\bin\pnpm.(cjs|mjs).
|
|
598
|
+
const pnpmEntry = entry !== null && /^pnpm\.[cm]js$/.test(file) && parse(dirname(entry)).base === "bin" &&
|
|
599
|
+
parse(packageDirectory).base === "pnpm" && parse(dirname(packageDirectory)).base === "node_modules";
|
|
600
|
+
if (shim?.exe === undefined && !pnpmEntry) throw new Error("Unknown pnpm wrapper; refusing execution or replacement");
|
|
601
|
+
if (pnpmEntry) {
|
|
602
|
+
// npm cmd-shim selects its sibling node.exe before PATH node. Preserve it.
|
|
603
|
+
onStep("node-discovery");
|
|
604
|
+
const localNode = join(directory, "node.exe");
|
|
605
|
+
const selectedNode = shim.node ?? (exists(localNode) ? localNode : (adapters.findNode ?? ((env) => findWindowsCommand(env, "node")))(env));
|
|
606
|
+
if (!selectedNode || !/\.exe$/i.test(selectedNode)) throw new Error("Unknown wrapper-selected Node");
|
|
607
|
+
const metadataPath = join(packageDirectory, "package.json");
|
|
608
|
+
for (const [step, path] of [["node-storage", selectedNode], ["entry-storage", entry], ["metadata-storage", metadataPath]]) {
|
|
609
|
+
onStep(step);
|
|
610
|
+
storage(path, env);
|
|
611
|
+
if (!regular(path)) throw new Error("Unsafe pnpm wrapper target");
|
|
612
|
+
}
|
|
613
|
+
onStep("package");
|
|
614
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
615
|
+
if (metadata.bin?.pnpm !== `bin/${file}`) throw new Error("Windows pnpm package target rejected");
|
|
616
|
+
if (metadata.name !== "pnpm" || !STABLE.test(metadata.version ?? "") || pinnedPnpmCompatible(metadata.version)) {
|
|
617
|
+
onStep("cli-proof");
|
|
618
|
+
proveCli(selectedNode, entry, metadata, env, processAdapter);
|
|
619
|
+
return { acquired: false, env, command: selectedNode, prefix: [entry] };
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
// @pnpm/exe hard-links its binary under both names; the shim may name either.
|
|
623
|
+
// CMD resolves an extensionless target through PATHEXT, in order.
|
|
624
|
+
let exe = target(shim.exe);
|
|
625
|
+
if (extname(exe) === "") exe = pathExtensions(env).map((candidate) => `${exe}${candidate}`).find(exists) ?? exe;
|
|
626
|
+
const metadataPath = join(dirname(exe), "package.json");
|
|
627
|
+
onStep("exe-storage");
|
|
628
|
+
storage(exe, env);
|
|
629
|
+
if (!/\.exe$/i.test(exe) || !regular(exe)) throw new Error("Unsafe pnpm wrapper target");
|
|
630
|
+
if (!exists(metadataPath)) throw notPnpm;
|
|
631
|
+
storage(metadataPath, env);
|
|
632
|
+
if (!regular(metadataPath)) throw new Error("Unsafe pnpm wrapper target");
|
|
633
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
634
|
+
if (metadata.name !== "pnpm" && !/^@pnpm\//.test(metadata.name ?? "")) throw notPnpm;
|
|
635
|
+
// A standalone pnpm embeds its own Node runtime: it must report its package's version.
|
|
636
|
+
onStep("exe-proof");
|
|
637
|
+
const version = processAdapter(exe, ["--version"], env);
|
|
638
|
+
if (!STABLE.test(version) || version !== metadata.version) throw new Error("Windows pnpm version rejected");
|
|
639
|
+
if (pinnedPnpmCompatible(version)) {
|
|
640
|
+
proveGlobal(exe, [], env, processAdapter);
|
|
641
|
+
return { acquired: false, env, command: exe, prefix: [] };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error !== notPnpm && !untrustedStorage.test(error?.check ?? "")) throw error;
|
|
646
|
+
}
|
|
647
|
+
onStep("tools-check");
|
|
648
|
+
storage(tools, env);
|
|
649
|
+
if (!lstatSync(tools).isDirectory() || realpathSync(tools) !== resolve(tools)) throw new Error("Unsafe Windows tools");
|
|
650
|
+
const destination = join(tools, "pnpm");
|
|
651
|
+
if (exists(destination)) throw new Error("Conflicting Windows pnpm destination");
|
|
652
|
+
onStep("download");
|
|
653
|
+
const bytes = await verifiedDownload("pnpm", adapters);
|
|
654
|
+
onStep("archive");
|
|
655
|
+
const entries = readWindowsPnpmArchive(bytes);
|
|
656
|
+
const pin = artifactFor("pnpm");
|
|
657
|
+
const packageEntry = entries.find((entry) => entry.name === "package/package.json" && !entry.directory);
|
|
658
|
+
const metadata = packageEntry && JSON.parse(packageEntry.bytes.toString("utf8"));
|
|
659
|
+
if (!metadata || metadata.name !== pin.name || metadata.version !== pin.version || metadata.engines?.node !== pin.engine || metadata.bin?.pnpm !== "bin/pnpm.mjs") throw new Error("Windows pnpm pin metadata rejected");
|
|
660
|
+
if (!entries.some((entry) => entry.name === "package/bin/pnpm.mjs" && !entry.directory)) throw new Error("Windows pnpm entry missing");
|
|
661
|
+
let owned = false;
|
|
662
|
+
onStep("publish");
|
|
663
|
+
try {
|
|
664
|
+
mkdirSync(destination); // atomic no-clobber claim under the verified private root
|
|
665
|
+
owned = true;
|
|
666
|
+
storage(destination, env);
|
|
667
|
+
for (const entry of entries) {
|
|
668
|
+
const path = join(destination, ...entry.name.split("/"));
|
|
669
|
+
if (entry.directory) mkdirSync(path, { recursive: true });
|
|
670
|
+
else {
|
|
671
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
672
|
+
writeFileSync(path, entry.bytes, { flag: "wx" });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const entry = join(destination, "package", "bin", "pnpm.mjs");
|
|
676
|
+
proveCli(node, entry, metadata, env, processAdapter);
|
|
677
|
+
return { acquired: true, env, command: node, prefix: [entry] };
|
|
678
|
+
} catch {
|
|
679
|
+
if (owned) rmSync(destination, { recursive: true, force: true });
|
|
680
|
+
throw new Error("Windows pnpm acquisition failed; no installation completed");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Fixed, non-sensitive helper diagnostics. Every explicit check on this path has
|
|
685
|
+
// one code; the step names the phase. Anything else, including fs, JSON or spawn
|
|
686
|
+
// errors, reports `unexpected-<step>`. No path, SID or exception text is copied.
|
|
687
|
+
const helperSteps = new Set(["bundle", "tools-storage", "pnpm-discovery", "wrapper-storage", "wrapper", "node-discovery", "node-storage", "entry-storage",
|
|
688
|
+
"metadata-storage", "package", "cli-proof", "exe-storage", "exe-proof", "tools-check", "download", "archive", "publish", "launch"]);
|
|
689
|
+
const helperRejections = new Map(Object.entries({
|
|
690
|
+
"Windows bootstrap requires native Windows": "native-unavailable",
|
|
691
|
+
"Native Windows storage verification unavailable": "native-unavailable",
|
|
692
|
+
"Required wizard bundle file missing": "bundle-missing",
|
|
693
|
+
"Windows repository prerequisite rejected": "prerequisite",
|
|
694
|
+
"Unsafe Windows storage path": "unsafe-path",
|
|
695
|
+
"Windows ACL evidence rejected": "acl-evidence",
|
|
696
|
+
"Unknown Windows cwd-search semantics": "cwd-search",
|
|
697
|
+
"Unknown Windows PATH/PATHEXT": "path-missing",
|
|
698
|
+
"Unknown Windows PATHEXT semantics": "pathext",
|
|
699
|
+
"Unknown Windows PATH": "path-entry",
|
|
700
|
+
"Unknown extensionless Windows prerequisite": "extensionless",
|
|
701
|
+
"Unknown pnpm wrapper; refusing replacement": "wrapper-unknown",
|
|
702
|
+
"Unknown pnpm wrapper": "wrapper-unproven",
|
|
703
|
+
"Unknown pnpm wrapper; refusing execution or replacement": "wrapper-unproven",
|
|
704
|
+
"Unknown wrapper-selected Node": "wrapper-node",
|
|
705
|
+
"Unsafe pnpm wrapper target": "wrapper-target",
|
|
706
|
+
"Windows pnpm package target rejected": "package-target",
|
|
707
|
+
"Windows pnpm engine or identity rejected": "pnpm-engine",
|
|
708
|
+
"Windows pnpm version rejected": "pnpm-version",
|
|
709
|
+
"Windows pnpm global capability rejected": "pnpm-capability",
|
|
710
|
+
"Windows prerequisite process failed": "process-failed",
|
|
711
|
+
"Unproven command interpreter target": "interpreter",
|
|
712
|
+
"Unsafe Windows tools": "unsafe-tools",
|
|
713
|
+
"Conflicting Windows pnpm destination": "pnpm-conflict",
|
|
714
|
+
"Windows pnpm pin metadata rejected": "pnpm-pin",
|
|
715
|
+
"Windows pnpm entry missing": "pnpm-entry",
|
|
716
|
+
"Windows pnpm acquisition failed; no installation completed": "acquisition",
|
|
717
|
+
"Future wizard entry is missing; no live wizard is available": "wizard-missing",
|
|
718
|
+
"Wizard child could not start": "wizard-start",
|
|
719
|
+
"Wizard child failed": "wizard-exit",
|
|
720
|
+
}));
|
|
721
|
+
const archiveRejection = /^Unsafe (?:tar|archive|duplicate archive|truncated tar|pnpm package root)\b/;
|
|
722
|
+
export function windowsBootstrapReason(error, step) {
|
|
723
|
+
if (!helperSteps.has(step)) throw new Error("Unknown Windows bootstrap step");
|
|
724
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
725
|
+
const check = error instanceof Error && storageChecks.test(error.check ?? "") ? error.check : undefined;
|
|
726
|
+
const code = check ?? helperRejections.get(message) ?? (archiveRejection.test(message ?? "") ? "archive" : undefined);
|
|
727
|
+
return code ? `${code} (${step})` : `unexpected-${step}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export async function bootstrapWindows({ bundle, tools, env }) {
|
|
731
|
+
let step = "bundle";
|
|
732
|
+
try {
|
|
733
|
+
if (process.platform !== "win32") throw new Error("Windows bootstrap requires native Windows");
|
|
734
|
+
for (const file of ["package.json", "bin/gentle-shell-install.mjs", "scripts/installer-downloads.mjs", "scripts/installer-windows.mjs", "scripts/installer-windows-artifacts.json"]) {
|
|
735
|
+
if (!regular(join(bundle, file))) throw new Error("Required wizard bundle file missing");
|
|
736
|
+
}
|
|
737
|
+
const metadata = JSON.parse(readFileSync(join(bundle, "package.json"), "utf8"));
|
|
738
|
+
if (!compatibleEngine(windowsNodeFloor, process.versions.node) || !compatibleEngine(metadata.engines?.node, process.versions.node) || metadata.packageManager !== `pnpm@${artifactFor("pnpm").version}`) throw new Error("Windows repository prerequisite rejected");
|
|
739
|
+
step = "tools-storage";
|
|
740
|
+
verifyWindowsStorage(tools, env);
|
|
741
|
+
const result = await ensureWindowsPnpm({ tools, env, onStep: (next) => { step = next; } });
|
|
742
|
+
// Fixed data handoff for the future T5 entry. No caller-controlled command text.
|
|
743
|
+
step = "launch";
|
|
744
|
+
// A native pnpm.exe is handed off as the command itself, a JS entry with its Node.
|
|
745
|
+
const handoff = result.prefix.length === 0 ? { GENTLE_INSTALL_PNPM_COMMAND: result.command }
|
|
746
|
+
: { GENTLE_INSTALL_PNPM_NODE: result.command, GENTLE_INSTALL_PNPM_ENTRY: result.prefix[0] };
|
|
747
|
+
const inherited = Object.entries(result.env).filter(([key]) => !/^GENTLE_INSTALL_PNPM_(?:NODE|ENTRY|COMMAND)$/i.test(key));
|
|
748
|
+
await launchWizard({ bundle, env: { ...Object.fromEntries(inherited), ...handoff } });
|
|
749
|
+
} catch (error) {
|
|
750
|
+
throw Object.assign(new Error("Windows bootstrap rejected"), { reason: windowsBootstrapReason(error, step) });
|
|
751
|
+
}
|
|
752
|
+
}
|