Commit Graph
32 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 5 3f379ca2ac Flatten the repo: move everything out of app/ to the root
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>
2026-08-16 11:14:01 +02:00
valknarandClaude Sonnet 5 30350d80f4 Replace the Python CLI with a Node CLI, add a systemd service command
The app is already 100% Node, so the Python launcher was pure overhead - it
existed mainly to bootstrap Node, which is circular. The CLI is now merged
into app/ (the single published npm package): `triggershell start` validates
the config and imports server.ts directly in-process, so server.ts's own
SIGTERM/SIGINT handling just works with no signal-relay/child-process layer
needed. `dev` is dropped from the public CLI (contributors use `pnpm --dir
app dev` directly); there's no `build` command either, since the package
ships a prebuilt `.next` via a `prepack` hook. Adds `triggershell service
install|uninstall|status` for running as a per-user or system systemd unit.

Also fixes two bugs found while wiring this up: server.ts resolved `.next`
relative to `process.cwd()`, which broke once the CLI could run from a
directory other than the app itself; and an explicitly-`files`-listed
package directory bypasses .npmignore for its subpaths, so `.next/cache`
was inflating the npm tarball to ~670MB (now stripped in `prepack`, ~7MB).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 11:03:25 +02:00
valknar e2b6c6102c chore: remove architecture doc in web app 2026-08-16 06:09:31 +02:00
valknarandClaude Sonnet 5 042391cdc3 Match run detail card width to the new-run form card
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 06:08:40 +02:00
valknarandClaude Sonnet 5 dc34b38b26 Drop "contributors" from the footer copyright line
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:59:11 +02:00
valknarandClaude Sonnet 5 346bf7995a Style inline code in docs as a pill instead of literal backticks
Tailwind Typography's default inline <code> style is just bold text
with decorative backtick characters added via CSS content - with docs
like CONFIG_REFERENCE.md that use backticks constantly, it read as
unstyled/half-parsed. Swapped it for the same muted rounded-pill look
already used for inline code elsewhere in the app (e.g. the dashboard's
"no scripts configured" message).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:58:14 +02:00
valknarandClaude Sonnet 5 1b415a7957 Add a footer and an in-app docs viewer
Footer (in the authenticated app layout, matching where Nav lives):
copyright line, a Docs link, and a link to the project repo.

Docs viewer: /docs lists docs/API.md, CONFIG_REFERENCE.md, and
ARCHITECTURE.md; /docs/[slug] renders one via react-markdown +
remark-gfm (tables, fenced code) inside a Tailwind Typography `prose`
block, dark-mode aware via prose-invert. The markdown files themselves
stay the single source of truth at the repo's docs/ - the app reads
them at request time rather than duplicating their content, resolved
from the app's cwd the same way config paths already are, since a
compiled Route Handler's module graph doesn't preserve source-relative
paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:54:15 +02:00
valknarandClaude Sonnet 5 d7c0be6b22 Add a Re-run button to the run detail page
Re-run takes you to the script's form pre-filled with that run's
variable values, rather than re-executing immediately - this is
necessary, not just cautious: stored run.variables already have secret
fields redacted to "***" (see the run detail command/variables view),
so a true one-click re-run would either fail validation or, worse,
silently pass the literal string "***" to the script as a real secret.
Pre-filling instead lets the user review/tweak values and forces secret
fields to be re-entered, both of which are new safe defaults now that
required fields are actually enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:32:30 +02:00
valknarandClaude Sonnet 5 75b92d103c Fix status/script filter dropdowns being cramped on mobile
SelectTrigger defaults to w-fit, and the toolbar only set a width from
sm: up, so below that breakpoint each dropdown shrank to its label text
instead of using the full row it was already stacked onto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:24:18 +02:00
valknarandClaude Sonnet 5 3f656baad1 Make Duration sortable on the runs page
It was left out because duration isn't a stored column, just
started_at/ended_at math - now expressed as a SQL case expression so it
can be sorted the same way as the other columns. Still-running rows
sort by elapsed-so-far; never-started (queued) rows sort as NULL, which
puts them out of the way at whichever end matches the current
direction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:20:06 +02:00
valknarandClaude Sonnet 5 3131d8ce5b Add search, filtering, sorting, and pagination to Run History
The runs page was a flat top-100 list with no way to narrow it down.
It's now driven entirely by the URL (?q=&status=&scriptId=&sort=&dir=&page=),
so filtered/sorted views are shareable and survive back/forward
navigation:

- free-text search across script name, triggered-by, and the resolved
  command
- status and script dropdown filters
- sortable Script/Status/Triggered-by/Started column headers
- offset pagination (25/page) with a real total count, clamped so an
  out-of-range page falls back to the last valid one instead of
  showing a misleading "no results"

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:17:49 +02:00
valknarandClaude Sonnet 5 d4e4c2c7d2 Show the full resolved command and variables on the run detail page
The command lived in a narrow truncate'd grid cell, so anything but a
short invocation was unreadable. It now gets its own scrollable code
block, plus a breakdown of each variable (using the script's configured
labels when available) and the value it resolved to for that run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:11:27 +02:00
valknarandClaude Sonnet 5 004586ee64 Enforce required on string/multiselect variables without explicit bounds
required:true only worked by accident: a bare z.string() or
z.array(...) accepts "" / [] just fine, so a required field with no
minLength (e.g. secret tokens like webhookToken, apiKey) could be
submitted empty and the run would start anyway. Required now implies
min(1) when no stricter bound is already configured, for both string
and multiselect fields. This is the single schema shared by the client
form resolver and the server run-creation route, so both now reject it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:06:39 +02:00
valknarandClaude Sonnet 5 e6008a22cd Fix live output missing for fast-finishing scripts
Scripts that finish in milliseconds (e.g. notify.js) could complete and
broadcast all their output/status before a client's WS subscribe message
even arrived, leaving the run page's terminal permanently blank with no
way to catch up. The server now answers every subscribe with whatever
log bytes were written past what the client's server-rendered page
already had, plus the run's current status, before it starts streaming
live broadcasts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 00:02:57 +02:00
valknarandClaude Sonnet 5 c3312f5dc6 Remove theme toggle from the login page
Keep it in the main app header only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:17:49 +02:00
valknarandClaude Sonnet 5 79a7c8f5cb Tighten xterm terminal line height
1.4 was too loose for a log/terminal view; 1.2 stays readable while
fitting more output on screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:14:11 +02:00
valknarandClaude Sonnet 5 a4db53f44d Add dark theme support with a toggle
shadcn had already generated full light/dark CSS variable sets in
globals.css (keyed off a .dark class via @custom-variant), but nothing
ever added that class - the app was always light regardless of OS
preference. Wires up next-themes (already a dependency, pulled in by
sonner.tsx but never provided) with attribute="class", defaulting to
the system preference. Adds a sun/moon ThemeToggle button - in the
header for the authenticated app, and in the top-right corner of the
login page since that route sits outside the header layout.

suppressHydrationWarning moves to <html> too (next-themes sets the
class there via a pre-hydration script, so server/client will
legitimately differ on first paint).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:11:27 +02:00
valknarandClaude Sonnet 5 35b0d95792 Remove unused create-next-app placeholder assets
file.svg, globe.svg, next.svg, vercel.svg, and window.svg were only
ever referenced by the scaffolded demo homepage, which is long gone.
Nothing else in the app references them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:02:26 +02:00
valknarandClaude Sonnet 5 e779c5a93b Use the header's Terminal icon as the app favicon
Replaces the default create-next-app favicon.ico with an icon.svg
matching lucide-react's Terminal glyph (same one used in the header)
exactly, on a dark rounded-square backdrop so it stays legible at
16x16 in the browser tab regardless of the OS/browser chrome theme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:01:13 +02:00
valknarandClaude Sonnet 5 acc6715a88 Allow timeoutSeconds: 0 to mean no timeout
Previously every script was forced to have a positive timeout capped
at 24h, with no way to run something genuinely unbounded. execa only
enforces its timeout option when it's greater than 0, so this just
relaxes the schema's lower bound from positive to >= 0 and lets that
flow through unchanged - no engine logic needed beyond a clarifying
comment. The 1800s default and 86400s cap for finite timeouts are
unchanged; 0 is an explicit opt-in, not the default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:57:24 +02:00
valknarandClaude Sonnet 5 97bb6cc092 Add per-page titles
Root layout now defines a title template (%s · TriggerShell) so every
page just sets its own short title instead of repeating the brand
name. Static titles for the dashboard, login, and run history pages;
dynamic generateMetadata for the script and run detail pages, since
those need the script/run name which isn't known until the route
params resolve.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:52:11 +02:00
valknarandClaude Sonnet 5 7daaa95ab9 Hide the Next.js dev-mode route indicator badge
Sets devIndicators: false in next.config.ts. Compile/runtime error
overlays are unaffected - only the floating route-info badge is hidden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:45:46 +02:00
valknarandClaude Sonnet 5 6d7788e48b Keep the TriggerShell wordmark visible on mobile
Only the icon-only nav links, username, and logout label needed to
collapse to fit a phone-width header - the brand text alone doesn't
push it over, so keep it always shown for identity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:40:37 +02:00
valknarandClaude Sonnet 5 88f561230e Fix header horizontal overflow on mobile
The nav bar's brand text, both nav-link labels, the username, and the
logout label were all always rendered, easily exceeding a phone-width
viewport since nothing could shrink or wrap. Collapse to icon-only
below the sm breakpoint (labels stay in the DOM via sr-only so they're
still announced to screen readers, just not painted) and truncate a
long username instead of letting it force overflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:36:55 +02:00
valknarandClaude Sonnet 5 7269886f9e Render run output with xterm.js instead of plain text
Replaces the plain-text output <div> with a real xterm.js terminal
(@xterm/xterm + @xterm/addon-fit), so ANSI color/control codes from
scripts render as actual colors instead of raw escape characters.
stderr chunks are wrapped in ANSI red so failures stand out even from
tools that don't colorize their own output. Also sets FORCE_COLOR=1/
CLICOLOR_FORCE=1 as env defaults (real/script env still wins) since
scripts run without a real TTY and most tools auto-disable color
without one of these overrides.

Fixed a React Strict Mode bug found while testing: the initial log was
written to the terminal via a "write once" ref flag in the parent,
but Strict Mode's dev-only mount->cleanup->remount cycle creates a
fresh Terminal on the real mount, so that flag silently skipped
writing to the surviving instance - the terminal looked completely
blank until live output arrived. Fixed by writing initialData inside
the same effect that creates the Terminal, so it's correct by
construction regardless of how many times the effect runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 20:29:03 +02:00
valknarandClaude Sonnet 5 20989987f6 Suppress hydration-warning noise from browser-extension DOM tweaks on body
cz-shortcut-listen is ColorZilla's own marker attribute, injected into
<body> client-side before React hydrates - not an app bug. This only
silences mismatches on body's own attributes, not its children, so a
real hydration bug elsewhere would still surface normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:49:08 +02:00
valknarandClaude Sonnet 5 9dc45f751a Delegate non-/ws/runs upgrade requests to Next instead of dropping them
Our upgrade handler on the shared httpServer was destroying every socket
that wasn't for /ws/runs, which silently killed Next's own dev-mode HMR
websocket (/_next/hmr) too - breaking hot reload with no useful error,
just a failed WebSocket connection in the browser console. Delegate to
app.getUpgradeHandler() instead, which must be called after prepare().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:46:41 +02:00
valknarandClaude Sonnet 5 d3e3360d6e Remove deprecated url.parse() usage from the custom server
Node flags legacy url.parse() (DEP0169) as having security implications
and recommends the WHATWG URL API instead. The main request handler's
parsedUrl argument to Next's handle() is optional and unused by us, so
that call is dropped entirely (matching Next's own minimal custom-server
example); the WS upgrade path-check now uses `new URL()` instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:41:37 +02:00
valknarandClaude Sonnet 5 466458a664 Point repo URLs at dev.pivoine.art
Replace the placeholder GitHub URL in the scaffolded config template
with the project's real repo, and add matching Repository/repository
metadata to pyproject.toml and app/package.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:34:54 +02:00
valknarandClaude Sonnet 5 ac0e645be3 Store password/token hashes in .env by default too
triggershell users add/add-token now generate a TRIGGERSHELL_USER_<name>_
PASSWORD_HASH / TRIGGERSHELL_TOKEN_<name>_HASH variable in .env (creating
or updating it idempotently) and print a ${VAR} snippet to paste into
auth.users/auth.tokens, instead of printing the raw hash. Pass --inline to
get the old behavior, since a hash - unlike sessionSecret - is safe to
store directly in the config (same trust model as /etc/shadow); this just
gives people who don't want it there at all an easy option.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:31:41 +02:00
valknarandClaude Sonnet 5 80c11d3bd3 Externalize auth secrets to .env and rename default config to triggershell.yml
sessionSecret was previously baked directly into the scaffolded config file;
`triggershell init` now generates a .env with TRIGGERSHELL_SESSION_SECRET
instead and references it via ${VAR} interpolation, keeping the actual
secret out of the (often committed) config file. `triggershell dev/start/
validate` load that .env automatically without overriding real env vars.

Also renames the default config filename from triggershell.config.yaml to
triggershell.yml throughout the CLI, app, docs, and examples.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 19:15:03 +02:00
valknarandClaude Sonnet 5 ced99a8e75 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>
2026-08-15 18:37:30 +02:00