From 59c7d8e4d974728051ae7df42fdb1e2275bbf53c Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 20 Nov 2025 08:22:14 +0000 Subject: [PATCH 01/10] Adding extension and grpc layer for services --- .../microsoft.azd.concurx/build.ps1 | 78 +++++++ .../extensions/microsoft.azd.concurx/build.sh | 66 ++++++ .../microsoft.azd.concurx/changelog.md | 3 + .../microsoft.azd.concurx/extension.yaml | 9 + .../extensions/microsoft.azd.concurx/go.mod | 74 ++++++ .../extensions/microsoft.azd.concurx/go.sum | 212 ++++++++++++++++++ .../internal/cmd/root.go | 28 +++ .../microsoft.azd.concurx/internal/cmd/up.go | 55 +++++ .../internal/cmd/version.go | 27 +++ .../extensions/microsoft.azd.concurx/main.go | 31 +++ cli/azd/grpc/proto/project.proto | 10 + .../internal/grpcserver/project_service.go | 64 ++++++ .../grpcserver/project_service_test.go | 15 +- cli/azd/pkg/azdext/project.pb.go | 100 +++++++-- cli/azd/pkg/azdext/project_grpc.pb.go | 46 +++- 15 files changed, 789 insertions(+), 29 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.concurx/build.ps1 create mode 100644 cli/azd/extensions/microsoft.azd.concurx/build.sh create mode 100644 cli/azd/extensions/microsoft.azd.concurx/changelog.md create mode 100644 cli/azd/extensions/microsoft.azd.concurx/extension.yaml create mode 100644 cli/azd/extensions/microsoft.azd.concurx/go.mod create mode 100644 cli/azd/extensions/microsoft.azd.concurx/go.sum create mode 100644 cli/azd/extensions/microsoft.azd.concurx/internal/cmd/root.go create mode 100644 cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go create mode 100644 cli/azd/extensions/microsoft.azd.concurx/internal/cmd/version.go create mode 100644 cli/azd/extensions/microsoft.azd.concurx/main.go diff --git a/cli/azd/extensions/microsoft.azd.concurx/build.ps1 b/cli/azd/extensions/microsoft.azd.concurx/build.ps1 new file mode 100644 index 00000000000..5ceb60a8bbc --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/build.ps1 @@ -0,0 +1,78 @@ +# Ensure script fails on any error +$ErrorActionPreference = 'Stop' + +# Get the directory of the script +$EXTENSION_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Change to the script directory +Set-Location -Path $EXTENSION_DIR + +# Create a safe version of EXTENSION_ID replacing dots with dashes +$EXTENSION_ID_SAFE = $env:EXTENSION_ID -replace '\.', '-' + +# Define output directory +$OUTPUT_DIR = if ($env:OUTPUT_DIR) { $env:OUTPUT_DIR } else { Join-Path $EXTENSION_DIR "bin" } + +# Create output directory if it doesn't exist +if (-not (Test-Path -Path $OUTPUT_DIR)) { + New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null +} + +# Get Git commit hash and build date +$COMMIT = git rev-parse HEAD +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to get git commit hash" + exit 1 +} +$BUILD_DATE = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ") + +# List of OS and architecture combinations +if ($env:EXTENSION_PLATFORM) { + $PLATFORMS = @($env:EXTENSION_PLATFORM) +} +else { + $PLATFORMS = @( + "windows/amd64", + "windows/arm64", + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64" + ) +} + +$APP_PATH = "$env:EXTENSION_ID/internal/cmd" + +# Loop through platforms and build +foreach ($PLATFORM in $PLATFORMS) { + $OS, $ARCH = $PLATFORM -split '/' + + $OUTPUT_NAME = Join-Path $OUTPUT_DIR "$EXTENSION_ID_SAFE-$OS-$ARCH" + + if ($OS -eq "windows") { + $OUTPUT_NAME += ".exe" + } + + Write-Host "Building for $OS/$ARCH..." + + # Delete the output file if it already exists + if (Test-Path -Path $OUTPUT_NAME) { + Remove-Item -Path $OUTPUT_NAME -Force + } + + # Set environment variables for Go build + $env:GOOS = $OS + $env:GOARCH = $ARCH + + go build ` + -ldflags="-X '$APP_PATH.Version=$env:EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" ` + -o $OUTPUT_NAME + + if ($LASTEXITCODE -ne 0) { + Write-Host "An error occurred while building for $OS/$ARCH" + exit 1 + } +} + +Write-Host "Build completed successfully!" +Write-Host "Binaries are located in the $OUTPUT_DIR directory." diff --git a/cli/azd/extensions/microsoft.azd.concurx/build.sh b/cli/azd/extensions/microsoft.azd.concurx/build.sh new file mode 100644 index 00000000000..f1a995ec5e9 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/build.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Get the directory of the script +EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Change to the script directory +cd "$EXTENSION_DIR" || exit + +# Create a safe version of EXTENSION_ID replacing dots with dashes +EXTENSION_ID_SAFE="${EXTENSION_ID//./-}" + +# Define output directory +OUTPUT_DIR="${OUTPUT_DIR:-$EXTENSION_DIR/bin}" + +# Create output and target directories if they don't exist +mkdir -p "$OUTPUT_DIR" + +# Get Git commit hash and build date +COMMIT=$(git rev-parse HEAD) +BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +# List of OS and architecture combinations +if [ -n "$EXTENSION_PLATFORM" ]; then + PLATFORMS=("$EXTENSION_PLATFORM") +else + PLATFORMS=( + "windows/amd64" + "windows/arm64" + "darwin/amd64" + "darwin/arm64" + "linux/amd64" + "linux/arm64" + ) +fi + +APP_PATH="$EXTENSION_ID/internal/cmd" + +# Loop through platforms and build +for PLATFORM in "${PLATFORMS[@]}"; do + OS=$(echo "$PLATFORM" | cut -d'/' -f1) + ARCH=$(echo "$PLATFORM" | cut -d'/' -f2) + + OUTPUT_NAME="$OUTPUT_DIR/$EXTENSION_ID_SAFE-$OS-$ARCH" + + if [ "$OS" = "windows" ]; then + OUTPUT_NAME+='.exe' + fi + + echo "Building for $OS/$ARCH..." + + # Delete the output file if it already exists + [ -f "$OUTPUT_NAME" ] && rm -f "$OUTPUT_NAME" + + # Set environment variables for Go build + GOOS=$OS GOARCH=$ARCH go build \ + -ldflags="-X '$APP_PATH.Version=$EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" \ + -o "$OUTPUT_NAME" + + if [ $? -ne 0 ]; then + echo "An error occurred while building for $OS/$ARCH" + exit 1 + fi +done + +echo "Build completed successfully!" +echo "Binaries are located in the $OUTPUT_DIR directory." diff --git a/cli/azd/extensions/microsoft.azd.concurx/changelog.md b/cli/azd/extensions/microsoft.azd.concurx/changelog.md new file mode 100644 index 00000000000..b88d613cce0 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/changelog.md @@ -0,0 +1,3 @@ +# Release History + +## 0.0.1 - Initial Version \ No newline at end of file diff --git a/cli/azd/extensions/microsoft.azd.concurx/extension.yaml b/cli/azd/extensions/microsoft.azd.concurx/extension.yaml new file mode 100644 index 00000000000..60db89236af --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/extension.yaml @@ -0,0 +1,9 @@ +capabilities: + - custom-commands +description: Concurrent execution for azd deployment +displayName: Concurx +id: microsoft.azd.concurx +language: go +namespace: concurx +usage: azd concurx [options] +version: 0.0.1 diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod new file mode 100644 index 00000000000..d968cd6a711 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -0,0 +1,74 @@ +module concurx + +go 1.25 + +replace github.com/azure/azure-dev/cli/azd => ../../ + +require ( + github.com/azure/azure-dev/cli/azd v0.0.0-00010101000000-000000000000 + github.com/fatih/color v1.18.0 + github.com/spf13/cobra v1.10.1 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // 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.20.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/blang/semver/v4 v4.0.0 // 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/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // 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.0 // 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/joho/godotenv v1.5.1 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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 v1.38.0 // 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/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.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/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum new file mode 100644 index 00000000000..a1cfd953fd8 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -0,0 +1,212 @@ +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.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +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/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/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/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/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/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/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/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.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/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/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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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= +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/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/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.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-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/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/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +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.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/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/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= +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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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/azd/extensions/microsoft.azd.concurx/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/root.go new file mode 100644 index 00000000000..1097db51e80 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/root.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func NewRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "azd concurx [options]", + Short: "Concurrent execution for azd deployment", + SilenceUsage: true, + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + } + + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") + + rootCmd.AddCommand(newUpCommand()) + rootCmd.AddCommand(newVersionCommand()) + + return rootCmd +} diff --git a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go new file mode 100644 index 00000000000..1963e1aeee9 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/spf13/cobra" +) + +func newUpCommand() *cobra.Command { + return &cobra.Command{ + Use: "up", + Short: "Runs azd up in concurrent mode", + RunE: func(cmd *cobra.Command, args []string) error { + // Create a new context that includes 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() + + // Get the original project configuration (services as defined in azure.yaml) + projectResp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("failed to get project: %w", err) + } + + fmt.Printf("Project: %s\n", projectResp.GetProject().Name) + fmt.Printf("\nServices from azure.yaml (%d):\n", len(projectResp.GetProject().Services)) + for name := range projectResp.GetProject().Services { + fmt.Printf(" - %s\n", name) + } + + // Get the resolved services (includes services generated by importers like Aspire) + resolvedResp, err := azdClient.Project().GetResolvedServices(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("failed to get resolved services: %w", err) + } + + fmt.Printf("\nResolved services after import processing (%d):\n", len(resolvedResp.GetServices())) + for name, svc := range resolvedResp.GetServices() { + fmt.Printf(" - %s (host: %s, language: %s)\n", name, svc.Host, svc.Language) + } + + return nil + }, + } +} diff --git a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/version.go b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/version.go new file mode 100644 index 00000000000..715323a6c5c --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/version.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + // Populated at build time + Version = "dev" // Default value for development builds + Commit = "none" + BuildDate = "unknown" +) + +func newVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints the version of the application", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s\nCommit: %s\nBuild Date: %s\n", Version, Commit, BuildDate) + }, + } +} diff --git a/cli/azd/extensions/microsoft.azd.concurx/main.go b/cli/azd/extensions/microsoft.azd.concurx/main.go new file mode 100644 index 00000000000..839f2c79a41 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/main.go @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "context" + "os" + + "concurx/internal/cmd" + + "github.com/fatih/color" +) + +func init() { + forceColorVal, has := os.LookupEnv("FORCE_COLOR") + if has && forceColorVal == "1" { + color.NoColor = false + } +} + +func main() { + // Execute the root command + ctx := context.Background() + rootCmd := cmd.NewRootCommand() + + if err := rootCmd.ExecuteContext(ctx); err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } +} diff --git a/cli/azd/grpc/proto/project.proto b/cli/azd/grpc/proto/project.proto index 62df83af009..8cc3643f7bc 100644 --- a/cli/azd/grpc/proto/project.proto +++ b/cli/azd/grpc/proto/project.proto @@ -15,6 +15,10 @@ service ProjectService { // AddService adds a new service to the project. rpc AddService(AddServiceRequest) returns (EmptyResponse); + + // GetResolvedServices gets the resolved list of services after processing any importers (e.g., Aspire projects). + // This returns the actual services that will be deployed, including those generated by importers. + rpc GetResolvedServices(EmptyRequest) returns (GetResolvedServicesResponse); } // GetProjectResponse message definition @@ -26,3 +30,9 @@ message GetProjectResponse { message AddServiceRequest { ServiceConfig service = 1; } + +// GetResolvedServicesResponse message definition +message GetResolvedServicesResponse { + // Map of service name to service configuration + map services = 1; +} diff --git a/cli/azd/internal/grpcserver/project_service.go b/cli/azd/internal/grpcserver/project_service.go index 04ffadc9ad1..10e209a7060 100644 --- a/cli/azd/internal/grpcserver/project_service.go +++ b/cli/azd/internal/grpcserver/project_service.go @@ -20,15 +20,18 @@ type projectService struct { lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] lazyEnvManager *lazy.Lazy[environment.Manager] + importManager *project.ImportManager } func NewProjectService( lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], lazyEnvManager *lazy.Lazy[environment.Manager], + importManager *project.ImportManager, ) azdext.ProjectServiceServer { return &projectService{ lazyAzdContext: lazyAzdContext, lazyEnvManager: lazyEnvManager, + importManager: importManager, } } @@ -125,3 +128,64 @@ func (s *projectService) AddService(ctx context.Context, req *azdext.AddServiceR return &azdext.EmptyResponse{}, nil } + +// GetResolvedServices returns the resolved list of services after processing any importers (e.g., Aspire projects). +// This includes services generated by importers like Aspire AppHost projects. +func (s *projectService) GetResolvedServices( + ctx context.Context, + req *azdext.EmptyRequest, +) (*azdext.GetResolvedServicesResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + if err != nil { + return nil, err + } + + envKeyMapper := func(env string) string { + return "" + } + + defaultEnvironment, err := azdContext.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + if defaultEnvironment != "" { + env, err := envManager.Get(ctx, defaultEnvironment) + if err == nil && env != nil { + envKeyMapper = env.Getenv + } + } + + // Get resolved services using ImportManager + servicesStable, err := s.importManager.ServiceStable(ctx, projectConfig) + if err != nil { + return nil, fmt.Errorf("resolving services: %w", err) + } + + // Convert to proto format + protoServices := make(map[string]*azdext.ServiceConfig) + for _, service := range servicesStable { + var protoService *azdext.ServiceConfig + + // Use mapper with environment variable resolver + if err := mapper.WithResolver(envKeyMapper).Convert(service, &protoService); err != nil { + return nil, fmt.Errorf("converting service config to proto: %w", err) + } + + protoServices[service.Name] = protoService + } + + return &azdext.GetResolvedServicesResponse{ + Services: protoServices, + }, nil +} diff --git a/cli/azd/internal/grpcserver/project_service_test.go b/cli/azd/internal/grpcserver/project_service_test.go index ff55a66ef13..42e45ca4f2f 100644 --- a/cli/azd/internal/grpcserver/project_service_test.go +++ b/cli/azd/internal/grpcserver/project_service_test.go @@ -32,8 +32,9 @@ func Test_ProjectService_NoProject(t *testing.T) { return nil, azdcontext.ErrNoProject }) - // Create the service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + // Create the service with ImportManager. + importManager := project.NewImportManager(&project.DotNetImporter{}) + service := NewProjectService(lazyAzdContext, lazyEnvManager, importManager) _, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) require.Error(t, err) } @@ -76,8 +77,9 @@ func Test_ProjectService_Flow(t *testing.T) { err = envManager.Save(*mockContext.Context, testEnv1) require.NoError(t, err) - // Create the service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + // Create the service with ImportManager. + importManager := project.NewImportManager(&project.DotNetImporter{}) + service := NewProjectService(lazyAzdContext, lazyEnvManager, importManager) // Test: Retrieve project details. getResponse, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) @@ -112,8 +114,9 @@ func Test_ProjectService_AddService(t *testing.T) { lazyAzdContext := lazy.From(azdContext) lazyEnvManager := lazy.From(envManager) - // Create the project service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + // Create the project service with ImportManager. + importManager := project.NewImportManager(&project.DotNetImporter{}) + service := NewProjectService(lazyAzdContext, lazyEnvManager, importManager) // Prepare a new service addition request. serviceRequest := &azdext.AddServiceRequest{ diff --git a/cli/azd/pkg/azdext/project.pb.go b/cli/azd/pkg/azdext/project.pb.go index c53579c6577..a28b6e16f94 100644 --- a/cli/azd/pkg/azdext/project.pb.go +++ b/cli/azd/pkg/azdext/project.pb.go @@ -114,6 +114,52 @@ func (x *AddServiceRequest) GetService() *ServiceConfig { return nil } +// GetResolvedServicesResponse message definition +type GetResolvedServicesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Map of service name to service configuration + Services map[string]*ServiceConfig `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResolvedServicesResponse) Reset() { + *x = GetResolvedServicesResponse{} + mi := &file_project_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResolvedServicesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResolvedServicesResponse) ProtoMessage() {} + +func (x *GetResolvedServicesResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResolvedServicesResponse.ProtoReflect.Descriptor instead. +func (*GetResolvedServicesResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{2} +} + +func (x *GetResolvedServicesResponse) GetServices() map[string]*ServiceConfig { + if x != nil { + return x.Services + } + return nil +} + var File_project_proto protoreflect.FileDescriptor const file_project_proto_rawDesc = "" + @@ -122,11 +168,17 @@ const file_project_proto_rawDesc = "" + "\x12GetProjectResponse\x12/\n" + "\aproject\x18\x01 \x01(\v2\x15.azdext.ProjectConfigR\aproject\"D\n" + "\x11AddServiceRequest\x12/\n" + - "\aservice\x18\x01 \x01(\v2\x15.azdext.ServiceConfigR\aservice2\x89\x01\n" + + "\aservice\x18\x01 \x01(\v2\x15.azdext.ServiceConfigR\aservice\"\xc0\x01\n" + + "\x1bGetResolvedServicesResponse\x12M\n" + + "\bservices\x18\x01 \x03(\v21.azdext.GetResolvedServicesResponse.ServicesEntryR\bservices\x1aR\n" + + "\rServicesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + + "\x05value\x18\x02 \x01(\v2\x15.azdext.ServiceConfigR\x05value:\x028\x012\xdb\x01\n" + "\x0eProjectService\x127\n" + "\x03Get\x12\x14.azdext.EmptyRequest\x1a\x1a.azdext.GetProjectResponse\x12>\n" + "\n" + - "AddService\x12\x19.azdext.AddServiceRequest\x1a\x15.azdext.EmptyResponseB/Z-github.com/azure/azure-dev/cli/azd/pkg/azdextb\x06proto3" + "AddService\x12\x19.azdext.AddServiceRequest\x1a\x15.azdext.EmptyResponse\x12P\n" + + "\x13GetResolvedServices\x12\x14.azdext.EmptyRequest\x1a#.azdext.GetResolvedServicesResponseB/Z-github.com/azure/azure-dev/cli/azd/pkg/azdextb\x06proto3" var ( file_project_proto_rawDescOnce sync.Once @@ -140,27 +192,33 @@ func file_project_proto_rawDescGZIP() []byte { return file_project_proto_rawDescData } -var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_project_proto_goTypes = []any{ - (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse - (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest - (*ProjectConfig)(nil), // 2: azdext.ProjectConfig - (*ServiceConfig)(nil), // 3: azdext.ServiceConfig - (*EmptyRequest)(nil), // 4: azdext.EmptyRequest - (*EmptyResponse)(nil), // 5: azdext.EmptyResponse + (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse + (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest + (*GetResolvedServicesResponse)(nil), // 2: azdext.GetResolvedServicesResponse + nil, // 3: azdext.GetResolvedServicesResponse.ServicesEntry + (*ProjectConfig)(nil), // 4: azdext.ProjectConfig + (*ServiceConfig)(nil), // 5: azdext.ServiceConfig + (*EmptyRequest)(nil), // 6: azdext.EmptyRequest + (*EmptyResponse)(nil), // 7: azdext.EmptyResponse } var file_project_proto_depIdxs = []int32{ - 2, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig - 3, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig - 4, // 2: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest - 1, // 3: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest - 0, // 4: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse - 5, // 5: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 4, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig + 5, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig + 3, // 2: azdext.GetResolvedServicesResponse.services:type_name -> azdext.GetResolvedServicesResponse.ServicesEntry + 5, // 3: azdext.GetResolvedServicesResponse.ServicesEntry.value:type_name -> azdext.ServiceConfig + 6, // 4: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest + 1, // 5: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest + 6, // 6: azdext.ProjectService.GetResolvedServices:input_type -> azdext.EmptyRequest + 0, // 7: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse + 7, // 8: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse + 2, // 9: azdext.ProjectService.GetResolvedServices:output_type -> azdext.GetResolvedServicesResponse + 7, // [7:10] is the sub-list for method output_type + 4, // [4:7] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_project_proto_init() } @@ -175,7 +233,7 @@ func file_project_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_project_proto_rawDesc), len(file_project_proto_rawDesc)), NumEnums: 0, - NumMessages: 2, + NumMessages: 4, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/project_grpc.pb.go b/cli/azd/pkg/azdext/project_grpc.pb.go index 95824d4a316..4bee10a85fb 100644 --- a/cli/azd/pkg/azdext/project_grpc.pb.go +++ b/cli/azd/pkg/azdext/project_grpc.pb.go @@ -22,8 +22,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" - ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" + ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" + ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" + ProjectService_GetResolvedServices_FullMethodName = "/azdext.ProjectService/GetResolvedServices" ) // ProjectServiceClient is the client API for ProjectService service. @@ -36,6 +37,9 @@ type ProjectServiceClient interface { Get(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) // AddService adds a new service to the project. AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // GetResolvedServices gets the resolved list of services after processing any importers (e.g., Aspire projects). + // This returns the actual services that will be deployed, including those generated by importers. + GetResolvedServices(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetResolvedServicesResponse, error) } type projectServiceClient struct { @@ -66,6 +70,16 @@ func (c *projectServiceClient) AddService(ctx context.Context, in *AddServiceReq return out, nil } +func (c *projectServiceClient) GetResolvedServices(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetResolvedServicesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetResolvedServicesResponse) + err := c.cc.Invoke(ctx, ProjectService_GetResolvedServices_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ProjectServiceServer is the server API for ProjectService service. // All implementations must embed UnimplementedProjectServiceServer // for forward compatibility. @@ -76,6 +90,9 @@ type ProjectServiceServer interface { Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) // AddService adds a new service to the project. AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) + // GetResolvedServices gets the resolved list of services after processing any importers (e.g., Aspire projects). + // This returns the actual services that will be deployed, including those generated by importers. + GetResolvedServices(context.Context, *EmptyRequest) (*GetResolvedServicesResponse, error) mustEmbedUnimplementedProjectServiceServer() } @@ -92,6 +109,9 @@ func (UnimplementedProjectServiceServer) Get(context.Context, *EmptyRequest) (*G func (UnimplementedProjectServiceServer) AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddService not implemented") } +func (UnimplementedProjectServiceServer) GetResolvedServices(context.Context, *EmptyRequest) (*GetResolvedServicesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetResolvedServices not implemented") +} func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} @@ -149,6 +169,24 @@ func _ProjectService_AddService_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _ProjectService_GetResolvedServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetResolvedServices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetResolvedServices_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetResolvedServices(ctx, req.(*EmptyRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -164,6 +202,10 @@ var ProjectService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AddService", Handler: _ProjectService_AddService_Handler, }, + { + MethodName: "GetResolvedServices", + Handler: _ProjectService_GetResolvedServices_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "project.proto", From f2fc08739c7ef22855a0f2f463950f36464159ee Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 20 Nov 2025 08:24:36 +0000 Subject: [PATCH 02/10] ignore binary --- .gitignore | 2 ++ cli/azd/extensions/microsoft.azd.concurx/README.md | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 cli/azd/extensions/microsoft.azd.concurx/README.md diff --git a/.gitignore b/.gitignore index 660ecdae526..06a094b7e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ cli/azd/extensions/microsoft.azd.ai.builder/microsoft.azd.ai.builder cli/azd/extensions/microsoft.azd.ai.builder/microsoft.azd.ai.builder.exe cli/azd/extensions/microsoft.azd.demo/microsoft.azd.demo cli/azd/extensions/microsoft.azd.demo/microsoft.azd.demo.exe +cli/azd/extensions/microsoft.azd.concurx/concurx +cli/azd/extensions/microsoft.azd.concurx/concurx.exe diff --git a/cli/azd/extensions/microsoft.azd.concurx/README.md b/cli/azd/extensions/microsoft.azd.concurx/README.md new file mode 100644 index 00000000000..2fb8c85c518 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.concurx/README.md @@ -0,0 +1,3 @@ +# `azd` ConcurX Extension + +An AZD extension to make deployments concurrently. From 84e396c70748723d72653237f69da3eb910e2e5b Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 20 Nov 2025 21:25:00 +0000 Subject: [PATCH 03/10] wip --- .../microsoft.azd.concurx/internal/cmd/up.go | 117 ++++++++++++++++-- 1 file changed, 105 insertions(+), 12 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go index 1963e1aeee9..1d062041299 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go +++ b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go @@ -4,9 +4,12 @@ package cmd import ( + "context" "fmt" + "sync" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -27,28 +30,118 @@ func newUpCommand() *cobra.Command { defer azdClient.Close() // Get the original project configuration (services as defined in azure.yaml) - projectResp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + projectClient := azdClient.Project() + + // Get the resolved services (includes services generated by importers like Aspire) + resolvedResp, err := projectClient.GetResolvedServices(ctx, &azdext.EmptyRequest{}) if err != nil { - return fmt.Errorf("failed to get project: %w", err) + return fmt.Errorf("failed to get resolved services: %w", err) } - fmt.Printf("Project: %s\n", projectResp.GetProject().Name) - fmt.Printf("\nServices from azure.yaml (%d):\n", len(projectResp.GetProject().Services)) - for name := range projectResp.GetProject().Services { - fmt.Printf(" - %s\n", name) + services := resolvedResp.GetServices() + fmt.Printf("\nResolved services after import processing (%d):\n", len(services)) + for name, svc := range services { + fmt.Printf(" - %s (host: %s, language: %s)\n", name, svc.Host, svc.Language) } - // Get the resolved services (includes services generated by importers like Aspire) - resolvedResp, err := azdClient.Project().GetResolvedServices(ctx, &azdext.EmptyRequest{}) + // Get the workflow client + workflowClient := azdClient.Workflow() + + // Step 1: Run package + color.New(color.FgCyan, color.Bold).Println("\n==> Running package...") + _, err = workflowClient.Run(ctx, &azdext.RunWorkflowRequest{ + Workflow: &azdext.Workflow{ + Name: "package", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"package"}, + }, + }, + }, + }, + }) if err != nil { - return fmt.Errorf("failed to get resolved services: %w", err) + return fmt.Errorf("failed to run package: %w", err) } + fmt.Println("Package completed") - fmt.Printf("\nResolved services after import processing (%d):\n", len(resolvedResp.GetServices())) - for name, svc := range resolvedResp.GetServices() { - fmt.Printf(" - %s (host: %s, language: %s)\n", name, svc.Host, svc.Language) + // Step 2: Run provision + color.New(color.FgCyan, color.Bold).Println("\n==> Running provision...") + _, err = workflowClient.Run(ctx, &azdext.RunWorkflowRequest{ + Workflow: &azdext.Workflow{ + Name: "provision", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"provision"}, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to run provision: %w", err) + } + fmt.Println("Provision completed") + + // Step 3: Run deploy concurrently for each service + color.New(color.FgCyan, color.Bold).Printf("\n==> Deploying %d services concurrently...\n", len(services)) + + var wg sync.WaitGroup + errChan := make(chan error, len(services)) + + for serviceName := range services { + wg.Add(1) + go func(svcName string) { + defer wg.Done() + + // Create a new independent context for each deployment + // This prevents context cancellation from affecting other concurrent deployments + deployCtx := azdext.WithAccessToken(context.Background()) + + color.New(color.FgYellow).Printf(" [%s] Starting deployment...\n", svcName) + + _, err := workflowClient.Run(deployCtx, &azdext.RunWorkflowRequest{ + Workflow: &azdext.Workflow{ + Name: fmt.Sprintf("deploy-%s", svcName), + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"deploy", svcName}, + }, + }, + }, + }, + }) + + if err != nil { + color.New(color.FgRed).Printf(" [%s] Deployment failed: %v\n", svcName, err) + errChan <- fmt.Errorf("failed to deploy service %s: %w", svcName, err) + return + } + + color.New(color.FgGreen).Printf(" [%s] Deployment completed successfully!\n", svcName) + }(serviceName) + } // Wait for all deployments to complete + wg.Wait() + close(errChan) + + // Check for any errors + var deployErrors []error + for err := range errChan { + deployErrors = append(deployErrors, err) + } + + if len(deployErrors) > 0 { + color.New(color.FgRed, color.Bold).Println("\n==> Deployment completed with errors:") + for _, err := range deployErrors { + fmt.Printf(" - %v\n", err) + } + return fmt.Errorf("%d service(s) failed to deploy", len(deployErrors)) } + color.New(color.FgGreen, color.Bold).Println("\n==> All services deployed successfully!") return nil }, } From 051b6d0fabc010f09a0f4954676fef752659cd59 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sun, 23 Nov 2025 21:19:50 +0000 Subject: [PATCH 04/10] wip --- .../microsoft.azd.concurx/internal/cmd/up.go | 231 ++++++++++++++---- 1 file changed, 185 insertions(+), 46 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go index 1d062041299..4c03e3bbac6 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go +++ b/cli/azd/extensions/microsoft.azd.concurx/internal/cmd/up.go @@ -4,15 +4,68 @@ package cmd import ( - "context" "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" "sync" + "sync/atomic" + "time" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/fatih/color" "github.com/spf13/cobra" ) +// buildGateWriter wraps an io.Writer and monitors written content. +// For Aspire/dotnet services, it closes a channel when it detects "Deploying services (azd deploy)" +// in the output, which indicates that the AppHost and dotnet project have been successfully built +// and the manifest has been generated. At this point, it's safe for other dotnet services to start +// building in parallel. +type buildGateWriter struct { + writer io.Writer + gateChannel chan struct{} + gateReleased *atomic.Bool + serviceName string + releaseMu sync.Mutex +} + +func newBuildGateWriter(w io.Writer, gateChannel chan struct{}, gateReleased *atomic.Bool, serviceName string) *buildGateWriter { + return &buildGateWriter{ + writer: w, + gateChannel: gateChannel, + gateReleased: gateReleased, + serviceName: serviceName, + } +} + +func (bw *buildGateWriter) Write(p []byte) (n int, err error) { + // Always write to the underlying writer first + n, err = bw.writer.Write(p) + + // Check if we should release the gate + bw.releaseMu.Lock() + defer bw.releaseMu.Unlock() + + if !bw.gateReleased.Load() { + content := string(p) + // Look for "Deploying services (azd deploy)" in the output + // This indicates the Aspire manifest has been generated and the dotnet build is complete + if strings.Contains(content, "Deploying services (azd deploy)") { + bw.gateReleased.Store(true) + close(bw.gateChannel) + color.New(color.FgCyan).Printf( + " [%s] Build complete, releasing gate for other services\n", + bw.serviceName, + ) + } + } + + return n, err +} + func newUpCommand() *cobra.Command { return &cobra.Command{ Use: "up", @@ -47,26 +100,7 @@ func newUpCommand() *cobra.Command { // Get the workflow client workflowClient := azdClient.Workflow() - // Step 1: Run package - color.New(color.FgCyan, color.Bold).Println("\n==> Running package...") - _, err = workflowClient.Run(ctx, &azdext.RunWorkflowRequest{ - Workflow: &azdext.Workflow{ - Name: "package", - Steps: []*azdext.WorkflowStep{ - { - Command: &azdext.WorkflowCommand{ - Args: []string{"package"}, - }, - }, - }, - }, - }) - if err != nil { - return fmt.Errorf("failed to run package: %w", err) - } - fmt.Println("Package completed") - - // Step 2: Run provision + // Step 1: Run provision color.New(color.FgCyan, color.Bold).Println("\n==> Running provision...") _, err = workflowClient.Run(ctx, &azdext.RunWorkflowRequest{ Workflow: &azdext.Workflow{ @@ -85,45 +119,150 @@ func newUpCommand() *cobra.Command { } fmt.Println("Provision completed") - // Step 3: Run deploy concurrently for each service + // Step 2: Run deploy concurrently for each service + // Strategy: For Aspire services (containerapp-dotnet host with dotnet language), + // we need to ensure one service builds first to generate the manifest and compile + // shared dependencies. We pick the first Aspire service and wait until it shows + // "Deploying services (azd deploy)" which indicates the AppHost has been built + // and manifest generated. After that, all other services can deploy in parallel safely. color.New(color.FgCyan, color.Bold).Printf("\n==> Deploying %d services concurrently...\n", len(services)) + // Create logs directory with unique timestamp + timestamp := time.Now().Format("20060102-150405") + logsDir := filepath.Join(".azure", "logs", "deploy", timestamp) + if err := os.MkdirAll(logsDir, 0755); err != nil { + return fmt.Errorf("failed to create logs directory: %w", err) + } + var wg sync.WaitGroup errChan := make(chan error, len(services)) + var activeDeployments atomic.Int32 + + // Build gate for Aspire services (containerapp-dotnet host + dotnet language) + // The first Aspire service will close this channel when "Deploying services (azd deploy)" + // appears, signaling that other Aspire services can start building + buildGateReleased := make(chan struct{}) - for serviceName := range services { + // Track if we need to use the build gate (only for first Aspire service) + firstAspireService := true + var firstAspireMu sync.Mutex + + for serviceName, service := range services { wg.Add(1) - go func(svcName string) { + activeDeployments.Add(1) + + go func(svcName string, svc *azdext.ServiceConfig) { defer wg.Done() + defer activeDeployments.Add(-1) - // Create a new independent context for each deployment - // This prevents context cancellation from affecting other concurrent deployments - deployCtx := azdext.WithAccessToken(context.Background()) - - color.New(color.FgYellow).Printf(" [%s] Starting deployment...\n", svcName) - - _, err := workflowClient.Run(deployCtx, &azdext.RunWorkflowRequest{ - Workflow: &azdext.Workflow{ - Name: fmt.Sprintf("deploy-%s", svcName), - Steps: []*azdext.WorkflowStep{ - { - Command: &azdext.WorkflowCommand{ - Args: []string{"deploy", svcName}, - }, - }, - }, - }, - }) + // Create unique log file for this deployment + logFileName := fmt.Sprintf("deploy-%s.log", svcName) + logFilePath := filepath.Join(logsDir, logFileName) + absLogPath, _ := filepath.Abs(logFilePath) + + logFile, err := os.Create(logFilePath) + if err != nil { + errChan <- fmt.Errorf("failed to create log file for service %s: %w", svcName, err) + return + } + defer logFile.Close() + + // Determine if this is an Aspire service (containerapp-dotnet host + dotnet language) + isAspireService := (svc.Host == "containerapp-dotnet" || svc.Host == "containerapp") && + (svc.Language == "dotnet" || svc.Language == "csharp") + // Track whether this goroutine is the first Aspire service + isFirstAspire := false + gateReleased := &atomic.Bool{} + + // If Aspire service, check if this is the first one + if isAspireService { + firstAspireMu.Lock() + if firstAspireService { + isFirstAspire = true + firstAspireService = false + firstAspireMu.Unlock() + + color.New(color.FgYellow).Printf( + " [%s] First Aspire service, waiting for build completion... (logs: %s)\n", + svcName, + absLogPath, + ) + } else { + firstAspireMu.Unlock() + + color.New(color.FgYellow).Printf( + " [%s] Waiting for first Aspire service to complete build...\n", + svcName, + ) + // Wait for the build gate to be released + <-buildGateReleased + + color.New(color.FgYellow).Printf( + " [%s] Deploying... (logs: %s)\n", + svcName, + absLogPath, + ) + } + } else { + color.New(color.FgYellow).Printf( + " [%s] Deploying... (logs: %s)\n", + svcName, + absLogPath, + ) + } + + // Ensure gate is released when function exits (if still held) + defer func() { + if isFirstAspire && !gateReleased.Load() { + gateReleased.Store(true) + close(buildGateReleased) + color.New(color.FgCyan).Printf( + " [%s] Build gate released (cleanup)\n", + svcName, + ) + } + }() + + // Create output writer with build gate monitoring for first Aspire service + var outputWriter io.Writer + if isFirstAspire { + // Use build gate writer that monitors output and releases gate + outputWriter = newBuildGateWriter(logFile, buildGateReleased, gateReleased, svcName) + } else { + outputWriter = logFile + } + + // Run azd deploy as a subprocess to capture output + cmd := exec.Command("azd", "deploy", svcName) + cmd.Stdout = outputWriter + cmd.Stderr = outputWriter + cmd.Dir, _ = os.Getwd() + // Disable ANSI color escape sequences in logs + // NO_COLOR is respected by the fatih/color library + // AZD_DEBUG_FORCE_NO_TTY ensures terminal detection returns false + cmd.Env = append(os.Environ(), "NO_COLOR=1", "AZD_FORCE_TTY=false") + + err = cmd.Run() if err != nil { - color.New(color.FgRed).Printf(" [%s] Deployment failed: %v\n", svcName, err) + color.New(color.FgRed).Printf( + " [%s] Failed (logs: %s)\n", + svcName, + absLogPath, + ) errChan <- fmt.Errorf("failed to deploy service %s: %w", svcName, err) return } - color.New(color.FgGreen).Printf(" [%s] Deployment completed successfully!\n", svcName) - }(serviceName) - } // Wait for all deployments to complete + color.New(color.FgGreen).Printf( + " [%s] Completed (logs: %s)\n", + svcName, + absLogPath, + ) + }(serviceName, service) + } + + // Wait for all deployments to complete wg.Wait() close(errChan) From 33aecc5034d28eee6c14b809e9fae5c3d121f6b5 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 11 Dec 2025 21:10:58 +0000 Subject: [PATCH 05/10] cspell --- cli/azd/.vscode/cspell-azd-dictionary.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index c0a270a06b9..185cd456b19 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -81,6 +81,7 @@ cmdrecord cmdsubst Cobo cognitiveservices +concurx conditionalize consolesize containeragent @@ -151,6 +152,7 @@ jmes jquery keychain kubelogin +langchain langchaingo LASTEXITCODE ldflags @@ -285,5 +287,4 @@ wireinject yacspin yamlnode ymlt -zerr -langchain +zerr \ No newline at end of file From fa333be42ff585c54ee97c6bfd8dbef64a05dcec Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 11 Dec 2025 21:16:59 +0000 Subject: [PATCH 06/10] updates to mod --- cli/azd/extensions/microsoft.azd.concurx/go.mod | 8 ++++---- cli/azd/extensions/microsoft.azd.concurx/go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod index d968cd6a711..ef5bf2a73cd 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.mod +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -63,10 +63,10 @@ require ( 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/net v0.46.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.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 diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum index a1cfd953fd8..b8f257e3601 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.sum +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -179,19 +179,19 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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= From bac1495d489666ebf06e0e3dad9c520c63a446cb Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 12 Dec 2025 19:04:12 +0000 Subject: [PATCH 07/10] Support interactive invocation opt-in for extensions --- cli/azd/extensions/extension.schema.json | 6 ++++++ .../microsoft.azd.extensions/internal/cmd/publish.go | 2 ++ .../internal/models/extension_schema.go | 4 ++++ cli/azd/pkg/extensions/extension.go | 1 + cli/azd/pkg/extensions/manager.go | 1 + cli/azd/pkg/extensions/registry.go | 3 +++ cli/azd/pkg/extensions/runner.go | 7 +++++++ 7 files changed, 24 insertions(+) diff --git a/cli/azd/extensions/extension.schema.json b/cli/azd/extensions/extension.schema.json index a834a5198ea..ed2647ed027 100644 --- a/cli/azd/extensions/extension.schema.json +++ b/cli/azd/extensions/extension.schema.json @@ -237,6 +237,12 @@ } }, "required": ["serve"] + }, + "interactive": { + "type": "boolean", + "title": "Interactive Mode", + "description": "When true, the extension runs in interactive mode with stdin/stdout/stderr directly connected to the terminal. This is required for TUI (Terminal User Interface) applications that use libraries like Bubble Tea. Default is false.", + "default": false } }, "required": [ diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go index 475b9859b51..02e0c9ce237 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go @@ -401,6 +401,7 @@ func addOrUpdateExtension( Examples: extensionMetadata.Examples, Dependencies: extensionMetadata.Dependencies, Providers: extensionMetadata.Providers, + Interactive: extensionMetadata.Interactive, Artifacts: artifacts, } @@ -417,6 +418,7 @@ func addOrUpdateExtension( Examples: extensionMetadata.Examples, Dependencies: extensionMetadata.Dependencies, Providers: extensionMetadata.Providers, + Interactive: extensionMetadata.Interactive, Artifacts: artifacts, }) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go index db1b0e727bf..ac830ceb350 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go @@ -30,6 +30,7 @@ type ExtensionSchema struct { Examples []extensions.ExtensionExample `yaml:"examples" json:"examples"` Tags []string `yaml:"tags" json:"tags,omitempty"` Dependencies []extensions.ExtensionDependency `yaml:"dependencies" json:"dependencies,omitempty"` + Interactive bool `yaml:"interactive" json:"interactive,omitempty"` Platforms map[string]map[string]any `yaml:"platforms" json:"platforms,omitempty"` Path string `yaml:"-" json:"-"` } @@ -72,6 +73,9 @@ func (e ExtensionSchema) MarshalYAML() (interface{}, error) { if len(e.Providers) > 0 { base["providers"] = e.Providers } + if e.Interactive { + base["interactive"] = e.Interactive + } if len(e.Platforms) > 0 { base["platforms"] = e.Platforms } diff --git a/cli/azd/pkg/extensions/extension.go b/cli/azd/pkg/extensions/extension.go index 4eb9d1f7704..48968ab019f 100644 --- a/cli/azd/pkg/extensions/extension.go +++ b/cli/azd/pkg/extensions/extension.go @@ -25,6 +25,7 @@ type Extension struct { Source string `json:"source"` Providers []Provider `json:"providers,omitempty"` McpConfig *McpConfig `json:"mcp,omitempty"` + Interactive bool `json:"interactive,omitempty"` stdin *bytes.Buffer stdout *output.DynamicMultiWriter diff --git a/cli/azd/pkg/extensions/manager.go b/cli/azd/pkg/extensions/manager.go index 065c7e5f7f4..8a0af1d641a 100644 --- a/cli/azd/pkg/extensions/manager.go +++ b/cli/azd/pkg/extensions/manager.go @@ -482,6 +482,7 @@ func (m *Manager) Install( Source: extension.Source, Providers: selectedVersion.Providers, McpConfig: selectedVersion.McpConfig, + Interactive: selectedVersion.Interactive, } if err := m.userConfig.Set(installedConfigKey, extensions); err != nil { diff --git a/cli/azd/pkg/extensions/registry.go b/cli/azd/pkg/extensions/registry.go index 8b39342abce..37fd39870a1 100644 --- a/cli/azd/pkg/extensions/registry.go +++ b/cli/azd/pkg/extensions/registry.go @@ -114,6 +114,9 @@ type ExtensionVersion struct { EntryPoint string `json:"entryPoint,omitempty"` // McpConfig is the MCP server configuration for this extension version McpConfig *McpConfig `json:"mcp,omitempty"` + // Interactive indicates whether the extension should run in interactive mode + // with stdin/stdout/stderr directly connected to the terminal (required for TUI apps) + Interactive bool `json:"interactive,omitempty"` } // ExtensionArtifact represents the artifact information of an extension diff --git a/cli/azd/pkg/extensions/runner.go b/cli/azd/pkg/extensions/runner.go index 16680fad16f..fb7841b5f0c 100644 --- a/cli/azd/pkg/extensions/runner.go +++ b/cli/azd/pkg/extensions/runner.go @@ -63,6 +63,13 @@ func (r *Runner) Invoke(ctx context.Context, extension *Extension, options *Invo runArgs = runArgs.WithStdErr(options.StdErr) } + // Enable interactive mode for extensions that opt-in via the "interactive" property + // in their extension.yaml. This ensures stdin/stdout/stderr are directly connected + // to the terminal, which is required for TUI applications like Bubble Tea. + if extension.Interactive { + runArgs = runArgs.WithInteractive(true) + } + runResult, err := r.commandRunner.Run(ctx, runArgs) if err != nil { return &runResult, &ExtensionRunError{Err: err, ExtensionId: extension.Id} From 27b4097600714e773f1e800de0e99fb047ceb744 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 16 Dec 2025 00:03:47 +0000 Subject: [PATCH 08/10] cleaner way to show interactive override from extensions owning the terminal --- cli/azd/pkg/extensions/runner.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/cli/azd/pkg/extensions/runner.go b/cli/azd/pkg/extensions/runner.go index fb7841b5f0c..9b2cd234711 100644 --- a/cli/azd/pkg/extensions/runner.go +++ b/cli/azd/pkg/extensions/runner.go @@ -51,23 +51,22 @@ func (r *Runner) Invoke(ctx context.Context, extension *Extension, options *Invo runArgs = runArgs.WithEnv(options.Env) } - if options.StdIn != nil { - runArgs = runArgs.WithStdIn(options.StdIn) - } - - if options.StdOut != nil { - runArgs = runArgs.WithStdOut(options.StdOut) - } - - if options.StdErr != nil { - runArgs = runArgs.WithStdErr(options.StdErr) - } - - // Enable interactive mode for extensions that opt-in via the "interactive" property - // in their extension.yaml. This ensures stdin/stdout/stderr are directly connected - // to the terminal, which is required for TUI applications like Bubble Tea. + // Interactive extensions bypass custom stdio and connect directly to the terminal. + // Non-interactive extensions use provided stdio for capture/processing. if extension.Interactive { runArgs = runArgs.WithInteractive(true) + } else { + if options.StdIn != nil { + runArgs = runArgs.WithStdIn(options.StdIn) + } + + if options.StdOut != nil { + runArgs = runArgs.WithStdOut(options.StdOut) + } + + if options.StdErr != nil { + runArgs = runArgs.WithStdErr(options.StdErr) + } } runResult, err := r.commandRunner.Run(ctx, runArgs) From f7c52d99881e6e7b7e5402db217bbc6756ebcecb Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 16 Dec 2025 20:03:07 +0000 Subject: [PATCH 09/10] revert and make interactive by default --- cli/azd/cmd/extensions.go | 7 ++----- cli/azd/extensions/extension.schema.json | 6 ------ .../internal/cmd/publish.go | 2 -- .../internal/models/extension_schema.go | 4 ---- cli/azd/pkg/extensions/extension.go | 1 - cli/azd/pkg/extensions/manager.go | 1 - cli/azd/pkg/extensions/registry.go | 3 --- cli/azd/pkg/extensions/runner.go | 15 +++++++-------- 8 files changed, 9 insertions(+), 30 deletions(-) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index d73dafa5230..13a8ca1728f 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -182,11 +182,8 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error } options := &extensions.InvokeOptions{ - Args: a.args, - Env: allEnv, - StdIn: a.console.Handles().Stdin, - StdOut: a.console.Handles().Stdout, - StdErr: a.console.Handles().Stderr, + Args: a.args, + Env: allEnv, } _, err = a.extensionRunner.Invoke(ctx, extension, options) diff --git a/cli/azd/extensions/extension.schema.json b/cli/azd/extensions/extension.schema.json index ed2647ed027..a834a5198ea 100644 --- a/cli/azd/extensions/extension.schema.json +++ b/cli/azd/extensions/extension.schema.json @@ -237,12 +237,6 @@ } }, "required": ["serve"] - }, - "interactive": { - "type": "boolean", - "title": "Interactive Mode", - "description": "When true, the extension runs in interactive mode with stdin/stdout/stderr directly connected to the terminal. This is required for TUI (Terminal User Interface) applications that use libraries like Bubble Tea. Default is false.", - "default": false } }, "required": [ diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go index 02e0c9ce237..475b9859b51 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go @@ -401,7 +401,6 @@ func addOrUpdateExtension( Examples: extensionMetadata.Examples, Dependencies: extensionMetadata.Dependencies, Providers: extensionMetadata.Providers, - Interactive: extensionMetadata.Interactive, Artifacts: artifacts, } @@ -418,7 +417,6 @@ func addOrUpdateExtension( Examples: extensionMetadata.Examples, Dependencies: extensionMetadata.Dependencies, Providers: extensionMetadata.Providers, - Interactive: extensionMetadata.Interactive, Artifacts: artifacts, }) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go index ac830ceb350..db1b0e727bf 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema.go @@ -30,7 +30,6 @@ type ExtensionSchema struct { Examples []extensions.ExtensionExample `yaml:"examples" json:"examples"` Tags []string `yaml:"tags" json:"tags,omitempty"` Dependencies []extensions.ExtensionDependency `yaml:"dependencies" json:"dependencies,omitempty"` - Interactive bool `yaml:"interactive" json:"interactive,omitempty"` Platforms map[string]map[string]any `yaml:"platforms" json:"platforms,omitempty"` Path string `yaml:"-" json:"-"` } @@ -73,9 +72,6 @@ func (e ExtensionSchema) MarshalYAML() (interface{}, error) { if len(e.Providers) > 0 { base["providers"] = e.Providers } - if e.Interactive { - base["interactive"] = e.Interactive - } if len(e.Platforms) > 0 { base["platforms"] = e.Platforms } diff --git a/cli/azd/pkg/extensions/extension.go b/cli/azd/pkg/extensions/extension.go index 48968ab019f..4eb9d1f7704 100644 --- a/cli/azd/pkg/extensions/extension.go +++ b/cli/azd/pkg/extensions/extension.go @@ -25,7 +25,6 @@ type Extension struct { Source string `json:"source"` Providers []Provider `json:"providers,omitempty"` McpConfig *McpConfig `json:"mcp,omitempty"` - Interactive bool `json:"interactive,omitempty"` stdin *bytes.Buffer stdout *output.DynamicMultiWriter diff --git a/cli/azd/pkg/extensions/manager.go b/cli/azd/pkg/extensions/manager.go index 8a0af1d641a..065c7e5f7f4 100644 --- a/cli/azd/pkg/extensions/manager.go +++ b/cli/azd/pkg/extensions/manager.go @@ -482,7 +482,6 @@ func (m *Manager) Install( Source: extension.Source, Providers: selectedVersion.Providers, McpConfig: selectedVersion.McpConfig, - Interactive: selectedVersion.Interactive, } if err := m.userConfig.Set(installedConfigKey, extensions); err != nil { diff --git a/cli/azd/pkg/extensions/registry.go b/cli/azd/pkg/extensions/registry.go index 37fd39870a1..8b39342abce 100644 --- a/cli/azd/pkg/extensions/registry.go +++ b/cli/azd/pkg/extensions/registry.go @@ -114,9 +114,6 @@ type ExtensionVersion struct { EntryPoint string `json:"entryPoint,omitempty"` // McpConfig is the MCP server configuration for this extension version McpConfig *McpConfig `json:"mcp,omitempty"` - // Interactive indicates whether the extension should run in interactive mode - // with stdin/stdout/stderr directly connected to the terminal (required for TUI apps) - Interactive bool `json:"interactive,omitempty"` } // ExtensionArtifact represents the artifact information of an extension diff --git a/cli/azd/pkg/extensions/runner.go b/cli/azd/pkg/extensions/runner.go index 9b2cd234711..b3ec3ed2d2d 100644 --- a/cli/azd/pkg/extensions/runner.go +++ b/cli/azd/pkg/extensions/runner.go @@ -15,11 +15,12 @@ import ( ) type InvokeOptions struct { - Args []string - Env []string - StdIn io.Reader - StdOut io.Writer - StdErr io.Writer + Args []string + Env []string + StdIn io.Reader + StdOut io.Writer + StdErr io.Writer + Interactive bool } type Runner struct { @@ -51,9 +52,7 @@ func (r *Runner) Invoke(ctx context.Context, extension *Extension, options *Invo runArgs = runArgs.WithEnv(options.Env) } - // Interactive extensions bypass custom stdio and connect directly to the terminal. - // Non-interactive extensions use provided stdio for capture/processing. - if extension.Interactive { + if options.Interactive { runArgs = runArgs.WithInteractive(true) } else { if options.StdIn != nil { From 1915e6ed5ac9d8bd3dfcf9da3f0d454548cbd451 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 16 Dec 2025 20:04:51 +0000 Subject: [PATCH 10/10] set interactive option for the call --- cli/azd/cmd/extensions.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index 13a8ca1728f..0b2a58ac3a2 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -184,6 +184,8 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error options := &extensions.InvokeOptions{ Args: a.args, Env: allEnv, + // cmd extensions are always interactive (connected to terminal) + Interactive: true, } _, err = a.extensionRunner.Invoke(ctx, extension, options)