Replace the Python CLI with a Node CLI, add a systemd service command
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>
This commit is contained in:
+32
-13
@@ -1,14 +1,15 @@
|
||||
# Architecture
|
||||
|
||||
```
|
||||
triggershell (Python CLI) app/ (Next.js, all server logic)
|
||||
─────────────────────── ──────────────────────────────
|
||||
triggershell dev|start server.ts (custom Node server)
|
||||
1. resolve + pre-flight the config ├─ Next.js request handler (pages, API routes)
|
||||
2. check node/pnpm, `pnpm install` if stale ├─ ws.WebSocketServer on /ws/runs
|
||||
3. set TRIGGERSHELL_CONFIG_PATH/PORT/HOST env └─ boot: migrate DB, sync auth, reconcile runs
|
||||
4. spawn `pnpm run dev|start`, forward signals
|
||||
5. poll /api/healthz, open browser │
|
||||
triggershell (Node CLI, src/cli) app/ (Next.js, all server logic)
|
||||
─────────────────────────────── ──────────────────────────────
|
||||
triggershell start server.ts (custom Node server)
|
||||
1. resolve + validate the config (loadConfig) ├─ Next.js request handler (pages, API routes)
|
||||
2. check the port is free ├─ ws.WebSocketServer on /ws/runs
|
||||
3. set TRIGGERSHELL_CONFIG_PATH/PORT/HOST/ └─ boot: migrate DB, sync auth, reconcile runs
|
||||
NODE_ENV
|
||||
4. import("./server.ts") — same process │
|
||||
5. poll /api/healthz, open browser │
|
||||
┌──────────┴──────────┐
|
||||
REST API WebSocket
|
||||
(src/app/api/**) (src/lib/ws/server.ts)
|
||||
@@ -28,12 +29,30 @@ request handler in a plain `http.createServer` and attaches a `ws.WebSocketServe
|
||||
`upgrade` event, scoped to `/ws/runs` with its own auth check (Route Handlers get auth via
|
||||
`next/headers`'s `cookies()`, which isn't available on a raw `http.IncomingMessage`).
|
||||
|
||||
## Why the Python CLI is thin
|
||||
## Why the CLI and server share one process
|
||||
|
||||
Everything Node/pnpm/Next.js needs to do (serve pages, run scripts, stream output, enforce auth)
|
||||
is naturally a Node problem — `execa` for argv-safe spawning, `ws` for streaming, Next for the UI.
|
||||
Python's job is just: get Node/pnpm ready, validate the config fast, and manage the child process's
|
||||
lifecycle (signals, readiness, browser launch) — a CLI concern, not a web-server concern.
|
||||
`triggershell start` (`src/cli/commands/start.ts`) doesn't spawn `server.ts` as a child process —
|
||||
it sets `process.env` (`TRIGGERSHELL_CONFIG_PATH`, `PORT`, `HOST`, `NODE_ENV`) and then dynamically
|
||||
`import()`s `server.ts` directly, in the same Node process. `server.ts` reads that env and installs
|
||||
its own `SIGTERM`/`SIGINT` handlers, so once it's imported, `Ctrl-C` or `systemctl stop` just work —
|
||||
there's no parent process relaying signals to a child, no separate lifecycle to manage. The CLI's
|
||||
`bin/triggershell.js` entry point registers `tsx`'s loader once for the whole process
|
||||
(`tsx/esm/api`'s `register()`), so both the CLI's own `.ts` command files and `server.ts` run
|
||||
straight from source, with no compile/bundle step for either.
|
||||
|
||||
## Running as a systemd service
|
||||
|
||||
`triggershell service install` renders a unit file (`src/cli/lib/systemd.ts`) whose `ExecStart`
|
||||
line pins the exact `node` binary (`process.execPath`) and the exact, symlink-resolved path to the
|
||||
installed CLI (`fs.realpathSync(process.argv[1])`) at install time — necessary because systemd
|
||||
services run with a minimal `PATH` that may not include wherever Node actually lives. By default it
|
||||
installs a per-user unit (`~/.config/systemd/user/triggershell.service`, no root required); `--system`
|
||||
targets `/etc/systemd/system/` instead and prints the `sudo` commands to run if not already root.
|
||||
`install` reloads the systemd daemon but does not enable/start the unit itself — that's a separate,
|
||||
explicit `systemctl --user enable --now triggershell`, since it's the point where the service
|
||||
actually starts listening and running scripts. `triggershell service status`/`uninstall` are thin
|
||||
wrappers around `systemctl`; log tailing is just `journalctl --user -u triggershell -f` — not
|
||||
reimplemented.
|
||||
|
||||
## Cross-module-graph state
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ secrets referenced via `${VAR}` don't have to be committed alongside the config.
|
||||
init` scaffolds both files together.
|
||||
|
||||
The canonical schema is the Zod schema at `app/src/lib/config/schema.ts` — this document mirrors
|
||||
it. `triggershell validate` runs the Python pre-flight checks below, then that full schema.
|
||||
it. `triggershell validate` loads and validates the config directly against the schema below (no
|
||||
separate pre-flight step).
|
||||
|
||||
## `server`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user