Files
paseo/packages/app/src/utils/workspace-script-links.ts
T
Mohamed Boudra 8aa55db1e8 feat(app): improve workspace service controls
Keep service route choices independent of daemon transport and persist the selected route per host. Use the shared web overlay stack so menus, tooltips, and toasts layer predictably.

Track proxy route changes from the Git branch rather than the workspace title to avoid transient service health changes.
2026-07-21 13:44:23 +02:00

93 lines
2.8 KiB
TypeScript

import { parseHostPort } from "@getpaseo/protocol/daemon-endpoints";
import type { WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
import type { ActiveConnection } from "@/runtime/host-runtime";
export type WorkspaceScriptLinkKind = "public" | "paseo" | "direct";
export interface WorkspaceScriptLinkTarget {
kind: WorkspaceScriptLinkKind;
label: string;
url: string;
}
export interface ResolvedWorkspaceScriptLink {
primary: WorkspaceScriptLinkTarget | null;
targets: WorkspaceScriptLinkTarget[];
}
function isLoopbackHost(host: string): boolean {
const normalizedHost = host.trim().toLowerCase();
return (
normalizedHost === "localhost" || normalizedHost === "127.0.0.1" || normalizedHost === "::1"
);
}
function isLocalOnlyUrl(url: string | null | undefined): boolean {
if (!url) return true;
try {
const hostname = new URL(url).hostname.toLowerCase();
return isLoopbackHost(hostname) || hostname.endsWith(".localhost");
} catch {
return true;
}
}
function stripUrlProtocol(url: string): string {
return url.replace(/^https?:\/\//, "");
}
function buildDirectServiceUrl(
activeConnection: ActiveConnection | null,
port: number | null,
): string | null {
if (port === null) return null;
if (activeConnection?.type !== "directTcp") {
return `http://localhost:${port}`;
}
try {
const { host, isIpv6 } = parseHostPort(activeConnection.endpoint);
let base = host;
if (isLoopbackHost(host)) {
base = "localhost";
} else if (isIpv6) {
base = `[${host}]`;
}
return `http://${base}:${port}`;
} catch {
return `http://localhost:${port}`;
}
}
function addTarget(
targets: WorkspaceScriptLinkTarget[],
kind: WorkspaceScriptLinkKind,
url: string | null | undefined,
): void {
if (!url || targets.some((target) => target.url === url)) return;
targets.push({ kind, label: stripUrlProtocol(url), url });
}
export function resolveWorkspaceScriptLink(input: {
script: WorkspaceScriptPayload;
activeConnection: ActiveConnection | null;
}): ResolvedWorkspaceScriptLink {
const { script, activeConnection } = input;
if (script.type !== "service" || script.lifecycle !== "running") {
return { primary: null, targets: [] };
}
// COMPAT(workspaceScriptSplitUrls): added in v0.2.0, remove after 2027-01-21.
// Old daemons only send proxyUrl, so classify it by reachability.
const localProxyUrl =
script.localProxyUrl ?? (isLocalOnlyUrl(script.proxyUrl) ? script.proxyUrl : null);
const publicProxyUrl =
script.publicProxyUrl ?? (!isLocalOnlyUrl(script.proxyUrl) ? script.proxyUrl : null);
const targets: WorkspaceScriptLinkTarget[] = [];
addTarget(targets, "public", publicProxyUrl);
addTarget(targets, "paseo", localProxyUrl);
addTarget(targets, "direct", buildDirectServiceUrl(activeConnection, script.port));
return { primary: targets[0] ?? null, targets };
}