Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90d68f2510 | ||
|
|
1fa8c5ba66 | ||
|
|
b291a119d5 | ||
|
|
e63129d156 | ||
|
|
96a66fc857 | ||
|
|
3272b9db76 | ||
|
|
06edc60b55 | ||
|
|
14158047ac | ||
|
|
e5fb28ef31 |
@@ -1,12 +1,12 @@
|
||||
name: Release
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
@@ -26,6 +26,24 @@ jobs:
|
||||
- run: pnpm run format:check
|
||||
- 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
|
||||
run: npm pkg set version="${GITHUB_REF_NAME#v}"
|
||||
|
||||
@@ -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 uninstall [--system]` | Stop, disable, and remove the systemd unit |
|
||||
| `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
|
||||
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ When `auth.enabled: true`, every endpoint below except `/api/healthz`, `/api/aut
|
||||
header using a token from `triggershell users add-token`.
|
||||
|
||||
`triggershell run <scriptId>` is a first-party client of this exact REST + WS contract (see
|
||||
[Running scripts from the CLI](../README.md#running-scripts-from-the-cli)) - nothing below is
|
||||
CLI-specific.
|
||||
[Running scripts from the CLI](https://dev.pivoine.art/valknar/triggershell/src/branch/main/README.md#running-scripts-from-the-cli)) -
|
||||
nothing below is CLI-specific.
|
||||
|
||||
## Auth
|
||||
|
||||
|
||||
@@ -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.
|
||||
`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
|
||||
actually starts listening and running scripts. `triggershell service status`/`uninstall` are thin
|
||||
wrappers around `systemctl`; log tailing is just `journalctl --user -u triggershell -f` — not
|
||||
reimplemented.
|
||||
actually starts listening and running scripts. `triggershell service status`/`uninstall`/`logs` are
|
||||
thin wrappers around `systemctl`/`journalctl` respectively - no unit-file parsing or log storage of
|
||||
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
|
||||
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "triggershell",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
@@ -60,6 +60,7 @@
|
||||
"lucide-react": "^1.31.0",
|
||||
"next": "16.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"pino": "^10.3.1",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"react-hook-form": "^7.85.0",
|
||||
@@ -86,6 +87,7 @@
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
Generated
+178
@@ -59,6 +59,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
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:
|
||||
specifier: 19.2.8
|
||||
version: 19.2.8
|
||||
@@ -132,6 +135,9 @@ importers:
|
||||
eslint-config-next:
|
||||
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)
|
||||
pino-pretty:
|
||||
specifier: ^13.1.3
|
||||
version: 13.1.3
|
||||
prettier:
|
||||
specifier: ^3.9.6
|
||||
version: 3.9.6
|
||||
@@ -1358,6 +1364,9 @@ packages:
|
||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
'@radix-ui/primitive@1.1.7':
|
||||
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
|
||||
|
||||
@@ -1987,6 +1996,10 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
@@ -2136,6 +2149,9 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
@@ -2226,6 +2242,9 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2439,6 +2458,9 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
enhanced-resolve@5.24.5:
|
||||
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -2685,6 +2707,9 @@ packages:
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-copy@4.0.4:
|
||||
resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -2702,6 +2727,9 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
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:
|
||||
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
|
||||
|
||||
@@ -2894,6 +2922,9 @@ packages:
|
||||
hast-util-whitespace@3.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
@@ -3172,6 +3203,10 @@ packages:
|
||||
jose@6.2.8:
|
||||
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:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -3649,6 +3684,10 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -3759,6 +3798,20 @@ packages:
|
||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
@@ -3863,6 +3916,9 @@ packages:
|
||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
process-warning@5.1.0:
|
||||
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
||||
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3877,6 +3933,9 @@ packages:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3888,6 +3947,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
range-parser@1.3.0:
|
||||
resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3950,6 +4012,13 @@ packages:
|
||||
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4024,12 +4093,19 @@ packages:
|
||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -4118,6 +4194,9 @@ packages:
|
||||
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
sonic-boom@4.2.1:
|
||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||
|
||||
sonner@2.0.8:
|
||||
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||
peerDependencies:
|
||||
@@ -4142,6 +4221,10 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
stable-hash@0.0.5:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
@@ -4215,6 +4298,10 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-json-comments@5.0.3:
|
||||
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
||||
|
||||
@@ -4258,6 +4345,10 @@ packages:
|
||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
thread-stream@4.2.0:
|
||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -5447,6 +5538,8 @@ snapshots:
|
||||
|
||||
'@phc/format@1.0.0': {}
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.7': {}
|
||||
|
||||
'@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: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
atomically@1.7.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
@@ -6172,6 +6267,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@11.1.0: {}
|
||||
@@ -6256,6 +6353,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
dependencies:
|
||||
mimic-fn: 3.1.0
|
||||
@@ -6357,6 +6456,10 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
enhanced-resolve@5.24.5:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -6877,6 +6980,8 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-copy@4.0.4: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.1:
|
||||
@@ -6899,6 +7004,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-safe-stringify@2.1.1: {}
|
||||
|
||||
fast-string-truncated-width@3.0.3: {}
|
||||
|
||||
fast-string-width@3.0.2:
|
||||
@@ -7105,6 +7212,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.5
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
hermes-estree@0.25.1: {}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
@@ -7350,6 +7459,8 @@ snapshots:
|
||||
|
||||
jose@6.2.8: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
@@ -7986,6 +8097,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.2
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
@@ -8108,6 +8221,42 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
pkg-up@3.1.0:
|
||||
@@ -8154,6 +8303,8 @@ snapshots:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
|
||||
process-warning@5.1.0: {}
|
||||
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
@@ -8172,6 +8323,11 @@ snapshots:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
pump@3.0.4:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qs@6.15.3:
|
||||
@@ -8181,6 +8337,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
range-parser@1.3.0: {}
|
||||
|
||||
raw-body@3.0.2:
|
||||
@@ -8248,6 +8406,10 @@ snapshots:
|
||||
|
||||
react@19.2.8: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
real-require@1.0.0: {}
|
||||
|
||||
recast@0.23.21:
|
||||
dependencies:
|
||||
ast-types: 0.16.1
|
||||
@@ -8369,10 +8531,14 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
secure-json-parse@4.1.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
@@ -8548,6 +8714,10 @@ snapshots:
|
||||
ip-address: 10.5.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):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
@@ -8566,6 +8736,8 @@ snapshots:
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
@@ -8661,6 +8833,8 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
strip-json-comments@5.0.3: {}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
dependencies:
|
||||
style-to-object: 1.0.14
|
||||
@@ -8690,6 +8864,10 @@ snapshots:
|
||||
|
||||
tapable@2.3.3: {}
|
||||
|
||||
thread-stream@4.2.0:
|
||||
dependencies:
|
||||
real-require: 1.0.0
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import "./src/bootstrap/async-local-storage-polyfill";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import next from "next";
|
||||
import type { Level } from "pino";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { getConfig } from "./src/lib/config/load";
|
||||
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 { killAllRuns } from "./src/lib/runner/registry";
|
||||
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 { config } = getConfig();
|
||||
@@ -31,11 +36,41 @@ process.env.TRIGGERSHELL_APP_ROOT = dir;
|
||||
const app = next({ dev, dir, hostname, port });
|
||||
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(() => {
|
||||
const nextUpgradeHandler = app.getUpgradeHandler();
|
||||
|
||||
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 });
|
||||
@@ -52,6 +87,7 @@ app.prepare().then(() => {
|
||||
authenticateUpgrade(req)
|
||||
.then((ok) => {
|
||||
if (!ok) {
|
||||
log.warn({ path: pathname }, "rejected unauthenticated WS upgrade");
|
||||
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
||||
socket.destroy();
|
||||
return;
|
||||
@@ -60,21 +96,28 @@ app.prepare().then(() => {
|
||||
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, () => {
|
||||
console.log(
|
||||
`> triggershell ready on http://${hostname}:${port} (${dev ? "development" : "production"})`,
|
||||
log.info(
|
||||
{ hostname, port, mode: dev ? "development" : "production" },
|
||||
"triggershell ready",
|
||||
);
|
||||
});
|
||||
|
||||
const shutdown = (signal: string) => {
|
||||
console.log(`> received ${signal}, shutting down...`);
|
||||
log.info({ signal }, "shutting down");
|
||||
killAllRuns();
|
||||
httpServer.close(() => process.exit(0));
|
||||
// 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"));
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function DocPage({ params }: DocPageProps) {
|
||||
if (content === null) notFound();
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-6">
|
||||
<div className="mx-auto flex max-w-5xl flex-col gap-6">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-muted-foreground hover:text-foreground flex w-fit items-center gap-1 text-sm"
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
recordFailedAttempt,
|
||||
clearAttempts,
|
||||
} from "@/lib/auth/rate-limit";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({ mod: "auth" });
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
@@ -20,6 +23,7 @@ const loginSchema = z.object({
|
||||
export async function POST(request: Request) {
|
||||
const rateLimitKey = request.headers.get("x-forwarded-for") ?? "local";
|
||||
if (isRateLimited(rateLimitKey)) {
|
||||
log.warn({ from: rateLimitKey }, "login rate-limited");
|
||||
return Response.json(
|
||||
{ error: "Too many attempts, try again later." },
|
||||
{ status: 429 },
|
||||
@@ -44,6 +48,10 @@ export async function POST(request: Request) {
|
||||
!(await verifyPassword(user.passwordHash, parsed.data.password))
|
||||
) {
|
||||
recordFailedAttempt(rateLimitKey);
|
||||
log.warn(
|
||||
{ from: rateLimitKey, username: parsed.data.username },
|
||||
"login failed: invalid credentials",
|
||||
);
|
||||
return Response.json({ error: "Invalid credentials" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -58,5 +66,7 @@ export async function POST(request: Request) {
|
||||
session.username = user.username;
|
||||
await session.save();
|
||||
|
||||
log.info({ from: rateLimitKey, username: user.username }, "login succeeded");
|
||||
|
||||
return Response.json({ user: { username: user.username } });
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { requireAuth, unauthorizedResponse } from "@/lib/auth/guard";
|
||||
import { getDb } from "@/lib/db/client";
|
||||
import { runs } from "@/lib/db/schema";
|
||||
import { cancelRun } from "@/lib/runner/registry";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({ mod: "api" });
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -27,11 +30,16 @@ export async function POST(
|
||||
|
||||
const cancelled = cancelRun(runId);
|
||||
if (!cancelled) {
|
||||
log.warn(
|
||||
{ runId },
|
||||
"cancel requested for a run not tracked by this process",
|
||||
);
|
||||
return Response.json(
|
||||
{ error: "Run is not active in this server process" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
log.info({ runId, requestedBy: auth.identity }, "cancel requested via API");
|
||||
return Response.json({ status: "cancelling" }, { status: 202 });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { requireAuth, unauthorizedResponse } from "@/lib/auth/guard";
|
||||
import { getScript } from "@/lib/config/load";
|
||||
import { buildVariableSchema } from "@/lib/validation/variable-schema";
|
||||
import { startRun } from "@/lib/runner/engine";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({ mod: "api" });
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -26,11 +29,13 @@ export async function POST(
|
||||
const variableSchema = buildVariableSchema(script);
|
||||
const parsed = variableSchema.safeParse(variablesInput);
|
||||
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(
|
||||
{
|
||||
error: "Validation failed",
|
||||
fieldErrors: parsed.error.flatten().fieldErrors,
|
||||
},
|
||||
{ error: "Validation failed", fieldErrors },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,3 +126,26 @@ export async function serviceStatusCommand(
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { runCommand } from "./commands/run";
|
||||
import { scriptsListCommand, scriptsShowCommand } from "./commands/scripts";
|
||||
import {
|
||||
serviceInstallCommand,
|
||||
serviceLogsCommand,
|
||||
serviceStatusCommand,
|
||||
serviceUninstallCommand,
|
||||
} from "./commands/service";
|
||||
@@ -180,6 +181,23 @@ service
|
||||
)
|
||||
.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) {
|
||||
program.outputHelp();
|
||||
process.exit(1);
|
||||
|
||||
+15
-1
@@ -2,12 +2,18 @@ import { eq } from "drizzle-orm";
|
||||
import { getDb } from "../db/client";
|
||||
import { users, apiTokens } from "../db/schema";
|
||||
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
|
||||
* runtime auth-check path is uniform and `lastLoginAt`/`lastUsedAt` can be tracked. Called on boot. */
|
||||
export function syncAuthFromConfig() {
|
||||
const { config } = getConfig();
|
||||
if (!config.auth.enabled) return;
|
||||
if (!config.auth.enabled) {
|
||||
log.debug("auth disabled, skipping config sync");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
@@ -53,4 +59,12 @@ export function syncAuthFromConfig() {
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
{
|
||||
userCount: config.auth.users.length,
|
||||
tokenCount: config.auth.tokens.length,
|
||||
},
|
||||
"synced auth config",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { configSchema, type TriggerShellConfig } from "./schema";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const log = logger.child({ mod: "config" });
|
||||
|
||||
export class ConfigError extends Error {
|
||||
issues: string[];
|
||||
@@ -90,6 +93,15 @@ declare global {
|
||||
export function getConfig(): LoadedConfig {
|
||||
if (!globalThis.__triggershellConfig) {
|
||||
globalThis.__triggershellConfig = loadConfig();
|
||||
const { config, configPath } = globalThis.__triggershellConfig;
|
||||
log.info(
|
||||
{
|
||||
configPath,
|
||||
scriptCount: config.scripts.length,
|
||||
authEnabled: config.auth.enabled,
|
||||
},
|
||||
"config loaded",
|
||||
);
|
||||
}
|
||||
return globalThis.__triggershellConfig;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import * as schema from "./schema";
|
||||
import { getConfig } from "../config/load";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const log = logger.child({ mod: "db" });
|
||||
|
||||
type Db = ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
||||
@@ -25,6 +28,7 @@ export function getDb(): Db {
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
|
||||
log.debug({ dbPath }, "opened database");
|
||||
globalThis.__triggershellDb = drizzle(sqlite, { schema });
|
||||
return globalThis.__triggershellDb;
|
||||
}
|
||||
@@ -37,4 +41,5 @@ export function migrateOnBoot() {
|
||||
"../../../drizzle",
|
||||
);
|
||||
migrate(db, { migrationsFolder });
|
||||
log.info("database migrations applied");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import type { ScriptConfig } from "../config/schema";
|
||||
import { buildInvocation, type Invocation } from "./build-args";
|
||||
import { registerRun, unregisterRun } from "./registry";
|
||||
import { emitRunMessage } from "./events";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const log = logger.child({ mod: "runner" });
|
||||
|
||||
export class ScriptNotFoundError extends Error {}
|
||||
|
||||
@@ -49,9 +52,14 @@ export async function startRun({
|
||||
})
|
||||
.run();
|
||||
|
||||
log.info({ runId, scriptId: script.id, triggeredBy }, "run queued");
|
||||
|
||||
// Fire and forget - the caller gets the runId immediately, progress streams over WS/polling.
|
||||
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;
|
||||
@@ -103,6 +111,11 @@ async function executeRun(
|
||||
|
||||
registerRun({ runId, scriptId: script.id, controller });
|
||||
setStatus("running", { startedAt: new Date() });
|
||||
log.info(
|
||||
{ runId, scriptId: script.id, command: script.command },
|
||||
"run started",
|
||||
);
|
||||
const startedAt = process.hrtime.bigint();
|
||||
|
||||
try {
|
||||
const subprocess = execa(script.command, invocation.argv, {
|
||||
@@ -145,11 +158,26 @@ async function executeRun(
|
||||
? (result.shortMessage ?? 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) {
|
||||
setStatus("failed", {
|
||||
endedAt: new Date(),
|
||||
errorMessage: (error as Error).message,
|
||||
});
|
||||
log.error(
|
||||
{ runId, scriptId: script.id, err: error },
|
||||
"run failed to execute",
|
||||
);
|
||||
} finally {
|
||||
logStream.end();
|
||||
unregisterRun(runId);
|
||||
@@ -170,7 +198,8 @@ export function reconcileOrphanedRuns() {
|
||||
})
|
||||
.where(eq(runs.status, "running"))
|
||||
.run();
|
||||
db.update(runs)
|
||||
const queued = db
|
||||
.update(runs)
|
||||
.set({
|
||||
status: "interrupted",
|
||||
endedAt: now,
|
||||
@@ -178,5 +207,14 @@ export function reconcileOrphanedRuns() {
|
||||
})
|
||||
.where(eq(runs.status, "queued"))
|
||||
.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;
|
||||
}
|
||||
|
||||
+18
-2
@@ -13,8 +13,11 @@ import { apiTokens, runs } from "../db/schema";
|
||||
import { runEvents } from "../runner/events";
|
||||
import { cancelRun } from "../runner/registry";
|
||||
import { readLogSince } from "../runner/log-file";
|
||||
import { logger } from "../logger";
|
||||
import { isClientMessage, type ServerMessage } from "./protocol";
|
||||
|
||||
const log = logger.child({ mod: "ws" });
|
||||
|
||||
const subscriptions = new Map<string, Set<WebSocket>>();
|
||||
|
||||
/** 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) {
|
||||
wss.on("connection", (ws: WebSocket) => {
|
||||
const connId = crypto.randomUUID();
|
||||
log.debug({ connId }, "connection opened");
|
||||
|
||||
ws.on("message", (raw) => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
log.warn({ connId }, "dropped unparseable WS message");
|
||||
return;
|
||||
}
|
||||
if (!isClientMessage(parsed)) {
|
||||
log.warn({ connId, parsed }, "dropped unrecognized WS message");
|
||||
return;
|
||||
}
|
||||
if (!isClientMessage(parsed)) return;
|
||||
|
||||
switch (parsed.type) {
|
||||
case "subscribe":
|
||||
log.debug({ connId, runId: parsed.runId }, "subscribe");
|
||||
subscribe(parsed.runId, ws, parsed.afterBytes ?? 0);
|
||||
break;
|
||||
case "unsubscribe":
|
||||
log.debug({ connId, runId: parsed.runId }, "unsubscribe");
|
||||
unsubscribe(parsed.runId, ws);
|
||||
break;
|
||||
case "cancel":
|
||||
log.info({ connId, runId: parsed.runId }, "cancel requested via WS");
|
||||
cancelRun(parsed.runId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => unsubscribeAll(ws));
|
||||
ws.on("close", () => {
|
||||
log.debug({ connId }, "connection closed");
|
||||
unsubscribeAll(ws);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { getConfig } from "@/lib/config/load";
|
||||
import { verifySessionCookieValue } from "@/lib/auth/session";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({ mod: "proxy" });
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
"/login",
|
||||
@@ -39,8 +42,13 @@ export async function proxy(request: NextRequest) {
|
||||
|
||||
if (!session?.userId) {
|
||||
if (isApiRoute) {
|
||||
log.warn({ path: pathname }, "blocked unauthenticated API request");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
log.debug(
|
||||
{ path: pathname },
|
||||
"redirected unauthenticated request to login",
|
||||
);
|
||||
const loginUrl = new URL("/login", request.url);
|
||||
loginUrl.searchParams.set("next", pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
|
||||
Reference in New Issue
Block a user