Harden local setup and development startup

This commit is contained in:
2026-07-25 17:14:19 -06:00
parent e8d2435cc2
commit feaa882fc3
10 changed files with 221 additions and 46 deletions
+29
View File
@@ -0,0 +1,29 @@
import fs from "node:fs";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const runtimeDir = path.join(projectDir, ".cache", "runtime");
fs.mkdirSync(runtimeDir, { recursive: true });
const preflight = spawnSync(process.execPath, [path.join(projectDir, "scripts", "preflight.mjs"), "--ports=3000,5173"], {
cwd: projectDir,
stdio: "inherit",
shell: false,
});
if (preflight.status !== 0) process.exit(preflight.status ?? 1);
const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
const stdout = fs.openSync(path.join(runtimeDir, "dev.stdout.log"), "a");
const stderr = fs.openSync(path.join(runtimeDir, "dev.stderr.log"), "a");
const child = spawn(process.execPath, [npmCli, "run", "dev"], {
cwd: projectDir,
detached: true,
stdio: ["ignore", stdout, stderr],
shell: false,
windowsHide: true,
});
child.unref();
console.log(`AgendaPro dev iniciado en segundo plano (PID ${child.pid}). Logs: ${path.join(runtimeDir, "dev.stdout.log")}`);
+52
View File
@@ -0,0 +1,52 @@
import net from "node:net";
const MIN_NODE = [22, 5, 0];
const actualNode = process.versions.node.split(".").map(Number);
const isVersionAtLeast = actualNode[0] > MIN_NODE[0]
|| actualNode[0] === MIN_NODE[0] && (actualNode[1] > MIN_NODE[1]
|| actualNode[1] === MIN_NODE[1] && actualNode[2] >= MIN_NODE[2]);
if (!isVersionAtLeast) {
console.error(`AgendaPro requiere Node.js >= ${MIN_NODE.join(".")}; detectado ${process.version}.`);
process.exit(1);
}
try {
await import("node:sqlite");
} catch (error) {
console.error("La instalación de Node no incluye node:sqlite, requerido por AgendaPro.");
console.error(error);
process.exit(1);
}
const portsArg = process.argv.find((arg) => arg.startsWith("--ports="))?.split("=", 2)[1];
const ports = portsArg ? portsArg.split(",").map(Number).filter(Boolean) : [];
function isPortBusy(port) {
return new Promise((resolve) => {
const socket = net.createConnection({ host: "127.0.0.1", port });
let settled = false;
const finish = (busy) => {
if (settled) return;
settled = true;
socket.destroy();
resolve(busy);
};
socket.once("connect", () => finish(true));
socket.once("error", () => finish(false));
socket.setTimeout(500, () => finish(false));
});
}
const busyPorts = [];
for (const port of ports) {
if (await isPortBusy(port)) busyPorts.push(port);
}
if (busyPorts.length) {
console.error(`No se puede iniciar AgendaPro: los puertos ${busyPorts.join(", ")} ya están ocupados.`);
console.error("Cierra el proceso anterior o cambia PORT/VITE_PORT; en Windows puedes localizarlo con: netstat -ano | findstr :<puerto>");
process.exit(1);
}
console.log(`Preflight OK: Node ${process.version}${ports.length ? `; puertos ${ports.join(", ")} disponibles` : ""}.`);
+34
View File
@@ -0,0 +1,34 @@
import os from "node:os";
// tsx creates a temporary directory using os.userInfo(). On some Windows
// runners that syscall fails even though the rest of Node is healthy.
// Keep the normal implementation when it works and provide a safe fallback.
const nativeUserInfo = os.userInfo.bind(os);
const safeUserInfo = (...args) => {
try {
return nativeUserInfo(...args);
} catch {
const username = (process.env.USERNAME || process.env.USER || "agendapro").replace(/[<>:"/\\|?*]/g, "-");
return {
uid: -1,
gid: -1,
username,
homedir: os.homedir(),
shell: null,
};
}
};
try {
os.userInfo = safeUserInfo;
} catch {
Object.defineProperty(os, "userInfo", { configurable: true, writable: true, value: safeUserInfo });
}
try {
await import("tsx/cli");
} catch (error) {
console.error("No se pudo iniciar tsx. Ejecuta primero npm run setup.");
console.error(error);
process.exitCode = 1;
}
+17
View File
@@ -0,0 +1,17 @@
import path from "node:path";
import { spawnSync } from "node:child_process";
const cacheDir = path.resolve(".cache", "npm");
const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
const result = spawnSync(process.execPath, [npmCli, "ci", "--cache", cacheDir], {
stdio: "inherit",
// Invoke npm-cli.js through node so paths with spaces need no shell quoting.
shell: false,
});
if (result.error) {
console.error(`No se pudo ejecutar npm: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);