feat(server): built-in service proxy — absorb portless into daemon

Services defined in paseo.json get reverse-proxied through the daemon
via hostname-based routing on *.localhost. Each service receives $PORT,
$HOST, and $PASEO_SERVICE_URL env vars, and is accessible at
{service}.localhost:6767 (main) or {branch}.{service}.localhost:6767
(worktrees).
This commit is contained in:
Mohamed Boudra
2026-03-30 21:41:03 +07:00
parent 0ba2f6b194
commit 266d042528
9 changed files with 829 additions and 0 deletions
+40
View File
@@ -20,6 +20,7 @@ interface PaseoConfig {
teardown?: string[];
terminals?: WorktreeTerminalConfig[];
};
services?: Record<string, ServiceConfig>;
}
const execAsync = promisify(exec);
@@ -84,6 +85,11 @@ export interface WorktreeTerminalConfig {
command: string;
}
export interface ServiceConfig {
command: string;
port?: number; // explicit port override, otherwise auto-assigned
}
export class WorktreeSetupError extends Error {
readonly results: WorktreeSetupCommandResult[];
@@ -194,6 +200,40 @@ export function getWorktreeTerminalSpecs(repoRoot: string): WorktreeTerminalConf
return specs;
}
export function getServiceConfigs(repoRoot: string): Map<string, ServiceConfig> {
const config = readPaseoConfig(repoRoot);
const services = config?.services;
if (!services || typeof services !== "object") {
return new Map();
}
const result = new Map<string, ServiceConfig>();
for (const [name, entry] of Object.entries(services)) {
if (!entry || typeof entry !== "object") {
continue;
}
const rawCommand = entry.command;
if (typeof rawCommand !== "string") {
continue;
}
const command = rawCommand.trim();
if (!command) {
continue;
}
const serviceConfig: ServiceConfig = { command };
if (typeof entry.port === "number" && Number.isFinite(entry.port)) {
serviceConfig.port = entry.port;
}
result.set(name, serviceConfig);
}
return result;
}
async function execSetupCommand(
command: string,
options: { cwd: string; env: NodeJS.ProcessEnv },