diff --git a/cli/src/commands/run.ts b/cli/src/commands/run.ts index cfdd5ad6..d96b010c 100644 --- a/cli/src/commands/run.ts +++ b/cli/src/commands/run.ts @@ -64,41 +64,60 @@ export async function runCommand(opts: RunOptions): Promise { await importServerEntry(); } +function formatError(err: unknown): string { + if (err instanceof Error) { + if (err.message && err.message.trim().length > 0) return err.message; + return err.name; + } + if (typeof err === "string") return err; + try { + return JSON.stringify(err); + } catch { + return String(err); + } +} + +function isModuleNotFoundError(err: unknown): boolean { + if (!(err instanceof Error)) return false; + const code = (err as { code?: unknown }).code; + if (code === "ERR_MODULE_NOT_FOUND") return true; + return err.message.includes("Cannot find module"); +} + async function importServerEntry(): Promise { const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); const fileCandidates = [ - path.resolve(projectRoot, "server/dist/index.js"), path.resolve(projectRoot, "server/src/index.ts"), + path.resolve(projectRoot, "server/dist/index.js"), ]; + const existingFileCandidates = fileCandidates.filter((filePath) => fs.existsSync(filePath)); + if (existingFileCandidates.length > 0) { + for (const filePath of existingFileCandidates) { + try { + await import(pathToFileURL(filePath).href); + return; + } catch (err) { + throw new Error(`Failed to start Paperclip server from ${filePath}: ${formatError(err)}`); + } + } + } - const specifierCandidates: string[] = [ - "@paperclipai/server/dist/index.js", - "@paperclipai/server/src/index.ts", - ]; - - const importErrors: string[] = []; - + const specifierCandidates: string[] = ["@paperclipai/server/dist/index.js", "@paperclipai/server/src/index.ts"]; + const missingErrors: string[] = []; for (const specifier of specifierCandidates) { try { await import(specifier); return; } catch (err) { - importErrors.push(`${specifier}: ${err instanceof Error ? err.message : String(err)}`); - } - } - - for (const filePath of fileCandidates) { - if (!fs.existsSync(filePath)) continue; - try { - await import(pathToFileURL(filePath).href); - return; - } catch (err) { - importErrors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`); + if (isModuleNotFoundError(err)) { + missingErrors.push(`${specifier}: ${formatError(err)}`); + continue; + } + throw new Error(`Failed to start Paperclip server from ${specifier}: ${formatError(err)}`); } } throw new Error( - `Could not start Paperclip server entrypoint. Tried: ${[...specifierCandidates, ...fileCandidates].join(", ")}\n` + - importErrors.join("\n"), + `Could not locate a Paperclip server entrypoint. Tried: ${[...fileCandidates, ...specifierCandidates].join(", ")}\n${missingErrors.join("\n")}`, ); } diff --git a/server/src/index.ts b/server/src/index.ts index 2018db16..433b48b2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -225,27 +225,14 @@ if (config.databaseUrl) { } const dataDir = resolve(config.embeddedPostgresDataDir); - const port = config.embeddedPostgresPort; + const configuredPort = config.embeddedPostgresPort; + let port = configuredPort; if (config.databaseMode === "postgres") { logger.warn("Database mode is postgres but no connection string was set; falling back to embedded PostgreSQL"); } - logger.info(`No DATABASE_URL set — using embedded PostgreSQL (${dataDir}) on port ${port}`); - embeddedPostgres = new EmbeddedPostgres({ - databaseDir: dataDir, - user: "paperclip", - password: "paperclip", - port, - persistent: true, - }); const clusterVersionFile = resolve(dataDir, "PG_VERSION"); - if (!existsSync(clusterVersionFile)) { - await embeddedPostgres.initialise(); - } else { - logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`); - } - const postmasterPidFile = resolve(dataDir, "postmaster.pid"); const isPidRunning = (pid: number): boolean => { try { @@ -271,8 +258,31 @@ if (config.databaseUrl) { const runningPid = getRunningPid(); if (runningPid) { - logger.warn({ pid: runningPid }, "Embedded PostgreSQL already running; reusing existing process"); + logger.warn({ pid: runningPid, port }, "Embedded PostgreSQL already running; reusing existing process"); } else { + const detectedPort = await detectPort(configuredPort); + if (detectedPort !== configuredPort) { + logger.warn( + { requestedPort: configuredPort, selectedPort: detectedPort }, + "Embedded PostgreSQL port is in use; using next free port", + ); + } + port = detectedPort; + logger.info(`No DATABASE_URL set — using embedded PostgreSQL (${dataDir}) on port ${port}`); + embeddedPostgres = new EmbeddedPostgres({ + databaseDir: dataDir, + user: "paperclip", + password: "paperclip", + port, + persistent: true, + }); + + if (!existsSync(clusterVersionFile)) { + await embeddedPostgres.initialise(); + } else { + logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`); + } + if (existsSync(postmasterPidFile)) { logger.warn("Removing stale embedded PostgreSQL lock file"); rmSync(postmasterPidFile, { force: true });