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,399 @@
|
|
|
1
|
+
// The web installer's bundled path (T1c; S1, S2, S3, S12): a new installation
|
|
2
|
+
// of the release channel whose release publishes the distribution assets gets
|
|
3
|
+
// Gentle Shell as one self-contained product, through the shared module in
|
|
4
|
+
// bundled-install.mjs. The user's Node, npm, pnpm, Go and Pi are not used or
|
|
5
|
+
// changed. Everything else keeps the standard runner (installer-runner.mjs).
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
8
|
+
import { posix, win32 } from "node:path";
|
|
9
|
+
import { DISTRIBUTION_ASSETS, activateVersion, applyPathEntry, checkLauncher, claimPrefix, ensureLauncher, ensureRuntime, installVersion, pathEntryPlan,
|
|
10
|
+
prefixLayout, prefixRoot, readDistribution, removePathEntry, runCommand, setupEnvironment, userNpmrc, writeNpmrcAuth, writePathReceipt } from "./bundled-install.mjs";
|
|
11
|
+
import { removeGlobalCommand, removeGlobalShell } from "./main-channel.mjs";
|
|
12
|
+
import { artifactFor, download as httpDownload, goPinVersion } from "./installer-downloads.mjs";
|
|
13
|
+
import { setupErrorDetail } from "./installer-runner.mjs";
|
|
14
|
+
import { windowsProcessCheck } from "./installer-windows.mjs";
|
|
15
|
+
|
|
16
|
+
/** Where each release's distribution assets live: <base>/v<version>/<asset>. */
|
|
17
|
+
export const DISTRIBUTION_RELEASES = "https://github.com/Gentleman-Programming/gentle-shell/releases/download";
|
|
18
|
+
// GitHub answers a release asset with a redirect to its asset host.
|
|
19
|
+
const ASSET_HOSTS = Object.freeze(["github.com", "objects.githubusercontent.com", "release-assets.githubusercontent.com"]);
|
|
20
|
+
const ASSET_LIMITS = Object.freeze({ distribution: 64 * 1024, lockfile: 8 * 1024 * 1024 });
|
|
21
|
+
const TARGETS = Object.freeze(["darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64", "win32-x64", "win32-arm64"]);
|
|
22
|
+
const SETUP_TIMEOUT = 20 * 60 * 1000;
|
|
23
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
24
|
+
const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
|
|
25
|
+
|
|
26
|
+
/** Every `failedStep` and blocked `reason` runBundledInstall can report, in step order. */
|
|
27
|
+
export const bundledFailedSteps = Object.freeze(["claim-prefix", "copy-npm-settings", "install-runtime", "install-version", "activate-version", "write-launcher",
|
|
28
|
+
"verify-launcher", "bundled-path", "shell-setup"]);
|
|
29
|
+
export const bundledBlockedReasons = Object.freeze(["invalid-request", "consent-required"]);
|
|
30
|
+
|
|
31
|
+
/** The asset pair of release v<version>: { status: "published", manifest,
|
|
32
|
+
* lockfile } once readDistribution accepts it (exact versions, the recorded
|
|
33
|
+
* lockfile sha256); "missing" when GitHub answers 404 for either asset (a
|
|
34
|
+
* release from before the assets existed); "rejected" when they do not read;
|
|
35
|
+
* "unavailable" on any other failure. Each asset is size-bounded and fetched
|
|
36
|
+
* with the identity encoding.
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchDistribution({ version, base = DISTRIBUTION_RELEASES, download = httpDownload }) {
|
|
39
|
+
const texts = [];
|
|
40
|
+
for (const kind of ["distribution", "lockfile"]) {
|
|
41
|
+
try {
|
|
42
|
+
const bytes = await download({ url: `${base}/v${version}/${DISTRIBUTION_ASSETS[kind]}`, maxBytes: ASSET_LIMITS[kind], redirectHosts: ASSET_HOSTS });
|
|
43
|
+
if (!Buffer.isBuffer(bytes) || bytes.length > ASSET_LIMITS[kind]) return { status: "unavailable" };
|
|
44
|
+
texts.push(bytes.toString("utf8"));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { status: error?.status === 404 ? "missing" : "unavailable" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return { status: "published", ...readDistribution(texts[0], texts[1]) };
|
|
51
|
+
} catch {
|
|
52
|
+
return { status: "rejected" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The gate (pure): the bundled install only for a new installation (preflight
|
|
57
|
+
* found no Gentle Shell) of the release channel on a supported target, when this
|
|
58
|
+
* package's own release (`version`) publishes readable distribution assets.
|
|
59
|
+
* Anything else keeps the standard path unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export function bundledGate({ channel, inventory, distribution, version }) {
|
|
62
|
+
return channel === "release" && TARGETS.includes(`${inventory?.platform}-${inventory?.arch}`) && inventory?.shell?.available === false &&
|
|
63
|
+
distribution?.status === "published" && distribution.manifest?.shell === version;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The migration gate (S4, pure): the owner ("pnpm" or "npm") of an existing
|
|
67
|
+
* Gentle Shell on the release channel and a supported target, when release
|
|
68
|
+
* `version` publishes readable assets and is not older than that Gentle Shell;
|
|
69
|
+
* otherwise null. An npm link or an unknown owner is never migrated.
|
|
70
|
+
*/
|
|
71
|
+
export function migrationGate({ channel, inventory, distribution, version }) {
|
|
72
|
+
const shell = inventory?.shell;
|
|
73
|
+
const owner = shell?.available === true && ["pnpm", "npm"].includes(shell.owner) ? shell.owner : null;
|
|
74
|
+
const [found, release] = [STABLE.exec(String(shell?.version))?.slice(1).map(Number), STABLE.exec(String(version))?.slice(1).map(Number)];
|
|
75
|
+
const at = found && release ? found.findIndex((part, index) => part !== release[index]) : -1;
|
|
76
|
+
const newer = at !== -1 && found[at] > release[at];
|
|
77
|
+
return channel === "release" && TARGETS.includes(`${inventory?.platform}-${inventory?.arch}`) && owner && !newer && distribution?.status === "published" &&
|
|
78
|
+
distribution.manifest?.shell === version ? owner : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The fixed bundled actions; the PATH is a step only when the installer applies
|
|
82
|
+
* it. A migration the plan confirms ("plan") ends with removing the old global
|
|
83
|
+
* gentle-pi; one the upgrade confirms ("ask") leaves setup to the launcher's
|
|
84
|
+
* own first run and asks about the removal afterwards.
|
|
85
|
+
*/
|
|
86
|
+
function bundledActions(pathKind, migrate) {
|
|
87
|
+
return [
|
|
88
|
+
{ id: "bundled-prefix", kind: "configure", target: "prefix" },
|
|
89
|
+
{ id: "bundled-runtime", kind: "acquire", target: "runtime" },
|
|
90
|
+
{ id: "bundled-version", kind: "install", target: "shell" },
|
|
91
|
+
{ id: "bundled-activate", kind: "configure", target: "current" },
|
|
92
|
+
...(["symlink", "profile", "registry"].includes(pathKind) ? [{ id: "bundled-path", kind: "configure", target: "path" }] : []),
|
|
93
|
+
...(migrate?.removal === "ask" ? [] : [{ id: "bundled-setup", kind: "normal-setup", target: "shell" }]),
|
|
94
|
+
...(migrate?.removal === "plan" ? [{ id: "remove-global-shell", kind: "remove", target: "global-shell" }] : []),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/** What the plan copy and the outcome show of a pathEntryPlan. */
|
|
98
|
+
function pathSummary(plan) {
|
|
99
|
+
if (plan.kind === "registry") return { kind: "registry", entry: plan.entry };
|
|
100
|
+
if (plan.kind === "symlink") return { kind: "symlink", file: plan.path };
|
|
101
|
+
if (plan.kind === "profile" || plan.kind === "manual") return { kind: plan.kind, file: plan.path, line: plan.line };
|
|
102
|
+
return { kind: "none" };
|
|
103
|
+
}
|
|
104
|
+
/** The bundled plan the user consents to, in the standard plan's shape (no
|
|
105
|
+
* tools or blockers), plus `bundled`: the exact versions, the lockfile's sha256,
|
|
106
|
+
* the prefix, the pinned runtimes and the single PATH change (pathEntryPlan).
|
|
107
|
+
*/
|
|
108
|
+
export function bundledPlan({ platform, distribution, layout, path, migrate }) {
|
|
109
|
+
const { shell, pi } = distribution.manifest;
|
|
110
|
+
const migration = migrate ? { manager: migrate.manager, command: removeGlobalCommand(migrate.manager), removal: migrate.removal } : null;
|
|
111
|
+
return {
|
|
112
|
+
tools: {}, blockers: [], actions: bundledActions(path.kind, migration), ready: false,
|
|
113
|
+
bundled: { shell, pi, id: `${shell}-${pi}`, lockfile: sha256(distribution.lockfile), root: layout.root, bin: layout.bin,
|
|
114
|
+
node: artifactFor("node", "linux", "x64").version, pnpm: artifactFor("pnpm").version, go: platform === "win32" ? goPinVersion : null, path: pathSummary(path),
|
|
115
|
+
...(migration ? { migrate: migration } : {}) },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The consented migration, exactly as bundledPlan writes it, or null. */
|
|
120
|
+
function validMigration(migrate) {
|
|
121
|
+
if (migrate === undefined) return null;
|
|
122
|
+
const ok = ["pnpm", "npm"].includes(migrate?.manager) && ["plan", "ask"].includes(migrate.removal) && migrate.command === removeGlobalCommand(migrate.manager);
|
|
123
|
+
return ok ? migrate : false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The consented plan is exactly bundledPlan's for these assets. */
|
|
127
|
+
function validPlan(plan, distribution) {
|
|
128
|
+
const bundled = plan?.bundled;
|
|
129
|
+
if (typeof bundled !== "object" || bundled === null || distribution?.status !== "published") return false;
|
|
130
|
+
const { shell, pi } = distribution.manifest;
|
|
131
|
+
const migrate = validMigration(bundled.migrate);
|
|
132
|
+
return migrate !== false && bundled.shell === shell && bundled.pi === pi && bundled.id === `${shell}-${pi}` && bundled.lockfile === sha256(distribution.lockfile) &&
|
|
133
|
+
JSON.stringify(plan.actions) === JSON.stringify(bundledActions(bundled.path?.kind, migrate));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* runBundledInstall({ plan, consent }, adapters) -> outcome, in the standard
|
|
138
|
+
* runner's shapes: blocked (nothing changed), failed (stopped after
|
|
139
|
+
* `completed`, with `detail` for install-version and shell-setup: pnpm's or
|
|
140
|
+
* setup's last error line, the home as ~), ready, or terminal-action-required
|
|
141
|
+
* with `action` "open-new-terminal" (a profile line or the user Path was added)
|
|
142
|
+
* or "add-path-line" with `pathLine` { file, line } (a profile the installer may
|
|
143
|
+
* not edit). Steps, logged with log({ step, status }) like the runner's:
|
|
144
|
+
* claim-prefix, copy-npm-settings (the user's npmrc, filtered, S12),
|
|
145
|
+
* install-runtime (our Node with its npm, pnpm, and Go on Windows),
|
|
146
|
+
* install-version (frozen, from the release lockfile), activate-version,
|
|
147
|
+
* write-launcher, bundled-path (only symlink, profile or registry), shell-setup.
|
|
148
|
+
* A migration (S4) checks the launcher's `--version` (verify-launcher) right
|
|
149
|
+
* before the PATH step; its plan may skip setup ("ask") or end with
|
|
150
|
+
* remove-global-shell ("plan"), which runs only after every step passed. A
|
|
151
|
+
* failed removal keeps the outcome and adds removeGlobal { status, command }.
|
|
152
|
+
* Adapters: platform, arch, env, home, distribution (the fetchDistribution
|
|
153
|
+
* result the plan was made from), registry (Windows), log, removeGlobal(manager)
|
|
154
|
+
* -> true once the old global gentle-pi is removed, and operations (trusted
|
|
155
|
+
* local overrides of the bundled-install functions and run).
|
|
156
|
+
*/
|
|
157
|
+
export async function runBundledInstall(request, adapters) {
|
|
158
|
+
const { platform, arch, env, home, distribution, registry, log = () => {} } = adapters;
|
|
159
|
+
const completed = [];
|
|
160
|
+
const blocked = (reason) => {
|
|
161
|
+
log({ step: "gate", status: "blocked", reason });
|
|
162
|
+
return { outcome: "blocked", reason, completed };
|
|
163
|
+
};
|
|
164
|
+
if (request?.consent !== true) return blocked(validPlan(request?.plan, distribution) ? "consent-required" : "invalid-request");
|
|
165
|
+
if (!validPlan(request.plan, distribution)) return blocked("invalid-request");
|
|
166
|
+
const ops = { claimPrefix, userNpmrc, writeNpmrcAuth, ensureRuntime, installVersion, activateVersion, ensureLauncher, pathEntryPlan, applyPathEntry, checkLauncher,
|
|
167
|
+
writePathReceipt, removePathEntry, run: runCommand, ...adapters.operations };
|
|
168
|
+
const consented = request.plan.bundled;
|
|
169
|
+
const planned = (id) => request.plan.actions.some((action) => action.id === id);
|
|
170
|
+
let layout = null;
|
|
171
|
+
let runtime = null;
|
|
172
|
+
let detail = null;
|
|
173
|
+
const lastLine = (result) => setupErrorDetail(result?.stderr, home, platform) ?? setupErrorDetail(result?.stdout, home, platform);
|
|
174
|
+
const steps = [
|
|
175
|
+
["claim-prefix", () => { layout = ops.claimPrefix({ platform, env, home, root: consented.root }); }],
|
|
176
|
+
["copy-npm-settings", () => ops.writeNpmrcAuth(layout, ops.userNpmrc({ platform, env, home }))],
|
|
177
|
+
["install-runtime", async () => { runtime = await ops.ensureRuntime({ layout, platform, arch, go: platform === "win32", env }); }],
|
|
178
|
+
["install-version", async () => {
|
|
179
|
+
try {
|
|
180
|
+
await ops.installVersion({ layout, manifest: distribution.manifest, lockfile: distribution.lockfile, platform, env, go: runtime.go });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
detail = lastLine({ stderr: error?.stderr });
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}],
|
|
186
|
+
["activate-version", () => ops.activateVersion(layout, consented.id)],
|
|
187
|
+
["write-launcher", () => ops.ensureLauncher(layout)],
|
|
188
|
+
// A migration checks the new launcher before the PATH finds it; the old Gentle Shell is removed only after this passed.
|
|
189
|
+
...(consented.migrate ? [["verify-launcher", async () => {
|
|
190
|
+
const check = await ops.checkLauncher(layout, { id: consented.id, shell: consented.shell, pi: consented.pi, platform, env, run: ops.run, launcher: true });
|
|
191
|
+
if (check?.ok === true) return;
|
|
192
|
+
// What the launcher reported against what the plan installed (bounded; its stderr only when it printed nothing).
|
|
193
|
+
const expected = `"gentle-shell ${consented.shell} / pi ${consented.pi}"`;
|
|
194
|
+
const reported = String(check?.reported ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(" / ").slice(0, 120);
|
|
195
|
+
const cause = reported ? "" : lastLine({ stderr: check?.stderr });
|
|
196
|
+
detail = setupErrorDetail(`gentle-shell --version reported ${reported ? `"${reported}"` : "nothing"} instead of ${expected}${cause ? `: ${cause}` : ""}`, home, platform);
|
|
197
|
+
throw new Error("The bundled launcher did not report its versions");
|
|
198
|
+
}]] : []),
|
|
199
|
+
// The change consented to, planned again now that the prefix exists: anything else is not applied.
|
|
200
|
+
...(planned("bundled-path") ? [["bundled-path", () => {
|
|
201
|
+
const plan = ops.pathEntryPlan(layout, { platform, env, home });
|
|
202
|
+
if (JSON.stringify(pathSummary(plan)) !== JSON.stringify(consented.path)) throw new Error("The PATH change differs from the plan");
|
|
203
|
+
const receipt = ops.applyPathEntry(plan, { registry });
|
|
204
|
+
if (receipt?.applied !== true) return;
|
|
205
|
+
// self-uninstall reverts exactly this receipt; an entry it could not record is taken back.
|
|
206
|
+
try {
|
|
207
|
+
ops.writePathReceipt(layout, receipt);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ops.removePathEntry(receipt, { registry });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}]] : []),
|
|
213
|
+
// The .cmd launcher would need cmd.exe on Windows: there setup runs the Node and entry it runs.
|
|
214
|
+
...(planned("bundled-setup") ? [["shell-setup", async () => {
|
|
215
|
+
const entry = win32.join(layout.versions, consented.id, "node_modules", "gentle-pi", "bin", "gentle-shell.mjs");
|
|
216
|
+
const [command, args] = platform === "win32" ? [layout.node, [entry, "setup"]] : [layout.launcher, ["setup"]];
|
|
217
|
+
const result = await ops.run(command, args, { cwd: layout.root, env: setupEnvironment(layout, { platform, env, id: consented.id }), timeout: SETUP_TIMEOUT });
|
|
218
|
+
if (result?.status === 0) return;
|
|
219
|
+
detail = lastLine(result);
|
|
220
|
+
throw new Error("gentle-shell setup failed");
|
|
221
|
+
}]] : []),
|
|
222
|
+
];
|
|
223
|
+
for (const [step, run] of steps) {
|
|
224
|
+
try {
|
|
225
|
+
await run();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// A migration's failure always says why: the step's own detail, otherwise its error message.
|
|
228
|
+
if (!detail && consented.migrate) detail = lastLine({ stderr: String(error?.message ?? "") });
|
|
229
|
+
log({ step, status: "failed" });
|
|
230
|
+
return { outcome: "failed", failedStep: step, completed, ...(detail ? { detail } : {}) };
|
|
231
|
+
}
|
|
232
|
+
completed.push(step);
|
|
233
|
+
log({ step, status: "done" });
|
|
234
|
+
}
|
|
235
|
+
let removal = {};
|
|
236
|
+
if (planned("remove-global-shell")) {
|
|
237
|
+
const removed = await Promise.resolve().then(() => adapters.removeGlobal?.(consented.migrate.manager)).catch(() => false);
|
|
238
|
+
if (removed === true) completed.push("remove-global-shell");
|
|
239
|
+
log({ step: "remove-global-shell", status: removed === true ? "done" : "failed" });
|
|
240
|
+
removal = { removeGlobal: { status: removed === true ? "removed" : "failed", command: consented.migrate.command } };
|
|
241
|
+
}
|
|
242
|
+
const path = consented.path;
|
|
243
|
+
if (path.kind === "manual") return { outcome: "terminal-action-required", action: "add-path-line", pathLine: { file: path.file, line: path.line }, completed, ...removal };
|
|
244
|
+
if (path.kind === "profile" || path.kind === "registry") return { outcome: "terminal-action-required", action: "open-new-terminal", completed, ...removal };
|
|
245
|
+
return { outcome: "ready", completed, ...removal };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The `gentle-shell` a new terminal would run before the bundled launcher, or
|
|
249
|
+
* null when the launcher comes first (or none is found). A new terminal's PATH
|
|
250
|
+
* is this one with the planned change:
|
|
251
|
+
* - a zsh or bash profile line (or the line the user adds) goes first: it is
|
|
252
|
+
* appended to the startup file read last for terminals (.zshrc after
|
|
253
|
+
* .zprofile; the macOS bash login file or .bashrc), after pnpm setup's own
|
|
254
|
+
* PNPM_HOME line there;
|
|
255
|
+
* - a fish line (`profile`, conf.d/gentle-shell.fish) counts as last: fish reads
|
|
256
|
+
* conf.d before config.fish, so whatever config.fish prepends (pnpm setup's
|
|
257
|
+
* PNPM_HOME block) comes before it, and the current PATH already shows it;
|
|
258
|
+
* - the Windows user Path entry goes last;
|
|
259
|
+
* - a ~/.local/bin link or an existing entry stays where it already is.
|
|
260
|
+
* fs: { exists(path), realpath(path) }.
|
|
261
|
+
*/
|
|
262
|
+
export function shadowingCommand({ platform, env, layout, pathKind, profile, fs = { exists: existsSync, realpath: realpathSync } }) {
|
|
263
|
+
const windows = platform === "win32";
|
|
264
|
+
const path = windows ? win32 : posix;
|
|
265
|
+
const value = windows ? Object.entries(env).find(([key]) => key.toUpperCase() === "PATH")?.[1] : env.PATH;
|
|
266
|
+
const entries = String(value ?? "").split(path.delimiter).filter(Boolean);
|
|
267
|
+
const line = ["profile", "manual"].includes(pathKind);
|
|
268
|
+
const last = pathKind === "registry" || (line && /\.fish$/.test(String(profile ?? "")));
|
|
269
|
+
const ordered = last ? [...entries, layout.bin] : line ? [layout.bin, ...entries] : entries;
|
|
270
|
+
const names = windows ? ["gentle-shell.exe", "gentle-shell.cmd", "gentle-shell.bat"] : ["gentle-shell"];
|
|
271
|
+
const real = (file) => { try { return fs.realpath(file); } catch { return file; } };
|
|
272
|
+
for (const directory of ordered) {
|
|
273
|
+
const found = names.map((name) => path.join(directory, name)).find((file) => fs.exists(file));
|
|
274
|
+
if (!found) continue;
|
|
275
|
+
const same = windows ? real(found).toLowerCase() === real(layout.launcher).toLowerCase() : real(found) === real(layout.launcher);
|
|
276
|
+
return same ? null : found;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const yes = (answer) => /^\s*y(?:es)?\s*$/i.test(String(answer ?? ""));
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `gentle-shell upgrade` from a global pnpm or npm install (S4): when release
|
|
285
|
+
* `latest` publishes the assets (migrationGate), explains the move, installs the
|
|
286
|
+
* bundled prefix with runBundledInstall (a plan with removal "ask": no setup,
|
|
287
|
+
* the launcher's own first run does it), then asks one question: remove the old
|
|
288
|
+
* global gentle-pi with its own manager (default No; without a terminal it
|
|
289
|
+
* never removes and prints the command). Returns null when the migration does
|
|
290
|
+
* not apply, or when it failed before the question (it prints the step and its
|
|
291
|
+
* detail, and the old package is untouched): the caller keeps its own path for
|
|
292
|
+
* this run. Otherwise { code, installed: true }. The owner is probed only once
|
|
293
|
+
* the release, the assets and the versions allow a migration.
|
|
294
|
+
* adapters: owner() -> ownerManager's { name, command, prefix } or a throw;
|
|
295
|
+
* distribution(version) -> fetchDistribution's result; run (the manager's
|
|
296
|
+
* runner); registry (Windows); operations and fs as runBundledInstall and
|
|
297
|
+
* shadowingCommand take them.
|
|
298
|
+
*/
|
|
299
|
+
export async function migrateGlobalShell({ latest, currentVersion, platform, arch, env, home, interactive, ask, out, adapters }) {
|
|
300
|
+
const untouched = "Your global gentle-pi was left untouched; it is updated the usual way instead.";
|
|
301
|
+
const distribution = await adapters.distribution(latest);
|
|
302
|
+
const shell = { available: true, version: currentVersion };
|
|
303
|
+
// Every other condition first: the owner probe runs the user's `npm root -g`.
|
|
304
|
+
if (!migrationGate({ channel: "release", inventory: { platform, arch, shell: { ...shell, owner: "npm" } }, distribution, version: latest })) return null;
|
|
305
|
+
const manager = await Promise.resolve().then(() => adapters.owner()).catch(() => null);
|
|
306
|
+
if (!migrationGate({ channel: "release", inventory: { platform, arch, shell: { ...shell, owner: manager?.name ?? null } }, distribution, version: latest })) {
|
|
307
|
+
out("Gentle Shell now installs as one self-contained product, but this Gentle Shell is not a global pnpm or npm package (an `npm link`, for example), so it is not moved.");
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const ops = { prefixRoot, pathEntryPlan, ...adapters.operations };
|
|
311
|
+
let layout;
|
|
312
|
+
let plan;
|
|
313
|
+
try {
|
|
314
|
+
layout = prefixLayout({ platform, env, home, root: ops.prefixRoot({ platform, env }) });
|
|
315
|
+
plan = bundledPlan({ platform, distribution, layout, path: ops.pathEntryPlan(layout, { platform, env, home }), migrate: { manager: manager.name, removal: "ask" } });
|
|
316
|
+
} catch (error) {
|
|
317
|
+
out(`The self-contained install could not be planned: ${setupErrorDetail(String(error?.message ?? ""), home, platform) ?? "unknown error"}. ${untouched}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const { shell: shellVersion, pi, migrate } = plan.bundled;
|
|
321
|
+
for (const line of [`Gentle Shell ${shellVersion} installs as one self-contained product in ${layout.root}, with its own Node.js, npm and pnpm and exactly Pi ${pi}.`,
|
|
322
|
+
"Your own Node.js, npm, pnpm and Pi are not used or changed, and your Gentle Shell home (~/.gentle-shell/agent) stays where it is.",
|
|
323
|
+
`Your global gentle-pi (installed with ${manager.name}) is kept until you choose to remove it.`]) out(line);
|
|
324
|
+
const outcome = await runBundledInstall({ plan, consent: true }, { platform, arch, env, home, distribution, registry: adapters.registry,
|
|
325
|
+
operations: adapters.operations });
|
|
326
|
+
if (outcome.outcome !== "ready" && outcome.outcome !== "terminal-action-required") {
|
|
327
|
+
const step = outcome.failedStep ?? outcome.reason ?? "unknown";
|
|
328
|
+
out(`The self-contained install stopped at ${step}${outcome.detail ? `: ${outcome.detail}` : ""}. ${untouched}`);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
out(`Installed Gentle Shell ${shellVersion} with Pi ${pi} in ${layout.root}.`);
|
|
332
|
+
if (outcome.action === "add-path-line") out(`Add this line to ${outcome.pathLine.file}, then open a new terminal: ${outcome.pathLine.line}`);
|
|
333
|
+
else if (outcome.action === "open-new-terminal") out("Open a new terminal so it finds the new `gentle-shell`.");
|
|
334
|
+
const question = `Remove the old global gentle-pi with ${manager.name} (${migrate.command})? [y/N] `;
|
|
335
|
+
let code = 0;
|
|
336
|
+
if (interactive && yes(await ask(question))) {
|
|
337
|
+
if (await removeGlobalShell(manager, adapters.run)) {
|
|
338
|
+
out(`Removed the old global gentle-pi with ${manager.name}.`);
|
|
339
|
+
return { code, installed: true };
|
|
340
|
+
}
|
|
341
|
+
out(`The old global gentle-pi could not be removed; Gentle Shell ${shellVersion} stays installed in ${layout.root}. Remove it yourself with: ${migrate.command}`);
|
|
342
|
+
code = 1;
|
|
343
|
+
} else {
|
|
344
|
+
out(`The old global gentle-pi was kept. To remove it, run: ${migrate.command}`);
|
|
345
|
+
}
|
|
346
|
+
const shadow = shadowingCommand({ platform, env, layout, pathKind: plan.bundled.path.kind, profile: plan.bundled.path.file, ...(adapters.fs ? { fs: adapters.fs } : {}) });
|
|
347
|
+
if (shadow) {
|
|
348
|
+
out(`New terminals still run ${shadow} (the old gentle-pi), because its folder comes first on PATH. ` +
|
|
349
|
+
`Remove the old package with ${migrate.command}, or put ${layout.bin} before that folder on PATH.`);
|
|
350
|
+
}
|
|
351
|
+
return { code, installed: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// The HKCU user Path, read without expanding %VARIABLES%, keeps its value kind;
|
|
355
|
+
// a WM_SETTINGCHANGE broadcast (through a user variable set and removed) lets
|
|
356
|
+
// new terminals see it. The entry and the mode are data, never script text.
|
|
357
|
+
const pathScript = `$ErrorActionPreference = 'Stop'
|
|
358
|
+
$entry = $env:GENTLE_BUNDLED_PATH_ENTRY
|
|
359
|
+
$add = $env:GENTLE_BUNDLED_PATH_MODE -eq 'add'
|
|
360
|
+
$same = { param($part) $part.TrimEnd('\\') -ieq $entry.TrimEnd('\\') }
|
|
361
|
+
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
|
|
362
|
+
try {
|
|
363
|
+
$exists = $key.GetValueNames() -contains 'Path'
|
|
364
|
+
$kind = if ($exists) { $key.GetValueKind('Path') } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
|
365
|
+
if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw 'unsupported Path kind' }
|
|
366
|
+
$current = if ($exists) { [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { '' }
|
|
367
|
+
$parts = @($current.Split(';') | Where-Object { $_ -ne '' })
|
|
368
|
+
$ours = @($parts | Where-Object { & $same $_ })
|
|
369
|
+
if ($add -and $ours.Count -gt 0) { $result = 'present' }
|
|
370
|
+
elseif (-not $add -and $ours.Count -eq 0) { $result = 'absent' }
|
|
371
|
+
else {
|
|
372
|
+
$next = if ($add) { @($parts) + $entry } else { @($parts | Where-Object { -not (& $same $_) }) }
|
|
373
|
+
$key.SetValue('Path', ($next -join ';'), $kind)
|
|
374
|
+
$result = if ($add) { 'added' } else { 'removed' }
|
|
375
|
+
}
|
|
376
|
+
} finally { $key.Close() }
|
|
377
|
+
if ($result -eq 'added' -or $result -eq 'removed') {
|
|
378
|
+
[Environment]::SetEnvironmentVariable('GENTLE_SHELL_PATH_REFRESH', '1', 'User')
|
|
379
|
+
[Environment]::SetEnvironmentVariable('GENTLE_SHELL_PATH_REFRESH', $null, 'User')
|
|
380
|
+
}
|
|
381
|
+
$result`;
|
|
382
|
+
/** The registry adapter applyPathEntry and removePathEntry use on Windows:
|
|
383
|
+
* add(entry) appends the absolute `entry` to the HKCU user Path once and returns
|
|
384
|
+
* "added", or "present" when the Path already held it; remove(entry) removes
|
|
385
|
+
* every spelling of it; nothing else in the Path changes.
|
|
386
|
+
*/
|
|
387
|
+
export function windowsPathRegistry(env, processAdapter = windowsProcessCheck) {
|
|
388
|
+
const systemRoot = env.SystemRoot ?? Object.entries(env).find(([key]) => key.toLowerCase() === "systemroot")?.[1];
|
|
389
|
+
const edit = (mode, entry, expected) => {
|
|
390
|
+
if (!win32.isAbsolute(entry ?? "") || entry.startsWith("\\\\") || /[;"\r\n]/.test(entry) || !win32.isAbsolute(systemRoot ?? "")) {
|
|
391
|
+
throw new Error("Unsafe entry for the user Path");
|
|
392
|
+
}
|
|
393
|
+
const output = processAdapter(win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
394
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", pathScript], { ...env, GENTLE_BUNDLED_PATH_ENTRY: entry, GENTLE_BUNDLED_PATH_MODE: mode });
|
|
395
|
+
if (!expected.includes(output)) throw new Error("The user Path could not be changed");
|
|
396
|
+
return output;
|
|
397
|
+
};
|
|
398
|
+
return { add: (entry) => edit("add", entry, ["added", "present"]), remove: (entry) => edit("remove", entry, ["removed", "absent"]) };
|
|
399
|
+
}
|
package/scripts/check-types.mjs
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// TypeScript diagnostic ratchet; the committed baseline is now clean.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Fixing diagnostics and refreshing the baseline is the intended way to shrink
|
|
8
|
-
// it.
|
|
4
|
+
// With a zero baseline, every diagnostic fails the gate. If a nonzero baseline
|
|
5
|
+
// is explicitly accepted, increases for any (file, error code) pair or the
|
|
6
|
+
// total fail. Fixing diagnostics and refreshing the baseline strengthens it.
|
|
9
7
|
//
|
|
10
8
|
// Known blind spot, stated rather than hidden: keying on (file, code) counts is
|
|
11
9
|
// what keeps the baseline stable while files are edited, because line numbers
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
writeFile,
|
|
14
14
|
} from "node:fs/promises";
|
|
15
15
|
import https from "node:https";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
16
17
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
import { execFile } from "node:child_process";
|
|
@@ -36,7 +37,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
37
|
// version check below) derives from this constant instead of repeating the
|
|
37
38
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
39
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "4.
|
|
40
|
+
export const INSTALLER_VERSION = "4.1.1";
|
|
40
41
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
42
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
43
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -45,11 +46,11 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
46
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
|
|
46
47
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
|
|
47
48
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.
|
|
49
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.1.1`
|
|
49
50
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
+
// tag resolves to commit 898d3976e8049a7238eaa7de60bff325c30d2b3d, the published v4.1.1 release head.
|
|
51
52
|
// v4.0.0 moved the Go module path to the /v4 major-version suffix.
|
|
52
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1
|
|
53
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=";
|
|
53
54
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
54
55
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
55
56
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -68,7 +69,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
68
69
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
69
70
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
70
71
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
71
|
-
// still holds it. The v4.
|
|
72
|
+
// still holds it. The v4.1.1 digests are pinned from the published release:
|
|
72
73
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
73
74
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
74
75
|
// executables.
|
|
@@ -110,15 +111,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
113
|
-
// distributing unsigned Windows builds in c4b764d0, so v4.
|
|
114
|
+
// distributing unsigned Windows builds in c4b764d0, so v4.1.1 publishes signed
|
|
114
115
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
115
116
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
116
117
|
// upstream ships signed Windows assets.
|
|
117
118
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
118
|
-
"darwin/amd64": asset("gentle-ai_4.
|
|
119
|
-
"darwin/arm64": asset("gentle-ai_4.
|
|
120
|
-
"linux/amd64": asset("gentle-ai_4.
|
|
121
|
-
"linux/arm64": asset("gentle-ai_4.
|
|
119
|
+
"darwin/amd64": asset("gentle-ai_4.1.1_darwin_amd64.tar.gz", "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338", "gentle-ai"),
|
|
120
|
+
"darwin/arm64": asset("gentle-ai_4.1.1_darwin_arm64.tar.gz", "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a", "gentle-ai"),
|
|
121
|
+
"linux/amd64": asset("gentle-ai_4.1.1_linux_amd64.tar.gz", "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe", "gentle-ai"),
|
|
122
|
+
"linux/arm64": asset("gentle-ai_4.1.1_linux_arm64.tar.gz", "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55", "gentle-ai"),
|
|
122
123
|
});
|
|
123
124
|
|
|
124
125
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -194,16 +195,32 @@ export async function downloadGentleAiAsset(url, destination, maxBytes = MAX_DOW
|
|
|
194
195
|
const downloadOnce = async () => {
|
|
195
196
|
const response = await responseFor(url, redirects), contentLength = Number(response.headers["content-length"] ?? "0");
|
|
196
197
|
if (!Number.isSafeInteger(contentLength) || contentLength < 0 || contentLength > maxBytes) { response.resume(); throw new Error("Gentle AI download exceeds the maximum allowed size"); }
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
let created = false;
|
|
199
|
+
try {
|
|
200
|
+
await new Promise((resolve, reject) => {
|
|
201
|
+
const output = createWriteStream(destination, { flags: "wx", mode: 0o600 }); let received = 0, settled = false;
|
|
202
|
+
output.on("open", () => { created = true; });
|
|
203
|
+
let timer = setTimeout(() => response.destroy(downloadTimeoutError("body")), bodyTimeoutMs);
|
|
204
|
+
// The exclusive open is asynchronous, so a failed attempt may settle the
|
|
205
|
+
// download before its destination stream finished with the filesystem. Wait
|
|
206
|
+
// for that stream to close first: otherwise a late creation outlives the
|
|
207
|
+
// retry's removal of the destination and the next exclusive open fails with
|
|
208
|
+
// EEXIST instead of reporting the timeout that caused the retry.
|
|
209
|
+
const closed = () => new Promise((done) => { if (output.closed) done(); else output.on("close", done); });
|
|
210
|
+
const finish = (callback, value) => { if (settled) return; settled = true; clearTimeout(timer); void closed().then(() => callback(value)); };
|
|
211
|
+
const fail = (error) => { response.destroy(); output.destroy(); finish(reject, error); };
|
|
212
|
+
const reset = () => { clearTimeout(timer); timer = setTimeout(() => response.destroy(downloadTimeoutError("body")), bodyTimeoutMs); };
|
|
213
|
+
response.on("data", (chunk) => { reset(); received += chunk.length; if (received > maxBytes) response.destroy(new Error("Gentle AI download exceeds the maximum allowed size")); });
|
|
214
|
+
response.on("error", fail); response.setTimeout?.(bodyTimeoutMs, () => response.destroy(downloadTimeoutError("body")));
|
|
215
|
+
output.on("error", fail); output.on("finish", () => finish(resolve)); response.pipe(output);
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// A failed attempt removes only the file it created, once its stream closed, so
|
|
219
|
+
// the retry starts from a free destination path. A path this download never
|
|
220
|
+
// owned is left alone instead of being deleted with it.
|
|
221
|
+
if (created) await rm(destination, { force: true });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
207
224
|
};
|
|
208
225
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try { if (attempt > 1) await rm(destination, { force: true }); await downloadOnce(); return; } catch (error) {
|
|
209
226
|
if (attempt === maxAttempts || !isRetryableDownloadError(error)) throw error;
|
|
@@ -568,7 +585,7 @@ async function recoverInterruptedPublication(runtimeRoot, bundleIsValid, options
|
|
|
568
585
|
const live = versionBundlePath(runtimeRoot);
|
|
569
586
|
const liveExists = await realBundleDirectory(live, runtimeRoot, "live bundle");
|
|
570
587
|
if (!liveExists) {
|
|
571
|
-
try { await (
|
|
588
|
+
try { await renameWithWindowsPublicationRetry(backup, live, options); }
|
|
572
589
|
catch (error) { throw bundleRecoveryError(runtimeRoot, `could not restore valid backup ${backup}: ${error instanceof Error ? error.message : String(error)}`); }
|
|
573
590
|
return;
|
|
574
591
|
}
|
|
@@ -585,21 +602,43 @@ async function cleanupStaleStagingBundles(runtimeRoot) {
|
|
|
585
602
|
}
|
|
586
603
|
}
|
|
587
604
|
|
|
605
|
+
// Windows publication can lose a race with the just-executed gentle-ai.exe or a
|
|
606
|
+
// real-time scanner still holding the staging directory. Retry only those lock
|
|
607
|
+
// codes, and only on the effective win32 platform, so a permanent failure still
|
|
608
|
+
// surfaces as the original error instead of a successful install.
|
|
609
|
+
const WINDOWS_PUBLICATION_RETRY_DELAYS_MS = [200, 400, 800, 1600];
|
|
610
|
+
|
|
611
|
+
function isRetryableWindowsPublicationLock(error) {
|
|
612
|
+
return Boolean(error && typeof error === "object" && ["EPERM", "EBUSY", "EACCES"].includes(error.code));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function renameWithWindowsPublicationRetry(from, to, options) {
|
|
616
|
+
const renameFile = options.rename ?? rename;
|
|
617
|
+
if ((options.platform ?? process.platform) !== "win32") return renameFile(from, to);
|
|
618
|
+
for (let attempt = 0; attempt <= WINDOWS_PUBLICATION_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
619
|
+
try { return await renameFile(from, to); }
|
|
620
|
+
catch (error) {
|
|
621
|
+
if (attempt === WINDOWS_PUBLICATION_RETRY_DELAYS_MS.length || !isRetryableWindowsPublicationLock(error)) throw error;
|
|
622
|
+
await new Promise((resolve) => setTimeout(resolve, WINDOWS_PUBLICATION_RETRY_DELAYS_MS[attempt]));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
588
627
|
async function publishBundle(runtimeRoot, stagingDirectory, options) {
|
|
589
|
-
const versionDirectory = join(runtimeRoot, `v${INSTALLER_VERSION}`)
|
|
628
|
+
const versionDirectory = join(runtimeRoot, `v${INSTALLER_VERSION}`);
|
|
590
629
|
const backupDirectory = join(runtimeRoot, `.v${INSTALLER_VERSION}.backup-${process.pid}-${Date.now()}`);
|
|
591
630
|
let movedPrior = false;
|
|
592
631
|
try {
|
|
593
632
|
try {
|
|
594
633
|
const current = await lstat(versionDirectory);
|
|
595
634
|
if (!current.isDirectory() || current.isSymbolicLink()) throw new Error("Gentle AI package-local version directory must be a real directory");
|
|
596
|
-
await
|
|
635
|
+
await renameWithWindowsPublicationRetry(versionDirectory, backupDirectory, options);
|
|
597
636
|
movedPrior = true;
|
|
598
637
|
} catch (error) { if (!(error && typeof error === "object" && error.code === "ENOENT")) throw error; }
|
|
599
|
-
await
|
|
638
|
+
await renameWithWindowsPublicationRetry(stagingDirectory, versionDirectory, options);
|
|
600
639
|
} catch (error) {
|
|
601
640
|
if (movedPrior) {
|
|
602
|
-
try { await
|
|
641
|
+
try { await renameWithWindowsPublicationRetry(backupDirectory, versionDirectory, options); }
|
|
603
642
|
catch (rollbackError) { throw new Error("Gentle AI bundle publication failed and rollback could not restore the prior bundle", { cause: rollbackError }); }
|
|
604
643
|
}
|
|
605
644
|
throw error;
|
|
@@ -623,31 +662,39 @@ async function installWindowsGentleAiFromGoSumdb(options, packageRoot, architect
|
|
|
623
662
|
return withInstallLock(packageRoot, options, async (runtimeRoot) => {
|
|
624
663
|
await cleanupStaleStagingBundles(runtimeRoot);
|
|
625
664
|
const stagingDirectory = await mkdtemp(join(runtimeRoot, `.v${INSTALLER_VERSION}.staging-`));
|
|
665
|
+
// Go builds in a short private directory under the system temp directory, never inside
|
|
666
|
+
// the package: from a deep pnpm store path (179 characters, run 38063142924) the
|
|
667
|
+
// working directories Go gives asm.exe inside GOMODCACHE exceed MAX_PATH and
|
|
668
|
+
// CreateProcess fails with "The directory name is invalid."
|
|
669
|
+
let buildDirectory = null;
|
|
626
670
|
try {
|
|
627
671
|
await chmod(stagingDirectory, 0o700);
|
|
628
|
-
|
|
629
|
-
await
|
|
672
|
+
buildDirectory = await mkdtemp(join(options.temporaryDirectory ?? tmpdir(), "gai-"));
|
|
673
|
+
await chmod(buildDirectory, 0o700);
|
|
630
674
|
const goPath = await resolveWindowsGoExecutable(options);
|
|
631
675
|
const environment = sealedGoEnvironment(goPath, buildDirectory, architecture);
|
|
632
676
|
for (const directory of [environment.GOBIN, environment.GOPATH, environment.GOMODCACHE, environment.GOCACHE, environment.TEMP]) await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
633
677
|
await recoverInterruptedPublication(runtimeRoot, (directory) => existingWindowsSourceBundleMatches(directory, execute, goPath, environment, architecture), options);
|
|
634
678
|
const existing = versionBundlePath(runtimeRoot);
|
|
635
679
|
if (await existingWindowsSourceBundleMatches(existing, execute, goPath, environment, architecture)) return { installed: false, binaryPath: join(existing, "gentle-ai.exe"), method: GENTLE_AI_INSTALL_METHOD.GO_SUMDB_SOURCE_BUILD };
|
|
636
|
-
await assertGoToolchain(execute, goPath, environment,
|
|
637
|
-
try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment,
|
|
680
|
+
await assertGoToolchain(execute, goPath, environment, buildDirectory);
|
|
681
|
+
try { await runCommand(execute, goPath, ["install", GENTLE_AI_WINDOWS_SOURCE_PACKAGE], commandOptions(environment, buildDirectory, GO_INSTALL_TIMEOUT_MS)); }
|
|
638
682
|
catch (error) { throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, `Gentle AI Go SumDB source installation failed for ${GENTLE_AI_WINDOWS_SOURCE_PACKAGE}.`, error); }
|
|
639
683
|
const builtBinary = join(environment.GOBIN, "gentle-ai.exe"), binaryPath = join(stagingDirectory, "gentle-ai.exe");
|
|
640
684
|
const details = await lstat(builtBinary);
|
|
641
685
|
if (!details.isFile() || details.isSymbolicLink()) throw new GentleAiInstallerError(GENTLE_AI_GO_INSTALL_FAILED_CODE, "Gentle AI Go installation produced a non-regular gentle-ai.exe.");
|
|
642
686
|
await copyFile(builtBinary, binaryPath);
|
|
643
|
-
const metadata = await verifyGoBuildMetadata(execute, goPath, binaryPath, environment,
|
|
644
|
-
await assertExactGentleAiVersion(execute, binaryPath, environment,
|
|
687
|
+
const metadata = await verifyGoBuildMetadata(execute, goPath, binaryPath, environment, buildDirectory, architecture);
|
|
688
|
+
await assertExactGentleAiVersion(execute, binaryPath, environment, buildDirectory);
|
|
645
689
|
const binarySha256 = await sha256File(binaryPath);
|
|
646
690
|
await writeFile(join(stagingDirectory, "integrity.json"), canonicalManifest(windowsSourceManifest(metadata, binarySha256, architecture)), { mode: 0o600 });
|
|
647
691
|
await safeRemoveDirectory(buildDirectory);
|
|
648
692
|
const published = await publishBundle(runtimeRoot, stagingDirectory, options);
|
|
649
693
|
return { installed: true, binaryPath: join(published, "gentle-ai.exe"), method: GENTLE_AI_INSTALL_METHOD.GO_SUMDB_SOURCE_BUILD };
|
|
650
|
-
} finally {
|
|
694
|
+
} finally {
|
|
695
|
+
try { if (buildDirectory !== null) await safeRemoveDirectory(buildDirectory); }
|
|
696
|
+
finally { await safeRemoveDirectory(stagingDirectory); }
|
|
697
|
+
}
|
|
651
698
|
});
|
|
652
699
|
}
|
|
653
700
|
|