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
5 changes: 5 additions & 0 deletions .changeset/dev-server-clickable-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": minor
---

The Astro dev server now prints absolute, clickable URLs for the admin UI and (when enabled) the MCP server, along with a dev-bypass shortcut link that signs you in as a dev admin without going through passkey setup or auth. The startup banner also shows the installed EmDash version. The dev-bypass link is dev-only and the underlying endpoint returns 403 in production.
47 changes: 40 additions & 7 deletions packages/core/src/astro/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { AstroIntegration, AstroIntegrationLogger } from "astro";

import { validateAllowedOrigins, validateOriginShape } from "../../auth/allowed-origins.js";
import type { ResolvedPlugin } from "../../plugins/types.js";
import { VERSION } from "../../version.js";
import { local } from "../storage/adapters.js";
import { notoSans } from "./font-provider.js";
import {
Expand Down Expand Up @@ -68,15 +69,24 @@ const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
function printBanner(_logger: AstroIntegrationLogger): void {
const banner = `

${bold(cyan("— E M D A S H —"))}
${bold(cyan("— E M D A S H —"))} ${dim(`v${VERSION}`)}
`;
console.log(banner);
}

/** Print route injection summary */
function printRoutesSummary(_logger: AstroIntegrationLogger): void {
console.log(`\n ${dim("›")} Admin UI ${cyan("/_emdash/admin")}`);
console.log(` ${dim("›")} API ${cyan("/_emdash/api/*")}`);
/**
* Print dev-server route info with absolute (clickable) URLs, including the
* dev-bypass shortcut that skips passkey auth. Dev only -- the dev-bypass
* endpoint returns 403 in production.
*/
function printDevServerInfo(baseUrl: string, mcpEnabled: boolean): void {
const devBypassUrl = `${baseUrl}/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin`;
console.log(`\n ${dim("›")} Admin UI ${cyan(`${baseUrl}/_emdash/admin`)}`);
if (mcpEnabled) {
console.log(` ${dim("›")} MCP server ${cyan(`${baseUrl}/_emdash/api/mcp`)}`);
}
console.log(` ${dim("›")} Dev bypass ${cyan(devBypassUrl)}`);
console.log(` ${dim("Skips passkey setup/auth and signs you in as a dev admin")}`);
console.log("");
}

Expand Down Expand Up @@ -211,6 +221,10 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
// Check if auth is an AuthDescriptor (has entrypoint) indicating external auth
const useExternalAuth = !!(resolvedConfig.auth && "entrypoint" in resolvedConfig.auth);

// Captured in astro:config:setup so the astro:server:setup hook can tell
// whether we're running `astro dev` (where the dev-bypass shortcut applies).
let astroCommand: "dev" | "build" | "preview" | "sync" | undefined;

return {
name: "emdash",
hooks: {
Expand All @@ -222,6 +236,7 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
config: astroConfig,
command,
}) => {
astroCommand = command;
printBanner(logger);
// Capture the host's Astro version so the runtime can expose it
// to the admin and the registry install gate for `env:astro`
Expand Down Expand Up @@ -258,7 +273,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
const securityConfig: Record<string, unknown> = {
checkOrigin: false,
...(resolvedConfig.siteUrl
? { allowedDomains: [{ hostname: new URL(resolvedConfig.siteUrl).hostname }] }
? {
allowedDomains: [{ hostname: new URL(resolvedConfig.siteUrl).hostname }],
}
: {}),
};

Expand Down Expand Up @@ -375,9 +392,25 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
order: "pre",
});

printRoutesSummary(logger);
// Route info is printed with absolute, clickable URLs once the
// dev server is listening (see astro:server:setup), since the
// port isn't known yet here. Nothing useful to print for build.
},
"astro:server:setup": ({ server, logger }) => {
// Print route info with absolute, clickable URLs once the server
// is listening. Only in `astro dev` -- the dev-bypass shortcut is
// dev-only and the port is unknown until now.
if (astroCommand === "dev") {
server.httpServer?.once("listening", () => {
const address = server.httpServer?.address();
if (!address || typeof address === "string") return;
Comment on lines +404 to +406

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] The URL formatting from server.httpServer.address() is broken for IPv6 addresses other than ::1. When the dev server binds to :: (all interfaces) or a specific IPv6 address, the resulting string lacks brackets and is not a valid URL — for example http://:::4321. This defeats the PR's goal of "clickable URLs" on IPv6-enabled systems or when using --host.

Bracket IPv6 addresses and map :: / 0.0.0.0 to localhost, matching Vite's own behavior:

Suggested change
server.httpServer?.once("listening", () => {
const address = server.httpServer?.address();
if (!address || typeof address === "string") return;
server.httpServer?.once("listening", () => {
const address = server.httpServer?.address();
if (!address || typeof address === "string") return;
let host = address.address;
if (host === "::1" || host === "::" || host === "0.0.0.0") {
host = "localhost";
} else if (address.family === "IPv6") {
host = `[${host}]`;
}
printDevServerInfo(
`http://${host}:${address.port}`,
resolvedConfig.mcp !== false,
);
});

printDevServerInfo(
`http://${address.address === "::1" ? "localhost" : address.address}:${address.port}`,
resolvedConfig.mcp !== false,
);
Comment on lines +405 to +410
});
}

// Generate types once the server is listening.
// The endpoint returns the types content; we write the file here
// (in Node) because workerd has no real filesystem access.
Expand Down
Loading