gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
package/bin/gentle-shell.mjs
CHANGED
|
@@ -6,23 +6,28 @@
|
|
|
6
6
|
// process, filesystem, and child process.
|
|
7
7
|
import {
|
|
8
8
|
accessSync,
|
|
9
|
+
chmodSync,
|
|
9
10
|
closeSync,
|
|
10
11
|
constants as fsConstants,
|
|
11
12
|
existsSync,
|
|
13
|
+
fstatSync,
|
|
14
|
+
lstatSync,
|
|
12
15
|
mkdirSync,
|
|
16
|
+
mkdtempSync,
|
|
13
17
|
openSync,
|
|
14
18
|
readdirSync,
|
|
15
19
|
readFileSync,
|
|
20
|
+
readSync,
|
|
16
21
|
realpathSync,
|
|
17
22
|
renameSync,
|
|
18
23
|
rmSync,
|
|
19
24
|
statSync,
|
|
20
25
|
writeFileSync,
|
|
21
26
|
} from "node:fs";
|
|
22
|
-
import {
|
|
23
|
-
import { constants as osConstants, homedir } from "node:os";
|
|
27
|
+
import { constants as osConstants, homedir, tmpdir } from "node:os";
|
|
24
28
|
import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
|
|
25
29
|
import { spawn, spawnSync } from "node:child_process";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
26
31
|
import { fileURLToPath } from "node:url";
|
|
27
32
|
import {
|
|
28
33
|
buildPiInvocation,
|
|
@@ -53,8 +58,46 @@ import {
|
|
|
53
58
|
restoreJsonField,
|
|
54
59
|
shellQuote,
|
|
55
60
|
} from "../runtime/gentle-shell-launcher.mjs";
|
|
56
|
-
import {
|
|
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";
|
|
88
|
+
import {
|
|
89
|
+
parseResumeHandoff,
|
|
90
|
+
planResumeHint,
|
|
91
|
+
RESUME_HANDOFF_DIR_PREFIX,
|
|
92
|
+
RESUME_HANDOFF_ENV,
|
|
93
|
+
RESUME_HANDOFF_FILE,
|
|
94
|
+
} from "../runtime/gentle-shell-resume-hint.mjs";
|
|
95
|
+
import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
|
|
57
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";
|
|
58
101
|
|
|
59
102
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
60
103
|
|
|
@@ -72,17 +115,30 @@ function readJsonIfExists(path) {
|
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
118
|
+
// Resolve the public ESM entry without importing the agent or reaching through
|
|
119
|
+
// its exports map. Only an absent optional peer permits PATH fallback; malformed
|
|
120
|
+
// installed metadata must not silently select a different runtime.
|
|
77
121
|
function resolveBundledCli() {
|
|
122
|
+
let publicEntry;
|
|
78
123
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
124
|
+
publicEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error.code === "ERR_MODULE_NOT_FOUND") return undefined;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
const entry = realpathSync(publicEntry);
|
|
130
|
+
const root = dirname(dirname(entry));
|
|
131
|
+
const expectedEntry = join(root, "dist", "index.js");
|
|
132
|
+
const metadata = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
133
|
+
if (entry !== expectedEntry || metadata.name !== "@earendil-works/pi-coding-agent" ||
|
|
134
|
+
metadata.bin?.pi !== "dist/bundle/cli.js") {
|
|
135
|
+
throw new Error("Unsupported adjacent Pi package entry/name/bin metadata");
|
|
136
|
+
}
|
|
137
|
+
const cliPath = join(root, metadata.bin.pi);
|
|
138
|
+
if (realpathSync(cliPath) !== cliPath || !statSync(cliPath).isFile()) {
|
|
139
|
+
throw new Error("Unsupported adjacent Pi CLI path");
|
|
85
140
|
}
|
|
141
|
+
return cliPath;
|
|
86
142
|
}
|
|
87
143
|
|
|
88
144
|
function findOnPath(name) {
|
|
@@ -107,6 +163,218 @@ function signalExitCode(signal) {
|
|
|
107
163
|
return 128 + (typeof number === "number" ? number : 0);
|
|
108
164
|
}
|
|
109
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 read-only in Engram v3.3.2: `engram instance-id` first runs a GitHub
|
|
280
|
+
// update check that sends GH_TOKEN or GITHUB_TOKEN (cmd/engram/main.go:742-744,
|
|
281
|
+
// 835-847; internal/version/check.go:23-24,60-70,171-176) unless
|
|
282
|
+
// ENGRAM_NO_UPDATE_CHECK is set (check.go:19,49-51,114-117). It then resolves the
|
|
283
|
+
// default ~/.engram (main.go:749-765) and runs EnsureInstanceID
|
|
284
|
+
// (internal/store/instance.go:15-79): the data dir, `.instance-id.lock` (waits
|
|
285
|
+
// up to 60s, internal/store/migration_lock.go:29-76) and `.instance-id`. It
|
|
286
|
+
// returns (main.go:767-775) before orphan migration (main.go:779) and opens no
|
|
287
|
+
// database. The timeout stays below that lock wait: a held lock means a
|
|
288
|
+
// concurrent creator, and memory then stays off for the launch.
|
|
289
|
+
const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
|
|
290
|
+
|
|
291
|
+
// A fresh launch's store identity, provisioned by Shell itself with the verified
|
|
292
|
+
// core before Pi starts, then recorded. False when any step fails.
|
|
293
|
+
function provisionEngramStore() {
|
|
294
|
+
const core = verifiedBundledCore();
|
|
295
|
+
if (core === undefined || observeEngramStore().store !== "absent") return false;
|
|
296
|
+
let target;
|
|
297
|
+
try {
|
|
298
|
+
target = join(realpathSync(homedir()), ".engram");
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
// The provider's own environment, except for this one invocation: no update
|
|
303
|
+
// check and no GitHub tokens. ENGRAM_DATA_DIR is never set here.
|
|
304
|
+
const env = { ...process.env, ENGRAM_NO_UPDATE_CHECK: "1" };
|
|
305
|
+
delete env.GH_TOKEN;
|
|
306
|
+
delete env.GITHUB_TOKEN;
|
|
307
|
+
const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
308
|
+
const observed = observeEngramStore();
|
|
309
|
+
if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
|
|
310
|
+
String(result.stdout).trim() !== observed.instanceId) return false;
|
|
311
|
+
return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeEngramProvenance(record) {
|
|
315
|
+
try {
|
|
316
|
+
const path = engramProvenancePath(bundledInstall.root, process.platform);
|
|
317
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
318
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
319
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
320
|
+
renameSync(temporary, path);
|
|
321
|
+
return true;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Use Pi's supported configuration-only install command, never an alternate
|
|
329
|
+
// extension loader. Verify advertised enforcement before trusting the policy.
|
|
330
|
+
async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
|
|
331
|
+
const metadata = join(home.dir, "npm", "node_modules", "gentle-engram", "package.json");
|
|
332
|
+
let packages;
|
|
333
|
+
try {
|
|
334
|
+
packages = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages ?? [];
|
|
335
|
+
} catch {
|
|
336
|
+
return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from malformed settings; repair settings and recheck" };
|
|
337
|
+
}
|
|
338
|
+
if (!Array.isArray(packages)) return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from non-array packages" };
|
|
339
|
+
let pinned = false;
|
|
340
|
+
let local = false;
|
|
341
|
+
let npmSelected = false;
|
|
342
|
+
for (const entry of packages) {
|
|
343
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
344
|
+
if (source === ENGRAM_COMPANION_SOURCE) pinned = true;
|
|
345
|
+
if (typeof source === "string" && /^npm:gentle-engram(?:@|$)/.test(source)) npmSelected = true;
|
|
346
|
+
if (typeof source !== "string" || source.startsWith("npm:")) continue;
|
|
347
|
+
// Preserve an explicit local companion selection rather than overwrite it.
|
|
348
|
+
const selected = resolvePath(home.dir, source);
|
|
349
|
+
if (readPackageName(selected) !== "gentle-engram") continue;
|
|
350
|
+
if (!supportsEngramStartupPolicy(readJsonIfExists(join(selected, "package.json")))) {
|
|
351
|
+
return { ok: false, exitCode: 1, message: `gentle-shell: selected companion ${source} lacks engram.startupPolicy=1; update it and recheck (selection preserved)` };
|
|
352
|
+
}
|
|
353
|
+
local = true;
|
|
354
|
+
}
|
|
355
|
+
if (local && !npmSelected) return { ok: true };
|
|
356
|
+
if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true };
|
|
357
|
+
process.stderr.write(`gentle-shell: installing policy-capable companion ${ENGRAM_COMPANION_SOURCE} through Pi\n`);
|
|
358
|
+
const result = await spawnAndWait(runtime.command, [...runtime.args, "install", ENGRAM_COMPANION_SOURCE], buildSetupEnv(home, runtime), stdio, timeoutMs);
|
|
359
|
+
let declared = false;
|
|
360
|
+
try {
|
|
361
|
+
declared = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages?.some((entry) =>
|
|
362
|
+
(typeof entry === "string" ? entry : entry?.source) === ENGRAM_COMPANION_SOURCE) === true;
|
|
363
|
+
} catch { /* Missing or malformed readback cannot establish enforcement. */ }
|
|
364
|
+
if (!result.ok || result.timedOut || !declared || !supportsEngramStartupPolicy(readJsonIfExists(metadata))) {
|
|
365
|
+
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." };
|
|
366
|
+
}
|
|
367
|
+
return { ok: true };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// The bundled children's environment (bundledChildEnv), with the pinned Engram
|
|
371
|
+
// only once it is published: computed again right before Pi starts, so an
|
|
372
|
+
// Engram the automatic setup just published is used in the same launch.
|
|
373
|
+
function bundledProcessEnv() {
|
|
374
|
+
return bundledChildEnv(process.env, { node: process.execPath, cli: bundledCli, prefix: bundledInstall.root, engram: verifiedBundledCore(),
|
|
375
|
+
guard: ENGRAM_GUARD, memory: bundledMemory });
|
|
376
|
+
}
|
|
377
|
+
|
|
110
378
|
function ownPackageVersion() {
|
|
111
379
|
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
112
380
|
return packageJson.version;
|
|
@@ -162,6 +430,45 @@ function isDirectory(path) {
|
|
|
162
430
|
}
|
|
163
431
|
}
|
|
164
432
|
|
|
433
|
+
// Reuse Herdr's managed bridge, never its transport. This process-boundary
|
|
434
|
+
// lookup is deliberately best-effort and does not modify either agent home.
|
|
435
|
+
function managedHerdrExtensionArgs(home, args) {
|
|
436
|
+
const env = process.env;
|
|
437
|
+
if (home.mode !== "isolated" || args.piSubcommand !== undefined || args.passthrough[0] === "mcp") return [];
|
|
438
|
+
if (env.HERDR_ENV !== "1" || !env.HERDR_SOCKET_PATH?.trim() || !env.HERDR_PANE_ID?.trim()) return [];
|
|
439
|
+
if (env.GENTLE_PI_AGENTS_CHILD === "1" || !process.stdin.isTTY || !process.stdout.isTTY) return [];
|
|
440
|
+
// Only automatic interactive loading: a user opt-out must not become an
|
|
441
|
+
// explicit -e (which Pi loads even under --no-extensions). Conservatively
|
|
442
|
+
// skip ambiguous mode flags too; --mode text alone still allows a TUI.
|
|
443
|
+
const forwarded = args.passthrough;
|
|
444
|
+
if (forwarded.some((arg) => ["--no-extensions", "-ne", "--print", "-p", "--export", "--list-models", "-v"].includes(arg))) return [];
|
|
445
|
+
if (forwarded.some((arg, i) => (arg === "--mode" && forwarded[i + 1] !== "text") || arg.startsWith("--mode="))) return [];
|
|
446
|
+
try {
|
|
447
|
+
if (!statSync(env.HERDR_SOCKET_PATH).isSocket()) return [];
|
|
448
|
+
} catch {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
// Prefer a bridge in the selected home over adding a competing copy; keep
|
|
452
|
+
// the incoming Pi home override before falling back to Herdr's usual home.
|
|
453
|
+
const agentHomes = [home.dir, env.PI_CODING_AGENT_DIR, join(homedir(), ".pi", "agent")];
|
|
454
|
+
for (const agentHome of agentHomes) {
|
|
455
|
+
if (!agentHome) continue;
|
|
456
|
+
const bridge = join(agentHome, "extensions", "herdr-agent-state.ts");
|
|
457
|
+
try {
|
|
458
|
+
if (!statSync(bridge).isFile()) continue;
|
|
459
|
+
accessSync(bridge, fsConstants.R_OK);
|
|
460
|
+
// Pi's package-manager.toResolvedPaths and resource-loader.mergePaths
|
|
461
|
+
// dedupe canonical files across discovery, explicit -e and manifests.
|
|
462
|
+
// Existence alone is NOT proof of loading: declare the resource and
|
|
463
|
+
// let that resolver dedupe it, including explicit aliases from argv.
|
|
464
|
+
return ["-e", realpathSync(bridge)];
|
|
465
|
+
} catch {
|
|
466
|
+
// An absent/unreadable bridge never prevents the ordinary launch.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
|
|
165
472
|
// Real-fs adapter for discoverLooseExtensionEntries (lib/gentle-shell-launcher.ts):
|
|
166
473
|
// statSync-based isFile/isDirectory (not readdirSync's Dirent, which uses
|
|
167
474
|
// lstat and so would treat a symlinked file or directory as neither) so a
|
|
@@ -254,9 +561,46 @@ function readRawConfig(configPath) {
|
|
|
254
561
|
function writeRawConfig(configPath, config) {
|
|
255
562
|
const configDir = dirname(configPath);
|
|
256
563
|
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
564
|
+
writeFileAtomically(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// The one atomic write every launcher-owned config/settings update goes
|
|
568
|
+
// through: a temp file next to the real target, then a rename onto it, so a
|
|
569
|
+
// crash or kill never leaves a partial file. When `path` is a symlink (Nix
|
|
570
|
+
// home-manager, stow, and similar dotfile managers ship settings.json and
|
|
571
|
+
// config.json that way), the write lands on the link's real target and the
|
|
572
|
+
// link itself stays in place; renaming onto `path` would replace the link
|
|
573
|
+
// with a regular file. A missing path or a dangling link has no real target
|
|
574
|
+
// to preserve, so it is written at `path` itself, exactly as before this
|
|
575
|
+
// helper existed, instead of creating a file wherever a dangling link points.
|
|
576
|
+
// The permission bits are `mode` when given, otherwise the real target's own
|
|
577
|
+
// (applied with chmod, so the umask cannot narrow them); a new file keeps
|
|
578
|
+
// the default creation mode. The temp file is removed when the write or
|
|
579
|
+
// rename throws, and the error propagates to the caller's own handling.
|
|
580
|
+
function writeFileAtomically(path, data, { mode } = {}) {
|
|
581
|
+
let target = path;
|
|
582
|
+
try {
|
|
583
|
+
target = realpathSync(path);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (error.code !== "ENOENT") throw error;
|
|
586
|
+
}
|
|
587
|
+
let targetMode = mode;
|
|
588
|
+
if (targetMode === undefined) {
|
|
589
|
+
try {
|
|
590
|
+
targetMode = statSync(target).mode & 0o777;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (error.code !== "ENOENT") throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const tempPath = join(dirname(target), `.${basenameOf(target)}.gentle-shell-${process.pid}.tmp`);
|
|
596
|
+
try {
|
|
597
|
+
writeFileSync(tempPath, data);
|
|
598
|
+
if (targetMode !== undefined) chmodSync(tempPath, targetMode);
|
|
599
|
+
renameSync(tempPath, target);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
rmSync(tempPath, { force: true });
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
260
604
|
}
|
|
261
605
|
|
|
262
606
|
function loadConfig() {
|
|
@@ -453,15 +797,50 @@ function ensurePackageLocalGentleAi(binaryPath, pinnedVersion, stdio) {
|
|
|
453
797
|
// home, and the resolved pi runtime's directory is prepended to PATH so
|
|
454
798
|
// gentle-ai's (or pi's own) preflight finds `pi` even when it is bundled or
|
|
455
799
|
// given through GENTLE_SHELL_PI.
|
|
456
|
-
function buildSetupEnv(home, runtime) {
|
|
800
|
+
function buildSetupEnv(home, runtime, piLauncherDir) {
|
|
801
|
+
// Bundled: our node, then the version's `pi`, ahead of the user's PATH, and
|
|
802
|
+
// Gentle AI's state, persona and Engram inside the prefix (S9).
|
|
803
|
+
if (bundledEnv) {
|
|
804
|
+
const env = bundledSetupEnv({ ...bundledEnv, PI_CODING_AGENT_DIR: home.dir, GENTLE_PI_AGENT_HOME: home.dir }, { dirs: bundledSetupDirs, platform: process.platform });
|
|
805
|
+
return gentleAiIsolationEnv(env, { packageRoot });
|
|
806
|
+
}
|
|
457
807
|
return {
|
|
458
808
|
...process.env,
|
|
459
809
|
PI_CODING_AGENT_DIR: home.dir,
|
|
460
810
|
GENTLE_PI_AGENT_HOME: home.dir,
|
|
461
|
-
PATH:
|
|
811
|
+
PATH: [...(piLauncherDir ? [piLauncherDir] : []), dirname(runtime.command), process.env.PATH ?? ""].join(delimiter),
|
|
462
812
|
};
|
|
463
813
|
}
|
|
464
814
|
|
|
815
|
+
// Gentle AI requires a `pi` on its PATH and runs `pi install npm:gentle-pi`
|
|
816
|
+
// with it. A Pi gentle-shell resolved elsewhere (the adjacent package, run
|
|
817
|
+
// as `node <cli>`, or GENTLE_SHELL_PI) is not that `pi`, so a first launch
|
|
818
|
+
// from a terminal without one failed every time. This writes a private,
|
|
819
|
+
// temporary `pi` that runs exactly the resolved runtime; null when the
|
|
820
|
+
// runtime already is the `pi` on PATH (or for a bundled install, whose
|
|
821
|
+
// setup PATH already starts with the version's node_modules/.bin).
|
|
822
|
+
function writeSetupPiLauncher(runtime) {
|
|
823
|
+
if (bundledEnv || runtime.kind === "path") return null;
|
|
824
|
+
let dir;
|
|
825
|
+
try {
|
|
826
|
+
dir = mkdtempSync(join(tmpdir(), "gentle-shell-pi-"));
|
|
827
|
+
} catch {
|
|
828
|
+
// No usable temp folder: setup runs as it did before, without this `pi`.
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
if (process.platform === "win32") {
|
|
832
|
+
// Batch expands % even inside quotes; double it so a path stays literal.
|
|
833
|
+
const quote = (value) => `"${value.replaceAll("%", "%%")}"`;
|
|
834
|
+
// A batch runtime needs `call`, or control never returns for `exit /b`.
|
|
835
|
+
const call = /\.(?:cmd|bat)$/i.test(runtime.command) ? "call " : "";
|
|
836
|
+
writeFileSync(join(dir, "pi.cmd"), `@echo off\r\n${call}${[runtime.command, ...runtime.args].map(quote).join(" ")} %*\r\nexit /b %errorlevel%\r\n`);
|
|
837
|
+
} else {
|
|
838
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
839
|
+
writeFileSync(join(dir, "pi"), `#!/bin/sh\nexec ${[runtime.command, ...runtime.args].map(quote).join(" ")} "$@"\n`, { mode: 0o700 });
|
|
840
|
+
}
|
|
841
|
+
return dir;
|
|
842
|
+
}
|
|
843
|
+
|
|
465
844
|
// The shared Pi persona file gentle-ai writes on every install, regardless
|
|
466
845
|
// of the target home: its own PiPersonaConfigPath always resolves against
|
|
467
846
|
// the OS home, never PI_CODING_AGENT_DIR (gentle-ai internal/components/persona/inject.go),
|
|
@@ -473,6 +852,29 @@ function sharedPersonaPath() {
|
|
|
473
852
|
return join(homedir(), ".pi", "gentle-ai", "persona.json");
|
|
474
853
|
}
|
|
475
854
|
|
|
855
|
+
// The persona files a setup run may rewrite. Bundled (S9): the prefix's own,
|
|
856
|
+
// which a Gentle AI with GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and
|
|
857
|
+
// still the user's, which a pinned Gentle AI without it writes; restoring an
|
|
858
|
+
// unchanged file does nothing.
|
|
859
|
+
function setupPersonaPaths() {
|
|
860
|
+
if (!bundledInstall) return [sharedPersonaPath()];
|
|
861
|
+
return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Bundled setup (S9): the pinned Engram GENTLE_AI_ENGRAM_BIN names, published
|
|
865
|
+
// once in runtime/ (verified, never brew or the user's PATH). A failure stops
|
|
866
|
+
// setup rather than let Gentle AI acquire another Engram.
|
|
867
|
+
async function ensureBundledEngram() {
|
|
868
|
+
const { ensureEngram, prefixLayout } = await import("../scripts/bundled-install.mjs");
|
|
869
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundledInstall.root });
|
|
870
|
+
try {
|
|
871
|
+
await ensureEngram({ layout, platform: process.platform, arch: process.arch, env: process.env });
|
|
872
|
+
return { ok: true };
|
|
873
|
+
} catch (error) {
|
|
874
|
+
return { ok: false, exitCode: 1, message: `gentle-shell: could not install the pinned Engram in ${layout.engramDir} (${error.message})` };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
476
878
|
// Records `path`'s current state before a child process that might rewrite
|
|
477
879
|
// it runs: whether it exists, and if so its exact bytes and mode. Returns
|
|
478
880
|
// `{ path, existed: false }` for a missing file so restoreFile below knows
|
|
@@ -512,9 +914,7 @@ function restoreFile(snapshot) {
|
|
|
512
914
|
}
|
|
513
915
|
if (currentBytes !== undefined && currentBytes.equals(snapshot.bytes)) return false;
|
|
514
916
|
mkdirSync(dirname(path), { recursive: true });
|
|
515
|
-
|
|
516
|
-
writeFileSync(tempPath, snapshot.bytes, { mode: snapshot.mode });
|
|
517
|
-
renameSync(tempPath, path);
|
|
917
|
+
writeFileAtomically(path, snapshot.bytes, { mode: snapshot.mode });
|
|
518
918
|
return true;
|
|
519
919
|
}
|
|
520
920
|
|
|
@@ -574,10 +974,7 @@ function restoreManagedAssetDigestField(path, originalText) {
|
|
|
574
974
|
if (currentText === undefined) return false;
|
|
575
975
|
const restoredText = restoreJsonField(originalText, currentText, MANAGED_ASSET_DIGEST_FIELD);
|
|
576
976
|
if (restoredText === undefined) return false;
|
|
577
|
-
|
|
578
|
-
const tempPath = join(dirname(path), `.${basenameOf(path)}.gentle-shell-restore-${process.pid}.tmp`);
|
|
579
|
-
writeFileSync(tempPath, restoredText, { mode });
|
|
580
|
-
renameSync(tempPath, path);
|
|
977
|
+
writeFileAtomically(path, restoredText);
|
|
581
978
|
return true;
|
|
582
979
|
}
|
|
583
980
|
|
|
@@ -618,10 +1015,7 @@ function enforceDefaultThemeField(settingsPath, originalSettingsText) {
|
|
|
618
1015
|
? restoreJsonField(originalSettingsText, currentText, "theme")
|
|
619
1016
|
: forceJsonFieldIfAbsentInOriginal(originalSettingsText, currentText, "theme", DEFAULT_THEME_NAME);
|
|
620
1017
|
if (newText === undefined) return false;
|
|
621
|
-
|
|
622
|
-
const tempPath = join(dirname(settingsPath), `.${basenameOf(settingsPath)}.gentle-shell-restore-${process.pid}.tmp`);
|
|
623
|
-
writeFileSync(tempPath, newText, { mode });
|
|
624
|
-
renameSync(tempPath, settingsPath);
|
|
1018
|
+
writeFileAtomically(settingsPath, newText);
|
|
625
1019
|
return originalHadTheme ? "restored" : "forced";
|
|
626
1020
|
}
|
|
627
1021
|
|
|
@@ -641,6 +1035,53 @@ function safely(label, path, fallback, fn) {
|
|
|
641
1035
|
}
|
|
642
1036
|
}
|
|
643
1037
|
|
|
1038
|
+
// gentle-pi replaces Pi's replaceable builtin codemode with its own decorated
|
|
1039
|
+
// codemode tool (extensions/quiet-tools.ts -> registerCompactCodemode in
|
|
1040
|
+
// lib/codemode-renderer.ts), and Pi prints a startup warning whenever a
|
|
1041
|
+
// builtin loses its tool to another extension. Pi's only per-builtin opt-out
|
|
1042
|
+
// is a `-builtin:<name>` entry in the settings `extensions` array, so every
|
|
1043
|
+
// normal launch ensures that entry in the settings.json of a home
|
|
1044
|
+
// gentle-shell owns (see main, below).
|
|
1045
|
+
const BUILTIN_CODEMODE_EXTENSION = "builtin:codemode";
|
|
1046
|
+
|
|
1047
|
+
// Pure: returns `settingsText` with `-<builtin>` appended to its `extensions`
|
|
1048
|
+
// array (created when absent), or undefined when nothing should change — the
|
|
1049
|
+
// text does not parse as a JSON object, `extensions` exists but is not an
|
|
1050
|
+
// array, or the array already holds any explicit entry for the builtin
|
|
1051
|
+
// (`+`, `-`, `!`, or bare), which is the user's own decision. Keeps every
|
|
1052
|
+
// other key and entry in place, plus the text's own indentation and trailing
|
|
1053
|
+
// newline (same detection as detectJsonFormatting in
|
|
1054
|
+
// lib/gentle-shell-launcher.ts).
|
|
1055
|
+
function withBuiltinExtensionExcluded(settingsText, builtin) {
|
|
1056
|
+
let settings;
|
|
1057
|
+
try {
|
|
1058
|
+
settings = JSON.parse(settingsText);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return undefined;
|
|
1063
|
+
const extensions = Object.prototype.hasOwnProperty.call(settings, "extensions") ? settings.extensions : [];
|
|
1064
|
+
if (!Array.isArray(extensions)) return undefined;
|
|
1065
|
+
if (extensions.some((entry) => typeof entry === "string" && entry.replace(/^[+!-]/, "") === builtin)) return undefined;
|
|
1066
|
+
const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1];
|
|
1067
|
+
const serialized = JSON.stringify({ ...settings, extensions: [...extensions, `-${builtin}`] }, null, indent);
|
|
1068
|
+
return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Applies withBuiltinExtensionExcluded to an existing settings.json with the
|
|
1072
|
+
// same atomic temp-file-then-rename write as the restores above. A missing
|
|
1073
|
+
// settings.json is left missing: only the brand-new-home bootstrap
|
|
1074
|
+
// (installIsolatedTuiModeSetting) seeds that file, and this launch-time step
|
|
1075
|
+
// never takes over that role. Returns true when it actually wrote the file.
|
|
1076
|
+
function ensureBuiltinCodemodeExcluded(settingsPath) {
|
|
1077
|
+
const currentText = readJsonIfExists(settingsPath);
|
|
1078
|
+
if (currentText === undefined) return false;
|
|
1079
|
+
const newText = withBuiltinExtensionExcluded(currentText, BUILTIN_CODEMODE_EXTENSION);
|
|
1080
|
+
if (newText === undefined) return false;
|
|
1081
|
+
writeFileAtomically(settingsPath, newText);
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
644
1085
|
// Provisions `home` with everything `gentle-ai install --agent pi` installs
|
|
645
1086
|
// into a regular Pi, by spawning the package-local pinned gentle-ai binary
|
|
646
1087
|
// (never a PATH `gentle-ai`) with PI_CODING_AGENT_DIR/GENTLE_PI_AGENT_HOME set
|
|
@@ -682,10 +1123,24 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
682
1123
|
|
|
683
1124
|
process.stderr.write(`gentle-shell: provisioning ${home.dir} with the gentle-ai companion packages\n`);
|
|
684
1125
|
|
|
1126
|
+
if (bundledInstall && !dryRun) {
|
|
1127
|
+
const engram = await ensureBundledEngram();
|
|
1128
|
+
if (!engram.ok) return engram;
|
|
1129
|
+
}
|
|
1130
|
+
// The memory decision never skips setup: it only selects the Engram
|
|
1131
|
+
// environment (core or guard, policy, ENGRAM_URL) setup's children and the
|
|
1132
|
+
// launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
|
|
1133
|
+
// no store. Reclassified now that the verified core may just be published.
|
|
1134
|
+
if (bundledInstall) {
|
|
1135
|
+
bundledMemory = await classifyBundledMemory();
|
|
1136
|
+
bundledEnv = bundledProcessEnv();
|
|
1137
|
+
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
685
1140
|
const setupArgs = ["install", "--agent", "pi", "--scope", "global", ...(dryRun ? ["--dry-run"] : [])];
|
|
686
|
-
const
|
|
687
|
-
const
|
|
688
|
-
const
|
|
1141
|
+
const piLauncherDir = writeSetupPiLauncher(runtime);
|
|
1142
|
+
const env = buildSetupEnv(home, runtime, piLauncherDir);
|
|
1143
|
+
const personaSnapshots = setupPersonaPaths().map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
|
|
689
1144
|
const statePath = sharedGentleAiStatePath();
|
|
690
1145
|
const originalStateText = safely("read your Gentle AI state file", statePath, undefined, () => readParsableJsonText(statePath));
|
|
691
1146
|
// Theme restore (never for --link — that home is the user's own
|
|
@@ -702,8 +1157,11 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
702
1157
|
try {
|
|
703
1158
|
installResult = await spawnAndWait(binaryPath, setupArgs, env, stdio, timeoutMs);
|
|
704
1159
|
} finally {
|
|
705
|
-
if (
|
|
706
|
-
|
|
1160
|
+
if (piLauncherDir) rmSync(piLauncherDir, { recursive: true, force: true });
|
|
1161
|
+
for (const [personaPath, personaSnapshot] of personaSnapshots) {
|
|
1162
|
+
if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
|
|
1163
|
+
process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
|
|
1164
|
+
}
|
|
707
1165
|
}
|
|
708
1166
|
if (safely("restore your Gentle AI managed-asset record", statePath, false, () => restoreManagedAssetDigestField(statePath, originalStateText))) {
|
|
709
1167
|
process.stderr.write(
|
|
@@ -725,7 +1183,63 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
|
|
|
725
1183
|
}
|
|
726
1184
|
if (!installResult.ok) return installResult;
|
|
727
1185
|
|
|
728
|
-
|
|
1186
|
+
const cleanupResult = await runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
|
|
1187
|
+
if (cleanupResult.ok && !dryRun) {
|
|
1188
|
+
warnDisabledMCP(home);
|
|
1189
|
+
// The guard and policy protect memory, so the companion check only warns.
|
|
1190
|
+
const companion = bundledInstall ? await ensureEngramCompanion(home, runtime, stdio, timeoutMs) : { ok: true };
|
|
1191
|
+
if (!companion.ok) process.stderr.write(`${companion.message}\n`);
|
|
1192
|
+
}
|
|
1193
|
+
return cleanupResult;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function mcpAdapterActivity(packages) {
|
|
1197
|
+
if (typeof packages === "string") return packages === "npm:pi-mcp-adapter" || packages.startsWith("npm:pi-mcp-adapter@") ? "active" : "inactive";
|
|
1198
|
+
if (Array.isArray(packages)) {
|
|
1199
|
+
const activities = packages.map(mcpAdapterActivity);
|
|
1200
|
+
return activities.includes("active") ? "active" : activities.includes("unknown") ? "unknown" : "inactive";
|
|
1201
|
+
}
|
|
1202
|
+
if (packages === null || typeof packages !== "object") return "inactive";
|
|
1203
|
+
if (typeof packages.source !== "string") return mcpAdapterActivity(Object.keys(packages));
|
|
1204
|
+
if (mcpAdapterActivity(packages.source) === "inactive") return "inactive";
|
|
1205
|
+
const patterns = packages.extensions;
|
|
1206
|
+
if (patterns === undefined) return packages.autoload === false ? "inactive" : "active";
|
|
1207
|
+
if (!Array.isArray(patterns)) return "unknown";
|
|
1208
|
+
if (patterns.length === 0) return "inactive";
|
|
1209
|
+
if (packages.autoload === false) {
|
|
1210
|
+
return patterns.some(pattern => typeof pattern === "string" && !pattern.startsWith("!") && !pattern.startsWith("-")) ? "unknown" : "inactive";
|
|
1211
|
+
}
|
|
1212
|
+
// Ordinary exclusions run after includes, but exact +path entries can
|
|
1213
|
+
// restore resources. Other filters need actual package files to resolve.
|
|
1214
|
+
if (patterns.includes("!**") && !patterns.some(pattern => typeof pattern === "string" && pattern.startsWith("+"))) return "inactive";
|
|
1215
|
+
return "unknown";
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Onboarding cannot prove who disabled MCP: diagnose, never re-enable servers.
|
|
1219
|
+
// This also covers automatic provisioning because both routes share setup.
|
|
1220
|
+
function warnDisabledMCP(home) {
|
|
1221
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1222
|
+
const mcpPath = join(home.dir, "mcp.json");
|
|
1223
|
+
let inspecting = settingsPath;
|
|
1224
|
+
try {
|
|
1225
|
+
const settingsText = readJsonIfExists(settingsPath);
|
|
1226
|
+
if (settingsText === undefined) return;
|
|
1227
|
+
const settings = JSON.parse(settingsText);
|
|
1228
|
+
if (!Array.isArray(settings?.extensions) || !settings.extensions.includes("-builtin:mcp")) return;
|
|
1229
|
+
const activity = mcpAdapterActivity(settings.packages);
|
|
1230
|
+
if (activity === "active") return;
|
|
1231
|
+
inspecting = mcpPath;
|
|
1232
|
+
const mcpText = readJsonIfExists(mcpPath);
|
|
1233
|
+
if (mcpText === undefined) return;
|
|
1234
|
+
const servers = JSON.parse(mcpText)?.mcpServers;
|
|
1235
|
+
if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length === 0) return;
|
|
1236
|
+
const diagnosis = activity === "unknown"
|
|
1237
|
+
? `pi-mcp-adapter activity cannot be confirmed from its extension filters, so servers in ${mcpPath} may not load`
|
|
1238
|
+
: `pi-mcp-adapter is absent or inactive, so servers in ${mcpPath} will not load`;
|
|
1239
|
+
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`);
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
process.stderr.write(`gentle-shell: WARNING: Pi MCP configuration could not be inspected in ${inspecting}: ${error.message}; inspect or repair it manually.\n`);
|
|
1242
|
+
}
|
|
729
1243
|
}
|
|
730
1244
|
|
|
731
1245
|
// The stderr line printed once `source` is actually removed from `home`.
|
|
@@ -805,7 +1319,11 @@ async function removePostInstallSources(sources, index, home, runtime, stdio, ti
|
|
|
805
1319
|
// --dry-run, runs the shared flow with the child's stdio inherited (today's
|
|
806
1320
|
// behavior, unchanged), then exits with its result — this is the one place
|
|
807
1321
|
// that keeps the pre-S7 exit semantics `handleSetupCommand` always had.
|
|
808
|
-
|
|
1322
|
+
// A successful real run records the home exactly as auto-provisioning does
|
|
1323
|
+
// (#2106: both installers finish with this command, and the first launch
|
|
1324
|
+
// must not install the packages again), but only for a home auto-provisioning
|
|
1325
|
+
// itself would own: never pi's own default home or an unrelated directory.
|
|
1326
|
+
async function handleSetupCommand(commandArgs, home, runtime, { homeHadContentBeforeBootstrap }) {
|
|
809
1327
|
let dryRun = false;
|
|
810
1328
|
for (const arg of commandArgs) {
|
|
811
1329
|
if (arg === "--dry-run") {
|
|
@@ -817,6 +1335,19 @@ async function handleSetupCommand(commandArgs, home, runtime) {
|
|
|
817
1335
|
|
|
818
1336
|
const result = await runSetupFlow(home, runtime, { dryRun, stdio: "inherit" });
|
|
819
1337
|
if (!result.ok && result.message !== undefined) process.stderr.write(`${result.message}\n`);
|
|
1338
|
+
if (result.ok && !dryRun && (home.mode === "isolated" || home.mode === "path")) {
|
|
1339
|
+
const configPath = resolveConfigPath();
|
|
1340
|
+
const homeKey = safeRealpath(home.dir);
|
|
1341
|
+
const config = readRawConfig(configPath);
|
|
1342
|
+
if (!homeIsForeign(home, provisionedEntry(config, homeKey), homeHadContentBeforeBootstrap)) {
|
|
1343
|
+
try {
|
|
1344
|
+
writeRawConfig(configPath, recordProvisioned(config, homeKey, resolveSetupGentleAiPin(), ownPackageVersion(), new Date().toISOString()));
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
// Setup itself succeeded; the next launch only sets the home up again.
|
|
1347
|
+
process.stderr.write(`gentle-shell: could not record the setup in ${configPath} (${error.message})\n`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
820
1351
|
process.exit(result.exitCode);
|
|
821
1352
|
}
|
|
822
1353
|
|
|
@@ -1043,29 +1574,268 @@ async function maybeAutoProvisionHome(home, runtime, { homeHadContentBeforeBoots
|
|
|
1043
1574
|
}
|
|
1044
1575
|
}
|
|
1045
1576
|
|
|
1577
|
+
// The adapters `upgrade` and `self-uninstall` run npm and pnpm with: { fs, which, run, invocation? }.
|
|
1578
|
+
async function packageManagerAdapters() {
|
|
1579
|
+
const { hostAdapters } = await import("../scripts/installer-probes.mjs");
|
|
1580
|
+
const { upgradeInvocation } = await import("../scripts/installer-runner.mjs");
|
|
1581
|
+
const { run, fs: probeFs } = hostAdapters();
|
|
1582
|
+
// Windows: npm and pnpm are .cmd shims; run what they run, never through cmd.exe.
|
|
1583
|
+
const invocation = upgradeInvocation({ platform: process.platform, env: process.env, run, fs: probeFs });
|
|
1584
|
+
return {
|
|
1585
|
+
fs: await import("node:fs/promises"),
|
|
1586
|
+
which: async (name) => findOnPath(name) ?? null,
|
|
1587
|
+
...(invocation ? { invocation } : {}),
|
|
1588
|
+
run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000 }),
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// `gentle-shell upgrade`: runs before any Pi runtime check, since it may replace this package.
|
|
1593
|
+
// On release, a global pnpm or npm install moves to the bundled one when the
|
|
1594
|
+
// latest release publishes its assets (S4), with one question about the old package.
|
|
1595
|
+
async function handleUpgradeCommand(commandArgs) {
|
|
1596
|
+
const { MainChannelError, runUpgrade } = await import("../scripts/main-channel.mjs");
|
|
1597
|
+
const { fetchDistribution, migrateGlobalShell, windowsPathRegistry } = await import("../scripts/bundled-wizard.mjs");
|
|
1598
|
+
const { askLine } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1599
|
+
const managers = await packageManagerAdapters();
|
|
1600
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
1601
|
+
const migrate = ({ latest, owner }) => migrateGlobalShell({
|
|
1602
|
+
latest,
|
|
1603
|
+
currentVersion: ownPackageVersion(),
|
|
1604
|
+
platform: process.platform,
|
|
1605
|
+
arch: process.arch,
|
|
1606
|
+
env: process.env,
|
|
1607
|
+
home: homedir(),
|
|
1608
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
1609
|
+
// End of input or Ctrl+C answers No.
|
|
1610
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }).catch(() => ""),
|
|
1611
|
+
out,
|
|
1612
|
+
adapters: { owner, distribution: (version) => fetchDistribution({ version }), run: managers.run,
|
|
1613
|
+
...(process.platform === "win32" ? { registry: windowsPathRegistry(process.env) } : {}) },
|
|
1614
|
+
});
|
|
1615
|
+
try {
|
|
1616
|
+
process.exitCode = await runUpgrade({
|
|
1617
|
+
args: commandArgs,
|
|
1618
|
+
ctx: { env: process.env, home: homedir() },
|
|
1619
|
+
platform: process.platform,
|
|
1620
|
+
packageRoot,
|
|
1621
|
+
currentVersion: ownPackageVersion(),
|
|
1622
|
+
adapters: { fetch: globalThis.fetch, ...managers, migrate },
|
|
1623
|
+
out,
|
|
1624
|
+
});
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (error instanceof MainChannelError) fail(`gentle-shell upgrade: ${error.message}`, error.code === "upgrade-usage" ? 2 : 1);
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// `gentle-shell upgrade` in a bundled install: the latest release installed
|
|
1632
|
+
// beside the active version, verified, then `current` switched (two versions
|
|
1633
|
+
// kept), or `--rollback` to the kept one. Returns the exit code (a failure is
|
|
1634
|
+
// printed, not thrown, so `update --all` can go on); a failure leaves `current`
|
|
1635
|
+
// as it was.
|
|
1636
|
+
async function handleBundledUpgrade(bundled, commandArgs) {
|
|
1637
|
+
const parsed = parseBundledUpgradeArgs(commandArgs);
|
|
1638
|
+
if ("error" in parsed) fail(`gentle-shell upgrade: ${parsed.error}`, parsed.usage ? 2 : 1);
|
|
1639
|
+
const { prefixLayout, rollbackBundled, upgradeBundled } = await import("../scripts/bundled-install.mjs");
|
|
1640
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1641
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
1642
|
+
try {
|
|
1643
|
+
if (parsed.action === "rollback") {
|
|
1644
|
+
const { from, to } = await rollbackBundled({ layout, home: homedir(), platform: process.platform, arch: process.arch, env: process.env });
|
|
1645
|
+
out(`Switched Gentle Shell from ${from} to ${to}.`);
|
|
1646
|
+
return 0;
|
|
1647
|
+
}
|
|
1648
|
+
const { fetchDistribution } = await import("../scripts/bundled-wizard.mjs");
|
|
1649
|
+
const { latestRelease } = await import("../scripts/main-channel.mjs");
|
|
1650
|
+
await upgradeBundled({
|
|
1651
|
+
layout,
|
|
1652
|
+
home: homedir(),
|
|
1653
|
+
platform: process.platform,
|
|
1654
|
+
arch: process.arch,
|
|
1655
|
+
env: process.env,
|
|
1656
|
+
adapters: { latest: () => latestRelease(globalThis.fetch), distribution: (version) => fetchDistribution({ version }) },
|
|
1657
|
+
out,
|
|
1658
|
+
});
|
|
1659
|
+
return 0;
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
process.stderr.write(`gentle-shell upgrade: ${error.message}${error.stderr ? `\n${error.stderr}` : ""}\n`);
|
|
1662
|
+
return 1;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// The bundled npm as the `npmCommand` of a home Gentle Shell owns (the isolated
|
|
1667
|
+
// home, or a --home it created or provisioned; never --link or a foreign home),
|
|
1668
|
+
// so Pi installs packages with our npm while its tools keep the user's PATH.
|
|
1669
|
+
// Only that key is written, and only when it differs.
|
|
1670
|
+
async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap) {
|
|
1671
|
+
if (home.mode === "link") return;
|
|
1672
|
+
const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
|
|
1673
|
+
if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
|
|
1674
|
+
const { bundledNpmCommand, ensureNpmShim, prefixLayout } = await import("../scripts/bundled-install.mjs");
|
|
1675
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1676
|
+
// The entry npmCommand names, also for a runtime installed before it existed.
|
|
1677
|
+
ensureNpmShim(layout);
|
|
1678
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1679
|
+
const edit = withBundledNpmCommand(readJsonIfExists(settingsPath), bundledNpmCommand(layout));
|
|
1680
|
+
if (edit.status === "malformed") {
|
|
1681
|
+
process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so Pi's package installs use the npm on your PATH\n`);
|
|
1682
|
+
} else if (edit.status === "write") {
|
|
1683
|
+
writeFileAtomically(settingsPath, edit.text);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
|
|
1688
|
+
// folders and exactly the PATH entry its receipt records; on Windows the running
|
|
1689
|
+
// node.exe and launcher are removed by a detached process after this one exits.
|
|
1690
|
+
async function handleBundledSelfUninstall(bundled, commandArgs) {
|
|
1691
|
+
const { askLine, runBundledSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1692
|
+
const { describePathReceipt, prefixLayout, readPathReceipt, removePathEntry, scheduleWindowsRemoval } = await import("../scripts/bundled-install.mjs");
|
|
1693
|
+
const windows = process.platform === "win32";
|
|
1694
|
+
const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
|
|
1695
|
+
// Trusted only for the link or startup file the PATH plan can choose for this user.
|
|
1696
|
+
const receipt = readPathReceipt(layout, { env: process.env, home: homedir() });
|
|
1697
|
+
let pathEntry = receipt;
|
|
1698
|
+
if (receipt.status === "recorded") {
|
|
1699
|
+
const registry = windows ? (await import("../scripts/bundled-wizard.mjs")).windowsPathRegistry(process.env) : undefined;
|
|
1700
|
+
pathEntry = { status: "recorded", description: describePathReceipt(receipt.receipt), remove: () => removePathEntry(receipt.receipt, { registry }) };
|
|
1701
|
+
}
|
|
1702
|
+
return runBundledSelfUninstall({
|
|
1703
|
+
args: commandArgs,
|
|
1704
|
+
env: process.env,
|
|
1705
|
+
homedir: homedir(),
|
|
1706
|
+
platform: process.platform,
|
|
1707
|
+
interactive: process.stdin.isTTY === true,
|
|
1708
|
+
root: bundled.root,
|
|
1709
|
+
pathEntry,
|
|
1710
|
+
process: { pid: process.pid, execPath: process.execPath },
|
|
1711
|
+
...(windows ? { deferRemoval: (request) => scheduleWindowsRemoval({ ...request, env: process.env }) } : {}),
|
|
1712
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
|
|
1713
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
1714
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// `gentle-shell self-uninstall`: like upgrade, runs before any Pi runtime check,
|
|
1719
|
+
// so it also works when pi is missing.
|
|
1720
|
+
async function handleSelfUninstallCommand(commandArgs) {
|
|
1721
|
+
const { askLine, runSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
|
|
1722
|
+
const { ownerManager } = await import("../scripts/main-channel.mjs");
|
|
1723
|
+
const managers = await packageManagerAdapters();
|
|
1724
|
+
const ctx = { env: process.env, home: homedir() };
|
|
1725
|
+
process.exitCode = await runSelfUninstall({
|
|
1726
|
+
args: commandArgs,
|
|
1727
|
+
env: process.env,
|
|
1728
|
+
homedir: homedir(),
|
|
1729
|
+
platform: process.platform,
|
|
1730
|
+
interactive: process.stdin.isTTY === true,
|
|
1731
|
+
resolveOwner: () => ownerManager({ ctx, platform: process.platform, packageRoot, adapters: managers, command: "uninstall" }),
|
|
1732
|
+
run: (command, argv) => managers.run(command, argv),
|
|
1733
|
+
ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
|
|
1734
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
1735
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Before Pi exists: only the bundled interactive opening may offer an update.
|
|
1740
|
+
async function startupUpdate(bundled, args, startupEnv) {
|
|
1741
|
+
if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
|
|
1742
|
+
inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
|
|
1743
|
+
const configPath = resolveConfigPath();
|
|
1744
|
+
let skipped;
|
|
1745
|
+
try { skipped = readRawConfig(configPath).skippedStartupRelease; } catch { /* A notice must not prevent opening. */ }
|
|
1746
|
+
const { latestRelease } = await import("../scripts/main-channel.mjs");
|
|
1747
|
+
const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
|
|
1748
|
+
lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })) });
|
|
1749
|
+
if (!offer) return undefined;
|
|
1750
|
+
let choice;
|
|
1751
|
+
try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
|
|
1752
|
+
catch { return undefined; }
|
|
1753
|
+
return applyStartupChoice(choice, {
|
|
1754
|
+
omit: () => omitStartupVersion(() => readJsonIfExists(configPath), (text) => {
|
|
1755
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1756
|
+
writeFileAtomically(configPath, text, { mode: 0o600 });
|
|
1757
|
+
}, offer.latest),
|
|
1758
|
+
warn: (message) => process.stderr.write(`gentle-shell: ${message}\n`),
|
|
1759
|
+
upgrade: () => handleBundledUpgrade(bundled, []),
|
|
1760
|
+
relaunch: async () => {
|
|
1761
|
+
const next = await resolveStartupRelaunch({ root: bundled.root, home: homedir(), platform: process.platform,
|
|
1762
|
+
env: process.env, argv: process.argv.slice(2) });
|
|
1763
|
+
return new Promise((resolve, reject) => {
|
|
1764
|
+
const child = spawn(next.command, next.args, { stdio: "inherit", env: next.env, shell: false });
|
|
1765
|
+
const forwards = ["SIGINT", "SIGTERM", "SIGHUP"].map((signal) => {
|
|
1766
|
+
const handler = () => child.kill(signal);
|
|
1767
|
+
process.on(signal, handler);
|
|
1768
|
+
return [signal, handler];
|
|
1769
|
+
});
|
|
1770
|
+
const cleanup = () => { for (const [signal, handler] of forwards) process.off(signal, handler); };
|
|
1771
|
+
child.once("error", (error) => { cleanup(); reject(error); });
|
|
1772
|
+
child.once("exit", (code, signal) => { cleanup(); resolve(signal ? signalExitCode(signal) : (code ?? 1)); });
|
|
1773
|
+
});
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1046
1778
|
async function main() {
|
|
1779
|
+
// One launch only: never leak the update handoff opt-out into Pi or its tools.
|
|
1780
|
+
const startupEnv = { ...process.env };
|
|
1781
|
+
delete process.env.GENTLE_SHELL_SKIP_STARTUP_UPDATE;
|
|
1047
1782
|
const args = parseLauncherArgs(process.argv.slice(2));
|
|
1048
1783
|
if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
|
|
1049
1784
|
if (args.help) {
|
|
1050
|
-
process.stdout.write(`${helpText()}\n`);
|
|
1785
|
+
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. Back up data, update Engram, restart its server and Shell, then recheck.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
|
|
1051
1786
|
process.exit(0);
|
|
1052
1787
|
}
|
|
1053
1788
|
if (args.command === "home") {
|
|
1054
1789
|
handleHomeCommand(args.commandArgs);
|
|
1055
1790
|
return;
|
|
1056
1791
|
}
|
|
1792
|
+
// The verification input applies to `--version` only (scripts/bundled-install.mjs upgradeBundled).
|
|
1793
|
+
const verifying = args.version && args.command === undefined ? process.env[BUNDLED_VERIFY_ENV] : undefined;
|
|
1794
|
+
const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });
|
|
1795
|
+
if (args.command === "upgrade") {
|
|
1796
|
+
if (bundled) process.exitCode = await handleBundledUpgrade(bundled, args.commandArgs);
|
|
1797
|
+
else await handleUpgradeCommand(args.commandArgs);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (args.command === "self-uninstall") {
|
|
1801
|
+
if (bundled) process.exitCode = await handleBundledSelfUninstall(bundled, args.commandArgs);
|
|
1802
|
+
else await handleSelfUninstallCommand(args.commandArgs);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const startupCode = await startupUpdate(bundled, args, startupEnv);
|
|
1807
|
+
if (startupCode !== undefined) {
|
|
1808
|
+
process.exitCode = startupCode;
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1057
1811
|
|
|
1058
1812
|
const config = loadConfig();
|
|
1059
1813
|
let home = resolveHome({ args, env: process.env, homedir: homedir(), config });
|
|
1060
1814
|
if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
|
|
1061
1815
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1816
|
+
// Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
|
|
1817
|
+
// ensure the companion, print memory notices or touch provenance.
|
|
1818
|
+
const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
|
|
1819
|
+
let runtime;
|
|
1820
|
+
if (bundled) {
|
|
1821
|
+
// S8: only the version's own Pi, never GENTLE_SHELL_PI, an adjacent fallback or PATH.
|
|
1822
|
+
const pi = bundledPi(bundled, { platform: process.platform, fs: bundledFs });
|
|
1823
|
+
if (!pi.ok) fail(pi.message, 1);
|
|
1824
|
+
runtime = { kind: "bundled", command: process.execPath, args: [pi.cli] };
|
|
1825
|
+
bundledInstall = bundled;
|
|
1826
|
+
bundledCli = pi.cli;
|
|
1827
|
+
bundledMemory = engramQuiet ? { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE } : await classifyBundledMemory();
|
|
1828
|
+
bundledEnv = bundledProcessEnv();
|
|
1829
|
+
bundledSetupDirs = [dirname(process.execPath), join(bundled.versionDir, "node_modules", ".bin")];
|
|
1830
|
+
} else {
|
|
1831
|
+
runtime = resolvePiRuntime({
|
|
1832
|
+
env: process.env,
|
|
1833
|
+
resolveBundledCli,
|
|
1834
|
+
findOnPath,
|
|
1835
|
+
nodeExecPath: process.execPath,
|
|
1836
|
+
});
|
|
1837
|
+
if (runtime === undefined) fail(missingPiMessage(), 1);
|
|
1838
|
+
}
|
|
1069
1839
|
|
|
1070
1840
|
const versionProbePlan = planSpawn({ command: runtime.command, args: [...runtime.args, "--version"], platform: process.platform });
|
|
1071
1841
|
const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {
|
|
@@ -1073,6 +1843,7 @@ async function main() {
|
|
|
1073
1843
|
timeout: 15000,
|
|
1074
1844
|
encoding: "utf8",
|
|
1075
1845
|
shell: versionProbePlan.shell,
|
|
1846
|
+
env: bundledEnv ?? process.env,
|
|
1076
1847
|
});
|
|
1077
1848
|
if (versionProbe.error) fail(`Could not run the pi runtime at "${runtime.command}": ${versionProbe.error.message}`, 1);
|
|
1078
1849
|
const versionCheck = checkPiVersion(versionProbe.stdout ?? "");
|
|
@@ -1083,6 +1854,23 @@ async function main() {
|
|
|
1083
1854
|
process.exit(0);
|
|
1084
1855
|
}
|
|
1085
1856
|
|
|
1857
|
+
// S5: in a bundled install Pi updates only with Gentle Shell, so Pi's own
|
|
1858
|
+
// update becomes `gentle-shell upgrade` (run when interactive, named
|
|
1859
|
+
// otherwise); package updates still reach Pi.
|
|
1860
|
+
let passthrough = args.passthrough;
|
|
1861
|
+
if (bundled && args.piSubcommand === "update") {
|
|
1862
|
+
const plan = bundledUpdatePlan(args.passthrough);
|
|
1863
|
+
if (plan.self) {
|
|
1864
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1865
|
+
for (const line of bundledUpdateNotice({ piVersion: versionCheck.version, interactive })) process.stdout.write(`${line}\n`);
|
|
1866
|
+
const code = interactive ? await handleBundledUpgrade(bundled, []) : undefined;
|
|
1867
|
+
const next = bundledUpdateNext(plan, code);
|
|
1868
|
+
if ("exitCode" in next) process.exit(next.exitCode);
|
|
1869
|
+
if (next.note) process.stderr.write(`${next.note}\n`);
|
|
1870
|
+
}
|
|
1871
|
+
passthrough = plan.piArgs;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1086
1874
|
// Home-ownership signal for auto-provisioning (S9, homeIsForeign): must be
|
|
1087
1875
|
// read before the isolated-home bootstrap below creates and seeds a
|
|
1088
1876
|
// brand-new --home directory with its own settings.json — after that
|
|
@@ -1100,14 +1888,24 @@ async function main() {
|
|
|
1100
1888
|
// Isolated-home bootstrap: only on a home gentle-shell has not seen before
|
|
1101
1889
|
// (link never bootstraps — it reuses the user's own pi agent home as-is).
|
|
1102
1890
|
if ((home.mode === "isolated" || home.mode === "path") && !existsSync(home.dir)) {
|
|
1103
|
-
|
|
1891
|
+
// 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
|
|
1892
|
+
// which the bundled install's prefix claim must tighten or refuse.
|
|
1893
|
+
mkdirSync(home.dir, { recursive: true, mode: 0o700 });
|
|
1104
1894
|
await installIsolatedTuiModeSetting(home.dir);
|
|
1105
1895
|
writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
|
|
1106
1896
|
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`);
|
|
1107
1897
|
}
|
|
1108
1898
|
|
|
1899
|
+
if (bundled) {
|
|
1900
|
+
try {
|
|
1901
|
+
await ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap);
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1109
1907
|
if (args.command === "setup") {
|
|
1110
|
-
await handleSetupCommand(args.commandArgs, home, runtime);
|
|
1908
|
+
await handleSetupCommand(args.commandArgs, home, runtime, { homeHadContentBeforeBootstrap });
|
|
1111
1909
|
return;
|
|
1112
1910
|
}
|
|
1113
1911
|
|
|
@@ -1139,6 +1937,21 @@ async function main() {
|
|
|
1139
1937
|
// exits with the same signal-derived code instead of falling through
|
|
1140
1938
|
// to launch pi.
|
|
1141
1939
|
if (autoProvisionResult !== undefined) process.exit(autoProvisionResult.exitCode);
|
|
1940
|
+
|
|
1941
|
+
// Runs after auto-provision, so it sees settings.json exactly as that
|
|
1942
|
+
// run left it. Only a home gentle-shell owns, by the same rule
|
|
1943
|
+
// auto-provisioning uses (homeIsForeign): never --link (excluded above),
|
|
1944
|
+
// a foreign --home, or pi's own default agent home, since plain pi may
|
|
1945
|
+
// share those and would lose its builtin codemode. `setup` (including
|
|
1946
|
+
// --dry-run) returned before this point.
|
|
1947
|
+
const settingsPath = join(home.dir, "settings.json");
|
|
1948
|
+
const excluded = safely("exclude Pi's builtin codemode in your Gentle Shell settings", settingsPath, false, () => {
|
|
1949
|
+
const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
|
|
1950
|
+
return !homeIsForeign(home, previous, homeHadContentBeforeBootstrap) && ensureBuiltinCodemodeExcluded(settingsPath);
|
|
1951
|
+
});
|
|
1952
|
+
if (excluded) {
|
|
1953
|
+
process.stderr.write(`gentle-shell: disabled Pi's builtin codemode in ${settingsPath} (Gentle Shell ships its own codemode tool)\n`);
|
|
1954
|
+
}
|
|
1142
1955
|
}
|
|
1143
1956
|
|
|
1144
1957
|
const packageRootExplicit = args.packageRoot !== undefined;
|
|
@@ -1220,6 +2033,23 @@ async function main() {
|
|
|
1220
2033
|
}
|
|
1221
2034
|
}
|
|
1222
2035
|
|
|
2036
|
+
if (bundled) {
|
|
2037
|
+
// Recheck after setup and before provider loading: never carry a stale
|
|
2038
|
+
// fresh-store decision across an installer that could create data.
|
|
2039
|
+
if (args.piSubcommand === undefined && !engramQuiet) {
|
|
2040
|
+
bundledMemory = await classifyBundledMemory();
|
|
2041
|
+
bundledEnv = bundledProcessEnv();
|
|
2042
|
+
// The guard protects memory whatever provider loads, so a companion that
|
|
2043
|
+
// could not be verified is reported, never a reason to stop Shell.
|
|
2044
|
+
const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
|
|
2045
|
+
if (!companion.ok) process.stderr.write(`${companion.message}\n`);
|
|
2046
|
+
// A fresh store's identity and provenance exist before Pi starts, or
|
|
2047
|
+
// memory stays off: Pi never runs with allow without a written record.
|
|
2048
|
+
if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
|
|
2049
|
+
if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
|
|
2050
|
+
}
|
|
2051
|
+
bundledEnv = bundledProcessEnv();
|
|
2052
|
+
}
|
|
1223
2053
|
const invocation = buildPiInvocation({
|
|
1224
2054
|
runtime,
|
|
1225
2055
|
home,
|
|
@@ -1228,22 +2058,98 @@ async function main() {
|
|
|
1228
2058
|
takeOver,
|
|
1229
2059
|
otherPackagePaths,
|
|
1230
2060
|
looseExtensionEntries,
|
|
1231
|
-
passthrough: args
|
|
2061
|
+
passthrough: [...managedHerdrExtensionArgs(home, args), ...passthrough],
|
|
1232
2062
|
piSubcommand: args.piSubcommand,
|
|
1233
|
-
baseEnv: process.env,
|
|
2063
|
+
baseEnv: bundledEnv ?? process.env,
|
|
2064
|
+
homedir: homedir(),
|
|
2065
|
+
// The spawn below sets no cwd, so pi runs in the launcher's own.
|
|
2066
|
+
cwd: process.cwd(),
|
|
1234
2067
|
});
|
|
1235
2068
|
|
|
2069
|
+
// Only an interactive session ends with pi's exit resume hint, which
|
|
2070
|
+
// gentle-shell completes with its own line; a pi subcommand gets no handoff.
|
|
2071
|
+
const resumeHandoff = args.piSubcommand === undefined ? createResumeHandoff() : undefined;
|
|
2072
|
+
const childEnv = resumeHandoff ? { ...invocation.env, [RESUME_HANDOFF_ENV]: resumeHandoff.path } : invocation.env;
|
|
2073
|
+
|
|
1236
2074
|
const launchPlan = planSpawn({ command: invocation.command, args: invocation.args, platform: process.platform });
|
|
1237
|
-
|
|
2075
|
+
let child;
|
|
2076
|
+
try {
|
|
2077
|
+
child = spawn(launchPlan.command, launchPlan.args, { stdio: "inherit", env: childEnv, shell: launchPlan.shell });
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
resumeHandoff?.dispose();
|
|
2080
|
+
throw error;
|
|
2081
|
+
}
|
|
2082
|
+
// Only SIGHUP means the terminal is gone. pi may survive a forwarded
|
|
2083
|
+
// SIGINT and keep running, so other signals must not silence the hint.
|
|
2084
|
+
let terminalHungUp = false;
|
|
1238
2085
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
1239
|
-
process.on(signal, () =>
|
|
2086
|
+
process.on(signal, () => {
|
|
2087
|
+
if (signal === "SIGHUP") terminalHungUp = true;
|
|
2088
|
+
child.kill(signal);
|
|
2089
|
+
});
|
|
1240
2090
|
}
|
|
1241
|
-
child.on("error", (error) =>
|
|
2091
|
+
child.on("error", (error) => {
|
|
2092
|
+
resumeHandoff?.dispose();
|
|
2093
|
+
fail(`Could not start pi: ${error.message}`, 1);
|
|
2094
|
+
});
|
|
1242
2095
|
child.on("exit", (code, signal) => {
|
|
1243
|
-
|
|
2096
|
+
const exitCode = signal ? signalExitCode(signal) : (code ?? 1);
|
|
2097
|
+
if (resumeHandoff) {
|
|
2098
|
+
const hint = planResumeHint({
|
|
2099
|
+
handoff: resumeHandoff.read(),
|
|
2100
|
+
homeFlags: homeSelectorFlags(home),
|
|
2101
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
2102
|
+
terminalHungUp,
|
|
2103
|
+
platform: process.platform,
|
|
2104
|
+
color: process.stdout.hasColors?.() === true,
|
|
2105
|
+
});
|
|
2106
|
+
resumeHandoff.dispose();
|
|
2107
|
+
// TTY writes are asynchronous on Windows: exit only once the
|
|
2108
|
+
// hint is flushed, or it can be lost.
|
|
2109
|
+
if (hint) {
|
|
2110
|
+
// A write error (e.g. EIO on a closed terminal) must not turn
|
|
2111
|
+
// pi's exit into a launcher crash.
|
|
2112
|
+
process.stdout.once("error", () => process.exit(exitCode));
|
|
2113
|
+
process.stdout.write(hint, () => process.exit(exitCode));
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
process.exit(exitCode);
|
|
1244
2118
|
});
|
|
1245
2119
|
}
|
|
1246
2120
|
|
|
2121
|
+
// Private temp dir for the resume-hint handoff (lib/gentle-shell-resume-hint.ts).
|
|
2122
|
+
// Best effort: if it cannot be created, only pi's own hint is printed.
|
|
2123
|
+
function createResumeHandoff() {
|
|
2124
|
+
let dir;
|
|
2125
|
+
try {
|
|
2126
|
+
dir = mkdtempSync(join(tmpdir(), RESUME_HANDOFF_DIR_PREFIX));
|
|
2127
|
+
} catch {
|
|
2128
|
+
return undefined;
|
|
2129
|
+
}
|
|
2130
|
+
const path = join(dir, RESUME_HANDOFF_FILE);
|
|
2131
|
+
return {
|
|
2132
|
+
path,
|
|
2133
|
+
read: () => {
|
|
2134
|
+
try {
|
|
2135
|
+
const text = readJsonIfExists(path);
|
|
2136
|
+
return text === undefined ? undefined : parseResumeHandoff(text);
|
|
2137
|
+
} catch {
|
|
2138
|
+
return undefined;
|
|
2139
|
+
}
|
|
2140
|
+
},
|
|
2141
|
+
// Never throws: it runs inside the exit handler, where an EPERM on
|
|
2142
|
+
// Windows would otherwise replace pi's exit code with a crash.
|
|
2143
|
+
dispose: () => {
|
|
2144
|
+
try {
|
|
2145
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2146
|
+
} catch {
|
|
2147
|
+
// A leftover empty temp dir is harmless.
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1247
2153
|
main().catch((error) => {
|
|
1248
2154
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1249
2155
|
process.exit(1);
|