8 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 5 e5a1b7ce53 Rename package to scoped @valknar/triggershell
CI / Checks (push) Successful in 46s
CI / Publish to npm registry (push) Successful in 48s
Matches vpinball-wasm's move back to a scoped name: an unscoped
package on this Gitea npm registry can't be mapped via a normal
`.npmrc` `@scope:registry=` entry, since the registry doesn't proxy
npmjs.org. Scoping lets any consumer add one registry line instead of
pinning a tarball URL per dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD
2026-08-23 01:06:23 +02:00
valknar 90d68f2510 Fix prettier formatting
CI / Publish to npm registry (push) Successful in 49s
CI / Checks (push) Successful in 47s
CI caught this on the v1.1.0 tag push - format:check wasn't run locally before committing.
2026-08-19 18:10:28 +02:00
valknar 1fa8c5ba66 Bump version to 1.1.0
CI / Checks (push) Failing after 45s
CI / Publish to npm registry (push) Skipped
2026-08-19 18:07:35 +02:00
valknar b291a119d5 Emit raw JSON from service logs via journalctl -o cat
Strips journalctl's own prefix so each line is pino's raw JSON payload, pipeable into jq for pretty-printing without pulling pino-pretty into runtime dependencies.
2026-08-19 18:06:06 +02:00
valknar e63129d156 Add triggershell service logs command
CI / Checks (push) Failing after 48s
CI / Publish to npm registry (push) Skipped
Thin wrapper around journalctl, consistent with the existing status/uninstall wrappers around systemctl.
2026-08-19 17:58:41 +02:00
valknarandClaude Sonnet 5 96a66fc857 Add structured backend logging with pino
CI / Checks (push) Successful in 47s
CI / Publish to npm registry (push) Successful in 50s
Wires leveled, structured logging (pretty in dev, JSON in prod) through
the server lifecycle, HTTP/WS request handling, run engine, auth, db,
and config loading. CLI command output is left untouched since it's
user-facing terminal UX, not backend logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:54:16 +02:00
valknar 3272b9db76 fix: actually stage the checks/publish split
CI / Checks (push) Successful in 48s
CI / Publish to npm registry (push) Skipped
The previous commit renamed release.yaml to ci.yml but never staged
the content edit underneath it (git mv picked up the last-staged
version, not the unstaged working-tree changes) - it pushed with the
filename changed but the workflow itself untouched. This is the
content that commit was supposed to carry.
2026-08-17 17:38:31 +02:00
valknar 06edc60b55 ci: run checks on every push, publish only on tag
Same split as pulsenode's workflow: previously this only ran at all
when pushing a version tag, so lint/typecheck/format/test never ran on
regular commits or PRs - a broken push could sit unnoticed until
someone tried to cut a release. Now checks run on every push and PR;
publish to the npm registry stays gated to a tag push and requires
checks to pass first. Renamed release.yaml -> ci.yml to match.
2026-08-17 17:37:01 +02:00
18 changed files with 491 additions and 25 deletions
@@ -1,12 +1,12 @@
name: Release name: CI
on: on:
push: push:
tags: pull_request:
- "v*.*.*"
jobs: jobs:
release: checks:
name: Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: https://github.com/actions/checkout@v4 - uses: https://github.com/actions/checkout@v4
@@ -26,6 +26,24 @@ jobs:
- run: pnpm run format:check - run: pnpm run format:check
- run: pnpm run test - run: pnpm run test
publish:
name: Publish to npm registry
if: startsWith(github.ref, 'refs/tags/')
needs: checks
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/pnpm/action-setup@v4
with:
version: 11.21.0
- uses: https://github.com/actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- name: Set package version from the tag - name: Set package version from the tag
run: npm pkg set version="${GITHUB_REF_NAME#v}" run: npm pkg set version="${GITHUB_REF_NAME#v}"
+2 -1
View File
@@ -24,7 +24,7 @@ API for automation.
## Quickstart ## Quickstart
```bash ```bash
npm install -g triggershell # or: npx triggershell <command> for one-off use npm install -g @valknar/triggershell # or: npx @valknar/triggershell <command> for one-off use
triggershell init # scaffold triggershell.yml (+ .env for secrets) in the current directory 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 users add admin # create a login (skip if you set auth.enabled: false)
triggershell start # start the app and open the browser triggershell start # start the app and open the browser
@@ -113,6 +113,7 @@ the script — always as a discrete argv element or env var, never interpolated
| `triggershell service install [--system]` | Install a systemd unit that runs `triggershell start` (per-user by default, Linux only) | | `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 uninstall [--system]` | Stop, disable, and remove the systemd unit |
| `triggershell service status [--system]` | Show the systemd unit's status | | `triggershell service status [--system]` | Show the systemd unit's status |
| `triggershell service logs [-n LINES] [--no-follow] [--system]` | Tail the systemd unit's logs (wraps `journalctl -o cat`, so each line is raw JSON - pipe through `jq` for pretty-printing) |
### Running scripts from the CLI ### Running scripts from the CLI
+5 -3
View File
@@ -74,9 +74,11 @@ installs a per-user unit (`~/.config/systemd/user/triggershell.service`, no root
targets `/etc/systemd/system/` instead and prints the `sudo` commands to run if not already root. targets `/etc/systemd/system/` instead and prints the `sudo` commands to run if not already root.
`install` reloads the systemd daemon but does not enable/start the unit itself — that's a separate, `install` reloads the systemd daemon but does not enable/start the unit itself — that's a separate,
explicit `systemctl --user enable --now triggershell`, since it's the point where the service explicit `systemctl --user enable --now triggershell`, since it's the point where the service
actually starts listening and running scripts. `triggershell service status`/`uninstall` are thin actually starts listening and running scripts. `triggershell service status`/`uninstall`/`logs` are
wrappers around `systemctl`; log tailing is just `journalctl --user -u triggershell -f` — not thin wrappers around `systemctl`/`journalctl` respectively - no unit-file parsing or log storage of
reimplemented. our own, journald already does that. `logs` passes `-o cat` so each line is the raw pino JSON
payload rather than journalctl's own timestamp/hostname/unit prefix - pipeable straight into `jq`
for pretty-printing without pulling `pino-pretty` into the CLI's runtime dependencies.
## Cross-module-graph state ## Cross-module-graph state
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "triggershell", "name": "@valknar/triggershell",
"version": "0.1.0", "version": "1.2.0",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"repository": { "repository": {
@@ -60,6 +60,7 @@
"lucide-react": "^1.31.0", "lucide-react": "^1.31.0",
"next": "16.3.1", "next": "16.3.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pino": "^10.3.1",
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8", "react-dom": "19.2.8",
"react-hook-form": "^7.85.0", "react-hook-form": "^7.85.0",
@@ -86,6 +87,7 @@
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.3.1", "eslint-config-next": "16.3.1",
"pino-pretty": "^13.1.3",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"prettier-plugin-tailwindcss": "^0.8.1", "prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4", "tailwindcss": "^4",
+178
View File
@@ -59,6 +59,9 @@ importers:
next-themes: next-themes:
specifier: ^0.4.6 specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
pino:
specifier: ^10.3.1
version: 10.3.1
react: react:
specifier: 19.2.8 specifier: 19.2.8
version: 19.2.8 version: 19.2.8
@@ -132,6 +135,9 @@ importers:
eslint-config-next: eslint-config-next:
specifier: 16.3.1 specifier: 16.3.1
version: 16.3.1(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) version: 16.3.1(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
pino-pretty:
specifier: ^13.1.3
version: 13.1.3
prettier: prettier:
specifier: ^3.9.6 specifier: ^3.9.6
version: 3.9.6 version: 3.9.6
@@ -1358,6 +1364,9 @@ packages:
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@radix-ui/primitive@1.1.7': '@radix-ui/primitive@1.1.7':
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
@@ -1987,6 +1996,10 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
atomically@1.7.0: atomically@1.7.0:
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
engines: {node: '>=10.12.0'} engines: {node: '>=10.12.0'}
@@ -2136,6 +2149,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
comma-separated-tokens@2.0.3: comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -2226,6 +2242,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debounce-fn@4.0.0: debounce-fn@4.0.0:
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2439,6 +2458,9 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
enhanced-resolve@5.24.5: enhanced-resolve@5.24.5:
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -2685,6 +2707,9 @@ packages:
extend@3.0.2: extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
fast-copy@4.0.4:
resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2702,6 +2727,9 @@ packages:
fast-levenshtein@2.0.6: fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-string-truncated-width@3.0.3: fast-string-truncated-width@3.0.3:
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
@@ -2894,6 +2922,9 @@ packages:
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
hermes-estree@0.25.1: hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -3172,6 +3203,10 @@ packages:
jose@6.2.8: jose@6.2.8:
resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3649,6 +3684,10 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
on-finished@2.4.1: on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -3759,6 +3798,20 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pino-abstract-transport@3.0.0:
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
pino-pretty@13.1.3:
resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==}
hasBin: true
pino-std-serializers@7.1.0:
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
pino@10.3.1:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true
pkce-challenge@5.0.1: pkce-challenge@5.0.1:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'} engines: {node: '>=16.20.0'}
@@ -3863,6 +3916,9 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
process-warning@5.1.0:
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
prompts@2.4.2: prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -3877,6 +3933,9 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3888,6 +3947,9 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
range-parser@1.3.0: range-parser@1.3.0:
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3950,6 +4012,13 @@ packages:
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
real-require@1.0.0:
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
recast@0.23.21: recast@0.23.21:
resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -4024,12 +4093,19 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
secure-json-parse@4.1.0:
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@@ -4118,6 +4194,9 @@ packages:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
sonner@2.0.8: sonner@2.0.8:
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
peerDependencies: peerDependencies:
@@ -4142,6 +4221,10 @@ packages:
space-separated-tokens@2.0.2: space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stable-hash@0.0.5: stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@@ -4215,6 +4298,10 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
strip-json-comments@5.0.3:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
style-to-js@1.1.21: style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@@ -4258,6 +4345,10 @@ packages:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'} engines: {node: '>=6'}
thread-stream@4.2.0:
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
engines: {node: '>=20'}
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -5447,6 +5538,8 @@ snapshots:
'@phc/format@1.0.0': {} '@phc/format@1.0.0': {}
'@pinojs/redact@0.4.0': {}
'@radix-ui/primitive@1.1.7': {} '@radix-ui/primitive@1.1.7': {}
'@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)': '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)':
@@ -6031,6 +6124,8 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
atomic-sleep@1.0.0: {}
atomically@1.7.0: {} atomically@1.7.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
@@ -6172,6 +6267,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
colorette@2.0.20: {}
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
commander@11.1.0: {} commander@11.1.0: {}
@@ -6256,6 +6353,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.2 is-data-view: 1.0.2
dateformat@4.6.3: {}
debounce-fn@4.0.0: debounce-fn@4.0.0:
dependencies: dependencies:
mimic-fn: 3.1.0 mimic-fn: 3.1.0
@@ -6357,6 +6456,10 @@ snapshots:
encodeurl@2.0.0: {} encodeurl@2.0.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
enhanced-resolve@5.24.5: enhanced-resolve@5.24.5:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -6877,6 +6980,8 @@ snapshots:
extend@3.0.2: {} extend@3.0.2: {}
fast-copy@4.0.4: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-glob@3.3.1: fast-glob@3.3.1:
@@ -6899,6 +7004,8 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fast-safe-stringify@2.1.1: {}
fast-string-truncated-width@3.0.3: {} fast-string-truncated-width@3.0.3: {}
fast-string-width@3.0.2: fast-string-width@3.0.2:
@@ -7105,6 +7212,8 @@ snapshots:
dependencies: dependencies:
'@types/hast': 3.0.5 '@types/hast': 3.0.5
help-me@5.0.0: {}
hermes-estree@0.25.1: {} hermes-estree@0.25.1: {}
hermes-parser@0.25.1: hermes-parser@0.25.1:
@@ -7350,6 +7459,8 @@ snapshots:
jose@6.2.8: {} jose@6.2.8: {}
joycon@3.1.1: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.3.1: js-yaml@4.3.1:
@@ -7986,6 +8097,8 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-object-atoms: 1.1.2 es-object-atoms: 1.1.2
on-exit-leak-free@2.1.2: {}
on-finished@2.4.1: on-finished@2.4.1:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
@@ -8108,6 +8221,42 @@ snapshots:
picomatch@4.0.5: {} picomatch@4.0.5: {}
pino-abstract-transport@3.0.0:
dependencies:
split2: 4.2.0
pino-pretty@13.1.3:
dependencies:
colorette: 2.0.20
dateformat: 4.6.3
fast-copy: 4.0.4
fast-safe-stringify: 2.1.1
help-me: 5.0.0
joycon: 3.1.1
minimist: 1.2.8
on-exit-leak-free: 2.1.2
pino-abstract-transport: 3.0.0
pump: 3.0.4
secure-json-parse: 4.1.0
sonic-boom: 4.2.1
strip-json-comments: 5.0.3
pino-std-serializers@7.1.0: {}
pino@10.3.1:
dependencies:
'@pinojs/redact': 0.4.0
atomic-sleep: 1.0.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 3.0.0
pino-std-serializers: 7.1.0
process-warning: 5.1.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.2.1
thread-stream: 4.2.0
pkce-challenge@5.0.1: {} pkce-challenge@5.0.1: {}
pkg-up@3.1.0: pkg-up@3.1.0:
@@ -8154,6 +8303,8 @@ snapshots:
dependencies: dependencies:
parse-ms: 4.0.0 parse-ms: 4.0.0
process-warning@5.1.0: {}
prompts@2.4.2: prompts@2.4.2:
dependencies: dependencies:
kleur: 3.0.3 kleur: 3.0.3
@@ -8172,6 +8323,11 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode@2.3.1: {} punycode@2.3.1: {}
qs@6.15.3: qs@6.15.3:
@@ -8181,6 +8337,8 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
range-parser@1.3.0: {} range-parser@1.3.0: {}
raw-body@3.0.2: raw-body@3.0.2:
@@ -8248,6 +8406,10 @@ snapshots:
react@19.2.8: {} react@19.2.8: {}
real-require@0.2.0: {}
real-require@1.0.0: {}
recast@0.23.21: recast@0.23.21:
dependencies: dependencies:
ast-types: 0.16.1 ast-types: 0.16.1
@@ -8369,10 +8531,14 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-regex: 1.2.1 is-regex: 1.2.1
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
scheduler@0.27.0: {} scheduler@0.27.0: {}
secure-json-parse@4.1.0: {}
semver@6.3.1: {} semver@6.3.1: {}
semver@7.8.5: {} semver@7.8.5: {}
@@ -8548,6 +8714,10 @@ snapshots:
ip-address: 10.5.0 ip-address: 10.5.0
smart-buffer: 4.2.0 smart-buffer: 4.2.0
sonic-boom@4.2.1:
dependencies:
atomic-sleep: 1.0.0
sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies: dependencies:
react: 19.2.8 react: 19.2.8
@@ -8566,6 +8736,8 @@ snapshots:
space-separated-tokens@2.0.2: {} space-separated-tokens@2.0.2: {}
split2@4.2.0: {}
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
statuses@2.0.2: {} statuses@2.0.2: {}
@@ -8661,6 +8833,8 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
strip-json-comments@5.0.3: {}
style-to-js@1.1.21: style-to-js@1.1.21:
dependencies: dependencies:
style-to-object: 1.0.14 style-to-object: 1.0.14
@@ -8690,6 +8864,10 @@ snapshots:
tapable@2.3.3: {} tapable@2.3.3: {}
thread-stream@4.2.0:
dependencies:
real-require: 1.0.0
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tinyglobby@0.2.17: tinyglobby@0.2.17:
+49 -6
View File
@@ -1,8 +1,10 @@
import "./src/bootstrap/async-local-storage-polyfill"; import "./src/bootstrap/async-local-storage-polyfill";
import { randomUUID } from "node:crypto";
import { createServer } from "node:http"; import { createServer } from "node:http";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import next from "next"; import next from "next";
import type { Level } from "pino";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { getConfig } from "./src/lib/config/load"; import { getConfig } from "./src/lib/config/load";
import { migrateOnBoot } from "./src/lib/db/client"; import { migrateOnBoot } from "./src/lib/db/client";
@@ -10,6 +12,9 @@ import { syncAuthFromConfig } from "./src/lib/auth/sync";
import { reconcileOrphanedRuns } from "./src/lib/runner/engine"; import { reconcileOrphanedRuns } from "./src/lib/runner/engine";
import { killAllRuns } from "./src/lib/runner/registry"; import { killAllRuns } from "./src/lib/runner/registry";
import { attachWsServer, authenticateUpgrade } from "./src/lib/ws/server"; import { attachWsServer, authenticateUpgrade } from "./src/lib/ws/server";
import { logger } from "./src/lib/logger";
const log = logger.child({ mod: "server" });
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
const { config } = getConfig(); const { config } = getConfig();
@@ -31,11 +36,41 @@ process.env.TRIGGERSHELL_APP_ROOT = dir;
const app = next({ dev, dir, hostname, port }); const app = next({ dev, dir, hostname, port });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
// `/_next/*` asset requests happen dozens of times per page load and carry no operational
// signal - logged at debug so they don't drown out page/API requests in the default info level.
function accessLogLevel(pathname: string, statusCode: number): Level {
if (statusCode >= 500) return "error";
if (statusCode >= 400) return "warn";
return pathname.startsWith("/_next/") ? "debug" : "info";
}
app.prepare().then(() => { app.prepare().then(() => {
const nextUpgradeHandler = app.getUpgradeHandler(); const nextUpgradeHandler = app.getUpgradeHandler();
const httpServer = createServer((req, res) => { const httpServer = createServer((req, res) => {
handle(req, res); const reqId = req.headers["x-request-id"]?.toString() ?? randomUUID();
req.headers["x-request-id"] = reqId;
const startedAt = process.hrtime.bigint();
res.on("finish", () => {
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
const { pathname } = new URL(req.url ?? "/", "http://internal");
log[accessLogLevel(pathname, res.statusCode)](
{
reqId,
method: req.method,
path: pathname,
status: res.statusCode,
durationMs: Math.round(durationMs),
},
"request",
);
});
handle(req, res).catch((error: unknown) => {
log.error({ reqId, err: error }, "unhandled error handling request");
if (!res.headersSent) res.writeHead(500).end();
});
}); });
const wss = new WebSocketServer({ noServer: true }); const wss = new WebSocketServer({ noServer: true });
@@ -52,6 +87,7 @@ app.prepare().then(() => {
authenticateUpgrade(req) authenticateUpgrade(req)
.then((ok) => { .then((ok) => {
if (!ok) { if (!ok) {
log.warn({ path: pathname }, "rejected unauthenticated WS upgrade");
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy(); socket.destroy();
return; return;
@@ -60,21 +96,28 @@ app.prepare().then(() => {
wss.emit("connection", ws, req); wss.emit("connection", ws, req);
}); });
}) })
.catch(() => socket.destroy()); .catch((error: unknown) => {
log.error({ err: error }, "error authenticating WS upgrade");
socket.destroy();
});
}); });
httpServer.listen(port, hostname, () => { httpServer.listen(port, hostname, () => {
console.log( log.info(
`> triggershell ready on http://${hostname}:${port} (${dev ? "development" : "production"})`, { hostname, port, mode: dev ? "development" : "production" },
"triggershell ready",
); );
}); });
const shutdown = (signal: string) => { const shutdown = (signal: string) => {
console.log(`> received ${signal}, shutting down...`); log.info({ signal }, "shutting down");
killAllRuns(); killAllRuns();
httpServer.close(() => process.exit(0)); httpServer.close(() => process.exit(0));
// Force-exit if graceful shutdown hangs (e.g. a stuck WS connection). // Force-exit if graceful shutdown hangs (e.g. a stuck WS connection).
setTimeout(() => process.exit(1), 5000).unref(); setTimeout(() => {
log.warn("graceful shutdown timed out, forcing exit");
process.exit(1);
}, 5000).unref();
}; };
process.on("SIGTERM", () => shutdown("SIGTERM")); process.on("SIGTERM", () => shutdown("SIGTERM"));
+10
View File
@@ -11,6 +11,9 @@ import {
recordFailedAttempt, recordFailedAttempt,
clearAttempts, clearAttempts,
} from "@/lib/auth/rate-limit"; } from "@/lib/auth/rate-limit";
import { logger } from "@/lib/logger";
const log = logger.child({ mod: "auth" });
const loginSchema = z.object({ const loginSchema = z.object({
username: z.string().min(1), username: z.string().min(1),
@@ -20,6 +23,7 @@ const loginSchema = z.object({
export async function POST(request: Request) { export async function POST(request: Request) {
const rateLimitKey = request.headers.get("x-forwarded-for") ?? "local"; const rateLimitKey = request.headers.get("x-forwarded-for") ?? "local";
if (isRateLimited(rateLimitKey)) { if (isRateLimited(rateLimitKey)) {
log.warn({ from: rateLimitKey }, "login rate-limited");
return Response.json( return Response.json(
{ error: "Too many attempts, try again later." }, { error: "Too many attempts, try again later." },
{ status: 429 }, { status: 429 },
@@ -44,6 +48,10 @@ export async function POST(request: Request) {
!(await verifyPassword(user.passwordHash, parsed.data.password)) !(await verifyPassword(user.passwordHash, parsed.data.password))
) { ) {
recordFailedAttempt(rateLimitKey); recordFailedAttempt(rateLimitKey);
log.warn(
{ from: rateLimitKey, username: parsed.data.username },
"login failed: invalid credentials",
);
return Response.json({ error: "Invalid credentials" }, { status: 401 }); return Response.json({ error: "Invalid credentials" }, { status: 401 });
} }
@@ -58,5 +66,7 @@ export async function POST(request: Request) {
session.username = user.username; session.username = user.username;
await session.save(); await session.save();
log.info({ from: rateLimitKey, username: user.username }, "login succeeded");
return Response.json({ user: { username: user.username } }); return Response.json({ user: { username: user.username } });
} }
+8
View File
@@ -5,6 +5,9 @@ import { requireAuth, unauthorizedResponse } from "@/lib/auth/guard";
import { getDb } from "@/lib/db/client"; import { getDb } from "@/lib/db/client";
import { runs } from "@/lib/db/schema"; import { runs } from "@/lib/db/schema";
import { cancelRun } from "@/lib/runner/registry"; import { cancelRun } from "@/lib/runner/registry";
import { logger } from "@/lib/logger";
const log = logger.child({ mod: "api" });
export async function POST( export async function POST(
request: Request, request: Request,
@@ -27,11 +30,16 @@ export async function POST(
const cancelled = cancelRun(runId); const cancelled = cancelRun(runId);
if (!cancelled) { if (!cancelled) {
log.warn(
{ runId },
"cancel requested for a run not tracked by this process",
);
return Response.json( return Response.json(
{ error: "Run is not active in this server process" }, { error: "Run is not active in this server process" },
{ status: 409 }, { status: 409 },
); );
} }
log.info({ runId, requestedBy: auth.identity }, "cancel requested via API");
return Response.json({ status: "cancelling" }, { status: 202 }); return Response.json({ status: "cancelling" }, { status: 202 });
} }
+9 -4
View File
@@ -4,6 +4,9 @@ import { requireAuth, unauthorizedResponse } from "@/lib/auth/guard";
import { getScript } from "@/lib/config/load"; import { getScript } from "@/lib/config/load";
import { buildVariableSchema } from "@/lib/validation/variable-schema"; import { buildVariableSchema } from "@/lib/validation/variable-schema";
import { startRun } from "@/lib/runner/engine"; import { startRun } from "@/lib/runner/engine";
import { logger } from "@/lib/logger";
const log = logger.child({ mod: "api" });
export async function POST( export async function POST(
request: Request, request: Request,
@@ -26,11 +29,13 @@ export async function POST(
const variableSchema = buildVariableSchema(script); const variableSchema = buildVariableSchema(script);
const parsed = variableSchema.safeParse(variablesInput); const parsed = variableSchema.safeParse(variablesInput);
if (!parsed.success) { if (!parsed.success) {
const fieldErrors = parsed.error.flatten().fieldErrors;
log.warn(
{ scriptId: script.id, fields: Object.keys(fieldErrors) },
"run request failed variable validation",
);
return Response.json( return Response.json(
{ { error: "Validation failed", fieldErrors },
error: "Validation failed",
fieldErrors: parsed.error.flatten().fieldErrors,
},
{ status: 400 }, { status: 400 },
); );
} }
+23
View File
@@ -126,3 +126,26 @@ export async function serviceStatusCommand(
}); });
process.exitCode = result.exitCode ?? 1; process.exitCode = result.exitCode ?? 1;
} }
export interface ServiceLogsOptions extends ServiceScopeOptions {
follow?: boolean;
lines?: number;
}
export async function serviceLogsCommand(
opts: ServiceLogsOptions,
): Promise<void> {
const scope = scopeOf(opts);
const args = scope === "user" ? ["--user"] : [];
// -o cat strips journalctl's own prefix (timestamp/hostname/unit) so each line is the raw pino
// JSON payload - pipeable straight into `jq` or similar without journalctl's wrapper in the way.
args.push("-u", SERVICE_NAME, "-o", "cat");
if (opts.follow !== false) args.push("-f");
if (opts.lines !== undefined) args.push("-n", String(opts.lines));
const result = await execa("journalctl", args, {
stdio: "inherit",
reject: false,
});
process.exitCode = result.exitCode ?? 1;
}
+18
View File
@@ -5,6 +5,7 @@ import { runCommand } from "./commands/run";
import { scriptsListCommand, scriptsShowCommand } from "./commands/scripts"; import { scriptsListCommand, scriptsShowCommand } from "./commands/scripts";
import { import {
serviceInstallCommand, serviceInstallCommand,
serviceLogsCommand,
serviceStatusCommand, serviceStatusCommand,
serviceUninstallCommand, serviceUninstallCommand,
} from "./commands/service"; } from "./commands/service";
@@ -180,6 +181,23 @@ service
) )
.action(serviceStatusCommand); .action(serviceStatusCommand);
service
.command("logs")
.description("Tail the systemd unit's logs (journalctl).")
.option("-n, --lines <n>", "Number of recent log lines to show.", (v) =>
Number(v),
)
.option(
"--no-follow",
"Print recent logs and exit instead of tailing continuously.",
)
.option(
"--system",
"Target the system-wide unit instead of the per-user one.",
false,
)
.action(serviceLogsCommand);
if (process.argv.length <= 2) { if (process.argv.length <= 2) {
program.outputHelp(); program.outputHelp();
process.exit(1); process.exit(1);
+15 -1
View File
@@ -2,12 +2,18 @@ import { eq } from "drizzle-orm";
import { getDb } from "../db/client"; import { getDb } from "../db/client";
import { users, apiTokens } from "../db/schema"; import { users, apiTokens } from "../db/schema";
import { getConfig } from "../config/load"; import { getConfig } from "../config/load";
import { logger } from "../logger";
const log = logger.child({ mod: "auth" });
/** Config is the source of truth for who's allowed in; this materializes it into SQLite so the /** Config is the source of truth for who's allowed in; this materializes it into SQLite so the
* runtime auth-check path is uniform and `lastLoginAt`/`lastUsedAt` can be tracked. Called on boot. */ * runtime auth-check path is uniform and `lastLoginAt`/`lastUsedAt` can be tracked. Called on boot. */
export function syncAuthFromConfig() { export function syncAuthFromConfig() {
const { config } = getConfig(); const { config } = getConfig();
if (!config.auth.enabled) return; if (!config.auth.enabled) {
log.debug("auth disabled, skipping config sync");
return;
}
const db = getDb(); const db = getDb();
@@ -53,4 +59,12 @@ export function syncAuthFromConfig() {
.run(); .run();
} }
} }
log.info(
{
userCount: config.auth.users.length,
tokenCount: config.auth.tokens.length,
},
"synced auth config",
);
} }
+12
View File
@@ -2,6 +2,9 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
import { configSchema, type TriggerShellConfig } from "./schema"; import { configSchema, type TriggerShellConfig } from "./schema";
import { logger } from "../logger";
const log = logger.child({ mod: "config" });
export class ConfigError extends Error { export class ConfigError extends Error {
issues: string[]; issues: string[];
@@ -90,6 +93,15 @@ declare global {
export function getConfig(): LoadedConfig { export function getConfig(): LoadedConfig {
if (!globalThis.__triggershellConfig) { if (!globalThis.__triggershellConfig) {
globalThis.__triggershellConfig = loadConfig(); globalThis.__triggershellConfig = loadConfig();
const { config, configPath } = globalThis.__triggershellConfig;
log.info(
{
configPath,
scriptCount: config.scripts.length,
authEnabled: config.auth.enabled,
},
"config loaded",
);
} }
return globalThis.__triggershellConfig; return globalThis.__triggershellConfig;
} }
+5
View File
@@ -5,6 +5,9 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import * as schema from "./schema"; import * as schema from "./schema";
import { getConfig } from "../config/load"; import { getConfig } from "../config/load";
import { logger } from "../logger";
const log = logger.child({ mod: "db" });
type Db = ReturnType<typeof drizzle<typeof schema>>; type Db = ReturnType<typeof drizzle<typeof schema>>;
@@ -25,6 +28,7 @@ export function getDb(): Db {
sqlite.pragma("journal_mode = WAL"); sqlite.pragma("journal_mode = WAL");
sqlite.pragma("foreign_keys = ON"); sqlite.pragma("foreign_keys = ON");
log.debug({ dbPath }, "opened database");
globalThis.__triggershellDb = drizzle(sqlite, { schema }); globalThis.__triggershellDb = drizzle(sqlite, { schema });
return globalThis.__triggershellDb; return globalThis.__triggershellDb;
} }
@@ -37,4 +41,5 @@ export function migrateOnBoot() {
"../../../drizzle", "../../../drizzle",
); );
migrate(db, { migrationsFolder }); migrate(db, { migrationsFolder });
log.info("database migrations applied");
} }
+65
View File
@@ -0,0 +1,65 @@
import pino from "pino";
const isProduction = process.env.NODE_ENV === "production";
const level = process.env.LOG_LEVEL ?? (isProduction ? "info" : "debug");
function createLogger(): pino.Logger {
return pino({
name: "triggershell",
level,
// Values are never logged under these key names in the first place - this is defense in
// depth against a future call site accidentally passing one through.
redact: {
paths: [
"password",
"*.password",
"token",
"*.token",
"passwordHash",
"*.passwordHash",
"tokenHash",
"*.tokenHash",
"sessionSecret",
"*.sessionSecret",
"authorization",
"*.authorization",
"cookie",
"*.cookie",
],
censor: "[REDACTED]",
},
transport: isProduction
? undefined
: {
target: "pino-pretty",
options: { colorize: true, ignore: "pid,hostname" },
},
});
}
declare global {
var __triggershellLogger: pino.Logger | undefined;
}
// Anchored on `globalThis` - see the comment in `runner/events.ts` for why: Next compiles Route
// Handlers through a separate module graph from what `server.ts` requires directly.
//
// Lazily constructed behind a Proxy rather than built at module-evaluation time: `config/load.ts`
// (imported by every CLI command for `loadConfig()`) also imports this module, and the dev-mode
// pretty transport spins up a worker thread the instant `pino({ transport })` runs. Without the
// laziness, a plain CLI invocation like `triggershell scripts list` would pay for a logger no
// backend code path in that process will ever call.
function ensureLogger(): pino.Logger {
if (!globalThis.__triggershellLogger) {
globalThis.__triggershellLogger = createLogger();
}
return globalThis.__triggershellLogger;
}
export const logger: pino.Logger = new Proxy({} as pino.Logger, {
get(_target, prop) {
const target = ensureLogger();
const value = Reflect.get(target, prop, target);
return typeof value === "function" ? value.bind(target) : value;
},
});
+40 -2
View File
@@ -9,6 +9,9 @@ import type { ScriptConfig } from "../config/schema";
import { buildInvocation, type Invocation } from "./build-args"; import { buildInvocation, type Invocation } from "./build-args";
import { registerRun, unregisterRun } from "./registry"; import { registerRun, unregisterRun } from "./registry";
import { emitRunMessage } from "./events"; import { emitRunMessage } from "./events";
import { logger } from "../logger";
const log = logger.child({ mod: "runner" });
export class ScriptNotFoundError extends Error {} export class ScriptNotFoundError extends Error {}
@@ -49,9 +52,14 @@ export async function startRun({
}) })
.run(); .run();
log.info({ runId, scriptId: script.id, triggeredBy }, "run queued");
// Fire and forget - the caller gets the runId immediately, progress streams over WS/polling. // Fire and forget - the caller gets the runId immediately, progress streams over WS/polling.
void executeRun(runId, script, invocation, logFilePath).catch((error) => { void executeRun(runId, script, invocation, logFilePath).catch((error) => {
console.error(`[runner] unhandled error executing run ${runId}:`, error); log.error(
{ runId, scriptId: script.id, err: error },
"unhandled error executing run",
);
}); });
return runId; return runId;
@@ -103,6 +111,11 @@ async function executeRun(
registerRun({ runId, scriptId: script.id, controller }); registerRun({ runId, scriptId: script.id, controller });
setStatus("running", { startedAt: new Date() }); setStatus("running", { startedAt: new Date() });
log.info(
{ runId, scriptId: script.id, command: script.command },
"run started",
);
const startedAt = process.hrtime.bigint();
try { try {
const subprocess = execa(script.command, invocation.argv, { const subprocess = execa(script.command, invocation.argv, {
@@ -145,11 +158,26 @@ async function executeRun(
? (result.shortMessage ?? null) ? (result.shortMessage ?? null)
: null, : null,
}); });
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
log[status === "succeeded" ? "info" : "warn"](
{
runId,
scriptId: script.id,
status,
exitCode: result.exitCode ?? null,
durationMs: Math.round(durationMs),
},
"run finished",
);
} catch (error) { } catch (error) {
setStatus("failed", { setStatus("failed", {
endedAt: new Date(), endedAt: new Date(),
errorMessage: (error as Error).message, errorMessage: (error as Error).message,
}); });
log.error(
{ runId, scriptId: script.id, err: error },
"run failed to execute",
);
} finally { } finally {
logStream.end(); logStream.end();
unregisterRun(runId); unregisterRun(runId);
@@ -170,7 +198,8 @@ export function reconcileOrphanedRuns() {
}) })
.where(eq(runs.status, "running")) .where(eq(runs.status, "running"))
.run(); .run();
db.update(runs) const queued = db
.update(runs)
.set({ .set({
status: "interrupted", status: "interrupted",
endedAt: now, endedAt: now,
@@ -178,5 +207,14 @@ export function reconcileOrphanedRuns() {
}) })
.where(eq(runs.status, "queued")) .where(eq(runs.status, "queued"))
.run(); .run();
const interruptedCount = orphaned.changes + queued.changes;
if (interruptedCount > 0) {
log.warn(
{ runningCount: orphaned.changes, queuedCount: queued.changes },
"marked orphaned runs as interrupted after restart",
);
}
return orphaned; return orphaned;
} }
+18 -2
View File
@@ -13,8 +13,11 @@ import { apiTokens, runs } from "../db/schema";
import { runEvents } from "../runner/events"; import { runEvents } from "../runner/events";
import { cancelRun } from "../runner/registry"; import { cancelRun } from "../runner/registry";
import { readLogSince } from "../runner/log-file"; import { readLogSince } from "../runner/log-file";
import { logger } from "../logger";
import { isClientMessage, type ServerMessage } from "./protocol"; import { isClientMessage, type ServerMessage } from "./protocol";
const log = logger.child({ mod: "ws" });
const subscriptions = new Map<string, Set<WebSocket>>(); const subscriptions = new Map<string, Set<WebSocket>>();
/** A run can start and finish (emitting all its output+status over `runEvents`) before a /** A run can start and finish (emitting all its output+status over `runEvents`) before a
@@ -104,28 +107,41 @@ export async function authenticateUpgrade(
export function attachWsServer(wss: WebSocketServer) { export function attachWsServer(wss: WebSocketServer) {
wss.on("connection", (ws: WebSocket) => { wss.on("connection", (ws: WebSocket) => {
const connId = crypto.randomUUID();
log.debug({ connId }, "connection opened");
ws.on("message", (raw) => { ws.on("message", (raw) => {
let parsed: unknown; let parsed: unknown;
try { try {
parsed = JSON.parse(raw.toString()); parsed = JSON.parse(raw.toString());
} catch { } catch {
log.warn({ connId }, "dropped unparseable WS message");
return;
}
if (!isClientMessage(parsed)) {
log.warn({ connId, parsed }, "dropped unrecognized WS message");
return; return;
} }
if (!isClientMessage(parsed)) return;
switch (parsed.type) { switch (parsed.type) {
case "subscribe": case "subscribe":
log.debug({ connId, runId: parsed.runId }, "subscribe");
subscribe(parsed.runId, ws, parsed.afterBytes ?? 0); subscribe(parsed.runId, ws, parsed.afterBytes ?? 0);
break; break;
case "unsubscribe": case "unsubscribe":
log.debug({ connId, runId: parsed.runId }, "unsubscribe");
unsubscribe(parsed.runId, ws); unsubscribe(parsed.runId, ws);
break; break;
case "cancel": case "cancel":
log.info({ connId, runId: parsed.runId }, "cancel requested via WS");
cancelRun(parsed.runId); cancelRun(parsed.runId);
break; break;
} }
}); });
ws.on("close", () => unsubscribeAll(ws)); ws.on("close", () => {
log.debug({ connId }, "connection closed");
unsubscribeAll(ws);
});
}); });
} }
+8
View File
@@ -2,6 +2,9 @@ import { NextResponse } from "next/server";
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { getConfig } from "@/lib/config/load"; import { getConfig } from "@/lib/config/load";
import { verifySessionCookieValue } from "@/lib/auth/session"; import { verifySessionCookieValue } from "@/lib/auth/session";
import { logger } from "@/lib/logger";
const log = logger.child({ mod: "proxy" });
const PUBLIC_PATHS = [ const PUBLIC_PATHS = [
"/login", "/login",
@@ -39,8 +42,13 @@ export async function proxy(request: NextRequest) {
if (!session?.userId) { if (!session?.userId) {
if (isApiRoute) { if (isApiRoute) {
log.warn({ path: pathname }, "blocked unauthenticated API request");
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
log.debug(
{ path: pathname },
"redirected unauthenticated request to login",
);
const loginUrl = new URL("/login", request.url); const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("next", pathname); loginUrl.searchParams.set("next", pathname);
return NextResponse.redirect(loginUrl); return NextResponse.redirect(loginUrl);