@herbertgao/pi-extensions 2026.8.5 → 2026.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +9 -9
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* through the host's own render path (`ctx.ui.notify` / a display-only session
|
|
21
21
|
* entry) — never a raw write.
|
|
22
22
|
*/
|
|
23
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
24
|
import * as path from "node:path";
|
|
24
25
|
import { isTestMode } from "./env-utils.js";
|
|
25
26
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
@@ -82,6 +83,260 @@ const CONSOLE_METHODS = [
|
|
|
82
83
|
"dir",
|
|
83
84
|
];
|
|
84
85
|
let consoleGuardInstalled = false;
|
|
86
|
+
/** The console methods captured before the patch, for an exact restore. */
|
|
87
|
+
const originalConsoleMethods = new Map();
|
|
88
|
+
/** The replacements this module installed, so uninstall never clobbers a later patch. */
|
|
89
|
+
const installedConsoleMethods = new Map();
|
|
90
|
+
/**
|
|
91
|
+
* The async capture window (#1434).
|
|
92
|
+
*
|
|
93
|
+
* The guard must catch pi-lens's own writes without swallowing the host's. pi's
|
|
94
|
+
* one-shot CLI commands print through `console.log`, so a permanent global
|
|
95
|
+
* reroute makes `pi list` print nothing in any project whose cwd loads the
|
|
96
|
+
* extension first. The store answers "is pi-lens executing right now?" and
|
|
97
|
+
* `AsyncLocalStorage` carries that answer into every promise, timer and
|
|
98
|
+
* callback created inside the window — so an async handler still captures after
|
|
99
|
+
* an `await`, while host code outside the window keeps its real sink.
|
|
100
|
+
*
|
|
101
|
+
* Built LAZILY (S1b, #1434 review): an active `AsyncLocalStorage` enables an
|
|
102
|
+
* async_hooks init hook for every promise in the process on Node 22 — the same
|
|
103
|
+
* hazard `tree-sitter-client.ts`'s `parseCacheMeasurement.disable()` comment
|
|
104
|
+
* documents — so constructing one at module scope taxes every `await` in the
|
|
105
|
+
* host even when the kill switch (`PI_LENS_CONSOLE_GUARD=0`) or test mode means
|
|
106
|
+
* the guard never installs and nothing ever reads the store. Construction is
|
|
107
|
+
* deferred to the first `runInConsoleCaptureWindow` call, and only happens once
|
|
108
|
+
* `installConsoleGuard()` has actually installed (never under the kill switch
|
|
109
|
+
* or test mode, since `consoleGuardInstalled` stays false in both).
|
|
110
|
+
*/
|
|
111
|
+
let consoleCaptureStorage;
|
|
112
|
+
function getConsoleCaptureStorage() {
|
|
113
|
+
if (!consoleGuardInstalled)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (consoleCaptureStorage === undefined) {
|
|
116
|
+
consoleCaptureStorage = new AsyncLocalStorage();
|
|
117
|
+
}
|
|
118
|
+
return consoleCaptureStorage;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Module evaluation is the one window `AsyncLocalStorage` cannot wrap: the
|
|
122
|
+
* module graph is evaluated by the loader, not by a pi-lens call. It is
|
|
123
|
+
* synchronous, so a plain flag is enough (#1333's original concern — a
|
|
124
|
+
* transitively loaded dependency writing during its own init).
|
|
125
|
+
*/
|
|
126
|
+
let moduleLoadWindowOpen = false;
|
|
127
|
+
/**
|
|
128
|
+
* True while pi-lens owns execution, so console writes belong in the log.
|
|
129
|
+
*
|
|
130
|
+
* `consoleCaptureStorage` may not have been constructed yet (S1b) — that state
|
|
131
|
+
* means "no window was ever opened through `runInConsoleCaptureWindow`", i.e.
|
|
132
|
+
* the same as an empty store, so it reads as "no window" rather than throwing.
|
|
133
|
+
*/
|
|
134
|
+
export function isConsoleCaptureActive() {
|
|
135
|
+
return (moduleLoadWindowOpen || consoleCaptureStorage?.getStore() === true);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Open the module-evaluation window. Called by `clients/console-guard-install.ts`
|
|
139
|
+
* before any other pi-lens module evaluates.
|
|
140
|
+
*
|
|
141
|
+
* The window closes explicitly at the end of `index.ts`. The backstop below
|
|
142
|
+
* closes it anyway if that never runs (an import throws, or the entry changes
|
|
143
|
+
* shape), because a leaked window would capture the host's output — the very
|
|
144
|
+
* bug this design fixes. Module evaluation is synchronous, so the end of the
|
|
145
|
+
* current macrotask is always past it. The timer is unref'd, so it can never
|
|
146
|
+
* hold a one-shot CLI process open (defect shape 4).
|
|
147
|
+
*/
|
|
148
|
+
export function openModuleLoadConsoleWindow() {
|
|
149
|
+
if (moduleLoadWindowOpen)
|
|
150
|
+
return;
|
|
151
|
+
moduleLoadWindowOpen = true;
|
|
152
|
+
const backstop = setImmediate(() => {
|
|
153
|
+
moduleLoadWindowOpen = false;
|
|
154
|
+
});
|
|
155
|
+
backstop.unref?.();
|
|
156
|
+
}
|
|
157
|
+
/** Close the module-evaluation window. Idempotent. */
|
|
158
|
+
export function closeModuleLoadConsoleWindow() {
|
|
159
|
+
moduleLoadWindowOpen = false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Run `fn` inside a capture window. Console writes from `fn`, and from anything
|
|
163
|
+
* `fn` schedules, go to the extension log instead of the terminal.
|
|
164
|
+
*
|
|
165
|
+
* When the guard was never installed (kill switch or test mode), there is
|
|
166
|
+
* nothing for a window to gate — `installConsoleGuard`'s own dispatcher is the
|
|
167
|
+
* only reader of `isConsoleCaptureActive()`, and it short-circuits before that
|
|
168
|
+
* call in both cases. Skip constructing the `AsyncLocalStorage` (S1b) and run
|
|
169
|
+
* `fn` directly.
|
|
170
|
+
*/
|
|
171
|
+
export function runInConsoleCaptureWindow(fn) {
|
|
172
|
+
const storage = getConsoleCaptureStorage();
|
|
173
|
+
if (!storage)
|
|
174
|
+
return fn();
|
|
175
|
+
return storage.run(true, fn);
|
|
176
|
+
}
|
|
177
|
+
function inCaptureWindow(fn) {
|
|
178
|
+
return function (...args) {
|
|
179
|
+
return runInConsoleCaptureWindow(() => fn.apply(this, args));
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* True for every host API member that can hand pi-lens's own functions back to
|
|
184
|
+
* the host to be called later: `on(event, handler)` and any `register*`
|
|
185
|
+
* method (`registerTool`, `registerCommand`, `registerMessageRenderer`,
|
|
186
|
+
* `registerShortcut`, `registerFlag`, `registerMarkdownTransformer`,
|
|
187
|
+
* `registerEntryRenderer`, `registerProvider`, and any the host adds later).
|
|
188
|
+
*
|
|
189
|
+
* This is a DENY-LIST, not an allow-list (#1434 S1a review): every member
|
|
190
|
+
* matching this predicate gets its function arguments wrapped, with no
|
|
191
|
+
* per-method exemption. The earlier allow-list form (only `on` and
|
|
192
|
+
* `registerTool`) let the 9 `registerCommand` call sites and
|
|
193
|
+
* `registerMessageRenderer` bypass the window, regressing #1333 for those
|
|
194
|
+
* paths — a host callback fired for one of them would still write straight to
|
|
195
|
+
* the terminal.
|
|
196
|
+
*/
|
|
197
|
+
function isCaptureSeam(prop) {
|
|
198
|
+
return prop === "on" || (typeof prop === "string" && prop.startsWith("register"));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Wrap every function value found in `value`, in place. Handles the two
|
|
202
|
+
* shapes a `register*`/`on` call takes today: a function passed directly
|
|
203
|
+
* (`on(event, handler)`, `registerMessageRenderer(type, fn)`), and a
|
|
204
|
+
* function ONE level inside a plain options/definition object
|
|
205
|
+
* (`options.handler`, `tool.execute`).
|
|
206
|
+
*
|
|
207
|
+
* Deliberately NOT recursive past that one level (#1434 perf review): a tool
|
|
208
|
+
* definition's `parameters`/`schema` is a large, deeply nested TypeBox object
|
|
209
|
+
* that holds no functions, but walking its full tree on every `registerTool`
|
|
210
|
+
* call — across every tool, on every extension activation, across the many
|
|
211
|
+
* test files that re-activate the extension per case — measurably slowed
|
|
212
|
+
* activation (a wiring test tripped its 5s budget under transform load; see
|
|
213
|
+
* the commit body for the measured before/after). Checking only the
|
|
214
|
+
* argument's own top-level keys is O(keys), independent of how deep an
|
|
215
|
+
* unrelated nested value's own structure goes, and covers every shape
|
|
216
|
+
* PI-LENS actually registers (`options.handler`, `tool.execute` — never two
|
|
217
|
+
* levels deep). It does NOT cover every shape the host's `ExtensionAPI` type
|
|
218
|
+
* permits: `registerProvider`'s `config.oauth.*` callbacks sit two levels
|
|
219
|
+
* deep and `config.models[]` sits behind a skipped array. pi-lens never
|
|
220
|
+
* calls `registerProvider`; extend the descent if that changes (same known-
|
|
221
|
+
* gap convention as the `pi.events` note in AGENTS.md).
|
|
222
|
+
*
|
|
223
|
+
* Mutates the object in place rather than copying — a spread copy would drop
|
|
224
|
+
* non-enumerable or prototype-carried members of a definition object (defect
|
|
225
|
+
* shape 5), and these definitions are pi-lens's own, built just above the
|
|
226
|
+
* register call.
|
|
227
|
+
*/
|
|
228
|
+
function wrapFunctionsInPlace(value) {
|
|
229
|
+
if (typeof value === "function") {
|
|
230
|
+
return inCaptureWindow(value);
|
|
231
|
+
}
|
|
232
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
const obj = value;
|
|
236
|
+
for (const key of Object.keys(obj)) {
|
|
237
|
+
const propValue = obj[key];
|
|
238
|
+
if (typeof propValue === "function") {
|
|
239
|
+
assignWrapped(obj, key, propValue);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Assign a wrapped function back onto `obj[key]`, degrading instead of
|
|
246
|
+
* throwing or dropping the registration when the property resists (S2b).
|
|
247
|
+
* ESM is always strict mode, so assigning into a non-writable data property
|
|
248
|
+
* throws a `TypeError`; `Object.defineProperty` is tried next (it can
|
|
249
|
+
* redefine a configurable-but-non-writable property); if both fail the
|
|
250
|
+
* property is frozen solid, so the ORIGINAL unwrapped function is left in
|
|
251
|
+
* place — the registration still succeeds, just without the console-capture
|
|
252
|
+
* net for that one callback, which is strictly better than the tool/command
|
|
253
|
+
* never registering at all.
|
|
254
|
+
*/
|
|
255
|
+
function assignWrapped(obj, key, fn) {
|
|
256
|
+
const wrapped = inCaptureWindow(fn);
|
|
257
|
+
try {
|
|
258
|
+
obj[key] = wrapped;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Non-writable data property — fall through to defineProperty.
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
Object.defineProperty(obj, key, {
|
|
266
|
+
value: wrapped,
|
|
267
|
+
writable: true,
|
|
268
|
+
configurable: true,
|
|
269
|
+
enumerable: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Frozen solid: leave the original unwrapped function in place rather
|
|
274
|
+
// than throwing (which would abort the whole register call) or silently
|
|
275
|
+
// dropping it.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Wrap the host extension API so every pi-lens entry point runs in a capture
|
|
280
|
+
* window.
|
|
281
|
+
*
|
|
282
|
+
* pi calls our event handlers and tool bodies from its own async context, so a
|
|
283
|
+
* window opened during registration does not reach them. Wrapping every
|
|
284
|
+
* `register*`/`on` seam (see `isCaptureSeam`) covers every current and future
|
|
285
|
+
* handler from one place, instead of a hand-maintained list of call sites.
|
|
286
|
+
* Everything else forwards untouched, except it is bound to `target` (S3a) so
|
|
287
|
+
* a destructured reference (`const { getFlag } = pi`) keeps working, and
|
|
288
|
+
* wrapper functions are memoized per-proxy in `wrapperCache` (S3b) so
|
|
289
|
+
* `proxy.on === proxy.on` — code that compares handler identity, or a test
|
|
290
|
+
* asserting a mock was called with a specific function reference across two
|
|
291
|
+
* reads of the same property, sees a stable value.
|
|
292
|
+
*/
|
|
293
|
+
export function withConsoleCaptureWindows(api) {
|
|
294
|
+
const wrapperCache = new Map();
|
|
295
|
+
const proxy = new Proxy(api, {
|
|
296
|
+
get(target, prop) {
|
|
297
|
+
// A non-configurable, non-writable OWN data property is a proxy
|
|
298
|
+
// invariant: the get trap MUST return the exact value the target
|
|
299
|
+
// holds, or the engine throws a TypeError on read. Degrade to the raw
|
|
300
|
+
// value instead of tripping that invariant (S2a) — a frozen host API
|
|
301
|
+
// loses the capture window for that member, but keeps working.
|
|
302
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
303
|
+
if (descriptor && descriptor.writable === false && descriptor.configurable === false) {
|
|
304
|
+
return descriptor.value;
|
|
305
|
+
}
|
|
306
|
+
const cached = wrapperCache.get(prop);
|
|
307
|
+
if (cached !== undefined)
|
|
308
|
+
return cached;
|
|
309
|
+
const value = Reflect.get(target, prop, target);
|
|
310
|
+
if (typeof value !== "function")
|
|
311
|
+
return value;
|
|
312
|
+
const method = value;
|
|
313
|
+
// A host that returns `this` for chaining would hand back the raw API,
|
|
314
|
+
// so a chained `on(...).on(...)` would register an unwrapped handler.
|
|
315
|
+
// Keep the proxy on the chain.
|
|
316
|
+
const keepProxy = (result) => result === target ? proxy : result;
|
|
317
|
+
// Pass-through members are cached too (S3a/S3b: `proxy.getFlag ===
|
|
318
|
+
// proxy.getFlag`), but the cached wrapper re-reads `target[prop]` on
|
|
319
|
+
// EVERY call rather than closing over `method` -- a plain
|
|
320
|
+
// `method.bind(target)` pins the function reference captured at first
|
|
321
|
+
// access, so a host (or a test simulating one) that reassigns its own
|
|
322
|
+
// method after registration would silently keep calling the stale one.
|
|
323
|
+
// Re-reading keeps the cached wrapper's identity stable while staying
|
|
324
|
+
// live to whatever `target[prop]` currently is.
|
|
325
|
+
const wrapper = isCaptureSeam(prop)
|
|
326
|
+
? (...args) => {
|
|
327
|
+
const wrapped = args.map((arg) => wrapFunctionsInPlace(arg));
|
|
328
|
+
return keepProxy(method.apply(target, wrapped));
|
|
329
|
+
}
|
|
330
|
+
: (...args) => {
|
|
331
|
+
const current = Reflect.get(target, prop, target);
|
|
332
|
+
return current.apply(target, args);
|
|
333
|
+
};
|
|
334
|
+
wrapperCache.set(prop, wrapper);
|
|
335
|
+
return wrapper;
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return proxy;
|
|
339
|
+
}
|
|
85
340
|
function formatConsoleArgs(args) {
|
|
86
341
|
return args
|
|
87
342
|
.map((arg) => {
|
|
@@ -104,6 +359,12 @@ function formatConsoleArgs(args) {
|
|
|
104
359
|
* mirror of `mcp/server.ts`'s `console.log = console.error` guard, which
|
|
105
360
|
* protects the JSON-RPC stdout channel for the same structural reason.
|
|
106
361
|
*
|
|
362
|
+
* The patch is a DISPATCHER, not a permanent reroute (#1434). It sends a write
|
|
363
|
+
* to the extension log only while pi-lens owns execution; every other write
|
|
364
|
+
* goes to the original console method. pi's own one-shot CLI commands print
|
|
365
|
+
* through `console.log`, so an unconditional reroute silenced them: `pi list`
|
|
366
|
+
* exited 0 with no output in any project whose cwd loaded the extension first.
|
|
367
|
+
*
|
|
107
368
|
* This is a NET, not the fix: pi-lens's own sites are migrated to real sinks
|
|
108
369
|
* with real schemas. Idempotent, and inert under test mode (vitest owns the
|
|
109
370
|
* console) and under `PI_LENS_CONSOLE_GUARD=0`.
|
|
@@ -120,9 +381,16 @@ export function installConsoleGuard() {
|
|
|
120
381
|
consoleGuardInstalled = true;
|
|
121
382
|
const target = console;
|
|
122
383
|
for (const method of CONSOLE_METHODS) {
|
|
123
|
-
|
|
384
|
+
const original = target[method];
|
|
385
|
+
if (typeof original !== "function")
|
|
124
386
|
continue;
|
|
125
|
-
|
|
387
|
+
const originalFn = original;
|
|
388
|
+
originalConsoleMethods.set(method, originalFn);
|
|
389
|
+
const replacement = (...args) => {
|
|
390
|
+
if (!isConsoleCaptureActive()) {
|
|
391
|
+
originalFn.apply(console, args);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
126
394
|
logExtension({
|
|
127
395
|
subsystem: "console",
|
|
128
396
|
level: method === "warn" ? "warn" : method === "error" ? "error" : "debug",
|
|
@@ -130,10 +398,35 @@ export function installConsoleGuard() {
|
|
|
130
398
|
metadata: { method },
|
|
131
399
|
});
|
|
132
400
|
};
|
|
401
|
+
installedConsoleMethods.set(method, replacement);
|
|
402
|
+
target[method] = replacement;
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Restore the exact console methods captured at install time.
|
|
408
|
+
*
|
|
409
|
+
* A method someone else patched after us is left alone — restoring it would
|
|
410
|
+
* clobber their replacement. Returns true when this call uninstalled.
|
|
411
|
+
*/
|
|
412
|
+
export function uninstallConsoleGuard() {
|
|
413
|
+
if (!consoleGuardInstalled)
|
|
414
|
+
return false;
|
|
415
|
+
consoleGuardInstalled = false;
|
|
416
|
+
const target = console;
|
|
417
|
+
for (const [method, original] of originalConsoleMethods) {
|
|
418
|
+
if (target[method] === installedConsoleMethods.get(method)) {
|
|
419
|
+
target[method] = original;
|
|
420
|
+
}
|
|
133
421
|
}
|
|
422
|
+
originalConsoleMethods.clear();
|
|
423
|
+
installedConsoleMethods.clear();
|
|
134
424
|
return true;
|
|
135
425
|
}
|
|
136
|
-
/** Test-only:
|
|
426
|
+
/** Test-only: uninstall the guard and close every open capture window. */
|
|
137
427
|
export function _resetConsoleGuardForTests() {
|
|
428
|
+
uninstallConsoleGuard();
|
|
138
429
|
consoleGuardInstalled = false;
|
|
430
|
+
consoleCaptureStorage = undefined;
|
|
431
|
+
moduleLoadWindowOpen = false;
|
|
139
432
|
}
|
|
@@ -18,13 +18,15 @@ export function getWorklogPath(cwd) {
|
|
|
18
18
|
* diagnostics that were already fixed by the tool, `false` for
|
|
19
19
|
* diagnostics that are fixable but require agent action.
|
|
20
20
|
*/
|
|
21
|
-
export function appendToWorklog(cwd, diagnostics, autoFixed) {
|
|
21
|
+
export function appendToWorklog(cwd, diagnostics, autoFixed, identity) {
|
|
22
22
|
if (diagnostics.length === 0)
|
|
23
23
|
return;
|
|
24
24
|
const worklogPath = getWorklogPath(cwd);
|
|
25
25
|
try {
|
|
26
26
|
fs.mkdirSync(path.dirname(worklogPath), { recursive: true });
|
|
27
27
|
const timestamp = new Date().toISOString();
|
|
28
|
+
const model = identity?.model?.trim() || undefined;
|
|
29
|
+
const provider = identity?.provider?.trim() || undefined;
|
|
28
30
|
const lines = diagnostics
|
|
29
31
|
.map((d) => {
|
|
30
32
|
const entry = {
|
|
@@ -38,6 +40,8 @@ export function appendToWorklog(cwd, diagnostics, autoFixed) {
|
|
|
38
40
|
fixable: d.fixable ?? false,
|
|
39
41
|
fixSuggestion: d.fixSuggestion,
|
|
40
42
|
autoFixed,
|
|
43
|
+
model,
|
|
44
|
+
provider,
|
|
41
45
|
};
|
|
42
46
|
return JSON.stringify(entry);
|
|
43
47
|
})
|
|
@@ -59,7 +59,15 @@
|
|
|
59
59
|
* New optional fields may be added under the same `v: 1` for any of the three
|
|
60
60
|
* events; a breaking change to an existing field's meaning must bump that
|
|
61
61
|
* event's `v` independently (each event versions separately since they're
|
|
62
|
-
* unrelated payloads).
|
|
62
|
+
* unrelated payloads). `FormatQueuedPayload.kinds` (S3d, #1432 review) is now
|
|
63
|
+
* an always-present required field at `publishFormatQueued`'s call boundary —
|
|
64
|
+
* both in-repo callers already passed it, so the `?? ["format"]` fallback
|
|
65
|
+
* was fabricating data no caller asked for. This is NOT a `v` bump: `kinds`
|
|
66
|
+
* was already part of the v1 payload shape and every wire-format consumer
|
|
67
|
+
* still reads the same field; a v1 emitter built against an older copy of
|
|
68
|
+
* this module that omits `kinds` at the call site now fails to compile
|
|
69
|
+
* rather than silently emitting a guessed value, and any reader written
|
|
70
|
+
* against v1 that still treats `kinds` as possibly-absent remains correct.
|
|
63
71
|
*
|
|
64
72
|
* Fire-and-forget: publishing must never affect the write path's or
|
|
65
73
|
* `agent_end`'s success or latency. Any failure (bus unavailable, emit
|
|
@@ -70,7 +78,7 @@
|
|
|
70
78
|
import { logBusEvent } from "./bus-events-logger.js";
|
|
71
79
|
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
72
80
|
import { normalizeFilePath } from "./path-utils.js";
|
|
73
|
-
import { createLiveBusEmitter, recordStaleBusFailure, } from "./live-bus-emitter.js";
|
|
81
|
+
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
74
82
|
export const BUS_FORMAT_QUEUED_EVENT = "pilens:format:queued";
|
|
75
83
|
export const BUS_FORMAT_QUEUED_VERSION = 1;
|
|
76
84
|
export const BUS_FORMAT_START_EVENT = "pilens:format:start";
|
|
@@ -131,8 +139,13 @@ export function publishFormatQueued(args) {
|
|
|
131
139
|
}
|
|
132
140
|
return;
|
|
133
141
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
142
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
143
|
+
event: BUS_FORMAT_QUEUED_EVENT,
|
|
144
|
+
cwd: normalizeFilePath(args.cwd),
|
|
145
|
+
}));
|
|
146
|
+
if (resolution.outcome === "stale-session")
|
|
147
|
+
return;
|
|
148
|
+
if (resolution.outcome === "unwired") {
|
|
136
149
|
if (!hasLoggedQueuedUnwired) {
|
|
137
150
|
hasLoggedQueuedUnwired = true;
|
|
138
151
|
logBusEvent({
|
|
@@ -143,6 +156,7 @@ export function publishFormatQueued(args) {
|
|
|
143
156
|
}
|
|
144
157
|
return;
|
|
145
158
|
}
|
|
159
|
+
const busEmit = resolution.emit;
|
|
146
160
|
try {
|
|
147
161
|
const payload = {
|
|
148
162
|
v: BUS_FORMAT_QUEUED_VERSION,
|
|
@@ -150,6 +164,7 @@ export function publishFormatQueued(args) {
|
|
|
150
164
|
filePath: normalizeFilePath(args.filePath),
|
|
151
165
|
cwd: normalizeFilePath(args.cwd),
|
|
152
166
|
tool: args.tool,
|
|
167
|
+
kinds: args.kinds,
|
|
153
168
|
};
|
|
154
169
|
busEmit(BUS_FORMAT_QUEUED_EVENT, payload);
|
|
155
170
|
hasLoggedQueuedFailure = false;
|
|
@@ -166,6 +181,7 @@ export function publishFormatQueued(args) {
|
|
|
166
181
|
outcome: "emit_failed",
|
|
167
182
|
cwd: normalizeFilePath(args.cwd),
|
|
168
183
|
error: String(err),
|
|
184
|
+
ctxSource: resolution.ctxSource,
|
|
169
185
|
});
|
|
170
186
|
if (!hasLoggedQueuedFailure) {
|
|
171
187
|
hasLoggedQueuedFailure = true;
|
|
@@ -196,8 +212,13 @@ export function publishFormatStart(args) {
|
|
|
196
212
|
}
|
|
197
213
|
return;
|
|
198
214
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
215
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
216
|
+
event: BUS_FORMAT_START_EVENT,
|
|
217
|
+
cwd: normalizeFilePath(args.cwd),
|
|
218
|
+
}));
|
|
219
|
+
if (resolution.outcome === "stale-session")
|
|
220
|
+
return;
|
|
221
|
+
if (resolution.outcome === "unwired") {
|
|
201
222
|
if (!hasLoggedStartUnwired) {
|
|
202
223
|
hasLoggedStartUnwired = true;
|
|
203
224
|
logBusEvent({
|
|
@@ -208,6 +229,7 @@ export function publishFormatStart(args) {
|
|
|
208
229
|
}
|
|
209
230
|
return;
|
|
210
231
|
}
|
|
232
|
+
const busEmit = resolution.emit;
|
|
211
233
|
try {
|
|
212
234
|
const paths = args.paths.map((p) => normalizeFilePath(p));
|
|
213
235
|
const payload = {
|
|
@@ -216,6 +238,7 @@ export function publishFormatStart(args) {
|
|
|
216
238
|
cwd: normalizeFilePath(args.cwd),
|
|
217
239
|
paths,
|
|
218
240
|
fileCount: paths.length,
|
|
241
|
+
kinds: args.kinds ?? ["format"],
|
|
219
242
|
};
|
|
220
243
|
busEmit(BUS_FORMAT_START_EVENT, payload);
|
|
221
244
|
hasLoggedStartFailure = false;
|
|
@@ -232,6 +255,7 @@ export function publishFormatStart(args) {
|
|
|
232
255
|
outcome: "emit_failed",
|
|
233
256
|
cwd: normalizeFilePath(args.cwd),
|
|
234
257
|
error: String(err),
|
|
258
|
+
ctxSource: resolution.ctxSource,
|
|
235
259
|
});
|
|
236
260
|
if (!hasLoggedStartFailure) {
|
|
237
261
|
hasLoggedStartFailure = true;
|
|
@@ -265,8 +289,13 @@ export function publishAutofixStart(args) {
|
|
|
265
289
|
}
|
|
266
290
|
return;
|
|
267
291
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
292
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
293
|
+
event: BUS_AUTOFIX_START_EVENT,
|
|
294
|
+
cwd: normalizeFilePath(args.cwd),
|
|
295
|
+
}));
|
|
296
|
+
if (resolution.outcome === "stale-session")
|
|
297
|
+
return;
|
|
298
|
+
if (resolution.outcome === "unwired") {
|
|
270
299
|
if (!hasLoggedAutofixStartUnwired) {
|
|
271
300
|
hasLoggedAutofixStartUnwired = true;
|
|
272
301
|
logBusEvent({
|
|
@@ -277,6 +306,7 @@ export function publishAutofixStart(args) {
|
|
|
277
306
|
}
|
|
278
307
|
return;
|
|
279
308
|
}
|
|
309
|
+
const busEmit = resolution.emit;
|
|
280
310
|
try {
|
|
281
311
|
const paths = args.paths.map((p) => normalizeFilePath(p));
|
|
282
312
|
const payload = {
|
|
@@ -302,6 +332,7 @@ export function publishAutofixStart(args) {
|
|
|
302
332
|
outcome: "emit_failed",
|
|
303
333
|
cwd: normalizeFilePath(args.cwd),
|
|
304
334
|
error: String(err),
|
|
335
|
+
ctxSource: resolution.ctxSource,
|
|
305
336
|
});
|
|
306
337
|
if (!hasLoggedAutofixStartFailure) {
|
|
307
338
|
hasLoggedAutofixStartFailure = true;
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import * as nodeFs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { isPathIgnoredByProject } from "./file-utils.js";
|
|
5
4
|
import { tokenizeShellCommand } from "./bash-file-access.js";
|
|
6
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
7
5
|
import { logLatency } from "./latency-logger.js";
|
|
8
|
-
|
|
6
|
+
import { advisoryFileHash, advisoryPathKey, MAX_ADVISORY_AFFECTED_FILES, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
9
7
|
function resolveGuardPath(filePath, cwd) {
|
|
10
8
|
return path.resolve(cwd, filePath);
|
|
11
9
|
}
|
|
12
10
|
function guardPathKey(filePath, cwd) {
|
|
13
|
-
return
|
|
11
|
+
return advisoryPathKey(filePath, cwd);
|
|
14
12
|
}
|
|
15
13
|
function fileFingerprint(filePath) {
|
|
16
|
-
|
|
17
|
-
return createHash("sha256").update(nodeFs.readFileSync(filePath)).digest("hex");
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
return `unreadable:${err.code ?? "unknown"}`;
|
|
21
|
-
}
|
|
14
|
+
return advisoryFileHash(filePath);
|
|
22
15
|
}
|
|
23
16
|
function currentFileFingerprint(filePath) {
|
|
24
17
|
try {
|
|
@@ -31,19 +24,11 @@ function currentFileFingerprint(filePath) {
|
|
|
31
24
|
: `unreadable:${err.code ?? "unknown"}`;
|
|
32
25
|
}
|
|
33
26
|
}
|
|
34
|
-
function snapshotFileHashes(files, cwd) {
|
|
35
|
-
const hashes = {};
|
|
36
|
-
for (const file of files) {
|
|
37
|
-
const resolved = resolveGuardPath(file, cwd);
|
|
38
|
-
hashes[guardPathKey(resolved, cwd)] = currentFileFingerprint(resolved);
|
|
39
|
-
}
|
|
40
|
-
return hashes;
|
|
41
|
-
}
|
|
42
27
|
function capAffectedFiles(files, cwd) {
|
|
43
28
|
const unique = [...new Set(files.map((file) => resolveGuardPath(file, cwd)))];
|
|
44
29
|
return {
|
|
45
|
-
files: unique.slice(0,
|
|
46
|
-
truncated: unique.length >
|
|
30
|
+
files: unique.slice(0, MAX_ADVISORY_AFFECTED_FILES),
|
|
31
|
+
truncated: unique.length > MAX_ADVISORY_AFFECTED_FILES,
|
|
47
32
|
};
|
|
48
33
|
}
|
|
49
34
|
/** Recovery is safe only when blocker content and its file provenance agree. */
|
|
@@ -424,6 +409,17 @@ export function writeGitGuardRecord(cacheManager, runtime, cwd, record) {
|
|
|
424
409
|
fileSeqByPath[key] = runtime.getFileSeq(resolveGuardPath(file, cwd));
|
|
425
410
|
}
|
|
426
411
|
}
|
|
412
|
+
const currentProvenance = snapshotAdvisoryProvenance({
|
|
413
|
+
cwd,
|
|
414
|
+
runtime,
|
|
415
|
+
generation: 0,
|
|
416
|
+
files: capped.files.map((file) => ({ path: file, role: "affected" })),
|
|
417
|
+
truncated: capped.truncated,
|
|
418
|
+
});
|
|
419
|
+
const fileContentHashes = Object.fromEntries(currentProvenance.files.map((file) => [
|
|
420
|
+
guardPathKey(file.path, cwd),
|
|
421
|
+
file.sha256,
|
|
422
|
+
]));
|
|
427
423
|
const data = {
|
|
428
424
|
...record,
|
|
429
425
|
affectedFiles: capped.files,
|
|
@@ -432,7 +428,8 @@ export function writeGitGuardRecord(cacheManager, runtime, cwd, record) {
|
|
|
432
428
|
blockingFiles: Array.isArray(record.blockingFiles)
|
|
433
429
|
? capAffectedFiles(record.blockingFiles, cwd).files
|
|
434
430
|
: undefined,
|
|
435
|
-
fileContentHashes
|
|
431
|
+
fileContentHashes,
|
|
432
|
+
provenance: record.provenance ?? currentProvenance,
|
|
436
433
|
};
|
|
437
434
|
try {
|
|
438
435
|
cacheManager.writeCache("turn-end-findings", data, cwd);
|
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
* Docs: https://pkg.go.dev/golang.org/x/tools/gopls
|
|
8
8
|
*/
|
|
9
9
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
10
|
import * as path from "node:path";
|
|
12
|
-
import {
|
|
11
|
+
import { createToolchainAvailability, } from "./dispatch/runners/utils/toolchain-availability.js";
|
|
13
12
|
// --- Common install paths ---
|
|
14
13
|
const GO_WINDOWS_PATHS = [
|
|
15
14
|
"C:\\Program Files\\Go\\bin\\go.exe",
|
|
16
15
|
"C:\\Go\\bin\\go.exe",
|
|
17
16
|
"go.exe", // PATH
|
|
18
17
|
];
|
|
18
|
+
/** Budget for the PATH candidate's `go version` probe, ms. */
|
|
19
|
+
const PROBE_TIMEOUT_MS = 3_000;
|
|
19
20
|
const GO_UNIX_PATHS = [
|
|
20
21
|
"/usr/local/go/bin/go",
|
|
21
22
|
"/usr/bin/go",
|
|
@@ -23,58 +24,39 @@ const GO_UNIX_PATHS = [
|
|
|
23
24
|
];
|
|
24
25
|
// --- Client ---
|
|
25
26
|
export class GoClient {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Availability lifecycle, behind the shared transient-aware latch (#1476).
|
|
29
|
+
* The PATH candidate is resolved by spawning `go version` with a 3 s budget,
|
|
30
|
+
* so a host stall could latch "no Go toolchain" for the whole session and
|
|
31
|
+
* silently disable every Go diagnostic until restart.
|
|
32
|
+
*/
|
|
33
|
+
availability;
|
|
28
34
|
log;
|
|
29
35
|
constructor(verbose = false) {
|
|
30
36
|
this.log = verbose
|
|
31
37
|
? createSubsystemLogger("go")
|
|
32
38
|
: () => { };
|
|
39
|
+
this.availability = createToolchainAvailability({
|
|
40
|
+
tool: "go",
|
|
41
|
+
label: "Go",
|
|
42
|
+
windowsPaths: GO_WINDOWS_PATHS,
|
|
43
|
+
unixPaths: GO_UNIX_PATHS,
|
|
44
|
+
probeArgs: ["version"],
|
|
45
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
46
|
+
log: (msg) => this.log(msg),
|
|
47
|
+
});
|
|
33
48
|
}
|
|
34
49
|
/**
|
|
35
50
|
* Find go executable path (async — probes PATH candidates off the event loop).
|
|
36
51
|
*/
|
|
37
52
|
async findGoPathAsync() {
|
|
38
|
-
|
|
39
|
-
return this.goPath;
|
|
40
|
-
const paths = process.platform === "win32" ? GO_WINDOWS_PATHS : GO_UNIX_PATHS;
|
|
41
|
-
for (const p of paths) {
|
|
42
|
-
try {
|
|
43
|
-
if (p.includes("\\") || p.includes("/")) {
|
|
44
|
-
// Absolute path - check if exists
|
|
45
|
-
if (fs.existsSync(p)) {
|
|
46
|
-
this.goPath = p;
|
|
47
|
-
return p;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
// Relative (PATH) - try running it
|
|
52
|
-
const result = await safeSpawnAsync(p, ["version"], {
|
|
53
|
-
timeout: 3000,
|
|
54
|
-
});
|
|
55
|
-
if (!result.error && result.status === 0) {
|
|
56
|
-
this.goPath = p;
|
|
57
|
-
return p;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
void err;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
53
|
+
return this.availability.findPath();
|
|
66
54
|
}
|
|
67
55
|
/**
|
|
68
56
|
* Check if Go is installed (cached)
|
|
69
57
|
*/
|
|
70
58
|
async isGoAvailableAsync() {
|
|
71
|
-
|
|
72
|
-
return this.goAvailable;
|
|
73
|
-
this.goAvailable = (await this.findGoPathAsync()) !== null;
|
|
74
|
-
if (this.goAvailable) {
|
|
75
|
-
this.log(`Go found: ${this.goPath}`);
|
|
76
|
-
}
|
|
77
|
-
return this.goAvailable;
|
|
59
|
+
return this.availability.isAvailable();
|
|
78
60
|
}
|
|
79
61
|
/**
|
|
80
62
|
* Check if a file is a Go file
|