233 lines
6.1 KiB
JavaScript
233 lines
6.1 KiB
JavaScript
#!/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);
|
|
|
|
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);
|
|
});
|