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
package/bin/gentle-shell.mjs
CHANGED
|
@@ -10,11 +10,14 @@ import {
|
|
|
10
10
|
closeSync,
|
|
11
11
|
constants as fsConstants,
|
|
12
12
|
existsSync,
|
|
13
|
+
fstatSync,
|
|
14
|
+
lstatSync,
|
|
13
15
|
mkdirSync,
|
|
14
16
|
mkdtempSync,
|
|
15
17
|
openSync,
|
|
16
18
|
readdirSync,
|
|
17
19
|
readFileSync,
|
|
20
|
+
readSync,
|
|
18
21
|
realpathSync,
|
|
19
22
|
renameSync,
|
|
20
23
|
rmSync,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
import { constants as osConstants, homedir, tmpdir } from "node:os";
|
|
25
28
|
import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
|
|
26
29
|
import { spawn, spawnSync } from "node:child_process";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
27
31
|
import { fileURLToPath } from "node:url";
|
|
28
32
|
import {
|
|
29
33
|
buildPiInvocation,
|
|
@@ -54,6 +58,33 @@ import {
|
|
|
54
58
|
restoreJsonField,
|
|
55
59
|
shellQuote,
|
|
56
60
|
} from "../runtime/gentle-shell-launcher.mjs";
|
|
61
|
+
import {
|
|
62
|
+
BUNDLED_VERIFY_ENV,
|
|
63
|
+
bundledEngramSchema,
|
|
64
|
+
classifyEngramStartup,
|
|
65
|
+
ENGRAM_COMPANION_SOURCE,
|
|
66
|
+
ENGRAM_PROVISION_FAILED_NOTICE,
|
|
67
|
+
ENGRAM_RECOVERY_NOTICE,
|
|
68
|
+
engramHeaderAllows,
|
|
69
|
+
engramProbeAllowed,
|
|
70
|
+
engramProvenanceMatches,
|
|
71
|
+
engramProvenancePath,
|
|
72
|
+
parseEngramProvenance,
|
|
73
|
+
piVersionOrHelpRequested,
|
|
74
|
+
probeEngramEndpoint,
|
|
75
|
+
sqliteHeaderUserVersion,
|
|
76
|
+
supportsEngramStartupPolicy,
|
|
77
|
+
bundledChildEnv,
|
|
78
|
+
bundledPi,
|
|
79
|
+
bundledSetupEnv,
|
|
80
|
+
bundledUpdateNext,
|
|
81
|
+
bundledUpdateNotice,
|
|
82
|
+
bundledUpdatePlan,
|
|
83
|
+
nodeBundledFs,
|
|
84
|
+
parseBundledUpgradeArgs,
|
|
85
|
+
resolveBundledInstall,
|
|
86
|
+
withBundledNpmCommand,
|
|
87
|
+
} from "../runtime/gentle-shell-bundled.mjs";
|
|
57
88
|
import {
|
|
58
89
|
parseResumeHandoff,
|
|
59
90
|
planResumeHint,
|
|
@@ -61,8 +92,12 @@ import {
|
|
|
61
92
|
RESUME_HANDOFF_ENV,
|
|
62
93
|
RESUME_HANDOFF_FILE,
|
|
63
94
|
} from "../runtime/gentle-shell-resume-hint.mjs";
|
|
64
|
-
import { GENTLE_AI_VERSION, gentleAiBinaryPath, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
|
|
95
|
+
import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
|
|
65
96
|
import { DEFAULT_THEME_NAME, installIsolatedTuiModeSetting } from "../scripts/install-tui-mode-setting.mjs";
|
|
97
|
+
import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
98
|
+
import { shouldCheckStartupUpdate, findStartupUpdate } from "../runtime/gentle-shell-startup-update.mjs";
|
|
99
|
+
import { promptStartupUpdate } from "../runtime/gentle-shell-update-panel.mjs";
|
|
100
|
+
import { applyStartupChoice, omitStartupVersion, resolveStartupRelaunch } from "../runtime/gentle-shell-startup-flow.mjs";
|
|
66
101
|
|
|
67
102
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
68
103
|
|
|
@@ -128,6 +163,213 @@ function signalExitCode(signal) {
|
|
|
128
163
|
return 128 + (typeof number === "number" ? number : 0);
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
// A bundled install (S8): this package inside <prefix>/versions/<id> that
|
|
167
|
+
// `current` names (lib/gentle-shell-bundled.ts decides from these reads).
|
|
168
|
+
const bundledFs = nodeBundledFs;
|
|
169
|
+
|
|
170
|
+
// Set in a bundled install: the environment every child (Pi, setup, the version
|
|
171
|
+
// probe) starts from instead of process.env (the user's own plus Gentle Shell's
|
|
172
|
+
// variables), and the folders setup puts ahead of the user's PATH.
|
|
173
|
+
let bundledEnv;
|
|
174
|
+
let bundledSetupDirs;
|
|
175
|
+
// The bundled install and its Pi cli, for bundledProcessEnv and setup's Engram.
|
|
176
|
+
let bundledInstall;
|
|
177
|
+
let bundledCli;
|
|
178
|
+
let bundledMemory;
|
|
179
|
+
// The Shell-owned guard ENGRAM_BIN names when memory is disabled (bin/engram-guard).
|
|
180
|
+
const ENGRAM_GUARD = join(packageRoot, "bin", "engram-guard");
|
|
181
|
+
|
|
182
|
+
// The pinned core only as ensureEngram published it: its runtime marker for
|
|
183
|
+
// this release's archive and a regular file. Never a link or a PATH engram.
|
|
184
|
+
function verifiedBundledCore() {
|
|
185
|
+
const core = bundledGentleAiPaths(bundledInstall.root, process.platform).engram;
|
|
186
|
+
try {
|
|
187
|
+
const marker = readFileSync(join(dirname(core), ".gentle-shell-runtime"), "utf8");
|
|
188
|
+
if (marker === `${artifactFor("engram", process.platform, process.arch).url}\n` && lstatSync(core).isFile()) return core;
|
|
189
|
+
} catch {
|
|
190
|
+
// Missing, unmarked, or an unsupported target: no verified core.
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sha256File(path) {
|
|
196
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readSmallRegularFile(path, limit) {
|
|
200
|
+
try {
|
|
201
|
+
const info = lstatSync(path);
|
|
202
|
+
return info.isFile() && info.size <= limit ? readFileSync(path, "utf8") : undefined;
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The database's raw SQLite header, read as a plain file (never through
|
|
209
|
+
// SQLite): a regular, unlinked engram.db whose opened identity is the one seen.
|
|
210
|
+
function readSqliteUserVersion(path) {
|
|
211
|
+
let fd;
|
|
212
|
+
try {
|
|
213
|
+
const seen = lstatSync(path);
|
|
214
|
+
if (!seen.isFile()) return undefined;
|
|
215
|
+
fd = openSync(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
216
|
+
const opened = fstatSync(fd);
|
|
217
|
+
if (opened.ino !== seen.ino || opened.dev !== seen.dev) return undefined;
|
|
218
|
+
const header = Buffer.alloc(100);
|
|
219
|
+
return readSync(fd, header, 0, 100, 0) === 100 ? sqliteHeaderUserVersion(header, opened.size) : undefined;
|
|
220
|
+
} catch {
|
|
221
|
+
return undefined;
|
|
222
|
+
} finally {
|
|
223
|
+
if (fd !== undefined) closeSync(fd);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// The default data directory without opening it: presence, canonical path,
|
|
228
|
+
// the .instance-id file and the database header. A link is never followed.
|
|
229
|
+
function observeEngramStore() {
|
|
230
|
+
const dir = join(homedir(), ".engram");
|
|
231
|
+
let dataDir;
|
|
232
|
+
try {
|
|
233
|
+
if (!lstatSync(dir).isDirectory()) return { store: "unknown" };
|
|
234
|
+
dataDir = realpathSync(dir);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { store: error.code === "ENOENT" ? "absent" : "unknown" };
|
|
237
|
+
}
|
|
238
|
+
return { store: "present", dir, dataDir, instanceId: readSmallRegularFile(join(dir, ".instance-id"), 128)?.trim() };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Only the identity files Engram's `instance-id` writes: a store Shell
|
|
242
|
+
// provisioned whose session has not created its database yet.
|
|
243
|
+
function identityOnlyStore(dir) {
|
|
244
|
+
try {
|
|
245
|
+
return readdirSync(dir).every((name) => name === ".instance-id" || name === ".instance-id.lock" || name.startsWith(".instance-id-"));
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Shell's own store: the record Shell wrote when it provisioned this exact
|
|
252
|
+
// directory's identity, the same core digest, and a database header no newer
|
|
253
|
+
// than the bundled schema (or no database yet).
|
|
254
|
+
function ownEngramStore(observed, core) {
|
|
255
|
+
let record;
|
|
256
|
+
try {
|
|
257
|
+
record = parseEngramProvenance(readJsonIfExists(engramProvenancePath(bundledInstall.root, process.platform)));
|
|
258
|
+
} catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
if (record === undefined || observed.store !== "present" || core === undefined) return undefined;
|
|
262
|
+
if (!engramProvenanceMatches(record, { dataDir: observed.dataDir, instanceId: observed.instanceId, coreSha256: sha256File(core) })) return undefined;
|
|
263
|
+
const headerAllows = engramHeaderAllows(readSqliteUserVersion(join(observed.dir, "engram.db")), bundledEngramSchema(engramPinVersion));
|
|
264
|
+
return headerAllows || identityOnlyStore(observed.dir) ? { instanceId: record.instanceId } : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Evidence only: file metadata, the raw header and a bounded read-only probe
|
|
268
|
+
// of the default endpoint. No Engram executable, SQLite, identity repair, or
|
|
269
|
+
// contact with an explicitly selected endpoint.
|
|
270
|
+
async function classifyBundledMemory() {
|
|
271
|
+
const core = verifiedBundledCore();
|
|
272
|
+
const decision = { env: process.env, core: core !== undefined, coreVersion: engramPinVersion };
|
|
273
|
+
if (!engramProbeAllowed(process.env)) return classifyEngramStartup({ ...decision, store: "unknown", endpoint: { kind: "unknown" } });
|
|
274
|
+
const observed = observeEngramStore();
|
|
275
|
+
const endpoint = await probeEngramEndpoint();
|
|
276
|
+
return classifyEngramStartup({ ...decision, store: observed.store, endpoint, provenance: ownEngramStore(observed, core) });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Verified with the published Engram v3.4.0 darwin-arm64 binary in an isolated
|
|
280
|
+
// HOME: `instance-id` creates only `.instance-id.lock` and `.instance-id`,
|
|
281
|
+
// without a database. Keep update checks suppressed and GitHub tokens removed
|
|
282
|
+
// for this provisioning invocation. The timeout stays below the identity lock
|
|
283
|
+
// wait: a held lock means a concurrent creator, so memory stays off that launch.
|
|
284
|
+
const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
|
|
285
|
+
|
|
286
|
+
// A fresh launch's store identity, provisioned by Shell itself with the verified
|
|
287
|
+
// core before Pi starts, then recorded. False when any step fails.
|
|
288
|
+
function provisionEngramStore() {
|
|
289
|
+
const core = verifiedBundledCore();
|
|
290
|
+
if (core === undefined || observeEngramStore().store !== "absent") return false;
|
|
291
|
+
let target;
|
|
292
|
+
try {
|
|
293
|
+
target = join(realpathSync(homedir()), ".engram");
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
// The provider's own environment, except for this one invocation: no update
|
|
298
|
+
// check and no GitHub tokens. ENGRAM_DATA_DIR is never set here.
|
|
299
|
+
const env = { ...process.env, ENGRAM_NO_UPDATE_CHECK: "1" };
|
|
300
|
+
delete env.GH_TOKEN;
|
|
301
|
+
delete env.GITHUB_TOKEN;
|
|
302
|
+
const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
303
|
+
const observed = observeEngramStore();
|
|
304
|
+
if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
|
|
305
|
+
String(result.stdout).trim() !== observed.instanceId) return false;
|
|
306
|
+
return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function writeEngramProvenance(record) {
|
|
310
|
+
try {
|
|
311
|
+
const path = engramProvenancePath(bundledInstall.root, process.platform);
|
|
312
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
313
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
314
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
315
|
+
renameSync(temporary, path);
|
|
316
|
+
return true;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Use Pi's supported configuration-only install command, never an alternate
|
|
324
|
+
// extension loader. Verify advertised enforcement before trusting the policy.
|
|
325
|
+
async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
|
|
326
|
+
const metadata = join(home.dir, "npm", "node_modules", "gentle-engram", "package.json");
|
|
327
|
+
let packages;
|
|
328
|
+
try {
|
|
329
|
+
packages = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages ?? [];
|
|
330
|
+
} catch {
|
|
331
|
+
return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from malformed settings; repair settings and recheck" };
|
|
332
|
+
}
|
|
333
|
+
if (!Array.isArray(packages)) return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from non-array packages" };
|
|
334
|
+
let pinned = false;
|
|
335
|
+
let local = false;
|
|
336
|
+
let npmSelected = false;
|
|
337
|
+
for (const entry of packages) {
|
|
338
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
339
|
+
if (source === ENGRAM_COMPANION_SOURCE) pinned = true;
|
|
340
|
+
if (typeof source === "string" && /^npm:gentle-engram(?:@|$)/.test(source)) npmSelected = true;
|
|
341
|
+
if (typeof source !== "string" || source.startsWith("npm:")) continue;
|
|
342
|
+
// Preserve an explicit local companion selection rather than overwrite it.
|
|
343
|
+
const selected = resolvePath(home.dir, source);
|
|
344
|
+
if (readPackageName(selected) !== "gentle-engram") continue;
|
|
345
|
+
if (!supportsEngramStartupPolicy(readJsonIfExists(join(selected, "package.json")))) {
|
|
346
|
+
return { ok: false, exitCode: 1, message: `gentle-shell: selected companion ${source} lacks engram.startupPolicy=1; update it and recheck (selection preserved)` };
|
|
347
|
+
}
|
|
348
|
+
local = true;
|
|
349
|
+
}
|
|
350
|
+
if (local && !npmSelected) return { ok: true };
|
|
351
|
+
if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true };
|
|
352
|
+
process.stderr.write(`gentle-shell: installing policy-capable companion ${ENGRAM_COMPANION_SOURCE} through Pi\n`);
|
|
353
|
+
const result = await spawnAndWait(runtime.command, [...runtime.args, "install", ENGRAM_COMPANION_SOURCE], buildSetupEnv(home, runtime), stdio, timeoutMs);
|
|
354
|
+
let declared = false;
|
|
355
|
+
try {
|
|
356
|
+
declared = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages?.some((entry) =>
|
|
357
|
+
(typeof entry === "string" ? entry : entry?.source) === ENGRAM_COMPANION_SOURCE) === true;
|
|
358
|
+
} catch { /* Missing or malformed readback cannot establish enforcement. */ }
|
|
359
|
+
if (!result.ok || result.timedOut || !declared || !supportsEngramStartupPolicy(readJsonIfExists(metadata))) {
|
|
360
|
+
return { ok: false, exitCode: 1, message: "gentle-shell: companion installation did not establish engram.startupPolicy=1; the Engram guard still keeps unverified stores closed. Repair gentle-engram, then restart Shell." };
|
|
361
|
+
}
|
|
362
|
+
return { ok: true };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// The bundled children's environment (bundledChildEnv), with the pinned Engram
|
|
366
|
+
// only once it is published: computed again right before Pi starts, so an
|
|
367
|
+
// Engram the automatic setup just published is used in the same launch.
|
|
368
|
+
function bundledProcessEnv() {
|
|
369
|
+
return bundledChildEnv(process.env, { node: process.execPath, cli: bundledCli, prefix: bundledInstall.root, engram: verifiedBundledCore(),
|
|
370
|
+
guard: ENGRAM_GUARD, memory: bundledMemory });
|
|
371
|
+
}
|
|
372
|
+
|
|
131
373
|
function ownPackageVersion() {
|
|
132
374
|
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
133
375
|
return packageJson.version;
|
|
@@ -550,15 +792,50 @@ function ensurePackageLocalGentleAi(binaryPath, pinnedVersion, stdio) {
|
|
|
550
792
|
// home, and the resolved pi runtime's directory is prepended to PATH so
|
|
551
793
|
// gentle-ai's (or pi's own) preflight finds `pi` even when it is bundled or
|
|
552
794
|
// given through GENTLE_SHELL_PI.
|
|
553
|
-
function buildSetupEnv(home, runtime) {
|
|
795
|
+
function buildSetupEnv(home, runtime, piLauncherDir) {
|
|
796
|
+
// Bundled: our node, then the version's `pi`, ahead of the user's PATH, and
|
|
797
|
+
// Gentle AI's state, persona and Engram inside the prefix (S9).
|
|
798
|
+
if (bundledEnv) {
|
|
799
|
+
const env = bundledSetupEnv({ ...bundledEnv, PI_CODING_AGENT_DIR: home.dir, GENTLE_PI_AGENT_HOME: home.dir }, { dirs: bundledSetupDirs, platform: process.platform });
|
|
800
|
+
return gentleAiIsolationEnv(env, { packageRoot });
|
|
801
|
+
}
|
|
554
802
|
return {
|
|
555
803
|
...process.env,
|
|
556
804
|
PI_CODING_AGENT_DIR: home.dir,
|
|
557
805
|
GENTLE_PI_AGENT_HOME: home.dir,
|
|
558
|
-
PATH:
|
|
806
|
+
PATH: [...(piLauncherDir ? [piLauncherDir] : []), dirname(runtime.command), process.env.PATH ?? ""].join(delimiter),
|
|
559
807
|
};
|
|
560
808
|
}
|
|
561
809
|
|
|
810
|
+
// Gentle AI requires a `pi` on its PATH and runs `pi install npm:gentle-pi`
|
|
811
|
+
// with it. A Pi gentle-shell resolved elsewhere (the adjacent package, run
|
|
812
|
+
// as `node <cli>`, or GENTLE_SHELL_PI) is not that `pi`, so a first launch
|
|
813
|
+
// from a terminal without one failed every time. This writes a private,
|
|
814
|
+
// temporary `pi` that runs exactly the resolved runtime; null when the
|
|
815
|
+
// runtime already is the `pi` on PATH (or for a bundled install, whose
|
|
816
|
+
// setup PATH already starts with the version's node_modules/.bin).
|
|
817
|
+
function writeSetupPiLauncher(runtime) {
|
|
818
|
+
if (bundledEnv || runtime.kind === "path") return null;
|
|
819
|
+
let dir;
|
|
820
|
+
try {
|
|
821
|
+
dir = mkdtempSync(join(tmpdir(), "gentle-shell-pi-"));
|
|
822
|
+
} catch {
|
|
823
|
+
// No usable temp folder: setup runs as it did before, without this `pi`.
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
if (process.platform === "win32") {
|
|
827
|
+
// Batch expands % even inside quotes; double it so a path stays literal.
|
|
828
|
+
const quote = (value) => `"${value.replaceAll("%", "%%")}"`;
|
|
829
|
+
// A batch runtime needs `call`, or control never returns for `exit /b`.
|
|
830
|
+
const call = /\.(?:cmd|bat)$/i.test(runtime.command) ? "call " : "";
|
|
831
|
+
writeFileSync(join(dir, "pi.cmd"), `@echo off\r\n${call}${[runtime.command, ...runtime.args].map(quote).join(" ")} %*\r\nexit /b %errorlevel%\r\n`);
|
|
832
|
+
} else {
|
|
833
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
834
|
+
writeFileSync(join(dir, "pi"), `#!/bin/sh\nexec ${[runtime.command, ...runtime.args].map(quote).join(" ")} "$@"\n`, { mode: 0o700 });
|
|
835
|
+
}
|
|
836
|
+
return dir;
|
|
837
|
+
}
|
|
838
|
+
|
|
562
839
|
// The shared Pi persona file gentle-ai writes on every install, regardless
|
|
563
840
|
// of the target home: its own PiPersonaConfigPath always resolves against
|
|
564
841
|
// the OS home, never PI_CODING_AGENT_DIR (gentle-ai internal/components/persona/inject.go),
|
|
@@ -570,6 +847,29 @@ function sharedPersonaPath() {
|
|
|
570
847
|
return join(homedir(), ".pi", "gentle-ai", "persona.json");
|
|
571
848
|
}
|
|
572
849
|
|
|
850
|
+
// The persona files a setup run may rewrite. Bundled (S9): the prefix's own,
|
|
851
|
+
// which a Gentle AI with GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and
|
|
852
|
+
// still the user's, which a pinned Gentle AI without it writes; restoring an
|
|
853
|
+
// unchanged file does nothing.
|
|
854
|
+
function setupPersonaPaths() {
|
|
855
|
+
if (!bundledInstall) return [sharedPersonaPath()];
|
|
856
|
+
return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Bundled setup (S9): the pinned Engram GENTLE_AI_ENGRAM_BIN names, published
|
|
860
|
+
// once in runtime/ (verified, never brew or the user's PATH). A failure stops
|
|
861
|
+
// setup rather than let Gentle AI acquire another Engram.
|
|
862
|
+
async function ensureBundledEngram() {
|
|
863
|
+
const { ensureEngram, prefixLayout } = await import("../scripts/bundled-install.mjs");
|
|
864
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundledInstall.root });
|
|
865
|
+
try {
|
|
866
|
+
await ensureEngram({ layout, platform: process.platform, arch: process.arch, env: process.env });
|
|
867
|
+
return { ok: true };
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return { ok: false, exitCode: 1, message: `gentle-shell: could not install the pinned Engram in ${layout.engramDir} (${error.message})` };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
573
873
|
// Records `path`'s current state before a child process that might rewrite
|
|
574
874
|
// it runs: whether it exists, and if so its exact bytes and mode. Returns
|
|
575
875
|
// `{ path, existed: false }` for a missing file so restoreFile below knows
|
|
@@ -818,10 +1118,24 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
818
1118
|
|
|
819
1119
|
process.stderr.write(`gentle-shell: provisioning ${home.dir} with the gentle-ai companion packages\n`);
|
|
820
1120
|
|
|
1121
|
+
if (bundledInstall && !dryRun) {
|
|
1122
|
+
const engram = await ensureBundledEngram();
|
|
1123
|
+
if (!engram.ok) return engram;
|
|
1124
|
+
}
|
|
1125
|
+
// The memory decision never skips setup: it only selects the Engram
|
|
1126
|
+
// environment (core or guard, policy, ENGRAM_URL) setup's children and the
|
|
1127
|
+
// launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
|
|
1128
|
+
// no store. Reclassified now that the verified core may just be published.
|
|
1129
|
+
if (bundledInstall) {
|
|
1130
|
+
bundledMemory = await classifyBundledMemory();
|
|
1131
|
+
bundledEnv = bundledProcessEnv();
|
|
1132
|
+
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
821
1135
|
const setupArgs = ["install", "--agent", "pi", "--scope", "global", ...(dryRun ? ["--dry-run"] : [])];
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
const
|
|
1136
|
+
const piLauncherDir = writeSetupPiLauncher(runtime);
|
|
1137
|
+
const env = buildSetupEnv(home, runtime, piLauncherDir);
|
|
1138
|
+
const personaSnapshots = setupPersonaPaths().map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
|
|
825
1139
|
const statePath = sharedGentleAiStatePath();
|
|
826
1140
|
const originalStateText = safely("read your Gentle AI state file", statePath, undefined, () => readParsableJsonText(statePath));
|
|
827
1141
|
// Theme restore (never for --link — that home is the user's own
|
|
@@ -838,8 +1152,11 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
838
1152
|
try {
|
|
839
1153
|
installResult = await spawnAndWait(binaryPath, setupArgs, env, stdio, timeoutMs);
|
|
840
1154
|
} finally {
|
|
841
|
-
if (
|
|
842
|
-
|
|
1155
|
+
if (piLauncherDir) rmSync(piLauncherDir, { recursive: true, force: true });
|
|
1156
|
+
for (const [personaPath, personaSnapshot] of personaSnapshots) {
|
|
1157
|
+
if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
|
|
1158
|
+
process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
|
|
1159
|
+
}
|
|
843
1160
|
}
|
|
844
1161
|
if (safely("restore your Gentle AI managed-asset record", statePath, false, () => restoreManagedAssetDigestField(statePath, originalStateText))) {
|
|
845
1162
|
process.stderr.write(
|
|
@@ -861,7 +1178,63 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
861
1178
|
}
|
|
862
1179
|
if (!installResult.ok) return installResult;
|
|
863
1180
|
|
|
864
|
-
|
|
1181
|
+
const cleanupResult = await runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
|
|
1182
|
+
if (cleanupResult.ok && !dryRun) {
|
|
1183
|
+
warnDisabledMCP(home);
|
|
1184
|
+
// The guard and policy protect memory, so the companion check only warns.
|
|
1185
|
+
const companion = bundledInstall ? await ensureEngramCompanion(home, runtime, stdio, timeoutMs) : { ok: true };
|
|
1186
|
+
if (!companion.ok) process.stderr.write(`${companion.message}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
return cleanupResult;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function mcpAdapterActivity(packages) {
|
|
1192
|
+
if (typeof packages === "string") return packages === "npm:pi-mcp-adapter" || packages.startsWith("npm:pi-mcp-adapter@") ? "active" : "inactive";
|
|
1193
|
+
if (Array.isArray(packages)) {
|
|
1194
|
+
const activities = packages.map(mcpAdapterActivity);
|
|
1195
|
+
return activities.includes("active") ? "active" : activities.includes("unknown") ? "unknown" : "inactive";
|
|
1196
|
+
}
|
|
1197
|
+
if (packages === null || typeof packages !== "object") return "inactive";
|
|
1198
|
+
if (typeof packages.source !== "string") return mcpAdapterActivity(Object.keys(packages));
|
|
1199
|
+
if (mcpAdapterActivity(packages.source) === "inactive") return "inactive";
|
|
1200
|
+
const patterns = packages.extensions;
|
|
1201
|
+
if (patterns === undefined) return packages.autoload === false ? "inactive" : "active";
|
|
1202
|
+
if (!Array.isArray(patterns)) return "unknown";
|
|
1203
|
+
if (patterns.length === 0) return "inactive";
|
|
1204
|
+
if (packages.autoload === false) {
|
|
1205
|
+
return patterns.some(pattern => typeof pattern === "string" && !pattern.startsWith("!") && !pattern.startsWith("-")) ? "unknown" : "inactive";
|
|
1206
|
+
}
|
|
1207
|
+
// Ordinary exclusions run after includes, but exact +path entries can
|
|
1208
|
+
// restore resources. Other filters need actual package files to resolve.
|
|
1209
|
+
if (patterns.includes("!**") && !patterns.some(pattern => typeof pattern === "string" && pattern.startsWith("+"))) return "inactive";
|
|
1210
|
+
return "unknown";
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Onboarding cannot prove who disabled MCP: diagnose, never re-enable servers.
|
|
1214
|
+
// This also covers automatic provisioning because both routes share setup.
|
|
1215
|
+
function warnDisabledMCP(home) {
|
|
1216
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1217
|
+
const mcpPath = join(home.dir, "mcp.json");
|
|
1218
|
+
let inspecting = settingsPath;
|
|
1219
|
+
try {
|
|
1220
|
+
const settingsText = readJsonIfExists(settingsPath);
|
|
1221
|
+
if (settingsText === undefined) return;
|
|
1222
|
+
const settings = JSON.parse(settingsText);
|
|
1223
|
+
if (!Array.isArray(settings?.extensions) || !settings.extensions.includes("-builtin:mcp")) return;
|
|
1224
|
+
const activity = mcpAdapterActivity(settings.packages);
|
|
1225
|
+
if (activity === "active") return;
|
|
1226
|
+
inspecting = mcpPath;
|
|
1227
|
+
const mcpText = readJsonIfExists(mcpPath);
|
|
1228
|
+
if (mcpText === undefined) return;
|
|
1229
|
+
const servers = JSON.parse(mcpText)?.mcpServers;
|
|
1230
|
+
if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length === 0) return;
|
|
1231
|
+
const diagnosis = activity === "unknown"
|
|
1232
|
+
? `pi-mcp-adapter activity cannot be confirmed from its extension filters, so servers in ${mcpPath} may not load`
|
|
1233
|
+
: `pi-mcp-adapter is absent or inactive, so servers in ${mcpPath} will not load`;
|
|
1234
|
+
process.stderr.write(`gentle-shell: WARNING: Pi built-in MCP is disabled in ${settingsPath}; ${diagnosis}. If you want these servers enabled, remove -builtin:mcp from extensions in ${settingsPath}, then restart Pi. If MCP is intentionally disabled, keep the setting.\n`);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
process.stderr.write(`gentle-shell: WARNING: Pi MCP configuration could not be inspected in ${inspecting}: ${error.message}; inspect or repair it manually.\n`);
|
|
1237
|
+
}
|
|
865
1238
|
}
|
|
866
1239
|
|
|
867
1240
|
// The stderr line printed once `source` is actually removed from `home`.
|
|
@@ -941,7 +1314,11 @@ async function removePostInstallSources(sources, index, home, runtime, stdio, ti
|
|
|
941
1314
|
// --dry-run, runs the shared flow with the child's stdio inherited (today's
|
|
942
1315
|
// behavior, unchanged), then exits with its result — this is the one place
|
|
943
1316
|
// that keeps the pre-S7 exit semantics `handleSetupCommand` always had.
|
|
944
|
-
|
|
1317
|
+
// A successful real run records the home exactly as auto-provisioning does
|
|
1318
|
+
// (#2106: both installers finish with this command, and the first launch
|
|
1319
|
+
// must not install the packages again), but only for a home auto-provisioning
|
|
1320
|
+
// itself would own: never pi's own default home or an unrelated directory.
|
|
1321
|
+
async function handleSetupCommand(commandArgs, home, runtime, { homeHadContentBeforeBootstrap }) {
|
|
945
1322
|
let dryRun = false;
|
|
946
1323
|
for (const arg of commandArgs) {
|
|
947
1324
|
if (arg === "--dry-run") {
|
|
@@ -953,6 +1330,19 @@ async function handleSetupCommand(commandArgs, home, runtime) {
|
|
|
953
1330
|
|
|
954
1331
|
const result = await runSetupFlow(home, runtime, { dryRun, stdio: "inherit" });
|
|
955
1332
|
if (!result.ok && result.message !== undefined) process.stderr.write(`${result.message}\n`);
|
|
1333
|
+
if (result.ok && !dryRun && (home.mode === "isolated" || home.mode === "path")) {
|
|
1334
|
+
const configPath = resolveConfigPath();
|
|
1335
|
+
const homeKey = safeRealpath(home.dir);
|
|
1336
|
+
const config = readRawConfig(configPath);
|
|
1337
|
+
if (!homeIsForeign(home, provisionedEntry(config, homeKey), homeHadContentBeforeBootstrap)) {
|
|
1338
|
+
try {
|
|
1339
|
+
writeRawConfig(configPath, recordProvisioned(config, homeKey, resolveSetupGentleAiPin(), ownPackageVersion(), new Date().toISOString()));
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
// Setup itself succeeded; the next launch only sets the home up again.
|
|
1342
|
+
process.stderr.write(`gentle-shell: could not record the setup in ${configPath} (${error.message})\n`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
956
1346
|
process.exit(result.exitCode);
|
|
957
1347
|
}
|
|
958
1348
|
|
|
@@ -1179,29 +1569,269 @@ async function maybeAutoProvisionHome(home, runtime, { homeHadContentBeforeBoots
|
|
|
1179
1569
|
}
|
|
1180
1570
|
}
|
|
1181
1571
|
|
|
1572
|
+
// The adapters `upgrade` and `self-uninstall` run npm and pnpm with: { fs, which, run, invocation? }.
|
|
1573
|
+
async function packageManagerAdapters() {
|
|
1574
|
+
const { hostAdapters } = await import("../scripts/installer-probes.mjs");
|
|
1575
|
+
const { upgradeInvocation } = await import("../scripts/installer-runner.mjs");
|
|
1576
|
+
const { run, fs: probeFs } = hostAdapters();
|
|
1577
|
+
// Windows: npm and pnpm are .cmd shims; run what they run, never through cmd.exe.
|
|
1578
|
+
const invocation = upgradeInvocation({ platform: process.platform, env: process.env, run, fs: probeFs });
|
|
1579
|
+
return {
|
|
1580
|
+
fs: await import("node:fs/promises"),
|
|
1581
|
+
which: async (name) => findOnPath(name) ?? null,
|
|
1582
|
+
...(invocation ? { invocation } : {}),
|
|
1583
|
+
run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000 }),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// `gentle-shell upgrade`: runs before any Pi runtime check, since it may replace this package.
|
|
1588
|
+
// On release, a global pnpm or npm install moves to the bundled one when the
|
|
1589
|
+
// latest release publishes its assets (S4), with one question about the old package.
|
|
1590
|
+
async function handleUpgradeCommand(commandArgs) {
|
|
1591
|
+
const { MainChannelError, runUpgrade } = await import("../scripts/main-channel.mjs");
|
|
1592
|
+
const { fetchDistribution, migrateGlobalShell, windowsPathRegistry } = await import("../scripts/bundled-wizard.mjs");
|
|
1593
|
+
const { askLine } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1594
|
+
const managers = await packageManagerAdapters();
|
|
1595
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
1596
|
+
const migrate = ({ latest, owner }) => migrateGlobalShell({
|
|
1597
|
+
latest,
|
|
1598
|
+
currentVersion: ownPackageVersion(),
|
|
1599
|
+
platform: process.platform,
|
|
1600
|
+
arch: process.arch,
|
|
1601
|
+
env: process.env,
|
|
1602
|
+
home: homedir(),
|
|
1603
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
1604
|
+
// End of input or Ctrl+C answers No.
|
|
1605
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }).catch(() => ""),
|
|
1606
|
+
out,
|
|
1607
|
+
adapters: { owner, distribution: (version) => fetchDistribution({ version }), run: managers.run,
|
|
1608
|
+
...(process.platform === "win32" ? { registry: windowsPathRegistry(process.env) } : {}) },
|
|
1609
|
+
});
|
|
1610
|
+
try {
|
|
1611
|
+
process.exitCode = await runUpgrade({
|
|
1612
|
+
args: commandArgs,
|
|
1613
|
+
ctx: { env: process.env, home: homedir() },
|
|
1614
|
+
platform: process.platform,
|
|
1615
|
+
packageRoot,
|
|
1616
|
+
currentVersion: ownPackageVersion(),
|
|
1617
|
+
adapters: { fetch: globalThis.fetch, ...managers, migrate },
|
|
1618
|
+
out,
|
|
1619
|
+
});
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
if (error instanceof MainChannelError) fail(`gentle-shell upgrade: ${error.message}`, error.code === "upgrade-usage" ? 2 : 1);
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// `gentle-shell upgrade` in a bundled install: the latest release installed
|
|
1627
|
+
// beside the active version, verified, then `current` switched (two versions
|
|
1628
|
+
// kept), or `--rollback` to the kept one. Returns the exit code (a failure is
|
|
1629
|
+
// printed, not thrown, so `update --all` can go on); a failure leaves `current`
|
|
1630
|
+
// as it was.
|
|
1631
|
+
async function handleBundledUpgrade(bundled, commandArgs) {
|
|
1632
|
+
const parsed = parseBundledUpgradeArgs(commandArgs);
|
|
1633
|
+
if ("error" in parsed) fail(`gentle-shell upgrade: ${parsed.error}`, parsed.usage ? 2 : 1);
|
|
1634
|
+
const { prefixLayout, rollbackBundled, upgradeBundled } = await import("../scripts/bundled-install.mjs");
|
|
1635
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1636
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
1637
|
+
try {
|
|
1638
|
+
if (parsed.action === "rollback") {
|
|
1639
|
+
const { from, to } = await rollbackBundled({ layout, home: homedir(), platform: process.platform, arch: process.arch, env: process.env });
|
|
1640
|
+
out(`Switched Gentle Shell from ${from} to ${to}.`);
|
|
1641
|
+
return 0;
|
|
1642
|
+
}
|
|
1643
|
+
const { fetchDistribution } = await import("../scripts/bundled-wizard.mjs");
|
|
1644
|
+
const { latestRelease } = await import("../scripts/main-channel.mjs");
|
|
1645
|
+
await upgradeBundled({
|
|
1646
|
+
layout,
|
|
1647
|
+
home: homedir(),
|
|
1648
|
+
platform: process.platform,
|
|
1649
|
+
arch: process.arch,
|
|
1650
|
+
env: process.env,
|
|
1651
|
+
adapters: { latest: () => latestRelease(globalThis.fetch), distribution: (version) => fetchDistribution({ version }) },
|
|
1652
|
+
out,
|
|
1653
|
+
});
|
|
1654
|
+
return 0;
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
process.stderr.write(`gentle-shell upgrade: ${error.message}${error.stderr ? `\n${error.stderr}` : ""}\n`);
|
|
1657
|
+
return 1;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// The bundled npm as the `npmCommand` of a home Gentle Shell owns (the isolated
|
|
1662
|
+
// home, or a --home it created or provisioned; never --link or a foreign home),
|
|
1663
|
+
// so Pi installs packages with our npm while its tools keep the user's PATH.
|
|
1664
|
+
// Only that key is written, and only when it differs.
|
|
1665
|
+
async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap) {
|
|
1666
|
+
if (home.mode === "link") return;
|
|
1667
|
+
const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
|
|
1668
|
+
if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
|
|
1669
|
+
const { bundledNpmCommand, ensureNpmShim, prefixLayout } = await import("../scripts/bundled-install.mjs");
|
|
1670
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1671
|
+
// The entry npmCommand names, also for a runtime installed before it existed.
|
|
1672
|
+
ensureNpmShim(layout);
|
|
1673
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1674
|
+
const edit = withBundledNpmCommand(readJsonIfExists(settingsPath), bundledNpmCommand(layout));
|
|
1675
|
+
if (edit.status === "malformed") {
|
|
1676
|
+
process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so Pi's package installs use the npm on your PATH\n`);
|
|
1677
|
+
} else if (edit.status === "write") {
|
|
1678
|
+
writeFileAtomically(settingsPath, edit.text);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
|
|
1683
|
+
// folders and exactly the PATH entry its receipt records; on Windows the running
|
|
1684
|
+
// node.exe and launcher are removed by a detached process after this one exits.
|
|
1685
|
+
async function handleBundledSelfUninstall(bundled, commandArgs) {
|
|
1686
|
+
const { askLine, runBundledSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1687
|
+
const { describePathReceipt, prefixLayout, readPathReceipt, removePathEntry, scheduleWindowsRemoval } = await import("../scripts/bundled-install.mjs");
|
|
1688
|
+
const windows = process.platform === "win32";
|
|
1689
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1690
|
+
// Trusted only for the link or startup file the PATH plan can choose for this user.
|
|
1691
|
+
const receipt = readPathReceipt(layout, { env: process.env, home: homedir() });
|
|
1692
|
+
let pathEntry = receipt;
|
|
1693
|
+
if (receipt.status === "recorded") {
|
|
1694
|
+
const registry = windows ? (await import("../scripts/bundled-wizard.mjs")).windowsPathRegistry(process.env) : undefined;
|
|
1695
|
+
pathEntry = { status: "recorded", description: describePathReceipt(receipt.receipt), remove: () => removePathEntry(receipt.receipt, { registry }) };
|
|
1696
|
+
}
|
|
1697
|
+
return runBundledSelfUninstall({
|
|
1698
|
+
args: commandArgs,
|
|
1699
|
+
env: process.env,
|
|
1700
|
+
homedir: homedir(),
|
|
1701
|
+
platform: process.platform,
|
|
1702
|
+
interactive: process.stdin.isTTY === true,
|
|
1703
|
+
root: bundled.root,
|
|
1704
|
+
pathEntry,
|
|
1705
|
+
process: { pid: process.pid, execPath: process.execPath },
|
|
1706
|
+
...(windows ? { deferRemoval: (request) => scheduleWindowsRemoval({ ...request, env: process.env }) } : {}),
|
|
1707
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
|
|
1708
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
1709
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// `gentle-shell self-uninstall`: like upgrade, runs before any Pi runtime check,
|
|
1714
|
+
// so it also works when pi is missing.
|
|
1715
|
+
async function handleSelfUninstallCommand(commandArgs) {
|
|
1716
|
+
const { askLine, runSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1717
|
+
const { ownerManager } = await import("../scripts/main-channel.mjs");
|
|
1718
|
+
const managers = await packageManagerAdapters();
|
|
1719
|
+
const ctx = { env: process.env, home: homedir() };
|
|
1720
|
+
process.exitCode = await runSelfUninstall({
|
|
1721
|
+
args: commandArgs,
|
|
1722
|
+
env: process.env,
|
|
1723
|
+
homedir: homedir(),
|
|
1724
|
+
platform: process.platform,
|
|
1725
|
+
interactive: process.stdin.isTTY === true,
|
|
1726
|
+
resolveOwner: () => ownerManager({ ctx, platform: process.platform, packageRoot, adapters: managers, command: "uninstall" }),
|
|
1727
|
+
run: (command, argv) => managers.run(command, argv),
|
|
1728
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
|
|
1729
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
1730
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Before Pi exists: only the bundled interactive opening may offer an update.
|
|
1735
|
+
async function startupUpdate(bundled, args, startupEnv) {
|
|
1736
|
+
if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
|
|
1737
|
+
inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
|
|
1738
|
+
const configPath = resolveConfigPath();
|
|
1739
|
+
let skipped;
|
|
1740
|
+
try { skipped = readRawConfig(configPath).skippedStartupRelease; } catch { /* A notice must not prevent opening. */ }
|
|
1741
|
+
const { latestRelease } = await import("../scripts/main-channel.mjs");
|
|
1742
|
+
const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
|
|
1743
|
+
lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })) });
|
|
1744
|
+
if (!offer) return undefined;
|
|
1745
|
+
let choice;
|
|
1746
|
+
try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
|
|
1747
|
+
catch { return undefined; }
|
|
1748
|
+
return applyStartupChoice(choice, {
|
|
1749
|
+
omit: () => omitStartupVersion(() => readJsonIfExists(configPath), (text) => {
|
|
1750
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1751
|
+
writeFileAtomically(configPath, text, { mode: 0o600 });
|
|
1752
|
+
}, offer.latest),
|
|
1753
|
+
warn: (message) => process.stderr.write(`gentle-shell: ${message}\n`),
|
|
1754
|
+
upgrade: () => handleBundledUpgrade(bundled, []),
|
|
1755
|
+
relaunch: async () => {
|
|
1756
|
+
const next = await resolveStartupRelaunch({ root: bundled.root, home: homedir(), platform: process.platform,
|
|
1757
|
+
env: process.env, argv: process.argv.slice(2) });
|
|
1758
|
+
return new Promise((resolve, reject) => {
|
|
1759
|
+
const child = spawn(next.command, next.args, { stdio: "inherit", env: next.env, shell: false });
|
|
1760
|
+
const forwards = ["SIGINT", "SIGTERM", "SIGHUP"].map((signal) => {
|
|
1761
|
+
const handler = () => child.kill(signal);
|
|
1762
|
+
process.on(signal, handler);
|
|
1763
|
+
return [signal, handler];
|
|
1764
|
+
});
|
|
1765
|
+
const cleanup = () => { for (const [signal, handler] of forwards) process.off(signal, handler); };
|
|
1766
|
+
child.once("error", (error) => { cleanup(); reject(error); });
|
|
1767
|
+
child.once("exit", (code, signal) => { cleanup(); resolve(signal ? signalExitCode(signal) : (code ?? 1)); });
|
|
1768
|
+
});
|
|
1769
|
+
},
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1182
1773
|
async function main() {
|
|
1774
|
+
// One launch only: never leak the update handoff opt-out into Pi or its tools.
|
|
1775
|
+
const startupEnv = { ...process.env };
|
|
1776
|
+
delete process.env.GENTLE_SHELL_SKIP_STARTUP_UPDATE;
|
|
1183
1777
|
const args = parseLauncherArgs(process.argv.slice(2));
|
|
1184
1778
|
if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
|
|
1185
1779
|
if (args.help) {
|
|
1186
|
-
process.stdout.write(
|
|
1780
|
+
process.stdout.write("Pi updates: update reinstalls the verified supplier of the Pi this launcher runs (a global Pi, or the\nShell package whose adjacent peer it is, at the same Shell version) and reads the resulting Pi version back.\nUnknown ownership refuses without installing; update --extensions-only stays Pi-only.\nPrefix-bundled Pi updates with gentle-shell upgrade.\n");
|
|
1781
|
+
process.stdout.write(`${helpText()}\nBundled Engram safety: memory opens only a fresh, verified or Shell-provisioned store; otherwise ENGRAM_BIN\nis Shell's guard and memory stays off. Matching versions or updating alone do not prove compatibility.\nReuse requires a server with verified /compatibility evidence or matching Shell-provisioned store identity.\nBack up existing data before manual changes; do not delete it or bypass the safety check.\nShell rechecks on every launch and remains usable without memory.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
|
|
1187
1782
|
process.exit(0);
|
|
1188
1783
|
}
|
|
1189
1784
|
if (args.command === "home") {
|
|
1190
1785
|
handleHomeCommand(args.commandArgs);
|
|
1191
1786
|
return;
|
|
1192
1787
|
}
|
|
1788
|
+
// The verification input applies to `--version` only (scripts/bundled-install.mjs upgradeBundled).
|
|
1789
|
+
const verifying = args.version && args.command === undefined ? process.env[BUNDLED_VERIFY_ENV] : undefined;
|
|
1790
|
+
const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });
|
|
1791
|
+
if (args.command === "upgrade") {
|
|
1792
|
+
if (bundled) process.exitCode = await handleBundledUpgrade(bundled, args.commandArgs);
|
|
1793
|
+
else await handleUpgradeCommand(args.commandArgs);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (args.command === "self-uninstall") {
|
|
1797
|
+
if (bundled) process.exitCode = await handleBundledSelfUninstall(bundled, args.commandArgs);
|
|
1798
|
+
else await handleSelfUninstallCommand(args.commandArgs);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const startupCode = await startupUpdate(bundled, args, startupEnv);
|
|
1803
|
+
if (startupCode !== undefined) {
|
|
1804
|
+
process.exitCode = startupCode;
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1193
1807
|
|
|
1194
1808
|
const config = loadConfig();
|
|
1195
1809
|
let home = resolveHome({ args, env: process.env, homedir: homedir(), config });
|
|
1196
1810
|
if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
|
|
1197
1811
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1812
|
+
// Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
|
|
1813
|
+
// ensure the companion, print memory notices or touch provenance.
|
|
1814
|
+
const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
|
|
1815
|
+
let runtime;
|
|
1816
|
+
if (bundled) {
|
|
1817
|
+
// S8: only the version's own Pi, never GENTLE_SHELL_PI, an adjacent fallback or PATH.
|
|
1818
|
+
const pi = bundledPi(bundled, { platform: process.platform, fs: bundledFs });
|
|
1819
|
+
if (!pi.ok) fail(pi.message, 1);
|
|
1820
|
+
runtime = { kind: "bundled", command: process.execPath, args: [pi.cli] };
|
|
1821
|
+
bundledInstall = bundled;
|
|
1822
|
+
bundledCli = pi.cli;
|
|
1823
|
+
bundledMemory = engramQuiet ? { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE } : await classifyBundledMemory();
|
|
1824
|
+
bundledEnv = bundledProcessEnv();
|
|
1825
|
+
bundledSetupDirs = [dirname(process.execPath), join(bundled.versionDir, "node_modules", ".bin")];
|
|
1826
|
+
} else {
|
|
1827
|
+
runtime = resolvePiRuntime({
|
|
1828
|
+
env: process.env,
|
|
1829
|
+
resolveBundledCli,
|
|
1830
|
+
findOnPath,
|
|
1831
|
+
nodeExecPath: process.execPath,
|
|
1832
|
+
});
|
|
1833
|
+
if (runtime === undefined) fail(missingPiMessage(), 1);
|
|
1834
|
+
}
|
|
1205
1835
|
|
|
1206
1836
|
const versionProbePlan = planSpawn({ command: runtime.command, args: [...runtime.args, "--version"], platform: process.platform });
|
|
1207
1837
|
const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {
|
|
@@ -1209,6 +1839,7 @@ async function main() {
|
|
|
1209
1839
|
timeout: 15000,
|
|
1210
1840
|
encoding: "utf8",
|
|
1211
1841
|
shell: versionProbePlan.shell,
|
|
1842
|
+
env: bundledEnv ?? process.env,
|
|
1212
1843
|
});
|
|
1213
1844
|
if (versionProbe.error) fail(`Could not run the pi runtime at "${runtime.command}": ${versionProbe.error.message}`, 1);
|
|
1214
1845
|
const versionCheck = checkPiVersion(versionProbe.stdout ?? "");
|
|
@@ -1219,6 +1850,32 @@ async function main() {
|
|
|
1219
1850
|
process.exit(0);
|
|
1220
1851
|
}
|
|
1221
1852
|
|
|
1853
|
+
// S5: in a bundled install Pi updates only with Gentle Shell, so Pi's own
|
|
1854
|
+
// update becomes `gentle-shell upgrade` (run when interactive, named
|
|
1855
|
+
// otherwise); package updates still reach Pi.
|
|
1856
|
+
let passthrough = args.passthrough;
|
|
1857
|
+
if (bundled && args.piSubcommand === "update") {
|
|
1858
|
+
const plan = bundledUpdatePlan(args.passthrough);
|
|
1859
|
+
if (plan.self) {
|
|
1860
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1861
|
+
for (const line of bundledUpdateNotice({ piVersion: versionCheck.version, interactive })) process.stdout.write(`${line}\n`);
|
|
1862
|
+
const code = interactive ? await handleBundledUpgrade(bundled, []) : undefined;
|
|
1863
|
+
const next = bundledUpdateNext(plan, code);
|
|
1864
|
+
if ("exitCode" in next) process.exit(next.exitCode);
|
|
1865
|
+
if (next.note) process.stderr.write(`${next.note}\n`);
|
|
1866
|
+
}
|
|
1867
|
+
passthrough = plan.piArgs;
|
|
1868
|
+
} else if (args.piSubcommand === "update") {
|
|
1869
|
+
const { managedUpdatePlan, updateSelectedPi } = await import("../scripts/pi-update.mjs");
|
|
1870
|
+
const plan = managedUpdatePlan(args.passthrough);
|
|
1871
|
+
if (plan.self) {
|
|
1872
|
+
process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(),
|
|
1873
|
+
adapters: await packageManagerAdapters(), out: (line) => process.stdout.write(`${line}\n`) });
|
|
1874
|
+
if (plan.piArgs === undefined) return;
|
|
1875
|
+
}
|
|
1876
|
+
passthrough = plan.piArgs;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1222
1879
|
// Home-ownership signal for auto-provisioning (S9, homeIsForeign): must be
|
|
1223
1880
|
// read before the isolated-home bootstrap below creates and seeds a
|
|
1224
1881
|
// brand-new --home directory with its own settings.json — after that
|
|
@@ -1236,14 +1893,24 @@ async function main() {
|
|
|
1236
1893
|
// Isolated-home bootstrap: only on a home gentle-shell has not seen before
|
|
1237
1894
|
// (link never bootstraps — it reuses the user's own pi agent home as-is).
|
|
1238
1895
|
if ((home.mode === "isolated" || home.mode === "path") && !existsSync(home.dir)) {
|
|
1239
|
-
|
|
1896
|
+
// 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
|
|
1897
|
+
// which the bundled install's prefix claim must tighten or refuse.
|
|
1898
|
+
mkdirSync(home.dir, { recursive: true, mode: 0o700 });
|
|
1240
1899
|
await installIsolatedTuiModeSetting(home.dir);
|
|
1241
1900
|
writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
|
|
1242
1901
|
process.stderr.write(`gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats.\n`);
|
|
1243
1902
|
}
|
|
1244
1903
|
|
|
1904
|
+
if (bundled) {
|
|
1905
|
+
try {
|
|
1906
|
+
await ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1245
1912
|
if (args.command === "setup") {
|
|
1246
|
-
await handleSetupCommand(args.commandArgs, home, runtime);
|
|
1913
|
+
await handleSetupCommand(args.commandArgs, home, runtime, { homeHadContentBeforeBootstrap });
|
|
1247
1914
|
return;
|
|
1248
1915
|
}
|
|
1249
1916
|
|
|
@@ -1371,6 +2038,23 @@ async function main() {
|
|
|
1371
2038
|
}
|
|
1372
2039
|
}
|
|
1373
2040
|
|
|
2041
|
+
if (bundled) {
|
|
2042
|
+
// Recheck after setup and before provider loading: never carry a stale
|
|
2043
|
+
// fresh-store decision across an installer that could create data.
|
|
2044
|
+
if (args.piSubcommand === undefined && !engramQuiet) {
|
|
2045
|
+
bundledMemory = await classifyBundledMemory();
|
|
2046
|
+
bundledEnv = bundledProcessEnv();
|
|
2047
|
+
// The guard protects memory whatever provider loads, so a companion that
|
|
2048
|
+
// could not be verified is reported, never a reason to stop Shell.
|
|
2049
|
+
const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
|
|
2050
|
+
if (!companion.ok) process.stderr.write(`${companion.message}\n`);
|
|
2051
|
+
// A fresh store's identity and provenance exist before Pi starts, or
|
|
2052
|
+
// memory stays off: Pi never runs with allow without a written record.
|
|
2053
|
+
if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
|
|
2054
|
+
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
2055
|
+
}
|
|
2056
|
+
bundledEnv = bundledProcessEnv();
|
|
2057
|
+
}
|
|
1374
2058
|
const invocation = buildPiInvocation({
|
|
1375
2059
|
runtime,
|
|
1376
2060
|
home,
|
|
@@ -1379,10 +2063,12 @@ async function main() {
|
|
|
1379
2063
|
takeOver,
|
|
1380
2064
|
otherPackagePaths,
|
|
1381
2065
|
looseExtensionEntries,
|
|
1382
|
-
passthrough: [...managedHerdrExtensionArgs(home, args), ...
|
|
2066
|
+
passthrough: [...managedHerdrExtensionArgs(home, args), ...passthrough],
|
|
1383
2067
|
piSubcommand: args.piSubcommand,
|
|
1384
|
-
baseEnv: process.env,
|
|
2068
|
+
baseEnv: bundledEnv ?? process.env,
|
|
1385
2069
|
homedir: homedir(),
|
|
2070
|
+
// The spawn below sets no cwd, so pi runs in the launcher's own.
|
|
2071
|
+
cwd: process.cwd(),
|
|
1386
2072
|
});
|
|
1387
2073
|
|
|
1388
2074
|
// Only an interactive session ends with pi's exit resume hint, which
|