@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
|
@@ -18,6 +18,8 @@ import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/con
|
|
|
18
18
|
import { findGlobalBinary } from "../../../package-manager.js";
|
|
19
19
|
import { safeSpawnAsync } from "../../../safe-spawn.js";
|
|
20
20
|
import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
|
|
21
|
+
import { classifyProbeFailure, createAvailabilityLatch, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
|
|
22
|
+
export { createAvailabilityLatch, classifyProbeFailure, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
|
|
21
23
|
/**
|
|
22
24
|
* True when the LSP runner will cover `ctx.filePath` via the given PRIMARY server
|
|
23
25
|
* id. Used by CLI runners that duplicate a linter a warm LSP already wraps
|
|
@@ -64,6 +66,29 @@ if (typeof __dirname !== "undefined") {
|
|
|
64
66
|
}
|
|
65
67
|
// Managed tools directory (~/.pi-lens/tools) — where ensureTool() installs binaries
|
|
66
68
|
const _managedToolsDir = path.join(getGlobalPiLensDir(), "tools");
|
|
69
|
+
/**
|
|
70
|
+
* The managed shim for a Node CLI tool (`~/.pi-lens/tools/node_modules/.bin/<tool>`),
|
|
71
|
+
* or null when it is not on disk.
|
|
72
|
+
*
|
|
73
|
+
* When the shim exists the tool IS installed, so availability needs no spawn at
|
|
74
|
+
* all — and a spawn that cannot happen cannot time out (#1467). knip and jscpd
|
|
75
|
+
* each carried a line-for-line copy of this resolver; #1476 folds them into one
|
|
76
|
+
* definition so the next managed tool inherits the fast path instead of a
|
|
77
|
+
* fourth copy.
|
|
78
|
+
*
|
|
79
|
+
* The pi-lens dir is read per call, never memoized at module load, so tests that
|
|
80
|
+
* point `getGlobalPiLensDir` at a temp home still see their own tree.
|
|
81
|
+
*/
|
|
82
|
+
export function findManagedNodeToolBinary(tool) {
|
|
83
|
+
const base = path.join(getGlobalPiLensDir(), "tools", "node_modules", ".bin", tool);
|
|
84
|
+
const candidates = process.platform === "win32" ? [`${base}.cmd`, `${base}.exe`, base] : [base];
|
|
85
|
+
try {
|
|
86
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
67
92
|
/**
|
|
68
93
|
* Find a command in venv first, then fall back to global.
|
|
69
94
|
* Checks common venv locations (.venv, venv) before trying global.
|
|
@@ -191,6 +216,13 @@ export function resetDispatchAvailabilityState() {
|
|
|
191
216
|
* `zig --version`). Passing the wrong probe makes the runner silently skip on
|
|
192
217
|
* every machine, so toolchains with a non-standard version command must override
|
|
193
218
|
* this.
|
|
219
|
+
*
|
|
220
|
+
* ## Latch policy (#1467)
|
|
221
|
+
*
|
|
222
|
+
* A `missing` / `non-installable` verdict is durable and is cached for the
|
|
223
|
+
* session. A `transient` verdict — timeout, abort, EAGAIN — is NOT: it is
|
|
224
|
+
* cached only for a bounded cooldown, after which the next caller re-probes.
|
|
225
|
+
* An installed tool therefore recovers on its own, without a host restart.
|
|
194
226
|
*/
|
|
195
227
|
export function createAvailabilityChecker(command, windowsExt = "", versionArgs = ["--version"], options = {}) {
|
|
196
228
|
const cacheByCwd = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
@@ -219,16 +251,63 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
219
251
|
available: null,
|
|
220
252
|
command: null,
|
|
221
253
|
outcome: null,
|
|
254
|
+
cause: null,
|
|
255
|
+
elapsedMs: 0,
|
|
256
|
+
retryAtMs: 0,
|
|
257
|
+
transientAttempts: 0,
|
|
222
258
|
};
|
|
223
259
|
cacheByCwd.set(key, created);
|
|
224
260
|
return created;
|
|
225
261
|
}
|
|
262
|
+
/** Record a verdict on the cache and emit exactly one decision record. */
|
|
263
|
+
function noteDecision(cache, resolvedCwd, verdict) {
|
|
264
|
+
cache.available = verdict.available;
|
|
265
|
+
cache.outcome = verdict.outcome;
|
|
266
|
+
cache.cause = verdict.cause;
|
|
267
|
+
cache.elapsedMs = verdict.elapsedMs;
|
|
268
|
+
let retryAfterMs;
|
|
269
|
+
if (verdict.available) {
|
|
270
|
+
cache.retryAtMs = 0;
|
|
271
|
+
cache.transientAttempts = 0;
|
|
272
|
+
}
|
|
273
|
+
else if (isLatchingOutcome(verdict.outcome)) {
|
|
274
|
+
cache.retryAtMs = 0;
|
|
275
|
+
cache.transientAttempts = 0;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
cache.transientAttempts += 1;
|
|
279
|
+
retryAfterMs = transientRetryDelayMs(cache.transientAttempts, verdict.cause);
|
|
280
|
+
cache.retryAtMs = Date.now() + retryAfterMs;
|
|
281
|
+
}
|
|
282
|
+
logAvailabilityDecision({
|
|
283
|
+
tool: command,
|
|
284
|
+
verdict: verdict.available ? "available" : "unavailable",
|
|
285
|
+
outcome: verdict.outcome,
|
|
286
|
+
cause: verdict.cause,
|
|
287
|
+
elapsedMs: verdict.elapsedMs,
|
|
288
|
+
latched: verdict.available || isLatchingOutcome(verdict.outcome),
|
|
289
|
+
...(verdict.hostStallMs !== undefined && {
|
|
290
|
+
hostStallMs: verdict.hostStallMs,
|
|
291
|
+
}),
|
|
292
|
+
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
293
|
+
budgetMs: options.probeTimeout ?? 5000,
|
|
294
|
+
}, resolvedCwd);
|
|
295
|
+
}
|
|
226
296
|
async function isAvailableAsync(cwd) {
|
|
227
297
|
ensureCurrentGeneration();
|
|
228
298
|
const resolvedCwd = cwd || process.cwd();
|
|
229
299
|
const cache = getCache(resolvedCwd);
|
|
230
|
-
if (cache.available === false)
|
|
231
|
-
|
|
300
|
+
if (cache.available === false) {
|
|
301
|
+
// A durable "this machine does not have the tool" stays cached; a
|
|
302
|
+
// transient probe failure only holds until its cooldown expires, so an
|
|
303
|
+
// installed tool cannot be disabled for the life of the process by one
|
|
304
|
+
// slow second at warm-up (#1467).
|
|
305
|
+
if (cache.outcome !== "transient")
|
|
306
|
+
return false;
|
|
307
|
+
if (Date.now() < cache.retryAtMs)
|
|
308
|
+
return false;
|
|
309
|
+
cache.available = null;
|
|
310
|
+
}
|
|
232
311
|
if (cache.available === true && cache.command) {
|
|
233
312
|
if (await isSpawnableCommand(cache.command))
|
|
234
313
|
return true;
|
|
@@ -237,6 +316,7 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
237
316
|
cache.available = null;
|
|
238
317
|
cache.command = null;
|
|
239
318
|
cache.outcome = null;
|
|
319
|
+
cache.cause = null;
|
|
240
320
|
}
|
|
241
321
|
const key = path.resolve(resolvedCwd);
|
|
242
322
|
const existing = inFlightByCwd.get(key);
|
|
@@ -247,9 +327,13 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
247
327
|
promise = (async () => {
|
|
248
328
|
const fastPath = options.fastPath?.();
|
|
249
329
|
if (fastPath) {
|
|
250
|
-
cache.available = true;
|
|
251
|
-
cache.outcome = "success";
|
|
252
330
|
cache.command = fastPath;
|
|
331
|
+
noteDecision(cache, resolvedCwd, {
|
|
332
|
+
available: true,
|
|
333
|
+
outcome: "success",
|
|
334
|
+
cause: "fast-path",
|
|
335
|
+
elapsedMs: 0,
|
|
336
|
+
});
|
|
253
337
|
return true;
|
|
254
338
|
}
|
|
255
339
|
// A bad/removed workspace must not be mistaken for a missing tool and
|
|
@@ -258,49 +342,71 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
258
342
|
try {
|
|
259
343
|
const cwdStat = await fs.promises.stat(resolvedCwd);
|
|
260
344
|
if (!cwdStat.isDirectory()) {
|
|
261
|
-
cache
|
|
262
|
-
|
|
345
|
+
noteDecision(cache, resolvedCwd, {
|
|
346
|
+
available: false,
|
|
347
|
+
outcome: "non-installable",
|
|
348
|
+
cause: "bad-cwd",
|
|
349
|
+
elapsedMs: 0,
|
|
350
|
+
});
|
|
263
351
|
return false;
|
|
264
352
|
}
|
|
265
353
|
}
|
|
266
354
|
catch {
|
|
267
|
-
cache
|
|
268
|
-
|
|
355
|
+
noteDecision(cache, resolvedCwd, {
|
|
356
|
+
available: false,
|
|
357
|
+
outcome: "non-installable",
|
|
358
|
+
cause: "bad-cwd",
|
|
359
|
+
elapsedMs: 0,
|
|
360
|
+
});
|
|
269
361
|
return false;
|
|
270
362
|
}
|
|
271
363
|
const cmd = findCommand(resolvedCwd);
|
|
272
364
|
const env = await options.environment?.(resolvedCwd);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
365
|
+
// The probe budget is enforced by a HOST-side timer, so host event-loop
|
|
366
|
+
// stalls are charged to the child. Measure the stall that overlapped the
|
|
367
|
+
// window and hand it to the classifier (#1467).
|
|
368
|
+
const stallSampler = startHostStallSampler();
|
|
369
|
+
const startedAt = Date.now();
|
|
370
|
+
let result;
|
|
371
|
+
let hostStallMs;
|
|
372
|
+
try {
|
|
373
|
+
result = await safeSpawnAsync(cmd, versionArgs, {
|
|
374
|
+
timeout: options.probeTimeout ?? 5000,
|
|
375
|
+
cwd: resolvedCwd,
|
|
376
|
+
env,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
hostStallMs = stallSampler.stop();
|
|
381
|
+
}
|
|
382
|
+
const elapsedMs = Date.now() - startedAt;
|
|
278
383
|
if (!result.error && result.status === 0) {
|
|
279
|
-
cache.available = true;
|
|
280
|
-
cache.outcome = "success";
|
|
281
384
|
cache.command = cmd;
|
|
385
|
+
noteDecision(cache, resolvedCwd, {
|
|
386
|
+
available: true,
|
|
387
|
+
outcome: "success",
|
|
388
|
+
cause: "ok",
|
|
389
|
+
elapsedMs,
|
|
390
|
+
hostStallMs,
|
|
391
|
+
});
|
|
282
392
|
return true;
|
|
283
393
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
394
|
+
const { outcome, cause } = classifyProbeFailure(result, {
|
|
395
|
+
hostStallMs,
|
|
396
|
+
unclassifiedFailureOutcome: options.unclassifiedFailureOutcome,
|
|
397
|
+
});
|
|
398
|
+
// Only a TYPED tool-not-found invalidates the PATH walk memo; an
|
|
399
|
+
// `unclassifiedFailureOutcome: "missing"` compatibility verdict is a
|
|
400
|
+
// guess, not evidence that PATH changed.
|
|
401
|
+
if (result.spawnFailure?.kind === "tool-not-found")
|
|
287
402
|
resetPathWalkMemo();
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
errorCode === "EBUSY") {
|
|
296
|
-
cache.outcome = "transient";
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
// A present command that rejects its version probe (or an EACCES/
|
|
300
|
-
// EINVAL/UNKNOWN failure) is not repaired by reinstalling it.
|
|
301
|
-
cache.outcome =
|
|
302
|
-
options.unclassifiedFailureOutcome ?? "non-installable";
|
|
303
|
-
}
|
|
403
|
+
noteDecision(cache, resolvedCwd, {
|
|
404
|
+
available: false,
|
|
405
|
+
outcome,
|
|
406
|
+
cause,
|
|
407
|
+
elapsedMs,
|
|
408
|
+
hostStallMs,
|
|
409
|
+
});
|
|
304
410
|
return false;
|
|
305
411
|
})().finally(() => {
|
|
306
412
|
// A session reset clears this map and a caller may immediately start a
|
|
@@ -323,7 +429,18 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
|
|
|
323
429
|
ensureCurrentGeneration();
|
|
324
430
|
return getCache(cwd || process.cwd()).outcome;
|
|
325
431
|
}
|
|
326
|
-
|
|
432
|
+
function getVerdict(cwd) {
|
|
433
|
+
ensureCurrentGeneration();
|
|
434
|
+
const cache = getCache(cwd || process.cwd());
|
|
435
|
+
return {
|
|
436
|
+
outcome: cache.outcome,
|
|
437
|
+
cause: cache.cause,
|
|
438
|
+
elapsedMs: cache.elapsedMs,
|
|
439
|
+
latched: cache.available !== false || isLatchingOutcome(cache.outcome ?? "missing"),
|
|
440
|
+
retryAtMs: cache.retryAtMs,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return { isAvailableAsync, getCommand, getOutcome, getVerdict, reset };
|
|
327
444
|
}
|
|
328
445
|
/**
|
|
329
446
|
* Per-cwd cached availability probe for spawn signatures that don't fit
|
|
@@ -498,20 +615,46 @@ export function resolveAvailableOrInstall(checker, toolId, cwd) {
|
|
|
498
615
|
// =============================================================================
|
|
499
616
|
// SHARED AST-GREP AVAILABILITY
|
|
500
617
|
// =============================================================================
|
|
501
|
-
|
|
502
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Shared ast-grep availability across all slop runners, behind the transient-
|
|
620
|
+
* aware latch (#1476). This module-level memo carried the same shape `SgRunner`
|
|
621
|
+
* did — one failed sweep, including a timeout, disabled ast-grep for every slop
|
|
622
|
+
* runner for the life of the process.
|
|
623
|
+
*/
|
|
624
|
+
const sgLatch = createAvailabilityLatch();
|
|
503
625
|
let sgCmd = null;
|
|
504
626
|
let sgCmdArgs = [];
|
|
627
|
+
/** Classification of the current sweep, accumulated across candidates. */
|
|
628
|
+
let sgSweepSawTransient = false;
|
|
629
|
+
let sgSweepTransientCause = "probe-timeout";
|
|
630
|
+
let sgSweepHostStallMs = 0;
|
|
505
631
|
function isAstGrepVersionOutput(output) {
|
|
506
632
|
return /\bast[- ]grep\b/i.test(output);
|
|
507
633
|
}
|
|
508
634
|
async function probeAstGrepCommandAsync(cmd, argsPrefix = []) {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
635
|
+
const sampler = startHostStallSampler();
|
|
636
|
+
let check;
|
|
637
|
+
let hostStallMs;
|
|
638
|
+
try {
|
|
639
|
+
check = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
|
|
640
|
+
timeout: 5000,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
hostStallMs = sampler.stop();
|
|
645
|
+
sgSweepHostStallMs += hostStallMs;
|
|
646
|
+
}
|
|
647
|
+
if (!check.error &&
|
|
513
648
|
check.status === 0 &&
|
|
514
|
-
isAstGrepVersionOutput(`${check.stdout}\n${check.stderr}`))
|
|
649
|
+
isAstGrepVersionOutput(`${check.stdout}\n${check.stderr}`)) {
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
const { outcome, cause } = classifyProbeFailure(check, { hostStallMs });
|
|
653
|
+
if (outcome === "transient") {
|
|
654
|
+
sgSweepSawTransient = true;
|
|
655
|
+
sgSweepTransientCause = cause;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
515
658
|
}
|
|
516
659
|
/** Pre-filter local node_modules/.bin candidates that actually exist on disk. */
|
|
517
660
|
function buildSgLocalBins() {
|
|
@@ -545,7 +688,7 @@ let sgAvailabilityGeneration = availabilityStateGeneration;
|
|
|
545
688
|
function ensureCurrentSgGeneration() {
|
|
546
689
|
if (sgAvailabilityGeneration === availabilityStateGeneration)
|
|
547
690
|
return;
|
|
548
|
-
|
|
691
|
+
sgLatch.reset();
|
|
549
692
|
sgCmd = null;
|
|
550
693
|
sgCmdArgs = [];
|
|
551
694
|
sgAvailableInFlight = null;
|
|
@@ -553,17 +696,24 @@ function ensureCurrentSgGeneration() {
|
|
|
553
696
|
}
|
|
554
697
|
export async function isSgAvailableAsync() {
|
|
555
698
|
ensureCurrentSgGeneration();
|
|
556
|
-
|
|
557
|
-
|
|
699
|
+
// `read()` returns null when the last verdict was transient and its cooldown
|
|
700
|
+
// expired: re-probe rather than stay dead for the session (#1476).
|
|
701
|
+
const memo = sgLatch.read();
|
|
702
|
+
if (memo !== null)
|
|
703
|
+
return memo;
|
|
558
704
|
if (sgAvailableInFlight)
|
|
559
705
|
return sgAvailableInFlight;
|
|
560
706
|
sgAvailableInFlight = (async () => {
|
|
707
|
+
const startedAt = Date.now();
|
|
708
|
+
sgSweepSawTransient = false;
|
|
709
|
+
sgSweepTransientCause = "probe-timeout";
|
|
710
|
+
sgSweepHostStallMs = 0;
|
|
561
711
|
// 1. Local node_modules/.bin
|
|
562
712
|
for (const localBin of buildSgLocalBins()) {
|
|
563
713
|
if (await probeAstGrepCommandAsync(localBin)) {
|
|
564
714
|
sgCmd = localBin;
|
|
565
715
|
sgCmdArgs = [];
|
|
566
|
-
|
|
716
|
+
noteSgAvailable(startedAt);
|
|
567
717
|
return true;
|
|
568
718
|
}
|
|
569
719
|
}
|
|
@@ -572,7 +722,7 @@ export async function isSgAvailableAsync() {
|
|
|
572
722
|
if (await probeAstGrepCommandAsync(cmd)) {
|
|
573
723
|
sgCmd = cmd;
|
|
574
724
|
sgCmdArgs = [];
|
|
575
|
-
|
|
725
|
+
noteSgAvailable(startedAt);
|
|
576
726
|
return true;
|
|
577
727
|
}
|
|
578
728
|
}
|
|
@@ -583,7 +733,7 @@ export async function isSgAvailableAsync() {
|
|
|
583
733
|
if (globalBin && (await probeAstGrepCommandAsync(globalBin))) {
|
|
584
734
|
sgCmd = globalBin;
|
|
585
735
|
sgCmdArgs = [];
|
|
586
|
-
|
|
736
|
+
noteSgAvailable(startedAt);
|
|
587
737
|
return true;
|
|
588
738
|
}
|
|
589
739
|
}
|
|
@@ -591,16 +741,46 @@ export async function isSgAvailableAsync() {
|
|
|
591
741
|
if (await probeAstGrepCommandAsync("npx", ["--no", "--", "ast-grep"])) {
|
|
592
742
|
sgCmd = "npx";
|
|
593
743
|
sgCmdArgs = ["--no", "--", "ast-grep"];
|
|
594
|
-
|
|
744
|
+
noteSgAvailable(startedAt);
|
|
595
745
|
return true;
|
|
596
746
|
}
|
|
597
|
-
|
|
747
|
+
// A timeout on ANY candidate is evidence about the host, not the tool.
|
|
748
|
+
noteSgUnavailable(startedAt, sgSweepSawTransient ? "transient" : "missing", sgSweepSawTransient ? sgSweepTransientCause : "not-found");
|
|
598
749
|
return false;
|
|
599
750
|
})().finally(() => {
|
|
600
751
|
sgAvailableInFlight = null;
|
|
601
752
|
});
|
|
602
753
|
return sgAvailableInFlight;
|
|
603
754
|
}
|
|
755
|
+
/** Record a successful shared-ast-grep sweep, with one decision record. */
|
|
756
|
+
function noteSgAvailable(startedAt) {
|
|
757
|
+
sgLatch.noteAvailable();
|
|
758
|
+
logAvailabilityDecision({
|
|
759
|
+
tool: "ast-grep",
|
|
760
|
+
verdict: "available",
|
|
761
|
+
outcome: "success",
|
|
762
|
+
cause: "ok",
|
|
763
|
+
elapsedMs: Date.now() - startedAt,
|
|
764
|
+
latched: true,
|
|
765
|
+
hostStallMs: sgSweepHostStallMs,
|
|
766
|
+
budgetMs: 5000,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
/** Record a failed shared-ast-grep sweep; a transient verdict expires. */
|
|
770
|
+
function noteSgUnavailable(startedAt, outcome, cause) {
|
|
771
|
+
const retryAfterMs = sgLatch.noteUnavailable(outcome, cause);
|
|
772
|
+
logAvailabilityDecision({
|
|
773
|
+
tool: "ast-grep",
|
|
774
|
+
verdict: "unavailable",
|
|
775
|
+
outcome,
|
|
776
|
+
cause,
|
|
777
|
+
elapsedMs: Date.now() - startedAt,
|
|
778
|
+
latched: outcome !== "transient",
|
|
779
|
+
hostStallMs: sgSweepHostStallMs,
|
|
780
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
781
|
+
budgetMs: 5000,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
604
784
|
export function getSgCommand() {
|
|
605
785
|
ensureCurrentSgGeneration();
|
|
606
786
|
return {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared toolchain-availability lifecycle (#1476 Sonar follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `candidate-probe.ts` factored out the PATH sweep the Go and Rust clients ran,
|
|
5
|
+
* but the LIFECYCLE around it stayed written twice: the transient-aware latch,
|
|
6
|
+
* the in-flight dedupe that keeps concurrent first-time callers to one sweep,
|
|
7
|
+
* the path memo, and the two `availability_decision` records. Two copies of one
|
|
8
|
+
* rule is how #1467's fix missed seven sites, so the rule lives here once and
|
|
9
|
+
* each client contributes only its configuration.
|
|
10
|
+
*
|
|
11
|
+
* Callers keep their own public method names — `isGoAvailableAsync`,
|
|
12
|
+
* `findCargoPathAsync` — because other modules call them; only the body moves.
|
|
13
|
+
*/
|
|
14
|
+
import { createAvailabilityLatch, logAvailabilityDecision, } from "./availability-policy.js";
|
|
15
|
+
import { probeAvailabilityCandidates } from "./candidate-probe.js";
|
|
16
|
+
/**
|
|
17
|
+
* Own one toolchain's availability: sweep the platform candidate list, memoize
|
|
18
|
+
* the path that answered, and park the verdict behind the shared latch so a
|
|
19
|
+
* timed-out probe expires instead of latching "the toolchain is not installed"
|
|
20
|
+
* for the life of the process.
|
|
21
|
+
*/
|
|
22
|
+
export function createToolchainAvailability(config) {
|
|
23
|
+
const availabilityLatch = createAvailabilityLatch();
|
|
24
|
+
let toolPath = null;
|
|
25
|
+
/** Classification of the candidate sweep, for the retry decision. */
|
|
26
|
+
let sweepSawTransient = false;
|
|
27
|
+
let sweepTransientCause = "probe-timeout";
|
|
28
|
+
let sweepHostStallMs = 0;
|
|
29
|
+
let ensureInFlight = null;
|
|
30
|
+
async function findPath() {
|
|
31
|
+
if (toolPath)
|
|
32
|
+
return toolPath;
|
|
33
|
+
const paths = process.platform === "win32" ? config.windowsPaths : config.unixPaths;
|
|
34
|
+
const sweep = await probeAvailabilityCandidates(paths, config.probeArgs, config.budgetMs);
|
|
35
|
+
sweepSawTransient = sweep.sawTransient;
|
|
36
|
+
sweepTransientCause = sweep.transientCause;
|
|
37
|
+
sweepHostStallMs = sweep.hostStallMs;
|
|
38
|
+
if (sweep.foundPath)
|
|
39
|
+
toolPath = sweep.foundPath;
|
|
40
|
+
return sweep.foundPath;
|
|
41
|
+
}
|
|
42
|
+
async function resolveAvailability() {
|
|
43
|
+
const startedAt = Date.now();
|
|
44
|
+
const found = (await findPath()) !== null;
|
|
45
|
+
if (found) {
|
|
46
|
+
availabilityLatch.noteAvailable();
|
|
47
|
+
config.log(`${config.label} found: ${toolPath}`);
|
|
48
|
+
logAvailabilityDecision({
|
|
49
|
+
tool: config.tool,
|
|
50
|
+
verdict: "available",
|
|
51
|
+
outcome: "success",
|
|
52
|
+
cause: "ok",
|
|
53
|
+
elapsedMs: Date.now() - startedAt,
|
|
54
|
+
latched: true,
|
|
55
|
+
hostStallMs: sweepHostStallMs,
|
|
56
|
+
budgetMs: config.budgetMs,
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// A timed-out version probe is evidence about this moment, not about
|
|
61
|
+
// whether the toolchain is installed; it expires instead of latching.
|
|
62
|
+
const outcome = sweepSawTransient ? "transient" : "missing";
|
|
63
|
+
const cause = sweepSawTransient ? sweepTransientCause : "not-found";
|
|
64
|
+
const retryAfterMs = availabilityLatch.noteUnavailable(outcome, cause);
|
|
65
|
+
logAvailabilityDecision({
|
|
66
|
+
tool: config.tool,
|
|
67
|
+
verdict: "unavailable",
|
|
68
|
+
outcome,
|
|
69
|
+
cause,
|
|
70
|
+
elapsedMs: Date.now() - startedAt,
|
|
71
|
+
latched: outcome !== "transient",
|
|
72
|
+
hostStallMs: sweepHostStallMs,
|
|
73
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
74
|
+
budgetMs: config.budgetMs,
|
|
75
|
+
});
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
async function isAvailable() {
|
|
79
|
+
// `read()` returns null when the last verdict was transient and its
|
|
80
|
+
// cooldown expired, which re-enters the candidate sweep (#1476).
|
|
81
|
+
const memo = availabilityLatch.read();
|
|
82
|
+
if (memo !== null)
|
|
83
|
+
return memo;
|
|
84
|
+
// Now that a verdict can expire, concurrent callers arriving just after a
|
|
85
|
+
// cooldown must share ONE sweep rather than each spawning their own.
|
|
86
|
+
if (ensureInFlight)
|
|
87
|
+
return ensureInFlight;
|
|
88
|
+
ensureInFlight = resolveAvailability();
|
|
89
|
+
try {
|
|
90
|
+
return await ensureInFlight;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
ensureInFlight = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { findPath, isAvailable };
|
|
97
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { logBusEvent } from "./bus-events-logger.js";
|
|
25
25
|
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
26
26
|
import { normalizeFilePath } from "./path-utils.js";
|
|
27
|
-
import { createLiveBusEmitter, recordStaleBusFailure, } from "./live-bus-emitter.js";
|
|
27
|
+
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
28
28
|
export const BUS_DISPOSITION_EVENT = "pilens:diagnostic:disposition";
|
|
29
29
|
export const BUS_DISPOSITION_VERSION = 1;
|
|
30
30
|
const liveEmitter = createLiveBusEmitter();
|
|
@@ -67,8 +67,13 @@ export function publishDisposition(args) {
|
|
|
67
67
|
}
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
70
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
71
|
+
event: BUS_DISPOSITION_EVENT,
|
|
72
|
+
cwd: normalizeFilePath(args.cwd),
|
|
73
|
+
}));
|
|
74
|
+
if (resolution.outcome === "stale-session")
|
|
75
|
+
return;
|
|
76
|
+
if (resolution.outcome === "unwired") {
|
|
72
77
|
if (!hasLoggedUnwired) {
|
|
73
78
|
hasLoggedUnwired = true;
|
|
74
79
|
logBusEvent({
|
|
@@ -79,6 +84,7 @@ export function publishDisposition(args) {
|
|
|
79
84
|
}
|
|
80
85
|
return;
|
|
81
86
|
}
|
|
87
|
+
const busEmit = resolution.emit;
|
|
82
88
|
try {
|
|
83
89
|
const payload = {
|
|
84
90
|
v: BUS_DISPOSITION_VERSION,
|
|
@@ -106,6 +112,7 @@ export function publishDisposition(args) {
|
|
|
106
112
|
outcome: "emit_failed",
|
|
107
113
|
cwd: normalizeFilePath(args.cwd),
|
|
108
114
|
error: String(err),
|
|
115
|
+
ctxSource: resolution.ctxSource,
|
|
109
116
|
});
|
|
110
117
|
if (!hasLoggedFailure) {
|
|
111
118
|
hasLoggedFailure = true;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* The moment pi-lens's module graph started evaluating, captured as this
|
|
4
|
+
* module's first statement (#1374 host-boot vs extension-eval split).
|
|
5
|
+
*
|
|
6
|
+
* This module is otherwise side-effect-free by design (#1434 S3d review): it
|
|
7
|
+
* does nothing but capture a timestamp. `clients/console-guard-install.ts`
|
|
8
|
+
* imports it as its FIRST import so the capture happens before the guard's
|
|
9
|
+
* own install work, and `clients/startup-timing.ts` imports it too, for the
|
|
10
|
+
* constant alone. Before this split, `startup-timing.ts` imported the
|
|
11
|
+
* constant FROM `console-guard-install.ts` — which meant importing
|
|
12
|
+
* startup-timing anywhere (e.g. a test measuring load time) silently
|
|
13
|
+
* installed the console guard as a side effect. Splitting the constant out
|
|
14
|
+
* means importing this module, or `startup-timing.ts`, never installs
|
|
15
|
+
* anything.
|
|
16
|
+
*/
|
|
17
|
+
export const PI_LENS_EVAL_STARTED_MS = performance.now();
|