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
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { isolatedDir, launcherConfigPath, parseRawLauncherConfig, provisionedEntry, userPiHome } from "./gentle-shell-launcher.ts";
|
|
5
|
+
|
|
6
|
+
// `gentle-shell self-uninstall`: removes what Gentle Shell itself created, then
|
|
7
|
+
// the gentle-pi package with the package manager that owns it. Everything is
|
|
8
|
+
// planned and printed before anything is removed. Removal follows the
|
|
9
|
+
// installer's safe-deletion model (scripts/installer-downloads.mjs
|
|
10
|
+
// removeOwnedTools): exact paths only, lstat first, symbolic links are never
|
|
11
|
+
// followed or removed, and rmSync never follows links inside a directory.
|
|
12
|
+
// `gentle-shell uninstall <source>` stays pi's own alias for `remove`.
|
|
13
|
+
|
|
14
|
+
export const SELF_UNINSTALL_USAGE = "usage: gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]";
|
|
15
|
+
|
|
16
|
+
/** Gentle AI configuration in the config home that Pi without Gentle Shell can share. */
|
|
17
|
+
export const SHARED_CONFIG_FILES: readonly string[] = Object.freeze([
|
|
18
|
+
"profiles.json",
|
|
19
|
+
"profiles.export.json",
|
|
20
|
+
"banner.json",
|
|
21
|
+
"builtin-codemode-optout.json",
|
|
22
|
+
"background-subagents.json",
|
|
23
|
+
"double-esc-cancel.json",
|
|
24
|
+
"runtime-guardrails.json",
|
|
25
|
+
"persona.json",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const HOME_OWNERSHIP_MARKER = ".gentle-shell-home";
|
|
29
|
+
// scripts/installer-downloads.mjs marks each pinned Go version folder with it.
|
|
30
|
+
const GO_MARKER = ".gentle-shell-go";
|
|
31
|
+
const GO_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$/;
|
|
32
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
33
|
+
const PACKAGE = "gentle-pi";
|
|
34
|
+
|
|
35
|
+
export interface SelfUninstallOptions {
|
|
36
|
+
dryRun: boolean;
|
|
37
|
+
yes: boolean;
|
|
38
|
+
includeShared: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseSelfUninstallArgs(args: readonly string[]): SelfUninstallOptions | { error: string } {
|
|
42
|
+
const options = { dryRun: false, yes: false, includeShared: false };
|
|
43
|
+
for (const arg of args) {
|
|
44
|
+
if (arg === "--dry-run") options.dryRun = true;
|
|
45
|
+
else if (arg === "--yes") options.yes = true;
|
|
46
|
+
else if (arg === "--include-shared") options.includeShared = true;
|
|
47
|
+
else return { error: `unknown argument ${arg}; ${SELF_UNINSTALL_USAGE}` };
|
|
48
|
+
}
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How the owning package manager runs: `command` with `prefix` before its own
|
|
53
|
+
* arguments (on Windows, node.exe and the manager's JS entry, never a .cmd). */
|
|
54
|
+
export interface PackageManagerOwner {
|
|
55
|
+
name: "pnpm" | "npm";
|
|
56
|
+
command: string;
|
|
57
|
+
prefix: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RunResult {
|
|
61
|
+
code: number | null;
|
|
62
|
+
timedOut?: boolean;
|
|
63
|
+
signal?: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SelfUninstallInput {
|
|
67
|
+
args: readonly string[];
|
|
68
|
+
env: Record<string, string | undefined>;
|
|
69
|
+
homedir: string;
|
|
70
|
+
platform: string;
|
|
71
|
+
/** Whether a person can answer questions (stdin is a terminal). */
|
|
72
|
+
interactive: boolean;
|
|
73
|
+
/** The package manager that owns gentle-pi; throws an Error to refuse. */
|
|
74
|
+
resolveOwner: () => Promise<PackageManagerOwner>;
|
|
75
|
+
run: (command: string, argv: string[]) => Promise<RunResult>;
|
|
76
|
+
/** Resolves the typed line ("" at end of input); rejects with PromptInterrupted on Ctrl-C. */
|
|
77
|
+
ask: (question: string) => Promise<string>;
|
|
78
|
+
out: (line: string) => void;
|
|
79
|
+
err: (line: string) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface Removal {
|
|
83
|
+
path: string;
|
|
84
|
+
label: string;
|
|
85
|
+
/** "link": the symbolic link itself (the bundled install's `current`), never what it points at. */
|
|
86
|
+
kind: "dir" | "file" | "link";
|
|
87
|
+
/** Remove the parent directory too when this leaves it empty. */
|
|
88
|
+
pruneParent?: boolean;
|
|
89
|
+
/** Evidence that Gentle Shell made it; without it the path is kept with this reason. */
|
|
90
|
+
evidence?: { made: (path: string) => boolean; otherwise: string };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface Guard {
|
|
94
|
+
path: string;
|
|
95
|
+
label: string;
|
|
96
|
+
/** Whether lying inside `path` is refused too (being it or containing it always is). */
|
|
97
|
+
inside: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface NotTouched {
|
|
101
|
+
path?: string;
|
|
102
|
+
label: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface Plan {
|
|
106
|
+
refusals: string[];
|
|
107
|
+
/** A removal would reach a protected path: the removal list is not offered at all. */
|
|
108
|
+
pathRefused: boolean;
|
|
109
|
+
own: Removal[];
|
|
110
|
+
kept: string[];
|
|
111
|
+
shared: Removal[];
|
|
112
|
+
configHome: string;
|
|
113
|
+
owner?: PackageManagerOwner;
|
|
114
|
+
notTouched: NotTouched[];
|
|
115
|
+
manual: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function comparable(path: string, platform: string): string {
|
|
119
|
+
return platform === "win32" ? path.toLowerCase() : path;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The real path, resolving the nearest existing ancestor when `path` does not exist. */
|
|
123
|
+
function canonical(path: string): string {
|
|
124
|
+
const absolute = resolve(path);
|
|
125
|
+
const rest: string[] = [];
|
|
126
|
+
let current = absolute;
|
|
127
|
+
for (;;) {
|
|
128
|
+
try {
|
|
129
|
+
return join(realpathSync(current), ...rest);
|
|
130
|
+
} catch {
|
|
131
|
+
const parent = dirname(current);
|
|
132
|
+
if (parent === current) return absolute;
|
|
133
|
+
rest.unshift(basename(current));
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** True when `child` is `parent` itself or lies inside it. */
|
|
140
|
+
function within(parent: string, child: string, platform: string): boolean {
|
|
141
|
+
const path = relative(comparable(parent, platform), comparable(child, platform));
|
|
142
|
+
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type Inspection = "ok" | "missing" | "symlink" | "other";
|
|
146
|
+
|
|
147
|
+
function inspect(path: string, kind: "dir" | "file" | "link"): Inspection {
|
|
148
|
+
let info;
|
|
149
|
+
try {
|
|
150
|
+
info = lstatSync(path);
|
|
151
|
+
} catch {
|
|
152
|
+
return "missing";
|
|
153
|
+
}
|
|
154
|
+
if (kind === "link") return info.isSymbolicLink() ? "ok" : "other";
|
|
155
|
+
if (info.isSymbolicLink()) return "symlink";
|
|
156
|
+
return (kind === "dir" ? info.isDirectory() : info.isFile()) ? "ok" : "other";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readText(path: string): string | undefined {
|
|
160
|
+
try {
|
|
161
|
+
return readFileSync(path, "utf8");
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** An absolute directory from `env[name]`, the fallback when unset or empty, or undefined when relative. */
|
|
168
|
+
function settingDirectory(env: Record<string, string | undefined>, name: string, fallback: string): string | undefined {
|
|
169
|
+
const value = env[name];
|
|
170
|
+
if (value === undefined || value === "") return fallback;
|
|
171
|
+
return isAbsolute(value) ? value : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function entries(path: string): string[] {
|
|
175
|
+
try {
|
|
176
|
+
return readdirSync(path);
|
|
177
|
+
} catch {
|
|
178
|
+
return ["\0unreadable"];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Only what the main channel writes (scripts/main-channel.mjs): gentle-ai/<commit>/gentle-ai[.exe],
|
|
183
|
+
* packages/gentle-pi-*.tgz, and the .build and .source work folders. */
|
|
184
|
+
function mainChannelLayout(main: string): boolean {
|
|
185
|
+
return entries(main).every((name) => {
|
|
186
|
+
const path = join(main, name);
|
|
187
|
+
if (inspect(path, "dir") !== "ok") return false;
|
|
188
|
+
if (name === ".build" || name === ".source") return true;
|
|
189
|
+
if (name === "packages") return entries(path).every((file) => /^gentle-pi-.+\.tgz$/.test(file) && inspect(join(path, file), "file") === "ok");
|
|
190
|
+
if (name !== "gentle-ai") return false;
|
|
191
|
+
return entries(path).every((commit) => COMMIT.test(commit) && inspect(join(path, commit), "dir") === "ok"
|
|
192
|
+
&& entries(join(path, commit)).every((file) => ["gentle-ai", "gentle-ai.exe"].includes(file) && inspect(join(path, commit, file), "file") === "ok"));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Only pinned-Go version folders, each carrying the installer's marker. */
|
|
197
|
+
function pinnedGoOnly(go: string): boolean {
|
|
198
|
+
return entries(go).every((name) => GO_VERSION.test(name) && inspect(join(go, name), "dir") === "ok" && inspect(join(go, name, GO_MARKER), "file") === "ok");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** How `real` collides with a guard: it is it, contains it, or (for guards that say so) lies inside it. */
|
|
202
|
+
function collision(real: string, guards: readonly Guard[], platform: string): string | undefined {
|
|
203
|
+
for (const guard of guards) {
|
|
204
|
+
const target = canonical(guard.path);
|
|
205
|
+
const relation = comparable(real, platform) === comparable(target, platform) ? "is"
|
|
206
|
+
: within(real, target, platform) ? "contains"
|
|
207
|
+
: guard.inside && within(target, real, platform) ? "lies inside" : undefined;
|
|
208
|
+
if (relation) return `${relation} ${guard.path} (${guard.label})`;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A `.pi` directory (a project's `.pi`, `.pi/gentle-ai`, or the user's `~/.pi`) is never removed or emptied. */
|
|
214
|
+
function piDirectoryCollision(real: string, platform: string, allowedInside: string | undefined): string | undefined {
|
|
215
|
+
const segments = real.split(/[\\/]+/).filter((segment) => segment !== "");
|
|
216
|
+
const isPi = (segment: string | undefined) => segment !== undefined && comparable(segment, platform) === ".pi";
|
|
217
|
+
const exempt = allowedInside !== undefined && comparable(real, platform) === comparable(allowedInside, platform);
|
|
218
|
+
if (!exempt && isPi(segments.at(-1))) return "is a .pi directory";
|
|
219
|
+
if (!exempt && segments.slice(0, -1).some(isPi)) return "lies inside a .pi directory";
|
|
220
|
+
if (inspect(join(real, ".pi"), "dir") !== "missing") return `contains a .pi directory (${join(real, ".pi")})`;
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Whether the main-channel override in `dev-binary.json` points at a build under `<config home>/main/gentle-ai`. */
|
|
225
|
+
function mainChannelRegistration(path: string, configHome: string, platform: string): boolean {
|
|
226
|
+
try {
|
|
227
|
+
const registration = JSON.parse(readText(path) ?? "null");
|
|
228
|
+
if (typeof registration?.path !== "string") return false;
|
|
229
|
+
const builds = join(configHome, "main", "gentle-ai");
|
|
230
|
+
return within(builds, registration.path, platform) && resolve(registration.path) !== resolve(builds);
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function planSelfUninstall(input: SelfUninstallInput, configHome: string, isolated: string): Promise<Plan> {
|
|
237
|
+
const { env, homedir, platform } = input;
|
|
238
|
+
const plan: Plan = { refusals: [], pathRefused: false, own: [], kept: [], shared: [], configHome, notTouched: [], manual: [] };
|
|
239
|
+
const configPath = launcherConfigPath(homedir);
|
|
240
|
+
const shellDirectory = dirname(configPath);
|
|
241
|
+
const launcherConfig = parseRawLauncherConfig(readText(configPath));
|
|
242
|
+
const piHome = userPiHome(env, homedir);
|
|
243
|
+
const gentleAiState = join(homedir, ".gentle-ai");
|
|
244
|
+
const isolatedReal = canonical(isolated);
|
|
245
|
+
|
|
246
|
+
// Every place a removal starts from is checked at its real path, so a symbolic
|
|
247
|
+
// link (or a symlinked ancestor) never leads it somewhere protected.
|
|
248
|
+
// Inside a Gentle Shell session PI_CODING_AGENT_DIR is the isolated home itself
|
|
249
|
+
// and the user's Pi home travels in GENTLE_SHELL_USER_PI_HOME (see userPiHome).
|
|
250
|
+
// Only an existing directory other than the isolated home proves that session.
|
|
251
|
+
const sessionPiHome = env.GENTLE_SHELL_USER_PI_HOME ? canonical(env.GENTLE_SHELL_USER_PI_HOME) : null;
|
|
252
|
+
const inSession = sessionPiHome !== null && inspect(sessionPiHome, "dir") === "ok"
|
|
253
|
+
&& comparable(sessionPiHome, platform) !== comparable(isolatedReal, platform);
|
|
254
|
+
const sessionAgentDir = (name: string, value: string) => name === "PI_CODING_AGENT_DIR" && inSession
|
|
255
|
+
&& comparable(canonical(value), platform) === comparable(isolatedReal, platform);
|
|
256
|
+
const guards: Guard[] = [
|
|
257
|
+
{ path: homedir, label: "your home directory", inside: false },
|
|
258
|
+
{ path: join(homedir, ".pi", "agent"), label: "Pi's default home", inside: true },
|
|
259
|
+
...["PI_CODING_AGENT_DIR", "GENTLE_SHELL_USER_PI_HOME"].flatMap((name) => {
|
|
260
|
+
const value = env[name];
|
|
261
|
+
return value && !sessionAgentDir(name, value) ? [{ path: value, label: "your Pi home", inside: true }] : [];
|
|
262
|
+
}),
|
|
263
|
+
{ path: gentleAiState, label: "Gentle AI's state", inside: true },
|
|
264
|
+
];
|
|
265
|
+
const defaultConfigHome = canonical(join(homedir, ".pi", "gentle-ai"));
|
|
266
|
+
const candidates = [
|
|
267
|
+
{
|
|
268
|
+
who: env.GENTLE_SHELL_HOME ? `GENTLE_SHELL_HOME points at ${isolated}` : `The isolated home ${isolated}`,
|
|
269
|
+
path: isolated,
|
|
270
|
+
guards: [...guards, { path: configHome, label: "the Gentle AI config home", inside: true }],
|
|
271
|
+
allowedInsidePi: undefined,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
who: env.GENTLE_PI_CONFIG_HOME ? `GENTLE_PI_CONFIG_HOME points at ${configHome}` : `The config home ${configHome}`,
|
|
275
|
+
path: configHome,
|
|
276
|
+
guards,
|
|
277
|
+
// Its default location, ~/.pi/gentle-ai, is the one .pi path it may be.
|
|
278
|
+
allowedInsidePi: defaultConfigHome,
|
|
279
|
+
},
|
|
280
|
+
{ who: `The launcher folder ${shellDirectory}`, path: shellDirectory, guards, allowedInsidePi: undefined },
|
|
281
|
+
];
|
|
282
|
+
for (const candidate of candidates) {
|
|
283
|
+
const real = canonical(candidate.path);
|
|
284
|
+
const reason = collision(real, candidate.guards, platform) ?? piDirectoryCollision(real, platform, candidate.allowedInsidePi);
|
|
285
|
+
if (reason === undefined) continue;
|
|
286
|
+
const really = comparable(real, platform) === comparable(resolve(candidate.path), platform) ? "" : ` (really ${real})`;
|
|
287
|
+
plan.refusals.push(`${candidate.who}${really}, which ${reason}; refusing to remove anything.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const consider = (removal: Removal) => {
|
|
291
|
+
const state = inspect(removal.path, removal.kind);
|
|
292
|
+
if (state === "ok" && removal.evidence && !removal.evidence.made(removal.path)) plan.kept.push(`${removal.path}: ${removal.evidence.otherwise}`);
|
|
293
|
+
else if (state === "ok") plan.own.push(removal);
|
|
294
|
+
else if (state === "symlink") plan.kept.push(`${removal.path}: a symbolic link, not followed`);
|
|
295
|
+
else if (state === "other") plan.kept.push(`${removal.path}: not a ${removal.kind === "dir" ? "directory" : "regular file"}`);
|
|
296
|
+
};
|
|
297
|
+
consider({
|
|
298
|
+
path: join(configHome, "main"), label: "main-channel builds", kind: "dir",
|
|
299
|
+
evidence: { made: mainChannelLayout, otherwise: "not Gentle Shell's main-channel layout" },
|
|
300
|
+
});
|
|
301
|
+
consider({ path: join(configHome, "channel.json"), label: "recorded update channel", kind: "file" });
|
|
302
|
+
const devBinary = join(configHome, "dev-binary.json");
|
|
303
|
+
if (inspect(devBinary, "file") !== "ok" || mainChannelRegistration(devBinary, configHome, platform)) {
|
|
304
|
+
consider({ path: devBinary, label: "main-channel Gentle AI override", kind: "file" });
|
|
305
|
+
} else {
|
|
306
|
+
plan.kept.push(`${devBinary}: points at a binary you registered yourself`);
|
|
307
|
+
}
|
|
308
|
+
consider({
|
|
309
|
+
path: join(configHome, "tools", "go"), label: "Go the installer downloaded for builds", kind: "dir", pruneParent: true,
|
|
310
|
+
evidence: { made: pinnedGoOnly, otherwise: `not only the installer's pinned Go (version folders marked ${GO_MARKER})` },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// The default isolated home is Gentle Shell's by location; a GENTLE_SHELL_HOME
|
|
314
|
+
// directory only with the launcher's ownership marker or setup record.
|
|
315
|
+
const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
|
|
316
|
+
const owned = defaultHome || inspect(join(isolated, HOME_OWNERSHIP_MARKER), "file") === "ok"
|
|
317
|
+
|| provisionedEntry(launcherConfig, isolatedReal) !== undefined;
|
|
318
|
+
if (owned || inspect(isolated, "dir") !== "ok") consider({ path: isolated, label: "isolated home: its sign-ins, chats and settings", kind: "dir" });
|
|
319
|
+
else plan.kept.push(`${isolated}: not created by Gentle Shell (no ${HOME_OWNERSHIP_MARKER} marker or setup record)`);
|
|
320
|
+
consider({ path: configPath, label: "launcher settings", kind: "file" });
|
|
321
|
+
|
|
322
|
+
for (const name of SHARED_CONFIG_FILES) {
|
|
323
|
+
const path = join(configHome, name);
|
|
324
|
+
if (inspect(path, "file") === "ok") plan.shared.push({ path, label: name, kind: "file" });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const seen = new Set([comparable(isolatedReal, platform), comparable(canonical(piHome), platform)]);
|
|
328
|
+
plan.notTouched.push({ path: piHome, label: "your Pi home" });
|
|
329
|
+
const customHomes = [typeof launcherConfig.home === "string" && !["link", "isolated", ""].includes(launcherConfig.home) ? launcherConfig.home : undefined];
|
|
330
|
+
const provisioned = launcherConfig.provisioned;
|
|
331
|
+
if (typeof provisioned === "object" && provisioned !== null && !Array.isArray(provisioned)) customHomes.push(...Object.keys(provisioned));
|
|
332
|
+
for (const home of customHomes) {
|
|
333
|
+
if (home === undefined || !isAbsolute(home)) continue;
|
|
334
|
+
const key = comparable(canonical(home), platform);
|
|
335
|
+
if (seen.has(key)) continue;
|
|
336
|
+
seen.add(key);
|
|
337
|
+
plan.notTouched.push({ path: home, label: "custom --home home" });
|
|
338
|
+
}
|
|
339
|
+
plan.notTouched.push({ path: gentleAiState, label: "Gentle AI's own state" }, { label: "Each project's .pi/gentle-ai folder" });
|
|
340
|
+
|
|
341
|
+
// Nothing is both removed and not touched: a removal that is, holds or lies
|
|
342
|
+
// inside a path listed as not touched refuses the whole run.
|
|
343
|
+
if (plan.refusals.length === 0) {
|
|
344
|
+
for (const removal of [...plan.own, ...plan.shared]) {
|
|
345
|
+
const real = canonical(removal.path);
|
|
346
|
+
for (const kept of plan.notTouched) {
|
|
347
|
+
if (kept.path === undefined) continue;
|
|
348
|
+
const target = canonical(kept.path);
|
|
349
|
+
if (!within(real, target, platform) && !within(target, real, platform)) continue;
|
|
350
|
+
plan.refusals.push(`${removal.path} would be removed, but it overlaps ${kept.path} (${kept.label}), which is not touched; refusing to remove anything.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
plan.pathRefused = plan.refusals.length > 0;
|
|
355
|
+
|
|
356
|
+
plan.manual.push(
|
|
357
|
+
"Pi: pnpm remove -g @earendil-works/pi-coding-agent (or npm uninstall -g @earendil-works/pi-coding-agent)",
|
|
358
|
+
"Engram, installed by Gentle AI's setup: remove the engram binary on your PATH",
|
|
359
|
+
platform === "win32"
|
|
360
|
+
? "The PATH entry from `pnpm setup`: remove PNPM_HOME and its PATH entry from your user environment variables"
|
|
361
|
+
: "The PATH entry from `pnpm setup`: remove the PNPM_HOME lines it added to your shell profile",
|
|
362
|
+
);
|
|
363
|
+
if (platform === "win32") {
|
|
364
|
+
plan.manual.push("%USERPROFILE%\\.pnpm, the private pnpm home the installer may have chosen: delete it after removing the packages above");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
plan.owner = await input.resolveOwner();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
plan.refusals.push(`The ${PACKAGE} package cannot be removed: ${error instanceof Error ? error.message : String(error)}`);
|
|
371
|
+
}
|
|
372
|
+
return plan;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function removeArgv(owner: PackageManagerOwner): string[] {
|
|
376
|
+
return owner.name === "pnpm" ? ["remove", "-g", PACKAGE] : ["uninstall", "-g", PACKAGE];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function printPlan(plan: Plan, options: SelfUninstallOptions, interactive: boolean, out: (line: string) => void) {
|
|
380
|
+
out("Gentle Shell self-uninstall plan");
|
|
381
|
+
out("Remove Gentle Shell data:");
|
|
382
|
+
if (plan.pathRefused) out(" (refused, see below)");
|
|
383
|
+
else if (plan.own.length === 0) out(" (nothing found)");
|
|
384
|
+
for (const removal of plan.pathRefused ? [] : plan.own) out(` ${removal.path} (${removal.label})`);
|
|
385
|
+
out(`Remove the ${PACKAGE} package:`);
|
|
386
|
+
out(plan.owner ? ` ${plan.owner.name} ${removeArgv(plan.owner).join(" ")}` : " (refused, see below)");
|
|
387
|
+
if (plan.shared.length > 0 && !plan.pathRefused) {
|
|
388
|
+
const disposition = options.includeShared
|
|
389
|
+
? "removed (--include-shared)"
|
|
390
|
+
: options.yes || !interactive ? "kept; pass --include-shared to remove it" : "kept unless you accept when asked";
|
|
391
|
+
out(`Shared Gentle AI configuration in ${plan.configHome}, ${disposition}:`);
|
|
392
|
+
for (const removal of plan.shared) out(` ${removal.path}`);
|
|
393
|
+
}
|
|
394
|
+
if (plan.kept.length > 0) {
|
|
395
|
+
out("Kept:");
|
|
396
|
+
for (const line of plan.kept) out(` ${line}`);
|
|
397
|
+
}
|
|
398
|
+
out("Not touched:");
|
|
399
|
+
for (const kept of plan.notTouched) out(kept.path === undefined ? ` ${kept.label}` : ` ${kept.path} (${kept.label})`);
|
|
400
|
+
out("The installer may also have added these; remove them yourself if nothing else uses them:");
|
|
401
|
+
for (const line of plan.manual) out(` ${line}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Removes one planned path without following links; re-checks it right before removal. */
|
|
405
|
+
function remove(removal: Removal) {
|
|
406
|
+
const state = inspect(removal.path, removal.kind);
|
|
407
|
+
if (state === "missing") return;
|
|
408
|
+
if (state !== "ok") throw new Error("it changed after the plan was made");
|
|
409
|
+
if (removal.kind === "dir") rmSync(removal.path, { recursive: true });
|
|
410
|
+
else rmSync(removal.path);
|
|
411
|
+
if (removal.pruneParent) removeIfEmpty(dirname(removal.path));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function removeIfEmpty(directory: string) {
|
|
415
|
+
if (inspect(directory, "dir") === "ok" && readdirSync(directory).length === 0) rmdirSync(directory);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Ctrl-C at a question: the whole uninstall stops. */
|
|
419
|
+
export class PromptInterrupted extends Error {
|
|
420
|
+
constructor() {
|
|
421
|
+
super("interrupted");
|
|
422
|
+
this.name = "PromptInterrupted";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Asks one question on `input` and returns the typed line. End of input (Ctrl-D,
|
|
428
|
+
* a closed stdin) answers "", which every question reads as No; Ctrl-C rejects
|
|
429
|
+
* with PromptInterrupted. `terminal` follows readline (default: output.isTTY).
|
|
430
|
+
*/
|
|
431
|
+
export function askLine(question: string, streams: { input: NodeJS.ReadableStream; output: NodeJS.WritableStream; terminal?: boolean }): Promise<string> {
|
|
432
|
+
const input = streams.input as NodeJS.ReadableStream & { readableEnded?: boolean; destroyed?: boolean };
|
|
433
|
+
if (input.readableEnded === true || input.destroyed === true) return Promise.resolve("");
|
|
434
|
+
return new Promise((resolveAnswer, reject) => {
|
|
435
|
+
let settled = false;
|
|
436
|
+
const prompt = createInterface({ input: streams.input, output: streams.output, terminal: streams.terminal });
|
|
437
|
+
prompt.once("SIGINT", () => {
|
|
438
|
+
if (settled) return;
|
|
439
|
+
settled = true;
|
|
440
|
+
prompt.close();
|
|
441
|
+
reject(new PromptInterrupted());
|
|
442
|
+
});
|
|
443
|
+
prompt.once("close", () => {
|
|
444
|
+
if (settled) return;
|
|
445
|
+
settled = true;
|
|
446
|
+
resolveAnswer("");
|
|
447
|
+
});
|
|
448
|
+
prompt.question(question, (answer) => {
|
|
449
|
+
if (settled) return;
|
|
450
|
+
settled = true;
|
|
451
|
+
prompt.close();
|
|
452
|
+
resolveAnswer(answer);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isYes(answer: string): boolean {
|
|
458
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The one confirmation both uninstalls ask: --yes skips it; without a terminal
|
|
463
|
+
* it is a usage error (2); No or end of input cancels and Ctrl-C aborts (1).
|
|
464
|
+
* The shared data is asked about separately, default No. Returns whether to
|
|
465
|
+
* include the shared data, or the exit code.
|
|
466
|
+
*/
|
|
467
|
+
async function confirm(
|
|
468
|
+
input: Pick<SelfUninstallInput, "interactive" | "ask" | "out" | "err">,
|
|
469
|
+
options: SelfUninstallOptions,
|
|
470
|
+
sharedPresent: boolean,
|
|
471
|
+
sharedQuestion: string,
|
|
472
|
+
): Promise<{ includeShared: boolean } | { code: number }> {
|
|
473
|
+
let includeShared = options.includeShared;
|
|
474
|
+
if (options.yes) return { includeShared };
|
|
475
|
+
if (!input.interactive) {
|
|
476
|
+
input.err("gentle-shell self-uninstall: no terminal to confirm in; pass --yes to remove the above, or --dry-run to only show it.");
|
|
477
|
+
return { code: 2 };
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
if (!isYes(await input.ask("Remove Gentle Shell as listed above? [y/N] "))) {
|
|
481
|
+
input.out("Cancelled; nothing was removed.");
|
|
482
|
+
return { code: 1 };
|
|
483
|
+
}
|
|
484
|
+
if (sharedPresent && !includeShared) includeShared = isYes(await input.ask(sharedQuestion));
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (!(error instanceof PromptInterrupted)) throw error;
|
|
487
|
+
input.err("gentle-shell self-uninstall: Interrupted; nothing was removed.");
|
|
488
|
+
return { code: 1 };
|
|
489
|
+
}
|
|
490
|
+
return { includeShared };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function succeeded(result: RunResult): boolean {
|
|
494
|
+
return result.code === 0 && result.timedOut !== true && result.signal == null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Runs `gentle-shell self-uninstall`. Exit codes: 0 done or dry run, 1 refused,
|
|
499
|
+
* cancelled or failed, 2 usage (including no terminal without --yes).
|
|
500
|
+
*/
|
|
501
|
+
export async function runSelfUninstall(input: SelfUninstallInput): Promise<number> {
|
|
502
|
+
const { env, homedir, out, err } = input;
|
|
503
|
+
const options = parseSelfUninstallArgs(input.args);
|
|
504
|
+
if ("error" in options) {
|
|
505
|
+
err(`gentle-shell self-uninstall: ${options.error}`);
|
|
506
|
+
return 2;
|
|
507
|
+
}
|
|
508
|
+
const configHome = settingDirectory(env, "GENTLE_PI_CONFIG_HOME", join(homedir, ".pi", "gentle-ai"));
|
|
509
|
+
const isolated = settingDirectory(env, "GENTLE_SHELL_HOME", isolatedDir(env, homedir));
|
|
510
|
+
if (configHome === undefined || isolated === undefined) {
|
|
511
|
+
const name = configHome === undefined ? "GENTLE_PI_CONFIG_HOME" : "GENTLE_SHELL_HOME";
|
|
512
|
+
err(`gentle-shell self-uninstall: ${name}=${env[name]} is not an absolute path; refusing to remove anything.`);
|
|
513
|
+
return 1;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const plan = await planSelfUninstall(input, configHome, isolated);
|
|
517
|
+
printPlan(plan, options, input.interactive, out);
|
|
518
|
+
if (plan.refusals.length > 0 || plan.owner === undefined) {
|
|
519
|
+
for (const line of plan.refusals) err(`gentle-shell self-uninstall: ${line}`);
|
|
520
|
+
err("Nothing was removed.");
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
if (options.dryRun) {
|
|
524
|
+
out("Dry run: nothing was removed.");
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
const confirmation = await confirm(input, options, plan.shared.length > 0,
|
|
528
|
+
`Also remove the shared Gentle AI configuration in ${plan.configHome} listed above? [y/N] `);
|
|
529
|
+
if ("code" in confirmation) return confirmation.code;
|
|
530
|
+
const { includeShared } = confirmation;
|
|
531
|
+
|
|
532
|
+
for (const removal of [...plan.own, ...(includeShared ? plan.shared : [])]) {
|
|
533
|
+
try {
|
|
534
|
+
remove(removal);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
err(`gentle-shell self-uninstall: could not remove ${removal.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
537
|
+
err(`The ${PACKAGE} package and everything listed after it were kept; fix the cause and run gentle-shell self-uninstall again.`);
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const command = `${plan.owner.name} ${removeArgv(plan.owner).join(" ")}`;
|
|
543
|
+
let removed = false;
|
|
544
|
+
try {
|
|
545
|
+
removed = succeeded(await input.run(plan.owner.command, [...plan.owner.prefix, ...removeArgv(plan.owner)]));
|
|
546
|
+
} catch {
|
|
547
|
+
removed = false;
|
|
548
|
+
}
|
|
549
|
+
if (!removed) {
|
|
550
|
+
err(`gentle-shell self-uninstall: ${plan.owner.name} could not remove the ${PACKAGE} package. Gentle Shell's data was removed; remove the package yourself with: ${command}`);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
removeIfEmpty(dirname(launcherConfigPath(homedir)));
|
|
555
|
+
} catch {
|
|
556
|
+
// A leftover empty ~/.gentle-shell is harmless.
|
|
557
|
+
}
|
|
558
|
+
out("Gentle Shell was removed.");
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// The bundled install (S6, S7): `gentle-shell self-uninstall` from the bundled
|
|
564
|
+
// launcher removes what scripts/bundled-install.mjs wrote in its prefix and the
|
|
565
|
+
// one PATH entry its receipt records. The Gentle Shell home (sessions,
|
|
566
|
+
// sign-ins, extensions) is shared data, kept unless asked. The same planning,
|
|
567
|
+
// confirmation and safe-deletion rules as above apply.
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
/** The PATH entry a bundled install applied, read from its receipt
|
|
571
|
+
* (scripts/bundled-install.mjs readPathReceipt). Only "recorded" is removed;
|
|
572
|
+
* without a valid receipt the entry is named for the user, never guessed. */
|
|
573
|
+
export type BundledPathEntry =
|
|
574
|
+
| { status: "recorded"; description: string; remove: () => { removed: boolean; note?: string } }
|
|
575
|
+
| { status: "none" }
|
|
576
|
+
| { status: "invalid"; reason: string };
|
|
577
|
+
|
|
578
|
+
export interface BundledSelfUninstallInput extends Pick<SelfUninstallInput, "args" | "env" | "homedir" | "platform" | "interactive" | "ask" | "out" | "err"> {
|
|
579
|
+
/** The prefix of the bundled install this launcher runs from (resolveBundledInstall), a real path. */
|
|
580
|
+
root: string;
|
|
581
|
+
pathEntry: BundledPathEntry;
|
|
582
|
+
/** This process: on Windows its node.exe and the .cmd launcher cannot be removed while it runs. */
|
|
583
|
+
process: { pid: number; execPath: string };
|
|
584
|
+
/** Windows: removes `files`, then `last` (the prefix marker) once nothing is left at those paths,
|
|
585
|
+
* then each of `directories` that is empty, once process `pid` exited. */
|
|
586
|
+
deferRemoval?: (request: { pid: number; files: string[]; last?: string; directories: string[] }) => void;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// What scripts/bundled-install.mjs writes in the prefix, by name.
|
|
590
|
+
const BUNDLED_OWN: readonly { name: string; label: string; kind: "dir" | "file" | "current" }[] = Object.freeze([
|
|
591
|
+
{ name: "bin", label: "the gentle-shell launcher", kind: "dir" },
|
|
592
|
+
{ name: "current", label: "the active version", kind: "current" },
|
|
593
|
+
{ name: "versions", label: "the installed Gentle Shell versions", kind: "dir" },
|
|
594
|
+
{ name: "versions.history", label: "the version history", kind: "file" },
|
|
595
|
+
{ name: "runtime", label: "the bundled Node.js, npm, pnpm, Go and Engram", kind: "dir" },
|
|
596
|
+
{ name: "pnpm", label: "pnpm's store, cache, state and settings", kind: "dir" },
|
|
597
|
+
{ name: "npm", label: "npm's cache, prefix and settings", kind: "dir" },
|
|
598
|
+
{ name: "gentle-ai", label: "Gentle AI's state for this install", kind: "dir" },
|
|
599
|
+
{ name: "config", label: "Gentle Shell's settings for this install (config/gentle-ai)", kind: "dir" },
|
|
600
|
+
{ name: "tmp", label: "temporary files", kind: "dir" },
|
|
601
|
+
{ name: "path-entry.json", label: "the PATH entry receipt", kind: "file" },
|
|
602
|
+
]);
|
|
603
|
+
// The Windows prefix's claim marker (ensureWindowsPrivateFolder, scripts/bundled-install.mjs).
|
|
604
|
+
const BUNDLED_WINDOWS_MARKER = Object.freeze({ name: ".gentle-shell-bundle", text: "gentle-shell bundled install" });
|
|
605
|
+
const BUNDLED_PROFILE_MARK = "# gentle-shell bundled install";
|
|
606
|
+
|
|
607
|
+
interface BundledPlan {
|
|
608
|
+
refusals: string[];
|
|
609
|
+
own: Removal[];
|
|
610
|
+
shared: Removal[];
|
|
611
|
+
kept: string[];
|
|
612
|
+
/** Nothing foreign is in the prefix: it goes too, once nothing is left in it. */
|
|
613
|
+
removeRoot: boolean;
|
|
614
|
+
/** Windows: files this process holds, removed after it exits. */
|
|
615
|
+
held: string[];
|
|
616
|
+
notTouched: NotTouched[];
|
|
617
|
+
shellHome: string;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function planBundledSelfUninstall(input: BundledSelfUninstallInput): BundledPlan {
|
|
621
|
+
const { env, homedir, platform, root } = input;
|
|
622
|
+
const windows = platform === "win32";
|
|
623
|
+
const same = (a: string, b: string) => comparable(a, platform) === comparable(b, platform);
|
|
624
|
+
const shellHome = join(homedir, ".gentle-shell");
|
|
625
|
+
const plan: BundledPlan = { refusals: [], own: [], shared: [], kept: [], removeRoot: false, held: [], notTouched: [], shellHome };
|
|
626
|
+
const refuse = (reason: string) => plan.refusals.push(`The bundled prefix ${root} ${reason}; refusing to remove anything.`);
|
|
627
|
+
const piHome = userPiHome(env, homedir);
|
|
628
|
+
const userGuards: Guard[] = [
|
|
629
|
+
{ path: homedir, label: "your home directory", inside: false },
|
|
630
|
+
{ path: join(homedir, ".pi"), label: "Pi's folder", inside: true },
|
|
631
|
+
{ path: join(homedir, ".gentle-ai"), label: "Gentle AI's state", inside: true },
|
|
632
|
+
{ path: join(homedir, ".engram"), label: "Engram's memory", inside: true },
|
|
633
|
+
];
|
|
634
|
+
const guards: Guard[] = [...userGuards, { path: piHome, label: "your Pi home", inside: true }];
|
|
635
|
+
|
|
636
|
+
// Ours only by proof: never through a link, the Windows claim marker, or on
|
|
637
|
+
// POSIX the one folder claimPrefix uses, as a real directory.
|
|
638
|
+
const real = canonical(root);
|
|
639
|
+
if (!same(real, resolve(root))) refuse(`is reached through a symbolic link (really ${real})`);
|
|
640
|
+
const rootCollision = collision(real, userGuards, platform);
|
|
641
|
+
if (rootCollision) refuse(`${rootCollision}`);
|
|
642
|
+
if (windows) {
|
|
643
|
+
const marker = join(root, BUNDLED_WINDOWS_MARKER.name);
|
|
644
|
+
const text = inspect(marker, "file") === "ok" ? readText(marker) : undefined;
|
|
645
|
+
if (text === undefined) refuse(`has no ${BUNDLED_WINDOWS_MARKER.name} marker`);
|
|
646
|
+
else if (text.replace(/\r?\n$/, "") !== BUNDLED_WINDOWS_MARKER.text) refuse(`has a ${BUNDLED_WINDOWS_MARKER.name} marker that is not the bundled install's`);
|
|
647
|
+
} else if (inspect(shellHome, "dir") !== "ok" || !same(canonical(shellHome), real)) {
|
|
648
|
+
refuse(`is not ${shellHome}, the folder a bundled install uses, as a real directory`);
|
|
649
|
+
}
|
|
650
|
+
let names: string[] = [];
|
|
651
|
+
try {
|
|
652
|
+
names = readdirSync(root);
|
|
653
|
+
} catch {
|
|
654
|
+
refuse("cannot be read");
|
|
655
|
+
}
|
|
656
|
+
if (plan.refusals.length > 0) return plan;
|
|
657
|
+
|
|
658
|
+
let foreign = false;
|
|
659
|
+
const consider = (path: string, label: string, kind: "dir" | "file" | "current", into: Removal[]) => {
|
|
660
|
+
const removalKind = kind === "current" ? (windows ? "file" : "link") : kind;
|
|
661
|
+
const state = inspect(path, removalKind);
|
|
662
|
+
if (state === "missing") return;
|
|
663
|
+
const ours = state === "ok" && (removalKind !== "link" || /^versions\/[^/]+$/.test(readlinkSync(path)));
|
|
664
|
+
if (ours) {
|
|
665
|
+
into.push({ path, label, kind: removalKind });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (same(dirname(path), root)) foreign = true;
|
|
669
|
+
const what = removalKind === "link" ? "a link to an installed version" : removalKind === "dir" ? "a directory" : "a regular file";
|
|
670
|
+
plan.kept.push(`${path}: ${state === "symlink" ? "a symbolic link, not followed" : `not ${what}`}`);
|
|
671
|
+
};
|
|
672
|
+
for (const item of BUNDLED_OWN) consider(join(root, item.name), item.label, item.kind, plan.own);
|
|
673
|
+
if (windows) consider(join(root, BUNDLED_WINDOWS_MARKER.name), "the bundled install's folder marker", "file", plan.own);
|
|
674
|
+
consider(join(shellHome, "agent"), "your sign-ins, chats, settings and extensions", "dir", plan.shared);
|
|
675
|
+
consider(join(shellHome, "config.json"), "the launcher's settings", "file", plan.shared);
|
|
676
|
+
const known = new Set([...BUNDLED_OWN.map((item) => item.name), ...(windows ? [BUNDLED_WINDOWS_MARKER.name] : []),
|
|
677
|
+
...(same(canonical(shellHome), real) ? ["agent", "config.json"] : [])]);
|
|
678
|
+
for (const name of names) {
|
|
679
|
+
if (known.has(name)) continue;
|
|
680
|
+
foreign = true;
|
|
681
|
+
plan.kept.push(`${join(root, name)}: not written by the bundled install`);
|
|
682
|
+
}
|
|
683
|
+
plan.removeRoot = !foreign;
|
|
684
|
+
|
|
685
|
+
for (const removal of [...plan.own, ...plan.shared]) {
|
|
686
|
+
const reason = collision(canonical(removal.path), guards, platform);
|
|
687
|
+
if (reason) plan.refusals.push(`${removal.path} would be removed, but it ${reason}; refusing to remove anything.`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
plan.notTouched.push({ path: piHome, label: "your Pi home" }, { path: join(homedir, ".pi", "gentle-ai"), label: "Gentle AI's configuration for Pi" },
|
|
691
|
+
{ path: join(homedir, ".gentle-ai"), label: "Gentle AI's own state" }, { path: join(homedir, ".engram"), label: "Engram's memory" });
|
|
692
|
+
if (env.GENTLE_SHELL_HOME && !same(canonical(env.GENTLE_SHELL_HOME), canonical(join(shellHome, "agent")))) {
|
|
693
|
+
plan.notTouched.push({ path: env.GENTLE_SHELL_HOME, label: "the Gentle Shell home GENTLE_SHELL_HOME names" });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Windows: the running node.exe, the .cmd launcher and the marker (last) wait for this process to exit.
|
|
697
|
+
if (windows) {
|
|
698
|
+
const candidates = [canonical(input.process.execPath), join(root, "bin", "gentle-shell.cmd"), join(root, BUNDLED_WINDOWS_MARKER.name)];
|
|
699
|
+
plan.held = candidates.filter((path, index) => candidates.indexOf(path) === index && inspect(path, "file") === "ok"
|
|
700
|
+
&& plan.own.some((removal) => within(removal.path, path, platform)));
|
|
701
|
+
}
|
|
702
|
+
return plan;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Removes `directory`'s contents except the `held` files and the folders above them. */
|
|
706
|
+
function removeExcept(directory: string, held: readonly string[], platform: string) {
|
|
707
|
+
if (inspect(directory, "dir") !== "ok") throw new Error("it changed after the plan was made");
|
|
708
|
+
for (const name of readdirSync(directory)) {
|
|
709
|
+
const path = join(directory, name);
|
|
710
|
+
if (held.some((file) => comparable(file, platform) === comparable(path, platform))) continue;
|
|
711
|
+
if (held.some((file) => within(path, file, platform))) removeExcept(path, held, platform);
|
|
712
|
+
else rmSync(path, { recursive: true });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** The folders the deferred removal empties, deepest first, then the prefix when it may go. */
|
|
717
|
+
function heldDirectories(plan: BundledPlan, root: string, platform: string): string[] {
|
|
718
|
+
const directories: string[] = [];
|
|
719
|
+
for (const file of plan.held) {
|
|
720
|
+
for (let folder = dirname(file); within(root, folder, platform) && comparable(folder, platform) !== comparable(root, platform); folder = dirname(folder)) {
|
|
721
|
+
if (!directories.includes(folder)) directories.push(folder);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const depth = (path: string) => path.split(/[\\/]+/).length;
|
|
725
|
+
return [...directories.sort((a, b) => depth(b) - depth(a)), ...(plan.removeRoot ? [root] : [])];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function manualPathEntry(root: string, platform: string): string {
|
|
729
|
+
return platform === "win32"
|
|
730
|
+
? `If a new terminal still finds gentle-shell, remove ${join(root, "bin")} from your user Path (HKCU\\Environment).`
|
|
731
|
+
: `If a new terminal still finds gentle-shell, remove the line ending in "${BUNDLED_PROFILE_MARK}" from your shell's startup file, `
|
|
732
|
+
+ `or the gentle-shell link in ~/.local/bin to ${join(root, "bin", "gentle-shell")}.`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function printBundledPlan(plan: BundledPlan, input: BundledSelfUninstallInput, options: SelfUninstallOptions) {
|
|
736
|
+
const { out, pathEntry, root } = input;
|
|
737
|
+
out(`Gentle Shell self-uninstall plan (bundled install in ${root})`);
|
|
738
|
+
out("Remove the PATH entry:");
|
|
739
|
+
out(pathEntry.status === "recorded" ? ` ${pathEntry.description}` : " (none recorded; see below)");
|
|
740
|
+
out("Remove the bundled install:");
|
|
741
|
+
if (plan.refusals.length > 0) out(" (refused, see below)");
|
|
742
|
+
for (const removal of plan.refusals.length > 0 ? [] : plan.own) out(` ${removal.path} (${removal.label})`);
|
|
743
|
+
if (plan.refusals.length === 0 && plan.removeRoot) out(` ${root} itself, once nothing else is left in it`);
|
|
744
|
+
if (plan.shared.length > 0 && plan.refusals.length === 0) {
|
|
745
|
+
const disposition = options.includeShared
|
|
746
|
+
? "removed (--include-shared)"
|
|
747
|
+
: options.yes || !input.interactive ? "kept; pass --include-shared to remove it" : "kept unless you accept when asked";
|
|
748
|
+
out(`Your Gentle Shell home, shared with every Gentle Shell install, ${disposition}:`);
|
|
749
|
+
for (const removal of plan.shared) out(` ${removal.path} (${removal.label})`);
|
|
750
|
+
}
|
|
751
|
+
if (plan.kept.length > 0) {
|
|
752
|
+
out("Kept:");
|
|
753
|
+
for (const line of plan.kept) out(` ${line}`);
|
|
754
|
+
}
|
|
755
|
+
out("Not touched:");
|
|
756
|
+
for (const kept of plan.notTouched) out(kept.path === undefined ? ` ${kept.label}` : ` ${kept.path} (${kept.label})`);
|
|
757
|
+
if (pathEntry.status !== "recorded") {
|
|
758
|
+
out("The PATH entry was not recorded, so it is not guessed at; remove it yourself:");
|
|
759
|
+
if (pathEntry.status === "invalid") out(` ${pathEntry.reason}`);
|
|
760
|
+
out(` ${manualPathEntry(root, input.platform)}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Runs `gentle-shell self-uninstall` for the bundled install at `root`. Exit
|
|
766
|
+
* codes as runSelfUninstall: 0 done or dry run, 1 refused, cancelled or failed,
|
|
767
|
+
* 2 usage. Order: the recorded PATH entry, the prefix's own folders, then the
|
|
768
|
+
* shared home when asked, and the prefix once nothing is left in it. On
|
|
769
|
+
* Windows the files this process holds are removed after it exits.
|
|
770
|
+
*/
|
|
771
|
+
export async function runBundledSelfUninstall(input: BundledSelfUninstallInput): Promise<number> {
|
|
772
|
+
const { out, err, platform, root } = input;
|
|
773
|
+
const options = parseSelfUninstallArgs(input.args);
|
|
774
|
+
if ("error" in options) {
|
|
775
|
+
err(`gentle-shell self-uninstall: ${options.error}`);
|
|
776
|
+
return 2;
|
|
777
|
+
}
|
|
778
|
+
const plan = planBundledSelfUninstall(input);
|
|
779
|
+
printBundledPlan(plan, input, options);
|
|
780
|
+
if (plan.refusals.length > 0) {
|
|
781
|
+
for (const line of plan.refusals) err(`gentle-shell self-uninstall: ${line}`);
|
|
782
|
+
err("Nothing was removed.");
|
|
783
|
+
return 1;
|
|
784
|
+
}
|
|
785
|
+
if (options.dryRun) {
|
|
786
|
+
out("Dry run: nothing was removed.");
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
const confirmation = await confirm(input, options, plan.shared.length > 0,
|
|
790
|
+
"Also remove your Gentle Shell home (sign-ins, chats, settings and extensions) listed above? [y/N] ");
|
|
791
|
+
if ("code" in confirmation) return confirmation.code;
|
|
792
|
+
|
|
793
|
+
// A PATH entry the user changed since the install is kept, and said so at the end.
|
|
794
|
+
let pathKept: string | undefined;
|
|
795
|
+
if (input.pathEntry.status === "recorded") {
|
|
796
|
+
try {
|
|
797
|
+
const result = input.pathEntry.remove();
|
|
798
|
+
if (result.removed === false) pathKept = result.note ?? input.pathEntry.description;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
err(`gentle-shell self-uninstall: could not remove ${input.pathEntry.description}: ${error instanceof Error ? error.message : String(error)}`);
|
|
801
|
+
err("Nothing else was removed; fix the cause and run gentle-shell self-uninstall again.");
|
|
802
|
+
return 1;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const held = (path: string) => plan.held.some((file) => comparable(file, platform) === comparable(path, platform));
|
|
806
|
+
for (const removal of [...plan.own, ...(confirmation.includeShared ? plan.shared : [])]) {
|
|
807
|
+
try {
|
|
808
|
+
if (held(removal.path)) continue;
|
|
809
|
+
if (plan.held.some((file) => within(removal.path, file, platform))) removeExcept(removal.path, plan.held, platform);
|
|
810
|
+
else remove(removal);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
err(`gentle-shell self-uninstall: could not remove ${removal.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
813
|
+
err("Everything listed after it was kept; fix the cause and run gentle-shell self-uninstall again.");
|
|
814
|
+
return 1;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
if (confirmation.includeShared) removeIfEmpty(plan.shellHome);
|
|
819
|
+
if (plan.removeRoot && plan.held.length === 0) removeIfEmpty(root);
|
|
820
|
+
} catch {
|
|
821
|
+
// A leftover empty folder is harmless.
|
|
822
|
+
}
|
|
823
|
+
if (plan.held.length > 0) {
|
|
824
|
+
try {
|
|
825
|
+
if (!input.deferRemoval) throw new Error("no deferred removal on this system");
|
|
826
|
+
// The marker goes last: while it stays, a failed removal can be retried with self-uninstall.
|
|
827
|
+
const marker = join(root, BUNDLED_WINDOWS_MARKER.name);
|
|
828
|
+
const isMarker = (file: string) => comparable(file, platform) === comparable(marker, platform);
|
|
829
|
+
input.deferRemoval({ pid: input.process.pid, files: plan.held.filter((file) => !isMarker(file)), last: plan.held.find(isMarker),
|
|
830
|
+
directories: heldDirectories(plan, root, platform) });
|
|
831
|
+
} catch (error) {
|
|
832
|
+
err(`gentle-shell self-uninstall: ${error instanceof Error ? error.message : String(error)}; once this command exits, delete ${root} yourself.`);
|
|
833
|
+
return 1;
|
|
834
|
+
}
|
|
835
|
+
out(`Windows cannot remove the running Gentle Shell: ${plan.held.join(", ")} ${plan.held.length === 1 ? "is" : "are"} removed in the background after this command exits.`);
|
|
836
|
+
}
|
|
837
|
+
if (pathKept !== undefined) out(`The PATH entry was kept: ${pathKept}. Remove it yourself if nothing else uses it.`);
|
|
838
|
+
out("Gentle Shell was removed.");
|
|
839
|
+
return 0;
|
|
840
|
+
}
|