Spaces:
Build error
Build error
| import { spawn } from "node:child_process"; | |
| import { createInterface } from "node:readline/promises"; | |
| import { stdin, stdout } from "node:process"; | |
| const mode = process.argv[2] === "watch" ? "watch" : "dev"; | |
| const cliArgs = process.argv.slice(3); | |
| const tailscaleAuthFlagNames = new Set([ | |
| "--tailscale-auth", | |
| "--authenticated-private", | |
| ]); | |
| let tailscaleAuth = false; | |
| const forwardedArgs = []; | |
| for (const arg of cliArgs) { | |
| if (tailscaleAuthFlagNames.has(arg)) { | |
| tailscaleAuth = true; | |
| continue; | |
| } | |
| forwardedArgs.push(arg); | |
| } | |
| if (process.env.npm_config_tailscale_auth === "true") { | |
| tailscaleAuth = true; | |
| } | |
| if (process.env.npm_config_authenticated_private === "true") { | |
| tailscaleAuth = true; | |
| } | |
| const env = { | |
| ...process.env, | |
| PAPERCLIP_UI_DEV_MIDDLEWARE: "true", | |
| }; | |
| if (mode === "watch") { | |
| env.PAPERCLIP_MIGRATION_PROMPT ??= "never"; | |
| env.PAPERCLIP_MIGRATION_AUTO_APPLY ??= "true"; | |
| } | |
| if (tailscaleAuth) { | |
| env.PAPERCLIP_DEPLOYMENT_MODE = "authenticated"; | |
| env.PAPERCLIP_DEPLOYMENT_EXPOSURE = "private"; | |
| env.PAPERCLIP_AUTH_BASE_URL_MODE = "auto"; | |
| env.HOST = "0.0.0.0"; | |
| console.log("[paperclip] dev mode: authenticated/private (tailscale-friendly) on 0.0.0.0"); | |
| } else { | |
| console.log("[paperclip] dev mode: local_trusted (default)"); | |
| } | |
| const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; | |
| function toError(error, context = "Dev runner command failed") { | |
| if (error instanceof Error) return error; | |
| if (error === undefined) return new Error(context); | |
| if (typeof error === "string") return new Error(`${context}: ${error}`); | |
| try { | |
| return new Error(`${context}: ${JSON.stringify(error)}`); | |
| } catch { | |
| return new Error(`${context}: ${String(error)}`); | |
| } | |
| } | |
| process.on("uncaughtException", (error) => { | |
| const err = toError(error, "Uncaught exception in dev runner"); | |
| process.stderr.write(`${err.stack ?? err.message}\n`); | |
| process.exit(1); | |
| }); | |
| process.on("unhandledRejection", (reason) => { | |
| const err = toError(reason, "Unhandled promise rejection in dev runner"); | |
| process.stderr.write(`${err.stack ?? err.message}\n`); | |
| process.exit(1); | |
| }); | |
| function formatPendingMigrationSummary(migrations) { | |
| if (migrations.length === 0) return "none"; | |
| return migrations.length > 3 | |
| ? `${migrations.slice(0, 3).join(", ")} (+${migrations.length - 3} more)` | |
| : migrations.join(", "); | |
| } | |
| async function runPnpm(args, options = {}) { | |
| return await new Promise((resolve, reject) => { | |
| const child = spawn(pnpmBin, args, { | |
| stdio: options.stdio ?? ["ignore", "pipe", "pipe"], | |
| env: options.env ?? process.env, | |
| shell: process.platform === "win32", | |
| }); | |
| let stdoutBuffer = ""; | |
| let stderrBuffer = ""; | |
| if (child.stdout) { | |
| child.stdout.on("data", (chunk) => { | |
| stdoutBuffer += String(chunk); | |
| }); | |
| } | |
| if (child.stderr) { | |
| child.stderr.on("data", (chunk) => { | |
| stderrBuffer += String(chunk); | |
| }); | |
| } | |
| child.on("error", reject); | |
| child.on("exit", (code, signal) => { | |
| resolve({ | |
| code: code ?? 0, | |
| signal, | |
| stdout: stdoutBuffer, | |
| stderr: stderrBuffer, | |
| }); | |
| }); | |
| }); | |
| } | |
| async function maybePreflightMigrations() { | |
| if (mode !== "watch") return; | |
| const status = await runPnpm( | |
| ["--filter", "@paperclipai/db", "exec", "tsx", "src/migration-status.ts", "--json"], | |
| { env }, | |
| ); | |
| if (status.code !== 0) { | |
| process.stderr.write( | |
| status.stderr || | |
| status.stdout || | |
| `[paperclip] Command failed with code ${status.code}: pnpm --filter @paperclipai/db exec tsx src/migration-status.ts --json\n`, | |
| ); | |
| process.exit(status.code); | |
| } | |
| let payload; | |
| try { | |
| payload = JSON.parse(status.stdout.trim()); | |
| } catch (error) { | |
| process.stderr.write( | |
| status.stderr || | |
| status.stdout || | |
| "[paperclip] migration-status returned invalid JSON payload\n", | |
| ); | |
| throw toError(error, "Unable to parse migration-status JSON output"); | |
| } | |
| if (payload.status !== "needsMigrations" || payload.pendingMigrations.length === 0) { | |
| return; | |
| } | |
| const autoApply = env.PAPERCLIP_MIGRATION_AUTO_APPLY === "true"; | |
| let shouldApply = autoApply; | |
| if (!autoApply) { | |
| if (!stdin.isTTY || !stdout.isTTY) { | |
| shouldApply = true; | |
| } else { | |
| const prompt = createInterface({ input: stdin, output: stdout }); | |
| try { | |
| const answer = ( | |
| await prompt.question( | |
| `Apply pending migrations (${formatPendingMigrationSummary(payload.pendingMigrations)}) now? (y/N): `, | |
| ) | |
| ) | |
| .trim() | |
| .toLowerCase(); | |
| shouldApply = answer === "y" || answer === "yes"; | |
| } finally { | |
| prompt.close(); | |
| } | |
| } | |
| } | |
| if (!shouldApply) { | |
| process.stderr.write( | |
| `[paperclip] Pending migrations detected (${formatPendingMigrationSummary(payload.pendingMigrations)}). ` + | |
| "Refusing to start watch mode against a stale schema.\n", | |
| ); | |
| process.exit(1); | |
| } | |
| const migrate = spawn(pnpmBin, ["db:migrate"], { | |
| stdio: "inherit", | |
| env, | |
| shell: process.platform === "win32", | |
| }); | |
| const exit = await new Promise((resolve) => { | |
| migrate.on("exit", (code, signal) => resolve({ code: code ?? 0, signal })); | |
| }); | |
| if (exit.signal) { | |
| process.kill(process.pid, exit.signal); | |
| return; | |
| } | |
| if (exit.code !== 0) { | |
| process.exit(exit.code); | |
| } | |
| } | |
| await maybePreflightMigrations(); | |
| async function buildPluginSdk() { | |
| console.log("[paperclip] building plugin sdk..."); | |
| const result = await runPnpm( | |
| ["--filter", "@paperclipai/plugin-sdk", "build"], | |
| { stdio: "inherit" }, | |
| ); | |
| if (result.signal) { | |
| process.kill(process.pid, result.signal); | |
| return; | |
| } | |
| if (result.code !== 0) { | |
| console.error("[paperclip] plugin sdk build failed"); | |
| process.exit(result.code); | |
| } | |
| } | |
| await buildPluginSdk(); | |
| const serverScript = mode === "watch" ? "dev:watch" : "dev"; | |
| const child = spawn( | |
| pnpmBin, | |
| ["--filter", "@paperclipai/server", serverScript, ...forwardedArgs], | |
| { stdio: "inherit", env, shell: process.platform === "win32" }, | |
| ); | |
| child.on("exit", (code, signal) => { | |
| if (signal) { | |
| process.kill(process.pid, signal); | |
| return; | |
| } | |
| process.exit(code ?? 0); | |
| }); | |