/** * Server Manager * Manages development servers (any language/framework) - start, stop, restart, and monitor * Supports multiple runner types: native (spawn), docker, docker-compose * Persists state to .cdp-tools/servers.json for recovery and auto-run * Logs to .cdp-tools/logs// for cross-MCP access (native runner only) */ import * as net from 'net'; import { type ServerClaimsStore } from './server-claims.js'; import { type RunnerType } from './runners/index.js'; /** * Returns the matched auto-restart pattern name if `command` looks like it * self-restarts on file changes, or null otherwise. */ export declare function detectAutoRestartCommand(command: string): string | null; /** * Extract the Node inspector port from a `--inspect`/`--inspect-brk` flag in * a command string, if present. Falls back to Node's default port (9229) * when the flag is given with no explicit port. Does not handle a separate * `--inspect-port=` flag overriding a bare `--inspect`'s port - a rare * combination not worth the extra complexity here. */ export declare function extractInspectorPort(command: string): number | null; export interface ServerStatus { id: string; command: string; cwd: string; pid: number; containerId?: string; startedAt: Date; uptime: string; port?: number; running: boolean; autoRun: boolean; runnerType: RunnerType; global: boolean; watch: boolean; } export interface StartServerOptions { command: string; cwd: string; id: string; autoRun?: boolean; env?: Record; port?: number; /** Runner type - auto-detected from command if not specified */ runner?: RunnerType; /** If true, auto-add detected port to monitoredPorts with default level 'block' */ monitorPort?: boolean; /** If true, store server state in global ~/.cdp-tools/ instead of project directory */ global?: boolean; /** If true, watch this server's files and auto-restart it (pause-aware) on change instead of relying on --watch/nodemon */ watch?: boolean; /** Paths to watch when `watch` is true (default: [cwd]) */ watchPaths?: string[]; } export interface LogStats { serverId: string; newStdout: number; newStderr: number; } /** A watch-triggered restart that's queued behind a paused breakpoint debugger. */ export interface PendingRestartInfo { serverId: string; queuedAt: Date; } export type MonitoringLevel = 'inform' | 'error' | 'block'; export interface PersistedMonitoredPort { port: number; level: MonitoringLevel; description?: string; interval?: number; } export interface MonitoredPort { port: number; level: MonitoringLevel; description?: string; interval?: number; status: 'up' | 'down' | 'connecting'; socket: net.Socket | null; failedAt?: Date; acknowledged: boolean; reconnectTimer?: ReturnType; } export interface MonitoredPortStatus { port: number; level: MonitoringLevel; description?: string; interval?: number; status: 'up' | 'down' | 'connecting'; failedAt?: Date; acknowledged: boolean; } export interface PortFailureInfo { port: number; level: MonitoringLevel; description?: string; failedAt: Date; } /** Function type for getting interval for a monitoring level */ export type GetIntervalForLevel = (level: MonitoringLevel) => number; export type PendingStartupReason = 'timeout' | 'died'; export interface PendingStartup { serverId: string; startedAt: Date; timeoutAt: Date; acknowledged: boolean; reason?: PendingStartupReason; } export interface PersistedPendingStartup { serverId: string; startedAt: string; timeoutAt: string; acknowledged: boolean; reason?: PendingStartupReason; } export interface PendingStartupFailureInfo { serverId: string; startedAt: Date; reason: PendingStartupReason; } /** * Port Monitor - monitors ports using persistent TCP connections */ export declare class PortMonitor { private ports; private onFailureCallback?; private getIntervalForLevel; private pauseCount; constructor(getIntervalForLevel?: GetIntervalForLevel); onFailure(callback: (port: number, level: MonitoringLevel) => void): void; startMonitoring(port: number, level: MonitoringLevel, description?: string, interval?: number): Promise; private connectToPort; private scheduleReconnect; stopMonitoring(port: number): Promise; acknowledgeFailure(port: number): Promise; getFailedPorts(): PortFailureInfo[]; getFailedPortsByLevel(level: MonitoringLevel): PortFailureInfo[]; hasBlockingFailures(): boolean; isMonitoring(port: number): boolean; getStatus(port?: number): MonitoredPortStatus[]; getPersistedState(): PersistedMonitoredPort[]; restoreFromState(ports: PersistedMonitoredPort[]): Promise; stopAll(): Promise; /** * Pause all port monitoring (e.g., when paused at a breakpoint) * Stops reconnection attempts but preserves port state */ pauseMonitoring(): void; /** * Resume all port monitoring (e.g., when resuming from a breakpoint) * Restarts connection attempts for all monitored ports, once every * outstanding pauseMonitoring() call has a matching resumeMonitoring(). */ resumeMonitoring(): void; } export declare class ServerManager { private servers; private portMonitor; private pendingStartups; /** Mutex to serialize saveState calls - prevents concurrent file writes */ private saveMutex; /** Injected from index.ts - decouples ServerManager from ConnectionManager directly */ private pauseChecker; /** Watch-mode restart coordination, keyed by serverId - see WatchRestartState */ private watchState; /** Ownership claims - who may stop a shared dev server (see server-claims.ts) */ private readonly claims; constructor(claimsStore?: ServerClaimsStore); /** * Set the function used to check whether a CDP connection at a given * inspector port is currently paused at a breakpoint. Mirrors the existing * setChromeLauncher() injection pattern. */ setPauseChecker(fn: (inspectorPort: number) => boolean): void; /** * Get the port monitor instance (lazy-initialized) * Must be called after configManager.load() for config values to be used */ getPortMonitor(): PortMonitor; private getServersFilePath; /** * Initialize - load state and recover/start auto-run servers * Loads from both local (project) and global (~/.cdp-tools/) storage */ initialize(): Promise<{ recovered: string[]; started: string[]; failed: string[]; monitoredPorts: number[]; collected: string[]; }>; /** * Resume port detection for a server after MCP restart * @param serverId The server to resume detection for * @param remainingMs Time remaining until timeout */ private resumePortDetection; private loadState; /** * Save state to disk - serialized through mutex to prevent concurrent writes */ saveState(): Promise; /** * Internal save implementation - uses atomic writes to prevent corruption */ private doSaveState; private getRunnerCommand; private getRunnerCwd; /** * Find the managed server whose command's --inspect/--inspect-brk port * matches `port` - i.e. the server a CDP debugger connection at that port * actually belongs to. This is deliberately NOT the server's own detected * app/service port (e.g. an HTTP port parsed from "listening on port * 3000") - a Node process's inspector port and its own app port are * normally different numbers, so matching on the app port would almost * never find anything real. */ getManagedServerByInspectorPort(port: number): Promise<{ id: string; command: string; } | null>; /** Sync core of getManagedServerByInspectorPort() - reused by watch-restart lookups, which run in a sync pre-execution hot path (checkBreakpointPause) and can't await. */ private findManagedServerByInspectorPortSync; private isPortInUse; /** * Find PIDs of processes listening on a port using lsof */ private findProcessesOnPort; /** * Get the working directory of a process by PID */ private getProcessCwd; /** * Kill orphan processes holding a port, but only if they're in the expected directory * Returns: { killed: PIDs killed, foreign: PIDs that are from a different directory } */ private killOrphanProcessesOnPort; private waitForPortRelease; /** * Start a server */ startServer(options: StartServerOptions): Promise<{ id: string; pid: number; runnerType: RunnerType; containerId?: string; autoRestartWarning?: string; }>; /** * Background port detection with timeout management * @param serverId The server to detect port for * @param timeoutMs How long to wait before triggering blocking (default 30s) */ private detectPortInBackgroundWithTimeout; /** * @deprecated Use detectPortInBackgroundWithTimeout instead */ private detectPortInBackground; /** * Get pending startup failures that should trigger blocking * Returns only startups that have timed out or died and are not acknowledged */ getPendingStartupFailures(): PendingStartupFailureInfo[]; /** * Check if a server has a pending startup (regardless of state) */ hasPendingStartup(serverId: string): boolean; /** * Get pending startup status for a server */ getPendingStartup(serverId: string): PendingStartup | undefined; /** * Acknowledge a pending startup failure * Clears the blocking state and starts background health monitoring */ acknowledgeStartup(serverId: string): Promise; /** * Monitor server health after acknowledgment * Checks every 5 seconds for: * - Port detection (sets up port monitoring when found) * - Server death (re-triggers blocking) * Runs until port is found, server dies, or server is removed */ private monitorServerHealth; /** * Extend the startup timeout by another 30 seconds * Resets the acknowledged flag and reason, resumes detection */ extendStartupTimeout(serverId: string): Promise; /** * Remove pending startup entry (called when server is stopped or removed) */ private removePendingStartup; /** * Stop a server */ stopServer(serverId: string): Promise; /** * Restart a server * Reloads config from persisted state to pick up any manual edits (e.g., port changes) */ restartServer(serverId: string): Promise<{ id: string; pid: number; runnerType: RunnerType; containerId?: string; }>; private getOrCreateWatchState; private startWatcher; private stopWatcher; /** * Single entry point for every watch-triggered restart (file-change, * resume-triggered auto-fire, and its own re-entrant re-check once an * in-flight restart finishes). Always re-checks pause state fresh rather * than trusting a cached value - see issue #88. */ requestWatchRestart(serverId: string): Promise; /** * Explicit, forced restart (bypasses the pause-check entirely - the caller * has already decided to end any paused debug session). Shares the same * in-flight guard as requestWatchRestart(): if a watch-triggered restart is * already running, piggybacks on its result instead of racing it. */ forceRestart(serverId: string): Promise<{ id: string; pid: number; runnerType: RunnerType; containerId?: string; }>; /** Shared restart guard used by both requestWatchRestart() and forceRestart(). */ private performGuardedRestart; /** For checkBreakpointPause's lookup (via a paused connection's inspector port). */ getPendingRestartByInspectorPort(port: number): PendingRestartInfo | null; /** Discard a queued watch-restart without performing it - keep debugging. */ cancelPendingRestart(serverId: string): boolean; /** * Re-check a port's pause state and let its queued watch-restart (if any) * fire now that the debugger has resumed. Call this after any resume that * un-pauses a connection - requestWatchRestart() only re-fires on the next * file change otherwise, so without this hook a restart deferred by a * pause would sit queued forever if no further edits happen. */ retryPendingRestartByInspectorPort(port: number): void; /** * Set autoRun flag */ setAutoRun(serverId: string, autoRun: boolean): Promise; /** * Get status of servers */ getStatus(serverId?: string): Promise; /** * Get log stats (for native runner only) */ getLogStats(): LogStats[]; /** * Get logs from a server */ getLogs(serverId: string, options?: { type?: 'stdout' | 'stderr' | 'all'; lines?: number; delta?: boolean; }): Promise; /** * Clear logs (native runner only) */ clearLogs(serverId: string): Promise<{ logDir: string; stdoutPath: string; stderrPath: string; }>; /** * Get log access info (file paths for native, commands for docker) */ getLogAccess(serverId: string): { type: 'file'; logDir: string; stdoutPath: string; stderrPath: string; } | { type: 'command'; command: string; } | null; /** * Stop every server this session owns outright - one no live session other * than this one claims. Used when the session is going away for good (an * idle suspend, or a client that closed), which is the only time it is safe * to take a dev server down. * * A server another window is still using is left alone, and so is one whose * ownership cannot be established: over-stopping destroys running work, * under-stopping leaves a process for the next `initialize()` to collect. */ stopOwnedServers(): Promise<{ stopped: string[]; keptForOthers: string[]; }>; /** * Server ids whose every claim was dead, per storage scope, with those dead * claims deleted as a side effect. * * Must be read BEFORE this session claims anything during recovery - * otherwise our own fresh claim makes an orphan look owned, and nothing is * ever collected. * * A server with no claim file at all is absent from this: it predates * claims, or was started outside cdp-tools, and neither is ours to kill. */ private findAbandonedServerIds; /** * Stop all servers */ stopAll(): Promise; /** * Remove a server from config */ removeServer(serverId: string): Promise; /** * Get running server IDs */ getRunningServerIds(): Promise; /** * Cleanup - refresh status of all servers (no longer removes stopped servers) * Stopped servers remain in config and can be manually restarted */ cleanup(): Promise; /** * Add running servers that appeared in persisted state since this session * loaded, claiming each one: seeing a server is enough to depend on it. */ private adoptNewlyPersistedServers; /** * Reload a server's config from persisted state (for picking up manual edits) */ private reloadServerConfig; } //# sourceMappingURL=server-manager.d.ts.map