pi-lens 3.8.62 → 3.8.64
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/CHANGELOG.md +37 -0
- package/README.md +12 -9
- package/dist/clients/deadline-utils.js +93 -0
- package/dist/clients/installer/index.js +50 -35
- package/dist/clients/log-cleanup.js +36 -13
- package/dist/clients/lsp/client.js +156 -61
- package/dist/clients/lsp/index.js +218 -33
- package/dist/clients/lsp/interactive-install.js +14 -4
- package/dist/clients/lsp/launch.js +31 -102
- package/dist/clients/lsp/server.js +29 -22
- package/dist/clients/mcp/review.js +7 -4
- package/dist/clients/module-report-lsp.js +1 -16
- package/dist/clients/package-manager.js +223 -0
- package/dist/clients/pipeline.js +113 -39
- package/dist/clients/project-metadata.js +3 -23
- package/dist/clients/read-expansion.js +1 -14
- package/dist/clients/safe-spawn.js +9 -0
- package/dist/clients/tree-sitter-client.js +72 -14
- package/dist/index.js +16 -0
- package/dist/mcp/server.js +4 -2
- package/dist/tools/ast-grep-search.js +10 -6
- package/dist/tools/lens-diagnostics.js +43 -10
- package/dist/tools/lsp-diagnostics.js +23 -4
- package/dist/tools/scan-progress.js +38 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-bash.wasm.json +5 -0
- package/grammars/tree-sitter-css.wasm +0 -0
- package/grammars/tree-sitter-css.wasm.json +5 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-go.wasm.json +5 -0
- package/grammars/tree-sitter-html.wasm +0 -0
- package/grammars/tree-sitter-html.wasm.json +5 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-java.wasm.json +5 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm.json +5 -0
- package/grammars/tree-sitter-json.wasm +0 -0
- package/grammars/tree-sitter-json.wasm.json +5 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-python.wasm.json +5 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm.json +5 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm.json +5 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm.json +5 -0
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +5 -0
- package/package.json +7 -5
- package/rules/ast-grep-rules/rule-tests/incomplete-string-escaping-js-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/incomplete-string-escaping-test.yml +21 -0
- package/rules/ast-grep-rules/rules/incomplete-string-escaping-js.yml +19 -0
- package/rules/ast-grep-rules/rules/incomplete-string-escaping.yml +19 -0
- package/scripts/analyze-pi-lens-logs.mjs +101 -3
- package/scripts/download-grammars.js +171 -30
- package/scripts/grammars.lock.json +32 -0
- package/skills/write-ast-grep-rule/SKILL.md +36 -26
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,43 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
## [3.8.64] - 2026-07-04
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Opt-in `workspace/diagnostic` pull for the full scan — one request per server instead of N file opens (#387 Item 2)** — where a language server advertises `workspace/diagnostic` (e.g. TypeScript), `lens_diagnostics mode=full` can now issue a single project-wide pull per server instead of opening every file, detected via a new `workspaceDiagnostics` capability flag (distinct from per-document pull) and a `requestWorkspaceDiagnostics` client method. Gated behind `PI_LENS_LSP_WORKSPACE_PULL=1` (default off) and used per server-group only when the server advertises it and no file in the group has an auxiliary scanner; **any** miss (unsupported / dead / timeout / auxiliary present) falls back to the per-server-serial per-file path from #387 Item 1. Off by default because a **cold** server can answer a workspace pull with an empty/partial report that would read as a false "all clean", and the pull covers only the primary server — so it stays opt-in pending real-server validation before becoming the default. Completes the capability side of #387 (Item 1 shipped in #388).
|
|
18
|
+
- **Progress bar for the long full-mode diagnostic scans (`lens_diagnostics mode=full`, `lsp_diagnostics` batch/directory)** — these scans can run for seconds to minutes and were previously opaque until they returned. They now stream a throttled progress bar (`Scanning… [████░░░░░░] 45/123 (37%)`) to the tool's `onUpdate` callback — at most ~4×/s plus a guaranteed final tick — so the agent/user sees movement. The data already existed (the sweep's per-file completion count); this just surfaces it. Shared `tools/scan-progress.ts` (`renderScanProgress` + `makeProgressReporter`); `runWorkspaceDiagnostics` and `mapWithConcurrency` gained an optional `onProgress(completed, total)`.
|
|
19
|
+
|
|
20
|
+
- **Tree-sitter grammar provenance — sha256 sidecars + a committed manifest + a CI guard (#177)** — the grammar downloader now verifies every fetched `.wasm` against a committed provenance manifest (`scripts/grammars.lock.json`: package, version, per-grammar sha256) and writes a `<grammar>.wasm.json` sidecar recording what was installed. Two integrity gaps close: (1) fetched bytes are checked against the pinned hash — a grammar whose bytes don't match is never written (guards CDN corruption/tampering); (2) the old **skip-if-exists** behavior is replaced by **skip-if-verified** — on a version bump or hash mismatch the stale grammar is re-downloaded instead of silently persisting, so an ABI-mismatched `.wasm` can't survive against the deliberately pinned `web-tree-sitter`. A new `npm run check:grammars` (`scripts/check-grammar-provenance.mjs`, wired into CI like `check:lockfile`) re-hashes the installed grammars' bytes and fails on any drift from the manifest. Regenerate on a deliberate tree-sitter-wasms bump with `node scripts/download-grammars.js --write-manifest`.
|
|
21
|
+
- **Package-manager detection — pi-lens no longer hardcodes npm/npx (#374)** — new `clients/package-manager.ts` is the single source of truth for *which* Node package manager to use (npm/pnpm/yarn/bun) and *how* to spell each command (run-script / install / global-install / exec / global-bin). Resolution: the project's lockfile or corepack `packageManager` field when that manager is installed, else the first installed by preference (npm → pnpm → yarn → bun), else npm. Routed through it: tool auto-install and global-bin discovery (`clients/installer`), LSP global binary lookup (`clients/lsp/launch.ts`), the interactive LSP-server global install (`clients/lsp/interactive-install.ts`), the MCP `pilens_rebuild` (`runRebuild` reports which manager it used), and project run-command hints. This makes pi-lens work on hosts that ship bun/pnpm/yarn instead of npm.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Tree-sitter grammars: bundle a core set + fix cross-manager install** — grammars were fetched only by the `postinstall` script, which **npm** runs but **pnpm/bun block by default** (and **yarn** couldn't install at all — see below), so non-npm users depended entirely on a runtime CDN fetch and got *no* tree-sitter offline. Now the 12 core grammars (ts, tsx, js, python, go, rust, json, yaml, bash, html, css, java) are **downloaded at `prepare` time and shipped in the tarball** (`grammars/`, in `files[]`; ~+1 MB to the `.tgz` since wasm gzips well, +9 MB unpacked), so the common languages parse **offline on every package manager**. The long-tail grammars still lazy-fetch on first use, and a failed fetch now emits a **visible, actionable warning** instead of a silent debug line. Also removed the `tree-sitter-wasms: "npm:null@^0.11.0"` optional-dependency sentinel — npm/pnpm/bun skipped it as a failing optional but **yarn classic hard-errored** on it, so `yarn add pi-lens` failed outright; it's now installable under yarn.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **The LSP notify write is now bounded at the source, so a wedged server can't ride an edit to the 30 s dispatcher timeout** — the pre-dispatch sync fix bounded one call site, but *every* `touchFile` caller (the dispatch LSP runner and the workspace sweep too) `await`ed `notify.open`'s `didChange`/`didOpen` write, which backpressures indefinitely on a server whose stdin isn't drained. So edits on a wedged server still took ~31 s — the dispatch LSP runner hung on that write until the coarse 30 s per-runner ceiling killed it (`Runner lsp timed out after 30000ms`). The write is now bounded inside `touchFile` itself (`PI_LENS_LSP_NOTIFY_BUDGET_MS`, default 2 s): on a wedged server it degrades to "no fresh diagnostics" (logged as `lsp_notify_timeout` / `notifyWriteTimedOut`) instead of hanging, for **all** callers.
|
|
30
|
+
- **`lens_diagnostics`/`lsp_diagnostics` full sweep no longer floods a single-threaded server (#387)** — the sweep ran a flat 8-wide worker pool that was server-agnostic, so on a single-language repo all 8 concurrent touches hit one `tsserver`. It's single-threaded per project: they don't parallelize, they queue — inflating the working set (each `didOpen` can force a project recheck) and **cascading per-file-budget timeouts by queue position** (observed: 51 of 123 files "timed out" purely from being behind others, the count climbing 0→51 as the queue deepened). The sweep now **groups files by their primary server, serializes touches within a server (one in flight), and parallelizes across distinct servers** — real parallelism in a mixed TS+Python repo, no flooding in a single-language one. This uses the universal per-file `didOpen` path, so it works for every server regardless of `workspace/diagnostic` support (a single-request workspace pull for servers that advertise it remains the #387 follow-up).
|
|
31
|
+
- **An edit could hang indefinitely (and ignore Escape) on a wedged language server — the pre-dispatch LSP sync was unbounded** — after an edit, pi-lens syncs the new content to the language server (a `didChange`/`didOpen`) before dispatching lint. Client *acquisition* was capped, but the notify *write* was not: when the server's stdin isn't being drained (a CPU-bound/wedged server — e.g. TypeScript mid-recheck), that write backpressures forever, hanging the whole edit with **no per-call bound and no log at all** (the first instrumented phase, `read_file`, sits *after* the sync, so the stall left zero trace). Observed live: an edit wedged 8+ minutes with the server timing out on every request. Now the sync is raced against a hard budget (`PI_LENS_LSP_SYNC_BUDGET_MS`, default 3 s) **and** the turn's abort signal, so Escape cancels it and a slow server can't park the pipeline — the edit proceeds (the dispatch LSP runner, with its own 30 s cap, still tries). Escape now reaches this path because the turn signal is exposed for in-process LSP awaits (`getAmbientAbortSignal`), not just child-process spawns. Two observability gaps closed too: an abandoned sync logs an `lsp_sync_abandoned` phase (timeout vs aborted), and a new `tool_result_received` marker fires the instant pi-lens receives an edit — so a future stall is localizable (present-then-silent = in-pipeline; absent = upstream) instead of invisible.
|
|
32
|
+
- **Log retention was silently broken — rotated backups never got deleted, and three logs never rotated** — the 10 MB rotation worked, but the 7-day retention sweep that's supposed to reap the backups used a pattern (`/\.log\./`) that only matched the *legacy* `name.log.<ts>` shape, never the *current* `name.<ts>.log` that rotation actually produces — so every backup ever rotated accumulated forever (observed: ~200 MB of `~/.pi-lens` backups dating back 2.5 months against a 7-day policy). Separately, three of the eight global logs — `actionable-warnings.log`, `ast-grep-tools.log`, `dead-code.log` — were absent from the rotation list and grew unbounded. Both are fixed: the deletion pattern now matches both backup shapes (and never an active log), and all eight logs are managed from one shared `MANAGED_LOG_FILES` list so rotation and the storage summary can't drift apart again. Because `runLogCleanup` already runs unconditionally on every session start, the corrected sweep **self-heals existing backlogs** — the next launch after upgrade reaps each user's accumulated `>7 day` backups with no migration step.
|
|
33
|
+
- **`lens_diagnostics mode=full` could hang indefinitely and didn't cancel on Escape** — an unattended session was observed wedged for ~8 hours on a small repo. Two root causes, both fixed: **(1) Escape didn't cancel** — the tool honored only the tool-call `signal` positional, not `ctx.signal` (the turn-wired abort Escape fires for a registered extension tool), so the sweep ran on. It now combines both (`combineAbortSignals`, shared in `deadline-utils`) and threads the result through the LSP sweep and project-runner scan. **(2) It could hang forever** — per-file diagnostic waits were bounded, but *client acquisition* in the sweep was not, so a language server hanging on spawn/`initialize` parked a worker permanently. Now (a) each file gets a per-file wall-clock budget (`PI_LENS_LSP_WORKSPACE_PER_FILE_MS`, default 15 s) so a worker always returns to its abort check, and (b) the whole scan has a hard wall-clock ceiling (`PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS`, default 3 min) that aborts it to partial results rather than never returning. The sweep also now emits **start + periodic heartbeat** latency logs (with `completed/total`, `timedOutFiles`) so a future stall is debuggable instead of silent, and the `scripts/analyze-pi-lens-logs.mjs` smell report now consumes them — surfacing sweeps that started but never completed (the hang/kill signature, with the last heartbeat's `completed X/Y`), sweeps whose files hit the per-file budget, and any `*_timeout` phases. The same pass repairs the analyzer's `slow-background-tasks` detector, whose regex had drifted from the runtime's `session_start task … success runMs=<n>` format and was silently matching **zero** of ~2k rows — it now flags the real multi-second startup tasks (`call-graph`, `knip`, `project-index`) again, and accepts both the current `runMs=` and the older `(<n>ms)` shapes. The same `ctx.signal` cancellation gap is fixed in the sibling tools `lsp_diagnostics` (batch/directory scans) and `ast_grep_search` (which was already abort-aware but checked the wrong signal).
|
|
34
|
+
|
|
35
|
+
## [3.8.63] - 2026-07-01
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Consolidated the timeout-race helpers into one shared `clients/deadline-utils.ts` (#366)** — the "race a promise against a timer" pattern had drifted into three near-identical copies (`withTimeout` in the LSP client, `withBudget` in read-expansion, `withinRemaining` in module-report). They're now thin adapters over one `withDeadline` core, which also fixes two latent bugs the copies carried: `withBudget` didn't suppress the loser promise's late rejection (an unhandled rejection when the timer won first), and `withinRemaining` never cleared its timer. Behaviour at every call site is unchanged (covered by the consumer suites); the core's semantics are locked by dedicated tests including an explicit late-rejection-suppression probe.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Bounded the remaining unbounded LSP requests — `workspace/symbol`, `textDocument/codeAction`, `workspace/executeCommand` (#365)** — like the pull-diagnostics fix (#364), these were sent via `safeSendRequest` with no `withTimeout` ceiling, so a language server that accepts a request but never replies (alive but hung) would make the `await` never resolve — hanging symbol search (`pilens_symbol_search`), code-action lookups, and server-command execution. `workspace/symbol` and `textDocument/codeAction` now route through the shared `navRequest` helper (its `withTimeout` ceiling + single-file stale-drop), timing out to `[]`. `workspace/executeCommand` — which is mutating and legitimately long-running — gets a separate, generous 30s anti-deadlock backstop (`PI_LENS_LSP_EXECUTE_COMMAND_TIMEOUT_MS`) that returns an honest `executed:false, reason:"…may still be applying server-side"` rather than truncating valid work or pretending it ran; its allowlist-by-advertisement and server-edit-window hardening are preserved. Real (non-timeout) errors still propagate throughout.
|
|
46
|
+
- **Autofix project-snapshot walk no longer freezes the TUI on large repos (#368)** — `snapshotProjectFiles` (the `tool_result` autofix side-effect detection that snapshots the project tree before/after a formatter or fixer runs, to catch files it changed as a side effect) was a fully synchronous `readdirSync`/`statSync` walk bounded only by a 5,000-file cap — a ~130ms event-loop block at the cap (2–4× that under load), stalling keystrokes on large projects while autofix ran. It now walks asynchronously and yields to the event loop every 500 files, so it holds the loop only for a short chunk. The scan cap and directory-exclusion/confinement behavior are unchanged; a cap-scale (~5k-file) event-loop occupancy guard asserts the walk keeps yielding.
|
|
47
|
+
- **Bounded LSP pull-diagnostics request — a hung server can no longer hang `lens_diagnostics` (#349, #364)** — the pull path in `clientWaitForDiagnostics` awaited a `textDocument/diagnostic` request via `safeSendRequest`, which only settles on a reply or a *destroyed* stream. A pull-mode server that is alive but hung (accepts the request, never replies) made that `await` never resolve, hanging the diagnostics wait → the `dispatch_lint` pipeline phase → `flushDebouncedToolResults` → `lens_diagnostics`, forever (and `safeSpawnAsync`'s 30s cap doesn't apply — it's a pipe request, not a spawn). Unlike the navigation/init/shutdown callers, this request had no `withTimeout` ceiling; the `timeoutMs` passed into `clientWaitForDiagnostics` only bounded the push backstop and the pull *retry interval*, never the individual request. The request is now wrapped in the existing `withTimeout` helper, bounded by `min(PULL_REQUEST_TIMEOUT_MS, remaining caller budget)` (env `PI_LENS_LSP_PULL_REQUEST_TIMEOUT_MS`, default 10s, mirroring `NAV_REQUEST_TIMEOUT_MS`). On timeout the request is caught as `unavailable`, which per #240 is not read as clean and falls through to the already-bounded push backstop. Explains the intermittent repro — it only fires for pull-mode servers (rust-analyzer being the classic) stalling mid-analysis. Regression test: a pull server whose `sendRequest` never resolves now resolves within the caller's budget instead of hanging.
|
|
48
|
+
- **POSIX LSP teardown now cleans up the whole process tree, not just the direct child (#362, #363)** — on POSIX, LSP servers launched through wrappers (npm shims, shell/node launchers) could leave descendants alive after pi-lens reset or shut down an LSP service; observed most visibly as `vscode-html-language-server` processes accumulating across long-lived zellij sessions and pressuring memory. LSP servers are now spawned detached into their own process group and teardown signals the group (`process.kill(-pid, ...)`) before falling back to the direct child, bringing POSIX cleanup in line with the existing Windows `taskkill /T` behavior. Windows teardown is unchanged (`taskkill /T` mid-session, handle-only kill for `processExiting`). Guarded by the `pid <= 0` check so a group signal can never degrade into `process.kill(-0)` against pi-lens's own process group.
|
|
49
|
+
|
|
13
50
|
## [3.8.62] - 2026-06-28
|
|
14
51
|
|
|
15
52
|
### Added
|
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Thanks goes to these wonderful people:
|
|
|
71
71
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anh-chu"><img src="https://avatars.githubusercontent.com/u/34973633?v=4" width="100px;" alt=""/><br /><sub><b>Anh Chu</b></sub></a><br /><a href="#code-anh-chu" title="Code">💻</a></td>
|
|
72
72
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Istar-Eldritch"><img src="https://avatars.githubusercontent.com/u/3746468?v=4" width="100px;" alt=""/><br /><sub><b>Ruben Paz</b></sub></a><br /><a href="#code-Istar-Eldritch" title="Code">💻</a> <a href="#bug-Istar-Eldritch" title="Bug reports">🐛</a></td>
|
|
73
73
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tifandotme"><img src="https://avatars.githubusercontent.com/u/33323177?v=4" width="100px;" alt=""/><br /><sub><b>Tifan Dwi Avianto</b></sub></a><br /><a href="#code-tifandotme" title="Code">💻</a></td>
|
|
74
|
-
</tr
|
|
74
|
+
</tr><br />
|
|
75
75
|
<tr>
|
|
76
76
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ricardoraposo"><img src="https://avatars.githubusercontent.com/u/50217712?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Raposo</b></sub></a><br /><a href="#code-ricardoraposo" title="Code">💻</a></td>
|
|
77
77
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cjunxiang"><img src="https://avatars.githubusercontent.com/u/26619858?v=4" width="100px;" alt=""/><br /><sub><b>C.Junxiang</b></sub></a><br /><a href="#code-cjunxiang" title="Code">💻</a> <a href="#bug-cjunxiang" title="Bug reports">🐛</a></td>
|
|
@@ -80,7 +80,7 @@ Thanks goes to these wonderful people:
|
|
|
80
80
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fractiunate"><img src="https://avatars.githubusercontent.com/u/78024279?v=4" width="100px;" alt=""/><br /><sub><b>Fractiunate // David Rahäuser</b></sub></a><br /><a href="#code-fractiunate" title="Code">💻</a></td>
|
|
81
81
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bloodf"><img src="https://avatars.githubusercontent.com/u/1626923?v=4" width="100px;" alt=""/><br /><sub><b>Heitor Ramon Ribeiro</b></sub></a><br /><a href="#code-bloodf" title="Code">💻</a></td>
|
|
82
82
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JayceFreeman"><img src="https://avatars.githubusercontent.com/u/92962110?v=4" width="100px;" alt=""/><br /><sub><b>JayceFreeman</b></sub></a><br /><a href="#code-JayceFreeman" title="Code">💻</a></td>
|
|
83
|
-
</tr
|
|
83
|
+
</tr><br />
|
|
84
84
|
<tr>
|
|
85
85
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaDmx"><img src="https://avatars.githubusercontent.com/u/91132641?v=4" width="100px;" alt=""/><br /><sub><b>Max L.</b></sub></a><br /><a href="#code-shaDmx" title="Code">💻</a></td>
|
|
86
86
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Roman-Galeev"><img src="https://avatars.githubusercontent.com/u/40388226?v=4" width="100px;" alt=""/><br /><sub><b>Roman Galeev</b></sub></a><br /><a href="#code-Roman-Galeev" title="Code">💻</a></td>
|
|
@@ -89,7 +89,7 @@ Thanks goes to these wonderful people:
|
|
|
89
89
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/flowing-abyss"><img src="https://avatars.githubusercontent.com/u/98622217?v=4" width="100px;" alt=""/><br /><sub><b>flowing-abyss</b></sub></a><br /><a href="#code-flowing-abyss" title="Code">💻</a></td>
|
|
90
90
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerryfan"><img src="https://avatars.githubusercontent.com/u/2540814?v=4" width="100px;" alt=""/><br /><sub><b>jerryfan</b></sub></a><br /><a href="#code-jerryfan" title="Code">💻</a></td>
|
|
91
91
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loss-and-quick"><img src="https://avatars.githubusercontent.com/u/39405619?v=4" width="100px;" alt=""/><br /><sub><b>minicx</b></sub></a><br /><a href="#code-loss-and-quick" title="Code">💻</a></td>
|
|
92
|
-
</tr
|
|
92
|
+
</tr><br />
|
|
93
93
|
<tr>
|
|
94
94
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/spyrosbazios"><img src="https://avatars.githubusercontent.com/u/37960233?v=4" width="100px;" alt=""/><br /><sub><b>spyrosbazios</b></sub></a><br /><a href="#code-spyrosbazios" title="Code">💻</a></td>
|
|
95
95
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/trvon"><img src="https://avatars.githubusercontent.com/u/6031322?v=4" width="100px;" alt=""/><br /><sub><b>Trevon</b></sub></a><br /><a href="#code-trvon" title="Code">💻</a></td>
|
|
@@ -98,7 +98,7 @@ Thanks goes to these wonderful people:
|
|
|
98
98
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdbooth"><img src="https://avatars.githubusercontent.com/u/1318691?v=4" width="100px;" alt=""/><br /><sub><b>Matthew Booth</b></sub></a><br /><a href="#bug-mdbooth" title="Bug reports">🐛</a></td>
|
|
99
99
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wiedzmin89"><img src="https://avatars.githubusercontent.com/u/61706855?v=4" width="100px;" alt=""/><br /><sub><b>Wiedzmin89</b></sub></a><br /><a href="#ideas-Wiedzmin89" title="Ideas & Planning">🤔</a></td>
|
|
100
100
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grodingo"><img src="https://avatars.githubusercontent.com/u/244184972?v=4" width="100px;" alt=""/><br /><sub><b>Virgile</b></sub></a><br /><a href="#bug-grodingo" title="Bug reports">🐛</a></td>
|
|
101
|
-
</tr
|
|
101
|
+
</tr><br />
|
|
102
102
|
<tr>
|
|
103
103
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/finnvyrn"><img src="https://avatars.githubusercontent.com/u/90801772?v=4" width="100px;" alt=""/><br /><sub><b>Finn</b></sub></a><br /><a href="#ideas-finnvyrn" title="Ideas & Planning">🤔</a></td>
|
|
104
104
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ywh555hhh"><img src="https://avatars.githubusercontent.com/u/121592812?v=4" width="100px;" alt=""/><br /><sub><b>Wayne E</b></sub></a><br /><a href="#bug-ywh555hhh" title="Bug reports">🐛</a></td>
|
|
@@ -107,7 +107,7 @@ Thanks goes to these wonderful people:
|
|
|
107
107
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pvtri96"><img src="https://avatars.githubusercontent.com/u/28696888?v=4" width="100px;" alt=""/><br /><sub><b>Tri Van Pham</b></sub></a><br /><a href="#bug-pvtri96" title="Bug reports">🐛</a></td>
|
|
108
108
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BingWuJ"><img src="https://avatars.githubusercontent.com/u/117666511?v=4" width="100px;" alt=""/><br /><sub><b>BingWuJ</b></sub></a><br /><a href="#bug-BingWuJ" title="Bug reports">🐛</a></td>
|
|
109
109
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tackleberrybey"><img src="https://avatars.githubusercontent.com/u/156954032?v=4" width="100px;" alt=""/><br /><sub><b>tackleberrybey</b></sub></a><br /><a href="#bug-tackleberrybey" title="Bug reports">🐛</a></td>
|
|
110
|
-
</tr
|
|
110
|
+
</tr><br />
|
|
111
111
|
<tr>
|
|
112
112
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TaterDoge"><img src="https://avatars.githubusercontent.com/u/121467933?v=4" width="100px;" alt=""/><br /><sub><b>Mariann Abshire</b></sub></a><br /><a href="#bug-TaterDoge" title="Bug reports">🐛</a></td>
|
|
113
113
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ProbabilityEngineer"><img src="https://avatars.githubusercontent.com/u/38498804?v=4" width="100px;" alt=""/><br /><sub><b>ProbabilityEngineer</b></sub></a><br /><a href="#ideas-ProbabilityEngineer" title="Ideas & Planning">🤔</a></td>
|
|
@@ -116,7 +116,7 @@ Thanks goes to these wonderful people:
|
|
|
116
116
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rstacruz"><img src="https://avatars.githubusercontent.com/u/74385?v=4" width="100px;" alt=""/><br /><sub><b>Rico Sta. Cruz</b></sub></a><br /><a href="#ideas-rstacruz" title="Ideas & Planning">🤔</a></td>
|
|
117
117
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/acidnik"><img src="https://avatars.githubusercontent.com/u/1227955?v=4" width="100px;" alt=""/><br /><sub><b>Nikita Bilous</b></sub></a><br /><a href="#bug-acidnik" title="Bug reports">🐛</a></td>
|
|
118
118
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/av1155"><img src="https://avatars.githubusercontent.com/u/117413846?v=4" width="100px;" alt=""/><br /><sub><b>Andrea Arturo Venti Fuentes</b></sub></a><br /><a href="#bug-av1155" title="Bug reports">🐛</a></td>
|
|
119
|
-
</tr
|
|
119
|
+
</tr><br />
|
|
120
120
|
<tr>
|
|
121
121
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dacec354"><img src="https://avatars.githubusercontent.com/u/90093629?v=4" width="100px;" alt=""/><br /><sub><b>dacec354</b></sub></a><br /><a href="#bug-dacec354" title="Bug reports">🐛</a></td>
|
|
122
122
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuli500177"><img src="https://avatars.githubusercontent.com/u/62830942?v=4" width="100px;" alt=""/><br /><sub><b>Xu Yili</b></sub></a><br /><a href="#bug-xuli500177" title="Bug reports">🐛</a></td>
|
|
@@ -125,7 +125,7 @@ Thanks goes to these wonderful people:
|
|
|
125
125
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">🐛</a></td>
|
|
126
126
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrahamJenkins"><img src="https://avatars.githubusercontent.com/u/6607975?v=4" width="100px;" alt=""/><br /><sub><b>Graham Jenkins</b></sub></a><br /><a href="#bug-GrahamJenkins" title="Bug reports">🐛</a></td>
|
|
127
127
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wings1848"><img src="https://avatars.githubusercontent.com/u/120104016?v=4" width="100px;" alt=""/><br /><sub><b>Wings Butterfly</b></sub></a><br /><a href="#bug-wings1848" title="Bug reports">🐛</a></td>
|
|
128
|
-
</tr
|
|
128
|
+
</tr><br />
|
|
129
129
|
<tr>
|
|
130
130
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohannesKlauss"><img src="https://avatars.githubusercontent.com/u/6214415?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Klauss</b></sub></a><br /><a href="#ideas-JohannesKlauss" title="Ideas & Planning">🤔</a></td>
|
|
131
131
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tan-yong-sheng"><img src="https://avatars.githubusercontent.com/u/64836390?v=4" width="100px;" alt=""/><br /><sub><b>Tan Yong Sheng</b></sub></a><br /><a href="#ideas-tan-yong-sheng" title="Ideas & Planning">🤔</a></td>
|
|
@@ -134,7 +134,7 @@ Thanks goes to these wonderful people:
|
|
|
134
134
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">🐛</a></td>
|
|
135
135
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ortonomy"><img src="https://avatars.githubusercontent.com/u/6688676?v=4" width="100px;" alt=""/><br /><sub><b>Gregory Orton</b></sub></a><br /><a href="#bug-ortonomy" title="Bug reports">🐛</a></td>
|
|
136
136
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jimallen"><img src="https://avatars.githubusercontent.com/u/868773?v=4" width="100px;" alt=""/><br /><sub><b>Jim Allen</b></sub></a><br /><a href="#bug-jimallen" title="Bug reports">🐛</a></td>
|
|
137
|
-
</tr
|
|
137
|
+
</tr><br />
|
|
138
138
|
<tr>
|
|
139
139
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/quantfiction"><img src="https://avatars.githubusercontent.com/u/49965454?v=4" width="100px;" alt=""/><br /><sub><b>quantfiction</b></sub></a><br /><a href="#bug-quantfiction" title="Bug reports">🐛</a></td>
|
|
140
140
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Artawower"><img src="https://avatars.githubusercontent.com/u/16963833?v=4" width="100px;" alt=""/><br /><sub><b>Art</b></sub></a><br /><a href="#bug-Artawower" title="Bug reports">🐛</a></td>
|
|
@@ -143,10 +143,13 @@ Thanks goes to these wonderful people:
|
|
|
143
143
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">🐛</a></td>
|
|
144
144
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/robertoecf"><img src="https://avatars.githubusercontent.com/u/54923863?v=4" width="100px;" alt=""/><br /><sub><b>Roberto Freitas</b></sub></a><br /><a href="#bug-robertoecf" title="Bug reports">🐛</a></td>
|
|
145
145
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tmttodd"><img src="https://avatars.githubusercontent.com/u/160077416?v=4" width="100px;" alt=""/><br /><sub><b>tmttodd</b></sub></a><br /><a href="#bug-tmttodd" title="Bug reports">🐛</a></td>
|
|
146
|
-
</tr
|
|
146
|
+
</tr><br />
|
|
147
147
|
<tr>
|
|
148
148
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smaharnav"><img src="https://avatars.githubusercontent.com/u/263328627?v=4" width="100px;" alt=""/><br /><sub><b>smaharnav</b></sub></a><br /><a href="#bug-smaharnav" title="Bug reports">🐛</a></td>
|
|
149
149
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MzaGuille"><img src="https://avatars.githubusercontent.com/u/151482698?v=4" width="100px;" alt=""/><br /><sub><b>Mza.Guille</b></sub></a><br /><a href="#bug-MzaGuille" title="Bug reports">🐛</a></td>
|
|
150
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cmptr"><img src="https://avatars.githubusercontent.com/u/32746889?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Bell</b></sub></a><br /><a href="#code-cmptr" title="Code">💻</a> <a href="#bug-cmptr" title="Bug reports">🐛</a></td>
|
|
151
|
+
<td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">🐛</a></td>
|
|
152
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moj02090"><img src="https://avatars.githubusercontent.com/u/57255166?v=4" width="100px;" alt=""/><br /><sub><b>moj02090</b></sub></a><br /><a href="#code-moj02090" title="Code">💻</a></td>
|
|
150
153
|
</tr>
|
|
151
154
|
</tbody>
|
|
152
155
|
</table>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One implementation of the "race a promise against a timer" pattern that had
|
|
3
|
+
* drifted into three near-identical copies (#366): `withTimeout` (clients/lsp),
|
|
4
|
+
* `withBudget` (read-expansion), and `withinRemaining` (module-report-lsp).
|
|
5
|
+
*
|
|
6
|
+
* The differences between them were real — timeout can *reject* or *resolve
|
|
7
|
+
* undefined*, and the raced promise's own rejection can *propagate* or be
|
|
8
|
+
* *swallowed* — so they are kept as named adapters over one core. Consolidating
|
|
9
|
+
* fixes two latent bugs the copies had: `withBudget` did not suppress the loser
|
|
10
|
+
* promise's late rejection (an unhandled rejection if the timer won first), and
|
|
11
|
+
* `withinRemaining` never cleared its timer.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Combine multiple abort signals into one that aborts when ANY of them does.
|
|
15
|
+
* Returns the single signal unchanged when only one is live, and `undefined`
|
|
16
|
+
* when none are — so callers can pass it straight through. Used so a tool honors
|
|
17
|
+
* both its tool-call `signal` positional and the turn-wired `ctx.signal` (Escape),
|
|
18
|
+
* and to fold in a wall-clock ceiling via `AbortSignal.timeout`.
|
|
19
|
+
*/
|
|
20
|
+
export function combineAbortSignals(...signals) {
|
|
21
|
+
const live = signals.filter((s) => s !== undefined);
|
|
22
|
+
if (live.length <= 1)
|
|
23
|
+
return live[0];
|
|
24
|
+
if (typeof AbortSignal.any === "function")
|
|
25
|
+
return AbortSignal.any(live);
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
for (const s of live) {
|
|
28
|
+
if (s.aborted) {
|
|
29
|
+
controller.abort(s.reason);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
|
|
33
|
+
}
|
|
34
|
+
return controller.signal;
|
|
35
|
+
}
|
|
36
|
+
export function withDeadline(promise, options) {
|
|
37
|
+
const onTimeout = options.onTimeout ?? "reject";
|
|
38
|
+
const onReject = options.onReject ?? "propagate";
|
|
39
|
+
const ms = options.ms ??
|
|
40
|
+
(options.deadlineAt !== undefined ? options.deadlineAt - Date.now() : 0);
|
|
41
|
+
// Past deadline / non-positive budget: settle immediately, no timer.
|
|
42
|
+
if (ms <= 0) {
|
|
43
|
+
return onTimeout === "undefined"
|
|
44
|
+
? Promise.resolve(undefined)
|
|
45
|
+
: Promise.reject(new Error(`Timeout after ${Math.max(0, ms)}ms`));
|
|
46
|
+
}
|
|
47
|
+
// Base promise with rejection handled per `onReject`. In propagate mode we
|
|
48
|
+
// still attach a no-op catch so that if the timer wins the race, the loser
|
|
49
|
+
// promise's later rejection does not surface as an unhandled rejection.
|
|
50
|
+
const base = onReject === "undefined" ? promise.catch(() => undefined) : promise;
|
|
51
|
+
if (onReject === "propagate")
|
|
52
|
+
promise.catch(() => { });
|
|
53
|
+
let timer;
|
|
54
|
+
const timeoutPromise = new Promise((resolve, reject) => {
|
|
55
|
+
timer = setTimeout(() => {
|
|
56
|
+
if (onTimeout === "undefined")
|
|
57
|
+
resolve(undefined);
|
|
58
|
+
else
|
|
59
|
+
reject(new Error(`Timeout after ${ms}ms`));
|
|
60
|
+
}, ms);
|
|
61
|
+
});
|
|
62
|
+
return Promise.race([base, timeoutPromise]).finally(() => {
|
|
63
|
+
if (timer)
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve `promise`, or reject with `Error("Timeout after <ms>ms")` once
|
|
69
|
+
* `timeoutMs` elapses. The raced promise's own rejection propagates.
|
|
70
|
+
*/
|
|
71
|
+
export function withTimeout(promise, timeoutMs) {
|
|
72
|
+
return withDeadline(promise, { ms: timeoutMs });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve `promise`, or `undefined` once `budgetMs` elapses (a non-positive
|
|
76
|
+
* budget resolves `undefined` immediately). The raced promise's own rejection
|
|
77
|
+
* propagates.
|
|
78
|
+
*/
|
|
79
|
+
export function withBudget(promise, budgetMs) {
|
|
80
|
+
return withDeadline(promise, { ms: budgetMs, onTimeout: "undefined" });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve `promise`, or `undefined` once the shared `deadlineAt` passes (a past
|
|
84
|
+
* deadline resolves `undefined` immediately). The raced promise's own rejection
|
|
85
|
+
* is swallowed to `undefined`.
|
|
86
|
+
*/
|
|
87
|
+
export function withinRemaining(promise, deadlineAt) {
|
|
88
|
+
return withDeadline(promise, {
|
|
89
|
+
deadlineAt,
|
|
90
|
+
onTimeout: "undefined",
|
|
91
|
+
onReject: "undefined",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -56,6 +56,7 @@ const _installerRequire = createRequire(import.meta.url);
|
|
|
56
56
|
import { createGunzip } from "node:zlib";
|
|
57
57
|
import { isTestMode } from "../env-utils.js";
|
|
58
58
|
import { getGlobalPiLensDir } from "../file-utils.js";
|
|
59
|
+
import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
|
|
59
60
|
// Global installation directory for pi-lens tools
|
|
60
61
|
const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
|
|
61
62
|
// Directory for GitHub-downloaded binaries
|
|
@@ -1150,6 +1151,8 @@ export async function updateProbeCache(toolId, resolvedPath) {
|
|
|
1150
1151
|
export function resetProbeCacheStateForTesting() {
|
|
1151
1152
|
_probeCache = null;
|
|
1152
1153
|
_probeCacheDirty = false;
|
|
1154
|
+
resolvedPathCache.clear();
|
|
1155
|
+
ensureInFlight.clear();
|
|
1153
1156
|
if (_probeCacheFlushTimer !== null) {
|
|
1154
1157
|
clearTimeout(_probeCacheFlushTimer);
|
|
1155
1158
|
_probeCacheFlushTimer = null;
|
|
@@ -1637,19 +1640,10 @@ async function getNpmGlobalBinCandidates() {
|
|
|
1637
1640
|
else {
|
|
1638
1641
|
add(path.join(os.homedir(), ".npm-global", "bin"));
|
|
1639
1642
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
shell: process.platform === "win32",
|
|
1645
|
-
});
|
|
1646
|
-
let stdout = "";
|
|
1647
|
-
proc.stdout?.on("data", (data) => (stdout += data));
|
|
1648
|
-
proc.on("exit", (code) => resolve(code === 0 ? stdout.trim() : ""));
|
|
1649
|
-
proc.on("error", () => resolve(""));
|
|
1650
|
-
});
|
|
1651
|
-
if (prefix) {
|
|
1652
|
-
add(process.platform === "win32" ? prefix : path.join(prefix, "bin"));
|
|
1643
|
+
// Global bin dirs for every installed manager (npm/pnpm/yarn/bun) — a tool
|
|
1644
|
+
// may have been installed globally via any of them.
|
|
1645
|
+
for (const dir of await allAvailableGlobalBinDirs()) {
|
|
1646
|
+
add(dir);
|
|
1653
1647
|
}
|
|
1654
1648
|
return dirs;
|
|
1655
1649
|
}
|
|
@@ -2225,21 +2219,19 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2225
2219
|
catch {
|
|
2226
2220
|
await fs.writeFile(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2));
|
|
2227
2221
|
}
|
|
2228
|
-
//
|
|
2222
|
+
// Resolve the package manager for the tools dir and build install args.
|
|
2229
2223
|
const isWindows = process.platform === "win32";
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
pm = isWindows ? "bun.exe" : "bun";
|
|
2233
|
-
}
|
|
2224
|
+
const pm = await resolveNodePackageManager(TOOLS_DIR);
|
|
2225
|
+
const pmCommand = pmBinary(pm);
|
|
2234
2226
|
// Use --ignore-scripts unless the package explicitly needs postinstall
|
|
2235
2227
|
// (e.g. biome downloads a platform-specific native binary via postinstall).
|
|
2236
2228
|
const needsScripts = NEEDS_POSTINSTALL.has(packageName);
|
|
2237
|
-
const baseInstallArgs =
|
|
2238
|
-
|
|
2239
|
-
|
|
2229
|
+
const baseInstallArgs = installArgs(pm, packageName, {
|
|
2230
|
+
ignoreScripts: !needsScripts,
|
|
2231
|
+
});
|
|
2240
2232
|
const INSTALL_TIMEOUT_MS = 120_000;
|
|
2241
2233
|
const runInstallAttempt = async (args) => new Promise((resolve) => {
|
|
2242
|
-
const proc = spawn(
|
|
2234
|
+
const proc = spawn(pmCommand, args, {
|
|
2243
2235
|
cwd: TOOLS_DIR,
|
|
2244
2236
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2245
2237
|
shell: isWindows, // Required for .cmd files on Windows
|
|
@@ -2263,13 +2255,14 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2263
2255
|
});
|
|
2264
2256
|
});
|
|
2265
2257
|
let outcome = await runInstallAttempt(baseInstallArgs);
|
|
2266
|
-
|
|
2258
|
+
// --legacy-peer-deps is npm-only; retry just npm's ERESOLVE failures.
|
|
2267
2259
|
const erResolve = outcome.ok === false &&
|
|
2268
2260
|
/npm\s+error\s+ERESOLVE|\bERESOLVE\b|could not resolve/i.test(outcome.stderr);
|
|
2269
|
-
if (
|
|
2270
|
-
const retryArgs =
|
|
2271
|
-
|
|
2272
|
-
:
|
|
2261
|
+
if (pm === "npm" && erResolve) {
|
|
2262
|
+
const retryArgs = installArgs(pm, packageName, {
|
|
2263
|
+
ignoreScripts: !needsScripts,
|
|
2264
|
+
legacyPeerDeps: true,
|
|
2265
|
+
});
|
|
2273
2266
|
logSessionStart(`auto-install npm ${packageName}: retry with --legacy-peer-deps after ERESOLVE`);
|
|
2274
2267
|
outcome = await runInstallAttempt(retryArgs);
|
|
2275
2268
|
}
|
|
@@ -2559,8 +2552,17 @@ export async function installTool(toolId) {
|
|
|
2559
2552
|
* Ensure a tool is installed (check first, install if missing)
|
|
2560
2553
|
*/
|
|
2561
2554
|
export async function ensureTool(toolId, opts) {
|
|
2555
|
+
const cacheResolvedPath = (result) => {
|
|
2556
|
+
if (result) {
|
|
2557
|
+
resolvedPathCache.set(toolId, result);
|
|
2558
|
+
void updateProbeCache(toolId, result);
|
|
2559
|
+
}
|
|
2560
|
+
return result;
|
|
2561
|
+
};
|
|
2562
2562
|
// forceReinstall: nuke caches, download from managed source, skip PATH entirely.
|
|
2563
2563
|
// Used when a PATH-resolved tool proves broken at launch (e.g. broken symlink).
|
|
2564
|
+
// allowInstall:false wins over forceReinstall: caches are still cleared, but
|
|
2565
|
+
// the function falls back to discovery-only and never downloads.
|
|
2564
2566
|
if (opts?.forceReinstall) {
|
|
2565
2567
|
const ensureStartMs = Date.now();
|
|
2566
2568
|
logSessionStart(`auto-install ensure ${toolId}: force reinstall — clearing caches`);
|
|
@@ -2576,6 +2578,10 @@ export async function ensureTool(toolId, opts) {
|
|
|
2576
2578
|
catch {
|
|
2577
2579
|
// best-effort
|
|
2578
2580
|
}
|
|
2581
|
+
if (opts.allowInstall === false) {
|
|
2582
|
+
logSessionStart(`auto-install ensure ${toolId}: force reinstall blocked — install disabled, discovery only (${Date.now() - ensureStartMs}ms)`);
|
|
2583
|
+
return cacheResolvedPath(await getToolPath(toolId));
|
|
2584
|
+
}
|
|
2579
2585
|
// Force download
|
|
2580
2586
|
const installed = await installTool(toolId);
|
|
2581
2587
|
if (!installed) {
|
|
@@ -2583,10 +2589,8 @@ export async function ensureTool(toolId, opts) {
|
|
|
2583
2589
|
return undefined;
|
|
2584
2590
|
}
|
|
2585
2591
|
// Find the newly installed binary (github-local check now comes before PATH)
|
|
2586
|
-
const result = await getToolPath(toolId);
|
|
2592
|
+
const result = cacheResolvedPath(await getToolPath(toolId));
|
|
2587
2593
|
if (result) {
|
|
2588
|
-
resolvedPathCache.set(toolId, result);
|
|
2589
|
-
void updateProbeCache(toolId, result);
|
|
2590
2594
|
logSessionStart(`auto-install ensure ${toolId}: force reinstall success at ${result} (${Date.now() - ensureStartMs}ms)`);
|
|
2591
2595
|
}
|
|
2592
2596
|
return result;
|
|
@@ -2604,10 +2608,13 @@ export async function ensureTool(toolId, opts) {
|
|
|
2604
2608
|
}
|
|
2605
2609
|
// Coalesce the whole ensure operation, not just installation. Most startup
|
|
2606
2610
|
// duplicates race while checking already-installed tools, before installTool()
|
|
2607
|
-
// would ever run.
|
|
2608
|
-
|
|
2611
|
+
// would ever run. The key includes the install policy so a discovery-only
|
|
2612
|
+
// caller cannot accidentally inherit an install-allowed caller's download (or
|
|
2613
|
+
// vice versa).
|
|
2614
|
+
const inFlightKey = opts?.allowInstall === false ? `${toolId}:discovery-only` : toolId;
|
|
2615
|
+
const inFlight = ensureInFlight.get(inFlightKey);
|
|
2609
2616
|
if (inFlight) {
|
|
2610
|
-
logSessionStart(`auto-install ensure ${toolId}: waiting for in-flight ensure`);
|
|
2617
|
+
logSessionStart(`auto-install ensure ${toolId}: waiting for in-flight ensure (${inFlightKey})`);
|
|
2611
2618
|
return inFlight;
|
|
2612
2619
|
}
|
|
2613
2620
|
const ensureStartMs = Date.now();
|
|
@@ -2621,6 +2628,14 @@ export async function ensureTool(toolId, opts) {
|
|
|
2621
2628
|
logSessionStart(`auto-install ensure ${toolId}: already available at ${existingPath} (${Date.now() - ensureStartMs}ms)`);
|
|
2622
2629
|
return existingPath;
|
|
2623
2630
|
}
|
|
2631
|
+
// Discovery and install are SEPARATE concerns. getToolPath() above already
|
|
2632
|
+
// probed PATH / npm-global / managed bin — offline-safe, no download. When the
|
|
2633
|
+
// caller forbids installs (allowInstall:false, e.g. PI_LENS_DISABLE_LSP_INSTALL=1)
|
|
2634
|
+
// we must still return a discovered binary and only skip the actual install.
|
|
2635
|
+
if (opts?.allowInstall === false) {
|
|
2636
|
+
logSessionStart(`auto-install ensure ${toolId}: install disabled — discovery only, not found (${Date.now() - ensureStartMs}ms)`);
|
|
2637
|
+
return undefined;
|
|
2638
|
+
}
|
|
2624
2639
|
const installed = await installTool(toolId);
|
|
2625
2640
|
if (!installed) {
|
|
2626
2641
|
logSessionStart(`auto-install ensure ${toolId}: unavailable (${Date.now() - ensureStartMs}ms)`);
|
|
@@ -2637,12 +2652,12 @@ export async function ensureTool(toolId, opts) {
|
|
|
2637
2652
|
}
|
|
2638
2653
|
return result;
|
|
2639
2654
|
})();
|
|
2640
|
-
ensureInFlight.set(
|
|
2655
|
+
ensureInFlight.set(inFlightKey, ensurePromise);
|
|
2641
2656
|
try {
|
|
2642
2657
|
return await ensurePromise;
|
|
2643
2658
|
}
|
|
2644
2659
|
finally {
|
|
2645
|
-
ensureInFlight.delete(
|
|
2660
|
+
ensureInFlight.delete(inFlightKey);
|
|
2646
2661
|
}
|
|
2647
2662
|
}
|
|
2648
2663
|
// --- Integration Helpers ---
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* PI_LENS_MAX_LOG_SIZE_MB - Max size before rotation (default: 10)
|
|
7
7
|
*
|
|
8
8
|
* Scope:
|
|
9
|
-
* - ~/.pi-lens/*.log (latency
|
|
9
|
+
* - ~/.pi-lens/*.log (all 8 global logs — see MANAGED_LOG_FILES: latency,
|
|
10
|
+
* sessionstart, tree-sitter, cascade, read-guard, actionable-warnings,
|
|
11
|
+
* ast-grep-tools, dead-code)
|
|
10
12
|
* - ~/.pi-lens/logs/*.jsonl (daily diagnostic logs)
|
|
11
|
-
* - ~/.pi-lens
|
|
13
|
+
* - ~/.pi-lens/<name>.<timestamp>.log (rotated backups, and legacy .log.<ts>)
|
|
12
14
|
*
|
|
13
15
|
* Excluded (intentionally NOT cleaned - project-scoped or persistent):
|
|
14
16
|
* - <project-data>/worklog.jsonl - Agent fixable diagnostics
|
|
@@ -25,6 +27,30 @@ import * as path from "node:path";
|
|
|
25
27
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
26
28
|
const LOG_DIR = getGlobalPiLensDir();
|
|
27
29
|
const LOGS_SUBDIR = path.join(LOG_DIR, "logs");
|
|
30
|
+
/**
|
|
31
|
+
* Every global `.log` pi-lens writes under ~/.pi-lens. Kept as one list so
|
|
32
|
+
* rotation and the storage summary can't drift out of sync — that drift is
|
|
33
|
+
* exactly what left actionable-warnings/ast-grep-tools/dead-code unrotated and
|
|
34
|
+
* growing unbounded. New global log file? Add it here.
|
|
35
|
+
*/
|
|
36
|
+
export const MANAGED_LOG_FILES = [
|
|
37
|
+
"latency.log",
|
|
38
|
+
"sessionstart.log",
|
|
39
|
+
"tree-sitter.log",
|
|
40
|
+
"cascade.log",
|
|
41
|
+
"read-guard.log",
|
|
42
|
+
"actionable-warnings.log",
|
|
43
|
+
"ast-grep-tools.log",
|
|
44
|
+
"dead-code.log",
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Matches a rotated backup, never an active log. Rotation writes
|
|
48
|
+
* `name.<ISO-timestamp>.log` (timestamp before the extension); an older shape
|
|
49
|
+
* was `name.log.<timestamp>`. Match both so every backup — whichever version
|
|
50
|
+
* produced it — is reaped by the retention sweep. The active `name.log` has no
|
|
51
|
+
* timestamp segment and no trailing suffix after `.log`, so it never matches.
|
|
52
|
+
*/
|
|
53
|
+
export const ROTATED_BACKUP_RE = /(\.\d{4}-\d{2}-\d{2}T.*\.log|\.log\..+)$/;
|
|
28
54
|
function getConfig() {
|
|
29
55
|
return {
|
|
30
56
|
retentionDays: Math.max(1, Number.parseInt(process.env.PI_LENS_LOG_RETENTION_DAYS ?? "7", 10) || 7),
|
|
@@ -123,17 +149,15 @@ export function runLogCleanup(dbg) {
|
|
|
123
149
|
// Cleanup old daily diagnostic logs (*.jsonl)
|
|
124
150
|
const dailyLogs = cleanupOldLogs(LOGS_SUBDIR, /\.jsonl$/, config.retentionDays);
|
|
125
151
|
results.cleaned += dailyLogs.deleted.length;
|
|
126
|
-
// Cleanup old rotated log backups
|
|
127
|
-
|
|
152
|
+
// Cleanup old rotated log backups. This sweep runs unconditionally on every
|
|
153
|
+
// session start, so correcting the pattern self-heals any pre-existing
|
|
154
|
+
// backlog on the next launch — no separate migration needed. (The prior
|
|
155
|
+
// `/\.log\./` only matched the legacy `name.log.<ts>` shape, never the
|
|
156
|
+
// current `name.<ts>.log`, so backups accumulated indefinitely.)
|
|
157
|
+
const rotatedLogs = cleanupOldLogs(LOG_DIR, ROTATED_BACKUP_RE, config.retentionDays);
|
|
128
158
|
results.cleaned += rotatedLogs.deleted.length;
|
|
129
159
|
// Check main logs for rotation
|
|
130
|
-
const mainLogs =
|
|
131
|
-
path.join(LOG_DIR, "latency.log"),
|
|
132
|
-
path.join(LOG_DIR, "sessionstart.log"),
|
|
133
|
-
path.join(LOG_DIR, "tree-sitter.log"),
|
|
134
|
-
path.join(LOG_DIR, "cascade.log"),
|
|
135
|
-
path.join(LOG_DIR, "read-guard.log"),
|
|
136
|
-
];
|
|
160
|
+
const mainLogs = MANAGED_LOG_FILES.map((name) => path.join(LOG_DIR, name));
|
|
137
161
|
for (const logFile of mainLogs) {
|
|
138
162
|
const rotation = rotateLogIfNeeded(logFile, config.maxSizeMB);
|
|
139
163
|
if (rotation.rotated) {
|
|
@@ -171,8 +195,7 @@ export function getLogStorageSummary() {
|
|
|
171
195
|
const files = [];
|
|
172
196
|
let totalMB = 0;
|
|
173
197
|
// Main logs
|
|
174
|
-
const
|
|
175
|
-
for (const name of mainLogs) {
|
|
198
|
+
for (const name of MANAGED_LOG_FILES) {
|
|
176
199
|
const filePath = path.join(LOG_DIR, name);
|
|
177
200
|
if (fs.existsSync(filePath)) {
|
|
178
201
|
const sizeMB = getFileSizeMB(filePath);
|