# TriggerShell A CLI that launches a self-hosted web app for running your shell scripts: define scripts and their parameters in a config file, get a generated form UI, run them asynchronously with live streaming output, and keep a searchable history — all behind optional login, with a full REST/WS API for automation. ## Features - **One config file** defines the server, auth, and every script (command, working dir, timeout, and typed/validated variables with defaults and choices). - **Generated forms** — each variable renders as the right control (text, number, slider, select, radio, checkbox, switch, multi-select) based on its type, with client- and server-side validation from the same schema. - **Safe execution** — scripts run via argv arrays, never a shell string. User input can never break out into shell metacharacters. - **Live monitoring** — stdout/stderr and status stream over WebSocket to an in-browser terminal view; cancel a run from the UI or the API. - **Run history** — every run's variables (secrets redacted), resolved command, status, exit code, and full log are persisted in SQLite. - **Optional auth** — built-in username/password sessions and API tokens, toggleable per config. - **REST + WebSocket API** for scripting your script runner. ## Quickstart ```bash pip install triggershell # or: pip install -e . from a checkout triggershell init # scaffold triggershell.yml (+ .env for secrets) in the current directory triggershell users add admin # create a login (skip if you set auth.enabled: false) triggershell dev # start in dev mode and open the browser ``` Edit `triggershell.yml` to add your own scripts (see [Configuration](#configuration) below), then run `triggershell start` for a production build. ## Requirements - Python >= 3.9 - Node.js >= 20 (checked by the CLI; not auto-installed) - pnpm (auto-provisioned via Corepack if missing and Corepack is available) ## Configuration TriggerShell is driven entirely by one YAML file (default: `./triggershell.yml`, override with `-c/--config` or `TRIGGERSHELL_CONFIG_PATH`). Secrets referenced via `${VAR}` (like `auth.sessionSecret`) are meant to live in a `.env` file next to the config, not in the config itself — `triggershell init` generates both. Minimal example: ```yaml server: host: 127.0.0.1 port: 4173 auth: enabled: true sessionSecret: "${TRIGGERSHELL_SESSION_SECRET}" # >= 32 chars users: - username: admin passwordHash: "$argon2id$..." # from `triggershell users add` tokens: [] database: path: .triggershell/triggershell.db logs: dir: .triggershell/logs retentionDays: 30 scripts: - id: deploy-service name: Deploy Service command: bash args: ["./scripts/deploy.sh"] timeoutSeconds: 600 variables: - name: environment type: enum choices: [staging, production] default: staging required: true passAs: arg argName: --env - name: dryRun type: boolean default: false passAs: flag argName: --dry-run ``` A full, richly-commented example lives at [`examples/triggershell.yml`](examples/triggershell.yml) (with a matching [`.env.example`](examples/.env.example)), and the complete field-by-field reference is in [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md). Each variable's `type` (`string` / `number` / `boolean` / `enum` / `multiselect`) picks a sensible default UI control; set `control` explicitly to override it (e.g. `slider` for a `number`, `radio` for an `enum`). `passAs` (`arg` / `flag` / `env` / `stdin`) decides how the validated value reaches the script — always as a discrete argv element or env var, never interpolated into a shell string. ## 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 (fast Python pre-flight + full Node/Zod schema) | | `triggershell dev [-c CONFIG] [--port] [--host] [--no-browser]` | Run in development mode (hot reload) | | `triggershell start [-c CONFIG] [--port] [--host] [--no-browser] [--skip-build]` | Build (if stale) and run in production mode | | `triggershell doctor` | Print environment/config diagnostics | | `triggershell users add ` | Hash a password and print a config snippet for `auth.users` | | `triggershell users add-token ` | Generate an API token and print a config snippet for `auth.tokens` | ## Web App Guide - **Scripts** (`/`) — every configured script as a card; click through to its run form. - **Run form** (`/scripts/:id`) — a generated form for the script's variables, validated before submit. - **Run detail** (`/runs/:id`) — live streaming terminal output, status, exit code, and a Cancel button while the run is active. - **History** (`/runs`) — every past run, filterable by script/status via the API (`?scriptId=`, `?status=`). ## Authentication Set `auth.enabled: false` for trusted/local-only use — the app then requires no login at all. When enabled, sessions are signed/encrypted cookies (no server-side session store), and API clients can instead send `Authorization: Bearer ` using a token from `users add-token`. Passwords are hashed with argon2id; only the hash ever lives in the config file. ## API Reference 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` | ## Development ```bash pnpm --dir app install pnpm --dir app dev # or: triggershell dev, which wraps this pnpm --dir app lint pnpm --dir app typecheck pnpm --dir app db:studio # browse the SQLite DB ``` Repo layout: ``` triggershell/ Python CLI (launcher/orchestrator only) app/ Next.js app - all server logic (API, auth, script execution) lives here examples/ A runnable example config + scripts docs/ Config/architecture/API reference docs ``` See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the pieces fit together. ## Security Notes - 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 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 live child process ever sees the real value. - The server binds to `127.0.0.1` by default — set `server.host` explicitly to expose it further. - Route Handlers check auth themselves (`requireAuth()`); `proxy.ts` is only a fast, optimistic redirect layer, not the security boundary. - `auth.sessionSecret` should live in `.env`, not the config file — `triggershell init` sets this up for you. Only `passwordHash`/`tokenHash` values (one-way hashes, not the secrets themselves) belong in the config. ## License MIT — see [LICENSE](LICENSE).