@herbertgao/pi-extensions 2026.8.3 → 2026.8.5
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 +45 -4
- package/examples/pi-footer.json +275 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +8 -7
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
- package/node_modules/pi-footer/CHANGELOG.md +145 -0
- package/node_modules/pi-footer/LICENSE +21 -0
- package/node_modules/pi-footer/README.md +354 -0
- package/node_modules/pi-footer/package.json +68 -0
- package/node_modules/pi-footer/src/cache.ts +106 -0
- package/node_modules/pi-footer/src/colors.ts +237 -0
- package/node_modules/pi-footer/src/config.ts +202 -0
- package/node_modules/pi-footer/src/event-widgets.ts +41 -0
- package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
- package/node_modules/pi-footer/src/git.ts +135 -0
- package/node_modules/pi-footer/src/index.ts +268 -0
- package/node_modules/pi-footer/src/metrics.ts +141 -0
- package/node_modules/pi-footer/src/presets.ts +383 -0
- package/node_modules/pi-footer/src/render.ts +114 -0
- package/node_modules/pi-footer/src/separators.ts +52 -0
- package/node_modules/pi-footer/src/types.ts +122 -0
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
- package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
- package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
- package/node_modules/pi-footer/src/ui/events.ts +35 -0
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
- package/node_modules/pi-footer/src/ui/fields.ts +252 -0
- package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
- package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
- package/node_modules/pi-footer/src/ui/layout.ts +17 -0
- package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
- package/node_modules/pi-footer/src/ui/model.ts +64 -0
- package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
- package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
- package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
- package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
- package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
- package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
- package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
- package/node_modules/pi-footer/src/ui/screen.ts +243 -0
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
- package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
- package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
- package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
- package/node_modules/pi-footer/src/ui/theme.ts +48 -0
- package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
- package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
- package/node_modules/pi-footer/src/ui.ts +66 -0
- package/node_modules/pi-footer/src/widgets/context.ts +28 -0
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
- package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
- package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
- package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
- package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
- package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
- package/node_modules/pi-footer/src/widgets/options.ts +122 -0
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
- package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/store.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/types.ts +181 -0
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
- package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
- package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
- package/node_modules/pi-lens/CHANGELOG.md +2440 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
- package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
- package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
- package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +49361 -36888
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +211 -44
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/docs/agent-guide.md +2 -2
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +31 -2
- package/node_modules/pi-lens/docs/globalconfig.md +45 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +12 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +18 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
- package/node_modules/pi-mcp-adapter/README.md +34 -3
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +93 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/package.json +17 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* - pip packages
|
|
46
46
|
* - GitHub releases (platform-specific binaries → ~/.pi-lens/bin/)
|
|
47
47
|
*/
|
|
48
|
+
import { logExtension } from "../extension-log.js";
|
|
48
49
|
import { spawn } from "node:child_process";
|
|
49
50
|
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
50
51
|
import fs from "node:fs/promises";
|
|
@@ -52,17 +53,29 @@ import https from "node:https";
|
|
|
52
53
|
import { createRequire } from "node:module";
|
|
53
54
|
import os from "node:os";
|
|
54
55
|
import path from "node:path";
|
|
56
|
+
import { BoundedLruCache } from "../bounded-cache.js";
|
|
57
|
+
import { isFullyQualified } from "../path-utils.js";
|
|
58
|
+
import { assertInstallAllowed, projectTrustDenialReason, } from "../project-trust.js";
|
|
55
59
|
const _installerRequire = createRequire(import.meta.url);
|
|
56
60
|
import { createGunzip } from "node:zlib";
|
|
57
61
|
import { logSessionStart } from "../sessionstart-logger.js";
|
|
62
|
+
import { commitDurableStoreAsync } from "../durable-store.js";
|
|
58
63
|
import { getGlobalPiLensDir } from "../file-utils.js";
|
|
59
64
|
import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
|
|
60
|
-
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
65
|
+
import { resetSafeSpawnWindowsCommandCache, safeSpawnAsync, } from "../safe-spawn.js";
|
|
61
66
|
// Global installation directory for pi-lens tools
|
|
62
67
|
const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
|
|
63
68
|
const INSTALL_LOCK_PATH = path.join(TOOLS_DIR, ".install.lock");
|
|
64
69
|
const activeInstallLocks = new Set();
|
|
65
70
|
let installLockExitCleanupRegistered = false;
|
|
71
|
+
/**
|
|
72
|
+
* The managed tools tree, for callers that need to tell a path `getToolPath()`
|
|
73
|
+
* returned from the managed install apart from a global/PATH hit (the tool
|
|
74
|
+
* registry is this module's business — don't re-derive `<pi-lens home>/tools`).
|
|
75
|
+
*/
|
|
76
|
+
export function getManagedToolsDir() {
|
|
77
|
+
return TOOLS_DIR;
|
|
78
|
+
}
|
|
66
79
|
function isProcessAlive(pid) {
|
|
67
80
|
try {
|
|
68
81
|
process.kill(pid, 0);
|
|
@@ -164,12 +177,27 @@ async function acquireInstallLock() {
|
|
|
164
177
|
const GITHUB_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
|
|
165
178
|
// Debug flag - set via PI_LENS_DEBUG=1 or --debug
|
|
166
179
|
const DEBUG = process.env.PI_LENS_DEBUG === "1" || process.argv.includes("--debug");
|
|
180
|
+
/** Test-only platform seam for Windows resource-layout coverage on Linux CI. */
|
|
181
|
+
function installerPlatform() {
|
|
182
|
+
const override = process.env.PI_LENS_TEST_PLATFORM;
|
|
183
|
+
if (override === "win32" || override === "linux") {
|
|
184
|
+
return override;
|
|
185
|
+
}
|
|
186
|
+
return process.platform;
|
|
187
|
+
}
|
|
167
188
|
/**
|
|
168
189
|
* Log debug messages only when DEBUG is enabled
|
|
169
190
|
*/
|
|
170
191
|
function debugLog(...args) {
|
|
171
192
|
if (DEBUG) {
|
|
172
|
-
|
|
193
|
+
// #1333: DEBUG gate preserved; sink is extension.log, never the TUI.
|
|
194
|
+
logExtension({
|
|
195
|
+
subsystem: "auto-install",
|
|
196
|
+
level: "debug",
|
|
197
|
+
message: args
|
|
198
|
+
.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
|
|
199
|
+
.join(" "),
|
|
200
|
+
});
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
/**
|
|
@@ -177,16 +205,25 @@ function debugLog(...args) {
|
|
|
177
205
|
* the copy-pasted `if (platform === "linux") return arch === "arm64" ? … : …`
|
|
178
206
|
* ladder that several release entries repeat verbatim. Each platform maps to its
|
|
179
207
|
* `x64` (default) and optional `arm64` asset substring; a missing platform or
|
|
180
|
-
* arch ⇒ unsupported (`undefined`)
|
|
208
|
+
* arch ⇒ unsupported (`undefined`). Arches outside x64/arm64 (ia32, ppc64, …)
|
|
209
|
+
* are unsupported too — no release entry ships a 32-bit or exotic asset, and
|
|
210
|
+
* handing back the x64 substring there would install an unrunnable binary.
|
|
181
211
|
*/
|
|
182
212
|
function archAssetMatch(table) {
|
|
183
213
|
return (platform, arch) => {
|
|
184
|
-
|
|
185
|
-
if (!entry)
|
|
214
|
+
if (arch !== "x64" && arch !== "arm64")
|
|
186
215
|
return undefined;
|
|
187
|
-
return arch
|
|
216
|
+
return table[platform]?.[arch];
|
|
188
217
|
};
|
|
189
218
|
}
|
|
219
|
+
// Go-style `<os>_<arch>.zip` release assets, shared verbatim by tflint and
|
|
220
|
+
// terraform-ls — both entries carried byte-identical ladders, down to the asset
|
|
221
|
+
// strings themselves.
|
|
222
|
+
const OS_ARCH_ZIP_ASSETS = {
|
|
223
|
+
linux: { x64: "linux_amd64.zip", arm64: "linux_arm64.zip" },
|
|
224
|
+
darwin: { x64: "darwin_amd64.zip", arm64: "darwin_arm64.zip" },
|
|
225
|
+
win32: { x64: "windows_amd64.zip", arm64: "windows_arm64.zip" },
|
|
226
|
+
};
|
|
190
227
|
export const TOOLS = [
|
|
191
228
|
// Core LSP servers
|
|
192
229
|
{
|
|
@@ -418,6 +455,31 @@ export const TOOLS = [
|
|
|
418
455
|
},
|
|
419
456
|
},
|
|
420
457
|
},
|
|
458
|
+
{
|
|
459
|
+
id: "helm",
|
|
460
|
+
name: "Helm",
|
|
461
|
+
checkCommand: "helm",
|
|
462
|
+
checkArgs: ["version", "--short"],
|
|
463
|
+
installStrategy: "github",
|
|
464
|
+
binaryName: "helm",
|
|
465
|
+
github: {
|
|
466
|
+
repo: "helm/helm",
|
|
467
|
+
assetMatch: (platform, arch) => {
|
|
468
|
+
// helm publishes per-OS archives: tar.gz for POSIX, zip for Windows.
|
|
469
|
+
const cpu = arch === "arm64" ? "arm64" : "amd64";
|
|
470
|
+
if (platform === "linux")
|
|
471
|
+
return `linux-${cpu}.tar.gz`;
|
|
472
|
+
if (platform === "darwin")
|
|
473
|
+
return `darwin-${cpu}.tar.gz`;
|
|
474
|
+
if (platform === "win32")
|
|
475
|
+
return `windows-${cpu}.zip`;
|
|
476
|
+
return undefined;
|
|
477
|
+
},
|
|
478
|
+
// Release archives nest the executable under an OS/arch directory;
|
|
479
|
+
// the installer searches recursively and adds the Windows suffix.
|
|
480
|
+
binaryInArchive: "helm",
|
|
481
|
+
},
|
|
482
|
+
},
|
|
421
483
|
{
|
|
422
484
|
// Opengrep: a single standalone binary per platform on GitHub releases —
|
|
423
485
|
// no login, no telemetry (the reason for switching off Semgrep, #111).
|
|
@@ -919,18 +981,39 @@ export const TOOLS = [
|
|
|
919
981
|
binaryName: "tflint",
|
|
920
982
|
github: {
|
|
921
983
|
repo: "terraform-linters/tflint",
|
|
922
|
-
assetMatch: (
|
|
923
|
-
if (platform === "linux")
|
|
924
|
-
return arch === "arm64" ? "linux_arm64.zip" : "linux_amd64.zip";
|
|
925
|
-
if (platform === "darwin")
|
|
926
|
-
return arch === "arm64" ? "darwin_arm64.zip" : "darwin_amd64.zip";
|
|
927
|
-
if (platform === "win32")
|
|
928
|
-
return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
|
|
929
|
-
return undefined;
|
|
930
|
-
},
|
|
984
|
+
assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
|
|
931
985
|
binaryInArchive: "tflint",
|
|
932
986
|
},
|
|
933
987
|
},
|
|
988
|
+
{
|
|
989
|
+
// Terragrunt ships a bare native binary per platform on GitHub releases.
|
|
990
|
+
// Windows arm64 uses the x64 binary through Windows' built-in emulation —
|
|
991
|
+
// there is no terragrunt_windows_arm64.exe upstream.
|
|
992
|
+
id: "terragrunt",
|
|
993
|
+
name: "terragrunt",
|
|
994
|
+
checkCommand: "terragrunt",
|
|
995
|
+
checkArgs: ["--version"],
|
|
996
|
+
installStrategy: "github",
|
|
997
|
+
binaryName: "terragrunt",
|
|
998
|
+
github: {
|
|
999
|
+
repo: "gruntwork-io/terragrunt",
|
|
1000
|
+
assetMatch: archAssetMatch({
|
|
1001
|
+
linux: {
|
|
1002
|
+
x64: "terragrunt_linux_amd64",
|
|
1003
|
+
arm64: "terragrunt_linux_arm64",
|
|
1004
|
+
},
|
|
1005
|
+
darwin: {
|
|
1006
|
+
x64: "terragrunt_darwin_amd64",
|
|
1007
|
+
arm64: "terragrunt_darwin_arm64",
|
|
1008
|
+
},
|
|
1009
|
+
win32: {
|
|
1010
|
+
x64: "terragrunt_windows_amd64.exe",
|
|
1011
|
+
arm64: "terragrunt_windows_amd64.exe",
|
|
1012
|
+
},
|
|
1013
|
+
}),
|
|
1014
|
+
// bare binary — no binaryInArchive
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
934
1017
|
{
|
|
935
1018
|
id: "gitleaks",
|
|
936
1019
|
name: "gitleaks",
|
|
@@ -1055,15 +1138,7 @@ export const TOOLS = [
|
|
|
1055
1138
|
github: {
|
|
1056
1139
|
repo: "hashicorp/terraform-ls",
|
|
1057
1140
|
hashiCorpReleaseProduct: "terraform-ls",
|
|
1058
|
-
assetMatch: (
|
|
1059
|
-
if (platform === "linux")
|
|
1060
|
-
return arch === "arm64" ? "linux_arm64.zip" : "linux_amd64.zip";
|
|
1061
|
-
if (platform === "darwin")
|
|
1062
|
-
return arch === "arm64" ? "darwin_arm64.zip" : "darwin_amd64.zip";
|
|
1063
|
-
if (platform === "win32")
|
|
1064
|
-
return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
|
|
1065
|
-
return undefined;
|
|
1066
|
-
},
|
|
1141
|
+
assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
|
|
1067
1142
|
binaryInArchive: "terraform-ls",
|
|
1068
1143
|
},
|
|
1069
1144
|
},
|
|
@@ -1221,33 +1296,164 @@ export function getInstallFailureReason(toolId) {
|
|
|
1221
1296
|
return installFailureReasons.get(toolId);
|
|
1222
1297
|
}
|
|
1223
1298
|
// Session-lifetime cache: once a tool path is resolved, skip the process-spawn check on subsequent calls.
|
|
1224
|
-
const resolvedPathCache = new
|
|
1299
|
+
const resolvedPathCache = new BoundedLruCache(256);
|
|
1225
1300
|
const PROBE_CACHE_PATH = path.join(getGlobalPiLensDir(), "probe-cache.json");
|
|
1301
|
+
const PROBE_CACHE_LOCK_STALE_MS = 180_000;
|
|
1226
1302
|
const PROBE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1303
|
+
const PROBE_CACHE_FLUSH_LOCK_WAIT_MS = 250;
|
|
1304
|
+
const PROBE_CACHE_FLUSH_RETRY_DELAY_MS = 300;
|
|
1305
|
+
const PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS = 30_000;
|
|
1227
1306
|
let _probeCache = null;
|
|
1228
1307
|
let _probeCacheDirty = false;
|
|
1229
1308
|
let _probeCacheFlushTimer = null;
|
|
1309
|
+
let _probeCacheWriteInFlight = null;
|
|
1310
|
+
let _probeCacheRetryAttempt = 0;
|
|
1311
|
+
let _probeCacheChangeGeneration = 0;
|
|
1312
|
+
// Read-modify-write deltas let a long-lived process merge with entries another
|
|
1313
|
+
// process discovered after this process's initial read. `null` is a deliberate
|
|
1314
|
+
// deletion (TTL/stale-path eviction), not an absent map value.
|
|
1315
|
+
const _probeCacheChanges = new Map();
|
|
1316
|
+
const _probeCacheChangeVersions = new Map();
|
|
1230
1317
|
async function readProbeCache() {
|
|
1231
1318
|
if (_probeCache !== null)
|
|
1232
1319
|
return _probeCache;
|
|
1233
1320
|
try {
|
|
1234
1321
|
const raw = await fs.readFile(PROBE_CACHE_PATH, "utf-8");
|
|
1235
|
-
|
|
1322
|
+
const parsed = JSON.parse(raw);
|
|
1323
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1324
|
+
throw new Error("probe-cache root is not an object");
|
|
1325
|
+
}
|
|
1326
|
+
_probeCache = parsed;
|
|
1236
1327
|
}
|
|
1237
|
-
catch {
|
|
1328
|
+
catch (err) {
|
|
1329
|
+
const code = err?.code;
|
|
1330
|
+
if (code !== "ENOENT") {
|
|
1331
|
+
logSessionStart(`auto-install probe-cache: read failed (${code ?? "invalid"}); treating cache as unavailable`);
|
|
1332
|
+
}
|
|
1238
1333
|
_probeCache = {};
|
|
1239
1334
|
}
|
|
1240
1335
|
return _probeCache;
|
|
1241
1336
|
}
|
|
1242
|
-
function
|
|
1337
|
+
function markProbeCacheChange(toolId, entry) {
|
|
1338
|
+
_probeCacheChanges.set(toolId, entry);
|
|
1339
|
+
_probeCacheChangeVersions.set(toolId, ++_probeCacheChangeGeneration);
|
|
1340
|
+
_probeCacheDirty = true;
|
|
1341
|
+
scheduleProbeFlush();
|
|
1342
|
+
}
|
|
1343
|
+
function scheduleProbeFlush(delayMs = PROBE_CACHE_FLUSH_RETRY_DELAY_MS) {
|
|
1243
1344
|
if (_probeCacheFlushTimer !== null)
|
|
1244
1345
|
return;
|
|
1245
1346
|
_probeCacheFlushTimer = setTimeout(() => {
|
|
1246
1347
|
void flushProbeCache();
|
|
1247
|
-
},
|
|
1348
|
+
}, delayMs);
|
|
1248
1349
|
_probeCacheFlushTimer.unref?.();
|
|
1249
1350
|
}
|
|
1250
|
-
|
|
1351
|
+
function scheduleProbeFlushRetry() {
|
|
1352
|
+
const delay = Math.min(PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS, PROBE_CACHE_FLUSH_RETRY_DELAY_MS * 2 ** Math.min(_probeCacheRetryAttempt, 6));
|
|
1353
|
+
_probeCacheRetryAttempt += 1;
|
|
1354
|
+
scheduleProbeFlush(delay);
|
|
1355
|
+
}
|
|
1356
|
+
function snapshotProbeCacheChanges() {
|
|
1357
|
+
return {
|
|
1358
|
+
changes: new Map(_probeCacheChanges),
|
|
1359
|
+
versions: new Map(_probeCacheChangeVersions),
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function deserializeProbeCache(contents) {
|
|
1363
|
+
if (contents === undefined)
|
|
1364
|
+
return {};
|
|
1365
|
+
const parsed = JSON.parse(contents);
|
|
1366
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1367
|
+
throw new Error("probe-cache root is not an object");
|
|
1368
|
+
}
|
|
1369
|
+
return parsed;
|
|
1370
|
+
}
|
|
1371
|
+
function applyProbeCacheChanges(disk, changes) {
|
|
1372
|
+
for (const [toolId, entry] of changes) {
|
|
1373
|
+
if (entry === null)
|
|
1374
|
+
delete disk[toolId];
|
|
1375
|
+
else
|
|
1376
|
+
disk[toolId] = entry;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Install-lifetime ageing belongs to the authoritative merge, not only to the
|
|
1381
|
+
* lookup path: a sibling process may have left expired entries on disk since
|
|
1382
|
+
* this process loaded its in-memory snapshot. The async durable-store lock
|
|
1383
|
+
* supplies the probe cache's former quarantine/stale-owner recovery.
|
|
1384
|
+
*/
|
|
1385
|
+
function ageProbeCache(disk) {
|
|
1386
|
+
const cutoff = Date.now() - PROBE_CACHE_TTL_MS;
|
|
1387
|
+
for (const [toolId, entry] of Object.entries(disk)) {
|
|
1388
|
+
if (!Number.isFinite(entry.cachedAt) || entry.cachedAt < cutoff) {
|
|
1389
|
+
delete disk[toolId];
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function publishProbeCacheWrite(disk, snapshotVersions) {
|
|
1394
|
+
// Publish the sibling-process merge plus any updates that arrived during
|
|
1395
|
+
// the write. Only an unchanged snapshot entry is retired.
|
|
1396
|
+
const pendingAfterWrite = new Map(_probeCacheChanges);
|
|
1397
|
+
_probeCache = disk;
|
|
1398
|
+
for (const [toolId, entry] of pendingAfterWrite) {
|
|
1399
|
+
if (entry === null)
|
|
1400
|
+
delete _probeCache[toolId];
|
|
1401
|
+
else
|
|
1402
|
+
_probeCache[toolId] = entry;
|
|
1403
|
+
}
|
|
1404
|
+
for (const [toolId, version] of snapshotVersions) {
|
|
1405
|
+
if (_probeCacheChangeVersions.get(toolId) === version) {
|
|
1406
|
+
_probeCacheChanges.delete(toolId);
|
|
1407
|
+
_probeCacheChangeVersions.delete(toolId);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
_probeCacheDirty = _probeCacheChanges.size > 0;
|
|
1411
|
+
_probeCacheRetryAttempt = 0;
|
|
1412
|
+
return _probeCacheDirty ? "written-with-pending" : "written";
|
|
1413
|
+
}
|
|
1414
|
+
async function writeProbeCache() {
|
|
1415
|
+
try {
|
|
1416
|
+
// Snapshot versions before the awaited disk read/write. A new update for
|
|
1417
|
+
// the same tool may arrive while the atomic write is in flight; its newer
|
|
1418
|
+
// version must remain pending for the next flush.
|
|
1419
|
+
const { changes, versions } = snapshotProbeCacheChanges();
|
|
1420
|
+
let result = "written";
|
|
1421
|
+
const committed = await commitDurableStoreAsync({
|
|
1422
|
+
path: PROBE_CACHE_PATH,
|
|
1423
|
+
deserialize: deserializeProbeCache,
|
|
1424
|
+
merge: (disk) => {
|
|
1425
|
+
ageProbeCache(disk);
|
|
1426
|
+
applyProbeCacheChanges(disk, changes);
|
|
1427
|
+
return disk;
|
|
1428
|
+
},
|
|
1429
|
+
serialize: (disk) => JSON.stringify(disk, null, 2),
|
|
1430
|
+
waitMs: PROBE_CACHE_FLUSH_LOCK_WAIT_MS,
|
|
1431
|
+
retryMs: 25,
|
|
1432
|
+
staleMs: PROBE_CACHE_LOCK_STALE_MS,
|
|
1433
|
+
timeoutMessage: "Timed out waiting for probe-cache lock",
|
|
1434
|
+
onContention: "skip-log",
|
|
1435
|
+
logContention: () => {
|
|
1436
|
+
logSessionStart("auto-install probe-cache: flush deferred because another process owns the lock");
|
|
1437
|
+
},
|
|
1438
|
+
afterWriteLocked: (disk) => {
|
|
1439
|
+
result = publishProbeCacheWrite(disk, versions);
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
if (committed === undefined) {
|
|
1443
|
+
scheduleProbeFlushRetry();
|
|
1444
|
+
return "deferred";
|
|
1445
|
+
}
|
|
1446
|
+
return result;
|
|
1447
|
+
}
|
|
1448
|
+
catch (err) {
|
|
1449
|
+
// Keep dirty state so a later timer/explicit flush can retry. The error
|
|
1450
|
+
// is logged without paths, source, or command text; an unavailable cache
|
|
1451
|
+
// must never look like a clean empty cache to operators.
|
|
1452
|
+
logSessionStart(`auto-install probe-cache: flush failed (${err?.code ?? "write error"}); pending update retained`);
|
|
1453
|
+
scheduleProbeFlushRetry();
|
|
1454
|
+
return "failed";
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1251
1457
|
/** Await pending probe-cache persistence before a one-shot process exits. */
|
|
1252
1458
|
export async function flushProbeCache() {
|
|
1253
1459
|
if (_probeCacheFlushTimer !== null) {
|
|
@@ -1257,22 +1463,17 @@ export async function flushProbeCache() {
|
|
|
1257
1463
|
// #946 review F4: if the 300ms timer's write already started, the dirty
|
|
1258
1464
|
// flag is false but the write may still be in flight — an immediate
|
|
1259
1465
|
// process.exit would truncate it. Always await the in-flight write.
|
|
1260
|
-
if (
|
|
1466
|
+
if (_probeCacheWriteInFlight) {
|
|
1261
1467
|
await _probeCacheWriteInFlight;
|
|
1262
|
-
|
|
1468
|
+
if (!_probeCacheDirty)
|
|
1469
|
+
return "written";
|
|
1263
1470
|
}
|
|
1264
|
-
_probeCacheDirty
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
await fs.writeFile(PROBE_CACHE_PATH, JSON.stringify(_probeCache, null, 2));
|
|
1268
|
-
}
|
|
1269
|
-
catch {
|
|
1270
|
-
// Cache persistence is best effort; discovery remains authoritative.
|
|
1271
|
-
}
|
|
1272
|
-
})();
|
|
1471
|
+
if (!_probeCacheDirty || _probeCache === null)
|
|
1472
|
+
return "idle";
|
|
1473
|
+
const write = writeProbeCache();
|
|
1273
1474
|
_probeCacheWriteInFlight = write;
|
|
1274
1475
|
try {
|
|
1275
|
-
await write;
|
|
1476
|
+
return await write;
|
|
1276
1477
|
}
|
|
1277
1478
|
finally {
|
|
1278
1479
|
if (_probeCacheWriteInFlight === write)
|
|
@@ -1316,8 +1517,7 @@ export async function checkProbeCache(toolId) {
|
|
|
1316
1517
|
if (Date.now() - entry.cachedAt > PROBE_CACHE_TTL_MS) {
|
|
1317
1518
|
logSessionStart(`auto-install probe-cache ${toolId}: miss (ttl expired)`);
|
|
1318
1519
|
delete cache[toolId];
|
|
1319
|
-
|
|
1320
|
-
scheduleProbeFlush();
|
|
1520
|
+
markProbeCacheChange(toolId, null);
|
|
1321
1521
|
return undefined;
|
|
1322
1522
|
}
|
|
1323
1523
|
try {
|
|
@@ -1326,15 +1526,13 @@ export async function checkProbeCache(toolId) {
|
|
|
1326
1526
|
if (stat.mtimeMs !== entry.mtimeMs) {
|
|
1327
1527
|
logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime changed)`);
|
|
1328
1528
|
delete cache[toolId];
|
|
1329
|
-
|
|
1330
|
-
scheduleProbeFlush();
|
|
1529
|
+
markProbeCacheChange(toolId, null);
|
|
1331
1530
|
return undefined;
|
|
1332
1531
|
}
|
|
1333
1532
|
if (toolId === "ast-grep" && !(await verifyAstGrepProbePath(entry.path))) {
|
|
1334
1533
|
logSessionStart(`auto-install probe-cache ${toolId}: miss (not ast-grep: ${entry.path})`);
|
|
1335
1534
|
delete cache[toolId];
|
|
1336
|
-
|
|
1337
|
-
scheduleProbeFlush();
|
|
1535
|
+
markProbeCacheChange(toolId, null);
|
|
1338
1536
|
return undefined;
|
|
1339
1537
|
}
|
|
1340
1538
|
return entry.path;
|
|
@@ -1342,8 +1540,7 @@ export async function checkProbeCache(toolId) {
|
|
|
1342
1540
|
catch {
|
|
1343
1541
|
logSessionStart(`auto-install probe-cache ${toolId}: miss (gone: ${entry.path})`);
|
|
1344
1542
|
delete cache[toolId];
|
|
1345
|
-
|
|
1346
|
-
scheduleProbeFlush();
|
|
1543
|
+
markProbeCacheChange(toolId, null);
|
|
1347
1544
|
return undefined;
|
|
1348
1545
|
}
|
|
1349
1546
|
}
|
|
@@ -1352,13 +1549,13 @@ export async function updateProbeCache(toolId, resolvedPath) {
|
|
|
1352
1549
|
try {
|
|
1353
1550
|
const stat = await fs.stat(resolvedPath);
|
|
1354
1551
|
const cache = await readProbeCache();
|
|
1355
|
-
|
|
1552
|
+
const entry = {
|
|
1356
1553
|
path: resolvedPath,
|
|
1357
1554
|
mtimeMs: stat.mtimeMs,
|
|
1358
1555
|
cachedAt: Date.now(),
|
|
1359
1556
|
};
|
|
1360
|
-
|
|
1361
|
-
|
|
1557
|
+
cache[toolId] = entry;
|
|
1558
|
+
markProbeCacheChange(toolId, entry);
|
|
1362
1559
|
}
|
|
1363
1560
|
catch {
|
|
1364
1561
|
// best-effort
|
|
@@ -1368,16 +1565,33 @@ export async function updateProbeCache(toolId, resolvedPath) {
|
|
|
1368
1565
|
export function resetProbeCacheStateForTesting() {
|
|
1369
1566
|
_probeCache = null;
|
|
1370
1567
|
_probeCacheDirty = false;
|
|
1568
|
+
_probeCacheChanges.clear();
|
|
1569
|
+
_probeCacheChangeVersions.clear();
|
|
1570
|
+
_probeCacheChangeGeneration = 0;
|
|
1571
|
+
_probeCacheRetryAttempt = 0;
|
|
1371
1572
|
resolvedPathCache.clear();
|
|
1372
1573
|
ensureInFlight.clear();
|
|
1373
1574
|
installFailureReasons.clear();
|
|
1374
1575
|
lastManagedInstallVersion.clear();
|
|
1576
|
+
resetPathWalkMemo();
|
|
1375
1577
|
if (_probeCacheFlushTimer !== null) {
|
|
1376
1578
|
clearTimeout(_probeCacheFlushTimer);
|
|
1377
1579
|
_probeCacheFlushTimer = null;
|
|
1378
1580
|
}
|
|
1379
1581
|
}
|
|
1380
1582
|
// --- Check Functions ---
|
|
1583
|
+
const pathWalkMemo = new Map();
|
|
1584
|
+
function hashSync(value) {
|
|
1585
|
+
let hash = 0x811c9dc5;
|
|
1586
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1587
|
+
hash ^= value.charCodeAt(i);
|
|
1588
|
+
hash = Math.imul(hash, 0x01000193);
|
|
1589
|
+
}
|
|
1590
|
+
return (hash >>> 0).toString(16);
|
|
1591
|
+
}
|
|
1592
|
+
export function resetPathWalkMemo() {
|
|
1593
|
+
pathWalkMemo.clear();
|
|
1594
|
+
}
|
|
1381
1595
|
/**
|
|
1382
1596
|
* Check if a command is available in PATH by walking PATH entries and
|
|
1383
1597
|
* verifying each candidate is a real file with non-zero size.
|
|
@@ -1385,7 +1599,7 @@ export function resetProbeCacheStateForTesting() {
|
|
|
1385
1599
|
* spawning a process — ~μs per candidate vs ~50ms for which/where.
|
|
1386
1600
|
*/
|
|
1387
1601
|
export async function isCommandAvailable(command, _args) {
|
|
1388
|
-
const isWindows =
|
|
1602
|
+
const isWindows = installerPlatform() === "win32";
|
|
1389
1603
|
const pathEnv = process.env.PATH || process.env.Path || process.env.path || "";
|
|
1390
1604
|
const dirs = pathEnv.split(path.delimiter);
|
|
1391
1605
|
// On Windows, probe .exe, .cmd, and .bat extensions in addition to bare name.
|
|
@@ -1427,7 +1641,12 @@ export async function isSpawnableCommand(command) {
|
|
|
1427
1641
|
return false;
|
|
1428
1642
|
}
|
|
1429
1643
|
}
|
|
1430
|
-
|
|
1644
|
+
const memoKey = `${command}:${hashSync(process.env.PATH || "")}`;
|
|
1645
|
+
if (pathWalkMemo.has(memoKey))
|
|
1646
|
+
return pathWalkMemo.get(memoKey) ?? false;
|
|
1647
|
+
const isSpawnable = await isCommandAvailable(command);
|
|
1648
|
+
pathWalkMemo.set(memoKey, isSpawnable);
|
|
1649
|
+
return isSpawnable;
|
|
1431
1650
|
}
|
|
1432
1651
|
// --- Verification Functions
|
|
1433
1652
|
/**
|
|
@@ -1483,7 +1702,7 @@ const lastManagedInstallVersion = new Map();
|
|
|
1483
1702
|
*/
|
|
1484
1703
|
async function verifyToolBinary(binPath, onVersionOutput) {
|
|
1485
1704
|
return new Promise((resolve) => {
|
|
1486
|
-
const isWindows =
|
|
1705
|
+
const isWindows = installerPlatform() === "win32";
|
|
1487
1706
|
const hasKnownWindowsExt = /\.(cmd|exe|ps1)$/i.test(binPath);
|
|
1488
1707
|
// On Windows, resolve the best executable path:
|
|
1489
1708
|
// - extensionless → prefer .cmd (cmd.exe-safe)
|
|
@@ -1644,7 +1863,7 @@ export async function getAllToolStatuses() {
|
|
|
1644
1863
|
}
|
|
1645
1864
|
// 5. Check pi-lens auto-install (~/.pi-lens/tools/)
|
|
1646
1865
|
const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
|
|
1647
|
-
const localPath =
|
|
1866
|
+
const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
|
|
1648
1867
|
try {
|
|
1649
1868
|
await fs.access(localPath);
|
|
1650
1869
|
if (await verifyToolBinary(localPath)) {
|
|
@@ -1709,7 +1928,7 @@ export function resolvePlatformPackageBinary(tool) {
|
|
|
1709
1928
|
const spec = tool.platformPackage;
|
|
1710
1929
|
if (!spec || !tool.packageName)
|
|
1711
1930
|
return undefined;
|
|
1712
|
-
const suffix = spec.suffixes[`${
|
|
1931
|
+
const suffix = spec.suffixes[`${installerPlatform()}-${process.arch}`];
|
|
1713
1932
|
if (!suffix)
|
|
1714
1933
|
return undefined;
|
|
1715
1934
|
const platformPkg = `${spec.base ?? tool.packageName}-${suffix}`;
|
|
@@ -1725,7 +1944,7 @@ export function resolvePlatformPackageBinary(tool) {
|
|
|
1725
1944
|
catch {
|
|
1726
1945
|
pkgDir = path.dirname(_installerRequire.resolve(`${platformPkg}/package.json`));
|
|
1727
1946
|
}
|
|
1728
|
-
const isWin =
|
|
1947
|
+
const isWin = installerPlatform() === "win32";
|
|
1729
1948
|
for (const bin of spec.binaries) {
|
|
1730
1949
|
for (const name of isWin ? [`${bin}.exe`, bin] : [bin]) {
|
|
1731
1950
|
const candidate = path.join(pkgDir, name);
|
|
@@ -1771,7 +1990,7 @@ export async function getToolPath(toolId) {
|
|
|
1771
1990
|
// This avoids the ~2-5s overhead of spawning npm global probes and PATH
|
|
1772
1991
|
// searches for tools we already manage locally.
|
|
1773
1992
|
const localBase = path.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
|
|
1774
|
-
if (
|
|
1993
|
+
if (installerPlatform() === "win32") {
|
|
1775
1994
|
// Prefer .cmd over extensionless — Node.js can't execute POSIX shell scripts on Windows
|
|
1776
1995
|
const cmdPath = `${localBase}.cmd`;
|
|
1777
1996
|
try {
|
|
@@ -1798,15 +2017,17 @@ export async function getToolPath(toolId) {
|
|
|
1798
2017
|
// fall through to extensionless
|
|
1799
2018
|
}
|
|
1800
2019
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2020
|
+
if (installerPlatform() !== "win32") {
|
|
2021
|
+
try {
|
|
2022
|
+
await fs.access(localBase);
|
|
2023
|
+
if (await verifyToolBinary(localBase, recordVersion)) {
|
|
2024
|
+
return localBase;
|
|
2025
|
+
}
|
|
2026
|
+
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
|
|
2027
|
+
}
|
|
2028
|
+
catch {
|
|
2029
|
+
// fall through to global checks
|
|
1805
2030
|
}
|
|
1806
|
-
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
|
|
1807
|
-
}
|
|
1808
|
-
catch {
|
|
1809
|
-
// fall through to global checks
|
|
1810
2031
|
}
|
|
1811
2032
|
// npm/pnpm/bun: prefer the native per-platform binary directly. The main
|
|
1812
2033
|
// package's launcher often can't find it under a symlink store / after a
|
|
@@ -1909,7 +2130,6 @@ async function findNpmGlobalToolPath(binaryName) {
|
|
|
1909
2130
|
? [
|
|
1910
2131
|
path.join(dir, `${binaryName}.cmd`),
|
|
1911
2132
|
path.join(dir, `${binaryName}.exe`),
|
|
1912
|
-
path.join(dir, binaryName),
|
|
1913
2133
|
]
|
|
1914
2134
|
: [path.join(dir, binaryName)];
|
|
1915
2135
|
for (const candidate of candidates) {
|
|
@@ -2160,7 +2380,7 @@ async function installGitHubTool(tool) {
|
|
|
2160
2380
|
logSessionStart(`github-install ${tool.id}: release fetch failed: ${err.message}`);
|
|
2161
2381
|
return undefined;
|
|
2162
2382
|
}
|
|
2163
|
-
const asset = releaseJson.assets
|
|
2383
|
+
const asset = pickReleaseAsset(releaseJson.assets, assetSubstring) ??
|
|
2164
2384
|
deriveHashiCorpReleaseAsset(tool, releaseJson.tag_name, assetSubstring);
|
|
2165
2385
|
if (!asset) {
|
|
2166
2386
|
logSessionStart(`github-install ${tool.id}: no asset matched "${assetSubstring}"`);
|
|
@@ -2537,7 +2757,7 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2537
2757
|
await fs.writeFile(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2));
|
|
2538
2758
|
}
|
|
2539
2759
|
// Resolve the package manager for the tools dir and build install args.
|
|
2540
|
-
const isWindows =
|
|
2760
|
+
const isWindows = installerPlatform() === "win32";
|
|
2541
2761
|
const pm = await resolveNodePackageManager(TOOLS_DIR);
|
|
2542
2762
|
const testNpmScript = process.env.PI_LENS_TEST_MODE === "1"
|
|
2543
2763
|
? process.env.PI_LENS_TEST_NPM_SCRIPT
|
|
@@ -2583,9 +2803,12 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2583
2803
|
if (!outcome.ok) {
|
|
2584
2804
|
throw new Error(`Failed to install ${packageName}: ${outcome.stderr}`);
|
|
2585
2805
|
}
|
|
2586
|
-
|
|
2806
|
+
// npm creates a command shim on Windows; retain that actual executable path
|
|
2807
|
+
// rather than probing/storing the extensionless POSIX sibling.
|
|
2808
|
+
const binBase = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
|
|
2809
|
+
const binPath = installerPlatform() === "win32" ? `${binBase}.cmd` : binBase;
|
|
2587
2810
|
// Make executable on Unix
|
|
2588
|
-
if (
|
|
2811
|
+
if (installerPlatform() !== "win32") {
|
|
2589
2812
|
try {
|
|
2590
2813
|
await fs.chmod(binPath, 0o750);
|
|
2591
2814
|
}
|
|
@@ -2615,10 +2838,10 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2615
2838
|
try {
|
|
2616
2839
|
const packagePath = path.join(TOOLS_DIR, "node_modules", packageName);
|
|
2617
2840
|
await fs.rm(packagePath, { recursive: true, force: true });
|
|
2618
|
-
await fs.rm(
|
|
2841
|
+
await fs.rm(binBase, { force: true });
|
|
2619
2842
|
if (isWindows) {
|
|
2620
|
-
await fs.rm(`${
|
|
2621
|
-
await fs.rm(`${
|
|
2843
|
+
await fs.rm(`${binBase}.cmd`, { force: true });
|
|
2844
|
+
await fs.rm(`${binBase}.ps1`, { force: true });
|
|
2622
2845
|
}
|
|
2623
2846
|
}
|
|
2624
2847
|
catch {
|
|
@@ -2784,6 +3007,33 @@ async function installGemTool(packageName) {
|
|
|
2784
3007
|
return undefined;
|
|
2785
3008
|
}
|
|
2786
3009
|
}
|
|
3010
|
+
async function finishInstallAttempt(toolId, ok, startedAt) {
|
|
3011
|
+
logSessionStart(`auto-install ${toolId}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
3012
|
+
if (ok) {
|
|
3013
|
+
// A prior availability probe may have cached ENOENT for this exact child
|
|
3014
|
+
// PATH. Make a successful mutation visible immediately rather than waiting
|
|
3015
|
+
// for the bounded negative-cache TTL or the next session reset (#1199).
|
|
3016
|
+
resetSafeSpawnWindowsCommandCache();
|
|
3017
|
+
// #1276: the madge managed-path memo is keyed only by projectRoot, but
|
|
3018
|
+
// reads PATH/discovery/install state that a completed install can just
|
|
3019
|
+
// have changed — drop it here too, right alongside the safe-spawn reset.
|
|
3020
|
+
// AWAITED (not fire-and-forget): a caller that starts the next madge
|
|
3021
|
+
// resolution the instant `installTool`/`ensureTool` resolves must observe
|
|
3022
|
+
// the reset memo, not a stale pre-install one, and a rejected dynamic
|
|
3023
|
+
// import must not become a silent unhandled rejection. Dynamic import
|
|
3024
|
+
// avoids a static dependency-checker.js <-> installer/index.js cycle
|
|
3025
|
+
// (dependency-checker.js already imports this module dynamically for the
|
|
3026
|
+
// same reason).
|
|
3027
|
+
try {
|
|
3028
|
+
const { resetMadgeManagedPathMemo } = await import("../dependency-checker.js");
|
|
3029
|
+
resetMadgeManagedPathMemo();
|
|
3030
|
+
}
|
|
3031
|
+
catch (err) {
|
|
3032
|
+
logSessionStart(`auto-install ${toolId}: madge memo reset failed: ${err.message}`);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return ok;
|
|
3036
|
+
}
|
|
2787
3037
|
/**
|
|
2788
3038
|
* Install a tool by ID
|
|
2789
3039
|
*/
|
|
@@ -2806,49 +3056,37 @@ export async function installTool(toolId) {
|
|
|
2806
3056
|
if (!tool.packageName || !tool.binaryName)
|
|
2807
3057
|
return false;
|
|
2808
3058
|
const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
|
|
2809
|
-
|
|
2810
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2811
|
-
return ok;
|
|
3059
|
+
return finishInstallAttempt(tool.id, npmPath !== undefined, startedAt);
|
|
2812
3060
|
}
|
|
2813
3061
|
case "pip": {
|
|
2814
3062
|
if (!tool.packageName)
|
|
2815
3063
|
return false;
|
|
2816
3064
|
const pipPath = await installPipTool(tool.packageName);
|
|
2817
|
-
|
|
2818
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2819
|
-
return ok;
|
|
3065
|
+
return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
|
|
2820
3066
|
}
|
|
2821
3067
|
case "gem": {
|
|
2822
3068
|
if (!tool.packageName)
|
|
2823
3069
|
return false;
|
|
2824
3070
|
const gemPath = await installGemTool(tool.packageName);
|
|
2825
|
-
|
|
2826
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2827
|
-
return ok;
|
|
3071
|
+
return finishInstallAttempt(tool.id, gemPath !== undefined, startedAt);
|
|
2828
3072
|
}
|
|
2829
3073
|
case "github": {
|
|
2830
3074
|
if (!tool.github)
|
|
2831
3075
|
return false;
|
|
2832
3076
|
const ghPath = await installGitHubTool(tool);
|
|
2833
|
-
|
|
2834
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2835
|
-
return ok;
|
|
3077
|
+
return finishInstallAttempt(tool.id, ghPath !== undefined, startedAt);
|
|
2836
3078
|
}
|
|
2837
3079
|
case "maven": {
|
|
2838
3080
|
if (!tool.maven)
|
|
2839
3081
|
return false;
|
|
2840
3082
|
const mavenPath = await installMavenTool(tool);
|
|
2841
|
-
|
|
2842
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2843
|
-
return ok;
|
|
3083
|
+
return finishInstallAttempt(tool.id, mavenPath !== undefined, startedAt);
|
|
2844
3084
|
}
|
|
2845
3085
|
case "archive": {
|
|
2846
3086
|
if (!tool.archive)
|
|
2847
3087
|
return false;
|
|
2848
3088
|
const archivePath = await installArchiveTool(tool);
|
|
2849
|
-
|
|
2850
|
-
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
2851
|
-
return ok;
|
|
3089
|
+
return finishInstallAttempt(tool.id, archivePath !== undefined, startedAt);
|
|
2852
3090
|
}
|
|
2853
3091
|
default:
|
|
2854
3092
|
logSessionStart(`auto-install ${tool.id}: unsupported strategy`);
|
|
@@ -2861,9 +3099,24 @@ export async function installTool(toolId) {
|
|
|
2861
3099
|
}
|
|
2862
3100
|
}
|
|
2863
3101
|
/**
|
|
2864
|
-
* Ensure a tool is installed (check first, install if missing)
|
|
3102
|
+
* Ensure a tool is installed (check first, install if missing).
|
|
3103
|
+
*
|
|
3104
|
+
* #1334 S5: when the pi host has actively denied project trust, the INSTALL
|
|
3105
|
+
* half is switched off here — the request degrades to the existing
|
|
3106
|
+
* `allowInstall:false` discovery-only path rather than failing outright, so an
|
|
3107
|
+
* already-present binary keeps working while nothing is downloaded or executed
|
|
3108
|
+
* on behalf of an untrusted project. A host with no trust surface at all
|
|
3109
|
+
* (`"unknown"`) is unaffected.
|
|
2865
3110
|
*/
|
|
2866
3111
|
export async function ensureTool(toolId, opts) {
|
|
3112
|
+
if (opts?.allowInstall !== false &&
|
|
3113
|
+
!assertInstallAllowed(`managed tool ensure: ${toolId}`)) {
|
|
3114
|
+
logSessionStart(`auto-install ensure ${toolId}: install gated — ${projectTrustDenialReason()}; discovery only`);
|
|
3115
|
+
return ensureToolResolved(toolId, { ...opts, allowInstall: false });
|
|
3116
|
+
}
|
|
3117
|
+
return ensureToolResolved(toolId, opts);
|
|
3118
|
+
}
|
|
3119
|
+
async function ensureToolResolved(toolId, opts) {
|
|
2867
3120
|
installFailureReasons.delete(toolId);
|
|
2868
3121
|
const cacheResolvedPath = (result) => {
|
|
2869
3122
|
if (result) {
|
|
@@ -2885,8 +3138,7 @@ export async function ensureTool(toolId, opts) {
|
|
|
2885
3138
|
try {
|
|
2886
3139
|
const probeCache = await readProbeCache();
|
|
2887
3140
|
delete probeCache[toolId];
|
|
2888
|
-
|
|
2889
|
-
scheduleProbeFlush();
|
|
3141
|
+
markProbeCacheChange(toolId, null);
|
|
2890
3142
|
}
|
|
2891
3143
|
catch {
|
|
2892
3144
|
// best-effort
|
|
@@ -2925,8 +3177,23 @@ export async function ensureTool(toolId, opts) {
|
|
|
2925
3177
|
}
|
|
2926
3178
|
// Fast path 1: in-memory session cache — no I/O.
|
|
2927
3179
|
const cached = resolvedPathCache.get(toolId);
|
|
2928
|
-
if (cached)
|
|
2929
|
-
|
|
3180
|
+
if (cached) {
|
|
3181
|
+
if (!isFullyQualified(cached))
|
|
3182
|
+
return cached;
|
|
3183
|
+
try {
|
|
3184
|
+
await fs.access(cached);
|
|
3185
|
+
return cached;
|
|
3186
|
+
}
|
|
3187
|
+
catch {
|
|
3188
|
+
// The executor would report ENOENT for this cached positive. Evict it
|
|
3189
|
+
// before discovery so the failure heals on this call, not after restart.
|
|
3190
|
+
}
|
|
3191
|
+
resolvedPathCache.delete(toolId);
|
|
3192
|
+
const probeCache = await readProbeCache();
|
|
3193
|
+
delete probeCache[toolId];
|
|
3194
|
+
markProbeCacheChange(toolId, null);
|
|
3195
|
+
logSessionStart(`auto-install ensure ${toolId}: cached path disappeared; re-probing`);
|
|
3196
|
+
}
|
|
2930
3197
|
// Fast path 2: persistent probe cache — fs.access + stat, no process spawn.
|
|
2931
3198
|
const diskCached = await checkProbeCache(toolId);
|
|
2932
3199
|
if (diskCached) {
|
|
@@ -3100,9 +3367,11 @@ export const GITHUB_TOOLS = [
|
|
|
3100
3367
|
"zizmor",
|
|
3101
3368
|
"typos-lsp",
|
|
3102
3369
|
"tflint",
|
|
3370
|
+
"terragrunt",
|
|
3103
3371
|
"terraform-ls",
|
|
3104
3372
|
"zls",
|
|
3105
3373
|
"hadolint",
|
|
3374
|
+
"helm",
|
|
3106
3375
|
"gitleaks",
|
|
3107
3376
|
"taplo",
|
|
3108
3377
|
"vale",
|
|
@@ -3135,6 +3404,28 @@ export function resolveGitHubArchiveBinaryCandidates(toolId, platform, assetName
|
|
|
3135
3404
|
const binaryName = tool.github?.binaryInArchive ?? tool.binaryName ?? tool.id;
|
|
3136
3405
|
return getArchiveBinaryCandidates(binaryName, platform, assetName);
|
|
3137
3406
|
}
|
|
3407
|
+
// Signature/checksum siblings a release publishes next to the real asset. A
|
|
3408
|
+
// bare-binary tool's substring IS the whole asset name, so `includes` alone
|
|
3409
|
+
// matches `<asset>.asc` too and would install a signature file as the binary.
|
|
3410
|
+
const ASSET_SIDECAR_SUFFIXES = [
|
|
3411
|
+
".asc",
|
|
3412
|
+
".sig",
|
|
3413
|
+
".minisig",
|
|
3414
|
+
".pem",
|
|
3415
|
+
".cert",
|
|
3416
|
+
".sbom",
|
|
3417
|
+
".sha256",
|
|
3418
|
+
".sha256sum",
|
|
3419
|
+
".md5",
|
|
3420
|
+
];
|
|
3421
|
+
function isAssetSidecar(name) {
|
|
3422
|
+
const lower = name.toLowerCase();
|
|
3423
|
+
return ASSET_SIDECAR_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
3424
|
+
}
|
|
3425
|
+
export function pickReleaseAsset(assets, assetSubstring) {
|
|
3426
|
+
return (assets.find((a) => a.name === assetSubstring) ??
|
|
3427
|
+
assets.find((a) => a.name.includes(assetSubstring) && !isAssetSidecar(a.name)));
|
|
3428
|
+
}
|
|
3138
3429
|
function deriveHashiCorpReleaseAsset(tool, tagName, assetSubstring) {
|
|
3139
3430
|
const product = tool.github?.hashiCorpReleaseProduct;
|
|
3140
3431
|
if (!product || !tagName)
|