on main help sign in
feat: use mvdan.cc/sh instead of a the user shell (#45)
100d994Carlos Alexandro Becker
go.mod+2−9
@@ -32,15 +32,16 @@ require (
32 github.com/pressly/goose/v3 v3.24.2
32 github.com/pressly/goose/v3 v3.24.2
33 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
33 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
34 github.com/sahilm/fuzzy v0.1.1
34 github.com/sahilm/fuzzy v0.1.1
35− github.com/shirou/gopsutil/v4 v4.25.5
36 github.com/spf13/cobra v1.9.1
35 github.com/spf13/cobra v1.9.1
37 github.com/spf13/viper v1.20.0
36 github.com/spf13/viper v1.20.0
38 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
37 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
39 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
38 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
40 github.com/stretchr/testify v1.10.0
39 github.com/stretchr/testify v1.10.0
40+ mvdan.cc/sh/v3 v3.11.0
41)
41)
42
42
43require (
43require (
44+ golang.org/x/term v0.31.0 // indirect
44 cloud.google.com/go v0.116.0 // indirect
45 cloud.google.com/go v0.116.0 // indirect
45 cloud.google.com/go/auth v0.13.0 // indirect
46 cloud.google.com/go/auth v0.13.0 // indirect
46 cloud.google.com/go/compute/metadata v0.6.0 // indirect
47 cloud.google.com/go/compute/metadata v0.6.0 // indirect
@@ -75,11 +76,9 @@ require (
75 github.com/disintegration/gift v1.1.2 // indirect
76 github.com/disintegration/gift v1.1.2 // indirect
76 github.com/dlclark/regexp2 v1.11.4 // indirect
77 github.com/dlclark/regexp2 v1.11.4 // indirect
77 github.com/dustin/go-humanize v1.0.1 // indirect
78 github.com/dustin/go-humanize v1.0.1 // indirect
78− github.com/ebitengine/purego v0.8.4 // indirect
79 github.com/felixge/httpsnoop v1.0.4 // indirect
79 github.com/felixge/httpsnoop v1.0.4 // indirect
80 github.com/go-logr/logr v1.4.2 // indirect
80 github.com/go-logr/logr v1.4.2 // indirect
81 github.com/go-logr/stdr v1.2.2 // indirect
81 github.com/go-logr/stdr v1.2.2 // indirect
82− github.com/go-ole/go-ole v1.2.6 // indirect
83 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
82 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
84 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
83 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
85 github.com/google/go-cmp v0.7.0 // indirect
84 github.com/google/go-cmp v0.7.0 // indirect
@@ -91,7 +90,6 @@ require (
91 github.com/inconshreveable/mousetrap v1.1.0 // indirect
90 github.com/inconshreveable/mousetrap v1.1.0 // indirect
92 github.com/kylelemons/godebug v1.1.0 // indirect
91 github.com/kylelemons/godebug v1.1.0 // indirect
93 github.com/lucasb-eyer/go-colorful v1.2.0
92 github.com/lucasb-eyer/go-colorful v1.2.0
94− github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
95 github.com/mattn/go-isatty v0.0.20 // indirect
93 github.com/mattn/go-isatty v0.0.20 // indirect
96 github.com/mattn/go-runewidth v0.0.16 // indirect
94 github.com/mattn/go-runewidth v0.0.16 // indirect
97 github.com/mfridman/interpolate v0.0.2 // indirect
95 github.com/mfridman/interpolate v0.0.2 // indirect
@@ -105,9 +103,7 @@ require (
105 github.com/pelletier/go-toml/v2 v2.2.3 // indirect
103 github.com/pelletier/go-toml/v2 v2.2.3 // indirect
106 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
104 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
107 github.com/pmezard/go-difflib v1.0.0 // indirect
105 github.com/pmezard/go-difflib v1.0.0 // indirect
108− github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
109 github.com/rivo/uniseg v0.4.7
106 github.com/rivo/uniseg v0.4.7
110− github.com/rogpeppe/go-internal v1.14.1 // indirect
111 github.com/sagikazarmark/locafero v0.7.0 // indirect
107 github.com/sagikazarmark/locafero v0.7.0 // indirect
112 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
108 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
113 github.com/sethvargo/go-retry v0.3.0 // indirect
109 github.com/sethvargo/go-retry v0.3.0 // indirect
@@ -121,13 +117,10 @@ require (
121 github.com/tidwall/match v1.1.1 // indirect
117 github.com/tidwall/match v1.1.1 // indirect
122 github.com/tidwall/pretty v1.2.1 // indirect
118 github.com/tidwall/pretty v1.2.1 // indirect
123 github.com/tidwall/sjson v1.2.5 // indirect
119 github.com/tidwall/sjson v1.2.5 // indirect
124− github.com/tklauser/go-sysconf v0.3.12 // indirect
125− github.com/tklauser/numcpus v0.6.1 // indirect
126 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
120 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
127 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
121 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
128 github.com/yuin/goldmark v1.7.8 // indirect
122 github.com/yuin/goldmark v1.7.8 // indirect
129 github.com/yuin/goldmark-emoji v1.0.5 // indirect
123 github.com/yuin/goldmark-emoji v1.0.5 // indirect
130− github.com/yusufpapurcu/wmi v1.2.4 // indirect
131 go.opentelemetry.io/auto/sdk v1.1.0 // indirect
124 go.opentelemetry.io/auto/sdk v1.1.0 // indirect
132 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
125 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
133 go.opentelemetry.io/otel v1.35.0 // indirect
126 go.opentelemetry.io/otel v1.35.0 // indirect
go.sum+8−21
@@ -97,6 +97,8 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE
97github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
97github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
98github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
98github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
99github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
99github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
100+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
101+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
100github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
102github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
101github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
103github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
102github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
104github.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
108github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
110github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
109github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
111github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
110github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
112github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
111−github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
112−github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
113github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
113github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
114github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
114github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
115github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
115github.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=
123github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
123github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
124github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
124github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
125github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
125github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
126−github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
126+github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
127−github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
127+github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
128github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
128github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
129github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
129github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
130github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
130github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
131github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
131github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
132github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
132github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
133github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
133github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
134−github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
135github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
134github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
136github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
135github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
137github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
136github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
@@ -161,8 +160,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
161github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
160github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
162github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
161github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
163github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
162github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
164−github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
165−github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
166github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
163github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
167github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
164github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
168github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
165github.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
202github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
199github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
203github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
200github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
204github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
201github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
205−github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
206−github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
207github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
202github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
208github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
203github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
209github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
204github.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
228github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
223github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
229github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
224github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
230github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
225github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
231−github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
232−github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
233github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
226github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
234github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
227github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
235github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
228github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -266,10 +259,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
266github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
259github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
267github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
260github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
268github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
261github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
269−github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
270−github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
271−github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
272−github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
273github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
262github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
274github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
263github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
275github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
264github.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=
280github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
269github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
281github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
270github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
282github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
271github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
283−github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
284−github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
285go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
272go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
286go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
273go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
287go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
274go.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=
328golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
315golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
329golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
316golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
330golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
317golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
331−golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
332golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
318golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
333−golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
334golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
319golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
335golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
320golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
336golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
321golang.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=
339golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
324golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
340golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
325golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
341golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
326golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
342−golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
343golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
327golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
344golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
328golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
345golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
329golang.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=
353golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
337golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
354golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
338golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
355golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
339golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
340+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
341+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
356golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
342golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
357golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
343golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
358golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
344golang.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
367golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
353golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
368golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
354golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
369golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
355golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
370−golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
371google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI=
356google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI=
372google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY=
357google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY=
373google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
358google.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=
394modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
379modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
395modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA=
380modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA=
396modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
381modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
382+mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
383+mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
internal/config/config.go−15
@@ -73,12 +73,6 @@ type TUIConfig struct {
73 Theme string `json:"theme,omitempty"`
73 Theme string `json:"theme,omitempty"`
74}
74}
75
75
76−// ShellConfig defines the configuration for the shell used by the bash tool.
77−type ShellConfig struct {
78− Path string `json:"path,omitempty"`
79− Args []string `json:"args,omitempty"`
80−}
81−
82// Config is the main configuration structure for the application.
76// Config is the main configuration structure for the application.
83type Config struct {
77type Config struct {
84 Data Data `json:"data"`
78 Data Data `json:"data"`
@@ -91,7 +85,6 @@ type Config struct {
91 DebugLSP bool `json:"debugLSP,omitempty"`
85 DebugLSP bool `json:"debugLSP,omitempty"`
92 ContextPaths []string `json:"contextPaths,omitempty"`
86 ContextPaths []string `json:"contextPaths,omitempty"`
93 TUI TUIConfig `json:"tui"`
87 TUI TUIConfig `json:"tui"`
94− Shell ShellConfig `json:"shell,omitempty"`
95 AutoCompact bool `json:"autoCompact,omitempty"`
88 AutoCompact bool `json:"autoCompact,omitempty"`
96}
89}
97
90
@@ -224,14 +217,6 @@ func setDefaults(debug bool) {
224 viper.SetDefault("tui.theme", "crush")
217 viper.SetDefault("tui.theme", "crush")
225 viper.SetDefault("autoCompact", true)
218 viper.SetDefault("autoCompact", true)
226
219
227− // Set default shell from environment or fallback to /bin/bash
228− shellPath := os.Getenv("SHELL")
229− if shellPath == "" {
230− shellPath = "/bin/bash"
231− }
232− viper.SetDefault("shell.path", shellPath)
233− viper.SetDefault("shell.args", []string{"-l"})
234−
235 if debug {
220 if debug {
236 viper.SetDefault("debug", true)
221 viper.SetDefault("debug", true)
237 viper.Set("log.level", "debug")
222 viper.Set("log.level", "debug")
internal/llm/tools/bash.go+11−3
@@ -285,9 +285,17 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
285 }
285 }
286 }
286 }
287 startTime := time.Now()
287 startTime := time.Now()
288+ if params.Timeout > 0 {
289+ var cancel context.CancelFunc
290+ ctx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Millisecond)
291+ defer cancel()
292+ }
293+ stdout, stderr, err := shell.
288− shell := shell.GetPersistentShell(config.WorkingDirectory())
294+ GetPersistentShell(config.WorkingDirectory()).
289− stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout)
295+ Exec(ctx, params.Command)
296+ interrupted := shell.IsInterrupt(err)
297+ exitCode := shell.ExitCode(err)
290− if err != nil {
298+ if exitCode == 0 && !interrupted && err != nil {
291 return ToolResponse{}, fmt.Errorf("error executing command: %w", err)
299 return ToolResponse{}, fmt.Errorf("error executing command: %w", err)
292 }
300 }
293
301
internal/llm/tools/shell/comparison_test.go+7−18
@@ -1,8 +1,6 @@
1package shell
1package shell
2
2
3import (
3import (
4− "context"
5− "os"
6 "testing"
4 "testing"
7 "time"
5 "time"
8
6
@@ -11,16 +9,12 @@ import (
11)
9)
12
10
13func TestShellPerformanceComparison(t *testing.T) {
11func TestShellPerformanceComparison(t *testing.T) {
14− tmpDir, err := os.MkdirTemp("", "shell-test")
12+ shell := newPersistentShell(t.TempDir())
15− require.NoError(t, err)
16− defer os.RemoveAll(tmpDir)
17−
18− shell := GetPersistentShell(tmpDir)
19− defer shell.Close()
20
13
21 // Test quick command
14 // Test quick command
22 start := time.Now()
15 start := time.Now()
23− stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello'", 0)
16+ stdout, stderr, err := shell.Exec(t.Context(), "echo 'hello'")
17+ exitCode := ExitCode(err)
24 duration := time.Since(start)
18 duration := time.Since(start)
25
19
26 require.NoError(t, err)
20 require.NoError(t, err)
@@ -33,19 +27,14 @@ func TestShellPerformanceComparison(t *testing.T) {
33
27
34// Benchmark CPU usage during polling
28// Benchmark CPU usage during polling
35func BenchmarkShellPolling(b *testing.B) {
29func BenchmarkShellPolling(b *testing.B) {
36− tmpDir, err := os.MkdirTemp("", "shell-bench")
30+ shell := newPersistentShell(b.TempDir())
37− require.NoError(b, err)
38− defer os.RemoveAll(tmpDir)
39−
40− shell := GetPersistentShell(tmpDir)
41− defer shell.Close()
42
31
43− b.ResetTimer()
44 b.ReportAllocs()
32 b.ReportAllocs()
45
33
46− for i := 0; i < b.N; i++ {
34+ for b.Loop() {
47 // Use a short sleep to measure polling overhead
35 // Use a short sleep to measure polling overhead
48− _, _, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.02", 500)
36+ _, _, err := shell.Exec(b.Context(), "sleep 0.02")
37+ exitCode := ExitCode(err)
49 if err != nil || exitCode != 0 {
38 if err != nil || exitCode != 0 {
50 b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
39 b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
51 }
40 }
internal/llm/tools/shell/shell.go+46−271
@@ -1,312 +1,87 @@
1package shell
1package shell
2
2
3import (
3import (
4− "cmp"
4+ "bytes"
5 "context"
5 "context"
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8− "io"
9 "os"
8 "os"
10− "os/exec"
11− "path/filepath"
12 "strings"
9 "strings"
13 "sync"
10 "sync"
14− "syscall"
15− "time"
16
11
17− "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/logging"
12 "github.com/charmbracelet/crush/internal/logging"
19− "github.com/shirou/gopsutil/v4/process"
13+ "mvdan.cc/sh/v3/expand"
14+ "mvdan.cc/sh/v3/interp"
15+ "mvdan.cc/sh/v3/syntax"
20)
16)
21
17
22type PersistentShell struct {
18type PersistentShell struct {
23− cmd *exec.Cmd
19+ env []string
24− stdin io.WriteCloser
25− isAlive bool
26− cwd string
20+ cwd string
27− mu sync.Mutex
21+ mu sync.Mutex
28− commandQueue chan *commandExecution
29}
22}
30
23
31−type commandExecution struct {
24+var (
32− command string
25+ once sync.Once
33− timeout time.Duration
34− resultChan chan commandResult
35− ctx context.Context
36−}
37−
38−type commandResult struct {
39− stdout string
40− stderr string
41− exitCode int
42− interrupted bool
43− err error
44−}
45−
46−var shellInstance *PersistentShell
26+ shellInstance *PersistentShell
27+)
47
28
48−func GetPersistentShell(workingDir string) *PersistentShell {
29+func GetPersistentShell(cwd string) *PersistentShell {
49− if shellInstance == nil {
30+ once.Do(func() {
50− shellInstance = newPersistentShell(workingDir)
51− }
52− if !shellInstance.isAlive {
53− shellInstance = newPersistentShell(shellInstance.cwd)
31+ shellInstance = newPersistentShell(cwd)
54− }
32+ })
55 return shellInstance
33 return shellInstance
56}
34}
57
35
58func newPersistentShell(cwd string) *PersistentShell {
36func newPersistentShell(cwd string) *PersistentShell {
59− // Get shell configuration from config
37+ return &PersistentShell{
60− cfg := config.Get()
61−
62− // Default to environment variable if config is not set or nil
63− var shellPath string
64− var shellArgs []string
65−
66− if cfg != nil {
67− shellPath = cfg.Shell.Path
68− shellArgs = cfg.Shell.Args
69− }
70−
71− shellPath = cmp.Or(shellPath, os.Getenv("SHELL"), "/bin/bash")
72− if !strings.HasSuffix(shellPath, "bash") && !strings.HasSuffix(shellPath, "zsh") {
73− logging.Warn("only bash and zsh are supported at this time", "shell", shellPath)
74− shellPath = "/bin/bash"
75− }
76−
77− // Default shell args
78− if len(shellArgs) == 0 {
79− shellArgs = []string{"--login"}
80− }
81−
82− cmd := exec.Command(shellPath, shellArgs...)
83− cmd.Dir = cwd
38+ cwd: cwd,
84−
39+ env: os.Environ(),
85− stdinPipe, err := cmd.StdinPipe()
86− if err != nil {
87− return nil
88− }
89−
90− cmd.Env = append(os.Environ(), "GIT_EDITOR=true")
91−
92− err = cmd.Start()
93− if err != nil {
94− return nil
95− }
96−
97− shell := &PersistentShell{
98− cmd: cmd,
99− stdin: stdinPipe,
100− isAlive: true,
101− cwd: cwd,
102− commandQueue: make(chan *commandExecution, 10),
103− }
104−
105− go func() {
106− defer func() {
107− if r := recover(); r != nil {
108− fmt.Fprintf(os.Stderr, "Panic in shell command processor: %v\n", r)
109− shell.isAlive = false
110− close(shell.commandQueue)
111− }
112− }()
113− shell.processCommands()
114− }()
115−
116− go func() {
117− err := cmd.Wait()
118− if err != nil {
119− // Log the error if needed
120− }
121− shell.isAlive = false
122− close(shell.commandQueue)
123− }()
124−
125− return shell
126−}
127−
128−func (s *PersistentShell) processCommands() {
129− for cmd := range s.commandQueue {
130− cmd.resultChan <- s.execCommand(cmd.ctx, cmd.command, cmd.timeout)
131 }
40 }
132}
41}
133
42
134−const runBashCommandFormat = `%s </dev/null >%q 2>%q
135−echo $? >%q
136−pwd >%q`
137−
138−func (s *PersistentShell) execCommand(ctx context.Context, command string, timeout time.Duration) commandResult {
43+func (s *PersistentShell) Exec(ctx context.Context, command string) (string, string, error) {
139 s.mu.Lock()
44 s.mu.Lock()
140 defer s.mu.Unlock()
45 defer s.mu.Unlock()
141
46
142− if !s.isAlive {
47+ line, err := syntax.NewParser().Parse(strings.NewReader(command), "")
143− return commandResult{
144− stderr: "Shell is not alive",
145− exitCode: 1,
146− err: errors.New("shell is not alive"),
147− }
148− }
149−
150− tmp := os.TempDir()
151− now := time.Now().UnixNano()
152− stdoutFile := filepath.Join(tmp, fmt.Sprintf("crush-stdout-%d", now))
153− stderrFile := filepath.Join(tmp, fmt.Sprintf("crush-stderr-%d", now))
154− statusFile := filepath.Join(tmp, fmt.Sprintf("crush-status-%d", now))
155− cwdFile := filepath.Join(tmp, fmt.Sprintf("crush-cwd-%d", now))
156−
157− defer func() {
158− _ = os.Remove(stdoutFile)
159− _ = os.Remove(stderrFile)
160− _ = os.Remove(statusFile)
161− _ = os.Remove(cwdFile)
162− }()
163−
164− script := fmt.Sprintf(runBashCommandFormat, command, stdoutFile, stderrFile, statusFile, cwdFile)
165− if _, err := s.stdin.Write([]byte(script + "\n")); err != nil {
166− return commandResult{
167− stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
168− exitCode: 1,
169− err: err,
170− }
171− }
172−
173− interrupted := false
174− done := make(chan bool)
175− go func() {
176− // Use exponential backoff polling
177− pollInterval := 10 * time.Millisecond
178− maxPollInterval := time.Second
179−
180− ticker := time.NewTicker(pollInterval)
181− defer ticker.Stop()
182−
183− timeoutTicker := time.NewTicker(cmp.Or(timeout, time.Hour*99999))
184− defer timeoutTicker.Stop()
185−
186− for {
187− select {
188− case <-ctx.Done():
189− s.killChildren()
190− interrupted = true
191− done <- true
192− return
193−
194− case <-timeoutTicker.C:
195− s.killChildren()
196− interrupted = true
197− done <- true
198− return
199−
200− case <-ticker.C:
201− if fileSize(statusFile) > 0 {
202− done <- true
203− return
204− }
205−
206− // Exponential backoff to reduce CPU usage for longer-running commands
207− if pollInterval < maxPollInterval {
208− pollInterval = min(time.Duration(float64(pollInterval)*1.5), maxPollInterval)
209− ticker.Reset(pollInterval)
210− }
211− }
212− }
213− }()
214−
215− <-done
216−
217− stdout := readFileOrEmpty(stdoutFile)
218− stderr := readFileOrEmpty(stderrFile)
219− exitCodeStr := readFileOrEmpty(statusFile)
220− newCwd := readFileOrEmpty(cwdFile)
221−
222− exitCode := 0
223− if exitCodeStr != "" {
224− fmt.Sscanf(exitCodeStr, "%d", &exitCode)
225− } else if interrupted {
226− exitCode = 143
227− stderr += "\nCommand execution timed out or was interrupted"
228− }
229−
230− if newCwd != "" {
231− s.cwd = strings.TrimSpace(newCwd)
232− }
233−
234− return commandResult{
235− stdout: stdout,
236− stderr: stderr,
237− exitCode: exitCode,
238− interrupted: interrupted,
239− }
240−}
241−
242−func (s *PersistentShell) killChildren() {
243− if s.cmd == nil || s.cmd.Process == nil {
244− return
245− }
246− p, err := process.NewProcess(int32(s.cmd.Process.Pid))
247 if err != nil {
48 if err != nil {
248− logging.WarnPersist("could not kill persistent shell child processes", "err", err)
49+ return "", "", fmt.Errorf("could not parse command: %w", err)
249− return
250 }
50 }
251
51
252− children, err := p.Children()
52+ var stdout, stderr bytes.Buffer
53+ runner, err := interp.New(
54+ interp.StdIO(nil, &stdout, &stderr),
55+ interp.Interactive(false),
56+ interp.Env(expand.ListEnviron(s.env...)),
57+ interp.Dir(s.cwd),
58+ )
253 if err != nil {
59 if err != nil {
254− logging.WarnPersist("could not kill persistent shell child processes", "err", err)
60+ return "", "", fmt.Errorf("could not run command: %w", err)
255− return
256− }
257−
258− for _, child := range children {
259− if err := child.SendSignal(syscall.SIGTERM); err != nil {
260− logging.WarnPersist("could not kill persistent shell child processes", "err", err, "pid", child.Pid)
261− }
262− }
263−}
264−
265−func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs int) (string, string, int, bool, error) {
266− if !s.isAlive {
267− return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
268− }
269−
270− resultChan := make(chan commandResult)
271− s.commandQueue <- &commandExecution{
272− command: command,
273− timeout: time.Duration(timeoutMs) * time.Millisecond,
274− resultChan: resultChan,
275− ctx: ctx,
276− }
277−
278− result := <-resultChan
279− return result.stdout, result.stderr, result.exitCode, result.interrupted, result.err
280−}
281−
282−func (s *PersistentShell) Close() {
283− s.mu.Lock()
284− defer s.mu.Unlock()
285−
286− if !s.isAlive {
287− return
288 }
61 }
289
62
290− s.stdin.Write([]byte("exit\n"))
63+ err = runner.Run(ctx, line)
291−
64+ s.cwd = runner.Dir
292− if err := s.cmd.Process.Kill(); err != nil {
65+ s.env = []string{}
293− logging.WarnPersist("could not kill persistent shell", "err", err)
66+ for name, vr := range runner.Vars {
67+ s.env = append(s.env, fmt.Sprintf("%s=%s", name, vr.Str))
294 }
68 }
295− s.isAlive = false
69+ logging.InfoPersist("Command finished", "command", command, "err", err)
70+ return stdout.String(), stderr.String(), err
296}
71}
297
72
298−func readFileOrEmpty(path string) string {
73+func IsInterrupt(err error) bool {
299− content, err := os.ReadFile(path)
74+ return errors.Is(err, context.Canceled) ||
300− if err != nil {
75+ errors.Is(err, context.DeadlineExceeded)
301− return ""
302− }
303− return string(content)
304}
76}
305
77
306−func fileSize(path string) int64 {
78+func ExitCode(err error) int {
307− info, err := os.Stat(path)
308− if err != nil {
79+ if err == nil {
309 return 0
80 return 0
310 }
81 }
311− return info.Size()
82+ status, ok := interp.IsExitStatus(err)
83+ if ok {
84+ return int(status)
85+ }
86+ return 1
312}
87}
internal/llm/tools/shell/shell_test.go+65−12
@@ -2,28 +2,81 @@ package shell
2
2
3import (
3import (
4 "context"
4 "context"
5− "os"
6 "testing"
5 "testing"
7−
6+ "time"
8− "github.com/stretchr/testify/require"
9)
7)
10
8
11// Benchmark to measure CPU efficiency
9// Benchmark to measure CPU efficiency
12func BenchmarkShellQuickCommands(b *testing.B) {
10func BenchmarkShellQuickCommands(b *testing.B) {
13− tmpDir, err := os.MkdirTemp("", "shell-bench")
11+ shell := newPersistentShell(b.TempDir())
14− require.NoError(b, err)
15− defer os.RemoveAll(tmpDir)
16−
17− shell := GetPersistentShell(tmpDir)
18− defer shell.Close()
19
12
20− b.ResetTimer()
21 b.ReportAllocs()
13 b.ReportAllocs()
22
14
23− for i := 0; i < b.N; i++ {
15+ for b.Loop() {
24− _, _, exitCode, _, err := shell.Exec(context.Background(), "echo test", 0)
16+ _, _, err := shell.Exec(context.Background(), "echo test")
17+ exitCode := ExitCode(err)
25 if err != nil || exitCode != 0 {
18 if err != nil || exitCode != 0 {
26 b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
19 b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
27 }
20 }
28 }
21 }
29}
22}
23+
24+func TestTestTimeout(t *testing.T) {
25+ ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
26+ t.Cleanup(cancel)
27+
28+ shell := newPersistentShell(t.TempDir())
29+ _, _, err := shell.Exec(ctx, "sleep 10")
30+ if status := ExitCode(err); status == 0 {
31+ t.Fatalf("Expected non-zero exit status, got %d", status)
32+ }
33+ if !IsInterrupt(err) {
34+ t.Fatalf("Expected command to be interrupted, but it was not")
35+ }
36+ if err == nil {
37+ t.Fatalf("Expected an error due to timeout, but got none")
38+ }
39+}
40+
41+func TestTestCancel(t *testing.T) {
42+ ctx, cancel := context.WithCancel(t.Context())
43+ cancel() // immediately cancel the context
44+
45+ shell := newPersistentShell(t.TempDir())
46+ _, _, err := shell.Exec(ctx, "sleep 10")
47+ if status := ExitCode(err); status == 0 {
48+ t.Fatalf("Expected non-zero exit status, got %d", status)
49+ }
50+ if !IsInterrupt(err) {
51+ t.Fatalf("Expected command to be interrupted, but it was not")
52+ }
53+ if err == nil {
54+ t.Fatalf("Expected an error due to cancel, but got none")
55+ }
56+}
57+
58+func TestRunCommandError(t *testing.T) {
59+ shell := newPersistentShell(t.TempDir())
60+ _, _, err := shell.Exec(t.Context(), "nopenopenope")
61+ if status := ExitCode(err); status == 0 {
62+ t.Fatalf("Expected non-zero exit status, got %d", status)
63+ }
64+ if IsInterrupt(err) {
65+ t.Fatalf("Expected command to not be interrupted, but it was")
66+ }
67+ if err == nil {
68+ t.Fatalf("Expected an error, got nil")
69+ }
70+}
71+
72+func TestRunContinuity(t *testing.T) {
73+ shell := newPersistentShell(t.TempDir())
74+ shell.Exec(t.Context(), "export FOO=bar")
75+ dst := t.TempDir()
76+ shell.Exec(t.Context(), "cd "+dst)
77+ out, _, _ := shell.Exec(t.Context(), "echo $FOO ; pwd")
78+ expect := "bar\n" + dst + "\n"
79+ if out != expect {
80+ t.Fatalf("Expected output %q, got %q", expect, out)
81+ }
82+}