Initial implementation of TriggerShell
A Python CLI (typer) that bootstraps Node/pnpm and launches a Next.js 16 web app for running configured shell scripts: YAML config validated by a shared Zod schema, dynamic per-script forms mapped to shadcn controls, argv-safe execa execution with live WebSocket streaming, SQLite/Drizzle run history, and optional argon2 session + API token auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# 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.config.yaml 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.config.yaml` 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.config.yaml`, override
|
||||
with `-c/--config` or `TRIGGERSHELL_CONFIG_PATH`). 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.config.yaml`](examples/triggershell.config.yaml),
|
||||
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 (`--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 <username>` | Hash a password and print a config snippet for `auth.users` |
|
||||
| `triggershell users add-token <name>` | 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 <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
|
||||
|
||||
```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.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
Reference in New Issue
Block a user