gentle-pi 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- 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 +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- 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 +29 -25
- 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/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -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 +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- 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/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 +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- 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.ts +1 -1
- 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/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- 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-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- 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 +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- 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 +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- 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 +7 -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 +79 -32
- 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/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- 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 +52 -1
- 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 +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -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 +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- 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 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- 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-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- 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 +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- 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 +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -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-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- 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 +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- 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 +6 -5
- 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/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- 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 +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- 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 +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -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 +5 -4
- 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-risk-assessment.test.ts +109 -33
- 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-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- 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-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- 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 +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -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 +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -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 +27 -1
- package/tests/test-strength-contract.test.ts +124 -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.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /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,564 @@
|
|
|
1
|
+
import { spawn as spawnProcess } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
import { gentleAiBinaryPath } from "../runtime/gentle-ai-binary.mjs";
|
|
6
|
+
import { pnpmGlobalBin, requirements } from "./installer-preflight.mjs";
|
|
7
|
+
import { installOwner } from "./main-channel.mjs";
|
|
8
|
+
import {
|
|
9
|
+
PI_PACKAGE,
|
|
10
|
+
SHELL_PACKAGE,
|
|
11
|
+
childEnvironment,
|
|
12
|
+
contains,
|
|
13
|
+
genuineNpm,
|
|
14
|
+
lookPath,
|
|
15
|
+
npmInvocation,
|
|
16
|
+
packageNativeGentleAi,
|
|
17
|
+
pnpmInvocation,
|
|
18
|
+
recoverableStackRoot,
|
|
19
|
+
samePath,
|
|
20
|
+
spawnable,
|
|
21
|
+
succeeded,
|
|
22
|
+
windowsInvocation,
|
|
23
|
+
} from "./installer-runner.mjs";
|
|
24
|
+
import { verifyWindowsStorage, verifyWindowsStorageMany } from "./installer-windows.mjs";
|
|
25
|
+
|
|
26
|
+
// Real host probes for collectInventory. Every effect goes through injected
|
|
27
|
+
// adapters; probes only run fixed read-only argv (`--version`, `go version`,
|
|
28
|
+
// `pnpm list -g`, `npm config get prefix`, `npm root -g`) and never write,
|
|
29
|
+
// create directories or run setup/postinstall.
|
|
30
|
+
|
|
31
|
+
const SECOND = 1000;
|
|
32
|
+
const deadlines = Object.freeze({ version: 10 * SECOND, list: 30 * SECOND });
|
|
33
|
+
const BOOTSTRAP_TOOLS = /^\.gentle-shell-bootstrap-tools\./;
|
|
34
|
+
|
|
35
|
+
const unknown = () => ({ available: null });
|
|
36
|
+
const absent = () => ({ available: false });
|
|
37
|
+
|
|
38
|
+
function pathOf(platform) {
|
|
39
|
+
return platform === "win32" ? win32 : posix;
|
|
40
|
+
}
|
|
41
|
+
function pathKey(env, platform) {
|
|
42
|
+
return platform === "win32" ? Object.keys(env).find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
|
|
43
|
+
}
|
|
44
|
+
function envValue(env, name, platform) {
|
|
45
|
+
const key = platform === "win32" ? Object.keys(env).find((candidate) => candidate.toUpperCase() === name) : name;
|
|
46
|
+
return key === undefined ? undefined : env[key];
|
|
47
|
+
}
|
|
48
|
+
function plainObject(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
50
|
+
}
|
|
51
|
+
function exactVersion(text, pattern) {
|
|
52
|
+
const match = pattern.exec(String(text ?? "").trim());
|
|
53
|
+
return match ? `${match[1]}.${match[2]}.${match[3] ?? "0"}` : null;
|
|
54
|
+
}
|
|
55
|
+
const NUMBER = "(0|[1-9]\\d*)";
|
|
56
|
+
const STABLE = new RegExp(`^${NUMBER}\\.${NUMBER}\\.${NUMBER}$`);
|
|
57
|
+
// A main-channel Gentle Shell (scripts/main-channel.mjs): <stable>-main.<sha12>.
|
|
58
|
+
const MAIN_BUILD = new RegExp(`^${NUMBER}\\.${NUMBER}\\.${NUMBER}-main\\.[0-9a-f]{12}$`);
|
|
59
|
+
const NODE_VERSION = new RegExp(`^v${NUMBER}\\.${NUMBER}\\.${NUMBER}$`);
|
|
60
|
+
const GO_VERSION = new RegExp(`^go version go${NUMBER}\\.${NUMBER}(?:\\.${NUMBER})? \\S+$`);
|
|
61
|
+
function atLeast(version, minimum) {
|
|
62
|
+
const [left, right] = [version, minimum].map((value) => value.split(".").map(Number));
|
|
63
|
+
for (let i = 0; i < 3; i += 1) if (left[i] !== right[i]) return left[i] > right[i];
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalDirectory(path, value) {
|
|
68
|
+
return path.normalize(value).replace(/(.)[\\/]+$/, "$1");
|
|
69
|
+
}
|
|
70
|
+
function sameDirectory(platform, left, right) {
|
|
71
|
+
const path = pathOf(platform);
|
|
72
|
+
const [a, b] = [normalDirectory(path, left), normalDirectory(path, right)];
|
|
73
|
+
return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Bootstrap tool roots: `GENTLE_BOOTSTRAP_TOOLS` and every PATH entry segment
|
|
77
|
+
* named `.gentle-shell-bootstrap-tools.*` (the bootstrap's temporary directory).
|
|
78
|
+
*/
|
|
79
|
+
export function bootstrapRoots({ platform, env }) {
|
|
80
|
+
const path = pathOf(platform);
|
|
81
|
+
const roots = [];
|
|
82
|
+
const add = (root) => {
|
|
83
|
+
const normal = normalDirectory(path, root);
|
|
84
|
+
if (!roots.some((other) => sameDirectory(platform, other, normal))) roots.push(normal);
|
|
85
|
+
};
|
|
86
|
+
const tools = envValue(env, "GENTLE_BOOTSTRAP_TOOLS", platform);
|
|
87
|
+
if (typeof tools === "string" && path.isAbsolute(tools)) add(tools);
|
|
88
|
+
const marker = platform === "win32" ? new RegExp(BOOTSTRAP_TOOLS.source, "i") : BOOTSTRAP_TOOLS;
|
|
89
|
+
for (const entry of String(env[pathKey(env, platform)] ?? "").split(path.delimiter)) {
|
|
90
|
+
if (!path.isAbsolute(entry)) continue;
|
|
91
|
+
const parts = path.normalize(entry).split(path.sep);
|
|
92
|
+
const index = parts.findIndex((part) => marker.test(part));
|
|
93
|
+
if (index > 0) add(parts.slice(0, index + 1).join(path.sep) || path.sep);
|
|
94
|
+
}
|
|
95
|
+
return roots;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The user's own environment: PATH without bootstrap tool directories. */
|
|
99
|
+
export function userEnvironment({ platform, env }) {
|
|
100
|
+
const path = pathOf(platform);
|
|
101
|
+
const roots = bootstrapRoots({ platform, env });
|
|
102
|
+
const key = pathKey(env, platform);
|
|
103
|
+
if (env[key] === undefined) return { ...env };
|
|
104
|
+
const own = (entry) => !roots.some((root) => sameDirectory(platform, root, entry) || contains(root, entry, platform));
|
|
105
|
+
return { ...env, [key]: String(env[key]).split(path.delimiter).filter((entry) => entry.length > 0 && own(entry)).join(path.delimiter) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Real adapters: argv spawn without a shell, SIGKILL at the deadline, bounded
|
|
109
|
+
* stdout (stderr discarded: it can hold private paths) and read-only fs checks.
|
|
110
|
+
* Only a caller that passes `stderrTail: <bytes>` (a positive integer, clamped
|
|
111
|
+
* to 4 KiB) gets stderr piped; the result then carries just its last bytes as
|
|
112
|
+
* `stderrTail`. Callers must sanitize that text before showing it anywhere.
|
|
113
|
+
* The deadline signals the direct child only, not its descendants, and settles
|
|
114
|
+
* the result as timed out without waiting for the child's pipes to close.
|
|
115
|
+
* An optional `cwd` sets the child's working directory.
|
|
116
|
+
* `spawn` is injectable only for trusted tests.
|
|
117
|
+
*/
|
|
118
|
+
export function hostAdapters({ maxOutputBytes = 1024 * 1024, maxTextBytes = 1024 * 1024, spawn = spawnProcess } = {}) {
|
|
119
|
+
const run = (command, argv, { env, cwd, deadlineMs, stderrTail }) => new Promise((resolve) => {
|
|
120
|
+
let size = 0;
|
|
121
|
+
let truncated = false;
|
|
122
|
+
let timedOut = false;
|
|
123
|
+
const chunks = [];
|
|
124
|
+
const tailBytes = Number.isSafeInteger(stderrTail) && stderrTail > 0 ? Math.min(stderrTail, 4096) : 0;
|
|
125
|
+
let tail = Buffer.alloc(0);
|
|
126
|
+
const withTail = (result) => (tailBytes > 0 ? { ...result, stderrTail: tail.toString("utf8") } : result);
|
|
127
|
+
let child;
|
|
128
|
+
try {
|
|
129
|
+
child = spawn(command, argv, { env, cwd, shell: false, stdio: ["ignore", "pipe", tailBytes > 0 ? "pipe" : "ignore"], windowsHide: true });
|
|
130
|
+
} catch {
|
|
131
|
+
resolve(withTail({ code: null, signal: null, timedOut: false, truncated: false, stdout: "" }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let settled = false;
|
|
135
|
+
const settle = (code, signal) => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
resolve(withTail({ code, signal, timedOut, truncated, stdout: Buffer.concat(chunks).toString("utf8") }));
|
|
140
|
+
};
|
|
141
|
+
// Keep only the last bytes: a long setup log never accumulates in memory.
|
|
142
|
+
if (tailBytes > 0) child.stderr?.on("data", (chunk) => {
|
|
143
|
+
const joined = Buffer.concat([tail, chunk]);
|
|
144
|
+
tail = joined.subarray(Math.max(0, joined.length - tailBytes));
|
|
145
|
+
});
|
|
146
|
+
// A descendant can keep stdout open after the kill, so `close` may never come:
|
|
147
|
+
// settle at the deadline and release the pipes instead of waiting for it.
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
timedOut = true;
|
|
150
|
+
child.kill("SIGKILL");
|
|
151
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) stream?.destroy();
|
|
152
|
+
settle(null, "SIGKILL");
|
|
153
|
+
}, deadlineMs);
|
|
154
|
+
child.stdout.on("data", (chunk) => {
|
|
155
|
+
const room = maxOutputBytes - size;
|
|
156
|
+
if (chunk.length > room) truncated = true;
|
|
157
|
+
if (room > 0) {
|
|
158
|
+
chunks.push(chunk.subarray(0, room));
|
|
159
|
+
size += Math.min(room, chunk.length);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
child.once("error", () => settle(null, null));
|
|
163
|
+
child.once("close", settle);
|
|
164
|
+
});
|
|
165
|
+
const info = (path, follow = true) => (follow ? stat(path) : lstat(path)).catch((error) => error);
|
|
166
|
+
const fs = {
|
|
167
|
+
isFile: async (path) => (await stat(path).catch(() => null))?.isFile() === true,
|
|
168
|
+
isDirectory: async (path) => (await stat(path).catch(() => null))?.isDirectory() === true,
|
|
169
|
+
// Only a missing entry is absent; an unreadable one exists (and is then not a writable directory).
|
|
170
|
+
exists: async (path) => {
|
|
171
|
+
const result = await info(path, false);
|
|
172
|
+
return !(result instanceof Error) || !["ENOENT", "ENOTDIR"].includes(result.code);
|
|
173
|
+
},
|
|
174
|
+
realpath: (path) => realpath(path),
|
|
175
|
+
readText: async (path) => {
|
|
176
|
+
const result = await stat(path);
|
|
177
|
+
if (!result.isFile() || result.size > maxTextBytes) throw new Error("Unreadable probe file");
|
|
178
|
+
return readFile(path, "utf8");
|
|
179
|
+
},
|
|
180
|
+
writable: (path) => access(path, constants.W_OK).then(() => true, () => false),
|
|
181
|
+
// A symbolic link, or on Windows a junction (libuv's lstat reports both as links).
|
|
182
|
+
// A missing entry is no link; any other error throws.
|
|
183
|
+
isLink: async (path) => {
|
|
184
|
+
const result = await info(path, false);
|
|
185
|
+
if (!(result instanceof Error)) return result.isSymbolicLink();
|
|
186
|
+
if (["ENOENT", "ENOTDIR"].includes(result.code)) return false;
|
|
187
|
+
throw result;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
return { run, fs };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* createProbes({ platform, env, run, fs, home?, verifyGentleAi?, storage?, storageMany?, pnpmHome? }) -> the eight
|
|
195
|
+
* named collectInventory probes, plus `folders` (Windows only). `env` is the wizard's environment (bootstrap
|
|
196
|
+
* tools first on PATH); `run` and `fs` follow hostAdapters. Each probe returns
|
|
197
|
+
* the shape collectInventory documents, or { available: null } when unknown or
|
|
198
|
+
* failed; errors are never thrown or retained. On Windows, `storage(file)` walks a
|
|
199
|
+
* file the way the bootstrap does (verifyWindowsStorage by default), `storageMany(files)`
|
|
200
|
+
* walks many in one launch (verifyWindowsStorageMany) and `pnpmHome` is the
|
|
201
|
+
* wizard's PNPM_HOME decision (windowsPnpmHome).
|
|
202
|
+
*/
|
|
203
|
+
export function createProbes({ platform, env, run, fs, home, verifyGentleAi = packageNativeGentleAi,
|
|
204
|
+
storage = platform === "win32" ? (file) => verifyWindowsStorage(file, env) : null,
|
|
205
|
+
storageMany = platform === "win32" ? (files) => verifyWindowsStorageMany(files, env) : null, pnpmHome = null }) {
|
|
206
|
+
const path = pathOf(platform);
|
|
207
|
+
const user = userEnvironment({ platform, env });
|
|
208
|
+
const globalBin = pnpmGlobalBin({ platform, env: user });
|
|
209
|
+
// pnpm 11 global commands need `$PNPM_HOME/bin` on PATH, as in the runner.
|
|
210
|
+
const privateHome = platform === "win32" && pnpmHome?.source === "private";
|
|
211
|
+
const child = globalBin ? childEnvironment(env, platform, globalBin, { privateHome }) : env;
|
|
212
|
+
const userChild = globalBin ? childEnvironment(user, platform, globalBin, { privateHome }) : user;
|
|
213
|
+
/** Every file passes the storage walk; any failure means it is not usable. */
|
|
214
|
+
const trusted = async (files) => {
|
|
215
|
+
if (!storage) return true;
|
|
216
|
+
try {
|
|
217
|
+
for (const file of files) await storage(file);
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const userHome = home ?? (platform === "win32" ? env.USERPROFILE : env.HOME);
|
|
224
|
+
const output = async (command, argv, runEnv, deadlineMs) => {
|
|
225
|
+
const result = await run(command, argv, { env: runEnv, deadlineMs });
|
|
226
|
+
return succeeded(result) && result.truncated !== true ? String(result.stdout ?? "").trim() : null;
|
|
227
|
+
};
|
|
228
|
+
const persistentOn = async (name) => (await lookPath(name, user, platform, fs)) !== null;
|
|
229
|
+
const roots = bootstrapRoots({ platform, env });
|
|
230
|
+
const marker = platform === "win32" ? new RegExp(BOOTSTRAP_TOOLS.source, "i") : BOOTSTRAP_TOOLS;
|
|
231
|
+
/** A file inside the bootstrap's temporary tools directory. */
|
|
232
|
+
const fromBootstrap = (file) => path.isAbsolute(file) && (roots.some((root) => contains(root, file, platform)) ||
|
|
233
|
+
path.normalize(file).split(path.sep).some((part) => marker.test(part)));
|
|
234
|
+
|
|
235
|
+
let listing;
|
|
236
|
+
/** Output of the single `list -g` call, or null when it is unavailable. */
|
|
237
|
+
const globalListing = () => (listing ??= (async () => {
|
|
238
|
+
// A private PNPM_HOME this run has not created yet holds nothing; listing it
|
|
239
|
+
// could make pnpm create it, unprotected, before consent.
|
|
240
|
+
if (privateHome && globalBin && !(await fs.exists(globalBin.pnpmHome))) return "[]";
|
|
241
|
+
const pnpm = globalBin ? await pnpmInvocation(env, platform, fs) : null;
|
|
242
|
+
if (!pnpm) return null;
|
|
243
|
+
return output(pnpm.command, [...pnpm.prefix, "list", "-g", "--depth", "0", "--json"], child, deadlines.list);
|
|
244
|
+
})());
|
|
245
|
+
/** pnpm-global packages by name from that listing; null entries are ambiguous. */
|
|
246
|
+
const globalPackages = async () => {
|
|
247
|
+
const stdout = await globalListing();
|
|
248
|
+
if (stdout === null) return null;
|
|
249
|
+
const projects = JSON.parse(stdout);
|
|
250
|
+
if (!Array.isArray(projects)) return null;
|
|
251
|
+
const packages = new Map();
|
|
252
|
+
for (const project of projects) {
|
|
253
|
+
if (!plainObject(project)) return null;
|
|
254
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
255
|
+
const dependencies = project[field];
|
|
256
|
+
if (dependencies === undefined) continue;
|
|
257
|
+
if (!plainObject(dependencies)) return null;
|
|
258
|
+
for (const [name, entry] of Object.entries(dependencies)) {
|
|
259
|
+
packages.set(name, packages.has(name) || !plainObject(entry) ? null : entry);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return packages;
|
|
264
|
+
};
|
|
265
|
+
/** A package that is not pnpm-global but whose command resolves elsewhere is unknown, never absent. */
|
|
266
|
+
const globalPackage = async (name, command) => {
|
|
267
|
+
const packages = await globalPackages();
|
|
268
|
+
if (!packages) return { state: "unknown" };
|
|
269
|
+
if (!packages.has(name)) return (await persistentOn(command)) ? { state: "unknown", outsidePnpm: true } : { state: "absent" };
|
|
270
|
+
const entry = packages.get(name);
|
|
271
|
+
const version = exactVersion(entry?.version, STABLE);
|
|
272
|
+
// A main-channel Shell (<stable>-main.<sha12>) is present too: it can be updated.
|
|
273
|
+
const main = typeof entry?.version === "string" && MAIN_BUILD.test(entry.version) ? entry.version : null;
|
|
274
|
+
return entry && (version || main) ? { state: "present", entry, version: version ?? main } : { state: "unknown" };
|
|
275
|
+
};
|
|
276
|
+
// Still unknown (never absent or replaced), but says the command comes from
|
|
277
|
+
// another installation so the wizard can explain the blocker.
|
|
278
|
+
const notPnpmGlobal = (found) => (found.outsidePnpm ? { ...unknown(), outsidePnpm: true } : unknown());
|
|
279
|
+
/** How a command found on the user's PATH runs: as it is on POSIX, through
|
|
280
|
+
* windowsInvocation (never cmd.exe) on Windows. Null when it cannot run.
|
|
281
|
+
*/
|
|
282
|
+
const invocation = (file) => (platform === "win32" ? windowsInvocation(file, user, { run, fs }) : { command: file, prefix: [] });
|
|
283
|
+
/** The package a command on the user's PATH runs from: its real root and version.
|
|
284
|
+
* With `followShim`, a Windows shim is followed to the entry it runs.
|
|
285
|
+
*/
|
|
286
|
+
const packageOnPath = async (command, name, followShim = false) => {
|
|
287
|
+
const found = await lookPath(command, user, platform, fs);
|
|
288
|
+
if (!found) return null;
|
|
289
|
+
let start = found;
|
|
290
|
+
if (followShim && platform === "win32") {
|
|
291
|
+
const target = await invocation(found);
|
|
292
|
+
if (!target) return null;
|
|
293
|
+
start = target.prefix[0] ?? target.command;
|
|
294
|
+
}
|
|
295
|
+
let directory = path.dirname(await fs.realpath(start));
|
|
296
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
297
|
+
const text = await fs.readText(path.join(directory, "package.json")).catch(() => null);
|
|
298
|
+
if (text !== null) {
|
|
299
|
+
let manifest = null;
|
|
300
|
+
try { manifest = JSON.parse(text); } catch { manifest = null; }
|
|
301
|
+
if (manifest?.name === name && typeof manifest.version === "string") return { root: directory, version: manifest.version };
|
|
302
|
+
}
|
|
303
|
+
const parent = path.dirname(directory);
|
|
304
|
+
if (parent === directory) break;
|
|
305
|
+
directory = parent;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
};
|
|
309
|
+
/** Where `file` really is (realpath) and, for every link on its path (lstat:
|
|
310
|
+
* a symbolic link or junction), the real folder that holds that link: whoever
|
|
311
|
+
* can change that folder can retarget the link. A realpath that merely spells a
|
|
312
|
+
* component differently, such as an 8.3 short name (RUNNER~1), is no link.
|
|
313
|
+
* Null when the real location, or whether a component is a link, cannot be read.
|
|
314
|
+
*/
|
|
315
|
+
const canonicalFiles = async (file) => {
|
|
316
|
+
const real = await fs.realpath(file).catch(() => null);
|
|
317
|
+
if (real === null) return null;
|
|
318
|
+
const result = [real];
|
|
319
|
+
for (let current = file; path.dirname(current) !== current; current = path.dirname(current)) {
|
|
320
|
+
let link;
|
|
321
|
+
try {
|
|
322
|
+
link = (await fs.isLink?.(current)) === true;
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (!link) continue;
|
|
327
|
+
const holder = await fs.realpath(path.dirname(current)).catch(() => null);
|
|
328
|
+
if (holder === null) return null;
|
|
329
|
+
if (!result.some((entry) => samePath(entry, holder, platform))) result.push(holder);
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
};
|
|
333
|
+
let npmRoot;
|
|
334
|
+
/** `npm root -g`, real path, from the user's npm (npmInvocation). */
|
|
335
|
+
const npmGlobalRoot = () => (npmRoot ??= (async () => {
|
|
336
|
+
const npm = await npmInvocation(user, platform, { run, fs });
|
|
337
|
+
const stdout = npm ? await output(npm.command, [...npm.prefix, "root", "-g"], user, deadlines.version) : null;
|
|
338
|
+
const line = stdout?.split(/\r?\n/).at(-1)?.trim() ?? "";
|
|
339
|
+
return path.isAbsolute(line) ? fs.realpath(line).catch(() => null) : null;
|
|
340
|
+
})());
|
|
341
|
+
const shellBin = () => path.join(globalBin.path, platform === "win32" ? "gentle-shell.cmd" : "gentle-shell");
|
|
342
|
+
/** The Pi on the user's PATH outside pnpm: real root, its package version and owner (npm or null). */
|
|
343
|
+
const piOnPath = async () => {
|
|
344
|
+
const found = await packageOnPath("pi", PI_PACKAGE, true);
|
|
345
|
+
if (!found) return null;
|
|
346
|
+
return { root: found.root, version: found.version,
|
|
347
|
+
owner: installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), name: PI_PACKAGE, platform }) };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const probes = {
|
|
351
|
+
/** Windows (S6 notice): the folders of the tools the user's PATH resolves (Node,
|
|
352
|
+
* npm, Go, Pi, Gentle Shell), each command and what it runs, walked in one
|
|
353
|
+
* storageMany launch where they really are (canonicalFiles): links such as
|
|
354
|
+
* pnpm's global `node_modules\<pkg>` junction or a `pnpm runtime` node.exe are
|
|
355
|
+
* followed, never reported, and the folder holding each link is walked too.
|
|
356
|
+
* A real location that is not on a local drive (a mapped network drive, or a
|
|
357
|
+
* link to a UNC share) is never walked: it could not be checked, for its tool
|
|
358
|
+
* only. { node?, npm?, go?, pi?, shell? }: per tool the first concrete finding,
|
|
359
|
+
* else the first path that could not be checked ({ check: "unchecked", at });
|
|
360
|
+
* null when nothing fails or the walk cannot finish.
|
|
361
|
+
*/
|
|
362
|
+
async folders() {
|
|
363
|
+
if (!storageMany) return null;
|
|
364
|
+
const files = {};
|
|
365
|
+
const unchecked = new Map();
|
|
366
|
+
for (const [tool, command] of [["node", "node"], ["npm", "npm"], ["go", "go"], ["pi", "pi"], ["shell", "gentle-shell"]]) {
|
|
367
|
+
const found = await lookPath(command, user, platform, fs);
|
|
368
|
+
if (!found) continue;
|
|
369
|
+
const runs = await Promise.resolve().then(() => invocation(found)).catch(() => null);
|
|
370
|
+
files[tool] = [];
|
|
371
|
+
for (const file of [found, ...(runs ? [runs.command, ...runs.prefix] : [])]) {
|
|
372
|
+
const canonical = await canonicalFiles(file);
|
|
373
|
+
if (canonical === null) unchecked.set(file.toLowerCase(), { check: "unchecked", at: file });
|
|
374
|
+
for (const real of canonical ?? []) if (!/^[A-Za-z]:\\/.test(real)) unchecked.set(real.toLowerCase(), { check: "unchecked", at: real });
|
|
375
|
+
files[tool].push(...(canonical ?? [file]));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const key = (file) => file.toLowerCase();
|
|
379
|
+
const paths = [...new Map(Object.values(files).flat().filter((file) => !unchecked.has(key(file))).map((file) => [key(file), file])).values()];
|
|
380
|
+
if (paths.length === 0 && unchecked.size === 0) return null;
|
|
381
|
+
let results;
|
|
382
|
+
try {
|
|
383
|
+
results = paths.length === 0 ? [] : await storageMany(paths);
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const byPath = new Map([...unchecked, ...paths.map((file, index) => [key(file), results[index] ?? null])]);
|
|
388
|
+
const findings = Object.fromEntries(Object.entries(files).map(([tool, list]) => {
|
|
389
|
+
const found = list.map((file) => byPath.get(key(file))).filter(Boolean);
|
|
390
|
+
return [tool, found.find((finding) => finding.check !== "unchecked") ?? found[0]];
|
|
391
|
+
}).filter(([, finding]) => finding));
|
|
392
|
+
return Object.keys(findings).length > 0 ? findings : null;
|
|
393
|
+
},
|
|
394
|
+
/** The installed Gentle Shell for an update: real root, version and owner (pnpm, npm or null). */
|
|
395
|
+
async locateShell() {
|
|
396
|
+
const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
|
|
397
|
+
if (shell.state === "present" && typeof shell.entry.path === "string" && path.isAbsolute(shell.entry.path)) {
|
|
398
|
+
return { root: await fs.realpath(shell.entry.path), version: shell.version, owner: "pnpm" };
|
|
399
|
+
}
|
|
400
|
+
if (shell.state !== "unknown" || !shell.outsidePnpm) return null;
|
|
401
|
+
const found = await packageOnPath("gentle-shell", SHELL_PACKAGE, true);
|
|
402
|
+
if (!found) return null;
|
|
403
|
+
return { root: found.root, version: found.version, owner: installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), platform }) };
|
|
404
|
+
},
|
|
405
|
+
/** The single installed Pi for an update: real root, version and owner (pnpm, npm or null). */
|
|
406
|
+
async locatePi() {
|
|
407
|
+
const pi = await globalPackage(PI_PACKAGE, "pi");
|
|
408
|
+
if (pi.state === "present" && typeof pi.entry.path === "string" && path.isAbsolute(pi.entry.path)) {
|
|
409
|
+
return { root: await fs.realpath(pi.entry.path), version: pi.version, owner: "pnpm" };
|
|
410
|
+
}
|
|
411
|
+
return pi.state === "unknown" && pi.outsidePnpm ? piOnPath() : null;
|
|
412
|
+
},
|
|
413
|
+
async node() {
|
|
414
|
+
const persistent = await lookPath("node", user, platform, fs);
|
|
415
|
+
const bootstrap = await lookPath("node", env, platform, fs);
|
|
416
|
+
let node = persistent ?? bootstrap;
|
|
417
|
+
if (!node) return absent();
|
|
418
|
+
if (!spawnable(node, platform)) return unknown();
|
|
419
|
+
const nodeVersion = async (file) => exactVersion(await output(file, ["--version"], env, deadlines.version), NODE_VERSION);
|
|
420
|
+
let version = await nodeVersion(node);
|
|
421
|
+
if (!version) return unknown();
|
|
422
|
+
// An older Node on the user's PATH is left as it is: the bootstrap's verified
|
|
423
|
+
// Node runs the installer, so it is reported (bootstrap-only) with the older one found.
|
|
424
|
+
let found = null;
|
|
425
|
+
if (persistent && bootstrap && persistent !== bootstrap && !atLeast(version, requirements.node) && spawnable(bootstrap, platform)) {
|
|
426
|
+
const pinned = await nodeVersion(bootstrap);
|
|
427
|
+
if (pinned && atLeast(pinned, requirements.node)) [found, node, version] = [version, bootstrap, pinned];
|
|
428
|
+
}
|
|
429
|
+
// A usable npm must resolve without bootstrap tools, as in a fresh terminal.
|
|
430
|
+
let npm = null;
|
|
431
|
+
try {
|
|
432
|
+
npm = typeof (await genuineNpm(userChild, platform, node, { run, fs }, globalBin)) === "object";
|
|
433
|
+
} catch {
|
|
434
|
+
npm = null;
|
|
435
|
+
}
|
|
436
|
+
return { available: true, version, usable: true, persistent: persistent !== null && found === null, npm, ...(found ? { found } : {}) };
|
|
437
|
+
},
|
|
438
|
+
async pnpm() {
|
|
439
|
+
const pnpm = await pnpmInvocation(env, platform, fs);
|
|
440
|
+
// Without the direct handoff a Windows .cmd shim cannot run with shell:false.
|
|
441
|
+
if (!pnpm) return (await lookPath("pnpm", env, platform, fs)) ? unknown() : absent();
|
|
442
|
+
const version = exactVersion(await output(pnpm.command, [...pnpm.prefix, "--version"], child, deadlines.version), STABLE);
|
|
443
|
+
if (!version) return unknown();
|
|
444
|
+
// pnpm checks its Node engine at startup. Any major at the pin or newer: the
|
|
445
|
+
// runner's commands were run with pnpm 12 against a pnpm 11 global home and back.
|
|
446
|
+
const compatible = atLeast(version, requirements.pnpm);
|
|
447
|
+
// The bootstrap's own pnpm (POSIX PATH or Windows handoff entry) is bootstrap-only
|
|
448
|
+
// even next to the user's pnpm, which it acquired because that one is incompatible.
|
|
449
|
+
if (!fromBootstrap(pnpm.prefix[0] ?? pnpm.command)) {
|
|
450
|
+
return { available: true, version, usable: true, compatible, persistent: await persistentOn("pnpm") };
|
|
451
|
+
}
|
|
452
|
+
// That pnpm's version, read in the user's environment. On Windows only when its
|
|
453
|
+
// shim and what it runs pass the storage walk: an untrusted pnpm never runs.
|
|
454
|
+
const own = await lookPath("pnpm", user, platform, fs);
|
|
455
|
+
const ownPnpm = own ? await invocation(own) : null;
|
|
456
|
+
const runnable = ownPnpm !== null && await trusted([own, ownPnpm.command, ...ownPnpm.prefix]);
|
|
457
|
+
const result = runnable ? await run(ownPnpm.command, [...ownPnpm.prefix, "--version"], { env: user, cwd: path.parse(own).root, deadlineMs: deadlines.version }) : null;
|
|
458
|
+
const found = succeeded(result) && result.truncated !== true ? exactVersion(result.stdout, STABLE) : null;
|
|
459
|
+
const usableFound = found !== null && atLeast(found, requirements.pnpm);
|
|
460
|
+
// Persisting pnpm writes $PNPM_HOME/bin: a user's pnpm there with a stable version
|
|
461
|
+
// is reported as it is (an incompatible one blocks), never replaced or downgraded.
|
|
462
|
+
// One that fails the walk (untrusted), or whose shim is not recognized or whose
|
|
463
|
+
// version cannot be read (unchecked), is never run again: the bootstrap's pnpm is
|
|
464
|
+
// reported in its place, as persistent, so nothing is persisted over the user's.
|
|
465
|
+
if (own && globalBin && samePath(path.dirname(own), globalBin.path, platform)) {
|
|
466
|
+
if (found) return { available: true, version: found, usable: true, compatible: usableFound, persistent: true, inGlobalBin: true };
|
|
467
|
+
return { available: true, version, usable: true, compatible, persistent: true, inGlobalBin: true,
|
|
468
|
+
...(ownPnpm !== null && !runnable ? { untrusted: true } : { unchecked: true }) };
|
|
469
|
+
}
|
|
470
|
+
// The bootstraps run only the pinned major (installer-downloads pinnedPnpmCompatible): a pnpm
|
|
471
|
+
// of another major beside them is reported, so the plan says it is left unchanged.
|
|
472
|
+
const sameMajor = found !== null && found.split(".")[0] === requirements.pnpm.split(".")[0];
|
|
473
|
+
const replaced = found !== null && (!usableFound || !sameMajor);
|
|
474
|
+
return { available: true, version, usable: true, compatible, persistent: false, ...(replaced ? { found } : {}) };
|
|
475
|
+
},
|
|
476
|
+
// Only a Pi older than the minimum reports its owner (pnpm or npm): the one the installer updates.
|
|
477
|
+
async pi() {
|
|
478
|
+
const pi = await globalPackage(PI_PACKAGE, "pi");
|
|
479
|
+
const older = (version) => !MAIN_BUILD.test(version) && !atLeast(version, requirements.pi);
|
|
480
|
+
if (pi.state === "present") return { available: true, version: pi.version, usable: true, ...(older(pi.version) ? { owner: "pnpm" } : {}) };
|
|
481
|
+
if (pi.state === "absent" || !pi.outsidePnpm) return pi.state === "absent" ? absent() : unknown();
|
|
482
|
+
// Another installation of Pi: reused as long as it reports a stable version.
|
|
483
|
+
const command = await lookPath("pi", user, platform, fs);
|
|
484
|
+
const invoked = command ? await invocation(command) : null;
|
|
485
|
+
const version = invoked ? /(?:^|\s|v)(\d+\.\d+\.\d+)(?:\s|$)/.exec(await output(invoked.command, [...invoked.prefix, "--version"], user, deadlines.version) ?? "") : null;
|
|
486
|
+
if (!version) return notPnpmGlobal(pi);
|
|
487
|
+
const found = { available: true, version: version[1], usable: true, external: true };
|
|
488
|
+
if (!older(found.version)) return found;
|
|
489
|
+
// npm owns it only when its package in npm's global root reports that same version.
|
|
490
|
+
const located = await piOnPath().catch(() => null);
|
|
491
|
+
return located?.owner === "npm" && located.version === found.version ? { ...found, owner: "npm" } : found;
|
|
492
|
+
},
|
|
493
|
+
async shell() {
|
|
494
|
+
const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
|
|
495
|
+
if (shell.state === "present") return { available: true, version: shell.version, usable: await fs.isFile(shellBin()), global: true, owner: "pnpm" };
|
|
496
|
+
if (shell.state === "absent" || !shell.outsidePnpm) return shell.state === "absent" ? absent() : unknown();
|
|
497
|
+
// Installed by npm only when it really lives in npm's global root; a linked checkout stays unknown.
|
|
498
|
+
// A Windows shim is followed to the entry it runs, as for Pi.
|
|
499
|
+
const found = await packageOnPath("gentle-shell", SHELL_PACKAGE, true);
|
|
500
|
+
const owner = found ? installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), platform }) : null;
|
|
501
|
+
const version = found && (exactVersion(found.version, STABLE) ?? (MAIN_BUILD.test(found.version) ? found.version : null));
|
|
502
|
+
return owner === "npm" && version ? { available: true, version, usable: true, global: true, owner } : notPnpmGlobal(shell);
|
|
503
|
+
},
|
|
504
|
+
async gentleAi() {
|
|
505
|
+
const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
|
|
506
|
+
if (shell.state === "absent") return absent();
|
|
507
|
+
// Only this package version's native pin is the requirement being checked.
|
|
508
|
+
if (shell.state !== "present" || shell.version !== requirements.shell) return unknown();
|
|
509
|
+
if (typeof shell.entry.path !== "string" || !path.isAbsolute(shell.entry.path)) return unknown();
|
|
510
|
+
const [root, pnpmHome] = [await fs.realpath(shell.entry.path), await fs.realpath(globalBin.pnpmHome)];
|
|
511
|
+
if (!contains(pnpmHome, root, platform)) return unknown();
|
|
512
|
+
if (!(await fs.isFile(gentleAiBinaryPath(root, platform)))) return absent();
|
|
513
|
+
const verdict = await verifyGentleAi({ packageRoot: root, platform, env, home: userHome });
|
|
514
|
+
return verdict?.ok === true ? { available: true, version: requirements.gentleAi, usable: true, compatible: true } : unknown();
|
|
515
|
+
},
|
|
516
|
+
async go() {
|
|
517
|
+
const go = await lookPath("go", user, platform, fs);
|
|
518
|
+
if (!go) return absent();
|
|
519
|
+
if (!spawnable(go, platform)) return unknown();
|
|
520
|
+
const version = exactVersion(await output(go, ["version"], user, deadlines.version), GO_VERSION);
|
|
521
|
+
return version ? { available: true, version, usable: true } : unknown();
|
|
522
|
+
},
|
|
523
|
+
// Not needed to install; review, worktrees and repository features use it.
|
|
524
|
+
// Git itself is never run: on macOS /usr/bin/git is a shim that opens an
|
|
525
|
+
// install dialog without the Command Line Tools, so only `xcode-select -p`
|
|
526
|
+
// (which never opens one) decides whether that shim is real Git.
|
|
527
|
+
async git() {
|
|
528
|
+
const git = await lookPath("git", user, platform, fs);
|
|
529
|
+
if (!git) return absent();
|
|
530
|
+
if (platform !== "darwin" || git !== "/usr/bin/git") return { available: true };
|
|
531
|
+
return { available: succeeded(await run("/usr/bin/xcode-select", ["-p"], { env: user, deadlineMs: deadlines.version })) };
|
|
532
|
+
},
|
|
533
|
+
async globalBin() {
|
|
534
|
+
if (!globalBin) return unknown();
|
|
535
|
+
// Nearest existing ancestor (or the directory itself); nothing is created.
|
|
536
|
+
let current = globalBin.path;
|
|
537
|
+
while (!(await fs.exists(current))) {
|
|
538
|
+
const parent = path.dirname(current);
|
|
539
|
+
if (parent === current) return unknown();
|
|
540
|
+
current = parent;
|
|
541
|
+
}
|
|
542
|
+
const writable = (await fs.isDirectory(current)) && (await fs.writable(current));
|
|
543
|
+
return { available: true, path: globalBin.path, writable, onPath: globalBin.onPath };
|
|
544
|
+
},
|
|
545
|
+
async setup() {
|
|
546
|
+
// Setup readiness of an existing Shell has no read-only evidence. Only the
|
|
547
|
+
// pinned stack this pnpm installed (an earlier run whose setup did not
|
|
548
|
+
// finish) is recoverable: its public `gentle-shell setup` may be rerun.
|
|
549
|
+
const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
|
|
550
|
+
if (shell.state === "absent") return false;
|
|
551
|
+
if (shell.state !== "present") return unknown();
|
|
552
|
+
const root = await recoverableStackRoot(await globalListing(), globalBin.pnpmHome, platform, fs);
|
|
553
|
+
return root === null ? unknown() : { available: true, recoverable: true };
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
return Object.fromEntries(Object.entries(probes).map(([name, probe]) => [name, async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await probe();
|
|
559
|
+
} catch {
|
|
560
|
+
// Do not retain probe errors: they can contain private paths or credentials.
|
|
561
|
+
return unknown();
|
|
562
|
+
}
|
|
563
|
+
}]));
|
|
564
|
+
}
|