@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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 +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- 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/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -21,6 +21,7 @@ import { recordDegradation } from "./degradation-ledger.js";
|
|
|
21
21
|
import { logExtension } from "./extension-log.js";
|
|
22
22
|
import { isFullyQualifiedWin32 } from "./path-utils.js";
|
|
23
23
|
import { startSpawnUsageSampler } from "./resource-sampler.js";
|
|
24
|
+
import { compareOrdinal } from "./string-utils.js";
|
|
24
25
|
/** Intent-level spawn failure. `cause` retains the original OS Error/errno. */
|
|
25
26
|
export class SpawnFailureError extends Error {
|
|
26
27
|
kind;
|
|
@@ -701,7 +702,7 @@ export function resolveWindowsCommandForEnvironment(command, cwd, env) {
|
|
|
701
702
|
.flatMap(([key, value]) => /^=[A-Za-z]:$/.test(key)
|
|
702
703
|
? [[key.toLowerCase(), value]]
|
|
703
704
|
: [])
|
|
704
|
-
.sort(([left], [right]) => left
|
|
705
|
+
.sort(([left], [right]) => compareOrdinal(left, right));
|
|
705
706
|
// Keep presence separate from value: absent PATHEXT means the Windows
|
|
706
707
|
// default extension list, while PATHEXT="" means no implicit extensions.
|
|
707
708
|
// The per-drive snapshot is equally important: it is provenance for
|
|
@@ -792,6 +793,13 @@ function ensureUtf8ConsoleCodePageOnce() {
|
|
|
792
793
|
spawnSync(cmdExe, ["/d", "/s", "/c", "chcp 65001 >nul 2>&1"], {
|
|
793
794
|
shell: false,
|
|
794
795
|
windowsHide: true,
|
|
796
|
+
// #1980 population sweep: a synchronous spawn with no timeout parks
|
|
797
|
+
// the event loop for as long as the child takes, and nothing here
|
|
798
|
+
// bounds that. `chcp` is normally instant, but this runs before the
|
|
799
|
+
// FIRST real spawn of the process, which is exactly when a cold or
|
|
800
|
+
// antivirus-throttled cmd.exe is slowest. Matches the 5s bound every
|
|
801
|
+
// other synchronous child-process site in this repo already carries.
|
|
802
|
+
timeout: 5000,
|
|
795
803
|
});
|
|
796
804
|
}
|
|
797
805
|
catch {
|
|
@@ -891,6 +899,8 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
891
899
|
let aborted = false;
|
|
892
900
|
let killed = false;
|
|
893
901
|
let outputTruncated = false;
|
|
902
|
+
let killedForOutputCap = false;
|
|
903
|
+
let streamingMatch = false;
|
|
894
904
|
// #1651 review: a single boolean the close/error handlers both check
|
|
895
905
|
// AND set, so whichever one decides the outcome first wins outright —
|
|
896
906
|
// the DECISION itself is shape-based (see the `close` handler below),
|
|
@@ -937,21 +947,116 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
937
947
|
options.maxOutputBytes > 0
|
|
938
948
|
? Math.floor(options.maxOutputBytes)
|
|
939
949
|
: undefined;
|
|
940
|
-
const
|
|
950
|
+
const outputTruncationMarker = "\n...[output truncated]...\n";
|
|
951
|
+
let retainedHead = [];
|
|
952
|
+
let retainedTail = [];
|
|
953
|
+
let retainedTailBytes = 0;
|
|
954
|
+
let retainedTailLimit = 0;
|
|
955
|
+
let truncationStream = "stderr";
|
|
956
|
+
const streamingMatcher = options?.matchWhileStreaming;
|
|
957
|
+
const streamingCarryBytes = streamingMatcher
|
|
958
|
+
? Math.max(0, streamingMatcher.source.length - 1)
|
|
959
|
+
: 0;
|
|
960
|
+
const streamingCarry = {
|
|
961
|
+
stdout: "",
|
|
962
|
+
stderr: "",
|
|
963
|
+
};
|
|
964
|
+
const bytePrefix = (text, bytes) => Buffer.from(text).subarray(0, bytes).toString();
|
|
965
|
+
const byteSuffix = (text, bytes) => {
|
|
966
|
+
const buffer = Buffer.from(text);
|
|
967
|
+
return buffer.subarray(Math.max(0, buffer.byteLength - bytes)).toString();
|
|
968
|
+
};
|
|
969
|
+
const appendTail = (part, maxBytes) => {
|
|
970
|
+
if (maxBytes <= 0)
|
|
971
|
+
return;
|
|
972
|
+
retainedTail.push({ stream: part.stream, text: part.text });
|
|
973
|
+
retainedTailBytes += Buffer.byteLength(part.text);
|
|
974
|
+
while (retainedTailBytes > maxBytes && retainedTail.length > 0) {
|
|
975
|
+
const first = retainedTail[0];
|
|
976
|
+
const excess = retainedTailBytes - maxBytes;
|
|
977
|
+
const firstBytes = Buffer.byteLength(first.text);
|
|
978
|
+
if (firstBytes <= excess) {
|
|
979
|
+
retainedTail.shift();
|
|
980
|
+
retainedTailBytes -= firstBytes;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
first.text = byteSuffix(first.text, firstBytes - excess);
|
|
984
|
+
retainedTailBytes -= excess;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
const renderOutput = (stream) => {
|
|
989
|
+
const head = retainedHead
|
|
990
|
+
.filter((part) => part.stream === stream)
|
|
991
|
+
.map((part) => part.text)
|
|
992
|
+
.join("");
|
|
993
|
+
const tail = retainedTail
|
|
994
|
+
.filter((part) => part.stream === stream)
|
|
995
|
+
.map((part) => part.text)
|
|
996
|
+
.join("");
|
|
997
|
+
const marker = stream === truncationStream ? outputTruncationMarker : "";
|
|
998
|
+
return head + marker + tail;
|
|
999
|
+
};
|
|
1000
|
+
const refreshRetainedOutputs = () => {
|
|
1001
|
+
stdout = renderOutput("stdout");
|
|
1002
|
+
stderr = renderOutput("stderr");
|
|
1003
|
+
};
|
|
1004
|
+
const appendOutput = (stream, current, chunk) => {
|
|
941
1005
|
const text = typeof chunk === "string" ? chunk : chunk.toString();
|
|
942
|
-
if (maxOutputBytes === undefined)
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
return current;
|
|
1006
|
+
if (maxOutputBytes === undefined || outputTruncated) {
|
|
1007
|
+
if (maxOutputBytes === undefined)
|
|
1008
|
+
return current + text;
|
|
1009
|
+
appendTail({ stream, text }, retainedTailLimit);
|
|
1010
|
+
truncationStream = stream;
|
|
1011
|
+
return renderOutput(stream);
|
|
949
1012
|
}
|
|
1013
|
+
const used = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
|
|
950
1014
|
const bytes = Buffer.byteLength(text);
|
|
951
|
-
if (bytes <=
|
|
1015
|
+
if (used + bytes <= maxOutputBytes)
|
|
952
1016
|
return current + text;
|
|
953
1017
|
outputTruncated = true;
|
|
954
|
-
|
|
1018
|
+
truncationStream = stream;
|
|
1019
|
+
const available = Math.max(0, maxOutputBytes - Buffer.byteLength(outputTruncationMarker));
|
|
1020
|
+
const headBytes = Math.floor(available / 2);
|
|
1021
|
+
const tailBytes = available - headBytes;
|
|
1022
|
+
retainedTailLimit = tailBytes;
|
|
1023
|
+
const parts = [
|
|
1024
|
+
{ stream: "stdout", text: stdout },
|
|
1025
|
+
{ stream: "stderr", text: stderr },
|
|
1026
|
+
{ stream, text },
|
|
1027
|
+
];
|
|
1028
|
+
let headRemaining = headBytes;
|
|
1029
|
+
for (const part of parts) {
|
|
1030
|
+
if (headRemaining <= 0)
|
|
1031
|
+
break;
|
|
1032
|
+
const kept = bytePrefix(part.text, headRemaining);
|
|
1033
|
+
if (kept)
|
|
1034
|
+
retainedHead.push({ stream: part.stream, text: kept });
|
|
1035
|
+
headRemaining -= Buffer.byteLength(kept);
|
|
1036
|
+
}
|
|
1037
|
+
let tailRemaining = tailBytes;
|
|
1038
|
+
for (let index = parts.length - 1; index >= 0 && tailRemaining > 0; index--) {
|
|
1039
|
+
const part = parts[index];
|
|
1040
|
+
const kept = byteSuffix(part.text, tailRemaining);
|
|
1041
|
+
if (kept) {
|
|
1042
|
+
retainedTail.unshift({ stream: part.stream, text: kept });
|
|
1043
|
+
tailRemaining -= Buffer.byteLength(kept);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
retainedTailBytes = tailBytes - tailRemaining;
|
|
1047
|
+
return renderOutput(stream);
|
|
1048
|
+
};
|
|
1049
|
+
const matchStreamingChunk = (stream, chunk) => {
|
|
1050
|
+
if (streamingMatch || !streamingMatcher)
|
|
1051
|
+
return;
|
|
1052
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString();
|
|
1053
|
+
streamingMatcher.lastIndex = 0;
|
|
1054
|
+
streamingMatch = streamingMatcher.test(streamingCarry[stream] + text);
|
|
1055
|
+
if (streamingCarryBytes > 0) {
|
|
1056
|
+
streamingCarry[stream] = Buffer.from(streamingCarry[stream] + text)
|
|
1057
|
+
.subarray(-streamingCarryBytes)
|
|
1058
|
+
.toString();
|
|
1059
|
+
}
|
|
955
1060
|
};
|
|
956
1061
|
// Spawn the process (non-blocking). Keeping Node's `shell` option false
|
|
957
1062
|
// is important on every path here: shell:true concatenates tainted
|
|
@@ -1091,6 +1196,12 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1091
1196
|
}));
|
|
1092
1197
|
return;
|
|
1093
1198
|
}
|
|
1199
|
+
// Keep historical open stdin by default. An opted-in payload is written
|
|
1200
|
+
// completely and followed by EOF; input: "" closes stdin immediately.
|
|
1201
|
+
if (options?.input !== undefined) {
|
|
1202
|
+
child.stdin?.on("error", () => { });
|
|
1203
|
+
child.stdin?.end(options.input);
|
|
1204
|
+
}
|
|
1094
1205
|
if (child.pid && (posixProcessGroup || options?.lifetimeCoupled)) {
|
|
1095
1206
|
// #2026: POSIX registers unconditionally - detached children no
|
|
1096
1207
|
// longer receive terminal signals directly, so the lifetime
|
|
@@ -1201,11 +1312,17 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1201
1312
|
};
|
|
1202
1313
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1203
1314
|
// Output-cap kills are awaited by finalize() below, just like
|
|
1204
|
-
// timeout/abort kills.
|
|
1205
|
-
//
|
|
1315
|
+
// timeout/abort kills. An armed, unmatched streaming matcher is the one
|
|
1316
|
+
// exception: retention remains bounded, but the caller's timeout is the
|
|
1317
|
+
// bound while the child gets a chance to emit its rescue line.
|
|
1206
1318
|
let killPromise;
|
|
1207
1319
|
const stopForOutputLimit = () => {
|
|
1208
|
-
|
|
1320
|
+
const waitingForStreamingMatch = streamingMatcher !== undefined && !streamingMatch;
|
|
1321
|
+
if (outputTruncated &&
|
|
1322
|
+
!waitingForStreamingMatch &&
|
|
1323
|
+
!killed &&
|
|
1324
|
+
!child.killed) {
|
|
1325
|
+
killedForOutputCap = true;
|
|
1209
1326
|
killed = true;
|
|
1210
1327
|
killPromise = killTree();
|
|
1211
1328
|
}
|
|
@@ -1214,12 +1331,18 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1214
1331
|
child.stdout?.setEncoding("utf-8");
|
|
1215
1332
|
child.stderr?.setEncoding("utf-8");
|
|
1216
1333
|
child.stdout?.on("data", (data) => {
|
|
1217
|
-
|
|
1334
|
+
matchStreamingChunk("stdout", data);
|
|
1335
|
+
stdout = appendOutput("stdout", stdout, data);
|
|
1336
|
+
if (outputTruncated)
|
|
1337
|
+
refreshRetainedOutputs();
|
|
1218
1338
|
stopForOutputLimit();
|
|
1219
1339
|
rearmIdleGrace?.();
|
|
1220
1340
|
});
|
|
1221
1341
|
child.stderr?.on("data", (data) => {
|
|
1222
|
-
|
|
1342
|
+
matchStreamingChunk("stderr", data);
|
|
1343
|
+
stderr = appendOutput("stderr", stderr, data);
|
|
1344
|
+
if (outputTruncated)
|
|
1345
|
+
refreshRetainedOutputs();
|
|
1223
1346
|
stopForOutputLimit();
|
|
1224
1347
|
rearmIdleGrace?.();
|
|
1225
1348
|
});
|
|
@@ -1415,7 +1538,11 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1415
1538
|
resolved = true;
|
|
1416
1539
|
await waitForPipeIdle();
|
|
1417
1540
|
const resourceUsage = finishResourceUsage();
|
|
1418
|
-
const outputInfo =
|
|
1541
|
+
const outputInfo = {
|
|
1542
|
+
...(outputTruncated ? { outputTruncated: true } : {}),
|
|
1543
|
+
...(killedForOutputCap ? { killedForOutputCap: true } : {}),
|
|
1544
|
+
};
|
|
1545
|
+
const streamingMatchInfo = streamingMatch ? { streamingMatch: true } : {};
|
|
1419
1546
|
// #1816: surface the signal name as a field on every path where one
|
|
1420
1547
|
// exists, so callers can NAME it instead of scraping `error.message`.
|
|
1421
1548
|
const signalInfo = signal ? { signal } : {};
|
|
@@ -1440,6 +1567,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1440
1567
|
...(timeoutTeardown && { timeoutTeardown }),
|
|
1441
1568
|
spawnFailure: new SpawnFailureError("timeout", cause.message, cause),
|
|
1442
1569
|
...outputInfo,
|
|
1570
|
+
...streamingMatchInfo,
|
|
1443
1571
|
resourceUsage,
|
|
1444
1572
|
});
|
|
1445
1573
|
}
|
|
@@ -1454,6 +1582,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1454
1582
|
...signalInfo,
|
|
1455
1583
|
spawnFailure: new SpawnFailureError("killed", cause.message, cause),
|
|
1456
1584
|
...outputInfo,
|
|
1585
|
+
...streamingMatchInfo,
|
|
1457
1586
|
resourceUsage,
|
|
1458
1587
|
});
|
|
1459
1588
|
}
|
|
@@ -1468,6 +1597,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1468
1597
|
...signalInfo,
|
|
1469
1598
|
spawnFailure: new SpawnFailureError("killed", cause.message, cause),
|
|
1470
1599
|
...outputInfo,
|
|
1600
|
+
...streamingMatchInfo,
|
|
1471
1601
|
resourceUsage,
|
|
1472
1602
|
});
|
|
1473
1603
|
}
|
|
@@ -1497,11 +1627,19 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1497
1627
|
failure: "spawn",
|
|
1498
1628
|
spawnFailure,
|
|
1499
1629
|
...outputInfo,
|
|
1630
|
+
...streamingMatchInfo,
|
|
1500
1631
|
resourceUsage,
|
|
1501
1632
|
});
|
|
1502
1633
|
}
|
|
1503
1634
|
else {
|
|
1504
|
-
resolve({
|
|
1635
|
+
resolve({
|
|
1636
|
+
stdout,
|
|
1637
|
+
stderr,
|
|
1638
|
+
status: code,
|
|
1639
|
+
...outputInfo,
|
|
1640
|
+
...streamingMatchInfo,
|
|
1641
|
+
resourceUsage,
|
|
1642
|
+
});
|
|
1505
1643
|
}
|
|
1506
1644
|
};
|
|
1507
1645
|
child.on("exit", onChildSettle);
|
|
@@ -1545,6 +1683,8 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1545
1683
|
failure,
|
|
1546
1684
|
spawnFailure,
|
|
1547
1685
|
...(outputTruncated ? { outputTruncated: true } : {}),
|
|
1686
|
+
...(killedForOutputCap ? { killedForOutputCap: true } : {}),
|
|
1687
|
+
...(streamingMatch ? { streamingMatch: true } : {}),
|
|
1548
1688
|
resourceUsage,
|
|
1549
1689
|
});
|
|
1550
1690
|
if (controlFailure)
|
|
@@ -16,6 +16,9 @@ import * as path from "node:path";
|
|
|
16
16
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
17
17
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
18
18
|
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
19
|
+
import { createAvailabilityProbeFlight } from "./availability-probe-flight.js";
|
|
20
|
+
import { createSingleFlight } from "./single-flight.js";
|
|
21
|
+
const securityProbeFlights = createAvailabilityProbeFlight();
|
|
19
22
|
export class SecurityScanClient {
|
|
20
23
|
toolName;
|
|
21
24
|
/**
|
|
@@ -79,7 +82,8 @@ export class SecurityScanClient {
|
|
|
79
82
|
}
|
|
80
83
|
/** Outcome of the most recent `probeVersion` call. */
|
|
81
84
|
lastProbeOutcome = null;
|
|
82
|
-
|
|
85
|
+
/** Availability resolution is instance-owned; only its external probe is shared. */
|
|
86
|
+
ensureFlight = createSingleFlight();
|
|
83
87
|
inFlight = new Map();
|
|
84
88
|
binaryPath = null;
|
|
85
89
|
log;
|
|
@@ -99,15 +103,7 @@ export class SecurityScanClient {
|
|
|
99
103
|
async ensureAvailable() {
|
|
100
104
|
if (this.available !== null)
|
|
101
105
|
return this.available;
|
|
102
|
-
|
|
103
|
-
return this.ensureInFlight;
|
|
104
|
-
this.ensureInFlight = this.doEnsureAvailable();
|
|
105
|
-
try {
|
|
106
|
-
return await this.ensureInFlight;
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
this.ensureInFlight = null;
|
|
110
|
-
}
|
|
106
|
+
return this.ensureFlight.run("availability", () => this.doEnsureAvailable());
|
|
111
107
|
}
|
|
112
108
|
/**
|
|
113
109
|
* Spawn `toolName <versionArgs>` and report whether it answered cleanly.
|
|
@@ -128,10 +124,26 @@ export class SecurityScanClient {
|
|
|
128
124
|
const startedAt = Date.now();
|
|
129
125
|
let probe;
|
|
130
126
|
let hostStallMs;
|
|
127
|
+
let resolvedBinaryPath = null;
|
|
128
|
+
let probeJoined = false;
|
|
131
129
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const shared = securityProbeFlights.run(`security:${this.toolName}|${versionArgs.join("|")}`, async () => {
|
|
131
|
+
const { findManagedToolBinary } = await import("./installer/index.js");
|
|
132
|
+
const managed = await findManagedToolBinary(this.toolName);
|
|
133
|
+
return {
|
|
134
|
+
probe: await safeSpawnAsync(managed ?? this.toolName, versionArgs, {
|
|
135
|
+
timeout: 5000,
|
|
136
|
+
}),
|
|
137
|
+
binaryPath: managed ?? null,
|
|
138
|
+
};
|
|
134
139
|
});
|
|
140
|
+
probeJoined = shared.joined;
|
|
141
|
+
const flightResult = await shared.promise;
|
|
142
|
+
probe = flightResult.probe;
|
|
143
|
+
resolvedBinaryPath = flightResult.binaryPath;
|
|
144
|
+
if (!probe.error && probe.status === 0 && flightResult.binaryPath) {
|
|
145
|
+
this.binaryPath = flightResult.binaryPath;
|
|
146
|
+
}
|
|
135
147
|
}
|
|
136
148
|
finally {
|
|
137
149
|
hostStallMs = sampler.stop();
|
|
@@ -149,8 +161,14 @@ export class SecurityScanClient {
|
|
|
149
161
|
latched: true,
|
|
150
162
|
hostStallMs,
|
|
151
163
|
budgetMs: 5000,
|
|
152
|
-
classifiedBy: "probe",
|
|
153
|
-
evidence:
|
|
164
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
165
|
+
evidence: {
|
|
166
|
+
...describeProbeEvidence(probe),
|
|
167
|
+
...(resolvedBinaryPath && {
|
|
168
|
+
binary: path.basename(resolvedBinaryPath),
|
|
169
|
+
source: "managed-dir",
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
154
172
|
});
|
|
155
173
|
return true;
|
|
156
174
|
}
|
|
@@ -175,8 +193,14 @@ export class SecurityScanClient {
|
|
|
175
193
|
hostStallMs,
|
|
176
194
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
177
195
|
budgetMs: 5000,
|
|
178
|
-
classifiedBy: "probe",
|
|
179
|
-
evidence
|
|
196
|
+
classifiedBy: probeJoined ? "joined" : "probe",
|
|
197
|
+
evidence: {
|
|
198
|
+
...evidence,
|
|
199
|
+
...(resolvedBinaryPath && {
|
|
200
|
+
binary: path.basename(resolvedBinaryPath),
|
|
201
|
+
source: "managed-dir",
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
180
204
|
});
|
|
181
205
|
return false;
|
|
182
206
|
}
|
|
@@ -311,7 +335,14 @@ export class SecurityScanClient {
|
|
|
311
335
|
this.log(`Scan already in flight for ${key}; sharing result`);
|
|
312
336
|
return existing;
|
|
313
337
|
}
|
|
314
|
-
|
|
338
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
339
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
340
|
+
// evict a live successor a second writer registered under the same key
|
|
341
|
+
// mid-flight, after which the next caller starts a duplicate scan.
|
|
342
|
+
const promise = run().finally(() => {
|
|
343
|
+
if (this.inFlight.get(key) === promise)
|
|
344
|
+
this.inFlight.delete(key);
|
|
345
|
+
});
|
|
315
346
|
this.inFlight.set(key, promise);
|
|
316
347
|
return promise;
|
|
317
348
|
}
|