The app is already 100% Node, so the Python launcher was pure overhead - it existed mainly to bootstrap Node, which is circular. The CLI is now merged into app/ (the single published npm package): `triggershell start` validates the config and imports server.ts directly in-process, so server.ts's own SIGTERM/SIGINT handling just works with no signal-relay/child-process layer needed. `dev` is dropped from the public CLI (contributors use `pnpm --dir app dev` directly); there's no `build` command either, since the package ships a prebuilt `.next` via a `prepack` hook. Adds `triggershell service install|uninstall|status` for running as a per-user or system systemd unit. Also fixes two bugs found while wiring this up: server.ts resolved `.next` relative to `process.cwd()`, which broke once the CLI could run from a directory other than the app itself; and an explicitly-`files`-listed package directory bypasses .npmignore for its subpaths, so `.next/cache` was inflating the npm tarball to ~670MB (now stripped in `prepack`, ~7MB). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import "./src/bootstrap/async-local-storage-polyfill";
|
|
import { createServer } from "node:http";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import next from "next";
|
|
import { WebSocketServer } from "ws";
|
|
import { getConfig } from "./src/lib/config/load";
|
|
import { migrateOnBoot } from "./src/lib/db/client";
|
|
import { syncAuthFromConfig } from "./src/lib/auth/sync";
|
|
import { reconcileOrphanedRuns } from "./src/lib/runner/engine";
|
|
import { killAllRuns } from "./src/lib/runner/registry";
|
|
import { attachWsServer, authenticateUpgrade } from "./src/lib/ws/server";
|
|
|
|
const dev = process.env.NODE_ENV !== "production";
|
|
const { config } = getConfig();
|
|
|
|
const port = Number(process.env.PORT ?? config.server.port);
|
|
const hostname = process.env.HOST ?? config.server.host;
|
|
|
|
migrateOnBoot();
|
|
syncAuthFromConfig();
|
|
reconcileOrphanedRuns();
|
|
|
|
// `dir` must be this file's own directory, not `process.cwd()` - when launched by the installed
|
|
// `triggershell` CLI, the working directory is wherever the user's config lives, not the package.
|
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
const app = next({ dev, dir, hostname, port });
|
|
const handle = app.getRequestHandler();
|
|
|
|
app.prepare().then(() => {
|
|
const nextUpgradeHandler = app.getUpgradeHandler();
|
|
|
|
const httpServer = createServer((req, res) => {
|
|
handle(req, res);
|
|
});
|
|
|
|
const wss = new WebSocketServer({ noServer: true });
|
|
attachWsServer(wss);
|
|
|
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
const { pathname } = new URL(req.url ?? "/", "http://internal");
|
|
if (pathname !== "/ws/runs") {
|
|
// Anything else (e.g. Next's own dev-mode HMR websocket at /_next/hmr) is Next's to handle.
|
|
nextUpgradeHandler(req, socket, head).catch(() => socket.destroy());
|
|
return;
|
|
}
|
|
|
|
authenticateUpgrade(req)
|
|
.then((ok) => {
|
|
if (!ok) {
|
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
wss.emit("connection", ws, req);
|
|
});
|
|
})
|
|
.catch(() => socket.destroy());
|
|
});
|
|
|
|
httpServer.listen(port, hostname, () => {
|
|
console.log(
|
|
`> triggershell ready on http://${hostname}:${port} (${dev ? "development" : "production"})`,
|
|
);
|
|
});
|
|
|
|
const shutdown = (signal: string) => {
|
|
console.log(`> received ${signal}, shutting down...`);
|
|
killAllRuns();
|
|
httpServer.close(() => process.exit(0));
|
|
// Force-exit if graceful shutdown hangs (e.g. a stuck WS connection).
|
|
setTimeout(() => process.exit(1), 5000).unref();
|
|
};
|
|
|
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
});
|