@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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.
Files changed (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -61,9 +61,11 @@ import { assertInstallAllowed, projectTrustDenialReason, } from "../project-trus
61
61
  const _installerRequire = createRequire(import.meta.url);
62
62
  import { createGunzip } from "node:zlib";
63
63
  import { TRANSIENT_MAX_COOLDOWN_MS } from "../dispatch/runners/utils/availability-policy.js";
64
- import { recordDegradationOnce } from "../degradation-ledger.js";
64
+ import { getDegradationLedgerGeneration, recordDegradationOnce, } from "../degradation-ledger.js";
65
65
  import { commitDurableStoreAsync } from "../durable-store.js";
66
66
  import { getGlobalPiLensDir } from "../file-utils.js";
67
+ import { createGenerationMap } from "../generation-guard.js";
68
+ import { resolveToolCwd } from "../tool-cwd.js";
67
69
  import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
68
70
  import { resetSafeSpawnWindowsCommandCache, safeSpawnAsync, } from "../safe-spawn.js";
69
71
  import { probeToolAsync } from "../tool-probe.js";
@@ -267,6 +269,27 @@ function managedGitHubFormatterTool(spec) {
267
269
  };
268
270
  }
269
271
  const MANAGED_GITHUB_FORMATTERS = [
272
+ {
273
+ id: "typstyle",
274
+ name: "typstyle",
275
+ owner: "typstyle-rs",
276
+ repo: "typstyle",
277
+ assetPattern: archAssetMatch({
278
+ linux: {
279
+ x64: "typstyle-x86_64-unknown-linux-gnu",
280
+ arm64: "typstyle-aarch64-unknown-linux-gnu",
281
+ },
282
+ darwin: {
283
+ x64: "typstyle-x86_64-apple-darwin",
284
+ arm64: "typstyle-aarch64-apple-darwin",
285
+ },
286
+ win32: {
287
+ x64: "typstyle-x86_64-pc-windows-msvc.exe",
288
+ arm64: "typstyle-aarch64-pc-windows-msvc.exe",
289
+ },
290
+ }),
291
+ kind: "binary",
292
+ },
270
293
  {
271
294
  id: "stylua",
272
295
  name: "StyLua",
@@ -1054,7 +1077,7 @@ export const TOOLS = [
1054
1077
  : `${base}/lua-language-server-${version}-win32-x64.zip`;
1055
1078
  return undefined;
1056
1079
  },
1057
- kind: "zip",
1080
+ kind: (platform) => (platform === "win32" ? "zip" : "tgz"),
1058
1081
  stripComponents: 0,
1059
1082
  treeMarker: "bin",
1060
1083
  },
@@ -1432,6 +1455,34 @@ export const TOOLS = [
1432
1455
  binaryInArchive: "gleam",
1433
1456
  },
1434
1457
  },
1458
+ {
1459
+ // Tinymist publishes cargo-dist archives containing the `tinymist` binary
1460
+ // for the supported desktop targets. The LSP server uses `tinymist lsp`.
1461
+ id: "tinymist",
1462
+ name: "Tinymist",
1463
+ checkCommand: "tinymist",
1464
+ checkArgs: ["--version"],
1465
+ installStrategy: "github",
1466
+ binaryName: "tinymist",
1467
+ github: {
1468
+ repo: "Myriad-Dreamin/tinymist",
1469
+ assetMatch: archAssetMatch({
1470
+ linux: {
1471
+ x64: "tinymist-x86_64-unknown-linux-gnu.tar.gz",
1472
+ arm64: "tinymist-aarch64-unknown-linux-gnu.tar.gz",
1473
+ },
1474
+ darwin: {
1475
+ x64: "tinymist-x86_64-apple-darwin.tar.gz",
1476
+ arm64: "tinymist-aarch64-apple-darwin.tar.gz",
1477
+ },
1478
+ win32: {
1479
+ x64: "tinymist-x86_64-pc-windows-msvc.zip",
1480
+ arm64: "tinymist-aarch64-pc-windows-msvc.zip",
1481
+ },
1482
+ }),
1483
+ binaryInArchive: "tinymist",
1484
+ },
1485
+ },
1435
1486
  {
1436
1487
  // marksman ships a single BARE (uncompressed) binary per platform on GitHub
1437
1488
  // releases — no archive, so it lands via the bare-binary branch of
@@ -2678,12 +2729,14 @@ async function getNpmGlobalBinCandidates(onTransient) {
2678
2729
  return dirs;
2679
2730
  }
2680
2731
  async function findPipUserToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
2681
- const isWindows = process.platform === "win32";
2682
- const userBaseCandidates = await getPythonUserBaseCandidates();
2732
+ const isWindows = installerPlatform() === "win32";
2733
+ const userBaseCandidates = [
2734
+ path.join(getGlobalPiLensDir(), "pip-tools"),
2735
+ path.join(getGlobalPiLensDir(), "pip-user"),
2736
+ ...(await getPythonUserBaseCandidates()),
2737
+ ];
2683
2738
  for (const userBase of userBaseCandidates) {
2684
- const scriptDirs = [
2685
- path.join(userBase, isWindows ? "Scripts" : "bin"),
2686
- ];
2739
+ const scriptDirs = [pipScriptsDir(userBase, installerPlatform())];
2687
2740
  if (isWindows) {
2688
2741
  try {
2689
2742
  const children = await fs.readdir(userBase, { withFileTypes: true });
@@ -3600,7 +3653,9 @@ async function refreshPackageManagerManagedTool(tool) {
3600
3653
  const installed = tool.installStrategy === "pip"
3601
3654
  ? // `-U` is the whole fix: without it pip treats the installed copy as
3602
3655
  // satisfying the requirement and the day-one version never moves.
3603
- await installPipTool(tool.id, tool.packageName, { upgrade: true })
3656
+ await installPipTool(tool.id, tool.packageName, tool.binaryName ?? tool.id, {
3657
+ upgrade: true,
3658
+ })
3604
3659
  : // `gem install` always fetches the newest version that satisfies the
3605
3660
  // requirement, so the install command IS the upgrade command.
3606
3661
  await installGemTool(tool.id, tool.packageName);
@@ -3798,9 +3853,35 @@ async function installMavenTool(tool) {
3798
3853
  * may return `undefined` (unsupported → caller degrades to "unavailable").
3799
3854
  * Exported for the tool-registry contract test.
3800
3855
  */
3801
- export function resolveArchiveUrl(spec, platform = process.platform, arch = process.arch) {
3856
+ export function resolveArchiveUrl(spec, platform = installerPlatform(), arch = process.arch) {
3802
3857
  return typeof spec.url === "function" ? spec.url(platform, arch) : spec.url;
3803
3858
  }
3859
+ /** Resolve the archive format independently from the extractor implementation. */
3860
+ export function resolveArchiveKind(spec, platform = installerPlatform(), arch = process.arch) {
3861
+ return typeof spec.kind === "function"
3862
+ ? spec.kind(platform, arch)
3863
+ : spec.kind;
3864
+ }
3865
+ function recordArchiveExtractionDegradation(toolId, format, reason) {
3866
+ recordDegradationOnce({
3867
+ kind: "managed-tool-install",
3868
+ subject: `${toolId}:${format}`,
3869
+ reason: `archive extraction ${reason}`,
3870
+ });
3871
+ }
3872
+ async function stripExtractedArchiveRoot(dir, components) {
3873
+ for (let i = 0; i < components; i++) {
3874
+ const entries = await fs.readdir(dir, { withFileTypes: true });
3875
+ const [entry] = entries;
3876
+ if (entries.length !== 1 || !entry?.isDirectory())
3877
+ return false;
3878
+ const root = path.join(dir, entry.name);
3879
+ for (const child of await fs.readdir(root))
3880
+ await fs.rename(path.join(root, child), path.join(dir, child));
3881
+ await fs.rm(root, { recursive: true, force: true });
3882
+ }
3883
+ return true;
3884
+ }
3804
3885
  /**
3805
3886
  * Move a verified-good extracted tree into its final, stable location without
3806
3887
  * ever leaving `finalDir` empty or missing while a working copy was there
@@ -3876,8 +3957,10 @@ async function installArchiveTool(tool) {
3876
3957
  if (!spec)
3877
3958
  return undefined;
3878
3959
  const binaryName = tool.binaryName ?? tool.id;
3879
- const isWindows = process.platform === "win32";
3880
- const url = resolveArchiveUrl(spec);
3960
+ const platform = installerPlatform();
3961
+ const isWindows = platform === "win32";
3962
+ const archiveKind = resolveArchiveKind(spec, platform, process.arch);
3963
+ const url = resolveArchiveUrl(spec, platform, process.arch);
3881
3964
  if (!url) {
3882
3965
  logSessionStart(`archive-install ${tool.id}: no archive for ${process.platform}/${process.arch} — unsupported, skipping`);
3883
3966
  return undefined;
@@ -3907,7 +3990,7 @@ async function installArchiveTool(tool) {
3907
3990
  // installed copy is now untouched until the replacement is proven good.
3908
3991
  const extractName = tool.id;
3909
3992
  const tmpExtractName = `${extractName}.refresh-tmp`;
3910
- const archiveName = `${tool.id}.download.${spec.kind === "zip" ? "zip" : "tgz"}`;
3993
+ const archiveName = `${tool.id}.download.${archiveKind === "zip" ? "zip" : "tgz"}`;
3911
3994
  const extractDir = path.join(TOOLS_DIR, extractName);
3912
3995
  const tmpExtractDir = path.join(TOOLS_DIR, tmpExtractName);
3913
3996
  const tmpArchive = path.join(TOOLS_DIR, archiveName);
@@ -3925,34 +4008,62 @@ async function installArchiveTool(tool) {
3925
4008
  // dir — stripping would flatten/merge its sibling module folders — so the
3926
4009
  // flag is omitted. bsdtar handles both .tgz and .zip with -xf.
3927
4010
  const stripComponents = spec.stripComponents ?? 1;
3928
- const tarArgs = [
3929
- spec.kind === "tgz" ? "-xzf" : "-xf",
3930
- archiveName,
3931
- "-C",
3932
- tmpExtractName,
3933
- ...(stripComponents > 0 ? [`--strip-components=${stripComponents}`] : []),
3934
- ];
4011
+ const extractionArgs = archiveKind === "zip" && isWindows
4012
+ ? [
4013
+ "-NoProfile",
4014
+ "-Command",
4015
+ `Expand-Archive -LiteralPath '${archiveName}' -DestinationPath '${tmpExtractName}' -Force`,
4016
+ ]
4017
+ : archiveKind === "zip"
4018
+ ? ["-q", "-o", archiveName, "-d", tmpExtractName]
4019
+ : [
4020
+ "-xzf",
4021
+ archiveName,
4022
+ "-C",
4023
+ tmpExtractName,
4024
+ ...(stripComponents > 0
4025
+ ? [`--strip-components=${stripComponents}`]
4026
+ : []),
4027
+ ];
3935
4028
  // Resolve `tar` to an absolute path on Windows (System32\tar.exe is the
3936
4029
  // bsdtar shipped with Windows 10+) so extraction can't be hijacked via a
3937
4030
  // writable PATH entry — same hardening as the taskkill spawn. On POSIX `tar`
3938
4031
  // is a trusted coreutil whose absolute path varies by distro, so it stays
3939
4032
  // bare (consistent with every other tool spawn).
3940
- const tarBin = isWindows
3941
- ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\tar.exe`
3942
- : "tar";
3943
- const extractResult = await safeSpawnAsync(tarBin, tarArgs, {
4033
+ const extractor = archiveKind === "zip" && isWindows
4034
+ ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
4035
+ : archiveKind === "zip"
4036
+ ? "unzip"
4037
+ : isWindows
4038
+ ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\tar.exe`
4039
+ : "tar";
4040
+ const extractionResult = await safeSpawnAsync(extractor, extractionArgs, {
3944
4041
  cwd: TOOLS_DIR,
3945
4042
  timeout: 120_000,
3946
4043
  ignoreAmbientSignal: true,
3947
4044
  lifetimeCoupled: true,
3948
4045
  });
3949
4046
  const extracted = {
3950
- ok: extractResult.status === 0,
3951
- stderr: extractResult.error?.message ?? extractResult.stderr,
4047
+ ok: extractionResult.status === 0,
4048
+ reason: extractionResult.spawnFailure?.kind === "tool-not-found"
4049
+ ? "extractor-unavailable"
4050
+ : extractionResult.spawnFailure
4051
+ ? "extractor-error"
4052
+ : "extractor-exit",
3952
4053
  };
3953
4054
  await fs.rm(tmpArchive, { force: true });
3954
4055
  if (!extracted.ok) {
3955
- logSessionStart(`archive-install ${tool.id}: extraction failed: ${extracted.stderr} — keeping installed version`);
4056
+ recordArchiveExtractionDegradation(tool.id, archiveKind, extracted.reason);
4057
+ logSessionStart(`archive-install ${tool.id}: ${archiveKind} extraction failed (${extracted.reason}) — keeping installed version`);
4058
+ await fs
4059
+ .rm(tmpExtractDir, { recursive: true, force: true })
4060
+ .catch(() => { });
4061
+ return undefined;
4062
+ }
4063
+ if (archiveKind === "zip" &&
4064
+ !(await stripExtractedArchiveRoot(tmpExtractDir, stripComponents))) {
4065
+ recordArchiveExtractionDegradation(tool.id, archiveKind, "layout-invalid");
4066
+ logSessionStart(`archive-install ${tool.id}: ${archiveKind} layout invalid — keeping installed version`);
3956
4067
  await fs
3957
4068
  .rm(tmpExtractDir, { recursive: true, force: true })
3958
4069
  .catch(() => { });
@@ -3970,7 +4081,8 @@ async function installArchiveTool(tool) {
3970
4081
  await fs.access(tmpMarker);
3971
4082
  }
3972
4083
  catch {
3973
- logSessionStart(`archive-install ${tool.id}: tree marker not found at ${tmpMarker} after extraction — keeping installed version`);
4084
+ recordArchiveExtractionDegradation(tool.id, archiveKind, "marker-missing");
4085
+ logSessionStart(`archive-install ${tool.id}: ${archiveKind} marker missing after extraction — keeping installed version`);
3974
4086
  await fs
3975
4087
  .rm(tmpExtractDir, { recursive: true, force: true })
3976
4088
  .catch(() => { });
@@ -3992,7 +4104,8 @@ async function installArchiveTool(tool) {
3992
4104
  await fs.access(tmpResolvedInner);
3993
4105
  }
3994
4106
  catch {
3995
- logSessionStart(`archive-install ${tool.id}: launcher not found at ${tmpResolvedInner} after extraction — keeping installed version`);
4107
+ recordArchiveExtractionDegradation(tool.id, archiveKind, "launcher-missing");
4108
+ logSessionStart(`archive-install ${tool.id}: ${archiveKind} launcher missing after extraction — keeping installed version`);
3996
4109
  await fs
3997
4110
  .rm(tmpExtractDir, { recursive: true, force: true })
3998
4111
  .catch(() => { });
@@ -4231,24 +4344,25 @@ export function pipCommandCandidates() {
4231
4344
  ? ["pip", "py", "python"]
4232
4345
  : ["pip3", "pip", "python3", "python"];
4233
4346
  }
4347
+ /** Resolve the script directory used by a Python installation on each OS. */
4348
+ export function pipScriptsDir(base, platform = installerPlatform()) {
4349
+ return path.join(base, platform === "win32" ? "Scripts" : "bin");
4350
+ }
4351
+ const pipPep668LoggedRefusals = createGenerationMap("installer-pep668-log");
4234
4352
  /**
4235
4353
  * Install a pip package tool
4236
4354
  */
4237
- async function installPipTool(toolId, packageName,
4355
+ async function installPipTool(toolId, packageName, binaryName,
4238
4356
  /**
4239
4357
  * Add `-U`, turning the install into an upgrade. Without it `pip install`
4240
4358
  * treats an already-present package as satisfied and leaves the day-one
4241
4359
  * version in place forever — the freeze #1747 is about. The flag is the ONLY
4242
- * difference between install and refresh: same command ladder, same
4243
- * `--user` target, so a refresh can never write somewhere the install would
4244
- * not have.
4360
+ * difference between install and refresh within each selected environment.
4245
4361
  */
4246
4362
  options = {}) {
4247
4363
  try {
4248
- const isWindows = process.platform === "win32";
4249
- const verb = options.upgrade
4250
- ? ["install", "-U", "--user"]
4251
- : ["install", "--user"];
4364
+ const isWindows = installerPlatform() === "win32";
4365
+ const verb = options.upgrade ? ["install", "-U"] : ["install"];
4252
4366
  // Built from `pipCommandCandidates()` — the single source of truth this
4253
4367
  // module and any other caller (the tool-smoke lane's toolchain-presence
4254
4368
  // probe, #2661 review) share, rather than a second, independently
@@ -4259,21 +4373,145 @@ options = {}) {
4259
4373
  ? [...verb, packageName]
4260
4374
  : ["-m", "pip", ...verb, packageName],
4261
4375
  }));
4262
- const errors = [];
4263
- for (const candidate of pipCandidates) {
4264
- const pipResult = await safeSpawnAsync(candidate.command, candidate.args, {
4265
- timeout: 120_000,
4266
- ignoreAmbientSignal: true,
4267
- lifetimeCoupled: true,
4376
+ const pep668 = /externally-managed-environment/i;
4377
+ const refuse = (strategy, reason) => {
4378
+ if (!pep668.test(reason))
4379
+ return;
4380
+ const subject = `${toolId}:${strategy}`;
4381
+ recordDegradationOnce({
4382
+ kind: "pip-pep668-strategy-refused",
4383
+ subject,
4384
+ reason,
4268
4385
  });
4269
- const outcome = {
4270
- ok: pipResult.status === 0,
4271
- error: (pipResult.error?.message ?? pipResult.stderr).trim(),
4272
- };
4273
- if (outcome.ok) {
4274
- // Ensure user-level scripts directory is available in current process PATH.
4275
- // This helps tools installed via `pip install --user` become immediately callable.
4276
- const userBaseResult = await new Promise((resolve) => {
4386
+ const logKey = `${getDegradationLedgerGeneration()}:${subject}`;
4387
+ if (pipPep668LoggedRefusals.current(logKey) === 0) {
4388
+ pipPep668LoggedRefusals.bump(logKey);
4389
+ logSessionStart(`auto-install pip ${packageName}: ${strategy} refused by PEP 668 (${boundInstallError(reason)})`);
4390
+ }
4391
+ };
4392
+ const succeeded = (strategy, binaryPath) => {
4393
+ recordDegradationOnce({
4394
+ kind: "pip-install-strategy-succeeded",
4395
+ subject: `${toolId}:${strategy}`,
4396
+ reason: binaryPath,
4397
+ });
4398
+ return binaryPath;
4399
+ };
4400
+ const run = (command, args, env) => safeSpawnAsync(command, args, {
4401
+ timeout: 120_000,
4402
+ ignoreAmbientSignal: true,
4403
+ lifetimeCoupled: true,
4404
+ cwd: resolveToolCwd("runner", toolId, getGlobalPiLensDir(), {
4405
+ cwd: getGlobalPiLensDir(),
4406
+ suppressTelemetry: true,
4407
+ }).cwd,
4408
+ ...(env ? { env } : {}),
4409
+ });
4410
+ const addBinToPath = async (binDir) => {
4411
+ try {
4412
+ await fs.access(binDir);
4413
+ }
4414
+ catch {
4415
+ return undefined;
4416
+ }
4417
+ const currentPath = process.env.PATH || process.env.Path || "";
4418
+ const separator = isWindows ? ";" : path.delimiter;
4419
+ if (!currentPath
4420
+ .toLowerCase()
4421
+ .split(separator)
4422
+ .includes(binDir.toLowerCase())) {
4423
+ const updatedPath = `${binDir}${separator}${currentPath}`;
4424
+ process.env.PATH = updatedPath;
4425
+ if (isWindows)
4426
+ process.env.Path = updatedPath;
4427
+ }
4428
+ const names = isWindows
4429
+ ? [`${binaryName}.exe`, `${binaryName}.cmd`, binaryName]
4430
+ : [binaryName];
4431
+ for (const name of names) {
4432
+ const candidate = path.join(binDir, name);
4433
+ try {
4434
+ await fs.access(candidate);
4435
+ return candidate;
4436
+ }
4437
+ catch {
4438
+ // continue
4439
+ }
4440
+ }
4441
+ return undefined;
4442
+ };
4443
+ if (await isCommandAvailable("pipx")) {
4444
+ const result = await run("pipx", [
4445
+ options.upgrade ? "upgrade" : "install",
4446
+ packageName,
4447
+ ]);
4448
+ const error = (result.error?.message ?? result.stderr).trim();
4449
+ if (result.status === 0) {
4450
+ const location = await run("pipx", [
4451
+ "environment",
4452
+ "--value",
4453
+ "PIPX_BIN_DIR",
4454
+ ]);
4455
+ const binDir = location.status === 0 && location.stdout.trim()
4456
+ ? location.stdout.trim()
4457
+ : path.join(os.homedir(), ".local", "bin");
4458
+ const binaryPath = await addBinToPath(binDir);
4459
+ if (binaryPath)
4460
+ return succeeded("pipx", binaryPath);
4461
+ throw new Error(`pipx installed ${packageName} but ${binaryName} is not resolvable`);
4462
+ }
4463
+ refuse("pipx", error);
4464
+ }
4465
+ const pythonCandidates = pipCandidates.filter(({ command }) => command === "python3" || command === "python" || command === "py");
4466
+ const pythonAvailability = await Promise.all(pythonCandidates.map(({ command }) => isCommandAvailable(command)));
4467
+ const availablePythonCandidates = pythonCandidates.filter((_, index) => pythonAvailability[index]);
4468
+ const venvRoot = path.join(getGlobalPiLensDir(), "pip-tools");
4469
+ for (const candidate of availablePythonCandidates) {
4470
+ const venvBin = pipScriptsDir(venvRoot, installerPlatform());
4471
+ let venvPip = path.join(venvBin, isWindows ? "pip.exe" : "pip");
4472
+ try {
4473
+ await fs.access(venvPip);
4474
+ }
4475
+ catch {
4476
+ const created = await run(candidate.command, ["-m", "venv", venvRoot]);
4477
+ const error = (created.error?.message ?? created.stderr).trim();
4478
+ if (created.status !== 0) {
4479
+ refuse("venv", error || "python venv module unavailable");
4480
+ continue;
4481
+ }
4482
+ }
4483
+ try {
4484
+ await fs.access(venvPip);
4485
+ }
4486
+ catch {
4487
+ venvPip = path.join(venvBin, isWindows ? "pip.cmd" : "pip3");
4488
+ try {
4489
+ await fs.access(venvPip);
4490
+ }
4491
+ catch {
4492
+ continue;
4493
+ }
4494
+ }
4495
+ const result = await run(venvPip, [...verb, packageName]);
4496
+ const error = (result.error?.message ?? result.stderr).trim();
4497
+ if (result.status === 0) {
4498
+ const binaryPath = await addBinToPath(venvBin);
4499
+ if (binaryPath)
4500
+ return succeeded("venv", binaryPath);
4501
+ throw new Error(`venv installed ${packageName} but ${binaryName} is not resolvable`);
4502
+ }
4503
+ refuse("venv", error);
4504
+ }
4505
+ const candidateErrors = [];
4506
+ let userRefused = false;
4507
+ for (const candidate of pipCandidates) {
4508
+ const args = candidate.command === "pip" || candidate.command === "pip3"
4509
+ ? [...verb, "--user", packageName]
4510
+ : ["-m", "pip", ...verb, "--user", packageName];
4511
+ const result = await run(candidate.command, args);
4512
+ const error = (result.error?.message ?? result.stderr).trim();
4513
+ if (result.status === 0) {
4514
+ const base = await new Promise((resolve) => {
4277
4515
  let probe;
4278
4516
  try {
4279
4517
  probe = spawn(candidate.command, ["-m", "site", "--user-base"], {
@@ -4282,78 +4520,57 @@ options = {}) {
4282
4520
  });
4283
4521
  }
4284
4522
  catch {
4285
- // SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the
4286
- // pidusage bug class, #533) — best-effort probe, resolve empty.
4287
4523
  resolve("");
4288
4524
  return;
4289
4525
  }
4290
4526
  let stdout = "";
4291
4527
  probe.stdout?.on("data", (data) => (stdout += data));
4292
- probe.on("exit", (code) => {
4293
- if (code === 0)
4294
- resolve(stdout.trim());
4295
- else
4296
- resolve("");
4297
- });
4528
+ probe.on("exit", (code) => resolve(code === 0 ? stdout.trim() : ""));
4298
4529
  probe.on("error", () => resolve(""));
4299
4530
  });
4300
- if (userBaseResult) {
4301
- const candidateScriptDirs = [
4302
- path.join(userBaseResult, isWindows ? "Scripts" : "bin"),
4303
- ];
4304
- if (isWindows) {
4305
- // Some Python setups report USER_BASE as ...\Roaming\Python,
4306
- // while scripts live in ...\Roaming\Python\PythonXY\Scripts.
4307
- try {
4308
- const children = await fs.readdir(userBaseResult, {
4309
- withFileTypes: true,
4310
- });
4311
- for (const entry of children) {
4312
- if (!entry.isDirectory())
4313
- continue;
4314
- if (!/^python\d+$/i.test(entry.name))
4315
- continue;
4316
- candidateScriptDirs.push(path.join(userBaseResult, entry.name, "Scripts"));
4317
- }
4318
- }
4319
- catch {
4320
- // ignore
4321
- }
4322
- }
4323
- const currentPath = process.env.PATH || process.env.Path || process.env.path || "";
4324
- const separator = isWindows ? ";" : ":";
4325
- const normalizedPath = currentPath
4326
- .toLowerCase()
4327
- .split(separator)
4328
- .map((p) => p.trim());
4329
- for (const scriptsDir of candidateScriptDirs) {
4330
- try {
4331
- await fs.access(scriptsDir);
4332
- if (!normalizedPath.includes(scriptsDir.toLowerCase())) {
4333
- const existingPath = process.env.PATH ||
4334
- process.env.Path ||
4335
- process.env.path ||
4336
- "";
4337
- const updatedPath = `${scriptsDir}${separator}${existingPath}`;
4338
- process.env.PATH = updatedPath;
4339
- if (isWindows) {
4340
- process.env.Path = updatedPath;
4341
- }
4342
- debugLog(`Added pip user scripts dir to PATH: ${scriptsDir}`);
4343
- }
4344
- }
4345
- catch {
4346
- debugLog(`pip user scripts dir not accessible: ${scriptsDir}`);
4347
- }
4348
- }
4349
- }
4350
- return packageName;
4531
+ const binaryPath = base
4532
+ ? await addBinToPath(pipScriptsDir(base, installerPlatform()))
4533
+ : undefined;
4534
+ // Keep the historical normal-user result even when the interpreter's
4535
+ // user-base probe is unavailable. The next availability probe owns
4536
+ // resolution through PATH and its user-base candidates.
4537
+ return succeeded("user", binaryPath ?? packageName);
4538
+ }
4539
+ const candidateError = `${candidate.command} ${args.join(" ")}: ${boundInstallError(error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
4540
+ candidateErrors.push(candidateError);
4541
+ if (pep668.test(error)) {
4542
+ userRefused = true;
4543
+ refuse("user", error);
4544
+ }
4545
+ }
4546
+ if (!userRefused)
4547
+ throw new Error(`pip install failed: ${candidateErrors.join(" | ") || "unknown error"}`);
4548
+ const privateBase = path.join(getGlobalPiLensDir(), "pip-user");
4549
+ const privateEnv = { ...process.env, PYTHONUSERBASE: privateBase };
4550
+ for (const candidate of pipCandidates) {
4551
+ const args = candidate.command === "pip" || candidate.command === "pip3"
4552
+ ? [...verb, "--user", "--break-system-packages", packageName]
4553
+ : [
4554
+ "-m",
4555
+ "pip",
4556
+ ...verb,
4557
+ "--user",
4558
+ "--break-system-packages",
4559
+ packageName,
4560
+ ];
4561
+ const result = await run(candidate.command, args, privateEnv);
4562
+ const error = (result.error?.message ?? result.stderr).trim();
4563
+ if (result.status !== 0) {
4564
+ const candidateError = `${candidate.command} ${args.join(" ")}: ${boundInstallError(error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
4565
+ candidateErrors.push(candidateError);
4566
+ continue;
4351
4567
  }
4352
- const candidateError = `${candidate.command} ${candidate.args.join(" ")}: ${boundInstallError(outcome.error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
4353
- errors.push(candidateError);
4354
- debugLog(`[pip-fallback] ${candidateError}`);
4568
+ const binaryPath = await addBinToPath(pipScriptsDir(privateBase, installerPlatform()));
4569
+ if (binaryPath)
4570
+ return succeeded("private-prefix", binaryPath);
4571
+ throw new Error(`private-prefix pip installed ${packageName} but ${binaryName} is not resolvable`);
4355
4572
  }
4356
- throw new Error(`Failed to install ${packageName}: no usable pip command found (${errors.join(" | ") || "unknown error"})`);
4573
+ throw new Error(`pip install failed: ${candidateErrors.join(" | ") || "unknown error"}`);
4357
4574
  }
4358
4575
  catch (err) {
4359
4576
  return recordPackageManagerInstallException(toolId, "pip", packageName, err);
@@ -4493,7 +4710,7 @@ export async function installTool(toolId) {
4493
4710
  case "pip": {
4494
4711
  if (!tool.packageName)
4495
4712
  return false;
4496
- const pipPath = await installPipTool(tool.id, tool.packageName);
4713
+ const pipPath = await installPipTool(tool.id, tool.packageName, tool.binaryName ?? tool.id);
4497
4714
  return finishInstallAttempt(tool.id, pipPath !== undefined, startedAt);
4498
4715
  }
4499
4716
  case "gem": {
@@ -4871,6 +5088,8 @@ export const GITHUB_TOOLS = [
4871
5088
  "clojure-lsp",
4872
5089
  "cue",
4873
5090
  "gleam",
5091
+ "typstyle",
5092
+ "tinymist",
4874
5093
  "marksman",
4875
5094
  "expert",
4876
5095
  ];
@@ -0,0 +1,7 @@
1
+ /** Directory boundary for the backstop lock and cooldown stamp. Production
2
+ * shares the machine home; the test harness isolates only this state without
3
+ * relocating the registry, installed tools, or other machine-wide stores.
4
+ */
5
+ export function resolveBackstopStateDir(machineHome) {
6
+ return machineHome;
7
+ }