✨ Better log viewer in launchpad
This commit is contained in:
17
go.mod
17
go.mod
@@ -3,6 +3,9 @@ module git.solsynth.dev/goatworks/turbine
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3
|
github.com/gofiber/fiber/v3 v3.0.0-rc.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
@@ -13,8 +16,14 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/coreos/go-semver v0.3.1 // indirect
|
github.com/coreos/go-semver v0.3.1 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gofiber/schema v1.6.0 // indirect
|
github.com/gofiber/schema v1.6.0 // indirect
|
||||||
@@ -24,10 +33,17 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
@@ -37,6 +53,7 @@ require (
|
|||||||
github.com/tinylib/msgp v1.5.0 // indirect
|
github.com/tinylib/msgp v1.5.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
|
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -1,11 +1,29 @@
|
|||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -45,6 +63,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -52,6 +72,16 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
@@ -59,6 +89,9 @@ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJ
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
@@ -90,6 +123,8 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn
|
|||||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -125,6 +160,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -139,6 +176,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||||
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/logview"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,20 +23,25 @@ var colors = []string{
|
|||||||
"\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", "\033[31m",
|
"\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", "\033[31m",
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorReset = "\033[0m"
|
// RunDev starts selected services defined in the config in development mode.
|
||||||
|
func RunDev(cfg config.LaunchpadConfig, servicesToStart []config.Service) {
|
||||||
// RunDev starts all services defined in the config in development mode.
|
|
||||||
func RunDev(cfg config.LaunchpadConfig) {
|
|
||||||
log.Info().Msg("Starting services in development mode with dependency checks...")
|
log.Info().Msg("Starting services in development mode with dependency checks...")
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
defer func() {
|
||||||
|
log.Info().Msg("Shutting down all services...")
|
||||||
|
cancel()
|
||||||
|
wg.Wait()
|
||||||
|
log.Info().Msg("All services have been shut down.")
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
sig := <-sigChan
|
<-sigChan
|
||||||
log.Info().Msgf("Received signal: %v. Shutting down all services...", sig)
|
log.Info().Msg("Shutdown signal received.")
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -48,33 +54,43 @@ func RunDev(cfg config.LaunchpadConfig) {
|
|||||||
}
|
}
|
||||||
createDockerNetwork(devNetwork)
|
createDockerNetwork(devNetwork)
|
||||||
|
|
||||||
// --- Dependency-aware startup ---
|
|
||||||
serviceMap := make(map[string]config.Service)
|
serviceMap := make(map[string]config.Service)
|
||||||
for _, s := range cfg.Services {
|
for _, s := range cfg.Services {
|
||||||
serviceMap[s.Name] = s
|
serviceMap[s.Name] = s
|
||||||
}
|
}
|
||||||
|
|
||||||
started := make(map[string]chan bool)
|
started := make(map[string]chan bool)
|
||||||
for _, s := range cfg.Services {
|
serviceNamesToStart := []string{}
|
||||||
|
for _, s := range servicesToStart {
|
||||||
|
serviceNamesToStart = append(serviceNamesToStart, s.Name)
|
||||||
|
}
|
||||||
|
log.Info().Msgf("Attempting to start: %s", strings.Join(serviceNamesToStart, ", "))
|
||||||
|
|
||||||
|
logChan := make(chan logview.LogMessage, 100)
|
||||||
|
|
||||||
|
for i, s := range servicesToStart {
|
||||||
if _, exists := started[s.Name]; !exists {
|
if _, exists := started[s.Name]; !exists {
|
||||||
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork)
|
color := colors[i%len(colors)]
|
||||||
|
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork, color, logChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't start the log viewer if no services were selected to run
|
||||||
|
if len(servicesToStart) > 0 {
|
||||||
|
if err := logview.Start(logChan, serviceNamesToStart); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Log viewer failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
log.Info().Msg("All services have been shut down.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network string) {
|
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network, color string, logChan chan<- logview.LogMessage) {
|
||||||
if _, exists := started[s.Name]; exists {
|
if _, exists := started[s.Name]; exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a channel that will be closed when this service is healthy
|
|
||||||
healthyChan := make(chan bool)
|
healthyChan := make(chan bool)
|
||||||
started[s.Name] = healthyChan
|
started[s.Name] = healthyChan
|
||||||
|
|
||||||
// First, recursively start dependencies
|
|
||||||
var depNames []string
|
var depNames []string
|
||||||
for depName := range s.Prod.DependsOn {
|
for depName := range s.Prod.DependsOn {
|
||||||
depNames = append(depNames, depName)
|
depNames = append(depNames, depName)
|
||||||
@@ -82,11 +98,11 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
|
|||||||
|
|
||||||
for _, depName := range depNames {
|
for _, depName := range depNames {
|
||||||
if dep, ok := serviceMap[depName]; ok {
|
if dep, ok := serviceMap[depName]; ok {
|
||||||
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network)
|
// Dependencies get a default color for now
|
||||||
|
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network, "\033[37m", logChan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for dependencies to be healthy
|
|
||||||
log.Info().Str("service", s.Name).Msgf("Waiting for dependencies to be healthy: %v", depNames)
|
log.Info().Str("service", s.Name).Msgf("Waiting for dependencies to be healthy: %v", depNames)
|
||||||
for _, depName := range depNames {
|
for _, depName := range depNames {
|
||||||
if depChan, ok := started[depName]; ok {
|
if depChan, ok := started[depName]; ok {
|
||||||
@@ -101,33 +117,29 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, start the actual service
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(s config.Service, color string) {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var healthCheckPorts []int
|
var healthCheckPorts []int
|
||||||
if s.Type == "docker" {
|
if s.Type == "docker" {
|
||||||
// For docker, we use the dev healthcheck ports to also map them
|
|
||||||
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
||||||
startDockerService(ctx, s, color, network, healthCheckPorts)
|
startDockerService(ctx, s, color, network, healthCheckPorts, logChan)
|
||||||
} else if s.Dev.Command != "" {
|
} else if s.Dev.Command != "" {
|
||||||
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
||||||
startSourceService(ctx, s, color)
|
startSourceService(ctx, s, color, logChan)
|
||||||
} else {
|
} else {
|
||||||
log.Warn().Str("service", s.Name).Msg("No dev.command or docker type, skipping.")
|
log.Warn().Str("service", s.Name).Msg("No dev.command or docker type, skipping.")
|
||||||
close(healthyChan) // Mark as "healthy" so other things can proceed
|
close(healthyChan)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform health check on the service we just started
|
|
||||||
waitForHealth(ctx, s.Name, healthCheckPorts)
|
waitForHealth(ctx, s.Name, healthCheckPorts)
|
||||||
close(healthyChan) // Signal that this service is now healthy
|
close(healthyChan)
|
||||||
|
|
||||||
// Block until context is cancelled to keep the service running
|
|
||||||
// and ensure wg.Done is called at the right time.
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
}(s, colors[len(started)%len(colors)])
|
}(
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForHealth(ctx context.Context, serviceName string, ports []int) {
|
func waitForHealth(ctx context.Context, serviceName string, ports []int) {
|
||||||
@@ -158,9 +170,7 @@ func waitForHealth(ctx context.Context, serviceName string, ports []int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSourceService runs a service from its source code.
|
func startSourceService(ctx context.Context, s config.Service, color string, logChan chan<- logview.LogMessage) {
|
||||||
func startSourceService(ctx context.Context, s config.Service, color string) {
|
|
||||||
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
|
|
||||||
log.Info().Str("service", s.Name).Str("command", s.Dev.Command).Msg("Starting from source")
|
log.Info().Str("service", s.Name).Str("command", s.Dev.Command).Msg("Starting from source")
|
||||||
|
|
||||||
parts := strings.Fields(s.Dev.Command)
|
parts := strings.Fields(s.Dev.Command)
|
||||||
@@ -173,12 +183,10 @@ func startSourceService(ctx context.Context, s config.Service, color string) {
|
|||||||
}
|
}
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
|
|
||||||
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
|
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startDockerService runs a pre-built Docker image.
|
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int, logChan chan<- logview.LogMessage) {
|
||||||
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int) {
|
|
||||||
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
|
|
||||||
log.Info().Str("service", s.Name).Str("image", s.Prod.Image).Msg("Starting from Docker image")
|
log.Info().Str("service", s.Name).Str("image", s.Prod.Image).Msg("Starting from Docker image")
|
||||||
|
|
||||||
containerName := fmt.Sprintf("%s-dev", s.Name)
|
containerName := fmt.Sprintf("%s-dev", s.Name)
|
||||||
@@ -218,10 +226,10 @@ func startDockerService(ctx context.Context, s config.Service, color string, net
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
cmd := exec.Command("docker", args...)
|
cmd := exec.Command("docker", args...)
|
||||||
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
|
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefix string) {
|
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, color string, logChan chan<- logview.LogMessage) {
|
||||||
stdout, _ := cmd.StdoutPipe()
|
stdout, _ := cmd.StdoutPipe()
|
||||||
stderr, _ := cmd.StderrPipe()
|
stderr, _ := cmd.StderrPipe()
|
||||||
|
|
||||||
@@ -230,14 +238,14 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go streamOutput(stdout, prefix)
|
go streamOutput(stdout, serviceName, color, logChan)
|
||||||
go streamOutput(stderr, prefix)
|
go streamOutput(stderr, serviceName, color, logChan)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
log.Info().Str("service", serviceName).Msg("Process stopped.")
|
// This is expected on clean shutdown
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Error().Err(err).Str("service", serviceName).Msg("Exited with error")
|
log.Error().Err(err).Str("service", serviceName).Msg("Exited with error")
|
||||||
} else {
|
} else {
|
||||||
@@ -246,10 +254,10 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamOutput(pipe io.ReadCloser, prefix string) {
|
func streamOutput(pipe io.ReadCloser, serviceName, color string, logChan chan<- logview.LogMessage) {
|
||||||
scanner := bufio.NewScanner(pipe)
|
scanner := bufio.NewScanner(pipe)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
fmt.Printf("%s%s\n", prefix, scanner.Text())
|
logChan <- logview.LogMessage{ServiceName: serviceName, Line: scanner.Text(), Color: color}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
126
pkg/launchpad/interactive/dev_mode.go
Normal file
126
pkg/launchpad/interactive/dev_mode.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package interactive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("62")).
|
||||||
|
Foreground(lipgloss.Color("230")).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
services []config.Service
|
||||||
|
cursor int
|
||||||
|
selected map[int]struct{}
|
||||||
|
startServices bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialModel(services []config.Service) model {
|
||||||
|
selected := make(map[int]struct{})
|
||||||
|
for i := range services {
|
||||||
|
selected[i] = struct{}{}
|
||||||
|
}
|
||||||
|
return model{
|
||||||
|
services: services,
|
||||||
|
selected: selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "up", "k":
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
|
||||||
|
case "down", "j":
|
||||||
|
if m.cursor < len(m.services)-1 {
|
||||||
|
m.cursor++
|
||||||
|
}
|
||||||
|
|
||||||
|
case " ":
|
||||||
|
if _, ok := m.selected[m.cursor]; ok {
|
||||||
|
delete(m.selected, m.cursor)
|
||||||
|
} else {
|
||||||
|
m.selected[m.cursor] = struct{}{}
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
m.startServices = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(titleStyle.Render("Select services to start"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
for i, service := range m.services {
|
||||||
|
cursor := " "
|
||||||
|
if m.cursor == i {
|
||||||
|
cursor = ">"
|
||||||
|
}
|
||||||
|
|
||||||
|
checked := " "
|
||||||
|
if _, ok := m.selected[i]; ok {
|
||||||
|
checked = "x"
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(fmt.Sprintf("%s [%s] %s\n", cursor, checked, service.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(helpStyle.Render("Use up/down to navigate, space to select, enter to start, q to quit."))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) SelectedServices() []config.Service {
|
||||||
|
var selectedServices []config.Service
|
||||||
|
for i := range m.selected {
|
||||||
|
selectedServices = append(selectedServices, m.services[i])
|
||||||
|
}
|
||||||
|
return selectedServices
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectServices(services []config.Service) ([]config.Service, error) {
|
||||||
|
if len(services) == 0 {
|
||||||
|
return nil, fmt.Errorf("no services defined in launchpad.toml")
|
||||||
|
}
|
||||||
|
m := initialModel(services)
|
||||||
|
p := tea.NewProgram(m)
|
||||||
|
|
||||||
|
finalModel, err := p.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := finalModel.(model)
|
||||||
|
if fm.startServices {
|
||||||
|
return fm.SelectedServices(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil // User quit
|
||||||
|
}
|
||||||
171
pkg/launchpad/logview/viewer.go
Normal file
171
pkg/launchpad/logview/viewer.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package logview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogMessage is a message containing a line from a service's log.
|
||||||
|
type LogMessage struct {
|
||||||
|
ServiceName string
|
||||||
|
Line string
|
||||||
|
Color string
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
services []string
|
||||||
|
logChan <-chan LogMessage
|
||||||
|
logs map[string][]string // Key: service name, Value: log lines
|
||||||
|
allLogs []string
|
||||||
|
viewports map[string]viewport.Model
|
||||||
|
activeTab int
|
||||||
|
ready bool
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tabStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).Padding(0, 1)
|
||||||
|
activeTabStyle = tabStyle.Copy().Border(lipgloss.ThickBorder(), false, false, true, false).Foreground(lipgloss.Color("62"))
|
||||||
|
inactiveTabStyle = tabStyle.Copy()
|
||||||
|
windowStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewModel(logChan <-chan LogMessage, services []string) model {
|
||||||
|
m := model{
|
||||||
|
services: append([]string{"All"}, services...),
|
||||||
|
logChan: logChan,
|
||||||
|
logs: make(map[string][]string),
|
||||||
|
viewports: make(map[string]viewport.Model),
|
||||||
|
activeTab: 0,
|
||||||
|
}
|
||||||
|
for _, s := range m.services {
|
||||||
|
m.logs[s] = []string{}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return m.waitForLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForLogs waits for the next log message from the channel.
|
||||||
|
func (m model) waitForLogs() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return <-m.logChan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var (
|
||||||
|
cmd tea.Cmd
|
||||||
|
cmds []tea.Cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "right", "l":
|
||||||
|
m.activeTab = (m.activeTab + 1) % len(m.services)
|
||||||
|
m.updateViewportContent()
|
||||||
|
case "left", "h":
|
||||||
|
m.activeTab--
|
||||||
|
if m.activeTab < 0 {
|
||||||
|
m.activeTab = len(m.services) - 1
|
||||||
|
}
|
||||||
|
m.updateViewportContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
headerHeight := lipgloss.Height(m.renderHeader())
|
||||||
|
vpHeight := m.height - headerHeight - windowStyle.GetVerticalFrameSize()
|
||||||
|
|
||||||
|
if !m.ready {
|
||||||
|
for _, serviceName := range m.services {
|
||||||
|
m.viewports[serviceName] = viewport.New(m.width-windowStyle.GetHorizontalFrameSize(), vpHeight)
|
||||||
|
}
|
||||||
|
m.ready = true
|
||||||
|
} else {
|
||||||
|
for _, serviceName := range m.services {
|
||||||
|
vp := m.viewports[serviceName]
|
||||||
|
vp.Width = m.width - windowStyle.GetHorizontalFrameSize()
|
||||||
|
vp.Height = vpHeight
|
||||||
|
m.viewports[serviceName] = vp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.updateViewportContent()
|
||||||
|
|
||||||
|
case LogMessage:
|
||||||
|
logLine := fmt.Sprintf("%s%s%s", msg.Color, msg.Line, "\033[0m")
|
||||||
|
allLogLine := fmt.Sprintf("%s[%-10s]%s %s", msg.Color, msg.ServiceName, "\033[0m", msg.Line)
|
||||||
|
|
||||||
|
m.logs[msg.ServiceName] = append(m.logs[msg.ServiceName], logLine)
|
||||||
|
m.allLogs = append(m.allLogs, allLogLine)
|
||||||
|
|
||||||
|
m.updateViewportContent()
|
||||||
|
cmds = append(cmds, m.waitForLogs())
|
||||||
|
}
|
||||||
|
|
||||||
|
activeViewport := m.viewports[m.services[m.activeTab]]
|
||||||
|
activeViewport, cmd = activeViewport.Update(msg)
|
||||||
|
m.viewports[m.services[m.activeTab]] = activeViewport
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) updateViewportContent() {
|
||||||
|
if !m.ready {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeServiceName := m.services[m.activeTab]
|
||||||
|
var content string
|
||||||
|
if activeServiceName == "All" {
|
||||||
|
content = strings.Join(m.allLogs, "\n")
|
||||||
|
} else {
|
||||||
|
content = strings.Join(m.logs[activeServiceName], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
vp := m.viewports[activeServiceName]
|
||||||
|
vp.SetContent(content)
|
||||||
|
vp.GotoBottom()
|
||||||
|
m.viewports[activeServiceName] = vp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
if !m.ready {
|
||||||
|
return "Initializing..."
|
||||||
|
}
|
||||||
|
header := m.renderHeader()
|
||||||
|
activeService := m.services[m.activeTab]
|
||||||
|
viewport := m.viewports[activeService]
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s\n%s", header, windowStyle.Render(viewport.View()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) renderHeader() string {
|
||||||
|
var tabs []string
|
||||||
|
for i, service := range m.services {
|
||||||
|
if i == m.activeTab {
|
||||||
|
tabs = append(tabs, activeTabStyle.Render(service))
|
||||||
|
} else {
|
||||||
|
tabs = append(tabs, inactiveTabStyle.Render(service))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs the log viewer program.
|
||||||
|
func Start(logChan <-chan LogMessage, services []string) error {
|
||||||
|
m := NewModel(logChan, services)
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||||
|
return p.Start()
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/deploy"
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/deploy"
|
||||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/dev"
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/dev"
|
||||||
|
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/interactive"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,7 +35,7 @@ func main() {
|
|||||||
// Dispatch to the correct handler
|
// Dispatch to the correct handler
|
||||||
switch command {
|
switch command {
|
||||||
case "dev":
|
case "dev":
|
||||||
dev.RunDev(cfg)
|
handleDev(cfg, os.Args[2:])
|
||||||
case "deploy":
|
case "deploy":
|
||||||
log.Info().Msg("Generating docker-compose.yml for production deployment...")
|
log.Info().Msg("Generating docker-compose.yml for production deployment...")
|
||||||
deploy.GenerateDockerCompose(cfg)
|
deploy.GenerateDockerCompose(cfg)
|
||||||
@@ -43,3 +43,41 @@ func main() {
|
|||||||
log.Fatal().Msgf("Unknown command: %s", command)
|
log.Fatal().Msgf("Unknown command: %s", command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleDev(cfg config.LaunchpadConfig, args []string) {
|
||||||
|
var servicesToRun []config.Service
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// Interactive mode
|
||||||
|
selectedServices, err := interactive.SelectServices(cfg.Services)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Could not start interactive selection")
|
||||||
|
}
|
||||||
|
if len(selectedServices) == 0 {
|
||||||
|
log.Info().Msg("No services selected. Exiting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
servicesToRun = selectedServices
|
||||||
|
} else if len(args) == 1 && args[0] == "all" {
|
||||||
|
log.Info().Msg("Starting all services.")
|
||||||
|
servicesToRun = cfg.Services
|
||||||
|
} else {
|
||||||
|
// Start specific services from args
|
||||||
|
serviceMap := make(map[string]config.Service)
|
||||||
|
for _, s := range cfg.Services {
|
||||||
|
serviceMap[s.Name] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if service, ok := serviceMap[arg]; ok {
|
||||||
|
servicesToRun = append(servicesToRun, service)
|
||||||
|
} else {
|
||||||
|
log.Fatal().Msgf("Service '%s' not found in launchpad.toml", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(servicesToRun) > 0 {
|
||||||
|
dev.RunDev(cfg, servicesToRun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
9
pkg/shared/hash/mod.go
Normal file
9
pkg/shared/hash/mod.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package hash
|
||||||
|
|
||||||
|
import "hash/fnv"
|
||||||
|
|
||||||
|
func Hash(s string) uint32 {
|
||||||
|
h := fnv.New32a()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return h.Sum32()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user