@opengsd/gsd-pi 1.2.0-dev.9ad8ae33 → 1.2.0-dev.a6376d75
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/dist/cli-model-override.d.ts +15 -0
- package/dist/cli-model-override.js +21 -0
- package/dist/cli.js +1 -18
- package/dist/loader.js +6 -4
- package/dist/register-agent-bundles.d.ts +11 -2
- package/dist/register-agent-bundles.js +18 -4
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/ask-user-questions.js +3 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +447 -215
- package/dist/resources/extensions/claude-code-cli/turn-assembler.js +33 -1
- package/dist/resources/extensions/gsd/auto/closeout.js +215 -0
- package/dist/resources/extensions/gsd/auto/dispatch-history.js +21 -6
- package/dist/resources/extensions/gsd/auto/dispatch.js +365 -0
- package/dist/resources/extensions/gsd/auto/finalize.js +347 -0
- package/dist/resources/extensions/gsd/auto/loop.js +4 -1
- package/dist/resources/extensions/gsd/auto/milestone-lease-reclaim.js +56 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +85 -15
- package/dist/resources/extensions/gsd/auto/phase-helpers.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +17 -2372
- package/dist/resources/extensions/gsd/auto/pre-dispatch.js +534 -0
- package/dist/resources/extensions/gsd/auto/unit-phase.js +694 -0
- package/dist/resources/extensions/gsd/auto/workflow-unit-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto/worktree-safety-phase.js +125 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +15 -1
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +37 -7
- package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -2
- package/dist/resources/extensions/gsd/commands-workflow-templates.js +9 -2
- package/dist/resources/extensions/gsd/db/queries.js +30 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +256 -125
- package/dist/resources/extensions/gsd/guided-flow.js +88 -2
- package/dist/resources/extensions/gsd/health-widget.js +87 -28
- package/dist/resources/extensions/gsd/mcp-bridge.js +10 -0
- package/dist/resources/extensions/gsd/milestone-settlement.js +2 -2
- package/dist/resources/extensions/gsd/notifications.js +12 -7
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -1
- package/dist/resources/extensions/gsd/skill-activation.js +3 -6
- package/dist/resources/extensions/gsd/state.js +6 -2
- package/dist/resources/extensions/gsd/tool-surface-readiness.js +83 -31
- package/dist/resources/extensions/gsd/tools/complete-task.js +62 -0
- package/dist/resources/extensions/gsd/unit-context-composer.js +1 -1
- package/dist/resources/extensions/gsd/unit-registry.js +34 -4
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +2 -0
- package/dist/resources/extensions/gsd/workflow-mcp-readiness-cache.js +105 -0
- package/dist/resources/extensions/gsd/worktree-safety.js +28 -26
- package/dist/resources/extensions/mcp-client/manager.js +6 -1
- package/dist/runtime-checks.d.ts +10 -0
- package/dist/runtime-checks.js +27 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -2
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/sdk.js +6 -4
- package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +8 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +50 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +34 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +12 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +4 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +12 -3
- package/packages/mcp-server/dist/cli-runner.d.ts +40 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -0
- package/packages/mcp-server/dist/cli-runner.js +137 -0
- package/packages/mcp-server/dist/cli-runner.js.map +1 -0
- package/packages/mcp-server/dist/cli.js +2 -58
- package/packages/mcp-server/dist/cli.js.map +1 -1
- package/packages/mcp-server/dist/pid-registry.d.ts +46 -0
- package/packages/mcp-server/dist/pid-registry.d.ts.map +1 -0
- package/packages/mcp-server/dist/pid-registry.js +452 -0
- package/packages/mcp-server/dist/pid-registry.js.map +1 -0
- package/packages/mcp-server/dist/probe-mode.d.ts +4 -0
- package/packages/mcp-server/dist/probe-mode.d.ts.map +1 -0
- package/packages/mcp-server/dist/probe-mode.js +10 -0
- package/packages/mcp-server/dist/probe-mode.js.map +1 -0
- package/packages/mcp-server/dist/stdio-watchdog.d.ts +8 -0
- package/packages/mcp-server/dist/stdio-watchdog.d.ts.map +1 -0
- package/packages/mcp-server/dist/stdio-watchdog.js +40 -0
- package/packages/mcp-server/dist/stdio-watchdog.js.map +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +62 -43
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +5 -5
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +43 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +45 -17
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/dist/index.d.ts +1 -1
- package/packages/pi-tui/dist/index.d.ts.map +1 -1
- package/packages/pi-tui/dist/index.js +1 -1
- package/packages/pi-tui/dist/index.js.map +1 -1
- package/packages/pi-tui/dist/terminal-image.d.ts +33 -0
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +54 -2
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +63 -18
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/dist/utils.d.ts.map +1 -1
- package/packages/pi-tui/dist/utils.js +110 -36
- package/packages/pi-tui/dist/utils.js.map +1 -1
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +45 -17
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +7 -2
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +531 -226
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +672 -7
- package/src/resources/extensions/claude-code-cli/turn-assembler.ts +38 -1
- package/src/resources/extensions/gsd/auto/closeout.ts +309 -0
- package/src/resources/extensions/gsd/auto/dispatch-history.ts +22 -6
- package/src/resources/extensions/gsd/auto/dispatch.ts +449 -0
- package/src/resources/extensions/gsd/auto/finalize.ts +445 -0
- package/src/resources/extensions/gsd/auto/loop.ts +4 -1
- package/src/resources/extensions/gsd/auto/milestone-lease-reclaim.ts +74 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +95 -15
- package/src/resources/extensions/gsd/auto/phase-helpers.ts +199 -0
- package/src/resources/extensions/gsd/auto/phases.ts +58 -3061
- package/src/resources/extensions/gsd/auto/pre-dispatch.ts +704 -0
- package/src/resources/extensions/gsd/auto/unit-phase.ts +910 -0
- package/src/resources/extensions/gsd/auto/workflow-unit-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto/worktree-safety-phase.ts +149 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +20 -1
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +56 -6
- package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -2
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +11 -4
- package/src/resources/extensions/gsd/db/queries.ts +29 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +267 -142
- package/src/resources/extensions/gsd/guided-flow.ts +128 -2
- package/src/resources/extensions/gsd/health-widget.ts +91 -27
- package/src/resources/extensions/gsd/mcp-bridge.ts +39 -0
- package/src/resources/extensions/gsd/milestone-settlement.ts +2 -2
- package/src/resources/extensions/gsd/notifications.ts +13 -6
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -1
- package/src/resources/extensions/gsd/skill-activation.ts +3 -6
- package/src/resources/extensions/gsd/state.ts +7 -1
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-blocked-remediation-message.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +206 -22
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +76 -12
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +77 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +169 -1
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +141 -5
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +8 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +117 -91
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-environment-async.test.ts +104 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +47 -16
- package/src/resources/extensions/gsd/tests/mcp-readiness-preflight.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/milestone-settlement.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/notifications.test.ts +64 -9
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -2
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/remote-notification-from-desktop.test.ts +31 -81
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -17
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +184 -10
- package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp-readiness-cache.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/workflow-phase-contract-matrix.test.ts +332 -0
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-safety-phase.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +72 -0
- package/src/resources/extensions/gsd/tool-surface-readiness.ts +126 -19
- package/src/resources/extensions/gsd/tools/complete-task.ts +87 -0
- package/src/resources/extensions/gsd/unit-context-composer.ts +1 -1
- package/src/resources/extensions/gsd/unit-registry.ts +34 -4
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -0
- package/src/resources/extensions/gsd/workflow-mcp-readiness-cache.ts +150 -0
- package/src/resources/extensions/gsd/worktree-safety.ts +41 -39
- package/src/resources/extensions/mcp-client/manager.ts +7 -1
- /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_ssgManifest.js +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* integrate into the doctor pipeline via checkEnvironmentHealth().
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
|
-
import { execSync } from "node:child_process";
|
|
12
|
+
import { execFile, execSync } from "node:child_process";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { detectPythonExecutable } from "./python-resolver.js";
|
|
15
15
|
import { projectRootFromWorktreePath } from "./worktree-root.js";
|
|
@@ -56,44 +56,49 @@ function commandExists(name, cwd) {
|
|
|
56
56
|
/**
|
|
57
57
|
* Check that Node.js version meets the project's engines requirement.
|
|
58
58
|
*/
|
|
59
|
-
function
|
|
59
|
+
function readPackageEngineNode(basePath) {
|
|
60
60
|
const pkgPath = join(basePath, "package.json");
|
|
61
61
|
if (!existsSync(pkgPath))
|
|
62
62
|
return null;
|
|
63
63
|
try {
|
|
64
64
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
65
|
-
|
|
66
|
-
if (!required)
|
|
67
|
-
return null;
|
|
68
|
-
const currentVersion = tryExec("node --version", basePath);
|
|
69
|
-
if (!currentVersion) {
|
|
70
|
-
return { name: "node_version", status: "error", message: "Node.js not found in PATH" };
|
|
71
|
-
}
|
|
72
|
-
// Parse semver requirement (handles >=X.Y.Z format)
|
|
73
|
-
const reqMatch = required.match(/>=?\s*(\d+)(?:\.(\d+))?/);
|
|
74
|
-
if (!reqMatch)
|
|
75
|
-
return null;
|
|
76
|
-
const reqMajor = parseInt(reqMatch[1], 10);
|
|
77
|
-
const reqMinor = parseInt(reqMatch[2] ?? "0", 10);
|
|
78
|
-
const curMatch = currentVersion.match(/v?(\d+)\.(\d+)/);
|
|
79
|
-
if (!curMatch)
|
|
80
|
-
return null;
|
|
81
|
-
const curMajor = parseInt(curMatch[1], 10);
|
|
82
|
-
const curMinor = parseInt(curMatch[2], 10);
|
|
83
|
-
if (curMajor < reqMajor || (curMajor === reqMajor && curMinor < reqMinor)) {
|
|
84
|
-
return {
|
|
85
|
-
name: "node_version",
|
|
86
|
-
status: "warning",
|
|
87
|
-
message: `Node.js ${currentVersion} does not meet requirement "${required}"`,
|
|
88
|
-
detail: `Current: ${currentVersion}, Required: ${required}`,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return { name: "node_version", status: "ok", message: `Node.js ${currentVersion}` };
|
|
65
|
+
return pkg.engines?.node ?? null;
|
|
92
66
|
}
|
|
93
67
|
catch {
|
|
94
68
|
return null;
|
|
95
69
|
}
|
|
96
70
|
}
|
|
71
|
+
function buildNodeVersionResult(required, currentVersion) {
|
|
72
|
+
if (!currentVersion) {
|
|
73
|
+
return { name: "node_version", status: "error", message: "Node.js not found in PATH" };
|
|
74
|
+
}
|
|
75
|
+
// Parse semver requirement (handles >=X.Y.Z format)
|
|
76
|
+
const reqMatch = required.match(/>=?\s*(\d+)(?:\.(\d+))?/);
|
|
77
|
+
if (!reqMatch)
|
|
78
|
+
return null;
|
|
79
|
+
const reqMajor = parseInt(reqMatch[1], 10);
|
|
80
|
+
const reqMinor = parseInt(reqMatch[2] ?? "0", 10);
|
|
81
|
+
const curMatch = currentVersion.match(/v?(\d+)\.(\d+)/);
|
|
82
|
+
if (!curMatch)
|
|
83
|
+
return null;
|
|
84
|
+
const curMajor = parseInt(curMatch[1], 10);
|
|
85
|
+
const curMinor = parseInt(curMatch[2], 10);
|
|
86
|
+
if (curMajor < reqMajor || (curMajor === reqMajor && curMinor < reqMinor)) {
|
|
87
|
+
return {
|
|
88
|
+
name: "node_version",
|
|
89
|
+
status: "warning",
|
|
90
|
+
message: `Node.js ${currentVersion} does not meet requirement "${required}"`,
|
|
91
|
+
detail: `Current: ${currentVersion}, Required: ${required}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { name: "node_version", status: "ok", message: `Node.js ${currentVersion}` };
|
|
95
|
+
}
|
|
96
|
+
function checkNodeVersion(basePath) {
|
|
97
|
+
const required = readPackageEngineNode(basePath);
|
|
98
|
+
if (!required)
|
|
99
|
+
return null;
|
|
100
|
+
return buildNodeVersionResult(required, tryExec("node --version", basePath));
|
|
101
|
+
}
|
|
97
102
|
/**
|
|
98
103
|
* Check if node_modules exists and is not stale vs the lockfile.
|
|
99
104
|
*/
|
|
@@ -184,20 +189,15 @@ function checkEnvFiles(basePath) {
|
|
|
184
189
|
* Check for port conflicts on common dev server ports.
|
|
185
190
|
* Only checks ports that appear in package.json scripts.
|
|
186
191
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Try to detect ports from package.json scripts
|
|
192
|
+
// Detect the dev ports worth checking from package.json scripts, falling back to
|
|
193
|
+
// common defaults. Returns an empty set when there's no Node project (no
|
|
194
|
+
// package.json) or a parse failure — the caller then skips port checks entirely,
|
|
195
|
+
// avoiding false positives from system services (e.g. macOS AirPlay on 5000, #1381).
|
|
196
|
+
function collectPortsToCheck(basePath) {
|
|
193
197
|
const portsToCheck = new Set();
|
|
194
198
|
const pkgPath = join(basePath, "package.json");
|
|
195
|
-
if (!existsSync(pkgPath))
|
|
196
|
-
|
|
197
|
-
// entirely to avoid false positives from system services (e.g., macOS
|
|
198
|
-
// AirPlay Receiver on port 5000). (#1381)
|
|
199
|
-
return [];
|
|
200
|
-
}
|
|
199
|
+
if (!existsSync(pkgPath))
|
|
200
|
+
return portsToCheck;
|
|
201
201
|
try {
|
|
202
202
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
203
203
|
const scripts = pkg.scripts ?? {};
|
|
@@ -211,11 +211,8 @@ function checkPortConflicts(basePath) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
catch {
|
|
214
|
-
|
|
215
|
-
return [];
|
|
214
|
+
return new Set();
|
|
216
215
|
}
|
|
217
|
-
// If no ports found in scripts, check common defaults.
|
|
218
|
-
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
219
216
|
if (portsToCheck.size === 0) {
|
|
220
217
|
for (const p of DEFAULT_DEV_PORTS) {
|
|
221
218
|
if (p === 5000 && process.platform === "darwin")
|
|
@@ -223,30 +220,54 @@ function checkPortConflicts(basePath) {
|
|
|
223
220
|
portsToCheck.add(p);
|
|
224
221
|
}
|
|
225
222
|
}
|
|
223
|
+
return portsToCheck;
|
|
224
|
+
}
|
|
225
|
+
// Parse a single `lsof -nP -iTCP -sTCP:LISTEN` scan and report which of the
|
|
226
|
+
// requested ports are in use. Replaces the old per-port lsof loop (up to ~14
|
|
227
|
+
// system-wide socket scans, ~350ms) with one scan + an in-memory lookup.
|
|
228
|
+
function buildPortConflictResults(lsofOut, portsToCheck) {
|
|
229
|
+
const results = [];
|
|
230
|
+
const listening = new Map();
|
|
231
|
+
if (lsofOut) {
|
|
232
|
+
for (const line of lsofOut.split("\n")) {
|
|
233
|
+
// COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
|
234
|
+
// e.g. `node 12345 user 23u IPv4 0x.. 0t0 TCP *:3000 (LISTEN)`
|
|
235
|
+
const portMatch = line.match(/:(\d+)(?:\s+\(LISTEN\))?\s*$/);
|
|
236
|
+
if (!portMatch)
|
|
237
|
+
continue;
|
|
238
|
+
const port = parseInt(portMatch[1], 10);
|
|
239
|
+
if (!Number.isFinite(port) || listening.has(port))
|
|
240
|
+
continue;
|
|
241
|
+
const cols = line.split(/\s+/);
|
|
242
|
+
listening.set(port, { command: cols[0] ?? "unknown", pid: cols[1] ?? "?" });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
226
245
|
for (const port of portsToCheck) {
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
// Get process name
|
|
230
|
-
const nameResult = tryExec(`lsof -i :${port} -sTCP:LISTEN -Fp | head -2`, basePath);
|
|
231
|
-
const processName = nameResult?.match(/p(\d+)\n?c?(.+)?/)?.[2] ?? "unknown";
|
|
246
|
+
const hit = listening.get(port);
|
|
247
|
+
if (hit) {
|
|
232
248
|
results.push({
|
|
233
249
|
name: "port_conflict",
|
|
234
250
|
status: "warning",
|
|
235
|
-
message: `Port ${port} is already in use by ${
|
|
251
|
+
message: `Port ${port} is already in use by ${hit.command} (PID ${hit.pid})`,
|
|
236
252
|
detail: `Kill the process or use a different port`,
|
|
237
253
|
});
|
|
238
254
|
}
|
|
239
255
|
}
|
|
240
256
|
return results;
|
|
241
257
|
}
|
|
258
|
+
function checkPortConflicts(basePath) {
|
|
259
|
+
// Only run on macOS/Linux — lsof is not available on Windows
|
|
260
|
+
if (process.platform === "win32")
|
|
261
|
+
return [];
|
|
262
|
+
const portsToCheck = collectPortsToCheck(basePath);
|
|
263
|
+
if (portsToCheck.size === 0)
|
|
264
|
+
return [];
|
|
265
|
+
return buildPortConflictResults(tryExec("lsof -nP -iTCP -sTCP:LISTEN", basePath), portsToCheck);
|
|
266
|
+
}
|
|
242
267
|
/**
|
|
243
268
|
* Check available disk space on the working directory partition.
|
|
244
269
|
*/
|
|
245
|
-
function
|
|
246
|
-
// Only run on macOS/Linux
|
|
247
|
-
if (process.platform === "win32")
|
|
248
|
-
return null;
|
|
249
|
-
const dfOutput = tryExec(`df -k "${basePath}" | tail -1`, basePath);
|
|
270
|
+
function buildDiskSpaceResult(dfOutput) {
|
|
250
271
|
if (!dfOutput)
|
|
251
272
|
return null;
|
|
252
273
|
try {
|
|
@@ -267,11 +288,7 @@ function checkDiskSpace(basePath) {
|
|
|
267
288
|
};
|
|
268
289
|
}
|
|
269
290
|
if (availBytes < MIN_DISK_BYTES * 4) {
|
|
270
|
-
return {
|
|
271
|
-
name: "disk_space",
|
|
272
|
-
status: "warning",
|
|
273
|
-
message: `Disk space getting low: ${availGB}GB free`,
|
|
274
|
-
};
|
|
291
|
+
return { name: "disk_space", status: "warning", message: `Disk space getting low: ${availGB}GB free` };
|
|
275
292
|
}
|
|
276
293
|
return { name: "disk_space", status: "ok", message: `${availGB}GB free` };
|
|
277
294
|
}
|
|
@@ -279,16 +296,24 @@ function checkDiskSpace(basePath) {
|
|
|
279
296
|
return null;
|
|
280
297
|
}
|
|
281
298
|
}
|
|
299
|
+
function checkDiskSpace(basePath) {
|
|
300
|
+
// Only run on macOS/Linux
|
|
301
|
+
if (process.platform === "win32")
|
|
302
|
+
return null;
|
|
303
|
+
return buildDiskSpaceResult(tryExec(`df -k "${basePath}" | tail -1`, basePath));
|
|
304
|
+
}
|
|
282
305
|
/**
|
|
283
306
|
* Check if Docker is available when project has a Dockerfile.
|
|
284
307
|
*/
|
|
285
|
-
function
|
|
286
|
-
|
|
308
|
+
function hasDockerConfig(basePath) {
|
|
309
|
+
return existsSync(join(basePath, "Dockerfile")) ||
|
|
287
310
|
existsSync(join(basePath, "docker-compose.yml")) ||
|
|
288
311
|
existsSync(join(basePath, "docker-compose.yaml")) ||
|
|
289
312
|
existsSync(join(basePath, "compose.yml")) ||
|
|
290
313
|
existsSync(join(basePath, "compose.yaml"));
|
|
291
|
-
|
|
314
|
+
}
|
|
315
|
+
function checkDocker(basePath) {
|
|
316
|
+
if (!hasDockerConfig(basePath))
|
|
292
317
|
return null;
|
|
293
318
|
if (!commandExists("docker", basePath)) {
|
|
294
319
|
return {
|
|
@@ -308,74 +333,62 @@ function checkDocker(basePath) {
|
|
|
308
333
|
}
|
|
309
334
|
return { name: "docker", status: "ok", message: `Docker ${info}` };
|
|
310
335
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
function
|
|
315
|
-
const results = [];
|
|
336
|
+
// Decide which project tools to verify from package.json + marker files (pure fs).
|
|
337
|
+
// The actual `command -v` probes are left to the sync/async checkers so both share
|
|
338
|
+
// this logic.
|
|
339
|
+
function readProjectToolSpec(basePath) {
|
|
316
340
|
const pkgPath = join(basePath, "package.json");
|
|
317
341
|
if (!existsSync(pkgPath))
|
|
318
|
-
return
|
|
342
|
+
return null;
|
|
319
343
|
try {
|
|
320
344
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
321
|
-
const allDeps = {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (packageManager) {
|
|
328
|
-
const managerName = packageManager.split("@")[0];
|
|
329
|
-
if (managerName && managerName !== "npm" && !commandExists(managerName, basePath)) {
|
|
330
|
-
results.push({
|
|
331
|
-
name: "package_manager",
|
|
332
|
-
status: "warning",
|
|
333
|
-
message: `Project requires ${managerName} but it's not installed`,
|
|
334
|
-
detail: `Install with: npm install -g ${managerName}`,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Check for TypeScript if it's a dependency
|
|
339
|
-
if (allDeps["typescript"] && !existsSync(join(basePath, "node_modules", ".bin", "tsc"))) {
|
|
340
|
-
results.push({
|
|
341
|
-
name: "typescript",
|
|
342
|
-
status: "warning",
|
|
343
|
-
message: "TypeScript is a dependency but tsc is not available (run npm install)",
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
// Check for Python if pyproject.toml or requirements.txt exists
|
|
347
|
-
if (existsSync(join(basePath, "pyproject.toml")) || existsSync(join(basePath, "requirements.txt"))) {
|
|
348
|
-
if (detectPythonExecutable() === null) {
|
|
349
|
-
results.push({
|
|
350
|
-
name: "python",
|
|
351
|
-
status: "warning",
|
|
352
|
-
message: "Project has Python config but python is not installed",
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// Check for Rust if Cargo.toml exists
|
|
357
|
-
if (existsSync(join(basePath, "Cargo.toml"))) {
|
|
358
|
-
if (!commandExists("cargo", basePath)) {
|
|
359
|
-
results.push({
|
|
360
|
-
name: "cargo",
|
|
361
|
-
status: "warning",
|
|
362
|
-
message: "Project has Cargo.toml but cargo is not installed",
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// Check for Go if go.mod exists
|
|
367
|
-
if (existsSync(join(basePath, "go.mod"))) {
|
|
368
|
-
if (!commandExists("go", basePath)) {
|
|
369
|
-
results.push({
|
|
370
|
-
name: "go",
|
|
371
|
-
status: "warning",
|
|
372
|
-
message: "Project has go.mod but go is not installed",
|
|
373
|
-
});
|
|
374
|
-
}
|
|
345
|
+
const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
346
|
+
let packageManager = null;
|
|
347
|
+
if (pkg.packageManager) {
|
|
348
|
+
const managerName = String(pkg.packageManager).split("@")[0];
|
|
349
|
+
if (managerName && managerName !== "npm")
|
|
350
|
+
packageManager = managerName;
|
|
375
351
|
}
|
|
352
|
+
return {
|
|
353
|
+
packageManager,
|
|
354
|
+
needsTsc: Boolean(allDeps["typescript"]) && !existsSync(join(basePath, "node_modules", ".bin", "tsc")),
|
|
355
|
+
needsPython: existsSync(join(basePath, "pyproject.toml")) || existsSync(join(basePath, "requirements.txt")),
|
|
356
|
+
needsCargo: existsSync(join(basePath, "Cargo.toml")),
|
|
357
|
+
needsGo: existsSync(join(basePath, "go.mod")),
|
|
358
|
+
};
|
|
376
359
|
}
|
|
377
360
|
catch {
|
|
378
|
-
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function checkProjectTools(basePath) {
|
|
365
|
+
const spec = readProjectToolSpec(basePath);
|
|
366
|
+
if (!spec)
|
|
367
|
+
return [];
|
|
368
|
+
const results = [];
|
|
369
|
+
if (spec.packageManager && !commandExists(spec.packageManager, basePath)) {
|
|
370
|
+
results.push({
|
|
371
|
+
name: "package_manager",
|
|
372
|
+
status: "warning",
|
|
373
|
+
message: `Project requires ${spec.packageManager} but it's not installed`,
|
|
374
|
+
detail: `Install with: npm install -g ${spec.packageManager}`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (spec.needsTsc) {
|
|
378
|
+
results.push({
|
|
379
|
+
name: "typescript",
|
|
380
|
+
status: "warning",
|
|
381
|
+
message: "TypeScript is a dependency but tsc is not available (run npm install)",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (spec.needsPython && detectPythonExecutable() === null) {
|
|
385
|
+
results.push({ name: "python", status: "warning", message: "Project has Python config but python is not installed" });
|
|
386
|
+
}
|
|
387
|
+
if (spec.needsCargo && !commandExists("cargo", basePath)) {
|
|
388
|
+
results.push({ name: "cargo", status: "warning", message: "Project has Cargo.toml but cargo is not installed" });
|
|
389
|
+
}
|
|
390
|
+
if (spec.needsGo && !commandExists("go", basePath)) {
|
|
391
|
+
results.push({ name: "go", status: "warning", message: "Project has go.mod but go is not installed" });
|
|
379
392
|
}
|
|
380
393
|
return results;
|
|
381
394
|
}
|
|
@@ -461,6 +474,124 @@ function checkTestHealth(basePath) {
|
|
|
461
474
|
* Run all environment health checks. Returns structured results for
|
|
462
475
|
* integration with the doctor pipeline.
|
|
463
476
|
*/
|
|
477
|
+
// ── Async variants ─────────────────────────────────────────────────────────
|
|
478
|
+
// The always-on health widget refreshes off the first-paint path, but the sync
|
|
479
|
+
// `runEnvironmentChecks` blocks the event loop on its subprocess checks (~300ms,
|
|
480
|
+
// and again every 60s). These async siblings run the same checks via non-blocking
|
|
481
|
+
// `execFile` and fan the independent ones out concurrently, so the UI thread never
|
|
482
|
+
// stalls. The sync API above is unchanged for /gsd doctor and the dashboards.
|
|
483
|
+
function tryExecAsync(cmd, cwd) {
|
|
484
|
+
return new Promise((resolve) => {
|
|
485
|
+
const isWin = process.platform === "win32";
|
|
486
|
+
const child = execFile(isWin ? "cmd" : "sh", isWin ? ["/c", cmd] : ["-c", cmd], { cwd, timeout: CMD_TIMEOUT, encoding: "utf-8" }, (err, stdout) => resolve(err ? null : String(stdout).trim()));
|
|
487
|
+
child.on("error", () => resolve(null));
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async function commandExistsAsync(name, cwd) {
|
|
491
|
+
const whichCmd = process.platform === "win32" ? `where ${name}` : `command -v ${name}`;
|
|
492
|
+
return (await tryExecAsync(whichCmd, cwd)) !== null;
|
|
493
|
+
}
|
|
494
|
+
async function checkNodeVersionAsync(basePath) {
|
|
495
|
+
const required = readPackageEngineNode(basePath);
|
|
496
|
+
if (!required)
|
|
497
|
+
return null;
|
|
498
|
+
const currentVersion = await tryExecAsync("node --version", basePath);
|
|
499
|
+
return buildNodeVersionResult(required, currentVersion);
|
|
500
|
+
}
|
|
501
|
+
async function checkPortConflictsAsync(basePath) {
|
|
502
|
+
if (process.platform === "win32")
|
|
503
|
+
return [];
|
|
504
|
+
const portsToCheck = collectPortsToCheck(basePath);
|
|
505
|
+
if (portsToCheck.size === 0)
|
|
506
|
+
return [];
|
|
507
|
+
const lsofOut = await tryExecAsync("lsof -nP -iTCP -sTCP:LISTEN", basePath);
|
|
508
|
+
return buildPortConflictResults(lsofOut, portsToCheck);
|
|
509
|
+
}
|
|
510
|
+
async function checkDiskSpaceAsync(basePath) {
|
|
511
|
+
if (process.platform === "win32")
|
|
512
|
+
return null;
|
|
513
|
+
return buildDiskSpaceResult(await tryExecAsync(`df -k "${basePath}" | tail -1`, basePath));
|
|
514
|
+
}
|
|
515
|
+
async function checkDockerAsync(basePath) {
|
|
516
|
+
if (!hasDockerConfig(basePath))
|
|
517
|
+
return null;
|
|
518
|
+
if (!(await commandExistsAsync("docker", basePath))) {
|
|
519
|
+
return { name: "docker", status: "warning", message: "Project has Docker files but docker is not installed" };
|
|
520
|
+
}
|
|
521
|
+
const info = await tryExecAsync("docker info --format '{{.ServerVersion}}'", basePath);
|
|
522
|
+
if (!info) {
|
|
523
|
+
return {
|
|
524
|
+
name: "docker",
|
|
525
|
+
status: "warning",
|
|
526
|
+
message: "Docker is installed but daemon is not running",
|
|
527
|
+
detail: "Start Docker Desktop or the docker daemon",
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return { name: "docker", status: "ok", message: `Docker ${info}` };
|
|
531
|
+
}
|
|
532
|
+
async function checkProjectToolsAsync(basePath) {
|
|
533
|
+
const spec = readProjectToolSpec(basePath);
|
|
534
|
+
if (!spec)
|
|
535
|
+
return [];
|
|
536
|
+
const results = [];
|
|
537
|
+
if (spec.packageManager && !(await commandExistsAsync(spec.packageManager, basePath))) {
|
|
538
|
+
results.push({
|
|
539
|
+
name: "package_manager",
|
|
540
|
+
status: "warning",
|
|
541
|
+
message: `Project requires ${spec.packageManager} but it's not installed`,
|
|
542
|
+
detail: `Install with: npm install -g ${spec.packageManager}`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (spec.needsTsc) {
|
|
546
|
+
results.push({
|
|
547
|
+
name: "typescript",
|
|
548
|
+
status: "warning",
|
|
549
|
+
message: "TypeScript is a dependency but tsc is not available (run npm install)",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (spec.needsPython && detectPythonExecutable() === null) {
|
|
553
|
+
results.push({ name: "python", status: "warning", message: "Project has Python config but python is not installed" });
|
|
554
|
+
}
|
|
555
|
+
if (spec.needsCargo && !(await commandExistsAsync("cargo", basePath))) {
|
|
556
|
+
results.push({ name: "cargo", status: "warning", message: "Project has Cargo.toml but cargo is not installed" });
|
|
557
|
+
}
|
|
558
|
+
if (spec.needsGo && !(await commandExistsAsync("go", basePath))) {
|
|
559
|
+
results.push({ name: "go", status: "warning", message: "Project has go.mod but go is not installed" });
|
|
560
|
+
}
|
|
561
|
+
return results;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Non-blocking equivalent of `runEnvironmentChecks` for the health-widget
|
|
565
|
+
* background refresh. Cheap fs checks run inline; the subprocess-backed checks
|
|
566
|
+
* fan out concurrently so the whole suite costs ~max(check), not the sum.
|
|
567
|
+
*/
|
|
568
|
+
export async function runEnvironmentChecksAsync(basePath) {
|
|
569
|
+
const results = [];
|
|
570
|
+
// fs-only checks — already cheap and synchronous.
|
|
571
|
+
const depsCheck = checkDependenciesInstalled(basePath);
|
|
572
|
+
if (depsCheck)
|
|
573
|
+
results.push(depsCheck);
|
|
574
|
+
const envCheck = checkEnvFiles(basePath);
|
|
575
|
+
if (envCheck)
|
|
576
|
+
results.push(envCheck);
|
|
577
|
+
// subprocess-backed checks — run concurrently, off the event-loop critical path.
|
|
578
|
+
const [nodeCheck, portChecks, diskCheck, dockerCheck, toolChecks] = await Promise.all([
|
|
579
|
+
checkNodeVersionAsync(basePath),
|
|
580
|
+
checkPortConflictsAsync(basePath),
|
|
581
|
+
checkDiskSpaceAsync(basePath),
|
|
582
|
+
checkDockerAsync(basePath),
|
|
583
|
+
checkProjectToolsAsync(basePath),
|
|
584
|
+
]);
|
|
585
|
+
if (nodeCheck)
|
|
586
|
+
results.push(nodeCheck);
|
|
587
|
+
results.push(...portChecks);
|
|
588
|
+
if (diskCheck)
|
|
589
|
+
results.push(diskCheck);
|
|
590
|
+
if (dockerCheck)
|
|
591
|
+
results.push(dockerCheck);
|
|
592
|
+
results.push(...toolChecks);
|
|
593
|
+
return results;
|
|
594
|
+
}
|
|
464
595
|
export function runEnvironmentChecks(basePath) {
|
|
465
596
|
const results = [];
|
|
466
597
|
const nodeCheck = checkNodeVersion(basePath);
|
|
@@ -42,9 +42,14 @@ export { nextMilestoneIdReserved } from "./milestone-id-reservation.js";
|
|
|
42
42
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
43
43
|
import { buildCloseoutMenuActions, buildIdleMenuSummary, getPrimaryCloseoutRecommendation, handleCloseoutChoice, loadCloseoutContext, } from "./closeout-wizard.js";
|
|
44
44
|
import { buildRequirementsBacklogDiscussContext, countUnmappedActiveRequirements, showRequirementsBacklogReview, } from "./requirements-backlog.js";
|
|
45
|
-
import { selectAndApplyModel } from "./auto-model-selection.js";
|
|
45
|
+
import { selectAndApplyModel, getRegisteredToolSnapshot } from "./auto-model-selection.js";
|
|
46
46
|
import { DISCUSS_TOOLS_ALLOWLIST } from "./constants.js";
|
|
47
|
-
import { supportsStructuredQuestions } from "./workflow-mcp.js";
|
|
47
|
+
import { detectWorkflowMcpLaunchConfig, resolveWorkflowMcpProjectRoot, supportsStructuredQuestions, } from "./workflow-mcp.js";
|
|
48
|
+
import { usesWorkflowMcpTransport } from "./question-transport.js";
|
|
49
|
+
import { getCachedWorkflowMcpProbe, probeAndCacheWorkflowMcp, warmWorkflowMcpProbeInBackground, workflowMcpProbeAdvertisesSurface, WORKFLOW_MCP_PROBE_TIMEOUT_MS, } from "./workflow-mcp-readiness-cache.js";
|
|
50
|
+
import { probeCoversRequiredWorkflowTools } from "./tool-surface-readiness.js";
|
|
51
|
+
import { getRequiredWorkflowToolsForUnit } from "./unit-tool-contracts.js";
|
|
52
|
+
import { isWorkflowToolSurfaceName } from "./workflow-tool-surface.js";
|
|
48
53
|
import { getUnitWorkflowDispatchReadinessError } from "./tool-contract.js";
|
|
49
54
|
import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
|
|
50
55
|
import { verifyExpectedArtifact } from "./auto-recovery.js";
|
|
@@ -369,6 +374,67 @@ async function dispatchNextDeepProjectSetupStage(entry) {
|
|
|
369
374
|
export function resolveGuidedDispatchProjectRoot(basePath) {
|
|
370
375
|
return basePath ?? process.cwd();
|
|
371
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Wait until the workflow MCP server is reachable and advertising its tool
|
|
379
|
+
* surface. Returns failure details when timed out, or null when ready (or MCP
|
|
380
|
+
* is not the transport). Called inside dispatchWorkflow() so every guided-flow
|
|
381
|
+
* dispatch path is gated automatically.
|
|
382
|
+
*/
|
|
383
|
+
const MCP_READINESS_TIMEOUT_MS = 15_000;
|
|
384
|
+
const MCP_READINESS_POLL_MS = 200;
|
|
385
|
+
async function awaitWorkflowMcpReadiness(pi, ctx, basePath, options = {}) {
|
|
386
|
+
const provider = ctx.model?.provider;
|
|
387
|
+
const authMode = provider ? ctx.modelRegistry.getProviderAuthMode(provider) : undefined;
|
|
388
|
+
if (!usesWorkflowMcpTransport(authMode, ctx.model?.baseUrl))
|
|
389
|
+
return null;
|
|
390
|
+
const projectRoot = resolveWorkflowMcpProjectRoot(basePath);
|
|
391
|
+
const launch = detectWorkflowMcpLaunchConfig(projectRoot);
|
|
392
|
+
if (!launch)
|
|
393
|
+
return null;
|
|
394
|
+
const requiredTools = options.unitType
|
|
395
|
+
? getRequiredWorkflowToolsForUnit(options.unitType).filter(isWorkflowToolSurfaceName)
|
|
396
|
+
: [];
|
|
397
|
+
const coversExpectedSurface = (tools) => requiredTools.length > 0
|
|
398
|
+
? probeCoversRequiredWorkflowTools(tools, requiredTools)
|
|
399
|
+
: workflowMcpProbeAdvertisesSurface(tools);
|
|
400
|
+
const serverPrefix = `mcp__${launch.name}__`;
|
|
401
|
+
const systemPrompt = () => typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
402
|
+
const systemPromptCoversExpectedSurface = () => {
|
|
403
|
+
const prompt = systemPrompt();
|
|
404
|
+
return requiredTools.length > 0
|
|
405
|
+
? requiredTools.every((tool) => prompt.includes(`${serverPrefix}${tool}`))
|
|
406
|
+
: prompt.includes(serverPrefix);
|
|
407
|
+
};
|
|
408
|
+
const sessionAlreadyReady = () => coversExpectedSurface(getRegisteredToolSnapshot(pi)) ||
|
|
409
|
+
systemPromptCoversExpectedSurface();
|
|
410
|
+
if (sessionAlreadyReady())
|
|
411
|
+
return null;
|
|
412
|
+
if (coversExpectedSurface(getCachedWorkflowMcpProbe(projectRoot)?.tools ?? [])) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const probe = options.probe ?? probeAndCacheWorkflowMcp;
|
|
416
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? WORKFLOW_MCP_PROBE_TIMEOUT_MS;
|
|
417
|
+
ctx.ui.setStatus("gsd-step", `Waiting for ${launch.name} MCP server…`);
|
|
418
|
+
let lastError;
|
|
419
|
+
const deadline = Date.now() + (options.timeoutMs ?? MCP_READINESS_TIMEOUT_MS);
|
|
420
|
+
const pollMs = options.pollMs ?? MCP_READINESS_POLL_MS;
|
|
421
|
+
while (Date.now() < deadline) {
|
|
422
|
+
if (sessionAlreadyReady()) {
|
|
423
|
+
ctx.ui.setStatus("gsd-step", "");
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const result = await probe(projectRoot, { timeoutMs: probeTimeoutMs });
|
|
427
|
+
if (result.ok && coversExpectedSurface(result.tools)) {
|
|
428
|
+
ctx.ui.setStatus("gsd-step", "");
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
lastError = result.error;
|
|
432
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
433
|
+
}
|
|
434
|
+
ctx.ui.setStatus("gsd-step", "");
|
|
435
|
+
return lastError ? { server: launch.name, error: lastError } : { server: launch.name };
|
|
436
|
+
}
|
|
437
|
+
export const _awaitWorkflowMcpReadinessForTest = awaitWorkflowMcpReadiness;
|
|
372
438
|
/**
|
|
373
439
|
* Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
|
|
374
440
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
@@ -418,6 +484,25 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
|
|
|
418
484
|
ctx.ui.notify(compatibilityError, "error");
|
|
419
485
|
return;
|
|
420
486
|
}
|
|
487
|
+
// ── Live MCP readiness gate ────────────────────────────────────────
|
|
488
|
+
// Units with required workflow tools must not dispatch until the MCP
|
|
489
|
+
// surface covers that exact contract; otherwise the model can race into
|
|
490
|
+
// "No such tool available" before recovery sees a clean readiness error.
|
|
491
|
+
warmWorkflowMcpProbeInBackground(projectRoot);
|
|
492
|
+
const requiredWorkflowTools = getRequiredWorkflowToolsForUnit(unitType).filter(isWorkflowToolSurfaceName);
|
|
493
|
+
const strictBlocking = requiredWorkflowTools.length > 0
|
|
494
|
+
&& (process.env.GSD_GUIDED_MCP_BLOCKING ?? "").trim() !== "0";
|
|
495
|
+
if (strictBlocking) {
|
|
496
|
+
// If the workflow MCP server is configured but still connecting, wait
|
|
497
|
+
// for it instead of dispatching into a child session that will abort.
|
|
498
|
+
const readinessFailure = await awaitWorkflowMcpReadiness(pi, ctx, projectRoot, { unitType });
|
|
499
|
+
if (readinessFailure) {
|
|
500
|
+
const detail = readinessFailure.error ? ` ${readinessFailure.error}` : "";
|
|
501
|
+
ctx.ui.notify(`GSD workflow server "${readinessFailure.server}" did not connect in time.${detail} ` +
|
|
502
|
+
`Run \`/gsd mcp check ${readinessFailure.server}\` for details.`, "warning");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
421
506
|
}
|
|
422
507
|
// Scope tools for guided workflow turns (#2949, token-consumption savings).
|
|
423
508
|
// Providers with grammar-based constrained decoding (xAI/Grok) return
|
|
@@ -1300,6 +1385,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
|
|
|
1300
1385
|
}
|
|
1301
1386
|
export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
1302
1387
|
const stepMode = options?.step ?? true;
|
|
1388
|
+
warmWorkflowMcpProbeInBackground(basePath);
|
|
1303
1389
|
// ── Clear stale milestone ID reservations from previous cancelled sessions ──
|
|
1304
1390
|
// Reservations only need to survive within a single /gsd interaction.
|
|
1305
1391
|
// Without this, each cancelled session permanently bumps the next ID. (#2488)
|