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,255 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal DisableDelayedExpansion
|
|
3
|
+
rem Trusted extracted/checkout bundle only. Never pipe a remote script here.
|
|
4
|
+
rem Fixed stock PowerShell commands; paths are environment DATA, not PS source.
|
|
5
|
+
rem Each invocation is below CMD's logical command length limit.
|
|
6
|
+
set "GENTLE_BOOTSTRAP_BUNDLE=%~dp0.."
|
|
7
|
+
set "GENTLE_BOOTSTRAP_TOOLS=%LOCALAPPDATA%\.gentle-shell-bootstrap-tools.%RANDOM%.%RANDOM%.%RANDOM%"
|
|
8
|
+
set "GENTLE_BOOTSTRAP_FALLBACK_TOOLS=%USERPROFILE%\.gentle-shell-bootstrap-tools.%RANDOM%.%RANDOM%.%RANDOM%"
|
|
9
|
+
set "GENTLE_BOOTSTRAP_OWNED="
|
|
10
|
+
set "GENTLE_BOOTSTRAP_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
11
|
+
if not exist "%GENTLE_BOOTSTRAP_PS%" goto unavailable
|
|
12
|
+
|
|
13
|
+
rem Bundle/dependency checks precede ALL acquisition and per-user writes.
|
|
14
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
15
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
16
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
17
|
+
"$bundle = [IO.Path]::GetFullPath($env:GENTLE_BOOTSTRAP_BUNDLE);" ^
|
|
18
|
+
"foreach ($file in @('bin/gentle-shell-install.mjs','package.json','scripts/installer-downloads.mjs','scripts/installer-windows.mjs','scripts/installer-windows-artifacts.json')) {" ^
|
|
19
|
+
" $item = Get-Item -LiteralPath (Join-Path $bundle $file) -Force;" ^
|
|
20
|
+
" if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Unsafe bundle file' };" ^
|
|
21
|
+
"};" ^
|
|
22
|
+
"$metadata = Get-Content -LiteralPath (Join-Path $bundle 'package.json') -Raw | ConvertFrom-Json;" ^
|
|
23
|
+
"if ($metadata.engines.node -notmatch '^>=[0-9]+\.[0-9]+\.[0-9]+$' -or $metadata.packageManager -ne 'pnpm@11.1.1') { throw 'Repository prerequisite unknown' };" ^
|
|
24
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: future wizard/dependent bundle file missing, unsupported metadata, or managed policy denial. No acquisition attempted.'); exit 1 } }"
|
|
25
|
+
if errorlevel 1 goto failed
|
|
26
|
+
|
|
27
|
+
rem Claim an absent unique directory beneath a verified local per-user parent:
|
|
28
|
+
rem %LOCALAPPDATA% first, then one %USERPROFILE% fallback only when the first fails
|
|
29
|
+
rem acl-mask or home-owner. Each candidate gets the exact-parent rule and the whole
|
|
30
|
+
rem walk; exit 2 means the fallback was claimed. Its owner may be the user, SYSTEM
|
|
31
|
+
rem or Administrators; the new directory's owner is set to the user explicitly.
|
|
32
|
+
rem No security changes outside this new prerequisite directory.
|
|
33
|
+
rem Strict target/parent rights: ReadAndExecute + Synchronize (0x1200a9).
|
|
34
|
+
rem Distant existing ancestors additionally permit sibling CreateDirectories (4).
|
|
35
|
+
rem WriteData/reparse-affecting writes, deletion and ACL/owner changes still fail.
|
|
36
|
+
rem Failures append one fixed reason code (the failed check), then one line per
|
|
37
|
+
rem candidate naming its folder and, for acl-mask, the principal and its rights;
|
|
38
|
+
rem other exceptions report unexpected-<step>. ACLs use .NET, not Security-module
|
|
39
|
+
rem cmdlets: PowerShell 7's inherited PSModulePath breaks their 5.1 autoload.
|
|
40
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
41
|
+
"& { $claimed = $false; $step = 'policy'; $tools = $null; $tried = @(); try { $ErrorActionPreference = 'Stop';" ^
|
|
42
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'policy' };" ^
|
|
43
|
+
"$me = [Security.Principal.WindowsIdentity]::GetCurrent().User;" ^
|
|
44
|
+
"$trusted = @($me.Value,'S-1-5-18','S-1-5-32-544','S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464');" ^
|
|
45
|
+
"$owners = @($me.Value,'S-1-5-18','S-1-5-32-544'); $account = { param($sid) try { ' (' + $sid.Translate([Security.Principal.NTAccount]).Value + ')' } catch { '' } };" ^
|
|
46
|
+
"$candidates = @(,@($env:GENTLE_BOOTSTRAP_TOOLS,$env:LOCALAPPDATA)); if ($env:GENTLE_BOOTSTRAP_FALLBACK_TOOLS) { $candidates += ,@($env:GENTLE_BOOTSTRAP_FALLBACK_TOOLS,$env:USERPROFILE) };" ^
|
|
47
|
+
"for ($index = 0; $index -lt $candidates.Count; $index++) { $base = $candidates[$index][1]; $detail = ''; try {" ^
|
|
48
|
+
" $step = 'path-mismatch'; $tools = [IO.Path]::GetFullPath($candidates[$index][0]); $path = [IO.Directory]::GetParent($tools).FullName;" ^
|
|
49
|
+
" if (-not [IO.Path]::IsPathRooted($base) -or $tools.StartsWith('\\') -or $path -ne [IO.Path]::GetFullPath($base)) { throw 'path-mismatch' };" ^
|
|
50
|
+
" $depth = 1;" ^
|
|
51
|
+
" $step = 'home-owner'; $owner = ([IO.Directory]::GetAccessControl($path)).GetOwner([Security.Principal.SecurityIdentifier]); if ($owners -notcontains $owner.Value) { $detail = ' owned by ' + $owner.Value + (& $account $owner); throw 'home-owner' };" ^
|
|
52
|
+
" $step = 'ancestor-walk'; while ($path) {" ^
|
|
53
|
+
" $item = Get-Item -LiteralPath $path -Force; if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'ancestor-reparse' };" ^
|
|
54
|
+
" $acl = [IO.Directory]::GetAccessControl($path); if ($trusted -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { throw 'ancestor-owner' };" ^
|
|
55
|
+
" $allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };" ^
|
|
56
|
+
" foreach ($rule in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) {" ^
|
|
57
|
+
" if ($rule.AccessControlType -eq 'Allow' -and -not ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -and ([long]$rule.FileSystemRights -band (-bnot [long]$allowedRights)) -and $trusted -notcontains $rule.IdentityReference.Value) {" ^
|
|
58
|
+
" $detail = ' at ' + $path + ': ' + $rule.IdentityReference.Value + (& $account $rule.IdentityReference) + ' allowed 0x' + ([int]$rule.FileSystemRights).ToString('X8'); throw 'acl-mask' };" ^
|
|
59
|
+
" }; $parent = [IO.Directory]::GetParent($path); if ($null -eq $parent) { break }; $path = $parent.FullName; $depth++;" ^
|
|
60
|
+
" }; break;" ^
|
|
61
|
+
"} catch { $code = 'unexpected-' + $step; if ($_.Exception.Message -cmatch '^(path-mismatch|home-owner|ancestor-reparse|ancestor-owner|acl-mask)$') { $code = $_.Exception.Message };" ^
|
|
62
|
+
" $tried += ($base + ': ' + $code + $detail); if ($index -eq 0 -and $candidates.Count -gt 1 -and $code -cmatch '^(acl-mask|home-owner)$') { continue }; throw } };" ^
|
|
63
|
+
"$step = 'create'; if (Test-Path -LiteralPath $tools) { throw 'collision' }; $null = New-Item -ItemType Directory -Path $tools; $claimed = $true;" ^
|
|
64
|
+
"$step = 'private-acl'; $acl = New-Object Security.AccessControl.DirectorySecurity; $acl.SetOwner($me); $acl.SetAccessRuleProtection($true,$false);" ^
|
|
65
|
+
"foreach ($sid in @($me.Value,'S-1-5-18','S-1-5-32-544')) {" ^
|
|
66
|
+
" $identity = New-Object Security.Principal.SecurityIdentifier($sid);" ^
|
|
67
|
+
" $rule = New-Object Security.AccessControl.FileSystemAccessRule($identity,'FullControl','ContainerInherit,ObjectInherit','None','Allow'); $acl.AddAccessRule($rule);" ^
|
|
68
|
+
"}; [IO.Directory]::SetAccessControl($tools,$acl);" ^
|
|
69
|
+
"$verified = [IO.Directory]::GetAccessControl($tools); if (-not $verified.AreAccessRulesProtected) { throw 'protected-dacl' };" ^
|
|
70
|
+
"if ($verified.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $me.Value) { throw 'private-owner' };" ^
|
|
71
|
+
"foreach ($rule in $verified.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) { if ($rule.AccessControlType -ne 'Allow' -or @($me.Value,'S-1-5-18','S-1-5-32-544') -notcontains $rule.IdentityReference.Value) { throw 'private-ace' } };" ^
|
|
72
|
+
"$step = 'marker'; $null = New-Item -ItemType File -Path (Join-Path $tools '.bootstrap-owned') -Value 'gentle-pi prerequisite tooling only';" ^
|
|
73
|
+
"if ($index -gt 0) { exit 2 };" ^
|
|
74
|
+
"} catch { $reason = 'unexpected-' + $step; if ($_.Exception.Message -cmatch '^(policy|path-mismatch|home-owner|ancestor-reparse|ancestor-owner|acl-mask|collision|protected-dacl|private-owner|private-ace)$') { $reason = $_.Exception.Message };" ^
|
|
75
|
+
"if ($step -cmatch '^(create|private-acl|marker)$') { $tried += ($tools + ': ' + $reason) };" ^
|
|
76
|
+
"if ($claimed) { Remove-Item -LiteralPath $tools -Recurse -Force -ErrorAction SilentlyContinue }; [Console]::Error.WriteLine('Bootstrap: private storage ACL/reparse/ownership claim failed or policy denied it. Reason: ' + $reason); foreach ($line in $tried) { [Console]::Error.WriteLine('Bootstrap: storage candidate ' + $line) }; exit 1 } }"
|
|
77
|
+
if errorlevel 3 goto failed
|
|
78
|
+
if errorlevel 2 goto fallbackclaimed
|
|
79
|
+
if errorlevel 1 goto failed
|
|
80
|
+
goto claimed
|
|
81
|
+
:fallbackclaimed
|
|
82
|
+
set "GENTLE_BOOTSTRAP_TOOLS=%GENTLE_BOOTSTRAP_FALLBACK_TOOLS%"
|
|
83
|
+
:claimed
|
|
84
|
+
set "GENTLE_BOOTSTRAP_FALLBACK_TOOLS="
|
|
85
|
+
set "GENTLE_BOOTSTRAP_OWNED=1"
|
|
86
|
+
|
|
87
|
+
rem Select an existing native Node without invoking any command interpreter shim.
|
|
88
|
+
rem Absence authorizes pinned acquisition, and so does an older stable Node or one
|
|
89
|
+
rem whose storage the probe below cannot trust: it is never replaced or run again.
|
|
90
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
91
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
92
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
93
|
+
"$command = $null; try { $command = Get-Command node.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1 } catch [Management.Automation.CommandNotFoundException] { $command = $null };" ^
|
|
94
|
+
"if ($null -ne $command) { $bytes = (New-Object Text.UTF8Encoding($false)).GetBytes($command.Source); $record = [IO.File]::Open((Join-Path $env:GENTLE_BOOTSTRAP_TOOLS '.node-target'),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None); try { $record.Write($bytes,0,$bytes.Length) } finally { $record.Dispose() } };" ^
|
|
95
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: existing Node resolution failed or policy denied it.'); exit 1 } }"
|
|
96
|
+
if errorlevel 1 goto failed
|
|
97
|
+
|
|
98
|
+
:acquirenode
|
|
99
|
+
rem Fixed official ZIP transport: no redirects, bounded bytes/time, no TLS changes.
|
|
100
|
+
rem Integrity is verified before ZIP processing or executable publication.
|
|
101
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
102
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
103
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
104
|
+
"$tools = $env:GENTLE_BOOTSTRAP_TOOLS; if (Test-Path -LiteralPath (Join-Path $tools '.node-target')) { exit 0 };" ^
|
|
105
|
+
"$architecture = $env:PROCESSOR_ARCHITECTURE; if ($env:PROCESSOR_ARCHITEW6432) { $architecture = $env:PROCESSOR_ARCHITEW6432 };" ^
|
|
106
|
+
"switch ($architecture) { 'AMD64' { $arch = 'x64' } 'ARM64' { $arch = 'arm64' } default { throw 'Unsupported architecture' } };" ^
|
|
107
|
+
"$pins = Get-Content -LiteralPath (Join-Path $env:GENTLE_BOOTSTRAP_BUNDLE 'scripts/installer-windows-artifacts.json') -Raw | ConvertFrom-Json; $pin = $pins.node.$arch;" ^
|
|
108
|
+
"$stem = 'node-v24.21.0-win-' + $arch;" ^
|
|
109
|
+
"if ($pins.node.version -ne '24.21.0' -or $pin.url -ne ('https://nodejs.org/dist/v24.21.0/' + $stem + '.zip') -or $pin.sha256 -notmatch '^[a-f0-9]{64}$' -or $pins.node.maxBytes -ne 104857600) { throw 'Artifact pin rejected' };" ^
|
|
110
|
+
"$request = [Net.HttpWebRequest]::Create($pin.url); $request.AllowAutoRedirect = $false; $request.Timeout = 60000; $request.ReadWriteTimeout = 10000;" ^
|
|
111
|
+
"$watch = [Diagnostics.Stopwatch]::StartNew(); $response = $null; $output = $null; $input = $null;" ^
|
|
112
|
+
"try {" ^
|
|
113
|
+
" $response = $request.GetResponse(); if ([int]$response.StatusCode -ne 200 -or $response.ContentLength -gt 104857600) { throw 'Download rejected' };" ^
|
|
114
|
+
" $input = $response.GetResponseStream(); $output = [IO.File]::Open((Join-Path $tools 'node.zip'),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None);" ^
|
|
115
|
+
" $buffer = New-Object byte[] 65536; $total = 0;" ^
|
|
116
|
+
" while (($count = $input.Read($buffer,0,$buffer.Length)) -gt 0) { $total += $count; if ($total -gt 104857600 -or $watch.ElapsedMilliseconds -gt 60000) { throw 'Download bound exceeded' }; $output.Write($buffer,0,$count) };" ^
|
|
117
|
+
" if ($total -eq 0 -or ($response.ContentLength -ge 0 -and $response.ContentLength -ne $total) -or $watch.ElapsedMilliseconds -gt 60000) { throw 'Download truncated or timed out' };" ^
|
|
118
|
+
"} finally { if ($output) { $output.Dispose() }; if ($input) { $input.Dispose() }; if ($response) { $response.Dispose() } };" ^
|
|
119
|
+
"$actual = (Get-FileHash -LiteralPath (Join-Path $tools 'node.zip') -Algorithm SHA256).Hash; if ($actual -ne $pin.sha256) { throw 'Integrity mismatch' };" ^
|
|
120
|
+
"$null = New-Item -ItemType File -Path (Join-Path $tools '.node-stem') -Value $stem;" ^
|
|
121
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: Node download, bounds, integrity or policy check failed.'); exit 1 } }"
|
|
122
|
+
if errorlevel 1 goto failed
|
|
123
|
+
|
|
124
|
+
rem Inspect the entire ZIP namespace before extracting ONLY its regular node.exe.
|
|
125
|
+
rem Reject links, Windows device/ADS names, traversal and case aliases.
|
|
126
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
127
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
128
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
129
|
+
"$tools = $env:GENTLE_BOOTSTRAP_TOOLS; if (Test-Path -LiteralPath (Join-Path $tools '.node-target')) { exit 0 };" ^
|
|
130
|
+
"Add-Type -AssemblyName System.IO.Compression.FileSystem;" ^
|
|
131
|
+
"$stem = (Get-Content -LiteralPath (Join-Path $tools '.node-stem') -Raw).Trim();" ^
|
|
132
|
+
"$zip = [IO.Compression.ZipFile]::OpenRead((Join-Path $tools 'node.zip'));" ^
|
|
133
|
+
"try {" ^
|
|
134
|
+
" if ($zip.Entries.Count -eq 0 -or $zip.Entries.Count -gt 50000) { throw 'ZIP count rejected' };" ^
|
|
135
|
+
" $explicit = @{}; $names = @{}; $types = @{}; $selected = $null; $expanded = 0;" ^
|
|
136
|
+
" foreach ($entry in $zip.Entries) {" ^
|
|
137
|
+
" $name = $entry.FullName; $directory = $name.EndsWith('/'); if ($directory) { $name = $name.Substring(0,$name.Length - 1) };" ^
|
|
138
|
+
" $unixType = ($entry.ExternalAttributes -shr 16) -band 61440;" ^
|
|
139
|
+
" if (($entry.ExternalAttributes -band 1024) -or ($unixType -ne 0 -and $unixType -ne 32768 -and $unixType -ne 16384) -or ($unixType -eq 16384 -and -not $directory) -or ($directory -and $entry.Length -ne 0)) { throw 'ZIP links/types rejected' };" ^
|
|
140
|
+
" if ($entry.FullName.Contains('\') -or $explicit.ContainsKey($name)) { throw 'ZIP duplicate/path rejected' }; $explicit[$name] = $true;" ^
|
|
141
|
+
" $parts = $name.Split('/'); $path = '';" ^
|
|
142
|
+
" for ($index = 0; $index -lt $parts.Length; $index++) {" ^
|
|
143
|
+
" $part = $parts[$index];" ^
|
|
144
|
+
" if (-not $part -or $part -eq '.' -or $part -eq '..' -or $part -match '[<>:\x22|?*\x00-\x1f\x7f]' -or $part -match '[. ]$' -or $part -match '^(CON|PRN|AUX|NUL|COM[1-9\u00b9\u00b2\u00b3]|LPT[1-9\u00b9\u00b2\u00b3])(\.|$)') { throw 'ZIP Windows name rejected' };" ^
|
|
145
|
+
" if ($path) { $path += '/' }; $path += $part; $isDirectory = $directory -or $index -lt ($parts.Length - 1);" ^
|
|
146
|
+
" if ($names.ContainsKey($path) -and ($names[$path] -cne $path -or $types[$path] -ne $isDirectory)) { throw 'ZIP case/type alias rejected' }; $names[$path] = $path; $types[$path] = $isDirectory;" ^
|
|
147
|
+
" };" ^
|
|
148
|
+
" $expanded += $entry.Length; if ($expanded -gt 536870912 -or ($name -ne $stem -and -not $name.StartsWith($stem + '/',[StringComparison]::Ordinal))) { throw 'ZIP root/size rejected' };" ^
|
|
149
|
+
" if ($name -ceq ($stem + '/node.exe')) { if ($directory -or $entry.Length -eq 0 -or $entry.Length -gt 157286400) { throw 'Node member rejected' }; $selected = $entry };" ^
|
|
150
|
+
" }; if ($null -eq $selected) { throw 'Node member missing' };" ^
|
|
151
|
+
" $destination = Join-Path $tools 'node'; $null = New-Item -ItemType Directory -Path $destination;" ^
|
|
152
|
+
" $target = Join-Path $destination 'node.exe'; $output = [IO.File]::Open($target,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None); $input = $selected.Open();" ^
|
|
153
|
+
" try { $input.CopyTo($output) } finally { $input.Dispose(); $output.Dispose() };" ^
|
|
154
|
+
" $bytes = (New-Object Text.UTF8Encoding($false)).GetBytes($target); $record = [IO.File]::Open((Join-Path $tools '.node-target'),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None); try { $record.Write($bytes,0,$bytes.Length) } finally { $record.Dispose() };" ^
|
|
155
|
+
"} finally { $zip.Dispose() };" ^
|
|
156
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: Node ZIP namespace, extraction or policy check failed.'); exit 1 } }"
|
|
157
|
+
if errorlevel 1 goto failed
|
|
158
|
+
|
|
159
|
+
rem Production direct non-forking Node probe: 10s deadline and combined 1-MiB cap.
|
|
160
|
+
rem Drain both pipes asynchronously; valid output followed by a hang still fails.
|
|
161
|
+
rem Failures append one fixed reason code; walk codes name the component role only.
|
|
162
|
+
rem .node-target holds a possibly non-ASCII path: written and read as explicit UTF-8.
|
|
163
|
+
rem The user's Node (no .node-stem) is left as it is when it is an older stable
|
|
164
|
+
rem version, or when its storage fails the reparse/owner/ACL checks before it ever
|
|
165
|
+
rem runs: its record is removed and the pinned Node is acquired as when absent.
|
|
166
|
+
rem The acquired Node still has to pass every check; unknown versions still refuse.
|
|
167
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
168
|
+
"& { $child = $null; $started = $false; $acquired = $true; $step = 'policy'; try { $ErrorActionPreference = 'Stop';" ^
|
|
169
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'policy' };" ^
|
|
170
|
+
"$step = 'target'; $tools = $env:GENTLE_BOOTSTRAP_TOOLS; $record = Join-Path $tools '.node-target'; if (-not [IO.File]::Exists($record)) { throw 'missing-target' }; $acquired = Test-Path -LiteralPath (Join-Path $tools '.node-stem');" ^
|
|
171
|
+
"$node = [IO.File]::ReadAllText($record,[Text.Encoding]::UTF8).TrimEnd([char]13,[char]10); if (-not $node) { throw 'missing-target' };" ^
|
|
172
|
+
"$item = Get-Item -LiteralPath $node -Force; if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'unsafe-target' };" ^
|
|
173
|
+
"if (-not [IO.Path]::IsPathRooted($node) -or $node.StartsWith('\\')) { throw 'unsafe-path' };" ^
|
|
174
|
+
"$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value; $trusted = @($me,'S-1-5-18','S-1-5-32-544','S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'); $path = $node;" ^
|
|
175
|
+
"$depth = 0;" ^
|
|
176
|
+
"$step = 'acl-walk'; while ($path) {" ^
|
|
177
|
+
" $role = 'ancestor'; if ($depth -eq 0) { $role = 'target' } elseif ($depth -eq 1) { $role = 'parent' };" ^
|
|
178
|
+
" $item = Get-Item -LiteralPath $path -Force; if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw ($role + '-reparse') };" ^
|
|
179
|
+
" if ($item.PSIsContainer) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };" ^
|
|
180
|
+
" if ($trusted -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { throw ($role + '-owner') };" ^
|
|
181
|
+
" $allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };" ^
|
|
182
|
+
" foreach ($rule in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) { if ($rule.AccessControlType -eq 'Allow' -and -not ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -and ([long]$rule.FileSystemRights -band (-bnot [long]$allowedRights)) -and $trusted -notcontains $rule.IdentityReference.Value) { throw ($role + '-acl-mask') } };" ^
|
|
183
|
+
" $parent = [IO.Directory]::GetParent($path); if ($null -eq $parent) { break }; $path = $parent.FullName; $depth++;" ^
|
|
184
|
+
"};" ^
|
|
185
|
+
"$start = New-Object Diagnostics.ProcessStartInfo; $start.FileName = $node; $start.Arguments = '--version'; $start.UseShellExecute = $false; $start.CreateNoWindow = $true; $start.RedirectStandardOutput = $true; $start.RedirectStandardError = $true;" ^
|
|
186
|
+
"$step = 'start'; $child = New-Object Diagnostics.Process; $child.StartInfo = $start; if (-not $child.Start()) { throw 'no-start' }; $started = $true;" ^
|
|
187
|
+
"$step = 'drain'; $streams = @($child.StandardOutput.BaseStream,$child.StandardError.BaseStream); $buffers = @((New-Object byte[] 4096),(New-Object byte[] 4096));" ^
|
|
188
|
+
"$tasks = @($streams[0].ReadAsync($buffers[0],0,4096),$streams[1].ReadAsync($buffers[1],0,4096)); $closed = @($false,$false); $text = ''; $total = 0; $watch = [Diagnostics.Stopwatch]::StartNew();" ^
|
|
189
|
+
"while (-not ($closed[0] -and $closed[1] -and $child.HasExited)) {" ^
|
|
190
|
+
" if ($watch.ElapsedMilliseconds -ge 10000) { throw 'deadline' };" ^
|
|
191
|
+
" for ($index = 0; $index -lt 2; $index++) { if (-not $closed[$index] -and $tasks[$index].IsCompleted) {" ^
|
|
192
|
+
" $count = $tasks[$index].GetAwaiter().GetResult(); if ($count -eq 0) { $closed[$index] = $true } else {" ^
|
|
193
|
+
" $total += $count; if ($total -gt 1048576) { throw 'output-limit' };" ^
|
|
194
|
+
" if ($index -eq 0) { $text += [Text.Encoding]::UTF8.GetString($buffers[$index],0,$count) };" ^
|
|
195
|
+
" $tasks[$index] = $streams[$index].ReadAsync($buffers[$index],0,4096);" ^
|
|
196
|
+
" }" ^
|
|
197
|
+
" } }; Start-Sleep -Milliseconds 10;" ^
|
|
198
|
+
"}; if ($child.ExitCode -ne 0) { throw 'exit-code' };" ^
|
|
199
|
+
"$step = 'version'; $version = $text.Trim(); if ($version -notmatch '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { throw 'version-format' };" ^
|
|
200
|
+
"$actual = [Version]$version.Substring(1); $metadata = Get-Content -LiteralPath (Join-Path $env:GENTLE_BOOTSTRAP_BUNDLE 'package.json') -Raw | ConvertFrom-Json;" ^
|
|
201
|
+
"if ($acquired -and $version -ne 'v24.21.0') { throw 'acquired-version' };" ^
|
|
202
|
+
"if ($actual -lt [Version]'24.3.0' -or $actual -lt [Version]$metadata.engines.node.Substring(2)) { if ($acquired) { throw 'engine' }; [IO.File]::Delete((Join-Path $tools '.node-target')) };" ^
|
|
203
|
+
"} catch { $reason = 'unexpected-' + $step; if ($_.Exception.Message -cmatch '^(policy|missing-target|unsafe-target|unsafe-path|(target|parent|ancestor)-(reparse|owner|acl-mask)|no-start|deadline|output-limit|exit-code|version-format|engine|acquired-version)$') { $reason = $_.Exception.Message };" ^
|
|
204
|
+
"if (-not $acquired -and $reason -cmatch '^(unsafe-target|unsafe-path|(target|parent|ancestor)-(reparse|owner|acl-mask))$') { [IO.File]::Delete((Join-Path $env:GENTLE_BOOTSTRAP_TOOLS '.node-target')) }" ^
|
|
205
|
+
"else { [Console]::Error.WriteLine('Bootstrap: Node version, execution, deadline, output bound or policy check failed; refusing replacement. Reason: ' + $reason); exit 1 } }" ^
|
|
206
|
+
"finally { try { if ($child) { if ($started -and -not $child.HasExited) { $child.Kill(); if (-not $child.WaitForExit(1000)) { throw 'Child termination unconfirmed' } }; $child.Dispose() } } catch { [Console]::Error.WriteLine('Bootstrap: direct-child termination could not be confirmed.'); exit 1 } } }"
|
|
207
|
+
if errorlevel 1 goto failed
|
|
208
|
+
if not exist "%GENTLE_BOOTSTRAP_TOOLS%\.node-target" goto acquirenode
|
|
209
|
+
|
|
210
|
+
rem Launch the existing Node helper with data arguments; no shell evaluation.
|
|
211
|
+
rem Interactive wizard duration is intentionally unbounded. Only child PATH changes.
|
|
212
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
213
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
214
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
215
|
+
"$tools = $env:GENTLE_BOOTSTRAP_TOOLS; $record = Join-Path $tools '.node-target'; $node = [IO.File]::ReadAllText($record,[Text.Encoding]::UTF8).TrimEnd([char]13,[char]10);" ^
|
|
216
|
+
"$env:PATH = [IO.Path]::GetDirectoryName($node) + ';' + $env:PATH;" ^
|
|
217
|
+
"$helper = Join-Path $env:GENTLE_BOOTSTRAP_BUNDLE 'scripts/installer-downloads.mjs';" ^
|
|
218
|
+
"if (Test-Path -LiteralPath (Join-Path $tools 'node.zip')) { Remove-Item -LiteralPath (Join-Path $tools 'node.zip') };" ^
|
|
219
|
+
"& $node $helper '--bootstrap-windows' $env:GENTLE_BOOTSTRAP_BUNDLE $tools; if ($LASTEXITCODE -ne 0) { throw 'Helper failed' };" ^
|
|
220
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: prerequisite helper/wizard failed or managed policy denied execution.'); exit 1 } }"
|
|
221
|
+
if errorlevel 1 goto failed
|
|
222
|
+
|
|
223
|
+
rem Success: node/npm/pnpm now persist under PNPM_HOME or were already the user's.
|
|
224
|
+
rem Remove only our exact claimed, marked root; Directory.Delete does not recurse
|
|
225
|
+
rem through reparse points. A removal problem never fails the installation.
|
|
226
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
227
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
228
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
229
|
+
"$tools = [IO.Path]::GetFullPath($env:GENTLE_BOOTSTRAP_TOOLS); $parent = [IO.Directory]::GetParent($tools).FullName;" ^
|
|
230
|
+
"if ($tools.StartsWith('\\') -or ($parent -ne [IO.Path]::GetFullPath($env:LOCALAPPDATA) -and $parent -ne [IO.Path]::GetFullPath($env:USERPROFILE)) -or -not [IO.Path]::GetFileName($tools).StartsWith('.gentle-shell-bootstrap-tools.',[StringComparison]::Ordinal)) { throw 'Cleanup target rejected' };" ^
|
|
231
|
+
"$item = Get-Item -LiteralPath $tools -Force;" ^
|
|
232
|
+
"if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Cleanup target rejected' };" ^
|
|
233
|
+
"$marker = Get-Item -LiteralPath (Join-Path $tools '.bootstrap-owned') -Force;" ^
|
|
234
|
+
"if ($marker.PSIsContainer -or ($marker.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Get-Content -LiteralPath $marker.FullName -Raw) -ne 'gentle-pi prerequisite tooling only') { throw 'Cleanup marker rejected' };" ^
|
|
235
|
+
"[IO.Directory]::Delete($tools, $true);" ^
|
|
236
|
+
"} catch { $message = 'Bootstrap: installation finished, but temporary tools could not be removed: ' + $env:GENTLE_BOOTSTRAP_TOOLS;" ^
|
|
237
|
+
"if ([Console]::IsErrorRedirected) { $bytes = [Text.Encoding]::UTF8.GetBytes($message + [Environment]::NewLine); $stderr = [Console]::OpenStandardError(); $stderr.Write($bytes, 0, $bytes.Length); $stderr.Flush() } else { [Console]::Error.WriteLine($message) }; exit 1 } }"
|
|
238
|
+
endlocal & exit /b 0
|
|
239
|
+
|
|
240
|
+
:unavailable
|
|
241
|
+
>&2 echo Bootstrap: stock Windows PowerShell is unavailable. No acquisition attempted.
|
|
242
|
+
:failed
|
|
243
|
+
rem Never scan/reuse/clean other attempts. Only our successfully claimed root.
|
|
244
|
+
if not defined GENTLE_BOOTSTRAP_OWNED goto finishfailure
|
|
245
|
+
"%GENTLE_BOOTSTRAP_PS%" -NoLogo -NoProfile -NonInteractive -Command ^
|
|
246
|
+
"& { try { $ErrorActionPreference = 'Stop';" ^
|
|
247
|
+
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { throw 'Managed policy constraint' };" ^
|
|
248
|
+
"$tools = $env:GENTLE_BOOTSTRAP_TOOLS; $item = Get-Item -LiteralPath $tools -Force;" ^
|
|
249
|
+
"if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Cleanup target rejected' };" ^
|
|
250
|
+
"$marker = Get-Item -LiteralPath (Join-Path $tools '.bootstrap-owned') -Force;" ^
|
|
251
|
+
"if ($marker.PSIsContainer -or ($marker.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Get-Content -LiteralPath $marker.FullName -Raw) -ne 'gentle-pi prerequisite tooling only') { throw 'Cleanup marker rejected' };" ^
|
|
252
|
+
"Remove-Item -LiteralPath $tools -Recurse -Force;" ^
|
|
253
|
+
"} catch { [Console]::Error.WriteLine('Bootstrap: attempt-owned cleanup unavailable; no other storage touched.'); exit 1 } }"
|
|
254
|
+
:finishfailure
|
|
255
|
+
endlocal & exit /b 1
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Run from an extracted/checkout bundle, not from a remote pipe.
|
|
3
|
+
# Only prerequisite tooling is acquired here; standard installation belongs to T4.
|
|
4
|
+
set -eu
|
|
5
|
+
umask 077
|
|
6
|
+
fail() { printf '%s\n' "Bootstrap: $1" >&2; exit 1; }
|
|
7
|
+
need() { command -v "$1" >/dev/null 2>&1 || fail "Required utility missing: $1"; }
|
|
8
|
+
for utility in dirname awk; do need "$utility"; done
|
|
9
|
+
bundle=$(CDPATH= cd -P "$(dirname "$0")/.." && pwd) || fail 'Cannot locate installation bundle'
|
|
10
|
+
entry=$bundle/bin/gentle-shell-install.mjs
|
|
11
|
+
[ -f "$entry" ] && [ ! -L "$entry" ] || fail 'Future wizard entry is missing; this bundle cannot start a live wizard'
|
|
12
|
+
[ -f "$bundle/scripts/installer-downloads.mjs" ] || fail 'Download helper missing from bundle'
|
|
13
|
+
for utility in uname mkdir mktemp chmod mv rm sleep wc id ls; do need "$utility"; done
|
|
14
|
+
case ${HOME:-} in /*) ;; *) fail 'HOME must be an absolute per-user directory';; esac
|
|
15
|
+
# Refuse symlink ancestors and group/world-writable acquisition parents.
|
|
16
|
+
check_home=$HOME
|
|
17
|
+
while [ "$check_home" != / ]; do
|
|
18
|
+
[ ! -L "$check_home" ] || fail 'Unsafe symlink in HOME path'
|
|
19
|
+
check_home=$(dirname "$check_home")
|
|
20
|
+
done
|
|
21
|
+
[ -d "$HOME" ] || fail 'HOME directory is missing'
|
|
22
|
+
uid=$(id -u)
|
|
23
|
+
ls -nd "$HOME" | awk -v uid="$uid" '$3 == uid && substr($1,6,1) != "w" && substr($1,9,1) != "w" {ok=1} END {exit !ok}' || fail 'Unsafe HOME ownership or permissions'
|
|
24
|
+
minimum=$(awk -F '"' '{for (i=2; i<=NF-2; i+=2) if ($i == "node" && $(i+2) ~ /^>=[0-9]+\.[0-9]+\.[0-9]+$/) {value=$(i+2); sub(/^>=/, "", value); count++}} END {if (count == 1) print value}' "$bundle/package.json")
|
|
25
|
+
[ -n "$minimum" ] || fail 'Cannot establish repository Node minimum'
|
|
26
|
+
version_ok() {
|
|
27
|
+
awk -v value="$1" -v minimum="$2" 'BEGIN {
|
|
28
|
+
sub(/^v/, "", value)
|
|
29
|
+
if (value !~ /^[0-9]+\.[0-9]+\.[0-9]+$/) exit 1
|
|
30
|
+
split(value, a, "."); split(minimum, b, ".")
|
|
31
|
+
for (i=1; i<=3; i++) if (length(a[i]) > 9 || (length(a[i]) > 1 && substr(a[i],1,1) == "0")) exit 1
|
|
32
|
+
for (i=1; i<=3; i++) {
|
|
33
|
+
if (a[i]+0 > b[i]+0) exit 0
|
|
34
|
+
if (a[i]+0 < b[i]+0) exit 1
|
|
35
|
+
}
|
|
36
|
+
}'
|
|
37
|
+
}
|
|
38
|
+
# Any stable version at all: version_ok against 0.0.0 accepts exactly those.
|
|
39
|
+
stable_version() { version_ok "$1" 0.0.0; }
|
|
40
|
+
# A stock POSIX watchdog avoids assuming GNU timeout on macOS.
|
|
41
|
+
# These fixed prerequisite probes get an unignorable deadline, not graceful TERM.
|
|
42
|
+
# Signal only our direct child PID; do not kill inherited process groups.
|
|
43
|
+
bounded() {
|
|
44
|
+
seconds=$1
|
|
45
|
+
shift
|
|
46
|
+
"$@" &
|
|
47
|
+
version_pid=$!
|
|
48
|
+
(
|
|
49
|
+
sleep "$seconds" &
|
|
50
|
+
sleeper=$!
|
|
51
|
+
trap 'kill -s KILL "$sleeper" 2>/dev/null || :; wait "$sleeper" 2>/dev/null || :; exit 0' TERM
|
|
52
|
+
wait "$sleeper" || :
|
|
53
|
+
kill -s KILL "$version_pid" 2>/dev/null || :
|
|
54
|
+
) >/dev/null 2>&1 &
|
|
55
|
+
watchdog=$!
|
|
56
|
+
result=0
|
|
57
|
+
wait "$version_pid" || result=$?
|
|
58
|
+
kill "$watchdog" 2>/dev/null || :
|
|
59
|
+
wait "$watchdog" 2>/dev/null || :
|
|
60
|
+
return "$result"
|
|
61
|
+
}
|
|
62
|
+
tools=
|
|
63
|
+
owned=0
|
|
64
|
+
# Only the exact directory this attempt claimed; rm -rf never follows links in it.
|
|
65
|
+
owned_tools() {
|
|
66
|
+
[ "$owned" = 1 ] && [ -n "$tools" ] || return 1
|
|
67
|
+
case "$tools" in "$HOME"/.gentle-shell-bootstrap-tools.*/*) return 1;; "$HOME"/.gentle-shell-bootstrap-tools.*) ;; *) return 1;; esac
|
|
68
|
+
[ -d "$tools" ] && [ ! -L "$tools" ] && [ -d "$tools/.claim" ] && [ ! -L "$tools/.claim" ]
|
|
69
|
+
}
|
|
70
|
+
cleanup() {
|
|
71
|
+
if owned_tools; then
|
|
72
|
+
rm -rf "$tools"
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
75
|
+
trap cleanup EXIT
|
|
76
|
+
trap 'exit 1' HUP INT TERM
|
|
77
|
+
make_tools() {
|
|
78
|
+
tools=$(mktemp -d "$HOME/.gentle-shell-bootstrap-tools.XXXXXXXX") || fail 'Cannot create private tooling directory'
|
|
79
|
+
case "$tools" in "$HOME"/.gentle-shell-bootstrap-tools.*) ;; *) fail 'Conflicting tooling destination';; esac
|
|
80
|
+
[ -d "$tools" ] && [ ! -L "$tools" ] || fail 'Unsafe tooling destination'
|
|
81
|
+
ls -nd "$tools" | awk -v uid="$uid" '$3 == uid && $1 == "drwx------" {ok=1} END {exit !ok}' || fail 'Unsafe tooling ownership'
|
|
82
|
+
# No existing destination is ever reused or replaced.
|
|
83
|
+
mkdir "$tools/.claim" || fail 'Conflicting tooling destination'
|
|
84
|
+
owned=1
|
|
85
|
+
printf '%s\n' 'gentle-pi prerequisite tooling only' > "$tools/.bootstrap-owned" || fail 'Cannot mark tooling ownership'
|
|
86
|
+
}
|
|
87
|
+
acquire_node=1
|
|
88
|
+
if command -v node >/dev/null 2>&1; then
|
|
89
|
+
node=$(command -v node)
|
|
90
|
+
version=$(bounded 10 "$node" --version 2>/dev/null) || fail 'Existing Node version probe failed'
|
|
91
|
+
if version_ok "$version" "$minimum"; then acquire_node=0
|
|
92
|
+
# An older stable Node is left as it is: the verified Node below runs the
|
|
93
|
+
# installer instead, exactly as when Node is absent.
|
|
94
|
+
elif ! stable_version "$version"; then fail 'Existing Node is incompatible or unknown; refusing replacement'; fi
|
|
95
|
+
fi
|
|
96
|
+
if [ "$acquire_node" = 1 ]; then
|
|
97
|
+
os=$(uname -s)
|
|
98
|
+
arch=$(uname -m)
|
|
99
|
+
case "$os" in Darwin) os=darwin;; Linux) os=linux;; *) fail 'Unsupported POSIX platform';; esac
|
|
100
|
+
case "$arch" in x86_64|amd64) arch=x64;; aarch64|arm64) arch=arm64;; *) fail 'Unsupported architecture';; esac
|
|
101
|
+
if [ "$os" = linux ]; then
|
|
102
|
+
need getconf
|
|
103
|
+
libc=$(getconf GNU_LIBC_VERSION 2>/dev/null) || fail 'Cannot establish native glibc compatibility; musl is not supported'
|
|
104
|
+
case "$libc" in 'glibc '*) ;; *) fail 'Unsupported libc; only native glibc acquisition is planned';; esac
|
|
105
|
+
version_ok "${libc#glibc }.0" '2.28.0' || fail 'Native Node requires glibc >=2.28; musl is not supported'
|
|
106
|
+
fi
|
|
107
|
+
# Primary source: https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt
|
|
108
|
+
# Exact release URLs are fixed, even though the provenance page is moving.
|
|
109
|
+
case "$os-$arch" in
|
|
110
|
+
darwin-arm64) hash=bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057;;
|
|
111
|
+
darwin-x64) hash=1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097;;
|
|
112
|
+
linux-arm64) hash=724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5;;
|
|
113
|
+
linux-x64) hash=6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff;;
|
|
114
|
+
esac
|
|
115
|
+
need curl
|
|
116
|
+
need tar
|
|
117
|
+
if command -v sha256sum >/dev/null 2>&1; then hasher=sha256sum
|
|
118
|
+
elif command -v shasum >/dev/null 2>&1; then hasher=shasum
|
|
119
|
+
else fail 'Required SHA256 utility missing: sha256sum or shasum'; fi
|
|
120
|
+
make_tools
|
|
121
|
+
stem=node-v24.21.0-$os-$arch
|
|
122
|
+
archive=$tools/node.tar.gz
|
|
123
|
+
# Also bound disk use on older curl versions lacking streamed-size enforcement.
|
|
124
|
+
# POSIX uses 512-byte blocks; shells using KiB still impose a 200-MiB ceiling.
|
|
125
|
+
(
|
|
126
|
+
ulimit -f 204800 || exit 1
|
|
127
|
+
curl --fail --silent --show-error --proto '=https' --tlsv1.2 --connect-timeout 10 --max-time 120 --max-filesize 104857600 --output "$archive" "https://nodejs.org/dist/v24.21.0/$stem.tar.gz"
|
|
128
|
+
) 2>/dev/null || fail 'Node download failed, was truncated, or its file-size bound is unavailable'
|
|
129
|
+
size=$(wc -c < "$archive")
|
|
130
|
+
[ "$size" -gt 0 ] && [ "$size" -le 104857600 ] || fail 'Node download size is invalid'
|
|
131
|
+
if [ "$hasher" = sha256sum ]; then actual=$(bounded 30 sha256sum "$archive" 2>/dev/null) || fail 'SHA256 process failed'
|
|
132
|
+
else actual=$(bounded 30 shasum -a 256 "$archive" 2>/dev/null) || fail 'SHA256 process failed'; fi
|
|
133
|
+
actual=${actual%% *}
|
|
134
|
+
# Hash tools prefix escaped filenames (e.g. newline/backslash) with a backslash.
|
|
135
|
+
actual=${actual#\\}
|
|
136
|
+
[ "$actual" = "$hash" ] || fail 'Node checksum integrity mismatch'
|
|
137
|
+
mkdir "$tools/staging" || fail 'Conflicting staging destination'
|
|
138
|
+
# Extract only the trusted regular executable, not npm/Corepack or archive links.
|
|
139
|
+
member=$stem/bin/node
|
|
140
|
+
listing=$(bounded 30 tar -tvzf "$archive" "$member" 2>/dev/null) || fail 'Node archive is invalid'
|
|
141
|
+
case "$listing" in -*) ;; *) fail 'Node archive executable is not a regular file';; esac
|
|
142
|
+
bounded 30 tar -xzf "$archive" -C "$tools/staging" "$member" 2>/dev/null || fail 'Node extraction failed'
|
|
143
|
+
candidate=$tools/staging/$member
|
|
144
|
+
[ -f "$candidate" ] && [ ! -L "$candidate" ] || fail 'Node executable missing or unsafe'
|
|
145
|
+
version=$(bounded 10 "$candidate" --version 2>/dev/null) || fail 'Acquired Node cannot execute on this native platform'
|
|
146
|
+
[ "$version" = v24.21.0 ] && version_ok "$version" "$minimum" || fail 'Acquired Node version rejected'
|
|
147
|
+
bounded 10 chmod 700 "$candidate" 2>/dev/null || fail 'Cannot secure acquired Node executable'
|
|
148
|
+
[ ! -e "$tools/node" ] && [ ! -L "$tools/node" ] || fail 'Conflicting Node destination'
|
|
149
|
+
mv "$tools/staging/$stem" "$tools/node" || fail 'Cannot publish verified Node'
|
|
150
|
+
rm -f "$archive" || fail 'Cannot clean owned Node archive'
|
|
151
|
+
rm -rf "$tools/staging" || fail 'Cannot clean owned Node staging'
|
|
152
|
+
node=$tools/node/bin/node
|
|
153
|
+
PATH=$tools/node/bin:$PATH
|
|
154
|
+
export PATH
|
|
155
|
+
fi
|
|
156
|
+
# The helper creates an equally private tooling directory only if pnpm is absent.
|
|
157
|
+
# Do not exec: retain ownership cleanup on any helper/child failure.
|
|
158
|
+
"$node" "$bundle/scripts/installer-downloads.mjs" --bootstrap "$bundle" "$tools" || fail 'Prerequisite acquisition or wizard child failed'
|
|
159
|
+
# Exit 0 means node/npm/pnpm now persist under $PNPM_HOME or were already the
|
|
160
|
+
# user's. A removal problem never turns the completed installation into failure.
|
|
161
|
+
if [ "$owned" = 1 ]; then
|
|
162
|
+
mark=
|
|
163
|
+
if owned_tools && [ -f "$tools/.bootstrap-owned" ] && [ ! -L "$tools/.bootstrap-owned" ] &&
|
|
164
|
+
{ IFS= read -r mark < "$tools/.bootstrap-owned"; } 2>/dev/null && [ "$mark" = 'gentle-pi prerequisite tooling only' ] &&
|
|
165
|
+
rm -rf "$tools" 2>/dev/null; then :
|
|
166
|
+
else printf '%s\n' "Bootstrap: installation finished, but temporary tools could not be removed: $tools" >&2; fi
|
|
167
|
+
fi
|
|
168
|
+
owned=0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Release distribution assets (S3, T1b): gentle-pi at the release version and
|
|
3
|
+
// the exact Pi (PI_INSTALL_VERSION), with the pnpm-lock.yaml our pinned pnpm
|
|
4
|
+
// resolves for them, published as two release assets that installVersion
|
|
5
|
+
// installs with --frozen-lockfile. pnpm records every platform's optional
|
|
6
|
+
// packages (ffi-rs, fff-bin, esbuild prebuilds) in the lockfile whatever system
|
|
7
|
+
// resolves it, so one lockfile serves Linux, macOS and Windows.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/build-distribution.mjs build --shell <version> [--pi <version>] --out <dir>
|
|
10
|
+
// node scripts/build-distribution.mjs verify --assets <dir>
|
|
11
|
+
//
|
|
12
|
+
// Both commands work in a fresh temporary prefix with our own pinned Node and
|
|
13
|
+
// pnpm (and Go on Windows); the user's tools and home are never used.
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
import { parseArgs } from "node:util";
|
|
20
|
+
import { DISTRIBUTION_ASSETS, activateVersion, claimPrefix, distributionAsset, distributionFiles, ensureLauncher, ensureRuntime, installVersion,
|
|
21
|
+
pnpmEnvironment, readDistribution, runCommand } from "./bundled-install.mjs";
|
|
22
|
+
import { PI_INSTALL_VERSION } from "./installer-preflight.mjs";
|
|
23
|
+
|
|
24
|
+
const RESOLVE_TIMEOUT = 10 * 60 * 1000;
|
|
25
|
+
// Right after `npm publish` the registry may not list gentle-pi@<version> yet:
|
|
26
|
+
// 10 attempts 30 seconds apart wait about five minutes for it.
|
|
27
|
+
const RESOLVE_ATTEMPTS = 10;
|
|
28
|
+
const RESOLVE_DELAY = 30 * 1000;
|
|
29
|
+
const output = (result) => `${result.stdout ?? ""}${result.stderr ?? ""}`.slice(-4000);
|
|
30
|
+
|
|
31
|
+
/** Resolves the lockfile for gentle-pi `shell` and Pi `pi` with our pnpm
|
|
32
|
+
* (`install --lockfile-only`) in a fresh folder under the prefix, from the
|
|
33
|
+
* unmodified distributionFiles() output and the same environment installVersion
|
|
34
|
+
* uses. Only "no matching version" for this gentle-pi is retried (pnpm prints it
|
|
35
|
+
* on stdout); any other failure stops at once. Returns { manifest, assets }
|
|
36
|
+
* keyed by the release asset names.
|
|
37
|
+
*/
|
|
38
|
+
export async function buildDistribution({ layout, shell, pi = PI_INSTALL_VERSION, env = {}, adapters = {} }) {
|
|
39
|
+
const manifest = { shell, pi };
|
|
40
|
+
const files = distributionFiles(manifest);
|
|
41
|
+
const sleep = adapters.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
42
|
+
// `shell` is an exact SemVer here, so only its dots need escaping.
|
|
43
|
+
const unpublished = new RegExp(`ERR_PNPM_NO_(?:MATURE_)?MATCHING_VERSION\\] No matching version found for gentle-pi@${shell.replaceAll(".", "\\.")}(?![\\w.+-])`);
|
|
44
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
45
|
+
const folder = mkdtempSync(join(layout.tmp, "lockfile-"));
|
|
46
|
+
try {
|
|
47
|
+
for (const [name, text] of Object.entries(files)) writeFileSync(join(folder, name), text, { flag: "wx", mode: 0o600 });
|
|
48
|
+
const result = await (adapters.run ?? runCommand)(layout.node, [layout.pnpm, "install", "--lockfile-only"],
|
|
49
|
+
{ cwd: folder, env: pnpmEnvironment(layout, { env }), timeout: RESOLVE_TIMEOUT });
|
|
50
|
+
if (result.status === 0) {
|
|
51
|
+
// Only the lockfile is read back: pnpm may rewrite pnpm-workspace.yaml with placeholders.
|
|
52
|
+
const lockfile = readFileSync(join(folder, "pnpm-lock.yaml"), "utf8");
|
|
53
|
+
return { manifest, assets: { [DISTRIBUTION_ASSETS.distribution]: distributionAsset(manifest, lockfile), [DISTRIBUTION_ASSETS.lockfile]: lockfile } };
|
|
54
|
+
}
|
|
55
|
+
if (!unpublished.test(output(result))) throw Object.assign(new Error("pnpm lockfile resolution failed"), { output: output(result) });
|
|
56
|
+
if (attempt === RESOLVE_ATTEMPTS) {
|
|
57
|
+
throw Object.assign(new Error(`gentle-pi@${shell} is not on the registry after ${RESOLVE_ATTEMPTS} attempts; check its npm publication`), { output: output(result) });
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(folder, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
await sleep(RESOLVE_DELAY);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Runs the launcher with `--version`; the .cmd one through cmd.exe. */
|
|
67
|
+
function launch(launcher, { env, platform }) {
|
|
68
|
+
const options = { env, encoding: "utf8", timeout: 120000, windowsHide: true };
|
|
69
|
+
if (platform !== "win32") return spawnSync(launcher, ["--version"], options);
|
|
70
|
+
const cmd = join(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows", "System32", "cmd.exe");
|
|
71
|
+
return spawnSync(cmd, ["/d", "/s", "/c", `""${launcher}" --version"`], { ...options, windowsVerbatimArguments: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Proves a release asset pair: our runtimes (Go too on Windows, as
|
|
75
|
+
* installVersion requires), a frozen install of the pair, `current` switched to
|
|
76
|
+
* it, the launcher written, and `gentle-shell --version` reporting exactly the
|
|
77
|
+
* pair's gentle-pi and Pi. Returns { id, version }.
|
|
78
|
+
*/
|
|
79
|
+
export async function verifyDistribution({ layout, distribution, lockfile, platform, arch, env = {}, adapters = {} }) {
|
|
80
|
+
const { manifest } = readDistribution(distribution, lockfile);
|
|
81
|
+
const runtime = await ensureRuntime({ layout, platform, arch, go: platform === "win32", env, adapters });
|
|
82
|
+
const { id } = await installVersion({ layout, manifest, lockfile, platform, env, go: runtime.go, adapters });
|
|
83
|
+
activateVersion(layout, id);
|
|
84
|
+
ensureLauncher(layout);
|
|
85
|
+
// The environment our pnpm ran in (no NODE_OPTIONS, NODE_PATH or npm/pnpm
|
|
86
|
+
// settings from the host), without any Pi or Gentle override that could point
|
|
87
|
+
// the launcher at another Pi or agent home.
|
|
88
|
+
const launchEnv = Object.fromEntries(Object.entries(pnpmEnvironment(layout, { platform, env })).filter(([key]) => !/^(?:gentle_|pi_)/i.test(key)));
|
|
89
|
+
const result = await (adapters.launch ?? launch)(layout.launcher, { env: launchEnv, platform });
|
|
90
|
+
const version = String(result.stdout ?? "").trim();
|
|
91
|
+
const [shellLine, piLine] = version.split(/\r?\n/);
|
|
92
|
+
if (result.status !== 0 || shellLine !== `gentle-shell ${manifest.shell}` || piLine !== `pi ${manifest.pi}`) {
|
|
93
|
+
throw Object.assign(new Error(`The launcher reported ${JSON.stringify(version)}, not gentle-shell ${manifest.shell} with pi ${manifest.pi}`),
|
|
94
|
+
{ output: output(result) });
|
|
95
|
+
}
|
|
96
|
+
return { id, version };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A fresh prefix in a new temporary folder. On Windows the private-folder claim
|
|
100
|
+
* (owner, ACL, marker) has its own native tests; here the new folder stands in
|
|
101
|
+
* for it, as in those tests.
|
|
102
|
+
*/
|
|
103
|
+
function temporaryPrefix(root, platform, base) {
|
|
104
|
+
if (platform !== "win32") {
|
|
105
|
+
const home = join(root, "home");
|
|
106
|
+
mkdirSync(home, { mode: 0o700 });
|
|
107
|
+
const env = { ...base, HOME: home };
|
|
108
|
+
return { env, layout: claimPrefix({ platform, env, home }) };
|
|
109
|
+
}
|
|
110
|
+
const [local, roaming, profile] = ["local", "roaming", "profile"].map((name) => join(root, name));
|
|
111
|
+
for (const folder of [local, roaming, profile]) mkdirSync(folder);
|
|
112
|
+
const kept = Object.entries(base).filter(([key]) => !/^(?:localappdata|appdata|userprofile|home)$/i.test(key));
|
|
113
|
+
const env = { ...Object.fromEntries(kept), LOCALAPPDATA: local, APPDATA: roaming, USERPROFILE: profile, HOME: profile };
|
|
114
|
+
const claim = (folder) => { mkdirSync(folder); return "claimed"; };
|
|
115
|
+
return { env, layout: claimPrefix({ platform, env, home: profile, adapters: { storage: () => {}, claim } }) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The CLI. Its temporary prefix (`gsd-*`) is always removed, whether the
|
|
119
|
+
* command succeeded or failed; invalid input is refused before it exists.
|
|
120
|
+
*/
|
|
121
|
+
export async function main(argv, { platform = process.platform, arch = process.arch, env = process.env, temporaryRoot = tmpdir(), adapters = {} } = {}) {
|
|
122
|
+
const { positionals: [command], values } = parseArgs({ args: argv, allowPositionals: true,
|
|
123
|
+
options: { shell: { type: "string" }, pi: { type: "string" }, out: { type: "string" }, assets: { type: "string" } } });
|
|
124
|
+
const build = command === "build" && values.shell && values.out;
|
|
125
|
+
if (!build && !(command === "verify" && values.assets)) {
|
|
126
|
+
throw new Error("Usage: build-distribution.mjs build --shell <version> [--pi <version>] --out <dir> | verify --assets <dir>");
|
|
127
|
+
}
|
|
128
|
+
// A non-exact version is refused before anything is downloaded.
|
|
129
|
+
if (build) distributionFiles({ shell: values.shell, pi: values.pi ?? PI_INSTALL_VERSION });
|
|
130
|
+
const root = realpathSync(mkdtempSync(join(temporaryRoot, "gsd-")));
|
|
131
|
+
try {
|
|
132
|
+
const prefix = temporaryPrefix(root, platform, env);
|
|
133
|
+
if (build) {
|
|
134
|
+
await ensureRuntime({ layout: prefix.layout, platform, arch, env: prefix.env, adapters });
|
|
135
|
+
const { manifest, assets } = await buildDistribution({ layout: prefix.layout, shell: values.shell, pi: values.pi, env: prefix.env, adapters });
|
|
136
|
+
mkdirSync(values.out, { recursive: true });
|
|
137
|
+
for (const [name, text] of Object.entries(assets)) writeFileSync(join(values.out, name), text, { flag: "wx" });
|
|
138
|
+
console.log(`Built ${Object.keys(assets).join(" and ")} for gentle-pi ${manifest.shell} with pi ${manifest.pi} in ${values.out}`);
|
|
139
|
+
} else {
|
|
140
|
+
const read = (name) => readFileSync(join(values.assets, name), "utf8");
|
|
141
|
+
const result = await verifyDistribution({ layout: prefix.layout, distribution: read(DISTRIBUTION_ASSETS.distribution), lockfile: read(DISTRIBUTION_ASSETS.lockfile),
|
|
142
|
+
platform, arch, env: prefix.env, adapters });
|
|
143
|
+
console.log(`Verified ${result.id}:\n${result.version}`);
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
// Windows may hold a file a moment after its process exits.
|
|
147
|
+
rmSync(root, { recursive: true, force: true, maxRetries: 5 });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
152
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
153
|
+
const detail = error.output ?? error.stderr;
|
|
154
|
+
console.error(detail ? `${error.message}\n${detail}` : error.message);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
});
|
|
157
|
+
}
|