Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"build:app": "vite build",
"dev": "node scripts/dev-server.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/health.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
20 changes: 18 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type DevspaceUserConfig,
} from "./user-config.js";
import { expandHomePath } from "./roots.js";
import { checkEndpoint, formatEndpointHealth } from "./health.js";

type Command = "serve" | "init" | "doctor" | "config" | "help" | "version";
const require = createRequire(import.meta.url);
Expand Down Expand Up @@ -219,10 +220,25 @@ async function runDoctor(): Promise<void> {

try {
const config = loadConfig();
console.log(`Local MCP URL: http://${config.host}:${config.port}/mcp`);
console.log(`Public MCP URL: ${new URL("/mcp", config.publicBaseUrl).toString()}`);
const localMcpUrl = `http://${config.host}:${config.port}/mcp`;
const publicMcpUrl = new URL("/mcp", config.publicBaseUrl).toString();
const localHealthUrl = `http://${config.host}:${config.port}/healthz`;
const publicHealthUrl = new URL("/healthz", config.publicBaseUrl).toString();
Comment on lines +225 to +226

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\bhost\b' src/config.ts -C2
rg -nP 'DEVSPACE_HOST|host\s*[:=]' src/config.ts -C2

Repository: Waishnav/devspace

Length of output: 2133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant config and CLI code around the reported lines.
sed -n '200,270p' src/config.ts
printf '\n--- CLI ---\n'
sed -n '210,255p' src/cli.ts

Repository: Waishnav/devspace

Length of output: 4458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant config and CLI code around the reported lines.
sed -n '200,270p' src/config.ts
printf '\n--- CLI ---\n'
sed -n '210,255p' src/cli.ts

Repository: Waishnav/devspace

Length of output: 4458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how config.host is parsed from files and whether any validation constrains it.
rg -n "files\.config\.host|host:" src/config.ts -C3
printf '\n--- loadDevspaceFiles ---\n'
sed -n '1,220p' src/config.ts

Repository: Waishnav/devspace

Length of output: 8335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how config.host is parsed from files and whether any validation constrains it.
rg -n "files\.config\.host|host:" src/config.ts -C3
printf '\n--- loadDevspaceFiles ---\n'
sed -n '1,220p' src/config.ts

Repository: Waishnav/devspace

Length of output: 8335


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how the endpoint checker consumes the constructed URLs.
rg -n "function checkEndpoint|checkEndpoint\(" src/cli.ts -C3
sed -n '1,120p' src/cli.ts

Repository: Waishnav/devspace

Length of output: 4600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how the endpoint checker consumes the constructed URLs.
rg -n "function checkEndpoint|checkEndpoint\(" src/cli.ts -C3
sed -n '1,120p' src/cli.ts

Repository: Waishnav/devspace

Length of output: 4600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how the endpoint checker consumes the constructed URLs.
rg -n "function checkEndpoint|checkEndpoint\(" src/cli.ts -C3
sed -n '1,120p' src/cli.ts

Repository: Waishnav/devspace

Length of output: 4600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the health checker to see how these URLs are consumed.
sed -n '1,220p' src/health.ts

Repository: Waishnav/devspace

Length of output: 1047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the health checker to see how these URLs are consumed.
sed -n '1,220p' src/health.ts

Repository: Waishnav/devspace

Length of output: 1047


Normalize the probe host before building local URLs.
HOST/files.config.host is unrestricted (default 127.0.0.1), so localMcpUrl and localHealthUrl can break for IPv6 literals like ::1 and can point at unusable bind-all addresses like 0.0.0.0/::. Use a loopback address for probes and bracket IPv6 hosts, matching localPublicBaseUrl().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 225 - 226, Normalize the host used for the local
probe URLs in the CLI so `localMcpUrl` and `localHealthUrl` are built from a
loopback-safe address instead of raw `config.host`, since IPv6 literals and
bind-all hosts can produce invalid or unusable URLs. Update the URL construction
in the `src/cli.ts` logic that builds the local probe endpoints to match the
same host-normalization behavior used by `localPublicBaseUrl()`, including
bracketed IPv6 handling and replacing `0.0.0.0`/`::` with a loopback address.

console.log(`Local MCP URL: ${localMcpUrl}`);
console.log(`Public MCP URL: ${publicMcpUrl}`);
console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);

const localHealth = await checkEndpoint(localHealthUrl);
const publicHealth = await checkEndpoint(publicHealthUrl);
console.log(`Local server: ${formatEndpointHealth(localHealth)}`);
console.log(`Public tunnel: ${formatEndpointHealth(publicHealth)}`);

if (!localHealth.ok) {
console.log("Action: start DevSpace with `devspace serve`.");
} else if (!publicHealth.ok) {
console.log("Action: start or repair the HTTPS tunnel, then update publicBaseUrl and reconnect the MCP client.");
}
} catch (error) {
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
}
Expand Down
19 changes: 19 additions & 0 deletions src/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { checkEndpoint } from "./health.js";

assert.deepEqual(
await checkEndpoint("http://127.0.0.1:7676/healthz", async () => new Response("ok", { status: 200 })),
{ ok: true, status: 200 },
);

assert.deepEqual(
await checkEndpoint("https://stale.example.com/healthz", async () => {
throw new TypeError("fetch failed");
}),
{ ok: false, error: "fetch failed" },
);

assert.deepEqual(
await checkEndpoint("https://example.com/healthz", async () => new Response("no", { status: 502 })),
{ ok: false, status: 502, error: "HTTP 502" },
);
38 changes: 38 additions & 0 deletions src/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface EndpointHealth {
ok: boolean;
status?: number;
error?: string;
}

type Fetch = (input: string, init?: RequestInit) => Promise<Response>;

export async function checkEndpoint(
url: string,
fetcher: Fetch = fetch,
): Promise<EndpointHealth> {
try {
const response = await fetcher(url, {
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
return {
ok: false,
status: response.status,
error: `HTTP ${response.status}`,
};
}

return { ok: true, status: response.status };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}

export function formatEndpointHealth(health: EndpointHealth): string {
return health.ok
? `reachable (HTTP ${health.status})`
: `unreachable (${health.error ?? "unknown error"})`;
}