commit 3f7bdf02220de6a56951e041ff7adbac878a0637 Author: Geliebte <1297754537@qq.com> Date: Thu Jun 18 21:19:47 2026 +0800 Initial commit Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f9b420 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Binary +ci-server +ci-server.exe + +# Data directory (runtime) +data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..188cdf7 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +.PHONY: all build-backend build-frontend clean dev run + +# Default target: build everything +all: build-frontend build-backend + +# Build the Vue 3 frontend +build-frontend: + cd web && npm install && npm run build + +# Build the Go backend (with embedded frontend) +build-backend: + go build -o ci-server . + +# Build for Linux (cross-compile) +build-linux: + GOOS=linux GOARCH=amd64 go build -o ci-server . + +# Clean build artifacts +clean: + rm -rf ci-server ci-server.exe web/dist data + +# Run in development mode (frontend proxied via Vite) +dev: + @echo "Start the Go server in one terminal: go run ." + @echo "Start the Vite dev server in another: cd web && npm run dev" + @echo "Then open http://localhost:5173" + +# Run the production server (requires frontend built) +run: build-frontend + go run . diff --git a/ci-server.exe~ b/ci-server.exe~ new file mode 100644 index 0000000..94c170a Binary files /dev/null and b/ci-server.exe~ differ diff --git a/ci.iml b/ci.iml new file mode 100644 index 0000000..eacc75a --- /dev/null +++ b/ci.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9dd422b --- /dev/null +++ b/go.mod @@ -0,0 +1,68 @@ +module ci + +go 1.25.0 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-git/go-git/v5 v5.19.1 + golang.org/x/crypto v0.50.0 + gorm.io/gorm v1.31.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b752883 --- /dev/null +++ b/go.sum @@ -0,0 +1,197 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +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/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/internal/builder/builder.go b/internal/builder/builder.go new file mode 100644 index 0000000..8ec2556 --- /dev/null +++ b/internal/builder/builder.go @@ -0,0 +1,178 @@ +package builder + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "ci/internal/model" +) + +// LineCallback is called for each line of build output. +type LineCallback func(line string) + +// BuildResult holds the outcome of a build. +type BuildResult struct { + Success bool + LogPath string + CommitHash string + Output string +} + +// Service handles build operations. +type Service struct{} + +// New creates a new builder Service. +func New() *Service { + return &Service{} +} + +// Execute runs a build for the given project, streaming output via callback. +func (s *Service) Execute(ctx context.Context, project *model.Project, workDir, logsDir, commitHash string, onLine LineCallback) (*BuildResult, error) { + // Create log file + logFileName := fmt.Sprintf("build-%d.log", time.Now().UnixMilli()) + logPath := filepath.Join(logsDir, logFileName) + logFile, err := os.Create(logPath) + if err != nil { + return nil, fmt.Errorf("failed to create log file: %w", err) + } + defer logFile.Close() + + writer := io.MultiWriter(logFile, &callbackWriter{fn: onLine}) + + // Resolve the build script + script := strings.TrimSpace(project.BuildScript) + if script == "" { + // Default: try go build + script = "go build -o app ." + } + + // Determine shell for the build script + var cmd *exec.Cmd + if isShellScript(script) { + // Multi-line script or complex — write to temp file and execute + scriptPath := filepath.Join(workDir, ".build-script.sh") + if err := os.WriteFile(scriptPath, []byte("#!/bin/sh\nset -e\n"+script), 0755); err != nil { + return nil, fmt.Errorf("failed to write build script: %w", err) + } + cmd = exec.CommandContext(ctx, "sh", scriptPath) + } else { + cmd = exec.CommandContext(ctx, "sh", "-c", script) + } + + cmd.Dir = workDir + cmd.Env = os.Environ() + cmd.Stdout = writer + cmd.Stderr = writer + + fmt.Fprintf(writer, "=== Build started at %s ===\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(writer, "Working directory: %s\n", workDir) + fmt.Fprintf(writer, "Build script: %s\n", script) + fmt.Fprintf(writer, "Commit: %s\n\n", commitHash) + + startTime := time.Now() + err = cmd.Run() + duration := time.Since(startTime) + + result := &BuildResult{ + LogPath: logPath, + CommitHash: commitHash, + } + + if err != nil { + fmt.Fprintf(writer, "\n=== Build FAILED after %s ===\n", duration.Round(time.Second)) + fmt.Fprintf(writer, "Error: %v\n", err) + result.Success = false + result.Output = fmt.Sprintf("Build failed: %v", err) + } else { + fmt.Fprintf(writer, "\n=== Build SUCCESS after %s ===\n", duration.Round(time.Second)) + result.Success = true + result.Output = fmt.Sprintf("Build succeeded in %s", duration.Round(time.Second)) + } + + return result, nil +} + +// ReadLog reads a build log file and returns its contents. +func (s *Service) ReadLog(logPath string) (string, error) { + data, err := os.ReadFile(logPath) + if err != nil { + return "", err + } + return string(data), nil +} + +// ReadLogTail reads the last n lines from a log file. +func (s *Service) ReadLogTail(logPath string, lines int) ([]string, error) { + file, err := os.Open(logPath) + if err != nil { + return nil, err + } + defer file.Close() + + var result []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + result = append(result, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return nil, err + } + + if len(result) > lines { + result = result[len(result)-lines:] + } + return result, nil +} + +// isShellScript checks if the script looks multi-line or complex enough to need a temp file. +func isShellScript(script string) bool { + return strings.Contains(script, "\n") || len(script) > 200 +} + +// callbackWriter implements io.Writer, calling a function for each line written. +type callbackWriter struct { + fn LineCallback + buf []byte +} + +func (w *callbackWriter) Write(p []byte) (n int, err error) { + if w.fn != nil { + w.buf = append(w.buf, p...) + for { + idx := indexOfByte(w.buf, '\n') + if idx < 0 { + break + } + line := string(w.buf[:idx]) + if strings.HasSuffix(line, "\r") { + line = line[:len(line)-1] + } + w.fn(line) + w.buf = w.buf[idx+1:] + } + } + return len(p), nil +} + +func (w *callbackWriter) flush() { + if w.fn != nil && len(w.buf) > 0 { + w.fn(string(w.buf)) + w.buf = nil + } +} + +func indexOfByte(data []byte, b byte) int { + for i, c := range data { + if c == b { + return i + } + } + return -1 +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go new file mode 100644 index 0000000..aee0cbc --- /dev/null +++ b/internal/deployer/deployer.go @@ -0,0 +1,305 @@ +package deployer + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "strconv" + "time" + + "golang.org/x/crypto/ssh" +) + +// LineCallback is called for each line of deploy output. +type LineCallback func(line string) + +// FileMapping maps a local file path to a remote server path. +type FileMapping struct { + Local string `json:"local"` + Remote string `json:"remote"` +} + +// Config holds SSH connection parameters. +type Config struct { + Host string + Port int + Username string + AuthMethod string // "key" or "password" + SSHKey string // private key content (PEM) + Password string +} + +// DeployResult holds the outcome of a deployment. +type DeployResult struct { + Success bool + Output string +} + +// Service handles SSH deployment operations. +type Service struct{} + +// New creates a new deployer Service. +func New() *Service { + return &Service{} +} + +// Deploy performs a full deployment: connect, pre-commands, upload files, post-commands, health check. +// Output is streamed via the onLine callback. +func (s *Service) Deploy(ctx context.Context, cfg *Config, deployDir string, + mappings []FileMapping, preCmd, postCmd, healthCheckURL string, + healthCheckTimeout int, onLine LineCallback) (*DeployResult, error) { + + log := func(format string, args ...any) { + onLine(fmt.Sprintf(format, args...)) + } + + log("=== Deploy started at %s ===", time.Now().Format(time.RFC3339)) + log("Target: %s@%s:%d", cfg.Username, cfg.Host, cfg.Port) + log("Deploy directory: %s", deployDir) + log("Auth method: %s", cfg.AuthMethod) + + // 1. Connect to SSH server + client, err := s.dial(cfg) + if err != nil { + log("ERROR: SSH connection failed: %v", err) + return &DeployResult{Success: false, Output: fmt.Sprintf("SSH connection failed: %v", err)}, err + } + defer client.Close() + log("SSH connection established") + + // 2. Ensure remote deploy directory exists + if err := s.runCommand(client, fmt.Sprintf("mkdir -p %s", deployDir), onLine); err != nil { + log("ERROR: Failed to create deploy directory: %v", err) + return &DeployResult{Success: false, Output: fmt.Sprintf("mkdir failed: %v", err)}, err + } + log("Remote directory ensured: %s", deployDir) + + // 3. Execute pre-deploy commands + if preCmd != "" { + log("=== Executing pre-deploy command ===") + log("$ %s", preCmd) + if err := s.runCommand(client, preCmd, onLine); err != nil { + log("ERROR: Pre-deploy command failed: %v", err) + return &DeployResult{Success: false, Output: fmt.Sprintf("pre-deploy failed: %v", err)}, err + } + log("Pre-deploy command completed") + } + + // 4. Upload files + if len(mappings) > 0 { + log("=== Uploading %d file(s) ===", len(mappings)) + } + for _, m := range mappings { + if err := s.uploadFile(client, m.Local, m.Remote, onLine); err != nil { + log("ERROR: Failed to upload %s: %v", m.Local, err) + return &DeployResult{Success: false, Output: fmt.Sprintf("upload %s failed: %v", m.Local, err)}, err + } + } + + // 5. Execute post-deploy commands + if postCmd != "" { + log("=== Executing post-deploy command ===") + log("$ %s", postCmd) + if err := s.runCommand(client, postCmd, onLine); err != nil { + log("ERROR: Post-deploy command failed: %v", err) + return &DeployResult{Success: false, Output: fmt.Sprintf("post-deploy failed: %v", err)}, err + } + log("Post-deploy command completed") + } + + // 6. Health check + if healthCheckURL != "" { + log("=== Health check ===") + log("Checking %s (timeout: %ds)", healthCheckURL, healthCheckTimeout) + + timeout := time.Duration(healthCheckTimeout) * time.Second + if err := s.checkHealth(healthCheckURL, timeout); err != nil { + log("WARNING: Health check failed: %v", err) + // Health check failure is a warning, not a deploy failure + } else { + log("Health check passed") + } + } + + log("=== Deploy completed successfully at %s ===", time.Now().Format(time.RFC3339)) + return &DeployResult{Success: true, Output: "Deployment succeeded"}, nil +} + +// dial establishes an SSH connection. +func (s *Service) dial(cfg *Config) (*ssh.Client, error) { + auth, err := s.makeAuth(cfg) + if err != nil { + return nil, fmt.Errorf("make auth: %w", err) + } + + sshCfg := &ssh.ClientConfig{ + User: cfg.Username, + Auth: []ssh.AuthMethod{auth}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, + } + + addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)) + return ssh.Dial("tcp", addr, sshCfg) +} + +// makeAuth builds the ssh.AuthMethod from config. +func (s *Service) makeAuth(cfg *Config) (ssh.AuthMethod, error) { + switch cfg.AuthMethod { + case "password": + return ssh.Password(cfg.Password), nil + case "key": + // Try to parse the private key + key, err := ssh.ParsePrivateKey([]byte(cfg.SSHKey)) + if err != nil { + // Try parsing with passphrase (empty passphrase) + key, err2 := ssh.ParsePrivateKeyWithPassphrase([]byte(cfg.SSHKey), []byte{}) + if err2 != nil { + return nil, fmt.Errorf("parse private key: %w (original: %v)", err2, err) + } + return ssh.PublicKeys(key), nil + } + return ssh.PublicKeys(key), nil + default: + return nil, fmt.Errorf("unsupported auth method: %s", cfg.AuthMethod) + } +} + +// runCommand executes a command on the remote host and streams output line by line. +func (s *Service) runCommand(client *ssh.Client, cmd string, onLine LineCallback) error { + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("new session: %w", err) + } + defer session.Close() + + // Combine stdout and stderr + stdout, err := session.StdoutPipe() + if err != nil { + return fmt.Errorf("stdout pipe: %w", err) + } + stderr, err := session.StderrPipe() + if err != nil { + return fmt.Errorf("stderr pipe: %w", err) + } + + if err := session.Start(cmd); err != nil { + return fmt.Errorf("start command: %w", err) + } + + // Stream stdout lines + go streamLines(stdout, onLine) + // Stream stderr lines + go streamLines(stderr, onLine) + + err = session.Wait() + if err != nil { + if exitErr, ok := err.(*ssh.ExitError); ok { + return fmt.Errorf("command exited with %d", exitErr.ExitStatus()) + } + return fmt.Errorf("command error: %w", err) + } + return nil +} + +// uploadFile copies a local file to a remote path via SSH session. +// Uses "cat > remotePath" which is universally available on Linux. +func (s *Service) uploadFile(client *ssh.Client, localPath, remotePath string, onLine LineCallback) error { + // Get file info + info, err := os.Stat(localPath) + if err != nil { + return fmt.Errorf("stat local file: %w", err) + } + + fileSize := info.Size() + onLine(fmt.Sprintf("Uploading %s (%d bytes) -> %s", localPath, fileSize, remotePath)) + + // Open local file + f, err := os.Open(localPath) + if err != nil { + return fmt.Errorf("open local file: %w", err) + } + defer f.Close() + + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("new session: %w", err) + } + defer session.Close() + + // Pipe file content to cat > remotePath on the server + session.Stdin = f + + // Capture stderr for error reporting + stderr, err := session.StderrPipe() + if err != nil { + return fmt.Errorf("stderr pipe: %w", err) + } + + // Also create parent directories if needed + cmd := fmt.Sprintf("mkdir -p $(dirname %s) && cat > %s", remotePath, remotePath) + if err := session.Start(cmd); err != nil { + return fmt.Errorf("start upload: %w", err) + } + + // Read stderr in background + errBuf := make([]byte, 4096) + n, _ := stderr.Read(errBuf) + + err = session.Wait() + if err != nil { + if n > 0 { + return fmt.Errorf("upload failed: %s: %w", string(errBuf[:n]), err) + } + return fmt.Errorf("upload failed: %w", err) + } + + onLine(fmt.Sprintf("Uploaded %s successfully", localPath)) + return nil +} + +// checkHealth performs an HTTP GET to the health check URL. +func (s *Service) checkHealth(url string, timeout time.Duration) error { + client := &http.Client{ + Timeout: timeout, + } + + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("health check request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 200 && resp.StatusCode < 400 { + return nil + } + return fmt.Errorf("health check returned status %d", resp.StatusCode) +} + +// streamLines reads lines from a reader and calls onLine for each. +func streamLines(r io.Reader, onLine LineCallback) { + buf := make([]byte, 4096) + line := make([]byte, 0) + for { + n, err := r.Read(buf) + if n > 0 { + for _, b := range buf[:n] { + if b == '\n' { + onLine(string(line)) + line = line[:0] + } else if b != '\r' { + line = append(line, b) + } + } + } + if err != nil { + if len(line) > 0 { + onLine(string(line)) + } + return + } + } +} diff --git a/internal/git/git.go b/internal/git/git.go new file mode 100644 index 0000000..53dbfd4 --- /dev/null +++ b/internal/git/git.go @@ -0,0 +1,100 @@ +package git + +import ( + "fmt" + "os" + "path/filepath" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +// Service handles Git operations for projects. +type Service struct{} + +// New creates a new Git Service. +func New() *Service { + return &Service{} +} + +// CloneOrPull clones a repo if it doesn't exist locally, or pulls latest changes. +// Returns the commit hash of HEAD after the operation. +func (s *Service) CloneOrPull(url, branch, username, password, dstDir string) (string, error) { + // Check if repo already exists + repo, err := gogit.PlainOpen(dstDir) + if err == nil { + // Repo exists, pull + return s.pull(repo, branch, username, password) + } + + // Clone fresh + return s.clone(url, branch, username, password, dstDir) +} + +func (s *Service) clone(url, branch, username, password, dstDir string) (string, error) { + // Ensure parent directory exists + os.MkdirAll(filepath.Dir(dstDir), 0755) + // Remove target if it exists (shouldn't, but be safe) + os.RemoveAll(dstDir) + + var auth transport.AuthMethod + if username != "" || password != "" { + auth = &http.BasicAuth{ + Username: username, + Password: password, + } + } + + refName := plumbing.NewBranchReferenceName(branch) + repo, err := gogit.PlainClone(dstDir, false, &gogit.CloneOptions{ + URL: url, + Auth: auth, + ReferenceName: refName, + SingleBranch: true, + Depth: 1, + Progress: nil, + }) + if err != nil { + return "", fmt.Errorf("clone failed: %w", err) + } + + return getHeadHash(repo) +} + +func (s *Service) pull(repo *gogit.Repository, branch, username, password string) (string, error) { + w, err := repo.Worktree() + if err != nil { + return "", fmt.Errorf("worktree: %w", err) + } + + var auth transport.AuthMethod + if username != "" || password != "" { + auth = &http.BasicAuth{ + Username: username, + Password: password, + } + } + + refName := plumbing.NewBranchReferenceName(branch) + err = w.Pull(&gogit.PullOptions{ + RemoteName: "origin", + Auth: auth, + ReferenceName: refName, + SingleBranch: true, + }) + if err != nil && err != gogit.NoErrAlreadyUpToDate { + return "", fmt.Errorf("pull failed: %w", err) + } + + return getHeadHash(repo) +} + +func getHeadHash(repo *gogit.Repository) (string, error) { + ref, err := repo.Head() + if err != nil { + return "", err + } + return ref.Hash().String(), nil +} diff --git a/internal/handler/build.go b/internal/handler/build.go new file mode 100644 index 0000000..fba1c17 --- /dev/null +++ b/internal/handler/build.go @@ -0,0 +1,187 @@ +package handler + +import ( + "fmt" + "io" + "net/http" + "time" + + "context" + + "ci/internal/model" + "ci/internal/service" + + "github.com/gin-gonic/gin" +) + +// BuildHandler handles build/run/log endpoints. +type BuildHandler struct { + Svc *service.ProjectService +} + +func NewBuildHandler(svc *service.ProjectService) *BuildHandler { + return &BuildHandler{Svc: svc} +} + +// TriggerBuild starts a build for the project. +func (h *BuildHandler) TriggerBuild(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + // Build in background + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + go func() { + h.Svc.BuildProject(ctx, id) + }() + + c.JSON(http.StatusAccepted, gin.H{"message": "build started"}) +} + +// StartProject starts the project's binary. +func (h *BuildHandler) StartProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := h.Svc.StartProject(ctx, id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "started"}) +} + +// StopProject stops a running project. +func (h *BuildHandler) StopProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + if err := h.Svc.StopProject(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "stopped"}) +} + +// RestartProject restarts a project. +func (h *BuildHandler) RestartProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := h.Svc.RestartProject(ctx, id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "restarted"}) +} + +// GetBuilds lists build history for a project. +func (h *BuildHandler) GetBuilds(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + records, err := h.Svc.GetBuildHistory(id, 50) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if records == nil { + records = []model.BuildRecord{} + } + c.JSON(http.StatusOK, records) +} + +// GetBuildLog returns the full log for a specific build. +func (h *BuildHandler) GetBuildLog(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid build id"}) + return + } + + logContent, err := h.Svc.GetBuildLog(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.String(http.StatusOK, logContent) +} + +// StreamLogs streams real-time logs via SSE for a project. +func (h *BuildHandler) StreamLogs(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Access-Control-Allow-Origin", "*") + + ch := h.Svc.SubscribeLogs(id) + defer h.Svc.UnsubscribeLogs(id, ch) + + clientGone := c.Request.Context().Done() + + c.Stream(func(w io.Writer) bool { + select { + case <-clientGone: + return false + case line, ok := <-ch: + if !ok { + return false + } + fmt.Fprintf(w, "data: %s\n\n", line) + return true + } + }) +} + +// Webhook handles Git webhook triggers (Gitee/GitHub). +func (h *BuildHandler) Webhook(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + // Verify project exists + _, err = h.Svc.GetProject(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + // Trigger build + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + go func() { + h.Svc.BuildProject(ctx, id) + }() + + c.JSON(http.StatusOK, gin.H{"message": "webhook received, build triggered"}) +} diff --git a/internal/handler/deploy.go b/internal/handler/deploy.go new file mode 100644 index 0000000..f8ff759 --- /dev/null +++ b/internal/handler/deploy.go @@ -0,0 +1,151 @@ +package handler + +import ( + "context" + "net/http" + "strconv" + "time" + + "ci/internal/model" + "ci/internal/service" + + "github.com/gin-gonic/gin" +) + +// DeployHandler handles deployment endpoints. +type DeployHandler struct { + Svc *service.ProjectService +} + +// NewDeployHandler creates a new DeployHandler. +func NewDeployHandler(svc *service.ProjectService) *DeployHandler { + return &DeployHandler{Svc: svc} +} + +// GetDeployConfig returns the deploy config for a project. +func (h *DeployHandler) GetDeployConfig(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + cfg, err := h.Svc.GetDeployConfig(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if cfg == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "no deploy config"}) + return + } + c.JSON(http.StatusOK, cfg) +} + +// UpdateDeployConfig creates or updates the deploy config for a project. +func (h *DeployHandler) UpdateDeployConfig(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + var cfg model.DeployConfig + if err := c.ShouldBindJSON(&cfg); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + cfg.ProjectID = id + + if err := h.Svc.UpdateDeployConfig(&cfg); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Re-fetch to return clean state (sensitive fields hidden by json:"-") + result, _ := h.Svc.GetDeployConfig(id) + if result == nil { + c.JSON(http.StatusOK, gin.H{"message": "created"}) + return + } + c.JSON(http.StatusOK, result) +} + +// DeleteDeployConfig removes the deploy config for a project. +func (h *DeployHandler) DeleteDeployConfig(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + if err := h.Svc.DeleteDeployConfig(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "deploy config deleted"}) +} + +// TriggerDeploy starts a deployment for the project. +func (h *DeployHandler) TriggerDeploy(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + // Deploy in background + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + go func() { + h.Svc.TriggerDeploy(ctx, id, nil) + }() + + c.JSON(http.StatusAccepted, gin.H{"message": "deployment started"}) +} + +// GetDeployRecords lists deploy history for a project. +func (h *DeployHandler) GetDeployRecords(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + records, err := h.Svc.GetDeployHistory(id, 50) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if records == nil { + records = []model.DeployRecord{} + } + c.JSON(http.StatusOK, records) +} + +// GetDeployLog returns the full log for a specific deployment. +func (h *DeployHandler) GetDeployLog(c *gin.Context) { + didStr := c.Param("did") + did, err := parseIDStr(didStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid deploy id"}) + return + } + + logContent, err := h.Svc.GetDeployLog(did) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.String(http.StatusOK, logContent) +} + +// parseIDStr parses an ID string for non-param-based IDs (e.g., deploy record ID from URL path). +func parseIDStr(s string) (uint, error) { + id, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, err + } + return uint(id), nil +} diff --git a/internal/handler/project.go b/internal/handler/project.go new file mode 100644 index 0000000..523c971 --- /dev/null +++ b/internal/handler/project.go @@ -0,0 +1,129 @@ +package handler + +import ( + "net/http" + "strconv" + + "ci/internal/model" + "ci/internal/service" + + "github.com/gin-gonic/gin" +) + +// ProjectHandler handles project CRUD endpoints. +type ProjectHandler struct { + Svc *service.ProjectService +} + +func NewProjectHandler(svc *service.ProjectService) *ProjectHandler { + return &ProjectHandler{Svc: svc} +} + +// ListProjects returns all projects. +func (h *ProjectHandler) ListProjects(c *gin.Context) { + projects, err := h.Svc.ListProjects() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if projects == nil { + projects = []model.Project{} + } + c.JSON(http.StatusOK, projects) +} + +// GetProject returns a single project by ID. +func (h *ProjectHandler) GetProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + project, err := h.Svc.GetProject(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + c.JSON(http.StatusOK, project) +} + +// CreateProject creates a new project. +func (h *ProjectHandler) CreateProject(c *gin.Context) { + var p model.Project + if err := c.ShouldBindJSON(&p); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if p.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + if p.Mode != "git" && p.Mode != "upload" { + p.Mode = "git" // default + } + if err := h.Svc.CreateProject(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, p) +} + +// UpdateProject updates an existing project. +func (h *ProjectHandler) UpdateProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + existing, err := h.Svc.GetProject(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + var updates model.Project + if err := c.ShouldBindJSON(&updates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Preserve fields that should not be updated from the request + updates.ID = existing.ID + updates.CreatedAt = existing.CreatedAt + updates.Status = existing.Status + updates.PID = existing.PID + // Preserve password if not sent + if updates.GitPassword == "" { + updates.GitPassword = existing.GitPassword + } + + if err := h.Svc.UpdateProject(&updates); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, updates) +} + +// DeleteProject deletes a project. +func (h *ProjectHandler) DeleteProject(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + if err := h.Svc.DeleteProject(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "deleted"}) +} + +func parseID(c *gin.Context) (uint, error) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return 0, err + } + return uint(id), nil +} diff --git a/internal/handler/upload.go b/internal/handler/upload.go new file mode 100644 index 0000000..00eb8ea --- /dev/null +++ b/internal/handler/upload.go @@ -0,0 +1,76 @@ +package handler + +import ( + "fmt" + "net/http" + "path/filepath" + "strings" + + "ci/internal/service" + + "github.com/gin-gonic/gin" +) + +// UploadHandler handles file upload endpoints. +type UploadHandler struct { + Svc *service.ProjectService +} + +func NewUploadHandler(svc *service.ProjectService) *UploadHandler { + return &UploadHandler{Svc: svc} +} + +// UploadFiles handles source file upload for a project (zip/tar.gz). +func (h *UploadHandler) UploadFiles(c *gin.Context) { + id, err := parseID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) + return + } + + // Verify project exists and is upload mode + project, err := h.Svc.GetProject(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"}) + return + } + + // Validate file extension + ext := strings.ToLower(filepath.Ext(file.Filename)) + valid := false + if ext == ".zip" { + valid = true + } + if strings.HasSuffix(strings.ToLower(file.Filename), ".tar.gz") || strings.HasSuffix(strings.ToLower(file.Filename), ".tgz") { + valid = true + } + + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "only .zip and .tar.gz files are supported"}) + return + } + + // Save uploaded file to temp location + tmpPath := filepath.Join(h.Svc.Workspace.ProjectDir(project.ID), file.Filename) + if err := c.SaveUploadedFile(file, tmpPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save file: %v", err)}) + return + } + + // Extract to src directory + if err := h.Svc.Workspace.ExtractArchive(project.ID, tmpPath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to extract: %v", err)}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "file uploaded and extracted successfully", + "filename": file.Filename, + }) +} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..f914001 --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,88 @@ +package model + +import "time" + +// Project represents a managed CI/CD project +type Project struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"uniqueIndex;not null" json:"name"` + Description string `json:"description"` + Mode string `gorm:"default:'git'" json:"mode"` // "git" or "upload" + + // Git mode fields + GitURL string `json:"git_url"` + GitBranch string `gorm:"default:'master'" json:"git_branch"` + GitUsername string `json:"git_username"` + GitPassword string `json:"-"` // hidden from JSON responses + + // Build config + BuildScript string `json:"build_script"` + OutputBinary string `json:"output_binary"` // relative path to built binary + + // Runtime config + RunCommand string `json:"run_command"` // command to run the project + EnvVars string `json:"env_vars"` // JSON key-value env vars + Port int `json:"port"` // target port + + // Status + Status string `json:"status"` // "stopped", "running", "building", "error" + PID int `json:"pid"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// BuildRecord represents a single build execution +type BuildRecord struct { + ID uint `gorm:"primaryKey" json:"id"` + ProjectID uint `gorm:"index;not null" json:"project_id"` + Project *Project `gorm:"foreignKey:ProjectID" json:"project,omitempty"` + Status string `gorm:"default:'running'" json:"status"` // "running", "success", "failed" + LogPath string `json:"log_path"` // path to log file + CommitHash string `json:"commit_hash"` // git commit hash (if git mode) + StartedAt time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` +} + +// DeployConfig holds SSH deployment configuration for a project. +type DeployConfig struct { + ID uint `gorm:"primaryKey" json:"id"` + ProjectID uint `gorm:"uniqueIndex;not null" json:"project_id"` + + // SSH connection + Host string `json:"host"` + Port int `gorm:"default:22" json:"port"` + Username string `json:"username"` + AuthMethod string `gorm:"default:'key'" json:"auth_method"` // "key" or "password" + SSHKey string `json:"-"` // hidden from JSON responses + SSHPassword string `json:"-"` // hidden from JSON responses + + // Deployment settings + DeployDir string `json:"deploy_dir"` // remote target directory + FileMappings string `json:"file_mappings"` // JSON: [{"local":"app","remote":"/opt/myapp/app"}] + + PreDeployCommand string `json:"pre_deploy_command"` + PostDeployCommand string `json:"post_deploy_command"` + + // Health check + HealthCheckURL string `json:"health_check_url"` + HealthCheckTimeout int `gorm:"default:30" json:"health_check_timeout"` + + // Auto-deploy after successful build + AutoDeploy bool `gorm:"default:false" json:"auto_deploy"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// DeployRecord tracks a single deployment execution. +type DeployRecord struct { + ID uint `gorm:"primaryKey" json:"id"` + ProjectID uint `gorm:"index;not null" json:"project_id"` + Project *Project `gorm:"foreignKey:ProjectID" json:"project,omitempty"` + BuildRecordID *uint `json:"build_record_id"` // optional link to the build that triggered this deploy + Status string `gorm:"default:'running'" json:"status"` // "running", "success", "failed" + LogPath string `json:"log_path"` + StartedAt time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` +} diff --git a/internal/process/manager.go b/internal/process/manager.go new file mode 100644 index 0000000..993c1d7 --- /dev/null +++ b/internal/process/manager.go @@ -0,0 +1,279 @@ +package process + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "sync" + "syscall" + "time" +) + +// LineSubscriber receives individual log lines as they arrive. +type LineSubscriber func(line string) + +// ManagedProcess wraps a running child process with log streaming. +type ManagedProcess struct { + mu sync.RWMutex + cmd *exec.Cmd + PID int + ProjectID uint + Status string // "running", "stopped" + subscribers []LineSubscriber + logFile *os.File + cancel chan struct{} +} + +// Manager manages multiple running processes keyed by project ID. +type Manager struct { + mu sync.RWMutex + procs map[uint]*ManagedProcess // projectID -> process + stopChan chan struct{} +} + +// NewManager creates a new process Manager. +func NewManager() *Manager { + return &Manager{ + procs: make(map[uint]*ManagedProcess), + stopChan: make(chan struct{}), + } +} + +// Start launches a command for the given project and streams stdout/stderr. +func (m *Manager) Start(projectID uint, workDir string, command string, envVars map[string]string, logWriter io.Writer) (*ManagedProcess, error) { + m.mu.Lock() + defer m.mu.Unlock() + + // Stop existing process for this project if any + if existing, ok := m.procs[projectID]; ok { + existing.Stop() + } + + // Parse command: support simple "cmd arg1 arg2" or shell execution + var cmd *exec.Cmd + if isShellCommand(command) { + cmd = exec.Command("sh", "-c", command) + } else { + parts := splitCommand(command) + cmd = exec.Command(parts[0], parts[1:]...) + } + + cmd.Dir = workDir + cmd.Env = os.Environ() + for k, v := range envVars { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) + } + + // Pipe stdout and stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start process: %w", err) + } + + mp := &ManagedProcess{ + cmd: cmd, + PID: cmd.Process.Pid, + ProjectID: projectID, + Status: "running", + cancel: make(chan struct{}), + } + + // Stream stdout/stderr to subscribers and log writer + go mp.streamOutput(stdout, logWriter) + go mp.streamOutput(stderr, logWriter) + + // Monitor process exit + go func() { + err := cmd.Wait() + m.mu.Lock() + defer m.mu.Unlock() + mp.mu.Lock() + mp.Status = "stopped" + if err != nil { + mp.broadcast(fmt.Sprintf("[Process exited with error: %v]", err)) + } else { + mp.broadcast("[Process exited successfully]") + } + mp.mu.Unlock() + delete(m.procs, projectID) + }() + + m.procs[projectID] = mp + return mp, nil +} + +// Stop terminates a running process by project ID. +func (m *Manager) Stop(projectID uint) error { + m.mu.Lock() + defer m.mu.Unlock() + + mp, ok := m.procs[projectID] + if !ok { + return fmt.Errorf("no running process for project %d", projectID) + } + return mp.Stop() +} + +// Stop terminates the managed process. +func (mp *ManagedProcess) Stop() error { + mp.mu.Lock() + defer mp.mu.Unlock() + + if mp.Status == "stopped" { + return nil + } + + close(mp.cancel) + + // Try graceful shutdown first + if err := mp.cmd.Process.Signal(syscall.SIGTERM); err != nil { + // On Windows, SIGTERM is not supported; use Kill + mp.cmd.Process.Kill() + return nil + } + + // Wait with timeout + done := make(chan error, 1) + go func() { + _, err := mp.cmd.Process.Wait() + done <- err + }() + + select { + case <-done: + mp.Status = "stopped" + return nil + case <-time.After(10 * time.Second): + mp.cmd.Process.Kill() + mp.Status = "stopped" + return fmt.Errorf("process %d killed after timeout", mp.PID) + } +} + +// GetStatus returns the status of a project's process. +func (m *Manager) GetStatus(projectID uint) string { + m.mu.RLock() + defer m.mu.RUnlock() + + mp, ok := m.procs[projectID] + if !ok { + return "stopped" + } + + mp.mu.RLock() + defer mp.mu.RUnlock() + return mp.Status +} + +// GetProcess returns the managed process for a project, or nil. +func (m *Manager) GetProcess(projectID uint) *ManagedProcess { + m.mu.RLock() + defer m.mu.RUnlock() + return m.procs[projectID] +} + +// Subscribe adds a log line subscriber to a running process. +func (mp *ManagedProcess) Subscribe(fn LineSubscriber) { + mp.mu.Lock() + defer mp.mu.Unlock() + mp.subscribers = append(mp.subscribers, fn) +} + +// UnsubscribeAll removes all subscribers. +func (mp *ManagedProcess) UnsubscribeAll() { + mp.mu.Lock() + defer mp.mu.Unlock() + mp.subscribers = nil +} + +// IsRunning returns whether the process is currently running. +func (mp *ManagedProcess) IsRunning() bool { + mp.mu.RLock() + defer mp.mu.RUnlock() + return mp.Status == "running" +} + +func (mp *ManagedProcess) broadcast(line string) { + for _, sub := range mp.subscribers { + sub(line) + } +} + +func (mp *ManagedProcess) streamOutput(r io.Reader, logWriter io.Writer) { + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB buffer + for scanner.Scan() { + line := scanner.Text() + mp.mu.RLock() + mp.broadcast(line) + mp.mu.RUnlock() + if logWriter != nil { + fmt.Fprintln(logWriter, line) + } + } +} + +// Shutdown stops all managed processes gracefully. +func (m *Manager) Shutdown() { + m.mu.Lock() + defer m.mu.Unlock() + + for id, mp := range m.procs { + mp.Stop() + delete(m.procs, id) + } +} + +// isShellCommand returns true if the command contains shell metacharacters. +func isShellCommand(cmd string) bool { + for _, c := range cmd { + switch c { + case '|', '&', ';', '$', '>', '<', '`', '*', '?', '[', ']', '(', ')', '{', '}': + return true + } + } + return false +} + +// splitCommand splits a simple command string into parts, respecting quotes. +func splitCommand(cmd string) []string { + var parts []string + var current []rune + inQuote := false + quoteChar := rune(0) + + for _, r := range cmd { + switch { + case r == '"' || r == '\'': + if inQuote && r == quoteChar { + inQuote = false + } else if !inQuote { + inQuote = true + quoteChar = r + } else { + current = append(current, r) + } + case r == ' ' && !inQuote: + if len(current) > 0 { + parts = append(parts, string(current)) + current = nil + } + default: + current = append(current, r) + } + } + if len(current) > 0 { + parts = append(parts, string(current)) + } + return parts +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..4b1811f --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,184 @@ +package server + +import ( + "embed" + "io/fs" + "net/http" + "os" + "path/filepath" + + "ci/internal/handler" + "ci/internal/service" + "ci/internal/store" + "ci/internal/workspace" + + "github.com/gin-gonic/gin" +) + +// Config holds server configuration. +type Config struct { + Port string + DataDir string +} + +// DefaultConfig returns the default server configuration. +func DefaultConfig() Config { + return Config{ + Port: "8080", + DataDir: "data", + } +} + +// Server wraps the Gin engine and related services. +type Server struct { + engine *gin.Engine + config Config + svc *service.ProjectService +} + +// New creates a new Server with the given config and embedded frontend. +// Pass nil for frontendFS when using local file serving (dev mode). +func New(config Config, frontendFS *embed.FS) (*Server, error) { + // Resolve absolute data directory + absDataDir, err := filepath.Abs(config.DataDir) + if err != nil { + absDataDir = config.DataDir + } + + // Initialize store + st, err := store.New(absDataDir) + if err != nil { + return nil, err + } + + // Initialize workspace + workspaceDir := filepath.Join(absDataDir, "workspaces") + wm, err := workspace.New(workspaceDir) + if err != nil { + return nil, err + } + + // Initialize service + svc := service.NewProjectService(st, wm) + + // Initialize handlers + projectH := handler.NewProjectHandler(svc) + buildH := handler.NewBuildHandler(svc) + uploadH := handler.NewUploadHandler(svc) + deployH := handler.NewDeployHandler(svc) + + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + // CORS middleware for development + r.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + }) + + // Health check + r.GET("/api/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // API routes + api := r.Group("/api") + { + // Projects CRUD + api.GET("/projects", projectH.ListProjects) + api.POST("/projects", projectH.CreateProject) + api.GET("/projects/:id", projectH.GetProject) + api.PUT("/projects/:id", projectH.UpdateProject) + api.DELETE("/projects/:id", projectH.DeleteProject) + + // Build & Run + api.POST("/projects/:id/build", buildH.TriggerBuild) + api.POST("/projects/:id/start", buildH.StartProject) + api.POST("/projects/:id/stop", buildH.StopProject) + api.POST("/projects/:id/restart", buildH.RestartProject) + api.GET("/projects/:id/builds", buildH.GetBuilds) + api.GET("/projects/:id/logs/:buildId", buildH.GetBuildLog) + api.GET("/projects/:id/logs/stream", buildH.StreamLogs) + + // Deploy config + api.GET("/projects/:id/deploy-config", deployH.GetDeployConfig) + api.PUT("/projects/:id/deploy-config", deployH.UpdateDeployConfig) + api.DELETE("/projects/:id/deploy-config", deployH.DeleteDeployConfig) + + // Deploy execution + api.POST("/projects/:id/deploy", deployH.TriggerDeploy) + api.GET("/projects/:id/deploy-records", deployH.GetDeployRecords) + api.GET("/projects/:id/deploy-records/:did/log", deployH.GetDeployLog) + + // Upload + api.POST("/projects/:id/upload", uploadH.UploadFiles) + + // Webhook + api.POST("/webhook/:id", buildH.Webhook) + } + + // Serve frontend + if frontendFS != nil { + // Embedded mode: serve from embedded dist files + distFS, err := fs.Sub(frontendFS, "web/dist") + if err != nil { + // Try without the prefix + distFS = frontendFS + } + + staticFS, err := fs.Sub(distFS, ".") + _ = staticFS + _ = err + + // Use gin's static file serving with embedded FS + r.NoRoute(gin.WrapH(http.FileServer(http.FS(distFS)))) + } else { + // Dev mode: try serving from web/dist directory + localDist := "web/dist" + if _, err := os.Stat(localDist); err == nil { + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + if path == "/" { + c.File(filepath.Join(localDist, "index.html")) + return + } + // Try the specific file first + targetPath := filepath.Join(localDist, path) + if _, err := os.Stat(targetPath); err == nil { + c.File(targetPath) + return + } + // SPA fallback + c.File(filepath.Join(localDist, "index.html")) + }) + } else { + r.NoRoute(func(c *gin.Context) { + c.String(http.StatusOK, "CI/CD Server is running. Frontend not yet built — run `cd web && npm run build`.") + }) + } + } + + srv := &Server{ + engine: r, + config: config, + svc: svc, + } + + return srv, nil +} + +// Run starts the HTTP server. +func (s *Server) Run() error { + return s.engine.Run(":" + s.config.Port) +} + +// Shutdown gracefully shuts down the server. +func (s *Server) Shutdown() { + s.svc.Shutdown() +} diff --git a/internal/service/project.go b/internal/service/project.go new file mode 100644 index 0000000..541efd1 --- /dev/null +++ b/internal/service/project.go @@ -0,0 +1,549 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + + "ci/internal/builder" + "ci/internal/deployer" + gitsvc "ci/internal/git" + "ci/internal/model" + "ci/internal/process" + "ci/internal/store" + "ci/internal/workspace" +) + +// ProjectService handles all business logic for project management. +type ProjectService struct { + Store *store.Store + Workspace *workspace.Manager + Git *gitsvc.Service + Builder *builder.Service + Process *process.Manager + Deployer *deployer.Service + + // SSE subscribers for log streaming + mu sync.RWMutex + subscribers map[uint][]chan string // projectID -> list of subscriber channels +} + +// NewProjectService creates a new ProjectService. +func NewProjectService(s *store.Store, w *workspace.Manager) *ProjectService { + return &ProjectService{ + Store: s, + Workspace: w, + Git: gitsvc.New(), + Builder: builder.New(), + Process: process.NewManager(), + Deployer: deployer.New(), + subscribers: make(map[uint][]chan string), + } +} + +// CreateProject creates a new project and initializes its workspace. +func (s *ProjectService) CreateProject(p *model.Project) error { + p.Status = "stopped" + if err := s.Store.CreateProject(p); err != nil { + return err + } + return s.Workspace.InitProject(p.ID) +} + +// ListProjects returns all projects. +func (s *ProjectService) ListProjects() ([]model.Project, error) { + projects, err := s.Store.ListProjects() + if err != nil { + return nil, err + } + // Update status from process manager + for i := range projects { + status := s.Process.GetStatus(projects[i].ID) + if status == "running" { + projects[i].Status = "running" + } else if projects[i].Status == "running" { + projects[i].Status = "stopped" + } + } + return projects, nil +} + +// GetProject returns a single project. +func (s *ProjectService) GetProject(id uint) (*model.Project, error) { + p, err := s.Store.GetProject(id) + if err != nil { + return nil, err + } + status := s.Process.GetStatus(id) + if status == "running" { + p.Status = "running" + } else if p.Status == "running" { + p.Status = "stopped" + } + return p, nil +} + +// UpdateProject updates a project's configuration. +func (s *ProjectService) UpdateProject(p *model.Project) error { + return s.Store.UpdateProject(p) +} + +// DeleteProject deletes a project, stops its process, and cleans up its workspace. +func (s *ProjectService) DeleteProject(id uint) error { + s.Process.Stop(id) + s.Workspace.CleanProject(id) + s.Store.DeleteDeployConfig(id) // clean up deploy config if exists + return s.Store.DeleteProject(id) +} + +// BuildProject triggers a build for the project. +func (s *ProjectService) BuildProject(ctx context.Context, projectID uint) (*model.BuildRecord, error) { + project, err := s.Store.GetProject(projectID) + if err != nil { + return nil, err + } + + // Start a build record + now := time.Now() + record := &model.BuildRecord{ + ProjectID: projectID, + Status: "running", + StartedAt: now, + } + if err := s.Store.CreateBuildRecord(record); err != nil { + return nil, err + } + + // Update project status + project.Status = "building" + s.Store.UpdateProject(project) + + // For git mode, pull/clone first + var commitHash string + if project.Mode == "git" && project.GitURL != "" { + srcDir := s.Workspace.SrcDir(projectID) + hash, err := s.Git.CloneOrPull(project.GitURL, project.GitBranch, project.GitUsername, project.GitPassword, srcDir) + if err != nil { + s.finishBuild(record, project, "failed", err.Error(), "") + return record, err + } + commitHash = hash + record.CommitHash = hash + s.Store.UpdateBuildRecord(record) + } + + // Execute build + srcDir := s.Workspace.SrcDir(projectID) + logsDir := s.Workspace.LogsDir(projectID) + + // Notify SSE subscribers about the build starting + s.broadcast(projectID, fmt.Sprintf("[Build #%d] Starting build for project %s...", record.ID, project.Name)) + + result, err := s.Builder.Execute(ctx, project, srcDir, logsDir, commitHash, func(line string) { + s.broadcast(projectID, line) + }) + + if err != nil || !result.Success { + errMsg := "build failed" + if err != nil { + errMsg = err.Error() + } + if result != nil && result.Output != "" { + errMsg = result.Output + } + s.finishBuild(record, project, "failed", errMsg, result.LogPath) + if err != nil { + return record, err + } + return record, fmt.Errorf("%s", errMsg) + } + + finishTime := time.Now() + record.Status = "success" + record.LogPath = result.LogPath + record.CommitHash = result.CommitHash + record.FinishedAt = &finishTime + s.Store.UpdateBuildRecord(record) + + project.Status = "stopped" + s.Store.UpdateProject(project) + + s.broadcast(projectID, fmt.Sprintf("[Build #%d] Build completed successfully", record.ID)) + + // Auto-deploy if configured + if autoCfg, _ := s.Store.GetDeployConfig(projectID); autoCfg != nil && autoCfg.AutoDeploy { + s.broadcast(projectID, "[Deploy] Auto-deploy enabled, triggering deployment...") + go func() { + depCtx, depCancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer depCancel() + s.TriggerDeploy(depCtx, projectID, &record.ID) + }() + } + + return record, nil +} + +// StartProject starts the built binary for a project. +func (s *ProjectService) StartProject(ctx context.Context, projectID uint) error { + project, err := s.Store.GetProject(projectID) + if err != nil { + return err + } + + if s.Process.GetStatus(projectID) == "running" { + return fmt.Errorf("project %s is already running", project.Name) + } + + // Parse env vars from JSON string + var envVars map[string]string + if project.EnvVars != "" { + json.Unmarshal([]byte(project.EnvVars), &envVars) + } + if envVars == nil { + envVars = make(map[string]string) + } + + // Determine working directory + workDir := s.Workspace.SrcDir(projectID) + + // Determine the command to run + runCommand := project.RunCommand + if runCommand == "" { + if project.OutputBinary != "" { + runCommand = "./" + project.OutputBinary + } else if project.Mode == "git" { + runCommand = "./app" + } else { + return fmt.Errorf("no run command configured") + } + } + + // If port is specified, add it as an env var (PORT) + if project.Port > 0 { + envVars["PORT"] = fmt.Sprintf("%d", project.Port) + } + + logsDir := s.Workspace.LogsDir(projectID) + logFileName := fmt.Sprintf("%s/run-%d.log", logsDir, time.Now().UnixMilli()) + + // Create log file for run output + logFile, err := os.Create(logFileName) + if err != nil { + return fmt.Errorf("failed to create log file: %w", err) + } + + mp, err := s.Process.Start(projectID, workDir, runCommand, envVars, logFile) + if err != nil { + return err + } + + // Subscribe to process output for SSE + mp.Subscribe(func(line string) { + s.broadcast(projectID, line) + }) + + project.Status = "running" + project.PID = mp.PID + s.Store.UpdateProject(project) + + s.broadcast(projectID, fmt.Sprintf("[Process] Started with PID %d: %s", mp.PID, runCommand)) + return nil +} + +// StopProject stops a running project process. +func (s *ProjectService) StopProject(projectID uint) error { + if err := s.Process.Stop(projectID); err != nil { + return err + } + + project, err := s.Store.GetProject(projectID) + if err == nil { + project.Status = "stopped" + project.PID = 0 + s.Store.UpdateProject(project) + } + + s.broadcast(projectID, "[Process] Stopped") + return nil +} + +// RestartProject stops and restarts a project. +func (s *ProjectService) RestartProject(ctx context.Context, projectID uint) error { + s.StopProject(projectID) + // Brief pause to ensure port is freed + time.Sleep(500 * time.Millisecond) + return s.StartProject(ctx, projectID) +} + +// GetBuildHistory returns the build history for a project. +func (s *ProjectService) GetBuildHistory(projectID uint, limit int) ([]model.BuildRecord, error) { + return s.Store.ListBuildRecords(projectID, limit) +} + +// GetBuildLog returns the contents of a build log. +func (s *ProjectService) GetBuildLog(buildID uint) (string, error) { + record, err := s.Store.GetBuildRecord(buildID) + if err != nil { + return "", err + } + return s.Builder.ReadLog(record.LogPath) +} + +// --- Deploy Config --- + +// GetDeployConfig returns the deploy config for a project, or nil. +func (s *ProjectService) GetDeployConfig(projectID uint) (*model.DeployConfig, error) { + return s.Store.GetDeployConfig(projectID) +} + +// UpdateDeployConfig creates or updates the deploy config for a project. +func (s *ProjectService) UpdateDeployConfig(cfg *model.DeployConfig) error { + // Preserve sensitive fields if not sent + if existing, err := s.Store.GetDeployConfig(cfg.ProjectID); err == nil && existing != nil { + if cfg.SSHKey == "" { + cfg.SSHKey = existing.SSHKey + } + if cfg.SSHPassword == "" { + cfg.SSHPassword = existing.SSHPassword + } + } + return s.Store.UpsertDeployConfig(cfg) +} + +// DeleteDeployConfig removes the deploy config for a project. +func (s *ProjectService) DeleteDeployConfig(projectID uint) error { + return s.Store.DeleteDeployConfig(projectID) +} + +// --- Deploy Execution --- + +// TriggerDeploy starts a deployment for the project. +func (s *ProjectService) TriggerDeploy(ctx context.Context, projectID uint, buildRecordID *uint) (*model.DeployRecord, error) { + _, err := s.Store.GetProject(projectID) + if err != nil { + return nil, fmt.Errorf("project not found: %w", err) + } + + cfg, err := s.Store.GetDeployConfig(projectID) + if err != nil || cfg == nil { + return nil, fmt.Errorf("deploy config not found for project %d", projectID) + } + + // Create deploy record + now := time.Now() + record := &model.DeployRecord{ + ProjectID: projectID, + BuildRecordID: buildRecordID, + Status: "running", + StartedAt: now, + } + if err := s.Store.CreateDeployRecord(record); err != nil { + return nil, err + } + + // Log file path + logsDir := s.Workspace.LogsDir(projectID) + logFileName := fmt.Sprintf("deploy-%d.log", time.Now().UnixMilli()) + logPath := filepath.Join(logsDir, logFileName) + + // Parse file mappings + var mappings []deployer.FileMapping + if cfg.FileMappings != "" { + if err := json.Unmarshal([]byte(cfg.FileMappings), &mappings); err != nil { + s.finishDeploy(record, "failed", fmt.Sprintf("invalid file mappings: %v", err), logPath) + return record, err + } + } + + // Resolve local file paths relative to src dir + srcDir := s.Workspace.SrcDir(projectID) + for i, m := range mappings { + if !filepath.IsAbs(m.Local) { + mappings[i].Local = filepath.Join(srcDir, m.Local) + } + } + + // Build deployer config + dCfg := &deployer.Config{ + Host: cfg.Host, + Port: cfg.Port, + Username: cfg.Username, + AuthMethod: cfg.AuthMethod, + SSHKey: cfg.SSHKey, + Password: cfg.SSHPassword, + } + + // Create log file + logFile, err := os.Create(logPath) + if err != nil { + s.finishDeploy(record, "failed", err.Error(), logPath) + return record, err + } + defer logFile.Close() + + // MultiWriter: write to both log file and SSE broadcast + writer := io.MultiWriter(logFile, &deployLogWriter{fn: func(line string) { + s.broadcast(projectID, "[Deploy] "+line) + }}) + + s.broadcast(projectID, fmt.Sprintf("[Deploy #%d] Starting deployment to %s@%s:%d...", + record.ID, cfg.Username, cfg.Host, cfg.Port)) + + // Execute deployment + result, err := s.Deployer.Deploy(ctx, dCfg, cfg.DeployDir, mappings, + cfg.PreDeployCommand, cfg.PostDeployCommand, + cfg.HealthCheckURL, cfg.HealthCheckTimeout, + func(line string) { + fmt.Fprintln(writer, line) + }) + + if err != nil || !result.Success { + errMsg := "deploy failed" + if err != nil { + errMsg = err.Error() + } + if result != nil && result.Output != "" { + errMsg = result.Output + } + s.finishDeploy(record, "failed", errMsg, logPath) + if err != nil { + return record, err + } + return record, fmt.Errorf("%s", errMsg) + } + + // Success + finishTime := time.Now() + record.Status = "success" + record.LogPath = logPath + record.FinishedAt = &finishTime + s.Store.UpdateDeployRecord(record) + + s.broadcast(projectID, fmt.Sprintf("[Deploy #%d] Deployment completed successfully", record.ID)) + return record, nil +} + +// GetDeployHistory returns deploy history for a project. +func (s *ProjectService) GetDeployHistory(projectID uint, limit int) ([]model.DeployRecord, error) { + return s.Store.ListDeployRecords(projectID, limit) +} + +// GetDeployLog returns the full log for a specific deployment. +func (s *ProjectService) GetDeployLog(deployID uint) (string, error) { + record, err := s.Store.GetDeployRecord(deployID) + if err != nil { + return "", err + } + data, err := os.ReadFile(record.LogPath) + if err != nil { + return "", err + } + return string(data), nil +} + +// deployLogWriter implements io.Writer for SSE broadcasting. +type deployLogWriter struct { + fn func(string) + buf []byte +} + +func (w *deployLogWriter) Write(p []byte) (n int, err error) { + if w.fn != nil { + w.buf = append(w.buf, p...) + for { + idx := indexOfByteDeploy(w.buf, '\n') + if idx < 0 { + break + } + line := string(w.buf[:idx]) + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + w.fn(line) + w.buf = w.buf[idx+1:] + } + } + return len(p), nil +} + +func indexOfByteDeploy(data []byte, b byte) int { + for i, c := range data { + if c == b { + return i + } + } + return -1 +} + +// SSE + +// SubscribeLogs registers a channel to receive log lines for a project. +func (s *ProjectService) SubscribeLogs(projectID uint) chan string { + ch := make(chan string, 100) + s.mu.Lock() + s.subscribers[projectID] = append(s.subscribers[projectID], ch) + s.mu.Unlock() + return ch +} + +// UnsubscribeLogs removes a subscriber channel for a project. +func (s *ProjectService) UnsubscribeLogs(projectID uint, ch chan string) { + s.mu.Lock() + defer s.mu.Unlock() + subs := s.subscribers[projectID] + for i, sub := range subs { + if sub == ch { + s.subscribers[projectID] = append(subs[:i], subs[i+1:]...) + close(ch) + break + } + } +} + +func (s *ProjectService) broadcast(projectID uint, line string) { + s.mu.RLock() + defer s.mu.RUnlock() + for _, ch := range s.subscribers[projectID] { + select { + case ch <- line: + default: + // Drop if channel is full + } + } +} + +// Shutdown gracefully stops all running processes. +func (s *ProjectService) Shutdown() { + s.Process.Shutdown() +} + +// helper + +func (s *ProjectService) finishBuild(record *model.BuildRecord, project *model.Project, status, errMsg, logPath string) { + now := time.Now() + record.Status = status + record.FinishedAt = &now + if logPath != "" { + record.LogPath = logPath + } + s.Store.UpdateBuildRecord(record) + project.Status = "error" + s.Store.UpdateProject(project) + s.broadcast(project.ID, fmt.Sprintf("[Build #%d] %s", record.ID, errMsg)) +} + +func (s *ProjectService) finishDeploy(record *model.DeployRecord, status, errMsg, logPath string) { + now := time.Now() + record.Status = status + record.FinishedAt = &now + if logPath != "" { + record.LogPath = logPath + } + s.Store.UpdateDeployRecord(record) + s.broadcast(record.ProjectID, fmt.Sprintf("[Deploy #%d] %s", record.ID, errMsg)) +} diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..757b707 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,164 @@ +package store + +import ( + "ci/internal/model" + "os" + "path/filepath" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// Store wraps the database connection and provides data access methods. +type Store struct { + DB *gorm.DB +} + +// New creates a new Store, initializes the database, and runs migrations. +func New(dataDir string) (*Store, error) { + if err := os.MkdirAll(dataDir, 0755); err != nil { + return nil, err + } + + dbPath := filepath.Join(dataDir, "ci.db") + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, err + } + + if err := db.AutoMigrate(&model.Project{}, &model.BuildRecord{}, &model.DeployConfig{}, &model.DeployRecord{}); err != nil { + return nil, err + } + + return &Store{DB: db}, nil +} + +// --- Project CRUD --- + +func (s *Store) CreateProject(p *model.Project) error { + return s.DB.Create(p).Error +} + +func (s *Store) ListProjects() ([]model.Project, error) { + var projects []model.Project + err := s.DB.Order("created_at desc").Find(&projects).Error + return projects, err +} + +func (s *Store) GetProject(id uint) (*model.Project, error) { + var p model.Project + err := s.DB.First(&p, id).Error + if err != nil { + return nil, err + } + return &p, nil +} + +func (s *Store) UpdateProject(p *model.Project) error { + return s.DB.Save(p).Error +} + +func (s *Store) DeleteProject(id uint) error { + return s.DB.Delete(&model.Project{}, id).Error +} + +// --- BuildRecord CRUD --- + +func (s *Store) CreateBuildRecord(r *model.BuildRecord) error { + return s.DB.Create(r).Error +} + +func (s *Store) UpdateBuildRecord(r *model.BuildRecord) error { + return s.DB.Save(r).Error +} + +func (s *Store) GetBuildRecord(id uint) (*model.BuildRecord, error) { + var r model.BuildRecord + err := s.DB.Preload("Project").First(&r, id).Error + if err != nil { + return nil, err + } + return &r, nil +} + +func (s *Store) ListBuildRecords(projectID uint, limit int) ([]model.BuildRecord, error) { + var records []model.BuildRecord + if limit <= 0 { + limit = 20 + } + err := s.DB.Where("project_id = ?", projectID). + Order("started_at desc"). + Limit(limit). + Find(&records).Error + return records, err +} + +// --- DeployConfig CRUD --- + +// GetDeployConfig returns the deploy config for a project, or nil if not configured. +func (s *Store) GetDeployConfig(projectID uint) (*model.DeployConfig, error) { + var cfg model.DeployConfig + err := s.DB.Where("project_id = ?", projectID).First(&cfg).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil // not configured is not an error + } + return nil, err + } + return &cfg, nil +} + +// UpsertDeployConfig creates or updates the deploy config for a project. +func (s *Store) UpsertDeployConfig(cfg *model.DeployConfig) error { + // Use FirstOrCreate + Assign to handle upsert + var existing model.DeployConfig + err := s.DB.Where("project_id = ?", cfg.ProjectID).First(&existing).Error + if err == nil { + // Update existing + cfg.ID = existing.ID + cfg.CreatedAt = existing.CreatedAt + return s.DB.Save(cfg).Error + } + // Create new + return s.DB.Create(cfg).Error +} + +// DeleteDeployConfig removes the deploy config for a project. +func (s *Store) DeleteDeployConfig(projectID uint) error { + return s.DB.Where("project_id = ?", projectID).Delete(&model.DeployConfig{}).Error +} + +// --- DeployRecord CRUD --- + +// CreateDeployRecord creates a new deploy record. +func (s *Store) CreateDeployRecord(r *model.DeployRecord) error { + return s.DB.Create(r).Error +} + +// UpdateDeployRecord updates an existing deploy record. +func (s *Store) UpdateDeployRecord(r *model.DeployRecord) error { + return s.DB.Save(r).Error +} + +// GetDeployRecord returns a single deploy record by ID. +func (s *Store) GetDeployRecord(id uint) (*model.DeployRecord, error) { + var r model.DeployRecord + err := s.DB.Preload("Project").First(&r, id).Error + if err != nil { + return nil, err + } + return &r, nil +} + +// ListDeployRecords returns deploy history for a project. +func (s *Store) ListDeployRecords(projectID uint, limit int) ([]model.DeployRecord, error) { + var records []model.DeployRecord + if limit <= 0 { + limit = 20 + } + err := s.DB.Where("project_id = ?", projectID). + Order("started_at desc"). + Limit(limit). + Find(&records).Error + return records, err +} diff --git a/internal/workspace/workspace.go b/internal/workspace/workspace.go new file mode 100644 index 0000000..5b963bc --- /dev/null +++ b/internal/workspace/workspace.go @@ -0,0 +1,186 @@ +package workspace + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// Manager handles workspace directories for projects. +type Manager struct { + BaseDir string +} + +// New creates a new workspace Manager. +func New(baseDir string) (*Manager, error) { + if err := os.MkdirAll(baseDir, 0755); err != nil { + return nil, err + } + return &Manager{BaseDir: baseDir}, nil +} + +// ProjectDir returns the root workspace directory for a project. +func (m *Manager) ProjectDir(projectID uint) string { + return filepath.Join(m.BaseDir, fmt.Sprintf("project-%d", projectID)) +} + +// SrcDir returns the source directory for a project. +func (m *Manager) SrcDir(projectID uint) string { + return filepath.Join(m.ProjectDir(projectID), "src") +} + +// BuildsDir returns the builds directory for a project. +func (m *Manager) BuildsDir(projectID uint) string { + return filepath.Join(m.ProjectDir(projectID), "builds") +} + +// LogsDir returns the logs directory for a project. +func (m *Manager) LogsDir(projectID uint) string { + return filepath.Join(m.ProjectDir(projectID), "logs") +} + +// InitProject creates all required directories for a project. +func (m *Manager) InitProject(projectID uint) error { + dirs := []string{ + m.SrcDir(projectID), + m.BuildsDir(projectID), + m.LogsDir(projectID), + } + for _, d := range dirs { + if err := os.MkdirAll(d, 0755); err != nil { + return err + } + } + return nil +} + +// CleanProject removes the entire project workspace. +func (m *Manager) CleanProject(projectID uint) error { + return os.RemoveAll(m.ProjectDir(projectID)) +} + +// ExtractArchive detects archive type and extracts to the project src directory. +// Supports .zip, .tar.gz, .tgz. +func (m *Manager) ExtractArchive(projectID uint, filePath string) error { + dest := m.SrcDir(projectID) + // Clean destination first + os.RemoveAll(dest) + os.MkdirAll(dest, 0755) + + lower := strings.ToLower(filePath) + switch { + case strings.HasSuffix(lower, ".zip"): + return extractZip(filePath, dest) + case strings.HasSuffix(lower, ".tar.gz"), strings.HasSuffix(lower, ".tgz"): + return extractTarGz(filePath, dest) + default: + return fmt.Errorf("unsupported archive format: %s (use .zip or .tar.gz)", filePath) + } +} + +func extractZip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + // Prevent zip slip + path := filepath.Join(dest, f.Name) + if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path in zip: %s", f.Name) + } + + if f.FileInfo().IsDir() { + os.MkdirAll(path, 0755) + continue + } + + os.MkdirAll(filepath.Dir(path), 0755) + out, err := os.Create(path) + if err != nil { + return err + } + rc, err := f.Open() + if err != nil { + out.Close() + return err + } + _, err = io.Copy(out, rc) + rc.Close() + out.Close() + if err != nil { + return err + } + } + return nil +} + +func extractTarGz(src, dest string) error { + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + gzReader, err := gzip.NewReader(f) + if err != nil { + return err + } + defer gzReader.Close() + + tarReader := tar.NewReader(gzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + path := filepath.Join(dest, header.Name) + if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path in tar: %s", header.Name) + } + + switch header.Typeflag { + case tar.TypeDir: + os.MkdirAll(path, 0755) + case tar.TypeReg: + os.MkdirAll(filepath.Dir(path), 0755) + out, err := os.Create(path) + if err != nil { + return err + } + _, err = io.Copy(out, tarReader) + out.Close() + if err != nil { + return err + } + } + } + return nil +} + +// SaveUploadedFile saves an uploaded file to a temp location. +func (m *Manager) SaveUploadedFile(projectID uint, reader io.Reader, filename string) (string, error) { + dir := m.ProjectDir(projectID) + os.MkdirAll(dir, 0755) + + path := filepath.Join(dir, filename) + f, err := os.Create(path) + if err != nil { + return "", err + } + defer f.Close() + + _, err = io.Copy(f, reader) + return path, err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a9c737e --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "embed" + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "ci/internal/server" +) + +//go:embed web/dist +var frontend embed.FS + +func main() { + port := flag.String("port", "8080", "HTTP server port") + dataDir := flag.String("data", "data", "Data directory path") + flag.Parse() + + // Check if frontend is embedded (non-empty) + var embeddedFS *embed.FS + if _, err := frontend.ReadDir("web/dist"); err == nil { + embeddedFS = &frontend + fmt.Println("Using embedded frontend") + } else { + fmt.Println("Embedded frontend not found, using dev mode (web/dist)") + } + + config := server.Config{ + Port: *port, + DataDir: *dataDir, + } + + srv, err := server.New(config, embeddedFS) + if err != nil { + log.Fatalf("Failed to create server: %v", err) + } + + // Graceful shutdown on interrupt + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-quit + fmt.Println("\nShutting down...") + srv.Shutdown() + os.Exit(0) + }() + + fmt.Printf("CI/CD Server starting on http://localhost:%s\n", config.Port) + if err := srv.Run(); err != nil { + log.Fatalf("Server error: %v", err) + } +} diff --git a/server.log b/server.log new file mode 100644 index 0000000..e5843ad --- /dev/null +++ b/server.log @@ -0,0 +1,34 @@ +Using embedded frontend +CI/CD Server starting on http://localhost:8080 +[GIN] 2026/06/18 - 16:29:13 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 16:29:17 | 200 | 52.91ms | ::1 | GET "/" +[GIN] 2026/06/18 - 16:29:17 | 200 | 3.18ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:29:17 | 200 | 3.69ms | ::1 | GET "/assets/index-DHo9r-sY.css" +[GIN] 2026/06/18 - 16:29:17 | 200 | 1.65ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:29:17 | 200 | 505.7µs | ::1 | GET "/api/projects" +[GIN] 2026/06/18 - 16:29:34 | 200 | 0s | ::1 | GET "/" +[GIN] 2026/06/18 - 16:29:34 | 200 | 2.01ms | ::1 | GET "/assets/index-DHo9r-sY.css" +[GIN] 2026/06/18 - 16:29:34 | 200 | 4.44ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:29:34 | 200 | 3.18ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:29:34 | 200 | 0s | ::1 | GET "/api/projects" +[GIN] 2026/06/18 - 16:29:34 | 200 | 0s | ::1 | GET "/favicon.ico" +[GIN] 2026/06/18 - 16:54:28 | 200 | 0s | ::1 | GET "/" +[GIN] 2026/06/18 - 16:54:28 | 200 | 1.91ms | ::1 | GET "/assets/index-DHo9r-sY.css" +[GIN] 2026/06/18 - 16:54:28 | 200 | 4.1ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:54:28 | 200 | 2.04ms | ::1 | GET "/assets/index-BJ3d5HNU.js" +[GIN] 2026/06/18 - 16:54:28 | 200 | 0s | ::1 | GET "/api/projects" +[GIN] 2026/06/18 - 16:54:28 | 200 | 0s | ::1 | GET "/favicon.ico" +[GIN] 2026/06/18 - 19:40:22 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:40:48 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:41:12 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:41:50 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:41:56 | 404 | 9.01ms | ::1 | GET "/api/v1/public/company" +[GIN] 2026/06/18 - 19:41:56 | 404 | 0s | ::1 | GET "/api/v1/public/sites" +[GIN] 2026/06/18 - 19:41:56 | 404 | 0s | ::1 | GET "/api/v1/public/nav" +[GIN] 2026/06/18 - 19:41:56 | 404 | 0s | ::1 | POST "/api/v1/auth/send-sms" +[GIN] 2026/06/18 - 19:42:33 | 200 | 542.2µs | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:42:36 | 404 | 0s | ::1 | GET "/api/v1/public/company" +[GIN] 2026/06/18 - 19:42:36 | 404 | 0s | ::1 | GET "/api/v1/public/sites" +[GIN] 2026/06/18 - 19:42:36 | 404 | 0s | ::1 | POST "/api/v1/auth/send-sms" +[GIN] 2026/06/18 - 19:42:42 | 200 | 0s | ::1 | GET "/api/health" +[GIN] 2026/06/18 - 19:43:21 | 404 | 1.83ms | ::1 | GET "/api/v1/public/company" diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..741ebe2 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,40 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist/* +!dist/.gitkeep +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..b08fcb2 --- /dev/null +++ b/web/README.md @@ -0,0 +1,38 @@ +# web + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` diff --git a/web/dist/.gitkeep b/web/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ee0034f --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + CI Manager + + +
+ + + diff --git a/web/jsconfig.json b/web/jsconfig.json new file mode 100644 index 0000000..5a1f2d2 --- /dev/null +++ b/web/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..92cdfba --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2836 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "axios": "^1.17.0", + "element-plus": "^2.14.2", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.8", + "vite-plugin-vue-devtools": "^8.1.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/devtools-core": { + "version": "8.1.3", + "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.1.3.tgz", + "integrity": "sha512-xezkv5/CPH/o5C8PE2Len9MnTJMsctYYQbKbbUiNOJpKd+fRHj27nKDb/sbtYI8NSQduegeQhCJGKRgAiOV6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.3", + "@vue/devtools-shared": "^8.1.3" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.3", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.1.3.tgz", + "integrity": "sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.3", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.3", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.1.3.tgz", + "integrity": "sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "vue": "3.5.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-plus": { + "version": "2.14.2", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz", + "integrity": "sha512-eNH9uP3wQoNqieEIHXiNvIVv+zO5sZDU0CAZq5b0zqSN06DD0/V9xIq1R/qm3rw5k3nBTM1JvpxhCfRbaFLzDQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.3" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmmirror.com/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz", + "integrity": "sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^4.0.0", + "vite-hot-client": "^2.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-dev-rpc/node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite-hot-client": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz", + "integrity": "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.4.1", + "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz", + "integrity": "sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.0", + "error-stack-parser-es": "^1.0.5", + "obug": "^2.1.1", + "ohash": "^2.0.11", + "open": "^11.0.0", + "perfect-debounce": "^2.1.0", + "sirv": "^3.0.2", + "unplugin-utils": "^0.3.1", + "vite-dev-rpc": "^2.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.1.3", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.3.tgz", + "integrity": "sha512-KBTUhbTXvY+GsCdShnCHG4WdijEV74KIDxhF8erfSs5g5mS13g/cPRUf4mLpD10qr5FqHYosNt0j6rP5kpiS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.1.3", + "@vue/devtools-kit": "^8.1.3", + "@vue/devtools-shared": "^8.1.3", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^6.0.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", + "integrity": "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vue": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.5.tgz", + "integrity": "sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..c5e8767 --- /dev/null +++ b/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && touch dist/.gitkeep", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.17.0", + "element-plus": "^2.14.2", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.8", + "vite-plugin-vue-devtools": "^8.1.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..fc7bf2e --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,179 @@ + + + + + + + diff --git a/web/src/api/index.js b/web/src/api/index.js new file mode 100644 index 0000000..9eabb4e --- /dev/null +++ b/web/src/api/index.js @@ -0,0 +1,90 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000, +}) + +// --- Projects --- +export function listProjects() { + return api.get('/projects').then(r => r.data) +} + +export function getProject(id) { + return api.get(`/projects/${id}`).then(r => r.data) +} + +export function createProject(data) { + return api.post('/projects', data).then(r => r.data) +} + +export function updateProject(id, data) { + return api.put(`/projects/${id}`, data).then(r => r.data) +} + +export function deleteProject(id) { + return api.delete(`/projects/${id}`).then(r => r.data) +} + +// --- Build & Run --- +export function triggerBuild(id) { + return api.post(`/projects/${id}/build`).then(r => r.data) +} + +export function startProject(id) { + return api.post(`/projects/${id}/start`).then(r => r.data) +} + +export function stopProject(id) { + return api.post(`/projects/${id}/stop`).then(r => r.data) +} + +export function restartProject(id) { + return api.post(`/projects/${id}/restart`).then(r => r.data) +} + +export function getBuilds(id) { + return api.get(`/projects/${id}/builds`).then(r => r.data) +} + +export function getBuildLog(id, buildId) { + return api.get(`/projects/${id}/logs/${buildId}`).then(r => r.data) +} + +// --- Upload --- +export function uploadFile(id, formData) { + return api.post(`/projects/${id}/upload`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }).then(r => r.data) +} + +// --- Deploy Config --- +export function getDeployConfig(id) { + return api.get(`/projects/${id}/deploy-config`).then(r => r.data) +} + +export function updateDeployConfig(id, data) { + return api.put(`/projects/${id}/deploy-config`, data).then(r => r.data) +} + +export function deleteDeployConfig(id) { + return api.delete(`/projects/${id}/deploy-config`).then(r => r.data) +} + +// --- Deploy Execution --- +export function triggerDeploy(id) { + return api.post(`/projects/${id}/deploy`).then(r => r.data) +} + +export function getDeployRecords(id) { + return api.get(`/projects/${id}/deploy-records`).then(r => r.data) +} + +export function getDeployLog(id, deployId) { + return api.get(`/projects/${id}/deploy-records/${deployId}/log`).then(r => r.data) +} + +// --- SSE --- +export function createLogStream(id) { + return new EventSource(`/api/projects/${id}/logs/stream`) +} diff --git a/web/src/assets/base.css b/web/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/web/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/web/src/assets/logo.svg b/web/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/web/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/main.css b/web/src/assets/main.css new file mode 100644 index 0000000..36fb845 --- /dev/null +++ b/web/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/web/src/components/HelloWorld.vue b/web/src/components/HelloWorld.vue new file mode 100644 index 0000000..eff59f1 --- /dev/null +++ b/web/src/components/HelloWorld.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/web/src/components/TheWelcome.vue b/web/src/components/TheWelcome.vue new file mode 100644 index 0000000..68a970a --- /dev/null +++ b/web/src/components/TheWelcome.vue @@ -0,0 +1,95 @@ + + + diff --git a/web/src/components/WelcomeItem.vue b/web/src/components/WelcomeItem.vue new file mode 100644 index 0000000..6d7086a --- /dev/null +++ b/web/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/web/src/components/icons/IconCommunity.vue b/web/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000..2dc8b05 --- /dev/null +++ b/web/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/web/src/components/icons/IconDocumentation.vue b/web/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000..6d4791c --- /dev/null +++ b/web/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/web/src/components/icons/IconEcosystem.vue b/web/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000..c3a4f07 --- /dev/null +++ b/web/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/web/src/components/icons/IconSupport.vue b/web/src/components/icons/IconSupport.vue new file mode 100644 index 0000000..7452834 --- /dev/null +++ b/web/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/web/src/components/icons/IconTooling.vue b/web/src/components/icons/IconTooling.vue new file mode 100644 index 0000000..660598d --- /dev/null +++ b/web/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..571164a --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(ElementPlus) +app.use(router) +app.mount('#app') diff --git a/web/src/router/index.js b/web/src/router/index.js new file mode 100644 index 0000000..9a6c452 --- /dev/null +++ b/web/src/router/index.js @@ -0,0 +1,17 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import Dashboard from '../views/Dashboard.vue' +import ProjectCreate from '../views/ProjectCreate.vue' +import ProjectDetail from '../views/ProjectDetail.vue' + +const routes = [ + { path: '/', name: 'Dashboard', component: Dashboard }, + { path: '/projects/new', name: 'ProjectCreate', component: ProjectCreate }, + { path: '/projects/:id', name: 'ProjectDetail', component: ProjectDetail, props: true }, +] + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +export default router diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue new file mode 100644 index 0000000..7a9fca3 --- /dev/null +++ b/web/src/views/Dashboard.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/web/src/views/ProjectCreate.vue b/web/src/views/ProjectCreate.vue new file mode 100644 index 0000000..b232da8 --- /dev/null +++ b/web/src/views/ProjectCreate.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/web/src/views/ProjectDetail.vue b/web/src/views/ProjectDetail.vue new file mode 100644 index 0000000..106534e --- /dev/null +++ b/web/src/views/ProjectDetail.vue @@ -0,0 +1,687 @@ + + + + + diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..6422622 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, + server: { + proxy: { + '/api': 'http://localhost:8080', + }, + }, +}) diff --git a/web/web.iml b/web/web.iml new file mode 100644 index 0000000..80cc739 --- /dev/null +++ b/web/web.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file