Fix dev migration prompt and embedded db:migrate

This commit is contained in:
Dotta
2026-03-10 15:31:05 -05:00
parent 42c8aca5c0
commit 56aeddfa1c
8 changed files with 701 additions and 16 deletions

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env node
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);
@@ -43,6 +45,121 @@ if (tailscaleAuth) {
}
const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
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;
if (process.env.PAPERCLIP_MIGRATION_PROMPT === "never") 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);
process.exit(status.code);
}
let payload;
try {
payload = JSON.parse(status.stdout.trim());
} catch (error) {
process.stderr.write(status.stderr || status.stdout);
throw error;
}
if (payload.status !== "needsMigrations" || payload.pendingMigrations.length === 0) {
return;
}
const autoApply = process.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) return;
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();
if (mode === "watch") {
env.PAPERCLIP_MIGRATION_PROMPT = "never";
}
const serverScript = mode === "watch" ? "dev:watch" : "dev";
const child = spawn(
pnpmBin,
@@ -57,4 +174,3 @@ child.on("exit", (code, signal) => {
}
process.exit(code ?? 0);
});