* Internal URL parser that handles colons in the host segment.
*
* Standard `new URL()` interprets colons as port separators, which breaks
* namespaced internal URLs like `skill://plugin:name`. This parser extracts
* components via regex first, then falls back to a minimal URL-like object
* when `new URL()` fails.
*
* All code that parses internal URLs (router, protocol handlers, tools)
* MUST use this function instead of calling `new URL()` directly.
*/
import type { InternalUrl } from "./types";
const SCHEME_HOST_RE = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)/i;
const PATHNAME_RE = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/i;
* Parse an internal URL into an InternalUrl.
*
* Handles URLs where `new URL()` would fail (e.g., `skill://plugin:name`
* where the colon is not a port separator).
*/
export function parseInternalUrl(input: string): InternalUrl {
const hostMatch = input.match(SCHEME_HOST_RE);
const pathMatch = input.match(PATHNAME_RE);
let parsed: URL;
try {
parsed = new URL(input);
} catch {
if (!hostMatch) {
throw new Error(`Invalid URL: ${input}`);
}
const hashIdx = input.indexOf("#");
const hash = hashIdx !== -1 ? input.slice(hashIdx) : "";
const withoutHash = hashIdx !== -1 ? input.slice(0, hashIdx) : input;
const queryIdx = withoutHash.indexOf("?");
const search = queryIdx !== -1 ? withoutHash.slice(queryIdx) : "";
const queryString = search.slice(1);
let rawPathname = pathMatch?.[1] ?? "";
if (queryIdx !== -1 && rawPathname.includes("?")) {
rawPathname = rawPathname.slice(0, rawPathname.indexOf("?"));
}
parsed = {
protocol: `${hostMatch[1]}:`,
hostname: hostMatch[2] ?? "",
host: hostMatch[2] ?? "",
pathname: rawPathname,
href: input,
search,
hash,
searchParams: new URLSearchParams(queryString),
} as unknown as URL;
}
let rawHost = hostMatch ? hostMatch[2] : parsed.hostname;
try {
rawHost = decodeURIComponent(rawHost);
} catch {
}
const result = parsed as InternalUrl;
result.rawHost = rawHost;
result.rawPathname = pathMatch?.[1] ?? parsed.pathname;
return result;
}