Files
triggershell/README.md
T
valknarandClaude Sonnet 5 c6337ea942 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 <noreply@anthropic.com>
2026-08-16 13:55:25 +02:00

14 KiB

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

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 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:

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 (with a matching .env.example), and the complete field-by-field reference is in 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 scripts list [-c CONFIG] List configured scripts
triggershell scripts show <scriptId> [-c CONFIG] Show a script's command and variables
triggershell run <scriptId> [--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.
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

Running scripts from the CLI

triggershell run <scriptId> auto-detects whether the web app is already running (a quick /api/healthz check against server.host/server.port, overridable with --host/--port):

  • Server reachable — the run goes through the same POST /api/scripts/:id/runs endpoint the web UI uses, authenticated with --token/TRIGGERSHELL_API_TOKEN if auth.enabled. It's a completely normal run from the server's point of view: it shows up in Run History, and any browser tab open on /runs/:id streams its output live, exactly as if it had been started from the UI.
  • No server reachablerun executes the script itself, in its own process, using the same runner the web app uses. The run and its log are still persisted, just without a browser to watch it.

Force one or the other with --local/--remote (the latter fails instead of falling back if nothing's reachable). Pass variables with repeated --var name=value flags (repeat the same name for a multiselect variable); --no-wait prints the run ID and returns immediately instead of streaming output and blocking until it finishes. Exit code is 0 for a succeeded run, 1 otherwise. Ctrl-C while waiting cancels the run, the same as the UI's Cancel button.

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.

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:

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 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.