diff --git a/package.json b/package.json index 39f950f..9ff43cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "triggershell", - "version": "0.1.0", + "version": "1.0.4", "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c9a49f..4bcab4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/server.ts b/server.ts index 1dffe4d..611a33a 100644 --- a/server.ts +++ b/server.ts @@ -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")); diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index ada2cb2..4894456 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -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 } }); } diff --git a/src/app/api/runs/[runId]/cancel/route.ts b/src/app/api/runs/[runId]/cancel/route.ts index 6f556b3..dc04785 100644 --- a/src/app/api/runs/[runId]/cancel/route.ts +++ b/src/app/api/runs/[runId]/cancel/route.ts @@ -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 }); } diff --git a/src/app/api/scripts/[scriptId]/runs/route.ts b/src/app/api/scripts/[scriptId]/runs/route.ts index 1003bbc..2dd3a07 100644 --- a/src/app/api/scripts/[scriptId]/runs/route.ts +++ b/src/app/api/scripts/[scriptId]/runs/route.ts @@ -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 }, ); } diff --git a/src/lib/auth/sync.ts b/src/lib/auth/sync.ts index a8d2709..63dfe9a 100644 --- a/src/lib/auth/sync.ts +++ b/src/lib/auth/sync.ts @@ -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", + ); } diff --git a/src/lib/config/load.ts b/src/lib/config/load.ts index fd95dfb..daa17eb 100644 --- a/src/lib/config/load.ts +++ b/src/lib/config/load.ts @@ -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; } diff --git a/src/lib/db/client.ts b/src/lib/db/client.ts index 095c21a..a4279be 100644 --- a/src/lib/db/client.ts +++ b/src/lib/db/client.ts @@ -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>; @@ -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"); } diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..dc1585a --- /dev/null +++ b/src/lib/logger.ts @@ -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; + }, +}); diff --git a/src/lib/runner/engine.ts b/src/lib/runner/engine.ts index bbbe869..3dade7f 100644 --- a/src/lib/runner/engine.ts +++ b/src/lib/runner/engine.ts @@ -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; } diff --git a/src/lib/ws/server.ts b/src/lib/ws/server.ts index 9d7360f..df3e2d5 100644 --- a/src/lib/ws/server.ts +++ b/src/lib/ws/server.ts @@ -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>(); /** 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); + }); }); } diff --git a/src/proxy.ts b/src/proxy.ts index fc21c3a..743f32d 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -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);