ultimate-pi 0.18.0 → 0.19.0
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/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +2 -3
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.agents/skills/harness-review/SKILL.md +7 -7
- package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
- package/.agents/skills/harness-steer/SKILL.md +1 -1
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +1 -3
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
- package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
- package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
- package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/00-harness-project-control.ts +133 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/budget-guard.ts +2 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +7 -5
- package/.pi/extensions/harness-ask-user.ts +8 -8
- package/.pi/extensions/harness-debate-tools.ts +27 -43
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-live-widget.ts +33 -2
- package/.pi/extensions/harness-plan-approval.ts +12 -12
- package/.pi/extensions/harness-run-context.ts +1214 -852
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +36 -164
- package/.pi/extensions/harness-subagents.ts +4 -4
- package/.pi/extensions/harness-telemetry.ts +3 -1
- package/.pi/extensions/harness-web-tools.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +27 -5
- package/.pi/extensions/review-integrity.ts +91 -10
- package/.pi/extensions/sentrux-rules-sync.ts +3 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/test-diff-integrity.ts +1 -0
- package/.pi/extensions/trace-recorder.ts +3 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +38 -49
- package/.pi/harness/agents.policy.yaml +275 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/README.md +6 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
- package/.pi/harness/docs/practice-map.md +2 -2
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +1 -3
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
- package/.pi/lib/harness-subagent-auth.ts +51 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/lib/harness-ui-state.ts +27 -12
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +4 -6
- package/.pi/prompts/harness-review.md +9 -9
- package/.pi/prompts/harness-run.md +7 -7
- package/.pi/prompts/harness-setup.md +42 -68
- package/.pi/prompts/harness-steer.md +2 -2
- package/.pi/scripts/README.md +3 -5
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/graphify-kb-updater.mjs +48 -8
- package/.pi/scripts/harness-agents-manifest.mjs +61 -4
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +9 -2
- package/.pi/scripts/harness-project-toggle.mjs +129 -0
- package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +26 -0
- package/README.md +85 -58
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/agents/harness/planning/scout-graphify.md +0 -39
- package/.pi/agents/harness/planning/scout-semantic.md +0 -41
- package/.pi/agents/harness/planning/scout-structure.md +0 -37
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Process Launch Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles spawning LSP servers via various methods:
|
|
5
|
+
* - Direct binary execution (using absolute paths on Windows)
|
|
6
|
+
* - Node.js scripts (npx/bun)
|
|
7
|
+
* - Package manager execution
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type ChildProcess,
|
|
12
|
+
execFileSync,
|
|
13
|
+
execSync,
|
|
14
|
+
spawn as nodeSpawn,
|
|
15
|
+
type SpawnOptions,
|
|
16
|
+
} from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
export interface LSPProcess {
|
|
22
|
+
process: ChildProcess;
|
|
23
|
+
stdin: NodeJS.WritableStream;
|
|
24
|
+
stdout: NodeJS.ReadableStream;
|
|
25
|
+
stderr: NodeJS.ReadableStream;
|
|
26
|
+
pid: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isWindows = process.platform === "win32";
|
|
30
|
+
const DEFAULT_STARTUP_FAILURE_WINDOW_MS = 50;
|
|
31
|
+
const WINDOWS_NAV_STARTUP_FAILURE_WINDOW_MS = 500;
|
|
32
|
+
const SESSIONSTART_LOG_DIR = path.join(
|
|
33
|
+
process.cwd(),
|
|
34
|
+
".pi",
|
|
35
|
+
"harness",
|
|
36
|
+
".lens",
|
|
37
|
+
);
|
|
38
|
+
const SESSIONSTART_LOG = path.join(SESSIONSTART_LOG_DIR, "sessionstart.log");
|
|
39
|
+
const PI_LENS_BIN_DIR = path.join(
|
|
40
|
+
process.cwd(),
|
|
41
|
+
".pi",
|
|
42
|
+
"harness",
|
|
43
|
+
".lens",
|
|
44
|
+
"bin",
|
|
45
|
+
);
|
|
46
|
+
const PI_LENS_TOOLS_BIN_DIR = path.join(
|
|
47
|
+
process.cwd(),
|
|
48
|
+
".pi",
|
|
49
|
+
"harness",
|
|
50
|
+
".lens",
|
|
51
|
+
"tools",
|
|
52
|
+
"node_modules",
|
|
53
|
+
".bin",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function logSessionStart(msg: string): void {
|
|
57
|
+
if (
|
|
58
|
+
process.env.PI_LENS_TEST_MODE === "1" ||
|
|
59
|
+
(process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
64
|
+
try {
|
|
65
|
+
fs.mkdirSync(SESSIONSTART_LOG_DIR, { recursive: true });
|
|
66
|
+
fs.appendFileSync(SESSIONSTART_LOG, line);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compactLogValue(value: string, max = 280): string {
|
|
71
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
72
|
+
if (!normalized) return "";
|
|
73
|
+
return normalized.length > max
|
|
74
|
+
? `${normalized.slice(0, max)}...`
|
|
75
|
+
: normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function delimiterForPlatform(platform: NodeJS.Platform): string {
|
|
79
|
+
return platform === "win32" ? ";" : ":";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function splitPathEntries(
|
|
83
|
+
value: string | undefined,
|
|
84
|
+
delimiter: string,
|
|
85
|
+
): string[] {
|
|
86
|
+
if (!value) return [];
|
|
87
|
+
return value
|
|
88
|
+
.split(delimiter)
|
|
89
|
+
.map((entry) => entry.trim())
|
|
90
|
+
.filter((entry) => entry.length > 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizePathEntry(entry: string, platform: NodeJS.Platform): string {
|
|
94
|
+
const normalized = path.normalize(entry);
|
|
95
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function combinePathValuesForPlatform(
|
|
99
|
+
values: Array<string | undefined>,
|
|
100
|
+
platform: NodeJS.Platform = process.platform,
|
|
101
|
+
): string {
|
|
102
|
+
const unique: string[] = [];
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
const delimiter = delimiterForPlatform(platform);
|
|
105
|
+
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
for (const entry of splitPathEntries(value, delimiter)) {
|
|
108
|
+
const key = normalizePathEntry(entry, platform);
|
|
109
|
+
if (seen.has(key)) continue;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
unique.push(entry);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return unique.join(delimiter);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolvePathValue(env: NodeJS.ProcessEnv): string {
|
|
119
|
+
return combinePathValuesForPlatform([env.PATH, env.Path, env.path]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read live system+user PATH from Windows registry (bypasses stale process.env.PATH). */
|
|
123
|
+
function readWindowsRegistryPath(): string {
|
|
124
|
+
try {
|
|
125
|
+
const { execFileSync } =
|
|
126
|
+
require("node:child_process") as typeof import("node:child_process");
|
|
127
|
+
const out = execFileSync(
|
|
128
|
+
"powershell.exe",
|
|
129
|
+
[
|
|
130
|
+
"-NoProfile",
|
|
131
|
+
"-NonInteractive",
|
|
132
|
+
"-Command",
|
|
133
|
+
"[System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User')",
|
|
134
|
+
],
|
|
135
|
+
{ timeout: 3000, encoding: "utf8" },
|
|
136
|
+
);
|
|
137
|
+
return out.trim();
|
|
138
|
+
} catch {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let _liveWindowsPath: string | null = null;
|
|
144
|
+
function getLiveWindowsPath(): string {
|
|
145
|
+
if (_liveWindowsPath === null) {
|
|
146
|
+
_liveWindowsPath = readWindowsRegistryPath();
|
|
147
|
+
}
|
|
148
|
+
return _liveWindowsPath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildAugmentedPath(basePath?: string): string {
|
|
152
|
+
const candidates: string[] = [];
|
|
153
|
+
const nodeDir = path.dirname(process.execPath);
|
|
154
|
+
if (nodeDir) {
|
|
155
|
+
candidates.push(nodeDir);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isWindows) {
|
|
159
|
+
const home = os.homedir();
|
|
160
|
+
const driveRoot = path.parse(home).root; // e.g. "C:\"
|
|
161
|
+
candidates.push(path.join(home, ".cargo", "bin"));
|
|
162
|
+
candidates.push(path.join(home, "go", "bin"));
|
|
163
|
+
candidates.push(path.join(home, ".dotnet", "tools"));
|
|
164
|
+
candidates.push(PI_LENS_BIN_DIR);
|
|
165
|
+
candidates.push(PI_LENS_TOOLS_BIN_DIR);
|
|
166
|
+
candidates.push(path.join(driveRoot, "Program Files", "Go", "bin"));
|
|
167
|
+
candidates.push(path.join(driveRoot, "Go", "bin"));
|
|
168
|
+
// Ruby installer drops versioned dirs (e.g. Ruby34-x64) on the drive root — scan dynamically
|
|
169
|
+
try {
|
|
170
|
+
for (const entry of fs.readdirSync(driveRoot)) {
|
|
171
|
+
if (/^ruby\d/i.test(entry)) {
|
|
172
|
+
candidates.push(path.join(driveRoot, entry, "bin"));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// drive root not readable — skip
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// On Windows, merge the live registry PATH so newly installed tools are visible
|
|
181
|
+
// even if the pi agent process started before they were installed.
|
|
182
|
+
const effectiveBase = isWindows
|
|
183
|
+
? combinePathValuesForPlatform([basePath, getLiveWindowsPath()])
|
|
184
|
+
: basePath;
|
|
185
|
+
|
|
186
|
+
const existing = new Set<string>();
|
|
187
|
+
for (const entry of splitPathEntries(effectiveBase, path.delimiter)) {
|
|
188
|
+
if (!entry) continue;
|
|
189
|
+
existing.add(normalizePathEntry(entry, process.platform));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const toAppend: string[] = [];
|
|
193
|
+
for (const candidate of candidates) {
|
|
194
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
195
|
+
const normalized = normalizePathEntry(candidate, process.platform);
|
|
196
|
+
if (existing.has(normalized)) continue;
|
|
197
|
+
toAppend.push(candidate);
|
|
198
|
+
existing.add(normalized);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (toAppend.length === 0) return basePath ?? "";
|
|
202
|
+
if (!basePath) return toAppend.join(path.delimiter);
|
|
203
|
+
return `${basePath}${path.delimiter}${toAppend.join(path.delimiter)}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Find binary in npm global directory
|
|
208
|
+
* Works around PATH caching issue after npm install -g
|
|
209
|
+
*/
|
|
210
|
+
function _findBinaryInNpmGlobal(command: string): string | undefined {
|
|
211
|
+
try {
|
|
212
|
+
// Get npm global prefix
|
|
213
|
+
const prefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
|
|
214
|
+
|
|
215
|
+
// On Windows, binaries are directly in the prefix dir
|
|
216
|
+
// On Unix, they're in prefix/bin
|
|
217
|
+
const binDir = isWindows ? prefix : path.join(prefix, "bin");
|
|
218
|
+
|
|
219
|
+
// Check for Windows variants
|
|
220
|
+
const candidates = isWindows
|
|
221
|
+
? [
|
|
222
|
+
path.join(binDir, `${command}.cmd`),
|
|
223
|
+
path.join(binDir, `${command}.exe`),
|
|
224
|
+
path.join(binDir, command),
|
|
225
|
+
]
|
|
226
|
+
: [path.join(binDir, command)];
|
|
227
|
+
|
|
228
|
+
for (const candidate of candidates) {
|
|
229
|
+
if (fs.existsSync(candidate)) {
|
|
230
|
+
return candidate;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
} catch {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validate that a .cmd shim's target JS/script exists before attempting to
|
|
241
|
+
* spawn it. npm-generated .cmd files reference the actual script via a path
|
|
242
|
+
* like `"%~dp0\..\yaml-language-server\bin\yaml-language-server"`. If that
|
|
243
|
+
* target is missing the shim will exit immediately with code 1 after a 500ms
|
|
244
|
+
* startup window — pre-checking avoids the delay.
|
|
245
|
+
* Returns true if the shim is valid (or we can't determine), false if the
|
|
246
|
+
* target is definitively missing.
|
|
247
|
+
*/
|
|
248
|
+
export function isCmdShimValid(cmdPath: string): boolean {
|
|
249
|
+
try {
|
|
250
|
+
const content = fs.readFileSync(cmdPath, "utf-8");
|
|
251
|
+
// npm cmd shim pattern: "%~dp0\..\<relpath>" or "%~dp0/<relpath>"
|
|
252
|
+
// biome-ignore format: regex char-class \- must stay escaped — formatter strips it
|
|
253
|
+
const match = content.match(
|
|
254
|
+
/"%~dp0[/\\]\.\.[/\\]((?:[\w./@-]|\\)+\.(?:mjs|cjs|js))"/i,
|
|
255
|
+
);
|
|
256
|
+
if (!match) return true; // non-npm shim — let it through
|
|
257
|
+
const relPath = match[1].replace(/[/\\]/g, path.sep);
|
|
258
|
+
const target = path.resolve(path.dirname(cmdPath), "..", relPath);
|
|
259
|
+
return fs.existsSync(target);
|
|
260
|
+
} catch {
|
|
261
|
+
return true; // can't read — be permissive
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* On Windows, npm creates .ps1 wrappers that hang indefinitely when PowerShell
|
|
267
|
+
* execution policy is Restricted or AllSigned. Bypass by preferring the .cmd
|
|
268
|
+
* sibling (runs under cmd.exe, no execution policy) or falling back to direct
|
|
269
|
+
* Node.js execution of the JS entry point extracted from the PS1 script.
|
|
270
|
+
* Returns undefined if no safe bypass is found.
|
|
271
|
+
*/
|
|
272
|
+
function bypassPs1OnWindows(
|
|
273
|
+
ps1Path: string,
|
|
274
|
+
args: string[],
|
|
275
|
+
): { command: string; args: string[]; needsShell: boolean } | undefined {
|
|
276
|
+
// 1. Prefer the .cmd sibling — cmd.exe handles it without execution policy issues
|
|
277
|
+
const cmdPath = `${ps1Path.slice(0, -4)}.cmd`;
|
|
278
|
+
if (fs.existsSync(cmdPath)) {
|
|
279
|
+
return { command: cmdPath, args, needsShell: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2. Parse the .ps1 to find the JS entry point and invoke via node directly.
|
|
283
|
+
// npm-generated PS1 pattern: "$basedir/../<package>/bin/cli.js"
|
|
284
|
+
try {
|
|
285
|
+
const content = fs.readFileSync(ps1Path, "utf-8");
|
|
286
|
+
// biome-ignore format: regex char-class \- must stay escaped — formatter strips it
|
|
287
|
+
const match = content.match(
|
|
288
|
+
/"\$basedir[/\\]\.\.[/\\]((?:[\w./@-]|\\)+\.(?:mjs|cjs|js))"/i,
|
|
289
|
+
);
|
|
290
|
+
if (match) {
|
|
291
|
+
const relPath = match[1].replace(/[/\\]/g, path.sep);
|
|
292
|
+
const jsPath = path.resolve(path.dirname(ps1Path), "..", relPath);
|
|
293
|
+
if (fs.existsSync(jsPath)) {
|
|
294
|
+
return {
|
|
295
|
+
command: process.execPath,
|
|
296
|
+
args: [jsPath, ...args],
|
|
297
|
+
needsShell: false,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// Can't read PS1 — no bypass available
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function findBinaryOnPath(
|
|
309
|
+
command: string,
|
|
310
|
+
env: NodeJS.ProcessEnv,
|
|
311
|
+
): string | undefined {
|
|
312
|
+
try {
|
|
313
|
+
const result = execFileSync(isWindows ? "where" : "which", [command], {
|
|
314
|
+
encoding: "utf-8",
|
|
315
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
316
|
+
env,
|
|
317
|
+
})
|
|
318
|
+
.split(/\r?\n/)
|
|
319
|
+
.map((line) => line.trim())
|
|
320
|
+
.filter(Boolean);
|
|
321
|
+
for (const candidate of result) {
|
|
322
|
+
if (fs.existsSync(candidate)) {
|
|
323
|
+
return candidate;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// ignore lookup failures
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Try to spawn a process, throwing immediately if it fails
|
|
334
|
+
*/
|
|
335
|
+
function trySpawn(
|
|
336
|
+
command: string,
|
|
337
|
+
args: string[],
|
|
338
|
+
cwd: string,
|
|
339
|
+
env: NodeJS.ProcessEnv,
|
|
340
|
+
needsShell: boolean,
|
|
341
|
+
): ChildProcess {
|
|
342
|
+
let proc: ChildProcess;
|
|
343
|
+
|
|
344
|
+
if (needsShell) {
|
|
345
|
+
// Build a cmd.exe-safe command string: wrap in double quotes, escape internal
|
|
346
|
+
// quotes by doubling them, and escape cmd metacharacters (& | < > ^ ( ) !) with ^
|
|
347
|
+
const escapeCmdArg = (s: string): string => {
|
|
348
|
+
// Escape cmd.exe metacharacters first, then wrap in quotes if needed
|
|
349
|
+
const escaped = s.replace(/([&|<>^()!])/g, "^$1");
|
|
350
|
+
return /[\s"]/.test(escaped)
|
|
351
|
+
? `"${escaped.replace(/"/g, '""')}"`
|
|
352
|
+
: escaped;
|
|
353
|
+
};
|
|
354
|
+
// shell:true justified: Windows .cmd/.bat LSP binaries (e.g. typescript-language-server.cmd)
|
|
355
|
+
// cannot be spawned via execFile — cmd.exe must interpret the script wrapper.
|
|
356
|
+
const shellCommand = `"${command}" ${args.map(escapeCmdArg).join(" ")}`;
|
|
357
|
+
proc = nodeSpawn(shellCommand, [], {
|
|
358
|
+
cwd,
|
|
359
|
+
env,
|
|
360
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
361
|
+
detached: false,
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
shell: true,
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
// Use normal spawn without shell
|
|
367
|
+
proc = nodeSpawn(command, args, {
|
|
368
|
+
cwd,
|
|
369
|
+
env,
|
|
370
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
371
|
+
detached: false,
|
|
372
|
+
windowsHide: isWindows,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
377
|
+
throw new Error(`Failed to spawn LSP server: ${command}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if process exited immediately (spawn failure - synchronous check)
|
|
381
|
+
if (proc.exitCode !== null || proc.killed) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
|
|
384
|
+
`The binary may be missing or corrupted.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return proc;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Attach error handler to a spawned process to prevent ENOENT crashes
|
|
393
|
+
* This catches "command not found" errors and other spawn failures
|
|
394
|
+
* Returns a promise that rejects if an immediate error occurs
|
|
395
|
+
*/
|
|
396
|
+
function _attachErrorHandler(
|
|
397
|
+
proc: ChildProcess,
|
|
398
|
+
context: string,
|
|
399
|
+
logContext?: {
|
|
400
|
+
command: string;
|
|
401
|
+
args: string[];
|
|
402
|
+
cwd: string;
|
|
403
|
+
pid?: number;
|
|
404
|
+
},
|
|
405
|
+
rejectOnImmediateError?: (err: Error) => void,
|
|
406
|
+
): void {
|
|
407
|
+
let stderrPreview = "";
|
|
408
|
+
let closeLogged = false;
|
|
409
|
+
const onStderr = (chunk: Buffer | string): void => {
|
|
410
|
+
if (stderrPreview.length >= 4000) return;
|
|
411
|
+
stderrPreview += chunk.toString();
|
|
412
|
+
};
|
|
413
|
+
proc.stderr?.on("data", onStderr);
|
|
414
|
+
|
|
415
|
+
proc.on("error", (err) => {
|
|
416
|
+
if (logContext) {
|
|
417
|
+
logSessionStart(
|
|
418
|
+
"lsp process " +
|
|
419
|
+
context +
|
|
420
|
+
": spawn-error command=" +
|
|
421
|
+
logContext.command +
|
|
422
|
+
" args=" +
|
|
423
|
+
JSON.stringify(logContext.args) +
|
|
424
|
+
" cwd=" +
|
|
425
|
+
logContext.cwd +
|
|
426
|
+
" pid=" +
|
|
427
|
+
(logContext.pid ?? 0) +
|
|
428
|
+
" error=" +
|
|
429
|
+
err.message +
|
|
430
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// If we have a reject function and this is an immediate spawn error, reject
|
|
435
|
+
if (
|
|
436
|
+
rejectOnImmediateError &&
|
|
437
|
+
(err as NodeJS.ErrnoException).code === "ENOENT"
|
|
438
|
+
) {
|
|
439
|
+
rejectOnImmediateError(err);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
proc.on("close", (code, signal) => {
|
|
444
|
+
if (closeLogged) return;
|
|
445
|
+
closeLogged = true;
|
|
446
|
+
proc.stderr?.off("data", onStderr);
|
|
447
|
+
if (code !== 0 && code !== null) {
|
|
448
|
+
if (logContext) {
|
|
449
|
+
logSessionStart(
|
|
450
|
+
"lsp process " +
|
|
451
|
+
context +
|
|
452
|
+
": closed code=" +
|
|
453
|
+
code +
|
|
454
|
+
(signal ? " signal=" + signal : "") +
|
|
455
|
+
" command=" +
|
|
456
|
+
logContext.command +
|
|
457
|
+
" args=" +
|
|
458
|
+
JSON.stringify(logContext.args) +
|
|
459
|
+
" cwd=" +
|
|
460
|
+
logContext.cwd +
|
|
461
|
+
" pid=" +
|
|
462
|
+
(logContext.pid ?? 0) +
|
|
463
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
} else if (signal && logContext) {
|
|
467
|
+
logSessionStart(
|
|
468
|
+
"lsp process " +
|
|
469
|
+
context +
|
|
470
|
+
": closed signal=" +
|
|
471
|
+
signal +
|
|
472
|
+
" command=" +
|
|
473
|
+
logContext.command +
|
|
474
|
+
" args=" +
|
|
475
|
+
JSON.stringify(logContext.args) +
|
|
476
|
+
" cwd=" +
|
|
477
|
+
logContext.cwd +
|
|
478
|
+
" pid=" +
|
|
479
|
+
(logContext.pid ?? 0) +
|
|
480
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Spawn an LSP server process
|
|
488
|
+
*
|
|
489
|
+
* Key fixes for Windows:
|
|
490
|
+
* - Uses absolute paths (relative paths fail in shell mode)
|
|
491
|
+
* - Uses shell: true for .cmd files
|
|
492
|
+
* - Uses windowsHide to prevent console window popup
|
|
493
|
+
* - Detects immediate spawn failures (ENOENT) before returning
|
|
494
|
+
*
|
|
495
|
+
* @param command - Command to run (e.g., "typescript-language-server")
|
|
496
|
+
* @param args - Arguments (e.g., ["--stdio"])
|
|
497
|
+
* @param options - Spawn options including cwd, env
|
|
498
|
+
* @returns LSPProcess handle
|
|
499
|
+
*/
|
|
500
|
+
function isSimpleCommand(command: string): boolean {
|
|
501
|
+
return (
|
|
502
|
+
!path.isAbsolute(command) &&
|
|
503
|
+
!command.includes(path.sep) &&
|
|
504
|
+
!command.includes("/")
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function usesShellOnWindows(commandPath: string): boolean {
|
|
509
|
+
if (!isWindows) return false;
|
|
510
|
+
return (
|
|
511
|
+
commandPath.includes(" ") ||
|
|
512
|
+
/\.(cmd|bat)$/i.test(commandPath) ||
|
|
513
|
+
!/\.(exe|cmd|bat)$/i.test(commandPath)
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createLaunchContext(
|
|
518
|
+
command: string,
|
|
519
|
+
options: SpawnOptions & { startupFailureWindowMs?: number },
|
|
520
|
+
): {
|
|
521
|
+
cwd: string;
|
|
522
|
+
env: NodeJS.ProcessEnv;
|
|
523
|
+
resolvedCommand: string;
|
|
524
|
+
isSimple: boolean;
|
|
525
|
+
} {
|
|
526
|
+
const cwd = String(options.cwd ?? process.cwd());
|
|
527
|
+
const mergedEnv = { ...process.env, ...options.env };
|
|
528
|
+
const augmentedPath = buildAugmentedPath(resolvePathValue(mergedEnv));
|
|
529
|
+
const env: NodeJS.ProcessEnv = {
|
|
530
|
+
...mergedEnv,
|
|
531
|
+
PATH: augmentedPath,
|
|
532
|
+
...(isWindows ? { Path: augmentedPath } : {}),
|
|
533
|
+
};
|
|
534
|
+
const simple = isSimpleCommand(command);
|
|
535
|
+
const isRelativePath =
|
|
536
|
+
!path.isAbsolute(command) &&
|
|
537
|
+
(command.includes(path.sep) || command.includes("/"));
|
|
538
|
+
const explicitCommand = isRelativePath ? path.resolve(cwd, command) : command;
|
|
539
|
+
const resolvedCommand = simple
|
|
540
|
+
? (findBinaryOnPath(command, env) ?? explicitCommand)
|
|
541
|
+
: explicitCommand;
|
|
542
|
+
return { cwd, env, resolvedCommand, isSimple: simple };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function withNpmGlobalCandidate(command: string, spawnCommand: string): string {
|
|
546
|
+
if (!isSimpleCommand(command)) return spawnCommand;
|
|
547
|
+
return _findBinaryInNpmGlobal(command) ?? spawnCommand;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function prevalidateWindowsShim(spawnCommand: string): void {
|
|
551
|
+
if (!isWindows || !/\.(cmd|bat)$/i.test(spawnCommand)) return;
|
|
552
|
+
if (isCmdShimValid(spawnCommand)) return;
|
|
553
|
+
logSessionStart(
|
|
554
|
+
`lsp cmd-shim-invalid: ${spawnCommand} target missing — skipping candidate`,
|
|
555
|
+
);
|
|
556
|
+
throw new Error(
|
|
557
|
+
`LSP .cmd shim target not found: ${spawnCommand}. The npm package may not be installed.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function applyWindowsPs1Bypass(
|
|
562
|
+
spawnCommand: string,
|
|
563
|
+
args: string[],
|
|
564
|
+
needsShell: boolean,
|
|
565
|
+
): { spawnCommand: string; args: string[]; needsShell: boolean } {
|
|
566
|
+
if (!isWindows || !/\.ps1$/i.test(spawnCommand)) {
|
|
567
|
+
return { spawnCommand, args, needsShell };
|
|
568
|
+
}
|
|
569
|
+
const bypass = bypassPs1OnWindows(spawnCommand, args);
|
|
570
|
+
if (!bypass) {
|
|
571
|
+
logSessionStart(
|
|
572
|
+
`lsp ps1-bypass: no .cmd or JS entry found for ${spawnCommand}, spawn may hang`,
|
|
573
|
+
);
|
|
574
|
+
return { spawnCommand, args, needsShell };
|
|
575
|
+
}
|
|
576
|
+
logSessionStart(
|
|
577
|
+
`lsp ps1-bypass: ${spawnCommand} → ${bypass.command} shell=${bypass.needsShell}`,
|
|
578
|
+
);
|
|
579
|
+
return {
|
|
580
|
+
spawnCommand: bypass.command,
|
|
581
|
+
args: bypass.args,
|
|
582
|
+
needsShell: bypass.needsShell,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function spawnLspWithFallback(
|
|
587
|
+
command: string,
|
|
588
|
+
spawnCommand: string,
|
|
589
|
+
args: string[],
|
|
590
|
+
cwd: string,
|
|
591
|
+
env: NodeJS.ProcessEnv,
|
|
592
|
+
needsShell: boolean,
|
|
593
|
+
): { proc: ChildProcess; spawnCommand: string; needsShell: boolean } {
|
|
594
|
+
try {
|
|
595
|
+
const proc = trySpawn(spawnCommand, args, cwd, env, needsShell);
|
|
596
|
+
return { proc, spawnCommand, needsShell };
|
|
597
|
+
} catch (err) {
|
|
598
|
+
if (!isSimpleCommand(command)) throw err;
|
|
599
|
+
const npmGlobalPath = _findBinaryInNpmGlobal(command);
|
|
600
|
+
if (!npmGlobalPath || npmGlobalPath === spawnCommand) throw err;
|
|
601
|
+
const needsShellGlobal = usesShellOnWindows(npmGlobalPath);
|
|
602
|
+
const proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
603
|
+
return {
|
|
604
|
+
proc,
|
|
605
|
+
spawnCommand: npmGlobalPath,
|
|
606
|
+
needsShell: needsShellGlobal,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function ensureHealthyProcess(proc: ChildProcess, command: string): void {
|
|
612
|
+
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
613
|
+
throw new Error(`Failed to spawn LSP server: ${command}`);
|
|
614
|
+
}
|
|
615
|
+
if (proc.exitCode !== null || proc.killed) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
|
|
618
|
+
`The binary may be missing or corrupted.`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function formatStartupStderr(stderr: string): string {
|
|
624
|
+
const normalized = compactLogValue(stderr);
|
|
625
|
+
if (!normalized) return "";
|
|
626
|
+
return ` stderr=${normalized}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function startupFailureWindow(
|
|
630
|
+
options: SpawnOptions & { startupFailureWindowMs?: number },
|
|
631
|
+
needsShell: boolean,
|
|
632
|
+
): number {
|
|
633
|
+
if (options?.startupFailureWindowMs) return options.startupFailureWindowMs;
|
|
634
|
+
if (isWindows && needsShell) return WINDOWS_NAV_STARTUP_FAILURE_WINDOW_MS;
|
|
635
|
+
return DEFAULT_STARTUP_FAILURE_WINDOW_MS;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function waitForImmediateStartupFailures(
|
|
639
|
+
proc: ChildProcess,
|
|
640
|
+
command: string,
|
|
641
|
+
options: SpawnOptions & { startupFailureWindowMs?: number },
|
|
642
|
+
needsShell: boolean,
|
|
643
|
+
): Promise<void> {
|
|
644
|
+
let startupStderr = "";
|
|
645
|
+
const onStartupStderr = (chunk: Buffer | string): void => {
|
|
646
|
+
if (startupStderr.length >= 4000) return;
|
|
647
|
+
startupStderr += chunk.toString();
|
|
648
|
+
};
|
|
649
|
+
proc.stderr?.on("data", onStartupStderr);
|
|
650
|
+
try {
|
|
651
|
+
await new Promise<void>((resolve, reject) => {
|
|
652
|
+
let settled = false;
|
|
653
|
+
const fail = (error: Error) => {
|
|
654
|
+
if (settled) return;
|
|
655
|
+
settled = true;
|
|
656
|
+
reject(error);
|
|
657
|
+
};
|
|
658
|
+
const pass = () => {
|
|
659
|
+
if (settled) return;
|
|
660
|
+
settled = true;
|
|
661
|
+
resolve();
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
proc.on("error", (err: Error & { code?: string }) => {
|
|
665
|
+
if (err.code !== "ENOENT" && err.code !== "EINVAL") return;
|
|
666
|
+
fail(
|
|
667
|
+
new Error(
|
|
668
|
+
`LSP server binary not found: ${command}. Install it or check your PATH.${formatStartupStderr(startupStderr)}`,
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
proc.on("exit", (code: number | null) => {
|
|
674
|
+
if (code === null) return;
|
|
675
|
+
const isWindowsCmd = isWindows && command.endsWith(".cmd");
|
|
676
|
+
const errorMsg =
|
|
677
|
+
isWindowsCmd && code === 1
|
|
678
|
+
? "npm .cmd shim failed (underlying binary not installed). Run 'npm install' in this project or use a global installation."
|
|
679
|
+
: "The binary may be missing or corrupted.";
|
|
680
|
+
fail(
|
|
681
|
+
new Error(
|
|
682
|
+
`LSP server ${command} exited immediately with code ${code}. ${errorMsg}${formatStartupStderr(startupStderr)}`,
|
|
683
|
+
),
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
setTimeout(pass, startupFailureWindow(options, needsShell));
|
|
688
|
+
});
|
|
689
|
+
} finally {
|
|
690
|
+
proc.stderr?.off("data", onStartupStderr);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export async function launchLSP(
|
|
695
|
+
command: string,
|
|
696
|
+
args: string[] = [],
|
|
697
|
+
options: SpawnOptions & {
|
|
698
|
+
startupFailureWindowMs?: number;
|
|
699
|
+
} = {},
|
|
700
|
+
): Promise<LSPProcess> {
|
|
701
|
+
const context = createLaunchContext(command, options);
|
|
702
|
+
let spawnCommand = withNpmGlobalCandidate(command, context.resolvedCommand);
|
|
703
|
+
let needsShell = usesShellOnWindows(spawnCommand);
|
|
704
|
+
prevalidateWindowsShim(spawnCommand);
|
|
705
|
+
const bypassed = applyWindowsPs1Bypass(spawnCommand, args, needsShell);
|
|
706
|
+
spawnCommand = bypassed.spawnCommand;
|
|
707
|
+
args = bypassed.args;
|
|
708
|
+
needsShell = bypassed.needsShell;
|
|
709
|
+
|
|
710
|
+
const spawned = spawnLspWithFallback(
|
|
711
|
+
command,
|
|
712
|
+
spawnCommand,
|
|
713
|
+
args,
|
|
714
|
+
context.cwd,
|
|
715
|
+
context.env,
|
|
716
|
+
needsShell,
|
|
717
|
+
);
|
|
718
|
+
const proc = spawned.proc;
|
|
719
|
+
spawnCommand = spawned.spawnCommand;
|
|
720
|
+
needsShell = spawned.needsShell;
|
|
721
|
+
|
|
722
|
+
ensureHealthyProcess(proc, command);
|
|
723
|
+
logSessionStart(
|
|
724
|
+
`lsp launch: command=${command} resolved=${spawnCommand} args=${JSON.stringify(args)} cwd=${context.cwd} shell=${needsShell ? "true" : "false"} pid=${proc.pid ?? 0}`,
|
|
725
|
+
);
|
|
726
|
+
await waitForImmediateStartupFailures(proc, command, options, needsShell);
|
|
727
|
+
|
|
728
|
+
_attachErrorHandler(proc, command, {
|
|
729
|
+
command: spawnCommand,
|
|
730
|
+
args,
|
|
731
|
+
cwd: context.cwd,
|
|
732
|
+
pid: proc.pid ?? 0,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
process: proc,
|
|
737
|
+
stdin: proc.stdin as NodeJS.WritableStream,
|
|
738
|
+
stdout: proc.stdout as NodeJS.ReadableStream,
|
|
739
|
+
stderr: proc.stderr as NodeJS.ReadableStream,
|
|
740
|
+
pid: proc.pid ?? 0,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Spawn via package manager (npx/bun)
|
|
746
|
+
*/
|
|
747
|
+
export async function launchViaPackageManager(
|
|
748
|
+
packageName: string,
|
|
749
|
+
args: string[] = [],
|
|
750
|
+
options: SpawnOptions = {},
|
|
751
|
+
): Promise<LSPProcess> {
|
|
752
|
+
// Prefer bun if available, fall back to npx (use .cmd on Windows)
|
|
753
|
+
const isWin = process.platform === "win32";
|
|
754
|
+
|
|
755
|
+
if (process.env.BUN_INSTALL) {
|
|
756
|
+
return launchLSP(
|
|
757
|
+
isWin ? "bun.exe" : "bun",
|
|
758
|
+
["x", packageName, ...args],
|
|
759
|
+
options,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// shell:true justified: npx on Windows is npx.cmd — requires shell to execute.
|
|
764
|
+
if (isWin) {
|
|
765
|
+
const argsStr = args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ");
|
|
766
|
+
// --no prevents silent download of uncached packages
|
|
767
|
+
const shellCommand = `npx --no ${packageName}${argsStr ? ` ${argsStr}` : ""}`;
|
|
768
|
+
|
|
769
|
+
const cwd = String(options.cwd ?? process.cwd());
|
|
770
|
+
const mergedEnv = { ...process.env, ...options.env };
|
|
771
|
+
const augmentedPath = buildAugmentedPath(resolvePathValue(mergedEnv));
|
|
772
|
+
const env: NodeJS.ProcessEnv = {
|
|
773
|
+
...mergedEnv,
|
|
774
|
+
PATH: augmentedPath,
|
|
775
|
+
...(isWindows ? { Path: augmentedPath } : {}),
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const proc = nodeSpawn(shellCommand, [], {
|
|
779
|
+
cwd,
|
|
780
|
+
env,
|
|
781
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
782
|
+
detached: false,
|
|
783
|
+
windowsHide: true,
|
|
784
|
+
shell: true,
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
788
|
+
throw new Error(`Failed to spawn package manager for: ${packageName}`);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check for immediate spawn failure on Windows
|
|
792
|
+
await new Promise<void>((resolve, reject) => {
|
|
793
|
+
let settled = false;
|
|
794
|
+
|
|
795
|
+
proc.on("error", (err: Error & { code?: string }) => {
|
|
796
|
+
if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
|
|
797
|
+
settled = true;
|
|
798
|
+
reject(
|
|
799
|
+
new Error(
|
|
800
|
+
`Package manager not found for: ${packageName}. ` +
|
|
801
|
+
`Install Node.js or check your PATH.`,
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
proc.on("exit", (code: number | null) => {
|
|
808
|
+
if (!settled && code !== null) {
|
|
809
|
+
settled = true;
|
|
810
|
+
reject(
|
|
811
|
+
new Error(
|
|
812
|
+
`Package manager exited immediately for: ${packageName} (code: ${code})`,
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
setTimeout(() => {
|
|
819
|
+
if (!settled) {
|
|
820
|
+
settled = true;
|
|
821
|
+
resolve();
|
|
822
|
+
}
|
|
823
|
+
}, 50);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Attach permanent error handler
|
|
827
|
+
_attachErrorHandler(proc, packageName);
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
process: proc,
|
|
831
|
+
stdin: proc.stdin,
|
|
832
|
+
stdout: proc.stdout,
|
|
833
|
+
stderr: proc.stderr,
|
|
834
|
+
pid: proc.pid ?? 0,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// --no prevents silent download of uncached packages; user must have
|
|
839
|
+
// already installed the LSP server via the interactive-install flow.
|
|
840
|
+
return launchLSP("npx", ["--no", packageName, ...args], options);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Spawn via Node.js directly
|
|
845
|
+
*/
|
|
846
|
+
export async function launchViaNode(
|
|
847
|
+
scriptPath: string,
|
|
848
|
+
args: string[] = [],
|
|
849
|
+
options: SpawnOptions = {},
|
|
850
|
+
): Promise<LSPProcess> {
|
|
851
|
+
return launchLSP(process.execPath, [scriptPath, ...args], options);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Spawn via Python module
|
|
856
|
+
*/
|
|
857
|
+
export async function launchViaPython(
|
|
858
|
+
moduleName: string,
|
|
859
|
+
args: string[] = [],
|
|
860
|
+
options: SpawnOptions = {},
|
|
861
|
+
): Promise<LSPProcess> {
|
|
862
|
+
// On Windows, prefer 'py' launcher, fall back to 'python'
|
|
863
|
+
const pythonCmd = process.platform === "win32" ? "py" : "python3";
|
|
864
|
+
return launchLSP(pythonCmd, ["-m", moduleName, ...args], options);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Stop an LSP process gracefully
|
|
869
|
+
*/
|
|
870
|
+
export async function stopLSP(handle: LSPProcess): Promise<void> {
|
|
871
|
+
if (handle.process.exitCode !== null || handle.process.signalCode !== null) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return new Promise((resolve) => {
|
|
876
|
+
let settled = false;
|
|
877
|
+
let forceTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
878
|
+
let giveUpTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
879
|
+
|
|
880
|
+
const done = () => {
|
|
881
|
+
if (settled) return;
|
|
882
|
+
settled = true;
|
|
883
|
+
if (forceTimeout) clearTimeout(forceTimeout);
|
|
884
|
+
if (giveUpTimeout) clearTimeout(giveUpTimeout);
|
|
885
|
+
handle.process.off("exit", done);
|
|
886
|
+
handle.process.off("error", done);
|
|
887
|
+
resolve();
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
handle.process.once("exit", done);
|
|
891
|
+
handle.process.once("error", done);
|
|
892
|
+
|
|
893
|
+
const killWindowsTree = (): boolean => {
|
|
894
|
+
if (!isWindows || handle.pid <= 0) return false;
|
|
895
|
+
try {
|
|
896
|
+
// Absolute path avoids PATH-resolution substitution on Windows.
|
|
897
|
+
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
898
|
+
const killer = nodeSpawn(
|
|
899
|
+
taskkill,
|
|
900
|
+
["/F", "/T", "/PID", String(handle.pid)],
|
|
901
|
+
{
|
|
902
|
+
shell: false,
|
|
903
|
+
windowsHide: true,
|
|
904
|
+
},
|
|
905
|
+
);
|
|
906
|
+
killer.once("error", done);
|
|
907
|
+
return true;
|
|
908
|
+
} catch {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
try {
|
|
914
|
+
// On Windows, kill the tree first; killing the direct child can orphan
|
|
915
|
+
// grandchildren (e.g. tsserver.js behind a cmd/npm shim).
|
|
916
|
+
if (!killWindowsTree()) {
|
|
917
|
+
handle.process.kill("SIGTERM");
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
done();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
forceTimeout = setTimeout(() => {
|
|
925
|
+
if (settled) return;
|
|
926
|
+
try {
|
|
927
|
+
if (!killWindowsTree()) {
|
|
928
|
+
handle.process.kill("SIGKILL");
|
|
929
|
+
}
|
|
930
|
+
} catch {
|
|
931
|
+
done();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
// If the process had already exited before listeners were attached, no
|
|
935
|
+
// exit event will arrive. Resolve rather than hanging test cleanup forever.
|
|
936
|
+
giveUpTimeout = setTimeout(done, 500);
|
|
937
|
+
}, 5000);
|
|
938
|
+
});
|
|
939
|
+
}
|