diff --git a/cli/extension.yaml b/cli/extension.yaml index 9bcb0fe..290c7d0 100644 --- a/cli/extension.yaml +++ b/cli/extension.yaml @@ -7,6 +7,14 @@ version: 0.3.7 language: go capabilities: - custom-commands + - lifecycle-events + - mcp-server + - metadata +mcp: + serve: + args: ["mcp", "serve"] + env: + - "AZD_EXEC_PROJECT_DIR=${AZD_PROJECT_DIR}" examples: - name: execute script description: Execute a script file with azd context diff --git a/cli/go.mod b/cli/go.mod index c8b7b68..2e83c0e 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,26 +3,91 @@ module github.com/jongio/azd-exec/cli go 1.26.0 require ( - github.com/jongio/azd-core v0.5.1 + github.com/azure/azure-dev/cli/azd v0.0.0-20260221052936-16626caf33f0 + github.com/jongio/azd-core v0.5.2 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.38.0 ) 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/azcore v1.21.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.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 + github.com/braydonk/yaml v0.9.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/charmbracelet/colorprofile v0.3.2 // indirect + github.com/charmbracelet/glamour v0.10.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.10.2 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + 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/fatih/color v1.18.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golobby/container/v3 v3.3.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jmespath-community/go-jmespath v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/microsoft/ApplicationInsights-Go v0.4.4 // indirect + github.com/microsoft/go-deviceid v1.0.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + 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/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 + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.47.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index a9fd953..717b94d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,3 +1,8 @@ +code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -14,45 +19,275 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +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= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +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-20260221052936-16626caf33f0 h1:4rX+Dt8Dk2nMTEx5TOGnbRFwbCN9p7nkg5ITYU9YvQ0= +github.com/azure/azure-dev/cli/azd v0.0.0-20260221052936-16626caf33f0/go.mod h1:PpDDtoX9qleMXzyDEblT5DeOWoJ/gvLsvIJ4z2fKpOE= +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= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/braydonk/yaml v0.9.0 h1:ewGMrVmEVpsm3VwXQDR388sLg5+aQ8Yihp6/hc4m+h4= +github.com/braydonk/yaml v0.9.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4= +github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= +github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= +github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489 h1:a5q2sWiet6kgqucSGjYN1jhT2cn4bMKUwprtm2IGRto= +github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +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/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= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw= +github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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= -github.com/jongio/azd-core v0.5.1 h1:xrAWyRIjZFVF0EOTgwjEbcMzU8wpvI1xvp6pqiDhHxU= -github.com/jongio/azd-core v0.5.1/go.mod h1:jQCP+px3Pxb3B0fyShfvSVa3KsWT1j2jGXMsPpQezlI= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= +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/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/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +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= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= +github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= +github.com/microsoft/go-deviceid v1.0.0 h1:i5AQ654Xk9kfvwJeKQm3w2+eT1+ImBDVEpAR0AjpP40= +github.com/microsoft/go-deviceid v1.0.0/go.mod h1:KY13FeVdHkzD8gy+6T8+kVmD/7RMpTaWW75K+T4uZWg= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d h1:NqRhLdNVlozULwM1B3VaHhcXYSgrOAv8V5BE65om+1Q= +github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= +github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= +github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +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.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +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/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +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.1.0/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/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-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/src/cmd/exec/commands/commands_test.go b/cli/src/cmd/exec/commands/commands_test.go index 9492881..5a41a12 100644 --- a/cli/src/cmd/exec/commands/commands_test.go +++ b/cli/src/cmd/exec/commands/commands_test.go @@ -81,9 +81,10 @@ func TestNewListenCommand(t *testing.T) { func TestListenCommandExecution(t *testing.T) { cmd := NewListenCommand() - // Listen command should execute without error (it's a placeholder) + // Listen command requires a running azd server for gRPC communication. + // In unit tests without azd, it should fail with a connection error. err := cmd.Execute() - if err != nil { - t.Errorf("Listen command should not error, got: %v", err) + if err == nil { + t.Error("Listen command should error without azd server running") } } diff --git a/cli/src/cmd/exec/commands/listen.go b/cli/src/cmd/exec/commands/listen.go index 1bbac55..2eec69e 100644 --- a/cli/src/cmd/exec/commands/listen.go +++ b/cli/src/cmd/exec/commands/listen.go @@ -2,27 +2,34 @@ package commands import ( - "github.com/jongio/azd-core/cliout" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) -// NewListenCommand creates a new listen command for the azd extension framework. -// This command is required by the azd extension framework for inter-process communication. -// It is marked as hidden since it's an internal implementation detail not meant for direct use. -// The extension currently operates in "exec mode" without a persistent listener, -// so this command is a no-op placeholder for framework compatibility. +// NewListenCommand creates the listen command that starts the azd extension host. +// This command is invoked by azd to establish lifecycle event communication via gRPC. func NewListenCommand() *cobra.Command { return &cobra.Command{ Use: "listen", Short: "Start extension listener (internal use only)", - Long: `Start the extension listener for the azd extension framework. This command is used internally by azd and should not be called directly.`, Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { - // This is a placeholder for the extension framework's listen functionality. - // In a full implementation, this would start a gRPC server for azd communication. - // For now, this extension operates in "exec mode" without persistent listener, - // so returning nil is appropriate. - cliout.Info("Listen command is not implemented - azd-exec operates in exec mode") + ctx := azdext.WithAccessToken(cmd.Context()) + + azdClient, err := azdext.NewAzdClient() + if err != nil { + return fmt.Errorf("failed to create azd client: %w", err) + } + defer azdClient.Close() + + host := azdext.NewExtensionHost(azdClient) + + if err := host.Run(ctx); err != nil { + return fmt.Errorf("failed to run extension: %w", err) + } + return nil }, } diff --git a/cli/src/cmd/exec/commands/mcp.go b/cli/src/cmd/exec/commands/mcp.go new file mode 100644 index 0000000..4beba28 --- /dev/null +++ b/cli/src/cmd/exec/commands/mcp.go @@ -0,0 +1,437 @@ +package commands + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/jongio/azd-core/azdextutil" + "github.com/jongio/azd-core/security" + "github.com/jongio/azd-core/shellutil" + "github.com/jongio/azd-exec/cli/src/internal/version" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/spf13/cobra" +) + +const defaultTimeout = 30 * time.Second + +// NewMCPCommand creates the mcp parent command. +func NewMCPCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "mcp", + Short: "Model Context Protocol server operations", + Long: `Manage the Model Context Protocol (MCP) server for the azd exec extension.`, + Hidden: true, + } + cmd.AddCommand(newMCPServeCommand()) + return cmd +} + +func newMCPServeCommand() *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Start the MCP server", + Long: `Starts the Model Context Protocol server using stdio transport.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runMCPServer(cmd.Context()) + }, + } +} + +func runMCPServer(_ context.Context) error { + instructions := `This MCP server is provided by the azd exec extension for the Azure Developer CLI. + +**Extension Role:** +Execute scripts and commands with full Azure Developer CLI context, including environment +variables and Azure Key Vault secret resolution. + +**Tool Categories:** +- Execution: exec_script, exec_inline - Run scripts/commands with azd environment context +- Discovery: list_shells - Discover available shells on the system +- Configuration: get_environment - View current azd environment variables + +**Best Practices:** +- Always verify script paths before execution +- Use list_shells to discover available shells before specifying one +- Use get_environment to check available environment variables +- Prefer exec_script for file-based scripts, exec_inline for one-liners +- Be cautious with destructive operations; review commands before executing` + + s := server.NewMCPServer( + "exec-mcp-server", + version.Version, + server.WithToolCapabilities(true), + server.WithInstructions(instructions), + ) + + s.AddTools( + newExecScriptTool(), + newExecInlineTool(), + newListShellsTool(), + newGetEnvironmentTool(), + ) + + if err := server.ServeStdio(s); err != nil { + fmt.Fprintf(os.Stderr, "MCP server error: %v\n", err) + return err + } + return nil +} + +// --- exec_script tool --- + +func newExecScriptTool() server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool( + "exec_script", + mcp.WithTitleAnnotation("Execute Script File"), + mcp.WithDescription("Execute a script file with azd environment context and Key Vault integration. "+ + "The script runs with all azd environment variables available, including resolved Key Vault secrets."), + mcp.WithReadOnlyHintAnnotation(false), + mcp.WithDestructiveHintAnnotation(true), + mcp.WithString("script_path", + mcp.Description("Path to the script file to execute. Must be an existing file within the project directory."), + mcp.Required(), + ), + mcp.WithString("shell", + mcp.Description("Shell to use for execution (bash, sh, zsh, pwsh, powershell, cmd). Auto-detected from file extension if not specified."), + ), + mcp.WithString("args", + mcp.Description("Space-separated arguments to pass to the script."), + ), + ), + Handler: handleExecScript, + } +} + +func handleExecScript(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if !globalRateLimiter.Allow() { + return mcp.NewToolResultError("Rate limit exceeded. Please wait before making more requests."), nil + } + + args := getArgsMap(request) + + scriptPath, ok := getStringParam(args, "script_path") + if !ok || scriptPath == "" { + return mcp.NewToolResultError("script_path is required"), nil + } + + shell, _ := getStringParam(args, "shell") + if shell != "" { + if err := azdextutil.ValidateShellName(shell); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid shell: %v", err)), nil + } + } + + // Validate script path for security + projectDir, err := azdextutil.GetProjectDir("AZD_EXEC_PROJECT_DIR") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to determine project directory: %v", err)), nil + } + + validPath, err := security.ValidatePathWithinBases(scriptPath, projectDir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid script path: %v", err)), nil + } + + info, statErr := os.Stat(validPath) + if statErr != nil { + return mcp.NewToolResultError(fmt.Sprintf("Script file not found: %s", scriptPath)), nil + } + if info.IsDir() { + return mcp.NewToolResultError("script_path must be a file, not a directory"), nil + } + + // Parse extra args + var scriptArgs []string + if argsStr, ok := getStringParam(args, "args"); ok && argsStr != "" { + scriptArgs = strings.Fields(argsStr) + } + + // Detect shell + if shell == "" { + shell = shellutil.DetectShell(validPath) + } + + // Build and execute command + timeout := defaultTimeout + execCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + cmdArgs := buildShellArgs(shell, validPath, false, scriptArgs) + cmd := exec.CommandContext(execCtx, cmdArgs[0], cmdArgs[1:]...) + cmd.Env = os.Environ() + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + runErr := cmd.Run() + return marshalExecResult(stdout.String(), stderr.String(), cmd.ProcessState, runErr) +} + +// --- exec_inline tool --- + +func newExecInlineTool() server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool( + "exec_inline", + mcp.WithTitleAnnotation("Execute Inline Command"), + mcp.WithDescription("Execute an inline command with azd environment context. "+ + "The command runs with all azd environment variables, including resolved Key Vault secrets."), + mcp.WithReadOnlyHintAnnotation(false), + mcp.WithDestructiveHintAnnotation(true), + mcp.WithString("command", + mcp.Description("The command to execute inline."), + mcp.Required(), + ), + mcp.WithString("shell", + mcp.Description("Shell to use (bash, sh, zsh, pwsh, powershell, cmd). Defaults to bash on Unix, powershell on Windows."), + ), + ), + Handler: handleExecInline, + } +} + +func handleExecInline(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if !globalRateLimiter.Allow() { + return mcp.NewToolResultError("Rate limit exceeded. Please wait before making more requests."), nil + } + + args := getArgsMap(request) + + command, ok := getStringParam(args, "command") + if !ok || strings.TrimSpace(command) == "" { + return mcp.NewToolResultError("command is required and cannot be empty"), nil + } + + shell, _ := getStringParam(args, "shell") + if shell != "" { + if err := azdextutil.ValidateShellName(shell); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid shell: %v", err)), nil + } + } + if shell == "" { + if runtime.GOOS == "windows" { + shell = shellutil.ShellPowerShell + } else { + shell = shellutil.ShellBash + } + } + + timeout := defaultTimeout + execCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + cmdArgs := buildShellArgs(shell, command, true, nil) + cmd := exec.CommandContext(execCtx, cmdArgs[0], cmdArgs[1:]...) + cmd.Env = os.Environ() + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + runErr := cmd.Run() + return marshalExecResult(stdout.String(), stderr.String(), cmd.ProcessState, runErr) +} + +// --- list_shells tool --- + +type shellInfo struct { + Name string `json:"name"` + Available bool `json:"available"` +} + +func newListShellsTool() server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool( + "list_shells", + mcp.WithTitleAnnotation("List Available Shells"), + mcp.WithDescription("List shells available on the system for script execution."), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + ), + Handler: handleListShells, + } +} + +func handleListShells(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if !globalRateLimiter.Allow() { + return mcp.NewToolResultError("Rate limit exceeded. Please wait before making more requests."), nil + } + + shells := []string{ + shellutil.ShellBash, + shellutil.ShellSh, + shellutil.ShellZsh, + shellutil.ShellPwsh, + shellutil.ShellPowerShell, + shellutil.ShellCmd, + } + + var results []shellInfo + for _, sh := range shells { + _, err := exec.LookPath(sh) + results = append(results, shellInfo{ + Name: sh, + Available: err == nil, + }) + } + + return marshalToolResult(results) +} + +// --- get_environment tool --- + +type envVar struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func newGetEnvironmentTool() server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool( + "get_environment", + mcp.WithTitleAnnotation("Get Environment Variables"), + mcp.WithDescription("Get current azd environment variables available for script execution."), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + ), + Handler: handleGetEnvironment, + } +} + +func handleGetEnvironment(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if !globalRateLimiter.Allow() { + return mcp.NewToolResultError("Rate limit exceeded. Please wait before making more requests."), nil + } + + allowedPrefixes := []string{"AZD_", "AZURE_", "ARM_", "DOTNET_", "NODE_", "PYTHON"} + + var vars []envVar + for _, e := range os.Environ() { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + name := parts[0] + key := strings.ToUpper(name) + matched := false + for _, prefix := range allowedPrefixes { + if strings.HasPrefix(key, prefix) { + matched = true + break + } + } + if !matched { + continue + } + + // Exclude known secret-bearing variable names + secretPatterns := []string{"SECRET", "PASSWORD", "KEY", "TOKEN", "CREDENTIAL", "CERTIFICATE", "CONNECTION_STRING", "CONNSTR"} + isSecret := false + upperName := strings.ToUpper(name) + for _, pattern := range secretPatterns { + if strings.Contains(upperName, pattern) { + isSecret = true + break + } + } + if isSecret { + continue + } + + vars = append(vars, envVar{Key: name, Value: parts[1]}) + } + } + + return marshalToolResult(vars) +} + +// --- Helpers --- + +func getArgsMap(request mcp.CallToolRequest) map[string]interface{} { + if request.Params.Arguments != nil { + if m, ok := request.Params.Arguments.(map[string]interface{}); ok { + return m + } + } + return map[string]interface{}{} +} + +func getStringParam(args map[string]interface{}, key string) (string, bool) { + val, ok := args[key] + if !ok { + return "", false + } + s, ok := val.(string) + return s, ok +} + +type execResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + ExitCode int `json:"exitCode"` + Error string `json:"error,omitempty"` +} + +func marshalExecResult(stdout, stderr string, ps *os.ProcessState, err error) (*mcp.CallToolResult, error) { + result := execResult{ + Stdout: stdout, + Stderr: stderr, + } + if ps != nil { + result.ExitCode = ps.ExitCode() + } + if err != nil { + result.Error = err.Error() + if result.ExitCode == 0 { + result.ExitCode = -1 + } + } + return marshalToolResult(result) +} + +func marshalToolResult(data interface{}) (*mcp.CallToolResult, error) { + jsonBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal result: %v", err)), nil + } + return mcp.NewToolResultText(string(jsonBytes)), nil +} + +// buildShellArgs constructs command arguments for the given shell. +func buildShellArgs(shell, scriptOrCmd string, isInline bool, extraArgs []string) []string { + shellLower := strings.ToLower(shell) + switch shellLower { + case "cmd": + if isInline { + return []string{"cmd", "/c", scriptOrCmd} + } + args := []string{"cmd", "/c", scriptOrCmd} + args = append(args, extraArgs...) + return args + case "powershell", "pwsh": + if isInline { + return []string{shellLower, "-NoProfile", "-Command", scriptOrCmd} + } + args := []string{shellLower, "-NoProfile", "-File", scriptOrCmd} + args = append(args, extraArgs...) + return args + default: + // bash, sh, zsh + if isInline { + return []string{shellLower, "-c", scriptOrCmd} + } + args := []string{shellLower, scriptOrCmd} + args = append(args, extraArgs...) + return args + } +} diff --git a/cli/src/cmd/exec/commands/mcp_ratelimit.go b/cli/src/cmd/exec/commands/mcp_ratelimit.go new file mode 100644 index 0000000..c2c69e9 --- /dev/null +++ b/cli/src/cmd/exec/commands/mcp_ratelimit.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/jongio/azd-core/azdextutil" + +// globalRateLimiter uses the shared azdextutil token bucket. +// 10 burst tokens, refills at 1 token/second (≈60/min). +var globalRateLimiter = azdextutil.NewRateLimiter(10, 1.0) diff --git a/cli/src/cmd/exec/commands/mcp_test.go b/cli/src/cmd/exec/commands/mcp_test.go new file mode 100644 index 0000000..dcc6faf --- /dev/null +++ b/cli/src/cmd/exec/commands/mcp_test.go @@ -0,0 +1,301 @@ +package commands + +import ( + "context" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/jongio/azd-core/azdextutil" + "github.com/mark3labs/mcp-go/mcp" +) + +// --------------------------------------------------------------------------- +// TestBuildShellArgs +// --------------------------------------------------------------------------- + +func TestBuildShellArgs(t *testing.T) { + tests := []struct { + name string + shell string + script string + isInline bool + extra []string + wantBin string + wantParts []string + }{ + // bash inline + { + name: "bash inline", shell: "bash", script: "echo hi", isInline: true, + wantBin: "bash", wantParts: []string{"bash", "-c", "echo hi"}, + }, + // bash file with extra args + { + name: "bash file", shell: "bash", script: "/tmp/run.sh", isInline: false, extra: []string{"--flag"}, + wantBin: "bash", wantParts: []string{"bash", "/tmp/run.sh", "--flag"}, + }, + // pwsh inline + { + name: "pwsh inline", shell: "pwsh", script: "Get-Date", isInline: true, + wantBin: "pwsh", wantParts: []string{"pwsh", "-NoProfile", "-Command", "Get-Date"}, + }, + // pwsh file + { + name: "pwsh file", shell: "pwsh", script: "run.ps1", isInline: false, extra: []string{"a", "b"}, + wantBin: "pwsh", wantParts: []string{"pwsh", "-NoProfile", "-File", "run.ps1", "a", "b"}, + }, + // powershell inline + { + name: "powershell inline", shell: "powershell", script: "Write-Host hello", isInline: true, + wantBin: "powershell", wantParts: []string{"powershell", "-NoProfile", "-Command", "Write-Host hello"}, + }, + // powershell file + { + name: "powershell file", shell: "powershell", script: "run.ps1", isInline: false, + wantBin: "powershell", wantParts: []string{"powershell", "-NoProfile", "-File", "run.ps1"}, + }, + // cmd inline + { + name: "cmd inline", shell: "cmd", script: "dir", isInline: true, + wantBin: "cmd", wantParts: []string{"cmd", "/c", "dir"}, + }, + // cmd file with args + { + name: "cmd file", shell: "cmd", script: "run.bat", isInline: false, extra: []string{"/v"}, + wantBin: "cmd", wantParts: []string{"cmd", "/c", "run.bat", "/v"}, + }, + // sh inline + { + name: "sh inline", shell: "sh", script: "ls", isInline: true, + wantBin: "sh", wantParts: []string{"sh", "-c", "ls"}, + }, + // zsh file + { + name: "zsh file", shell: "zsh", script: "run.zsh", isInline: false, extra: []string{"x"}, + wantBin: "zsh", wantParts: []string{"zsh", "run.zsh", "x"}, + }, + // case insensitivity: BASH → bash + { + name: "uppercase BASH", shell: "BASH", script: "echo hi", isInline: true, + wantBin: "bash", wantParts: []string{"bash", "-c", "echo hi"}, + }, + // mixed case: Pwsh → pwsh + { + name: "mixed case Pwsh", shell: "Pwsh", script: "Get-Date", isInline: true, + wantBin: "pwsh", wantParts: []string{"pwsh", "-NoProfile", "-Command", "Get-Date"}, + }, + // mixed case CMD → cmd + { + name: "mixed case CMD", shell: "CMD", script: "dir", isInline: true, + wantBin: "cmd", wantParts: []string{"cmd", "/c", "dir"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := buildShellArgs(tc.shell, tc.script, tc.isInline, tc.extra) + if got[0] != tc.wantBin { + t.Errorf("binary = %q, want %q", got[0], tc.wantBin) + } + if len(got) != len(tc.wantParts) { + t.Fatalf("args length = %d, want %d: %v", len(got), len(tc.wantParts), got) + } + for i := range tc.wantParts { + if got[i] != tc.wantParts[i] { + t.Errorf("arg[%d] = %q, want %q", i, got[i], tc.wantParts[i]) + } + } + }) + } +} + +// --------------------------------------------------------------------------- +// TestHandleGetEnvironment_SecretFiltering +// --------------------------------------------------------------------------- + +func TestHandleGetEnvironment_SecretFiltering(t *testing.T) { + // Save and restore environment + origEnv := os.Environ() + defer func() { + os.Clearenv() + for _, e := range origEnv { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + _ = os.Setenv(parts[0], parts[1]) + } + } + }() + + // Set a controlled environment + os.Clearenv() + t.Setenv("AZD_ENVIRONMENT_NAME", "myenv") + t.Setenv("AZURE_LOCATION", "eastus2") + t.Setenv("AZURE_CLIENT_SECRET", "supersecret") + t.Setenv("ARM_CLIENT_SECRET", "anothersecret") + t.Setenv("AZD_ACCESS_TOKEN", "tok123") + t.Setenv("AZURE_SUBSCRIPTION_PASSWORD", "pw") + t.Setenv("AZURE_TENANT_ID", "tid") + t.Setenv("NODE_ENV", "production") + t.Setenv("HOME", "/home/user") // not an allowed prefix + + // Reset rate limiter so we don't hit limits + saved := globalRateLimiter + globalRateLimiter = azdextutil.NewRateLimiter(100, 100) + defer func() { globalRateLimiter = saved }() + + result, err := handleGetEnvironment(context.Background(), mcp.CallToolRequest{}) + if err != nil { + t.Fatalf("handleGetEnvironment returned error: %v", err) + } + + // Parse the JSON text content + if len(result.Content) == 0 { + t.Fatal("result has no content") + } + textContent, ok := result.Content[0].(mcp.TextContent) + if !ok { + t.Fatalf("expected TextContent, got %T", result.Content[0]) + } + + var vars []envVar + if err := json.Unmarshal([]byte(textContent.Text), &vars); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + // Build lookup + found := map[string]string{} + for _, v := range vars { + found[v.Key] = v.Value + } + + // Safe vars must be present + for _, key := range []string{"AZD_ENVIRONMENT_NAME", "AZURE_LOCATION", "AZURE_TENANT_ID", "NODE_ENV"} { + if _, ok := found[key]; !ok { + t.Errorf("expected safe var %q to be present", key) + } + } + + // Secret vars must be excluded + for _, key := range []string{"AZURE_CLIENT_SECRET", "ARM_CLIENT_SECRET", "AZD_ACCESS_TOKEN", "AZURE_SUBSCRIPTION_PASSWORD"} { + if _, ok := found[key]; ok { + t.Errorf("secret var %q should have been filtered out", key) + } + } + + // Non-prefixed vars must be excluded + if _, ok := found["HOME"]; ok { + t.Error("non-prefixed var HOME should not be included") + } +} + +// --------------------------------------------------------------------------- +// TestParseTimeout +// --------------------------------------------------------------------------- + +func TestParseTimeout(t *testing.T) { + // The MCP handlers use a hardcoded defaultTimeout constant. + // Verify the default is 30 seconds as documented. + if defaultTimeout.Seconds() != 30 { + t.Errorf("defaultTimeout = %v, want 30s", defaultTimeout) + } +} + +// --------------------------------------------------------------------------- +// TestRateLimiting +// --------------------------------------------------------------------------- + +func TestRateLimiting(t *testing.T) { + // Create a limiter with only 2 burst tokens and 0 refill to test exhaustion. + rl := azdextutil.NewRateLimiter(2, 0) + + if !rl.Allow() { + t.Error("first Allow() should succeed") + } + if !rl.Allow() { + t.Error("second Allow() should succeed") + } + // Third call must be rejected (burst exhausted, no refill) + if rl.Allow() { + t.Error("third Allow() should be rejected after burst exhaustion") + } +} + +func TestRateLimiting_HandlersReject(t *testing.T) { + // Swap the global rate limiter with an exhausted one + saved := globalRateLimiter + globalRateLimiter = azdextutil.NewRateLimiter(0, 0) // zero tokens + defer func() { globalRateLimiter = saved }() + + handlers := []struct { + name string + fn func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) + }{ + {"handleGetEnvironment", handleGetEnvironment}, + {"handleListShells", handleListShells}, + } + + for _, h := range handlers { + t.Run(h.name, func(t *testing.T) { + result, err := h.fn(context.Background(), mcp.CallToolRequest{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + text, ok := result.Content[0].(mcp.TextContent) + if !ok { + t.Fatalf("expected TextContent, got %T", result.Content[0]) + } + if !strings.Contains(text.Text, "Rate limit exceeded") { + t.Errorf("expected rate limit error, got: %s", text.Text) + } + }) + } +} + +// --------------------------------------------------------------------------- +// TestGetArgsMap / TestGetStringParam helpers +// --------------------------------------------------------------------------- + +func TestGetArgsMap(t *testing.T) { + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "shell": "bash", + "count": 42, + } + + m := getArgsMap(req) + if m["shell"] != "bash" { + t.Errorf("expected shell=bash, got %v", m["shell"]) + } + + // nil Arguments returns empty map + req2 := mcp.CallToolRequest{} + m2 := getArgsMap(req2) + if len(m2) != 0 { + t.Errorf("expected empty map for nil arguments, got %v", m2) + } +} + +func TestGetStringParam(t *testing.T) { + args := map[string]interface{}{ + "name": "test", + "count": 42, + } + + val, ok := getStringParam(args, "name") + if !ok || val != "test" { + t.Errorf("expected (test, true), got (%q, %v)", val, ok) + } + + // non-string value + _, ok = getStringParam(args, "count") + if ok { + t.Error("expected false for non-string value") + } + + // missing key + _, ok = getStringParam(args, "missing") + if ok { + t.Error("expected false for missing key") + } +} diff --git a/cli/src/cmd/exec/commands/metadata.go b/cli/src/cmd/exec/commands/metadata.go new file mode 100644 index 0000000..c615e8e --- /dev/null +++ b/cli/src/cmd/exec/commands/metadata.go @@ -0,0 +1,30 @@ +// Package commands provides subcommands for the azd exec extension. +package commands + +import ( + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/spf13/cobra" +) + +// NewMetadataCommand creates a metadata command that generates extension metadata +// using the official azdext SDK metadata generator. +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.exec", 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 + }, + } +} diff --git a/cli/src/cmd/exec/main.go b/cli/src/cmd/exec/main.go index da60615..de1b344 100644 --- a/cli/src/cmd/exec/main.go +++ b/cli/src/cmd/exec/main.go @@ -15,6 +15,7 @@ import ( "github.com/jongio/azd-exec/cli/src/internal/executor" "github.com/jongio/azd-exec/cli/src/internal/skills" "github.com/spf13/cobra" + "go.opentelemetry.io/otel/propagation" ) var ( @@ -103,6 +104,20 @@ Examples: return exec.ExecuteInline(cmd.Context(), scriptInput) }, 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) + // Set output format from flag if outputFormat == "json" { if err := cliout.SetFormat("json"); err != nil { @@ -186,6 +201,8 @@ Examples: rootCmd.AddCommand( commands.NewVersionCommand(&outputFormat), commands.NewListenCommand(), + commands.NewMetadataCommand(newRootCmd), + commands.NewMCPCommand(), ) return rootCmd diff --git a/cspell.json b/cspell.json index a501edf..7c6b23f 100644 --- a/cspell.json +++ b/cspell.json @@ -4,6 +4,8 @@ "words": [ "akvs", "azd", + "azdext", + "azdextutil", "azcore", "azidentity", "azsecrets", @@ -62,6 +64,8 @@ "subtest", "tcpdump", "testall", + "traceparent", + "tracestate", "testcache", "testcoverage", "testhelpers",