diff --git a/cli/go.mod b/cli/go.mod index e95bbd6..fdc481c 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -6,20 +6,21 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/azure/azure-dev/cli/azd v0.0.0-20260205194320-e04533f58fa7 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be - github.com/jongio/azd-core v0.5.2 + github.com/jongio/azd-core v0.5.3-0.20260224175512-adb4c33ad704 github.com/magefile/mage v1.15.0 github.com/mark3labs/mcp-go v0.43.2 github.com/spf13/cobra v1.10.2 - go.opentelemetry.io/otel v1.40.0 gopkg.in/yaml.v3 v3.0.1 ) require ( dario.cat/mergo v1.0.2 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymanbagabas/go-udiff v0.3.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -31,7 +32,6 @@ require ( github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20251126160633-0b68cdcd21da // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20260204111555-7642919e0bee // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect @@ -40,7 +40,6 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/drone/envsubst v1.0.3 // indirect - github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/fatih/color v1.18.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -69,7 +68,6 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -77,6 +75,7 @@ require ( github.com/yuin/goldmark v1.7.16 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect @@ -86,7 +85,10 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/azure/azure-dev/cli/azd => github.com/jongio/azure-dev/cli/azd v0.0.0-20260224163340-dd44e36d1cd2 diff --git a/cli/go.sum b/cli/go.sum index 8b0e0fa..38f5f93 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -9,6 +9,7 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b h1:g9SuFmxM/WucQFKTMSP+irxyf5m0RiUJreBDhGI6jSA= github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b/go.mod h1:XjvqMUpGd3Xn9Jtzk/4GEBCSoBX0eB2RyriXgne0IdM= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -23,8 +24,6 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/azure/azure-dev/cli/azd v0.0.0-20260205194320-e04533f58fa7 h1:QuZ6VYe6W6EQ0R3DZ2peqQ84jZAPahdlU7YxjoCyceM= -github.com/azure/azure-dev/cli/azd v0.0.0-20260205194320-e04533f58fa7/go.mod h1:vyRHLHkz8aTysH3glwBZASFFWeF2huUTDHUjuL8ZqZs= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -66,6 +65,7 @@ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -74,8 +74,6 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -103,6 +101,7 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -112,8 +111,10 @@ github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/ github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jongio/azd-core v0.5.2 h1:AGYzGkvzW/wv23YLL0uOapmqe0rAHlZv4c0j2ZEd4lY= -github.com/jongio/azd-core v0.5.2/go.mod h1:sNOxz/3TEMYDNybsyUSAzpx0VHhLt959b+aeiy1OG3Y= +github.com/jongio/azd-core v0.5.3-0.20260224175512-adb4c33ad704 h1:suCpNTx5Bi+GaTE5Cyj5LAQ4q3/gWsBEpZMehlgfp+E= +github.com/jongio/azd-core v0.5.3-0.20260224175512-adb4c33ad704/go.mod h1:jQCP+px3Pxb3B0fyShfvSVa3KsWT1j2jGXMsPpQezlI= +github.com/jongio/azure-dev/cli/azd v0.0.0-20260224163340-dd44e36d1cd2 h1:nw+lYEeXoPJJCETrAf9TXKsA0TGfi5WbmXch0ZkjalY= +github.com/jongio/azure-dev/cli/azd v0.0.0-20260224163340-dd44e36d1cd2/go.mod h1:zNtJ765AlE7Jsfy0T7qeiFHIHjNWLAYIXoPKVHVWpnk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -131,13 +132,16 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8 github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -182,6 +186,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= @@ -197,6 +202,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM 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= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= @@ -216,22 +222,47 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= diff --git a/cli/src/cmd/copilot/commands/listen.go b/cli/src/cmd/copilot/commands/listen.go index 39c7b87..6e1c6f0 100644 --- a/cli/src/cmd/copilot/commands/listen.go +++ b/cli/src/cmd/copilot/commands/listen.go @@ -15,43 +15,16 @@ import ( // NewListenCommand creates a new listen command that establishes // a connection with azd for extension framework operations. func NewListenCommand() *cobra.Command { - return &cobra.Command{ - Use: "listen", - Short: "Start the extension server (required by azd framework)", - Long: `Internal command used by the azd CLI to communicate with this extension via JSON-RPC over stdio.`, - Hidden: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - // Create a context with the AZD access token - ctx := azdext.WithAccessToken(cmd.Context()) - - // Create a new AZD client - azdClient, err := azdext.NewAzdClient() - if err != nil { - return fmt.Errorf("failed to create azd client: %w", err) - } - defer azdClient.Close() - - // Create an extension host with lifecycle event handlers - host := azdext.NewExtensionHost(azdClient). - // Project-level events - WithProjectEventHandler("preinit", handlePreInit). - WithProjectEventHandler("preprovision", handlePreProvision). - WithProjectEventHandler("postprovision", handlePostProvision). - // Service-level events - WithServiceEventHandler("predeploy", handlePreDeploy, &azdext.ServiceEventOptions{}). - WithServiceEventHandler("postdeploy", handlePostDeploy, &azdext.ServiceEventOptions{}) - - // Start the extension host - // This blocks until azd closes the connection - if err := host.Run(ctx); err != nil { - fmt.Fprintf(os.Stderr, "Extension host error: %v\n", err) - return fmt.Errorf("failed to run extension: %w", err) - } - - return nil - }, - } + return azdext.NewListenCommand(func(host *azdext.ExtensionHost) { + host. + // Project-level events + WithProjectEventHandler("preinit", handlePreInit). + WithProjectEventHandler("preprovision", handlePreProvision). + WithProjectEventHandler("postprovision", handlePostProvision). + // Service-level events + WithServiceEventHandler("predeploy", handlePreDeploy, &azdext.ServiceEventOptions{}). + WithServiceEventHandler("postdeploy", handlePostDeploy, &azdext.ServiceEventOptions{}) + }) } // handlePreInit is called before azd init completes diff --git a/cli/src/cmd/copilot/commands/mcp.go b/cli/src/cmd/copilot/commands/mcp.go index 78497f5..2298995 100644 --- a/cli/src/cmd/copilot/commands/mcp.go +++ b/cli/src/cmd/copilot/commands/mcp.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/jongio/azd-copilot/cli/src/internal/copilot" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -62,34 +63,28 @@ func newMCPConfigureCommand() *cobra.Command { // serveMCP starts the MCP server for azd-copilot extension func serveMCP(ctx context.Context) error { - // Create MCP server - s := server.NewMCPServer( - "azd-copilot", - Version, - server.WithToolCapabilities(true), - server.WithResourceCapabilities(true, false), - ) + builder := azdext.NewMCPServerBuilder("azd-copilot", Version). + WithRateLimit(10, 1.0). + WithResourceCapabilities(true, false) // Register tools - registerMCPTools(s) + registerMCPTools(builder) // Register resources - registerMCPResources(s) + registerMCPResources(builder) - // Start stdio server + // Build and start stdio server + s := builder.Build() return server.ServeStdio(s) } -func registerMCPTools(s *server.MCPServer) { +func registerMCPTools(builder *azdext.MCPServerBuilder) { // Register gRPC service tools (environments, deployments, accounts, workflows, compose) - registerGRPCTools(s) + registerGRPCTools(builder) // Tool: list_agents - s.AddTool( - mcp.NewTool("list_agents", - mcp.WithDescription("List all available Azure agents"), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + builder.AddTool("list_agents", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { agents := []string{ "azure-manager - Orchestrates all agents, main entry point", "azure-architect - Infrastructure design, Bicep, networking", @@ -106,14 +101,15 @@ func registerMCPTools(s *server.MCPServer) { } return mcp.NewToolResultText(result), nil }, + azdext.MCPToolOptions{ + Description: "List all available Azure agents", + ReadOnly: true, + }, ) // Tool: list_skills - s.AddTool( - mcp.NewTool("list_skills", - mcp.WithDescription("List all available Azure skills"), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + builder.AddTool("list_skills", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { skills := []string{ "azure-prepare - Initialize project for Azure hosting", "azure-deploy - Deployment patterns and best practices", @@ -128,56 +124,58 @@ func registerMCPTools(s *server.MCPServer) { } return mcp.NewToolResultText(result), nil }, + azdext.MCPToolOptions{ + Description: "List all available Azure skills", + ReadOnly: true, + }, ) // Tool: get_project_context - s.AddTool( - mcp.NewTool("get_project_context", - mcp.WithDescription("Get current azd project context including services and environment"), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // This would normally query the azd client + builder.AddTool("get_project_context", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { result := `Project Context: - Check azure.yaml for project configuration - Check .azure/ folder for environment settings - Run 'azd context' for full details` return mcp.NewToolResultText(result), nil }, + azdext.MCPToolOptions{ + Description: "Get current azd project context including services and environment", + ReadOnly: true, + }, ) // Tool: create_checkpoint - s.AddTool( - mcp.NewTool("create_checkpoint", - mcp.WithDescription("Create a checkpoint to save current build state"), - mcp.WithString("description", mcp.Required(), mcp.Description("Description of the checkpoint")), - mcp.WithString("phase", mcp.Description("Build phase (spec, design, develop, quality, deploy)")), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // Extract arguments from the map - args, ok := req.Params.Arguments.(map[string]interface{}) - if !ok { - return mcp.NewToolResultText("Error: invalid arguments"), nil + builder.AddTool("create_checkpoint", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { + description, err := args.RequireString("description") + if err != nil { + return azdext.MCPErrorResult("missing description: %s", err), nil } - description, _ := args["description"].(string) - phase, _ := args["phase"].(string) + phase := args.OptionalString("phase", "") result := fmt.Sprintf("Checkpoint created:\n- Description: %s\n- Phase: %s\n- Use 'azd copilot checkpoints' to list all checkpoints", description, phase) return mcp.NewToolResultText(result), nil }, + azdext.MCPToolOptions{ + Description: "Create a checkpoint to save current build state", + }, + mcp.WithString("description", mcp.Required(), mcp.Description("Description of the checkpoint")), + mcp.WithString("phase", mcp.Description("Build phase (spec, design, develop, quality, deploy)")), ) } -func registerMCPResources(s *server.MCPServer) { - // Resource: agents - s.AddResource( - mcp.NewResource( - "azd-copilot://agents", - "Azure Agents", - mcp.WithResourceDescription("List of available Azure agents"), - mcp.WithMIMEType("text/plain"), - ), - func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - content := `# Azure Agents +func registerMCPResources(builder *azdext.MCPServerBuilder) { + builder.AddResources( + server.ServerResource{ + Resource: mcp.NewResource( + "azd-copilot://agents", + "Azure Agents", + mcp.WithResourceDescription("List of available Azure agents"), + mcp.WithMIMEType("text/plain"), + ), + Handler: func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + content := `# Azure Agents The following specialized agents are available: @@ -194,26 +192,24 @@ The following specialized agents are available: Use 'azd copilot agents' for full details.` - return []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: req.Params.URI, - MIMEType: "text/plain", - Text: content, - }, - }, nil + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "text/plain", + Text: content, + }, + }, nil + }, }, - ) - - // Resource: skills - s.AddResource( - mcp.NewResource( - "azd-copilot://skills", - "Azure Skills", - mcp.WithResourceDescription("List of available Azure skills"), - mcp.WithMIMEType("text/plain"), - ), - func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - content := `# Azure Skills + server.ServerResource{ + Resource: mcp.NewResource( + "azd-copilot://skills", + "Azure Skills", + mcp.WithResourceDescription("List of available Azure skills"), + mcp.WithMIMEType("text/plain"), + ), + Handler: func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + content := `# Azure Skills Skills provide focused expertise for specific tasks: @@ -230,13 +226,14 @@ Skills provide focused expertise for specific tasks: Use 'azd copilot skills' for full details.` - return []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: req.Params.URI, - MIMEType: "text/plain", - Text: content, - }, - }, nil + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "text/plain", + Text: content, + }, + }, nil + }, }, ) } diff --git a/cli/src/cmd/copilot/commands/mcp_grpc_tools.go b/cli/src/cmd/copilot/commands/mcp_grpc_tools.go index 0c0ee13..c76141d 100644 --- a/cli/src/cmd/copilot/commands/mcp_grpc_tools.go +++ b/cli/src/cmd/copilot/commands/mcp_grpc_tools.go @@ -5,24 +5,19 @@ package commands import ( "context" - "encoding/json" "fmt" "github.com/azure/azure-dev/cli/azd/pkg/azdext" - "github.com/jongio/azd-core/azdextutil" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" ) -var grpcRateLimiter = azdextutil.NewRateLimiter(10, 1.0) - // registerGRPCTools registers MCP tools that wrap azd gRPC services. -func registerGRPCTools(s *server.MCPServer) { - registerEnvironmentTools(s) - registerDeploymentTools(s) - registerAccountTools(s) - registerWorkflowTools(s) - registerComposeTools(s) +func registerGRPCTools(builder *azdext.MCPServerBuilder) { + registerEnvironmentTools(builder) + registerDeploymentTools(builder) + registerAccountTools(builder) + registerWorkflowTools(builder) + registerComposeTools(builder) } // newAzdClient creates a new azd gRPC client and returns it along with a context @@ -35,27 +30,19 @@ func newAzdClient(ctx context.Context) (context.Context, *azdext.AzdClient, erro return azdext.WithAccessToken(ctx), client, nil } -func registerEnvironmentTools(s *server.MCPServer) { +func registerEnvironmentTools(builder *azdext.MCPServerBuilder) { // Tool: list_environments - s.AddTool( - mcp.NewTool("list_environments", - mcp.WithDescription("List all azd environments"), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("list_environments"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - + builder.AddTool("list_environments", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Environment().List(ctx, &azdext.EmptyRequest{}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("listing environments: %s", err)), nil + return azdext.MCPErrorResult("listing environments: %s", err), nil } type envInfo struct { @@ -75,44 +62,31 @@ func registerEnvironmentTools(s *server.MCPServer) { }) } - data, err := json.MarshalIndent(envs, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(envs), nil + }, + azdext.MCPToolOptions{ + Description: "List all azd environments", + ReadOnly: true, }, ) // Tool: get_environment_values - s.AddTool( - mcp.NewTool("get_environment_values", - mcp.WithDescription("Get all key-value pairs for an azd environment"), - mcp.WithString("environment_name", mcp.Required(), mcp.Description("Name of the environment")), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("get_environment_values"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - args, ok := req.Params.Arguments.(map[string]interface{}) - if !ok { - return mcp.NewToolResultError("invalid arguments"), nil - } - name, _ := args["environment_name"].(string) - if name == "" { - return mcp.NewToolResultError("environment_name is required"), nil + builder.AddTool("get_environment_values", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { + name, err := args.RequireString("environment_name") + if err != nil { + return azdext.MCPErrorResult("environment_name is required"), nil } ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{Name: name}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("getting environment values: %s", err)), nil + return azdext.MCPErrorResult("getting environment values: %s", err), nil } kvMap := make(map[string]string, len(resp.KeyValues)) @@ -120,42 +94,34 @@ func registerEnvironmentTools(s *server.MCPServer) { kvMap[kv.Key] = kv.Value } - data, err := json.MarshalIndent(kvMap, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(kvMap), nil + }, + azdext.MCPToolOptions{ + Description: "Get all key-value pairs for an azd environment", + ReadOnly: true, }, + mcp.WithString("environment_name", mcp.Required(), mcp.Description("Name of the environment")), ) // Tool: set_environment_value - s.AddTool( - mcp.NewTool("set_environment_value", - mcp.WithDescription("Set a key-value pair in an azd environment"), - mcp.WithString("environment_name", mcp.Required(), mcp.Description("Name of the environment")), - mcp.WithString("key", mcp.Required(), mcp.Description("Key to set")), - mcp.WithString("value", mcp.Required(), mcp.Description("Value to set")), - mcp.WithDestructiveHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("set_environment_value"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - args, ok := req.Params.Arguments.(map[string]interface{}) - if !ok { - return mcp.NewToolResultError("invalid arguments"), nil - } - envName, _ := args["environment_name"].(string) - key, _ := args["key"].(string) - value, _ := args["value"].(string) - if envName == "" || key == "" { - return mcp.NewToolResultError("environment_name and key are required"), nil + builder.AddTool("set_environment_value", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { + envName, err := args.RequireString("environment_name") + if err != nil { + return azdext.MCPErrorResult("environment_name is required"), nil + } + key, err := args.RequireString("key") + if err != nil { + return azdext.MCPErrorResult("key is required"), nil + } + value, err := args.RequireString("value") + if err != nil { + return azdext.MCPErrorResult("value is required"), nil } ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() @@ -165,40 +131,39 @@ func registerEnvironmentTools(s *server.MCPServer) { Value: value, }) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("setting environment value: %s", err)), nil + return azdext.MCPErrorResult("setting environment value: %s", err), nil } return mcp.NewToolResultText(fmt.Sprintf("Successfully set %s in environment %s", key, envName)), nil }, + azdext.MCPToolOptions{ + Description: "Set a key-value pair in an azd environment", + Destructive: true, + }, + mcp.WithString("environment_name", mcp.Required(), mcp.Description("Name of the environment")), + mcp.WithString("key", mcp.Required(), mcp.Description("Key to set")), + mcp.WithString("value", mcp.Required(), mcp.Description("Value to set")), ) } -func registerDeploymentTools(s *server.MCPServer) { +func registerDeploymentTools(builder *azdext.MCPServerBuilder) { // Tool: get_deployment_info - s.AddTool( - mcp.NewTool("get_deployment_info", - mcp.WithDescription("Get the latest Azure deployment info including ID, location, outputs, and resources"), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("get_deployment_info"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - + builder.AddTool("get_deployment_info", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Deployment().GetDeployment(ctx, &azdext.EmptyRequest{}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("getting deployment: %s", err)), nil + return azdext.MCPErrorResult("getting deployment: %s", err), nil } d := resp.Deployment if d == nil { - return mcp.NewToolResultError("no deployment found"), nil + return azdext.MCPErrorResult("no deployment found"), nil } info := map[string]interface{}{ "id": d.Id, @@ -211,34 +176,26 @@ func registerDeploymentTools(s *server.MCPServer) { "resources": d.Resources, } - data, err := json.MarshalIndent(info, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(info), nil + }, + azdext.MCPToolOptions{ + Description: "Get the latest Azure deployment info including ID, location, outputs, and resources", + ReadOnly: true, }, ) // Tool: get_deployment_context - s.AddTool( - mcp.NewTool("get_deployment_context", - mcp.WithDescription("Get current Azure deployment context including subscription, tenant, location, resource group, and resources"), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("get_deployment_context"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - + builder.AddTool("get_deployment_context", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Deployment().GetDeploymentContext(ctx, &azdext.EmptyRequest{}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("getting deployment context: %s", err)), nil + return azdext.MCPErrorResult("getting deployment context: %s", err), nil } info := map[string]interface{}{} @@ -252,36 +209,28 @@ func registerDeploymentTools(s *server.MCPServer) { } } - data, err := json.MarshalIndent(info, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(info), nil + }, + azdext.MCPToolOptions{ + Description: "Get current Azure deployment context including subscription, tenant, location, resource group, and resources", + ReadOnly: true, }, ) } -func registerAccountTools(s *server.MCPServer) { +func registerAccountTools(builder *azdext.MCPServerBuilder) { // Tool: list_subscriptions - s.AddTool( - mcp.NewTool("list_subscriptions", - mcp.WithDescription("List Azure subscriptions accessible to the current account"), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("list_subscriptions"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - + builder.AddTool("list_subscriptions", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Account().ListSubscriptions(ctx, &azdext.ListSubscriptionsRequest{}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("listing subscriptions: %s", err)), nil + return azdext.MCPErrorResult("listing subscriptions: %s", err), nil } type subInfo struct { @@ -301,54 +250,41 @@ func registerAccountTools(s *server.MCPServer) { }) } - data, err := json.MarshalIndent(subs, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(subs), nil + }, + azdext.MCPToolOptions{ + Description: "List Azure subscriptions accessible to the current account", + ReadOnly: true, }, ) } -func registerWorkflowTools(s *server.MCPServer) { +func registerWorkflowTools(builder *azdext.MCPServerBuilder) { // Tool: run_workflow - s.AddTool( - mcp.NewTool("run_workflow", - mcp.WithDescription("Execute an azd workflow with the given name and steps"), - mcp.WithString("workflow_name", mcp.Required(), mcp.Description("Name of the workflow to run")), - mcp.WithArray("steps", mcp.Required(), mcp.Description("Array of step objects, each with an 'args' array of command arguments")), - mcp.WithDestructiveHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("run_workflow"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - args, ok := req.Params.Arguments.(map[string]interface{}) - if !ok { - return mcp.NewToolResultError("invalid arguments"), nil - } - workflowName, _ := args["workflow_name"].(string) - if workflowName == "" { - return mcp.NewToolResultError("workflow_name is required"), nil + builder.AddTool("run_workflow", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { + workflowName, err := args.RequireString("workflow_name") + if err != nil { + return azdext.MCPErrorResult("workflow_name is required"), nil } - stepsRaw, ok := args["steps"].([]interface{}) + raw := args.Raw() + stepsRaw, ok := raw["steps"].([]interface{}) if !ok || len(stepsRaw) == 0 { - return mcp.NewToolResultError("steps array is required and must not be empty"), nil + return azdext.MCPErrorResult("steps array is required and must not be empty"), nil } var steps []*azdext.WorkflowStep for i, stepRaw := range stepsRaw { stepMap, ok := stepRaw.(map[string]interface{}) if !ok { - return mcp.NewToolResultError(fmt.Sprintf("step %d is not a valid object", i)), nil + return azdext.MCPErrorResult("step %d is not a valid object", i), nil } argsRaw, _ := stepMap["args"].([]interface{}) cmdArgs := make([]string, 0, len(argsRaw)) for j, a := range argsRaw { if a == nil { - return mcp.NewToolResultError(fmt.Sprintf("step %d arg %d: null values not allowed", i, j)), nil + return azdext.MCPErrorResult("step %d arg %d: null values not allowed", i, j), nil } switch v := a.(type) { case string: @@ -358,7 +294,7 @@ func registerWorkflowTools(s *server.MCPServer) { } } if len(cmdArgs) == 0 { - return mcp.NewToolResultError(fmt.Sprintf("step %d has no command arguments", i)), nil + return azdext.MCPErrorResult("step %d has no command arguments", i), nil } steps = append(steps, &azdext.WorkflowStep{ Command: &azdext.WorkflowCommand{Args: cmdArgs}, @@ -366,12 +302,12 @@ func registerWorkflowTools(s *server.MCPServer) { } if len(steps) == 0 { - return mcp.NewToolResultError("no valid workflow steps found"), nil + return azdext.MCPErrorResult("no valid workflow steps found"), nil } ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() @@ -382,35 +318,33 @@ func registerWorkflowTools(s *server.MCPServer) { }, }) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("running workflow: %s", err)), nil + return azdext.MCPErrorResult("running workflow: %s", err), nil } return mcp.NewToolResultText(fmt.Sprintf("Workflow '%s' completed successfully", workflowName)), nil }, + azdext.MCPToolOptions{ + Description: "Execute an azd workflow with the given name and steps", + Destructive: true, + }, + mcp.WithString("workflow_name", mcp.Required(), mcp.Description("Name of the workflow to run")), + mcp.WithArray("steps", mcp.Required(), mcp.Description("Array of step objects, each with an 'args' array of command arguments")), ) } -func registerComposeTools(s *server.MCPServer) { +func registerComposeTools(builder *azdext.MCPServerBuilder) { // Tool: list_compose_resources - s.AddTool( - mcp.NewTool("list_compose_resources", - mcp.WithDescription("List composability resources defined in the azd project"), - mcp.WithReadOnlyHintAnnotation(true), - ), - func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if err := grpcRateLimiter.CheckRateLimit("list_compose_resources"); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - + builder.AddTool("list_compose_resources", + func(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolResult, error) { ctx, client, err := newAzdClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return azdext.MCPErrorResult("%s", err), nil } defer client.Close() resp, err := client.Compose().ListResources(ctx, &azdext.EmptyRequest{}) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("listing compose resources: %s", err)), nil + return azdext.MCPErrorResult("listing compose resources: %s", err), nil } type resourceInfo struct { @@ -430,11 +364,11 @@ func registerComposeTools(s *server.MCPServer) { }) } - data, err := json.MarshalIndent(resources, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("marshaling response: %s", err)), nil - } - return mcp.NewToolResultText(string(data)), nil + return azdext.MCPJSONResult(resources), nil + }, + azdext.MCPToolOptions{ + Description: "List composability resources defined in the azd project", + ReadOnly: true, }, ) } diff --git a/cli/src/cmd/copilot/commands/metadata.go b/cli/src/cmd/copilot/commands/metadata.go index a1922c4..2585cdb 100644 --- a/cli/src/cmd/copilot/commands/metadata.go +++ b/cli/src/cmd/copilot/commands/metadata.go @@ -4,28 +4,11 @@ package commands import ( - "encoding/json" - "fmt" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) // NewMetadataCommand creates a metadata command using the official azdext SDK. -func NewMetadataCommand(rootCmdProvider func() *cobra.Command) *cobra.Command { - return &cobra.Command{ - Use: "metadata", - Short: "Output extension metadata for IntelliSense", - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - root := rootCmdProvider() - metadata := azdext.GenerateExtensionMetadata("1.0", "jongio.azd.copilot", root) - data, err := json.MarshalIndent(metadata, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal metadata: %w", err) - } - _, err = fmt.Fprintln(cmd.OutOrStdout(), string(data)) - return err - }, - } +func NewMetadataCommand(schemaVersion, extensionId string, rootCmdProvider func() *cobra.Command) *cobra.Command { + return azdext.NewMetadataCommand(schemaVersion, extensionId, rootCmdProvider) } diff --git a/cli/src/cmd/copilot/commands/version.go b/cli/src/cmd/copilot/commands/version.go index f15b728..2319df2 100644 --- a/cli/src/cmd/copilot/commands/version.go +++ b/cli/src/cmd/copilot/commands/version.go @@ -1,6 +1,7 @@ package commands import ( + "github.com/azure/azure-dev/cli/azd/pkg/azdext" coreversion "github.com/jongio/azd-core/version" "github.com/spf13/cobra" ) @@ -24,6 +25,6 @@ func init() { } // NewVersionCommand creates the version command. -func NewVersionCommand(outputFormat *string) *cobra.Command { - return coreversion.NewCommand(VersionInfo, outputFormat) +func NewVersionCommand(extensionId, version string, outputFormat *string) *cobra.Command { + return azdext.NewVersionCommand(extensionId, version, outputFormat) } diff --git a/cli/src/cmd/copilot/commands/version_test.go b/cli/src/cmd/copilot/commands/version_test.go index e7bd59b..81d7546 100644 --- a/cli/src/cmd/copilot/commands/version_test.go +++ b/cli/src/cmd/copilot/commands/version_test.go @@ -38,7 +38,7 @@ func TestVersionInfo(t *testing.T) { func TestNewVersionCommand(t *testing.T) { outputFormat := "default" - cmd := NewVersionCommand(&outputFormat) + cmd := NewVersionCommand("jongio.azd.copilot", Version, &outputFormat) if cmd == nil { t.Fatal("NewVersionCommand() returned nil") @@ -49,16 +49,11 @@ func TestNewVersionCommand(t *testing.T) { if cmd.Short == "" { t.Error("cmd.Short should not be empty") } - - flag := cmd.Flags().Lookup("quiet") - if flag == nil { - t.Error("--quiet flag should exist") - } } func TestVersionCommand_DefaultOutput(t *testing.T) { outputFormat := "default" - cmd := NewVersionCommand(&outputFormat) + cmd := NewVersionCommand("jongio.azd.copilot", Version, &outputFormat) var stdout bytes.Buffer cmd.SetOut(&stdout) diff --git a/cli/src/cmd/copilot/main.go b/cli/src/cmd/copilot/main.go index 7fcdba1..b431fca 100644 --- a/cli/src/cmd/copilot/main.go +++ b/cli/src/cmd/copilot/main.go @@ -1,12 +1,12 @@ package main import ( - "context" "fmt" "log/slog" "os" "strings" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/jongio/azd-copilot/cli/src/cmd/copilot/commands" "github.com/jongio/azd-copilot/cli/src/internal/assets" "github.com/jongio/azd-copilot/cli/src/internal/copilot" @@ -16,14 +16,10 @@ import ( "github.com/common-nighthawk/go-figure" "github.com/spf13/cobra" - "go.opentelemetry.io/otel/propagation" ) var ( - outputFormat string - debugMode bool structuredLogs bool - cwdFlag string // Root command flags for copilot session prompt string @@ -35,6 +31,9 @@ var ( verbose bool noBanner bool forceColor bool + + // SDK extension context + extCtx *azdext.ExtensionContext ) func main() { @@ -50,14 +49,18 @@ func main() { } func newRootCmd() *cobra.Command { - rootCmd := &cobra.Command{ - Use: "copilot", - Short: "Azure Copilot CLI - AI-powered Azure development assistant", + rootCmd, ec := azdext.NewExtensionRootCommand(azdext.ExtensionCommandOptions{ + Name: "copilot", + Version: commands.Version, + Short: "Azure Copilot CLI - AI-powered Azure development assistant", Long: fmt.Sprintf(`Azure Copilot CLI is an Azure Developer CLI extension that integrates GitHub Copilot CLI with %d specialized Azure agents and %d focused skills for Azure development. When run without subcommands, starts an interactive Copilot session with Azure context.`, assets.AgentCount(), assets.SkillCount()), - Example: ` # Start interactive session + }) + extCtx = ec + + rootCmd.Example = ` # Start interactive session azd copilot # Start with a specific prompt @@ -70,75 +73,60 @@ When run without subcommands, starts an interactive Copilot session with Azure c azd copilot --agent azure-architect # Auto-approve mode (careful!) - azd copilot --yolo`, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Inject OTel trace context from env vars while preserving cobra's signal handling - ctx := cmd.Context() - if ctx == nil { - ctx = context.Background() - } - if parent := os.Getenv("TRACEPARENT"); parent != "" { - tc := propagation.TraceContext{} - ctx = tc.Extract(ctx, propagation.MapCarrier{ - "traceparent": parent, - "tracestate": os.Getenv("TRACESTATE"), - }) - } - cmd.SetContext(ctx) - - // Change working directory if --cwd is specified - if cwdFlag != "" { - if err := os.Chdir(cwdFlag); err != nil { - return fmt.Errorf("failed to change to directory '%s': %w", cwdFlag, err) - } + azd copilot --yolo` + + // Chain extension-specific PersistentPreRunE after the SDK's + sdkPreRunE := rootCmd.PersistentPreRunE + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if sdkPreRunE != nil { + if err := sdkPreRunE(cmd, args); err != nil { + return err } + } - // Handle force color - if forceColor { - cliout.ForceColor() - _ = os.Setenv("FORCE_COLOR", "1") - } + // Handle force color + if forceColor { + cliout.ForceColor() + _ = os.Setenv("FORCE_COLOR", "1") + } - // Set global output format and debug mode - if debugMode { - _ = os.Setenv("AZD_DEBUG", "true") - _ = os.Setenv("AZD_COPILOT_DEBUG", "true") - slog.SetLogLoggerLevel(slog.LevelDebug) - } + // Set global output format and debug mode + if extCtx.Debug { + _ = os.Setenv("AZD_DEBUG", "true") + _ = os.Setenv("AZD_COPILOT_DEBUG", "true") + slog.SetLogLoggerLevel(slog.LevelDebug) + } - // Configure logging - logutil.SetupLogger(debugMode, structuredLogs) + // Configure logging + logutil.SetupLogger(extCtx.Debug, structuredLogs) - // Install azd-copilot self-skill - if err := selfskills.InstallSkill(); err != nil { - if debugMode { - slog.Debug("Failed to install copilot self-skill", "error", err) - } + // Install azd-copilot self-skill + if err := selfskills.InstallSkill(); err != nil { + if extCtx.Debug { + slog.Debug("Failed to install copilot self-skill", "error", err) } + } - // Log startup in debug mode - if debugMode { - logutil.Debug("Starting azd copilot extension", - "version", commands.Version, - "command", cmd.Name(), - "args", args, - "cwd", cwdFlag, - ) - } + // Log startup in debug mode + if extCtx.Debug { + logutil.Debug("Starting azd copilot extension", + "version", commands.Version, + "command", cmd.Name(), + "args", args, + "cwd", extCtx.Cwd, + ) + } + + return nil + } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - // Default behavior: start interactive Copilot session - return runCopilotSession(cmd) - }, + rootCmd.RunE = func(cmd *cobra.Command, args []string) error { + // Default behavior: start interactive Copilot session + return runCopilotSession(cmd) } - // Add global flags - rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "default", "Output format (default, json)") - rootCmd.PersistentFlags().BoolVar(&debugMode, "debug", false, "Enable debug logging") + // Add extension-specific persistent flags rootCmd.PersistentFlags().BoolVar(&structuredLogs, "structured-logs", false, "Enable structured JSON logging to stderr") - rootCmd.PersistentFlags().StringVarP(&cwdFlag, "cwd", "C", "", "Sets the current working directory") rootCmd.PersistentFlags().BoolVar(&forceColor, "color", false, "Force colored output") // Add root command flags for copilot session @@ -153,7 +141,7 @@ When run without subcommands, starts an interactive Copilot session with Azure c // Register all commands rootCmd.AddCommand( - commands.NewVersionCommand(&outputFormat), + commands.NewVersionCommand("jongio.azd.copilot", commands.Version, &extCtx.OutputFormat), commands.NewListenCommand(), commands.NewAgentsCommand(), commands.NewSkillsCommand(), @@ -163,7 +151,7 @@ When run without subcommands, starts an interactive Copilot session with Azure c commands.NewBuildCommand(), commands.NewSpecCommand(), commands.NewMCPCommand(), - commands.NewMetadataCommand(newRootCmd), + commands.NewMetadataCommand("1.0", "jongio.azd.copilot", newRootCmd), // Quick actions commands.NewInitCommand(), commands.NewReviewCommand(), @@ -194,7 +182,7 @@ func runCopilotSession(cmd *cobra.Command) error { // Configure MCP servers for Copilot CLI if err := copilot.ConfigureMCPServer(); err != nil { - if debugMode { + if extCtx.Debug { fmt.Fprintf(os.Stderr, "Warning: failed to configure MCP servers: %v\n", err) } } @@ -202,7 +190,7 @@ func runCopilotSession(cmd *cobra.Command) error { // Install agents and skills to ~/.azd/copilot/ assetDirs, err := setupAgentsAndSkills() if err != nil { - if debugMode { + if extCtx.Debug { fmt.Fprintf(os.Stderr, "Warning: failed to install agents/skills: %v\n", err) } } @@ -219,7 +207,7 @@ func runCopilotSession(cmd *cobra.Command) error { Model: model, AddDirs: append(addDirs, assetDirs...), Verbose: verbose, - Debug: debugMode, + Debug: extCtx.Debug, ProjectContext: projectContext, }) }