/** * Shared utilities for CLI commands (dev, start, run). * * This module extracts common patterns used across different launch modes * to reduce duplication and ensure consistent behavior. */ import type { ChildProcess } from "node:child_process"; import { DEFAULT_AGENTCTL_PORT, DEFAULT_BACKEND_PORT, DEFAULT_MCP_PORT, DEFAULT_WEB_PORT, } from "./constants"; import { pickAvailablePort } from "./ports"; import { createProcessSupervisor } from "./process"; export type PortConfig = { backendPort: number; webPort: number; agentctlPort: number; mcpPort: number; backendUrl: string; mcpUrl: string; }; /** * Picks available ports for all services, using provided values or finding free ports. * * @param backendPort - Optional preferred backend port * @param webPort - Optional preferred web port * @returns Resolved ports for all services */ export async function pickPorts(backendPort?: number, webPort?: number): Promise { const resolvedBackendPort = backendPort ?? (await pickAvailablePort(DEFAULT_BACKEND_PORT)); const resolvedWebPort = webPort ?? (await pickAvailablePort(DEFAULT_WEB_PORT)); const agentctlPort = await pickAvailablePort(DEFAULT_AGENTCTL_PORT); const mcpPort = await pickAvailablePort(DEFAULT_MCP_PORT); return { backendPort: resolvedBackendPort, webPort: resolvedWebPort, agentctlPort, mcpPort, backendUrl: `http://localhost:${resolvedBackendPort}`, mcpUrl: `http://localhost:${mcpPort}/sse`, }; } export type BackendEnvOptions = { ports: PortConfig; /** Log level: debug, info, warn, error (default: info) */ logLevel?: string; /** Additional environment variables to merge */ extra?: Record; }; /** * Builds environment variables for the backend process. * * @param options - Configuration options for the backend environment * @returns Environment object for the backend process */ export function buildBackendEnv(options: BackendEnvOptions): NodeJS.ProcessEnv { const { ports, logLevel, extra } = options; return { ...process.env, KANDEV_SERVER_PORT: String(ports.backendPort), KANDEV_AGENT_STANDALONE_PORT: String(ports.agentctlPort), KANDEV_AGENT_MCP_SERVER_PORT: String(ports.mcpPort), ...(logLevel ? { KANDEV_LOG_LEVEL: logLevel } : {}), ...extra, }; } export type WebEnvOptions = { ports: PortConfig; /** Include MCP URL environment variables (used in dev/start modes) */ includeMcp?: boolean; /** Set NODE_ENV to production */ production?: boolean; /** Enable debug mode (NEXT_PUBLIC_KANDEV_DEBUG) */ debug?: boolean; }; /** * Builds environment variables for the web process. * * @param options - Configuration options for the web environment * @returns Environment object for the web process */ export function buildWebEnv(options: WebEnvOptions): NodeJS.ProcessEnv { const { ports, includeMcp = false, production = false, debug = false } = options; // Server-side env vars use localhost (SSR runs on same machine as backend) // Client-side uses NEXT_PUBLIC_*_PORT to build URLs dynamically from window.location.hostname // This allows accessing the app from any device (iPhone, Tailscale, etc.) const env: NodeJS.ProcessEnv = { ...process.env, // Server-side: full localhost URL for SSR KANDEV_API_BASE_URL: ports.backendUrl, // Client-side: only pass ports, client builds URL from current hostname NEXT_PUBLIC_KANDEV_API_PORT: String(ports.backendPort), PORT: String(ports.webPort), }; if (includeMcp) { env.KANDEV_MCP_SERVER_URL = ports.mcpUrl; env.NEXT_PUBLIC_KANDEV_MCP_PORT = String(ports.mcpPort); } if (production) { (env as Record).NODE_ENV = "production"; } if (debug) { env.NEXT_PUBLIC_KANDEV_DEBUG = "true"; } return env; } export type StartupInfoOptions = { /** Mode header line, e.g. "dev mode: using local repo" or "release: v0.0.12 (github latest)" */ header: string; ports: PortConfig; /** Database file path */ dbPath?: string; /** Whether to log MCP-related ports */ includeMcp?: boolean; /** Log level being used */ logLevel?: string; }; /** * Logs a unified startup info block to the console. */ export function logStartupInfo(options: StartupInfoOptions): void { const { header, ports, dbPath, includeMcp = false, logLevel } = options; console.log(`[kandev] ${header}`); console.log("[kandev] backend port:", ports.backendPort); console.log("[kandev] web port:", ports.webPort); console.log("[kandev] agentctl port:", ports.agentctlPort); if (includeMcp) { console.log("[kandev] mcp port:", ports.mcpPort); console.log("[kandev] mcp url:", ports.mcpUrl); } if (dbPath) { console.log("[kandev] db path:", dbPath); } if (logLevel) { console.log("[kandev] log level:", logLevel); } } /** * Attaches a standardized exit handler to a backend process. * * When the backend exits, this handler logs the exit reason and triggers * a graceful shutdown of all supervised processes. If the process was * killed by a signal, it exits with code 0; otherwise it uses the * process exit code (defaulting to 1). * * @param backendProc - The backend child process * @param supervisor - The process supervisor managing child processes */ export function attachBackendExitHandler( backendProc: ChildProcess, supervisor: ReturnType, ): void { backendProc.on("exit", (code, signal) => { console.error(`[kandev] backend exited (code=${code}, signal=${signal})`); const exitCode = signal ? 0 : (code ?? 1); void supervisor.shutdown("backend exit").then(() => process.exit(exitCode)); }); }