Now that the CLI and the Next.js app are one package, nesting it inside app/ served no purpose - the repo root itself becomes the published npm package. Merges app/.gitignore and app/README.md into the root versions, drops the now-duplicate app/LICENSE, and updates path references (README, docs/ARCHITECTURE.md, docs/CONFIG_REFERENCE.md, package.json's repository.directory) that assumed the app/ nesting. Also fixes a real bug this surfaced: the in-app docs viewer resolved docs/ relative to process.cwd(), which only worked by accident when the CLI happened to be invoked from app/'s parent directory. A first attempt at fixing it with import.meta.dirname broke instead, for the same cross-module-graph reason config-path resolution already documented - Next compiles Route Handlers through a separate module graph that doesn't preserve source-relative import.meta paths. Fixed by exposing the app root via TRIGGERSHELL_APP_ROOT (set once in server.ts, where import.meta *does* resolve correctly), the same pattern already used for TRIGGERSHELL_CONFIG_PATH. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
9.1 KiB
Markdown
198 lines
9.1 KiB
Markdown
# 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
|
|
npm install -g triggershell # or: npx triggershell <command> for one-off use
|
|
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 start # start the app and open the browser
|
|
```
|
|
|
|
Edit `triggershell.yml` to add your own scripts (see [Configuration](#configuration) below), then
|
|
re-run `triggershell start`.
|
|
|
|
> The `triggershell` package isn't published to npm yet. Until it is, build and link a local copy
|
|
> instead: `pnpm install && pnpm build && pnpm link --global`.
|
|
|
|
## Requirements
|
|
|
|
- Node.js >= 20 — the only thing you need installed. Everything else `triggershell` needs ships
|
|
inside the package itself and is resolved automatically when you install it.
|
|
|
|
## 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: "${TRIGGERSHELL_USER_ADMIN_PASSWORD_HASH}" # 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 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 users add <username> [-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 <name> [-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 |
|
|
|
|
## 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 <token>` 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
|
|
|
|
This is the workflow for working on TriggerShell itself, not for installing/running it — it
|
|
bypasses the CLI entirely and talks to the app's own scripts directly, with hot reload. The app is
|
|
still not meant to be run standalone with `next dev`/`next start`, since it needs the custom server
|
|
(`server.ts`) for the WebSocket endpoint — `pnpm dev`/`pnpm start` below cover that:
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm dev # tsx watch server.ts - reads TRIGGERSHELL_CONFIG_PATH from the environment
|
|
pnpm lint
|
|
pnpm typecheck
|
|
pnpm test # CLI unit tests (src/cli/**/*.test.ts)
|
|
pnpm db:studio # browse the SQLite DB
|
|
```
|
|
|
|
Repo layout — the repo root itself is the published npm package (Next.js app + the `triggershell`
|
|
CLI in `bin/`/`src/cli/`), alongside:
|
|
|
|
```
|
|
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. `passwordHash`/`tokenHash` are one-way hashes (not the secrets themselves), so
|
|
storing them directly in the config is reasonably safe — the same trust model as `/etc/shadow`
|
|
or `.htpasswd` — but `triggershell users add`/`add-token` store them in `.env` via `${VAR}` by
|
|
default too, for cases where you don't want them readable by anyone with config access at all.
|
|
Pass `--inline` to get the old behavior of printing the raw hash to paste into the config.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|