From c6337ea942146d75e0b0bfe37790c23eac04258a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 16 Aug 2026 13:55:25 +0200 Subject: [PATCH] Run prettier across the repo, exclude the lockfile from it Prettier had never been run in --check mode here before, so this had drifted across most files (markdown tables, long option() chains, line wrapping). Purely formatting, no logic changes - needed so a CI format:check gate can actually pass. Adds .prettierignore for pnpm-lock.yaml specifically: prettier's YAML formatter rewrites every quoted key (single -> double quotes) producing an ~8700-line diff of pure noise on a file pnpm itself owns the formatting of. Co-Authored-By: Claude Sonnet 5 --- .prettierignore | 2 + README.md | 56 ++++++++--------- docs/API.md | 2 +- docs/ARCHITECTURE.md | 10 ++-- docs/CONFIG_REFERENCE.md | 110 +++++++++++++++++----------------- examples/scripts/cleanup.js | 4 +- examples/triggershell.yml | 2 +- src/app/(app)/runs/page.tsx | 26 ++++++-- src/app/layout.tsx | 6 +- src/cli/commands/doctor.ts | 9 ++- src/cli/commands/init.ts | 20 +++++-- src/cli/commands/run.ts | 27 +++++++-- src/cli/commands/scripts.ts | 8 ++- src/cli/commands/service.ts | 33 +++++++--- src/cli/commands/start.ts | 4 +- src/cli/commands/users.ts | 25 ++++++-- src/cli/index.ts | 92 ++++++++++++++++++++++------ src/cli/lib/env-file.test.ts | 5 +- src/cli/lib/env-file.ts | 10 +++- src/cli/lib/network.ts | 12 +++- src/cli/lib/paths.ts | 7 ++- src/cli/lib/systemd.test.ts | 8 ++- src/cli/lib/systemd.ts | 11 +++- src/cli/lib/variables.test.ts | 30 ++++++++-- src/cli/lib/variables.ts | 17 ++++-- src/cli/version.ts | 4 +- src/lib/config/load.ts | 4 +- src/lib/docs.ts | 9 ++- src/lib/runner/events.ts | 3 +- src/lib/runner/registry.ts | 3 +- 30 files changed, 387 insertions(+), 172 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..84c830d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Machine-generated - pnpm owns this file's formatting, not prettier. +pnpm-lock.yaml diff --git a/README.md b/README.md index f1d0cfa..e13c9d9 100644 --- a/README.md +++ b/README.md @@ -99,20 +99,20 @@ the script — always as a discrete argv element or env var, never interpolated ## CLI Usage -| Command | Description | -|---|---| -| `triggershell init [PATH]` | Scaffold a new config file + `.env` (`--port`, `--auth/--no-auth`, `--force`) | -| `triggershell validate [-c CONFIG]` | Validate a config file against the full schema | -| `triggershell start [-c CONFIG] [--port] [--host] [--no-browser]` | Run the web app | -| `triggershell doctor [-c CONFIG]` | Print environment/config diagnostics | -| `triggershell scripts list [-c CONFIG]` | List configured scripts | -| `triggershell scripts show [-c CONFIG]` | Show a script's command and variables | +| Command | Description | +| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `triggershell init [PATH]` | Scaffold a new config file + `.env` (`--port`, `--auth/--no-auth`, `--force`) | +| `triggershell validate [-c CONFIG]` | Validate a config file against the full schema | +| `triggershell start [-c CONFIG] [--port] [--host] [--no-browser]` | Run the web app | +| `triggershell doctor [-c CONFIG]` | Print environment/config diagnostics | +| `triggershell scripts list [-c CONFIG]` | List configured scripts | +| `triggershell scripts show [-c CONFIG]` | Show a script's command and variables | | `triggershell run [--var name=value...] [--token] [--local\|--remote] [--no-wait]` | Run a configured script - through an already-running server's API if one is reachable (so any open browser tab sees it live), otherwise standalone. See [Running scripts from the CLI](#running-scripts-from-the-cli). | -| `triggershell users add [-c CONFIG] [--inline]` | Hash a password, store it in `.env`, and print a `${VAR}` snippet for `auth.users` (`--inline` prints the raw hash instead) | -| `triggershell users add-token [-c CONFIG] [--inline]` | Generate an API token, store its hash in `.env`, and print a `${VAR}` snippet for `auth.tokens` (`--inline` prints the raw hash instead) | -| `triggershell service install [--system]` | Install a systemd unit that runs `triggershell start` (per-user by default, Linux only) | -| `triggershell service uninstall [--system]` | Stop, disable, and remove the systemd unit | -| `triggershell service status [--system]` | Show the systemd unit's status | +| `triggershell users add [-c CONFIG] [--inline]` | Hash a password, store it in `.env`, and print a `${VAR}` snippet for `auth.users` (`--inline` prints the raw hash instead) | +| `triggershell users add-token [-c CONFIG] [--inline]` | Generate an API token, store its hash in `.env`, and print a `${VAR}` snippet for `auth.tokens` (`--inline` prints the raw hash instead) | +| `triggershell service install [--system]` | Install a systemd unit that runs `triggershell start` (per-user by default, Linux only) | +| `triggershell service uninstall [--system]` | Stop, disable, and remove the systemd unit | +| `triggershell service status [--system]` | Show the systemd unit's status | ### Running scripts from the CLI @@ -154,20 +154,20 @@ Passwords are hashed with argon2id; only the hash ever lives in the config file. Full reference with request/response shapes and curl examples: [`docs/API.md`](docs/API.md). -| Method | Path | Notes | -|---|---|---| -| GET | `/api/healthz` | Unauthenticated readiness probe | -| POST | `/api/auth/login` | `{username, password}` → sets session cookie | -| POST | `/api/auth/logout` | Clears the session | -| GET | `/api/auth/session` | Current auth state | -| GET | `/api/scripts` | List configured scripts | -| GET | `/api/scripts/:scriptId` | Full script schema (variables, UI hints) | -| POST | `/api/scripts/:scriptId/runs` | Start a run: `{variables: {...}}` | -| GET | `/api/runs` | History: `?scriptId=&status=&limit=&cursor=` | -| GET | `/api/runs/:runId` | Single run's status/metadata | -| POST | `/api/runs/:runId/cancel` | Cancel an active run | -| GET | `/api/runs/:runId/logs` | Full or tailed log output (`?tail=&download=`) | -| WS | `/ws/runs` | Subscribe to a run's live output/status; send `cancel` | +| Method | Path | Notes | +| ------ | ----------------------------- | ------------------------------------------------------ | +| GET | `/api/healthz` | Unauthenticated readiness probe | +| POST | `/api/auth/login` | `{username, password}` → sets session cookie | +| POST | `/api/auth/logout` | Clears the session | +| GET | `/api/auth/session` | Current auth state | +| GET | `/api/scripts` | List configured scripts | +| GET | `/api/scripts/:scriptId` | Full script schema (variables, UI hints) | +| POST | `/api/scripts/:scriptId/runs` | Start a run: `{variables: {...}}` | +| GET | `/api/runs` | History: `?scriptId=&status=&limit=&cursor=` | +| GET | `/api/runs/:runId` | Single run's status/metadata | +| POST | `/api/runs/:runId/cancel` | Cancel an active run | +| GET | `/api/runs/:runId/logs` | Full or tailed log output (`?tail=&download=`) | +| WS | `/ws/runs` | Subscribe to a run's live output/status; send `cancel` | ## Development @@ -199,7 +199,7 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the pieces fit togeth - Scripts are always spawned with an argv array (`execa`), never a shell string — variable values can never inject additional shell commands. A script's own `command`/`args` may still use `shell: - true` as an explicit, documented opt-in when the script genuinely needs pipes/globs; that +true` as an explicit, documented opt-in when the script genuinely needs pipes/globs; that reintroduces shell interpretation of `passAs: arg` values, so prefer `passAs: env` for anything user-controlled in that case. - `secret: true` variables are masked in the UI and redacted from persisted run records; only the diff --git a/docs/API.md b/docs/API.md index 317e9ba..d64944a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -77,7 +77,7 @@ code). `404` if not found. ### `POST /api/runs/:runId/cancel` -`202 {"status": "cancelling"}`. `409` if the run already finished, or if it isn't tracked by *this* +`202 {"status": "cancelling"}`. `409` if the run already finished, or if it isn't tracked by _this_ server process (e.g. after a restart — see "orphaned runs" in `docs/ARCHITECTURE.md`). ### `GET /api/runs/:runId/logs` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1197cfd..693f4e3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -46,8 +46,8 @@ straight from source, with no compile/bundle step for either. logic in `src/lib/runner/engine.ts` - it just calls it from a different position: - If a server is reachable (`GET /api/healthz`), `run` is a plain HTTP+WS client: `POST - /api/scripts/:id/runs` (the same route the web UI's "Run" button calls) starts the run *inside - that server's process*, and `run` then subscribes over `/ws/runs` exactly like a browser tab +/api/scripts/:id/runs` (the same route the web UI's "Run" button calls) starts the run _inside + that server's process_, and `run` then subscribes over `/ws/runs` exactly like a browser tab would, using the same `ClientMessage`/`ServerMessage` protocol (`src/lib/ws/protocol.ts`). This is why a run started this way appears live in any open browser tab for free - the broadcast path (`emitRunMessage` → the `runEvents` listener in `src/lib/ws/server.ts` → every subscribed @@ -81,7 +81,7 @@ reimplemented. ## Cross-module-graph state Next compiles Route Handlers and Server Components through its own build/module graph, which is a -*separate* module instantiation from whatever `server.ts` imports directly via `tsx` at startup — +_separate_ module instantiation from whatever `server.ts` imports directly via `tsx` at startup — even though both run in the same OS process. A plain module-level singleton (e.g. `new Map()` at the top of a file) ends up duplicated, one copy per graph, which silently breaks anything that needs to be shared across that boundary (the WebSocket subscriber registry, the live-run-handle @@ -110,10 +110,10 @@ queued → running → succeeded | failed | cancelled | timed_out ## Auth - Session: `iron-session` — a stateless, encrypted+signed cookie (no session-store table). -- Config is the source of truth for *who* is allowed in; `src/lib/auth/sync.ts` upserts config +- Config is the source of truth for _who_ is allowed in; `src/lib/auth/sync.ts` upserts config users/tokens into SQLite on boot, giving a single DB-backed check path plus `lastLoginAt` tracking. - `src/proxy.ts` (Next's Proxy, formerly "Middleware") does a fast, cookie-only redirect for - unauthenticated page/API requests — explicitly *not* the real security boundary. Every Route + unauthenticated page/API requests — explicitly _not_ the real security boundary. Every Route Handler also calls `requireAuth()` itself; this is the actual auth check. - The WS `upgrade` handler is outside Next's request pipeline entirely, so it authenticates by hand (parsing the cookie header, or a `?token=` query param) via `authenticateUpgrade()`. diff --git a/docs/CONFIG_REFERENCE.md b/docs/CONFIG_REFERENCE.md index dd048ec..213114c 100644 --- a/docs/CONFIG_REFERENCE.md +++ b/docs/CONFIG_REFERENCE.md @@ -16,21 +16,21 @@ separate pre-flight step). ## `server` -| Field | Type | Default | Notes | -|---|---|---|---| -| `host` | string | `127.0.0.1` | Bind address | -| `port` | number | `4173` | 1-65535 | -| `basePath` | string | `""` | Reserved for future use | +| Field | Type | Default | Notes | +| ---------- | ------ | ----------- | ----------------------- | +| `host` | string | `127.0.0.1` | Bind address | +| `port` | number | `4173` | 1-65535 | +| `basePath` | string | `""` | Reserved for future use | ## `auth` -| Field | Type | Default | Notes | -|---|---|---|---| -| `enabled` | boolean | `true` | `false` disables login entirely | -| `sessionSecret` | string | — | Required, >= 32 chars, if `enabled`. Reference it via `${TRIGGERSHELL_SESSION_SECRET}` and set the real value in `.env`, not here | -| `sessionTtlHours` | number | `12` | Session cookie lifetime | -| `users` | array | `[]` | `{username, passwordHash}` — generate via `triggershell users add` | -| `tokens` | array | `[]` | `{name, tokenHash}` — generate via `triggershell users add-token` | +| Field | Type | Default | Notes | +| ----------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | boolean | `true` | `false` disables login entirely | +| `sessionSecret` | string | — | Required, >= 32 chars, if `enabled`. Reference it via `${TRIGGERSHELL_SESSION_SECRET}` and set the real value in `.env`, not here | +| `sessionTtlHours` | number | `12` | Session cookie lifetime | +| `users` | array | `[]` | `{username, passwordHash}` — generate via `triggershell users add` | +| `tokens` | array | `[]` | `{name, tokenHash}` — generate via `triggershell users add-token` | If `enabled: true`, at least one user or token must be configured. @@ -42,67 +42,67 @@ pass `--inline` to those commands to get the raw hash printed for pasting into t ## `database` -| Field | Type | Default | -|---|---|---| +| Field | Type | Default | +| ------ | ------ | ------------------------------- | | `path` | string | `.triggershell/triggershell.db` | ## `logs` -| Field | Type | Default | Notes | -|---|---|---|---| -| `dir` | string | `.triggershell/logs` | One `.log` file per run | -| `retentionDays` | number | `30` | Not yet enforced automatically — prune manually or via cron | +| Field | Type | Default | Notes | +| --------------- | ------ | -------------------- | ----------------------------------------------------------- | +| `dir` | string | `.triggershell/logs` | One `.log` file per run | +| `retentionDays` | number | `30` | Not yet enforced automatically — prune manually or via cron | ## `scripts[]` -| Field | Type | Default | Notes | -|---|---|---|---| -| `id` | string | — | Required, unique, `[a-zA-Z0-9][a-zA-Z0-9_-]*` | -| `name` | string | — | Required, display name | -| `description` | string | — | Optional | -| `command` | string | — | Required, e.g. `bash`, `node`, `./script.sh` | -| `args` | string[] | `[]` | Fixed leading args, before variable-derived ones | -| `cwd` | string | `./` | Resolved relative to the config file's directory | -| `shell` | boolean | `false` | Opt-in shell interpretation — see the Security Notes in the README before using this | -| `timeoutSeconds` | number | `1800` | 0–86400. `0` means no timeout - the run is never killed for taking too long | -| `variables` | array | `[]` | See below | +| Field | Type | Default | Notes | +| ---------------- | -------- | ------- | ------------------------------------------------------------------------------------ | +| `id` | string | — | Required, unique, `[a-zA-Z0-9][a-zA-Z0-9_-]*` | +| `name` | string | — | Required, display name | +| `description` | string | — | Optional | +| `command` | string | — | Required, e.g. `bash`, `node`, `./script.sh` | +| `args` | string[] | `[]` | Fixed leading args, before variable-derived ones | +| `cwd` | string | `./` | Resolved relative to the config file's directory | +| `shell` | boolean | `false` | Opt-in shell interpretation — see the Security Notes in the README before using this | +| `timeoutSeconds` | number | `1800` | 0–86400. `0` means no timeout - the run is never killed for taking too long | +| `variables` | array | `[]` | See below | ## `scripts[].variables[]` Common fields on every variable: -| Field | Type | Default | Notes | -|---|---|---|---| -| `name` | string | — | Required, unique per script | -| `label` | string | `name` | Display label | -| `description` | string | — | Shown as form help text | -| `required` | boolean | `false` | | -| `secret` | boolean | `false` | Only valid on `type: string`. Masks the UI control, redacts from persisted run records | -| `control` | string | type-based default | See mapping below | -| `passAs` | `arg` \| `flag` \| `env` \| `stdin` | `arg` | How the value reaches the process | -| `argName` | string | — | Required for `passAs: arg`/`flag`, e.g. `--env` | -| `envName` | string | — | Required for `passAs: env`, e.g. `SLACK_CHANNEL` | -| `joinWith` | string | `,` | Separator used when an array value is passed as a single arg/env string | +| Field | Type | Default | Notes | +| ------------- | ----------------------------------- | ------------------ | -------------------------------------------------------------------------------------- | +| `name` | string | — | Required, unique per script | +| `label` | string | `name` | Display label | +| `description` | string | — | Shown as form help text | +| `required` | boolean | `false` | | +| `secret` | boolean | `false` | Only valid on `type: string`. Masks the UI control, redacts from persisted run records | +| `control` | string | type-based default | See mapping below | +| `passAs` | `arg` \| `flag` \| `env` \| `stdin` | `arg` | How the value reaches the process | +| `argName` | string | — | Required for `passAs: arg`/`flag`, e.g. `--env` | +| `envName` | string | — | Required for `passAs: env`, e.g. `SLACK_CHANNEL` | +| `joinWith` | string | `,` | Separator used when an array value is passed as a single arg/env string | Type-specific fields: -| `type` | Extra fields | -|---|---| -| `string` | `default?: string`, `pattern?: string` (regex), `minLength?`, `maxLength?`, `multiline?: boolean` | -| `number` | `default?: number`, `min?`, `max?`, `step?` | -| `boolean` | `default: boolean` (default `false`) | -| `enum` | `choices: string[]` (required, non-empty), `default?: string` | -| `multiselect` | `choices: string[]` (required, non-empty), `default: string[]` (default `[]`) | +| `type` | Extra fields | +| ------------- | ------------------------------------------------------------------------------------------------- | +| `string` | `default?: string`, `pattern?: string` (regex), `minLength?`, `maxLength?`, `multiline?: boolean` | +| `number` | `default?: number`, `min?`, `max?`, `step?` | +| `boolean` | `default: boolean` (default `false`) | +| `enum` | `choices: string[]` (required, non-empty), `default?: string` | +| `multiselect` | `choices: string[]` (required, non-empty), `default: string[]` (default `[]`) | ### UI control mapping -| `type` | Default `control` | Valid overrides | -|---|---|---| -| `string` | `text` (or `password` if `secret: true`) | `textarea` (needs `multiline: true`), `password` | -| `number` | `number` | `slider` (requires both `min` and `max`) | -| `boolean` | `checkbox` | `switch` | -| `enum` | `select` | `radio` | -| `multiselect` | `multiselect` (combobox) | `checkboxGroup` | +| `type` | Default `control` | Valid overrides | +| ------------- | ---------------------------------------- | ------------------------------------------------ | +| `string` | `text` (or `password` if `secret: true`) | `textarea` (needs `multiline: true`), `password` | +| `number` | `number` | `slider` (requires both `min` and `max`) | +| `boolean` | `checkbox` | `switch` | +| `enum` | `select` | `radio` | +| `multiselect` | `multiselect` (combobox) | `checkboxGroup` | ### `passAs` semantics diff --git a/examples/scripts/cleanup.js b/examples/scripts/cleanup.js index ebba09b..67cd1e0 100644 --- a/examples/scripts/cleanup.js +++ b/examples/scripts/cleanup.js @@ -10,7 +10,9 @@ const targetDir = getArg("--dir"); const olderThanDays = getArg("--days"); const types = getArg("--types"); -console.log(`Scanning ${targetDir} for files older than ${olderThanDays} days (types: ${types})`); +console.log( + `Scanning ${targetDir} for files older than ${olderThanDays} days (types: ${types})`, +); if (process.env.CLEANUP_API_KEY) { console.log("Using configured external API key."); } diff --git a/examples/triggershell.yml b/examples/triggershell.yml index 51bbade..3a67308 100644 --- a/examples/triggershell.yml +++ b/examples/triggershell.yml @@ -72,7 +72,7 @@ scripts: label: Notify Slack Channel type: string required: false - pattern: '^#[a-z0-9-]+$' + pattern: "^#[a-z0-9-]+$" passAs: env envName: SLACK_CHANNEL diff --git a/src/app/(app)/runs/page.tsx b/src/app/(app)/runs/page.tsx index d313b67..ed19bb4 100644 --- a/src/app/(app)/runs/page.tsx +++ b/src/app/(app)/runs/page.tsx @@ -2,11 +2,20 @@ export const dynamic = "force-dynamic"; import type { Metadata } from "next"; import Link from "next/link"; -import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight } from "lucide-react"; +import { + ArrowDown, + ArrowUp, + ArrowUpDown, + ChevronLeft, + ChevronRight, +} from "lucide-react"; import { and, asc, desc, eq, like, or, sql, type SQL } from "drizzle-orm"; import { getDb } from "@/lib/db/client"; import { runs } from "@/lib/db/schema"; -import { RunStatusBadge, runStatusLabels } from "@/components/runs/run-status-badge"; +import { + RunStatusBadge, + runStatusLabels, +} from "@/components/runs/run-status-badge"; import { RunsToolbar } from "@/components/runs/runs-toolbar"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; @@ -89,7 +98,9 @@ export default async function RunsPage({ searchParams }: RunsPageProps) { ); } if (statusFilter !== "all" && statusFilter in runStatusLabels) { - conditions.push(eq(runs.status, statusFilter as keyof typeof runStatusLabels)); + conditions.push( + eq(runs.status, statusFilter as keyof typeof runStatusLabels), + ); } if (scriptFilter !== "all") { conditions.push(eq(runs.scriptId, scriptFilter)); @@ -167,7 +178,10 @@ export default async function RunsPage({ searchParams }: RunsPageProps) { {rows.length === 0 ? (

- {total === 0 && !search && statusFilter === "all" && scriptFilter === "all" + {total === 0 && + !search && + statusFilter === "all" && + scriptFilter === "all" ? "No runs yet." : "No runs match these filters."}

@@ -179,7 +193,9 @@ export default async function RunsPage({ searchParams }: RunsPageProps) { {sortHeader("script", "Script")} {sortHeader("status", "Status")} - {sortHeader("triggeredBy", "Triggered by")} + + {sortHeader("triggeredBy", "Triggered by")} + {sortHeader("started", "Started")} {sortHeader("duration", "Duration")} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4bf2910..d0ceec7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,9 @@ import type { Metadata } from "next"; -import { Bricolage_Grotesque, IBM_Plex_Mono, IBM_Plex_Sans } from "next/font/google"; +import { + Bricolage_Grotesque, + IBM_Plex_Mono, + IBM_Plex_Sans, +} from "next/font/google"; import { ThemeProvider } from "next-themes"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Toaster } from "@/components/ui/sonner"; diff --git a/src/cli/commands/doctor.ts b/src/cli/commands/doctor.ts index bbd300e..59c4ce5 100644 --- a/src/cli/commands/doctor.ts +++ b/src/cli/commands/doctor.ts @@ -14,7 +14,10 @@ export async function doctorCommand(opts: DoctorOptions): Promise { const configPath = resolveConfigPath(opts.config); const exists = fs.existsSync(configPath); - rows.push(["Config path", `${configPath} ${exists ? "(exists)" : "(not found)"}`]); + rows.push([ + "Config path", + `${configPath} ${exists ? "(exists)" : "(not found)"}`, + ]); if (exists) { try { @@ -22,7 +25,9 @@ export async function doctorCommand(opts: DoctorOptions): Promise { const portFree = await isPortFree(config.server.host, config.server.port); rows.push([ "Port available", - portFree ? "yes" : `no (${config.server.host}:${config.server.port} in use)`, + portFree + ? "yes" + : `no (${config.server.host}:${config.server.port} in use)`, ]); rows.push(["Scripts configured", String(config.scripts.length)]); rows.push(["Auth enabled", String(config.auth.enabled)]); diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 65e2604..08873d7 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -11,7 +11,10 @@ export interface InitOptions { force: boolean; } -export async function initCommand(targetPath: string | undefined, opts: InitOptions): Promise { +export async function initCommand( + targetPath: string | undefined, + opts: InitOptions, +): Promise { const targetDir = path.resolve(process.cwd(), targetPath ?? "."); fs.mkdirSync(targetDir, { recursive: true }); const configPath = path.join(targetDir, DEFAULT_CONFIG_NAME); @@ -22,7 +25,11 @@ export async function initCommand(targetPath: string | undefined, opts: InitOpti return; } - const templatePath = path.join(resolveAppRoot(), "templates", DEFAULT_CONFIG_NAME); + const templatePath = path.join( + resolveAppRoot(), + "templates", + DEFAULT_CONFIG_NAME, + ); const template = fs.readFileSync(templatePath, "utf-8"); const rendered = template .replace("__PORT__", String(opts.port)) @@ -38,10 +45,15 @@ export async function initCommand(targetPath: string | undefined, opts: InitOpti ); } else { const sessionSecret = crypto.randomBytes(32).toString("hex"); - fs.writeFileSync(envPath, `TRIGGERSHELL_SESSION_SECRET=${sessionSecret}\n`); + fs.writeFileSync( + envPath, + `TRIGGERSHELL_SESSION_SECRET=${sessionSecret}\n`, + ); console.log(`Created ${envPath} (keep this out of version control)`); } - console.log("\nAuth is enabled but no users are configured yet. Add one with:"); + console.log( + "\nAuth is enabled but no users are configured yet. Add one with:", + ); console.log(` triggershell users add --config ${configPath}`); } diff --git a/src/cli/commands/run.ts b/src/cli/commands/run.ts index fc3fd83..e613070 100644 --- a/src/cli/commands/run.ts +++ b/src/cli/commands/run.ts @@ -33,7 +33,10 @@ function exitCodeFor(status: RunStatus): number { return status === "succeeded" ? 0 : 1; } -export async function runCommand(scriptId: string, opts: RunOptions): Promise { +export async function runCommand( + scriptId: string, + opts: RunOptions, +): Promise { const configPath = resolveConfigPath(opts.config); loadDotenv(path.join(path.dirname(configPath), ".env")); @@ -161,7 +164,10 @@ async function runLocal( if (message.runId !== runId) return; if (message.type === "output") { process.stdout.write(message.chunk); - } else if (message.type === "status" && !NON_TERMINAL.includes(message.status)) { + } else if ( + message.type === "status" && + !NON_TERMINAL.includes(message.status) + ) { cleanup(); resolve(message.status); } @@ -187,7 +193,9 @@ async function runRemote( token: string | undefined, wait: boolean, ): Promise { - const headers: Record = { "Content-Type": "application/json" }; + const headers: Record = { + "Content-Type": "application/json", + }; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(`${url}/api/scripts/${scriptId}/runs`, { @@ -197,7 +205,9 @@ async function runRemote( }); if (!response.ok) { - const body = await response.json().catch(() => ({}) as Record); + const body = await response + .json() + .catch(() => ({}) as Record); if (response.status === 401) { console.error( "Unauthorized - pass --token or set TRIGGERSHELL_API_TOKEN (see `triggershell users add-token`).", @@ -210,7 +220,9 @@ async function runRemote( console.error(` ${field}: ${issues.join(", ")}`); } } else { - console.error((body.error as string) ?? `Request failed (${response.status})`); + console.error( + (body.error as string) ?? `Request failed (${response.status})`, + ); } process.exitCode = 1; return; @@ -252,7 +264,10 @@ async function runRemote( if (message.runId !== runId) return; if (message.type === "output") { process.stdout.write(message.chunk); - } else if (message.type === "status" && !NON_TERMINAL.includes(message.status)) { + } else if ( + message.type === "status" && + !NON_TERMINAL.includes(message.status) + ) { cleanup(); resolve(message.status); } else if (message.type === "error") { diff --git a/src/cli/commands/scripts.ts b/src/cli/commands/scripts.ts index a891c22..9bedb28 100644 --- a/src/cli/commands/scripts.ts +++ b/src/cli/commands/scripts.ts @@ -52,7 +52,9 @@ export async function scriptsShowCommand( if (!script) { console.error(`No script '${scriptId}' configured.`); if (scripts.length > 0) { - console.error(`Available scripts: ${scripts.map((s) => s.id).join(", ")}`); + console.error( + `Available scripts: ${scripts.map((s) => s.id).join(", ")}`, + ); } process.exitCode = 1; return; @@ -60,7 +62,9 @@ export async function scriptsShowCommand( console.log(script.name); if (script.description) console.log(script.description); - console.log(`\ncommand: ${script.command} ${script.args.join(" ")}`.trimEnd()); + console.log( + `\ncommand: ${script.command} ${script.args.join(" ")}`.trimEnd(), + ); if (script.variables.length === 0) { console.log("\nThis script takes no parameters."); diff --git a/src/cli/commands/service.ts b/src/cli/commands/service.ts index 32a6133..d62b45c 100644 --- a/src/cli/commands/service.ts +++ b/src/cli/commands/service.ts @@ -26,7 +26,9 @@ function scopeOf(opts: ServiceScopeOptions): "user" | "system" { return opts.system ? "system" : "user"; } -export async function serviceInstallCommand(opts: ServiceInstallOptions): Promise { +export async function serviceInstallCommand( + opts: ServiceInstallOptions, +): Promise { const scope = scopeOf(opts); const configPath = resolveConfigPath(opts.config); const unit = renderUnit({ @@ -68,18 +70,24 @@ export async function serviceInstallCommand(opts: ServiceInstallOptions): Promis const scratchPath = path.join(os.tmpdir(), `${SERVICE_NAME}.service`); fs.writeFileSync(scratchPath, unit); - console.log(`Not running as root - wrote the unit file to ${scratchPath} instead.`); + console.log( + `Not running as root - wrote the unit file to ${scratchPath} instead.`, + ); console.log("\nReview it, then run:"); console.log(` sudo install -m 644 ${scratchPath} ${systemUnitPath()}`); console.log(" sudo systemctl daemon-reload"); console.log(` sudo systemctl enable --now ${SERVICE_NAME}`); } -export async function serviceUninstallCommand(opts: ServiceScopeOptions): Promise { +export async function serviceUninstallCommand( + opts: ServiceScopeOptions, +): Promise { const scope = scopeOf(opts); if (scope === "user") { - await execa("systemctl", ["--user", "disable", "--now", SERVICE_NAME], { reject: false }); + await execa("systemctl", ["--user", "disable", "--now", SERVICE_NAME], { + reject: false, + }); const unitPath = userUnitPath(); if (fs.existsSync(unitPath)) fs.rmSync(unitPath); await execa("systemctl", ["--user", "daemon-reload"], { reject: false }); @@ -88,7 +96,9 @@ export async function serviceUninstallCommand(opts: ServiceScopeOptions): Promis } if (isRoot()) { - await execa("systemctl", ["disable", "--now", SERVICE_NAME], { reject: false }); + await execa("systemctl", ["disable", "--now", SERVICE_NAME], { + reject: false, + }); const unitPath = systemUnitPath(); if (fs.existsSync(unitPath)) fs.rmSync(unitPath); await execa("systemctl", ["daemon-reload"], { reject: false }); @@ -102,10 +112,17 @@ export async function serviceUninstallCommand(opts: ServiceScopeOptions): Promis console.log(" sudo systemctl daemon-reload"); } -export async function serviceStatusCommand(opts: ServiceScopeOptions): Promise { +export async function serviceStatusCommand( + opts: ServiceScopeOptions, +): Promise { const scope = scopeOf(opts); const args = - scope === "user" ? ["--user", "status", SERVICE_NAME] : ["status", SERVICE_NAME]; - const result = await execa("systemctl", args, { stdio: "inherit", reject: false }); + scope === "user" + ? ["--user", "status", SERVICE_NAME] + : ["status", SERVICE_NAME]; + const result = await execa("systemctl", args, { + stdio: "inherit", + reject: false, + }); process.exitCode = result.exitCode ?? 1; } diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 1d1bc15..b213df3 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -54,6 +54,8 @@ export async function startCommand(opts: StartOptions): Promise { }); } - const serverEntry = pathToFileURL(path.join(resolveAppRoot(), "server.ts")).href; + const serverEntry = pathToFileURL( + path.join(resolveAppRoot(), "server.ts"), + ).href; await import(serverEntry); } diff --git a/src/cli/commands/users.ts b/src/cli/commands/users.ts index 7151f0e..a13e846 100644 --- a/src/cli/commands/users.ts +++ b/src/cli/commands/users.ts @@ -20,18 +20,27 @@ function printSnippet(heading: string, entry: Record): void { async function promptNewPassword(): Promise { for (;;) { const first = await promptPassword({ message: "Password", mask: true }); - const second = await promptPassword({ message: "Confirm password", mask: true }); + const second = await promptPassword({ + message: "Confirm password", + mask: true, + }); if (first === second) return first; console.error("Passwords did not match, try again.\n"); } } -export async function usersAddCommand(username: string, opts: UsersOptions): Promise { +export async function usersAddCommand( + username: string, + opts: UsersOptions, +): Promise { const password = await promptNewPassword(); const passwordHash = await hashPassword(password); if (opts.inline) { - printSnippet("Add this under `auth.users:` in your config file:", { username, passwordHash }); + printSnippet("Add this under `auth.users:` in your config file:", { + username, + passwordHash, + }); return; } @@ -47,7 +56,10 @@ export async function usersAddCommand(username: string, opts: UsersOptions): Pro }); } -export async function usersAddTokenCommand(name: string, opts: UsersOptions): Promise { +export async function usersAddTokenCommand( + name: string, + opts: UsersOptions, +): Promise { const token = crypto.randomBytes(32).toString("hex"); const tokenHash = `sha256:${crypto.createHash("sha256").update(token).digest("hex")}`; @@ -56,7 +68,10 @@ export async function usersAddTokenCommand(name: string, opts: UsersOptions): Pr console.log(`Use it as: Authorization: Bearer ${token}`); if (opts.inline) { - printSnippet("Add this under `auth.tokens:` in your config file:", { name, tokenHash }); + printSnippet("Add this under `auth.tokens:` in your config file:", { + name, + tokenHash, + }); return; } diff --git a/src/cli/index.ts b/src/cli/index.ts index 8d0b9a3..961b80f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,7 +3,11 @@ import { doctorCommand } from "./commands/doctor"; import { initCommand } from "./commands/init"; import { runCommand } from "./commands/run"; import { scriptsListCommand, scriptsShowCommand } from "./commands/scripts"; -import { serviceInstallCommand, serviceStatusCommand, serviceUninstallCommand } from "./commands/service"; +import { + serviceInstallCommand, + serviceStatusCommand, + serviceUninstallCommand, +} from "./commands/service"; import { startCommand } from "./commands/start"; import { usersAddCommand, usersAddTokenCommand } from "./commands/users"; import { validateCommand } from "./commands/validate"; @@ -15,12 +19,19 @@ function collect(value: string, previous: string[]): string[] { const program = new Command("triggershell") .version(getVersion()) - .description("Launch the TriggerShell web app: run your configured shell scripts from a browser."); + .description( + "Launch the TriggerShell web app: run your configured shell scripts from a browser.", + ); program .command("init [path]") .description("Scaffold a new triggershell.yml (and .env, if auth is enabled)") - .option("--port ", "Port the web app will listen on.", (v) => Number(v), 4173) + .option( + "--port ", + "Port the web app will listen on.", + (v) => Number(v), + 4173, + ) .option("--no-auth", "Disable built-in login for the web app.") .option("--force", "Overwrite an existing config file.", false) .action(initCommand); @@ -35,7 +46,9 @@ program .command("start") .description("Run the web app in production mode.") .option("-c, --config ", "Path to the config file.") - .option("--port ", "Override the port from the config file.", (v) => Number(v)) + .option("--port ", "Override the port from the config file.", (v) => + Number(v), + ) .option("--host ", "Override the host from the config file.") .option("--no-browser", "Don't open a browser automatically.") .action(startCommand); @@ -46,7 +59,9 @@ program .option("-c, --config ", "Path to the config file.") .action(doctorCommand); -const scripts = program.command("scripts").description("List and inspect configured scripts."); +const scripts = program + .command("scripts") + .description("List and inspect configured scripts."); scripts .command("list") @@ -71,57 +86,98 @@ program [], ) .option("--host ", "Override the host from the config file.") - .option("--port ", "Override the port from the config file.", (v) => Number(v)) + .option("--port ", "Override the port from the config file.", (v) => + Number(v), + ) .option( "--token ", "API token for an already-running server (or set TRIGGERSHELL_API_TOKEN).", ) - .option("--local", "Always run in this process, even if the web server is reachable.", false) + .option( + "--local", + "Always run in this process, even if the web server is reachable.", + false, + ) .option( "--remote", "Require a reachable web server; don't fall back to running locally.", false, ) - .option("--no-wait", "Print the run ID and exit immediately instead of streaming output.") + .option( + "--no-wait", + "Print the run ID and exit immediately instead of streaming output.", + ) .action(runCommand); -const users = program.command("users").description("Manage auth users and API tokens defined in your config file."); +const users = program + .command("users") + .description("Manage auth users and API tokens defined in your config file."); users .command("add ") .description("Hash a password with argon2id and wire it up for auth.users.") - .option("-c, --config ", "Path to the config file (used to locate .env).") - .option("--inline", "Print the raw hash to paste into the config instead of storing it in .env.", false) + .option( + "-c, --config ", + "Path to the config file (used to locate .env).", + ) + .option( + "--inline", + "Print the raw hash to paste into the config instead of storing it in .env.", + false, + ) .action(usersAddCommand); users .command("add-token ") .description("Generate an API token and wire its hash up for auth.tokens.") - .option("-c, --config ", "Path to the config file (used to locate .env).") - .option("--inline", "Print the raw hash to paste into the config instead of storing it in .env.", false) + .option( + "-c, --config ", + "Path to the config file (used to locate .env).", + ) + .option( + "--inline", + "Print the raw hash to paste into the config instead of storing it in .env.", + false, + ) .action(usersAddTokenCommand); -const service = program.command("service").description("Manage the systemd service (Linux only)."); +const service = program + .command("service") + .description("Manage the systemd service (Linux only)."); service .command("install") .description("Install a systemd unit that runs `triggershell start`.") .option("-c, --config ", "Path to the config file.") - .option("--port ", "Override the port from the config file.", (v) => Number(v)) + .option("--port ", "Override the port from the config file.", (v) => + Number(v), + ) .option("--host ", "Override the host from the config file.") - .option("--system", "Install a system-wide unit instead of a per-user one.", false) + .option( + "--system", + "Install a system-wide unit instead of a per-user one.", + false, + ) .action(serviceInstallCommand); service .command("uninstall") .description("Stop, disable, and remove the systemd unit.") - .option("--system", "Target the system-wide unit instead of the per-user one.", false) + .option( + "--system", + "Target the system-wide unit instead of the per-user one.", + false, + ) .action(serviceUninstallCommand); service .command("status") .description("Show the systemd unit's status.") - .option("--system", "Target the system-wide unit instead of the per-user one.", false) + .option( + "--system", + "Target the system-wide unit instead of the per-user one.", + false, + ) .action(serviceStatusCommand); if (process.argv.length <= 2) { diff --git a/src/cli/lib/env-file.test.ts b/src/cli/lib/env-file.test.ts index 3ef5523..dde0e24 100644 --- a/src/cli/lib/env-file.test.ts +++ b/src/cli/lib/env-file.test.ts @@ -6,7 +6,10 @@ import { test } from "node:test"; import { loadDotenv, upsertEnvVar } from "./env-file"; function tmpEnvPath(): string { - return path.join(fs.mkdtempSync(path.join(os.tmpdir(), "triggershell-env-")), ".env"); + return path.join( + fs.mkdtempSync(path.join(os.tmpdir(), "triggershell-env-")), + ".env", + ); } test("upsertEnvVar appends a new key", () => { diff --git a/src/cli/lib/env-file.ts b/src/cli/lib/env-file.ts index 59c87e3..2e0dc0a 100644 --- a/src/cli/lib/env-file.ts +++ b/src/cli/lib/env-file.ts @@ -20,13 +20,19 @@ function parseEnvLines(content: string): EnvEntry[] { /** Loads a `.env` file into `process.env`, without overriding vars already set. */ export function loadDotenv(envPath: string): void { if (!fs.existsSync(envPath)) return; - for (const { key, value } of parseEnvLines(fs.readFileSync(envPath, "utf-8"))) { + for (const { key, value } of parseEnvLines( + fs.readFileSync(envPath, "utf-8"), + )) { if (process.env[key] === undefined) process.env[key] = value; } } /** Sets `key=value` in a `.env` file, replacing an existing line for that key rather than duplicating it. */ -export function upsertEnvVar(envPath: string, key: string, value: string): void { +export function upsertEnvVar( + envPath: string, + key: string, + value: string, +): void { const lines = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8").split("\n") : []; diff --git a/src/cli/lib/network.ts b/src/cli/lib/network.ts index 80bf183..e6b3b70 100644 --- a/src/cli/lib/network.ts +++ b/src/cli/lib/network.ts @@ -51,10 +51,18 @@ export async function isServerReachable(url: string): Promise { export function openBrowser(url: string): void { const command = - process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; + process.platform === "darwin" + ? "open" + : process.platform === "win32" + ? "start" + : "xdg-open"; const args = process.platform === "win32" ? ["", url] : [url]; try { - spawn(command, args, { detached: true, stdio: "ignore", shell: process.platform === "win32" }).unref(); + spawn(command, args, { + detached: true, + stdio: "ignore", + shell: process.platform === "win32", + }).unref(); } catch { // best-effort - not fatal if no browser opener is available } diff --git a/src/cli/lib/paths.ts b/src/cli/lib/paths.ts index 8031a10..cbba9a9 100644 --- a/src/cli/lib/paths.ts +++ b/src/cli/lib/paths.ts @@ -6,7 +6,12 @@ const DEFAULT_CONFIG_NAME = "triggershell.yml"; /** Root of the installed `triggershell` package - one level up from `src/cli/lib`. */ export function resolveAppRoot(): string { - return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + return path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "..", + ); } export function resolveConfigPath(configArg?: string): string { diff --git a/src/cli/lib/systemd.test.ts b/src/cli/lib/systemd.test.ts index 0559c5f..fdf8e39 100644 --- a/src/cli/lib/systemd.test.ts +++ b/src/cli/lib/systemd.test.ts @@ -5,7 +5,8 @@ import { renderUnit } from "./systemd"; test("renderUnit builds an absolute-path ExecStart with the given args", () => { const unit = renderUnit({ execPath: "/usr/bin/node", - binPath: "/home/user/.local/share/pnpm/global/5/node_modules/.bin/triggershell", + binPath: + "/home/user/.local/share/pnpm/global/5/node_modules/.bin/triggershell", configPath: "/home/user/project/triggershell.yml", configDir: "/home/user/project", port: 8080, @@ -31,5 +32,8 @@ test("renderUnit uses multi-user.target for the system scope", () => { }); assert.match(unit, /WantedBy=multi-user\.target/); - assert.match(unit, /ExecStart=\/usr\/bin\/node .*start --config .*--no-browser$/m); + assert.match( + unit, + /ExecStart=\/usr\/bin\/node .*start --config .*--no-browser$/m, + ); }); diff --git a/src/cli/lib/systemd.ts b/src/cli/lib/systemd.ts index fea3039..b946a33 100644 --- a/src/cli/lib/systemd.ts +++ b/src/cli/lib/systemd.ts @@ -22,7 +22,8 @@ export function renderUnit(opts: UnitOptions): string { .map((part) => (part.includes(" ") ? `"${part}"` : part)) .join(" "); - const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target"; + const wantedBy = + opts.scope === "user" ? "default.target" : "multi-user.target"; return `[Unit] Description=TriggerShell - self-hosted script runner @@ -42,7 +43,13 @@ WantedBy=${wantedBy} } export function userUnitPath(): string { - return path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`); + return path.join( + os.homedir(), + ".config", + "systemd", + "user", + `${SERVICE_NAME}.service`, + ); } export function systemUnitPath(): string { diff --git a/src/cli/lib/variables.test.ts b/src/cli/lib/variables.test.ts index d21e2c1..a38aa57 100644 --- a/src/cli/lib/variables.test.ts +++ b/src/cli/lib/variables.test.ts @@ -3,7 +3,10 @@ import { test } from "node:test"; import type { VariableConfig } from "../../lib/config/schema"; import { coerceVariables, parseVarFlags } from "./variables"; -function stringVar(name: string, overrides: Partial = {}): VariableConfig { +function stringVar( + name: string, + overrides: Partial = {}, +): VariableConfig { return { type: "string", name, @@ -62,18 +65,32 @@ test("parseVarFlags rejects a flag with no '='", () => { }); test("coerceVariables coerces booleans and numbers, passes strings through", () => { - const variables = [stringVar("environment"), boolVar("dryRun"), numberVar("replicas")]; + const variables = [ + stringVar("environment"), + boolVar("dryRun"), + numberVar("replicas"), + ]; const values = coerceVariables(variables, { environment: ["staging"], dryRun: ["true"], replicas: ["3"], }); - assert.deepEqual(values, { environment: "staging", dryRun: true, replicas: 3 }); + assert.deepEqual(values, { + environment: "staging", + dryRun: true, + replicas: 3, + }); }); test("coerceVariables rejects an invalid boolean/number", () => { - assert.throws(() => coerceVariables([boolVar("dryRun")], { dryRun: ["yes"] }), /must be 'true' or 'false'/); - assert.throws(() => coerceVariables([numberVar("replicas")], { replicas: ["abc"] }), /not a valid number/); + assert.throws( + () => coerceVariables([boolVar("dryRun")], { dryRun: ["yes"] }), + /must be 'true' or 'false'/, + ); + assert.throws( + () => coerceVariables([numberVar("replicas")], { replicas: ["abc"] }), + /not a valid number/, + ); }); test("coerceVariables collects a multiselect variable's repeats into an array", () => { @@ -85,7 +102,8 @@ test("coerceVariables collects a multiselect variable's repeats into an array", test("coerceVariables rejects a non-multiselect variable given more than once", () => { assert.throws( - () => coerceVariables([stringVar("environment")], { environment: ["a", "b"] }), + () => + coerceVariables([stringVar("environment")], { environment: ["a", "b"] }), /given 2 times/, ); }); diff --git a/src/cli/lib/variables.ts b/src/cli/lib/variables.ts index 07e981f..a065f1b 100644 --- a/src/cli/lib/variables.ts +++ b/src/cli/lib/variables.ts @@ -7,7 +7,9 @@ export function parseVarFlags(pairs: string[]): Record { for (const pair of pairs) { const eq = pair.indexOf("="); if (eq === -1) { - throw new Error(`--var ${pair} is missing '=' - expected --var name=value`); + throw new Error( + `--var ${pair} is missing '=' - expected --var name=value`, + ); } const name = pair.slice(0, eq); const value = pair.slice(eq + 1); @@ -29,7 +31,9 @@ export function coerceVariables( for (const name of Object.keys(grouped)) { if (!known.has(name)) { - throw new Error(`--var ${name}=... does not match any variable on this script`); + throw new Error( + `--var ${name}=... does not match any variable on this script`, + ); } } @@ -52,11 +56,16 @@ export function coerceVariables( if (variable.type === "boolean") { if (value === "true") values[variable.name] = true; else if (value === "false") values[variable.name] = false; - else throw new Error(`--var ${variable.name}=${value} must be 'true' or 'false'`); + else + throw new Error( + `--var ${variable.name}=${value} must be 'true' or 'false'`, + ); } else if (variable.type === "number") { const n = Number(value); if (Number.isNaN(n)) { - throw new Error(`--var ${variable.name}=${value} is not a valid number`); + throw new Error( + `--var ${variable.name}=${value} is not a valid number`, + ); } values[variable.name] = n; } else { diff --git a/src/cli/version.ts b/src/cli/version.ts index 0501b1c..a447433 100644 --- a/src/cli/version.ts +++ b/src/cli/version.ts @@ -4,6 +4,8 @@ import { resolveAppRoot } from "./lib/paths"; export function getVersion(): string { const pkgPath = path.join(resolveAppRoot(), "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version: string }; + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { + version: string; + }; return pkg.version; } diff --git a/src/lib/config/load.ts b/src/lib/config/load.ts index 464c97c..fd95dfb 100644 --- a/src/lib/config/load.ts +++ b/src/lib/config/load.ts @@ -36,9 +36,7 @@ export interface LoadedConfig { export function resolveConfigPath(configPathArg?: string): string { const candidate = - configPathArg ?? - process.env.TRIGGERSHELL_CONFIG_PATH ?? - "triggershell.yml"; + configPathArg ?? process.env.TRIGGERSHELL_CONFIG_PATH ?? "triggershell.yml"; return path.resolve(/*turbopackIgnore: true*/ candidate); } diff --git a/src/lib/docs.ts b/src/lib/docs.ts index 317c8b9..57ee425 100644 --- a/src/lib/docs.ts +++ b/src/lib/docs.ts @@ -12,13 +12,15 @@ export const DOCS: DocMeta[] = [ { slug: "api", title: "API Reference", - description: "REST and WebSocket endpoints, auth, and request/response shapes.", + description: + "REST and WebSocket endpoints, auth, and request/response shapes.", file: "API.md", }, { slug: "config", title: "Config Reference", - description: "Every field in triggershell.yml: server, auth, scripts, and variables.", + description: + "Every field in triggershell.yml: server, auth, scripts, and variables.", file: "CONFIG_REFERENCE.md", }, ]; @@ -34,7 +36,8 @@ export function getDocMeta(slug: string): DocMeta | undefined { // Falls back to a source-relative resolution for contexts where server.ts never ran (e.g. tests). function docsDir(): string { const appRoot = - process.env.TRIGGERSHELL_APP_ROOT ?? path.resolve(import.meta.dirname, "..", ".."); + process.env.TRIGGERSHELL_APP_ROOT ?? + path.resolve(import.meta.dirname, "..", ".."); return path.resolve(appRoot, "docs"); } diff --git a/src/lib/runner/events.ts b/src/lib/runner/events.ts index 651814a..2035c90 100644 --- a/src/lib/runner/events.ts +++ b/src/lib/runner/events.ts @@ -9,7 +9,8 @@ declare global { * Anchored on `globalThis` because Next compiles Route Handlers through its own module graph, * separate from the modules `server.ts` requires directly via tsx - a plain module-level * singleton would silently end up duplicated (one copy per graph) instead of shared. */ -export const runEvents: EventEmitter = globalThis.__triggershellRunEvents ?? new EventEmitter(); +export const runEvents: EventEmitter = + globalThis.__triggershellRunEvents ?? new EventEmitter(); globalThis.__triggershellRunEvents = runEvents; runEvents.setMaxListeners(0); diff --git a/src/lib/runner/registry.ts b/src/lib/runner/registry.ts index f734ed8..7c8cf18 100644 --- a/src/lib/runner/registry.ts +++ b/src/lib/runner/registry.ts @@ -13,7 +13,8 @@ declare global { * Anchored on `globalThis` - see the comment in `runner/events.ts` for why a plain module-level * singleton isn't safe here (Next compiles Route Handlers through a separate module graph from * what `server.ts` requires directly). */ -const handles: Map = globalThis.__triggershellRunHandles ?? new Map(); +const handles: Map = + globalThis.__triggershellRunHandles ?? new Map(); globalThis.__triggershellRunHandles = handles; export function registerRun(handle: RunHandle) {