diff --git a/go.mod b/go.mod index 6e39841d125c18db69d77c9ab0bf88222c348206..f7c8f4d1c11ea2bd7bcc747c94ddab224a63a7bf 100644 --- a/go.mod +++ b/go.mod @@ -32,15 +32,16 @@ require ( github.com/pressly/goose/v3 v3.24.2 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/sahilm/fuzzy v0.1.1 - github.com/shirou/gopsutil/v4 v4.25.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.0 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef github.com/stretchr/testify v1.10.0 + mvdan.cc/sh/v3 v3.11.0 ) require ( + golang.org/x/term v0.31.0 // indirect cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.13.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect @@ -75,11 +76,9 @@ require ( github.com/disintegration/gift v1.1.2 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -91,7 +90,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -105,9 +103,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.7 - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect @@ -121,13 +117,10 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect diff --git a/go.sum b/go.sum index 9148472301521bcb9a31d84bab3444655bef8f03..56284da1290cd092f24115a9a71084564d62284d 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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= @@ -108,8 +110,6 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -123,15 +123,14 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= @@ -161,8 +160,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -202,8 +199,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/errors v0.8.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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU= github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -228,8 +223,6 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -266,10 +259,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -280,8 +269,6 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -328,9 +315,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -339,7 +324,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -353,6 +337,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -367,7 +353,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI= google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= @@ -394,3 +379,5 @@ modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA= modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws= +mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= +mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= diff --git a/internal/config/config.go b/internal/config/config.go index 1d7786a1dd2eef536ee0e014b47c93ecbd50fa9d..3554bd8e4da08ac6a95a95b3f0fe0b6e712eccff 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -73,12 +73,6 @@ type TUIConfig struct { Theme string `json:"theme,omitempty"` } -// ShellConfig defines the configuration for the shell used by the bash tool. -type ShellConfig struct { - Path string `json:"path,omitempty"` - Args []string `json:"args,omitempty"` -} - // Config is the main configuration structure for the application. type Config struct { Data Data `json:"data"` @@ -91,7 +85,6 @@ type Config struct { DebugLSP bool `json:"debugLSP,omitempty"` ContextPaths []string `json:"contextPaths,omitempty"` TUI TUIConfig `json:"tui"` - Shell ShellConfig `json:"shell,omitempty"` AutoCompact bool `json:"autoCompact,omitempty"` } @@ -224,14 +217,6 @@ func setDefaults(debug bool) { viper.SetDefault("tui.theme", "crush") viper.SetDefault("autoCompact", true) - // Set default shell from environment or fallback to /bin/bash - shellPath := os.Getenv("SHELL") - if shellPath == "" { - shellPath = "/bin/bash" - } - viper.SetDefault("shell.path", shellPath) - viper.SetDefault("shell.args", []string{"-l"}) - if debug { viper.SetDefault("debug", true) viper.Set("log.level", "debug") diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index da47526a5b252af7166562cb61165ed308a2b348..2a3f4e745e80243c1d5aa2ffdbdb344e007a8fcd 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -285,9 +285,17 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) } } startTime := time.Now() - shell := shell.GetPersistentShell(config.WorkingDirectory()) - stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout) - if err != nil { + if params.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Millisecond) + defer cancel() + } + stdout, stderr, err := shell. + GetPersistentShell(config.WorkingDirectory()). + Exec(ctx, params.Command) + interrupted := shell.IsInterrupt(err) + exitCode := shell.ExitCode(err) + if exitCode == 0 && !interrupted && err != nil { return ToolResponse{}, fmt.Errorf("error executing command: %w", err) } diff --git a/internal/llm/tools/shell/comparison_test.go b/internal/llm/tools/shell/comparison_test.go index 550714f9f5b3c48d4bdc5ab364badef9d7fb274b..4906a5c8acb8136d7b7947177f999f7019080f5f 100644 --- a/internal/llm/tools/shell/comparison_test.go +++ b/internal/llm/tools/shell/comparison_test.go @@ -1,8 +1,6 @@ package shell import ( - "context" - "os" "testing" "time" @@ -11,16 +9,12 @@ import ( ) func TestShellPerformanceComparison(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "shell-test") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - shell := GetPersistentShell(tmpDir) - defer shell.Close() + shell := newPersistentShell(t.TempDir()) // Test quick command start := time.Now() - stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello'", 0) + stdout, stderr, err := shell.Exec(t.Context(), "echo 'hello'") + exitCode := ExitCode(err) duration := time.Since(start) require.NoError(t, err) @@ -33,19 +27,14 @@ func TestShellPerformanceComparison(t *testing.T) { // Benchmark CPU usage during polling func BenchmarkShellPolling(b *testing.B) { - tmpDir, err := os.MkdirTemp("", "shell-bench") - require.NoError(b, err) - defer os.RemoveAll(tmpDir) - - shell := GetPersistentShell(tmpDir) - defer shell.Close() + shell := newPersistentShell(b.TempDir()) - b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for b.Loop() { // Use a short sleep to measure polling overhead - _, _, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.02", 500) + _, _, err := shell.Exec(b.Context(), "sleep 0.02") + exitCode := ExitCode(err) if err != nil || exitCode != 0 { b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) } diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 25ba75a7877a7e4009991c78e6f09774c0d1bb79..cbf33e700c15ae06b66a4cbafa620bbc7ceb1405 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -1,312 +1,87 @@ package shell import ( - "cmp" + "bytes" "context" "errors" "fmt" - "io" "os" - "os/exec" - "path/filepath" "strings" "sync" - "syscall" - "time" - "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/logging" - "github.com/shirou/gopsutil/v4/process" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" ) type PersistentShell struct { - cmd *exec.Cmd - stdin io.WriteCloser - isAlive bool - cwd string - mu sync.Mutex - commandQueue chan *commandExecution + env []string + cwd string + mu sync.Mutex } -type commandExecution struct { - command string - timeout time.Duration - resultChan chan commandResult - ctx context.Context -} - -type commandResult struct { - stdout string - stderr string - exitCode int - interrupted bool - err error -} - -var shellInstance *PersistentShell +var ( + once sync.Once + shellInstance *PersistentShell +) -func GetPersistentShell(workingDir string) *PersistentShell { - if shellInstance == nil { - shellInstance = newPersistentShell(workingDir) - } - if !shellInstance.isAlive { - shellInstance = newPersistentShell(shellInstance.cwd) - } +func GetPersistentShell(cwd string) *PersistentShell { + once.Do(func() { + shellInstance = newPersistentShell(cwd) + }) return shellInstance } func newPersistentShell(cwd string) *PersistentShell { - // Get shell configuration from config - cfg := config.Get() - - // Default to environment variable if config is not set or nil - var shellPath string - var shellArgs []string - - if cfg != nil { - shellPath = cfg.Shell.Path - shellArgs = cfg.Shell.Args - } - - shellPath = cmp.Or(shellPath, os.Getenv("SHELL"), "/bin/bash") - if !strings.HasSuffix(shellPath, "bash") && !strings.HasSuffix(shellPath, "zsh") { - logging.Warn("only bash and zsh are supported at this time", "shell", shellPath) - shellPath = "/bin/bash" - } - - // Default shell args - if len(shellArgs) == 0 { - shellArgs = []string{"--login"} - } - - cmd := exec.Command(shellPath, shellArgs...) - cmd.Dir = cwd - - stdinPipe, err := cmd.StdinPipe() - if err != nil { - return nil - } - - cmd.Env = append(os.Environ(), "GIT_EDITOR=true") - - err = cmd.Start() - if err != nil { - return nil - } - - shell := &PersistentShell{ - cmd: cmd, - stdin: stdinPipe, - isAlive: true, - cwd: cwd, - commandQueue: make(chan *commandExecution, 10), - } - - go func() { - defer func() { - if r := recover(); r != nil { - fmt.Fprintf(os.Stderr, "Panic in shell command processor: %v\n", r) - shell.isAlive = false - close(shell.commandQueue) - } - }() - shell.processCommands() - }() - - go func() { - err := cmd.Wait() - if err != nil { - // Log the error if needed - } - shell.isAlive = false - close(shell.commandQueue) - }() - - return shell -} - -func (s *PersistentShell) processCommands() { - for cmd := range s.commandQueue { - cmd.resultChan <- s.execCommand(cmd.ctx, cmd.command, cmd.timeout) + return &PersistentShell{ + cwd: cwd, + env: os.Environ(), } } -const runBashCommandFormat = `%s %q 2>%q -echo $? >%q -pwd >%q` - -func (s *PersistentShell) execCommand(ctx context.Context, command string, timeout time.Duration) commandResult { +func (s *PersistentShell) Exec(ctx context.Context, command string) (string, string, error) { s.mu.Lock() defer s.mu.Unlock() - if !s.isAlive { - return commandResult{ - stderr: "Shell is not alive", - exitCode: 1, - err: errors.New("shell is not alive"), - } - } - - tmp := os.TempDir() - now := time.Now().UnixNano() - stdoutFile := filepath.Join(tmp, fmt.Sprintf("crush-stdout-%d", now)) - stderrFile := filepath.Join(tmp, fmt.Sprintf("crush-stderr-%d", now)) - statusFile := filepath.Join(tmp, fmt.Sprintf("crush-status-%d", now)) - cwdFile := filepath.Join(tmp, fmt.Sprintf("crush-cwd-%d", now)) - - defer func() { - _ = os.Remove(stdoutFile) - _ = os.Remove(stderrFile) - _ = os.Remove(statusFile) - _ = os.Remove(cwdFile) - }() - - script := fmt.Sprintf(runBashCommandFormat, command, stdoutFile, stderrFile, statusFile, cwdFile) - if _, err := s.stdin.Write([]byte(script + "\n")); err != nil { - return commandResult{ - stderr: fmt.Sprintf("Failed to write command to shell: %v", err), - exitCode: 1, - err: err, - } - } - - interrupted := false - done := make(chan bool) - go func() { - // Use exponential backoff polling - pollInterval := 10 * time.Millisecond - maxPollInterval := time.Second - - ticker := time.NewTicker(pollInterval) - defer ticker.Stop() - - timeoutTicker := time.NewTicker(cmp.Or(timeout, time.Hour*99999)) - defer timeoutTicker.Stop() - - for { - select { - case <-ctx.Done(): - s.killChildren() - interrupted = true - done <- true - return - - case <-timeoutTicker.C: - s.killChildren() - interrupted = true - done <- true - return - - case <-ticker.C: - if fileSize(statusFile) > 0 { - done <- true - return - } - - // Exponential backoff to reduce CPU usage for longer-running commands - if pollInterval < maxPollInterval { - pollInterval = min(time.Duration(float64(pollInterval)*1.5), maxPollInterval) - ticker.Reset(pollInterval) - } - } - } - }() - - <-done - - stdout := readFileOrEmpty(stdoutFile) - stderr := readFileOrEmpty(stderrFile) - exitCodeStr := readFileOrEmpty(statusFile) - newCwd := readFileOrEmpty(cwdFile) - - exitCode := 0 - if exitCodeStr != "" { - fmt.Sscanf(exitCodeStr, "%d", &exitCode) - } else if interrupted { - exitCode = 143 - stderr += "\nCommand execution timed out or was interrupted" - } - - if newCwd != "" { - s.cwd = strings.TrimSpace(newCwd) - } - - return commandResult{ - stdout: stdout, - stderr: stderr, - exitCode: exitCode, - interrupted: interrupted, - } -} - -func (s *PersistentShell) killChildren() { - if s.cmd == nil || s.cmd.Process == nil { - return - } - p, err := process.NewProcess(int32(s.cmd.Process.Pid)) + line, err := syntax.NewParser().Parse(strings.NewReader(command), "") if err != nil { - logging.WarnPersist("could not kill persistent shell child processes", "err", err) - return + return "", "", fmt.Errorf("could not parse command: %w", err) } - children, err := p.Children() + var stdout, stderr bytes.Buffer + runner, err := interp.New( + interp.StdIO(nil, &stdout, &stderr), + interp.Interactive(false), + interp.Env(expand.ListEnviron(s.env...)), + interp.Dir(s.cwd), + ) if err != nil { - logging.WarnPersist("could not kill persistent shell child processes", "err", err) - return - } - - for _, child := range children { - if err := child.SendSignal(syscall.SIGTERM); err != nil { - logging.WarnPersist("could not kill persistent shell child processes", "err", err, "pid", child.Pid) - } - } -} - -func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs int) (string, string, int, bool, error) { - if !s.isAlive { - return "", "Shell is not alive", 1, false, errors.New("shell is not alive") - } - - resultChan := make(chan commandResult) - s.commandQueue <- &commandExecution{ - command: command, - timeout: time.Duration(timeoutMs) * time.Millisecond, - resultChan: resultChan, - ctx: ctx, - } - - result := <-resultChan - return result.stdout, result.stderr, result.exitCode, result.interrupted, result.err -} - -func (s *PersistentShell) Close() { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.isAlive { - return + return "", "", fmt.Errorf("could not run command: %w", err) } - s.stdin.Write([]byte("exit\n")) - - if err := s.cmd.Process.Kill(); err != nil { - logging.WarnPersist("could not kill persistent shell", "err", err) + err = runner.Run(ctx, line) + s.cwd = runner.Dir + s.env = []string{} + for name, vr := range runner.Vars { + s.env = append(s.env, fmt.Sprintf("%s=%s", name, vr.Str)) } - s.isAlive = false + logging.InfoPersist("Command finished", "command", command, "err", err) + return stdout.String(), stderr.String(), err } -func readFileOrEmpty(path string) string { - content, err := os.ReadFile(path) - if err != nil { - return "" - } - return string(content) +func IsInterrupt(err error) bool { + return errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) } -func fileSize(path string) int64 { - info, err := os.Stat(path) - if err != nil { +func ExitCode(err error) int { + if err == nil { return 0 } - return info.Size() + status, ok := interp.IsExitStatus(err) + if ok { + return int(status) + } + return 1 } diff --git a/internal/llm/tools/shell/shell_test.go b/internal/llm/tools/shell/shell_test.go index 48198426e06ec2ade627bd6f24db0d02ad005b0f..630995aaa756295ff1eb14b05d11a2ee8f634733 100644 --- a/internal/llm/tools/shell/shell_test.go +++ b/internal/llm/tools/shell/shell_test.go @@ -2,28 +2,81 @@ package shell import ( "context" - "os" "testing" - - "github.com/stretchr/testify/require" + "time" ) // Benchmark to measure CPU efficiency func BenchmarkShellQuickCommands(b *testing.B) { - tmpDir, err := os.MkdirTemp("", "shell-bench") - require.NoError(b, err) - defer os.RemoveAll(tmpDir) - - shell := GetPersistentShell(tmpDir) - defer shell.Close() + shell := newPersistentShell(b.TempDir()) - b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, _, exitCode, _, err := shell.Exec(context.Background(), "echo test", 0) + for b.Loop() { + _, _, err := shell.Exec(context.Background(), "echo test") + exitCode := ExitCode(err) if err != nil || exitCode != 0 { b.Fatalf("Command failed: %v, exit code: %d", err, exitCode) } } } + +func TestTestTimeout(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond) + t.Cleanup(cancel) + + shell := newPersistentShell(t.TempDir()) + _, _, err := shell.Exec(ctx, "sleep 10") + if status := ExitCode(err); status == 0 { + t.Fatalf("Expected non-zero exit status, got %d", status) + } + if !IsInterrupt(err) { + t.Fatalf("Expected command to be interrupted, but it was not") + } + if err == nil { + t.Fatalf("Expected an error due to timeout, but got none") + } +} + +func TestTestCancel(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + cancel() // immediately cancel the context + + shell := newPersistentShell(t.TempDir()) + _, _, err := shell.Exec(ctx, "sleep 10") + if status := ExitCode(err); status == 0 { + t.Fatalf("Expected non-zero exit status, got %d", status) + } + if !IsInterrupt(err) { + t.Fatalf("Expected command to be interrupted, but it was not") + } + if err == nil { + t.Fatalf("Expected an error due to cancel, but got none") + } +} + +func TestRunCommandError(t *testing.T) { + shell := newPersistentShell(t.TempDir()) + _, _, err := shell.Exec(t.Context(), "nopenopenope") + if status := ExitCode(err); status == 0 { + t.Fatalf("Expected non-zero exit status, got %d", status) + } + if IsInterrupt(err) { + t.Fatalf("Expected command to not be interrupted, but it was") + } + if err == nil { + t.Fatalf("Expected an error, got nil") + } +} + +func TestRunContinuity(t *testing.T) { + shell := newPersistentShell(t.TempDir()) + shell.Exec(t.Context(), "export FOO=bar") + dst := t.TempDir() + shell.Exec(t.Context(), "cd "+dst) + out, _, _ := shell.Exec(t.Context(), "echo $FOO ; pwd") + expect := "bar\n" + dst + "\n" + if out != expect { + t.Fatalf("Expected output %q, got %q", expect, out) + } +}