diff --git a/cli/azd/docs/extensions/extension-framework.md b/cli/azd/docs/extensions/extension-framework.md index 39bd88b1095..48739d425c3 100644 --- a/cli/azd/docs/extensions/extension-framework.md +++ b/cli/azd/docs/extensions/extension-framework.md @@ -641,7 +641,7 @@ Usage: `azd x init` Usage: `azd x build` -- `--cwd` - The extension directory, defaults to `.`. +- `-C, --cwd` - The extension directory, inherited from azd's global flag (defaults to the current directory). - `--all` - Builds binaries for all supported operating systems and architecture. - `--output, -o` - Path to the output directory, defaults to `./bin`. - `--skip-install` - When skips local installation after successful build. @@ -652,7 +652,7 @@ Usage: `azd x build` Usage: `azd x watch` -- `--cwd` - The extension directory, defaults to `.`. +- `-C, --cwd` - The extension directory, inherited from azd's global flag (defaults to the current directory). --- @@ -660,7 +660,7 @@ Usage: `azd x watch` Usage: `azd x pack` -- `--cwd` - The extension directory, defaults to `.`. +- `-C, --cwd` - The extension directory, inherited from azd's global flag (defaults to the current directory). - `--input, -i` - Path to the input directory that contains binary files. - `--output, -o` - Path to the artifacts output directory, defaults to local `azd` artifacts path, `~/.azd/registry`. - `--rebuild` - When set forces a rebuild before packaging. @@ -671,7 +671,7 @@ Usage: `azd x pack` Usage: `azd x release --repo {owner}/{name}` -- `--cwd` - The extension directory, defaults to `.`. +- `-C, --cwd` - The extension directory, inherited from azd's global flag (defaults to the current directory). - `--artifacts` - Path to the artifacts to upload for the release, defaults to local `azd` artifacts path, `~/.azd/registry`. - `--repo` - The Github repo name in `{owner}/{repo}` format. - `--title, -t` - The name of the release, defaults to extension name plus version. @@ -687,7 +687,7 @@ Usage: `azd x release --repo {owner}/{name}` Usage: `azd x publish --repo {owner}/{name}` -- `--cwd` - The extension directory, defaults to `.`. +- `-C, --cwd` - The extension directory, inherited from azd's global flag (defaults to the current directory). - `--registry, -r` - The path to the registry.json file to update, defaults to local extension registry - `--repo` - The Github repo name in `{owner}/{repo}` format. - `--version, -v` - The version of the release, defaults to extension version from extension manifest @@ -1026,6 +1026,7 @@ Extensions can declare the following capabilities in their manifest: - **`mcp-server`**: Provide Model Context Protocol tools for AI agents - **`service-target-provider`**: Provide custom service deployment targets - **`framework-service-provider`**: Provide custom language frameworks and build systems +- **`provisioning-provider`**: Provide a custom infrastructure provisioning experience (alternative to Bicep / Terraform) - **`metadata`**: Provide comprehensive metadata about commands and configuration schemas #### Complete Extension Manifest Example diff --git a/cli/azd/docs/extensions/extension-migration-guide.md b/cli/azd/docs/extensions/extension-migration-guide.md index f313d690d5a..3e1d89d613e 100644 --- a/cli/azd/docs/extensions/extension-migration-guide.md +++ b/cli/azd/docs/extensions/extension-migration-guide.md @@ -133,6 +133,18 @@ func NewRootCommand() *cobra.Command { `PersistentPreRunE`. The `ExtensionContext` struct provides typed access to the parsed values. +### Migrating `--cwd` handling + +`-C/--cwd` is now an SDK-managed global flag. `NewExtensionRootCommand` reads +the flag or `AZD_CWD`, resolves the value, and changes the process working +directory before the subcommand runs. Extensions that previously declared their +own `--cwd` flag or changed directories in their root `PersistentPreRunE` +should remove that custom flag and directory-changing code, then use the current +working directory inside command handlers. + +If an extension still needs the original value for display or diagnostics, read +`extCtx.Cwd`; do not re-register `--cwd` on subcommands. + --- ## M3: Listen Command — Manual Host Setup → NewListenCommand diff --git a/cli/azd/extensions/extension.schema.json b/cli/azd/extensions/extension.schema.json index 90dbf068d66..4195e9076f8 100644 --- a/cli/azd/extensions/extension.schema.json +++ b/cli/azd/extensions/extension.schema.json @@ -113,7 +113,7 @@ "capabilities": { "type": "array", "title": "Capabilities", - "description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, metadata. Select one or more from the allowed list. Each value must be unique.", + "description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, provisioning-provider, metadata. Select one or more from the allowed list. Each value must be unique.", "minItems": 1, "uniqueItems": true, "items": { @@ -142,15 +142,21 @@ "title": "Service Target Provider", "description": "Service target provider enables extensions to provide custom service deployment targets." }, - { - "type": "string", - "const": "framework-service-provider", - "title": "Framework Service Provider", - "description": "Framework service provider enables extensions to provide custom language frameworks and build systems." - }, - { - "type": "string", - "const": "metadata", + { + "type": "string", + "const": "framework-service-provider", + "title": "Framework Service Provider", + "description": "Framework service provider enables extensions to provide custom language frameworks and build systems." + }, + { + "type": "string", + "const": "provisioning-provider", + "title": "Provisioning Provider", + "description": "Provisioning provider enables extensions to provide a custom infrastructure provisioning experience." + }, + { + "type": "string", + "const": "metadata", "title": "Metadata", "description": "Metadata capability enables extensions to provide comprehensive metadata about their commands and capabilities via a metadata command." } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go index a6c888de01f..14286c08bb1 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go @@ -30,7 +30,7 @@ type buildFlags struct { skipInstall bool } -func newBuildCommand() *cobra.Command { +func newBuildCommand(outputPath *string) *cobra.Command { flags := &buildFlags{} buildCmd := &cobra.Command{ @@ -42,6 +42,9 @@ func newBuildCommand() *cobra.Command { "Builds the azd extension project for one or more platforms", ) + if outputPath != nil { + flags.outputPath = *outputPath + } defaultBuildFlags(flags) err := runBuildAction(cmd.Context(), flags) if err != nil { @@ -53,11 +56,11 @@ func newBuildCommand() *cobra.Command { }, } - buildCmd.Flags().StringVarP( - &flags.outputPath, - "output", "o", "./bin", - "Path to the output directory. Defaults to ./bin folder.", - ) + azdext.RegisterFlagOptions(buildCmd, azdext.FlagOptions{ + Name: "output", + Default: "./bin", + Usage: "Path to the output directory.", + }) buildCmd.Flags().BoolVar( &flags.allPlatforms, "all", false, "When set builds for all os/platforms. Defaults to the current os/platform only.", diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init.go index ba772fbad7a..d4091f1a36e 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init.go @@ -13,6 +13,7 @@ import ( "os/exec" "path" "path/filepath" + "slices" "strings" "text/template" @@ -38,7 +39,7 @@ type initFlags struct { namespace string } -func newInitCommand() *cobra.Command { +func newInitCommand(noPrompt *bool) *cobra.Command { flags := &initFlags{} initCmd := &cobra.Command{ @@ -50,6 +51,10 @@ func newInitCommand() *cobra.Command { "Initializes a new azd extension project from a template", ) + if noPrompt != nil { + flags.noPrompt = *noPrompt + } + // Validate required parameters when in headless mode if flags.noPrompt { var missingParams []string @@ -90,12 +95,6 @@ func newInitCommand() *cobra.Command { "When set will create a local extension source registry.", ) - initCmd.Flags().BoolVar( - &flags.noPrompt, - "no-prompt", false, - "Skip all prompts by providing all required parameters via command-line flags.", - ) - initCmd.Flags().StringVar( &flags.id, "id", "", @@ -111,8 +110,10 @@ func newInitCommand() *cobra.Command { initCmd.Flags().StringSliceVar( &flags.capabilities, "capabilities", []string{}, - "The list of capabilities for the extension "+ - "(e.g., custom-commands,lifecycle-events,mcp-server,service-target-provider).", + fmt.Sprintf( + "The list of capabilities for the extension (e.g., %s).", + strings.Join(validCapabilityNames(), ","), + ), ) initCmd.Flags().StringVar( @@ -391,20 +392,13 @@ func collectExtensionMetadataFromFlags(flags *initFlags) (*models.ExtensionSchem ) } - // Validate capabilities - validCapabilities := map[string]bool{ - "custom-commands": true, - "lifecycle-events": true, - "mcp-server": true, - "service-target-provider": true, - } - + supportedNames := validCapabilityNames() for _, cap := range flags.capabilities { - if !validCapabilities[cap] { + if !slices.Contains(extensions.ValidCapabilities, extensions.CapabilityType(cap)) { return nil, fmt.Errorf( - "invalid capability '%s', supported capabilities are: "+ - "custom-commands, lifecycle-events, mcp-server, service-target-provider", + "invalid capability '%s', supported capabilities are: %s", cap, + strings.Join(supportedNames, ", "), ) } } @@ -523,33 +517,8 @@ func collectExtensionMetadata(ctx context.Context, azdClient *azdext.AzdClient) capabilitiesPrompt, err := azdClient.Prompt().MultiSelect(ctx, &azdext.MultiSelectRequest{ Options: &azdext.MultiSelectOptions{ - Message: "Select capabilities for your extension", - Choices: []*azdext.MultiSelectChoice{ - { - Label: "Custom Commands", - Value: "custom-commands", - }, - { - Label: "Framework Service Provider", - Value: "framework-service-provider", - }, - { - Label: "Lifecycle Events", - Value: "lifecycle-events", - }, - { - Label: "Metadata", - Value: "metadata", - }, - { - Label: "MCP Server", - Value: "mcp-server", - }, - { - Label: "Service Target Provider", - Value: "service-target-provider", - }, - }, + Message: "Select capabilities for your extension", + Choices: capabilityPromptChoices(), EnableFiltering: new(false), DisplayNumbers: new(false), HelpMessage: "Capabilities define the features and functionalities of your extension. " + @@ -626,6 +595,45 @@ func collectExtensionMetadata(ctx context.Context, azdClient *azdext.AzdClient) }, nil } +func validCapabilityNames() []string { + names := make([]string, len(extensions.ValidCapabilities)) + for i, cap := range extensions.ValidCapabilities { + names[i] = string(cap) + } + + return names +} + +func capabilityPromptChoices() []*azdext.MultiSelectChoice { + choices := make([]*azdext.MultiSelectChoice, len(extensions.ValidCapabilities)) + for i, cap := range extensions.ValidCapabilities { + choices[i] = &azdext.MultiSelectChoice{ + Label: capabilityLabel(cap), + Value: string(cap), + } + } + + return choices +} + +func capabilityLabel(cap extensions.CapabilityType) string { + words := strings.Split(string(cap), "-") + for i, word := range words { + if word == "" { + continue + } + + switch strings.ToLower(word) { + case "mcp": + words[i] = "MCP" + default: + words[i] = strings.ToUpper(word[:1]) + word[1:] + } + } + + return strings.Join(words, " ") +} + func createExtensionDirectory( ctx context.Context, azdClient *azdext.AzdClient, diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init_test.go new file mode 100644 index 00000000000..4f674dc14d4 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init_test.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/extensions" + "github.com/stretchr/testify/require" +) + +func TestCapabilityPromptChoicesMatchValidCapabilities(t *testing.T) { + choices := capabilityPromptChoices() + require.Len(t, choices, len(extensions.ValidCapabilities)) + + for i, capability := range extensions.ValidCapabilities { + require.Equal(t, string(capability), choices[i].Value) + require.NotEmpty(t, choices[i].Label) + } +} + +func TestCapabilityLabel(t *testing.T) { + require.Equal(t, "Custom Commands", capabilityLabel(extensions.CustomCommandCapability)) + require.Equal(t, "MCP Server", capabilityLabel(extensions.McpServerCapability)) + require.Equal(t, "Provisioning Provider", capabilityLabel(extensions.ProvisioningProviderCapability)) +} diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/pack.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/pack.go index b12cd6ab5f4..d554eaffbde 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/pack.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/pack.go @@ -28,7 +28,7 @@ type packageFlags struct { rebuild bool } -func newPackCommand() *cobra.Command { +func newPackCommand(outputPath *string) *cobra.Command { flags := &packageFlags{} packageCmd := &cobra.Command{ @@ -40,6 +40,11 @@ func newPackCommand() *cobra.Command { "Packages the azd extension project and updates the registry", ) + // For pack, an empty output path means "use the local registry artifacts path". + // Only copy the SDK-managed --output value when the user explicitly supplied it. + if outputPath != nil && cmd.Flags().Changed("output") { + flags.outputPath = *outputPath + } defaultPackageFlags(flags) err := runPackageAction(cmd.Context(), flags) if err != nil { @@ -51,11 +56,11 @@ func newPackCommand() *cobra.Command { }, } - packageCmd.Flags().StringVarP( - &flags.outputPath, - "output", "o", "", - "Path to the artifacts output directory. If not provided, will use local registry artifacts path.", - ) + azdext.RegisterFlagOptions(packageCmd, azdext.FlagOptions{ + Name: "output", + Usage: "Path to the artifacts output directory. If omitted, uses the local registry artifacts path.", + HideDefault: true, + }) packageCmd.Flags().StringVarP( &flags.inputPath, 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 01d953d6be1..c9e9b93a060 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/publish.go @@ -160,17 +160,22 @@ func runPublishAction(ctx context.Context, flags *publishFlags, defaultRegistryU release, err = ghCli.ViewRelease(absExtensionPath, flags.repository, tagName) if err != nil { if errors.Is(err, github.ErrReleaseNotFound) { - return internal.NewUserFriendlyError("Github Release not found", strings.Join([]string{ - fmt.Sprintf( - "The %s extension does not have a release tagged with version %s.", - output.WithHighLightFormat(extensionMetadata.Id), - output.WithHighLightFormat(flags.version), - ), - fmt.Sprintf( - "To create a new release, run: %s and then try again.", - output.WithHighLightFormat("azd x release --repo {owner}/{repo}"), - ), - }, "\n")) + return &azdext.LocalError{ + Message: "GitHub release not found", + Code: "github_release_not_found", + Category: azdext.LocalErrorCategoryDependency, + Suggestion: strings.Join([]string{ + fmt.Sprintf( + "The %s extension does not have a release tagged with version %s.", + output.WithHighLightFormat(extensionMetadata.Id), + output.WithHighLightFormat(flags.version), + ), + fmt.Sprintf( + "To create a new release, run: %s and then try again.", + output.WithHighLightFormat("azd x release --repo {owner}/{repo}"), + ), + }, "\n"), + } } return err diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/release.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/release.go index da06e7d07ff..f59bca3a31c 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/release.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/release.go @@ -188,9 +188,8 @@ func runReleaseAction(ctx context.Context, flags *releaseFlags) error { return fmt.Errorf("failed to initialize GitHub CLI: %w", err) } - // Check if GitHub CLI is installed using the new method that returns UserFriendlyError if err := ghCli.CheckAndGetInstallError(); err != nil { - return err // Pass the UserFriendlyError through + return err } repo, err := ghCli.ViewRepository(absExtensionPath, flags.repository) @@ -294,8 +293,11 @@ func runReleaseAction(ctx context.Context, flags *releaseFlags) error { releaseResult, err := ghCli.CreateRelease(absExtensionPath, tagName, releaseOptions, files) if err != nil { if errors.Is(err, github.ErrReleaseAlreadyExists) { - err = internal.NewUserFriendlyError("Release already exists", - strings.Join([]string{ + err = &azdext.LocalError{ + Message: "Release already exists", + Code: "github_release_already_exists", + Category: azdext.LocalErrorCategoryValidation, + Suggestion: strings.Join([]string{ fmt.Sprintf( "The %s extension already has been released with version %s", output.WithHighLightFormat(extensionMetadata.Id), @@ -303,7 +305,7 @@ func runReleaseAction(ctx context.Context, flags *releaseFlags) error { ), "Please update the version number or delete the existing release before trying again.", }, "\n"), - ) + } } return ux.Error, common.NewDetailedError("Release failed", err) diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/root.go index 5be78d9872b..df5734b8463 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/root.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/root.go @@ -4,55 +4,59 @@ package cmd import ( + "io" + "log" "os" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) -type rootFlags struct { - cwd string -} - func NewRootCommand() *cobra.Command { - flags := &rootFlags{} - - rootCmd := &cobra.Command{ - Use: "x [options]", - Short: "Runs azd developer extension commands", - SilenceUsage: true, - SilenceErrors: true, - CompletionOptions: cobra.CompletionOptions{ - DisableDefaultCmd: true, - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Set the working directory to the one specified in the flags - if flags.cwd != "." { - if err := os.Chdir(flags.cwd); err != nil { - return err - } + // Build the root command using the SDK helper so the extension picks up + // azd's standard persistent flags (--debug, --no-prompt, -C/--cwd, + // -e/--environment, -o/--output) and avoids name collisions with azd's + // reserved global flags. The SDK's --cwd already changes the working + // directory in PersistentPreRunE, which matches the previous custom flag's + // purpose of pointing at the extension project directory. + rootCmd, extCtx := azdext.NewExtensionRootCommand(azdext.ExtensionCommandOptions{ + Name: "x", + Use: "x [options]", + Short: "Runs azd developer extension commands", + }) + sdkPersistentPreRunE := rootCmd.PersistentPreRunE + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if sdkPersistentPreRunE != nil { + if err := sdkPersistentPreRunE(cmd, args); err != nil { + return err } + } + + if extCtx.Debug { + log.SetOutput(os.Stderr) + } else { + log.SetOutput(io.Discard) + } - return nil - }, + return nil + } + + rootCmd.SilenceUsage = true + rootCmd.SilenceErrors = true + rootCmd.CompletionOptions = cobra.CompletionOptions{ + DisableDefaultCmd: true, } rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") - rootCmd.AddCommand(newInitCommand()) - rootCmd.AddCommand(newBuildCommand()) + rootCmd.AddCommand(newInitCommand(&extCtx.NoPrompt)) + rootCmd.AddCommand(newBuildCommand(&extCtx.OutputFormat)) rootCmd.AddCommand(newWatchCommand()) - rootCmd.AddCommand(newPackCommand()) + rootCmd.AddCommand(newPackCommand(&extCtx.OutputFormat)) rootCmd.AddCommand(newReleaseCommand()) rootCmd.AddCommand(newPublishCommand()) rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newMetadataCommand()) - rootCmd.PersistentFlags().StringVar( - &flags.cwd, - "cwd", ".", - "Path to the azd extension project", - ) - return rootCmd } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/github/github.go b/cli/azd/extensions/microsoft.azd.extensions/internal/github/github.go index 11ef0833296..45c07a64ac8 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/github/github.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/github/github.go @@ -10,7 +10,7 @@ import ( "runtime" "strings" - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.extensions/internal" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/common" ) @@ -79,15 +79,16 @@ func (gh *GitHubCli) IsInstalled() (bool, error) { return true, nil } -// CheckAndGetInstallError checks if GitHub CLI is installed and returns a UserFriendlyError if it's not +// CheckAndGetInstallError checks if GitHub CLI is installed and returns a structured error if it's not. func (gh *GitHubCli) CheckAndGetInstallError() error { installed, err := gh.IsInstalled() if err != nil || !installed { - // Create a user-friendly error with installation instructions in the user details - return internal.NewUserFriendlyError( - "GitHub CLI is required for this operation", - gh.getInstallInstructions(), - ) + return &azdext.LocalError{ + Message: "GitHub CLI is required for this operation", + Code: "github_cli_missing", + Category: azdext.LocalErrorCategoryDependency, + Suggestion: gh.getInstallInstructions(), + } } return nil } 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 a0ece39ef50..772ea256c16 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 @@ -10,7 +10,7 @@ import ( "path/filepath" "strings" - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.extensions/internal" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "github.com/azure/azure-dev/cli/azd/pkg/output" "go.yaml.in/yaml/v3" @@ -117,16 +117,20 @@ func LoadExtension(extensionPath string) (*ExtensionSchema, error) { if os.IsNotExist(err) { extensionYaml := output.WithHighLightFormat("extension.yaml") - return nil, internal.NewUserFriendlyErrorf( - "Extension manifest file not found", - `Ensure that the %s file exists in the current directory. + return nil, &azdext.LocalError{ + Message: "Extension manifest file not found", + Code: "missing_extension_manifest", + Category: azdext.LocalErrorCategoryDependency, + Suggestion: fmt.Sprintf( + `Ensure that the %s file exists in the current directory. Alternatively, you can specify the path to the %s file using the --cwd flag. Example: %s`, - extensionYaml, - extensionYaml, - output.WithHighLightFormat("azd x --cwd "), - ) + extensionYaml, + extensionYaml, + output.WithHighLightFormat("azd x --cwd "), + ), + } } return nil, fmt.Errorf("failed to read metadata: %w", err) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema_coverage_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema_coverage_test.go index 78850829596..ec20b8a8918 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema_coverage_test.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/models/extension_schema_coverage_test.go @@ -5,10 +5,12 @@ package models import ( "encoding/json" + "errors" "os" "path/filepath" "testing" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "github.com/stretchr/testify/require" ) @@ -110,6 +112,12 @@ func TestLoadExtension_FileNotFound(t *testing.T) { require.Error(t, err) require.Nil(t, ext) require.Contains(t, err.Error(), "Extension manifest file not found") + + localErr, ok := errors.AsType[*azdext.LocalError](err) + require.True(t, ok) + require.Equal(t, "missing_extension_manifest", localErr.Code) + require.Equal(t, azdext.LocalErrorCategoryDependency, localErr.Category) + require.Contains(t, localErr.Suggestion, "extension.yaml") } func TestLoadExtension_MissingId(t *testing.T) { diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.mod.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.mod.tmpl index 698b261a942..adae2acbbf0 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.mod.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.mod.tmpl @@ -1,29 +1,36 @@ module {{.Metadata.Id}} -go 1.25 +go 1.26.2 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 - github.com/azure/azure-dev/cli/azd v0.0.0-20260116183934-428498d0f124 + github.com/azure/azure-dev/cli/azd v1.25.0 github.com/fatih/color v1.18.0 github.com/spf13/cobra v1.10.1 ) require ( - dario.cat/mergo v1.0.2 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 // 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/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b // 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/buger/goterm v1.0.4 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.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 @@ -31,26 +38,31 @@ require ( 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/cli/browser v1.3.0 // 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/gofrs/flock v0.12.1 // 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/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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // 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/mark3labs/mcp-go v0.41.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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // 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 @@ -62,25 +74,31 @@ require ( 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/theckman/yacspin v0.13.12 // 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 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.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.45.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 + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.sum b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.sum index 8218c48f3f2..b8335ba534d 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.sum +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/go.sum @@ -1,6 +1,4 @@ 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= @@ -13,16 +11,28 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 h1:nnQ9vXH039UrEFxi08pPuZBE7VfqSJt343uJLw0rhWI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0/go.mod h1:4YIVtzMFVsPwBvitCDX7J9sqthSj43QD1sP6fYc1egc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= 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 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +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= @@ -37,8 +47,8 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp 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-20260116183934-428498d0f124 h1:V3jGfHri9K94pW8BjnPB3RmZkKHkXcwe9naOHR63P8A= -github.com/azure/azure-dev/cli/azd v0.0.0-20260116183934-428498d0f124/go.mod h1:j+bdvNwQPdYtSfFe/xbfWqYr8Guw9hiP1JOVpIBERj0= +github.com/azure/azure-dev/cli/azd v1.25.0 h1:gb8Ah5ntUcUAKIDBhCdpx8xxDWSAtCLGyck+Y50QZhw= +github.com/azure/azure-dev/cli/azd v1.25.0/go.mod h1:1ZoZZlUbK8FMTZRibM9hEo/UqSaEXA+SFeIKpya4fsY= 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= @@ -51,8 +61,10 @@ 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/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= @@ -69,9 +81,13 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489 h1:a5q2s 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/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= 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 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +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= @@ -80,16 +96,18 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +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/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 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= @@ -107,11 +125,15 @@ 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 h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +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/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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -131,13 +153,18 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= +github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +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= @@ -171,6 +198,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV 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.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= @@ -181,6 +210,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS 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= @@ -194,51 +224,82 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ 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.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= 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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 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-20210331175145-43e1dd70ce54/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.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/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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= -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= +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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/listen.go.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/listen.go.tmpl index fb60f7dd720..9bf3b2703f3 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/listen.go.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/listen.go.tmpl @@ -13,45 +13,23 @@ import ( ) func newListenCommand() *cobra.Command { - return &cobra.Command{ - Use: "listen", - Short: "Starts the extension and listens for events.", - 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() - - host := azdext.NewExtensionHost(azdClient). - WithProjectEventHandler("preprovision", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important work in extension...\n", i) - time.Sleep(250 * time.Millisecond) - } - - return nil - }). - WithServiceEventHandler("prepackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important work in extension...\n", i) - time.Sleep(250 * time.Millisecond) - } - - return nil - }, nil) - - // Start listening for events - // This is a blocking call and will not return until the server connection is closed. - if err := host.Run(ctx); err != nil { - return fmt.Errorf("failed to run extension: %w", err) - } - - return nil - }, - } + return azdext.NewListenCommand(func(host *azdext.ExtensionHost) { + host. + WithProjectEventHandler("preprovision", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). + WithServiceEventHandler("prepackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }, nil) + }) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/metadata.go.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/metadata.go.tmpl index 209f34ab810..f541e9a0c97 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/metadata.go.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/metadata.go.tmpl @@ -4,37 +4,12 @@ package cmd import ( - "encoding/json" - "fmt" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) -func newMetadataCommand() *cobra.Command { - return &cobra.Command{ - Use: "metadata", - Short: "Generate extension metadata including command structure and configuration schemas", - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - // Get root command for metadata generation - rootCmd := cmd.Root() - - // Generate extension metadata with commands and configuration - metadata := azdext.GenerateExtensionMetadata( - "1.0", // schema version - "{{.Metadata.Id}}", // extension id - rootCmd, - ) - - // Output as JSON - jsonBytes, err := json.MarshalIndent(metadata, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal metadata: %w", err) - } - - fmt.Println(string(jsonBytes)) - return nil - }, - } +func newMetadataCommand(rootCmd *cobra.Command) *cobra.Command { + return azdext.NewMetadataCommand("1.0", "{{.Metadata.Id}}", func() *cobra.Command { + return rootCmd + }) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/root.go.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/root.go.tmpl index d01267f8f9c..57bab41c295 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/root.go.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/root.go.tmpl @@ -4,28 +4,30 @@ package cmd import ( + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) func NewRootCommand() *cobra.Command { - rootCmd := &cobra.Command{ - Use: "azd {{.Metadata.Namespace}} [options]", - Short: "{{.Metadata.Description}}", - SilenceUsage: true, - SilenceErrors: true, - CompletionOptions: cobra.CompletionOptions{ - DisableDefaultCmd: true, - }, + rootCmd, extCtx := azdext.NewExtensionRootCommand(azdext.ExtensionCommandOptions{ + Name: "{{.Metadata.Namespace}}", + Use: "azd {{.Metadata.Namespace}} [options]", + Short: "{{.Metadata.Description}}", + }) + + rootCmd.SilenceUsage = true + rootCmd.SilenceErrors = true + rootCmd.CompletionOptions = cobra.CompletionOptions{ + DisableDefaultCmd: true, } rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode") rootCmd.AddCommand(newListenCommand()) rootCmd.AddCommand(newContextCommand()) rootCmd.AddCommand(newPromptCommand()) - rootCmd.AddCommand(newVersionCommand()) - rootCmd.AddCommand(newMetadataCommand()) + rootCmd.AddCommand(newVersionCommand(&extCtx.OutputFormat)) + rootCmd.AddCommand(newMetadataCommand(rootCmd)) return rootCmd } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/version.go.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/version.go.tmpl index 715323a6c5c..65b607a0104 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/version.go.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/internal/cmd/version.go.tmpl @@ -4,8 +4,7 @@ package cmd import ( - "fmt" - + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/spf13/cobra" ) @@ -16,12 +15,6 @@ var ( 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) - }, - } +func newVersionCommand(outputFormat *string) *cobra.Command { + return azdext.NewVersionCommand("{{.Metadata.Id}}", Version, outputFormat) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/main.go.tmpl b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/main.go.tmpl index 43541c6934d..383416bbce5 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/main.go.tmpl +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/go/main.go.tmpl @@ -4,27 +4,11 @@ package main import ( - "context" - "os" - "{{.Metadata.Id}}/internal/cmd" - "github.com/fatih/color" -) -func init() { - forceColorVal, has := os.LookupEnv("FORCE_COLOR") - if has && forceColorVal == "1" { - color.NoColor = false - } -} + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) 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) - } + azdext.Run(cmd.NewRootCommand()) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error.go b/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error.go deleted file mode 100644 index fc40280a319..00000000000 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package internal - -import "fmt" - -// UserFriendlyError represents an error with additional user-friendly details. -// This error type is designed to separate technical error messages (which may be displayed -// in red) from instructional content (which should be displayed in normal text). -type UserFriendlyError struct { - // Technical error message that will be shown as an error - ErrorMessage string - - // User-friendly additional details or instructions that should be shown in normal text - UserDetails string -} - -// Error returns the technical error message, implementing the error interface -func (e *UserFriendlyError) Error() string { - return e.ErrorMessage -} - -// GetUserDetails returns the user-friendly additional details -func (e *UserFriendlyError) GetUserDetails() string { - return e.UserDetails -} - -// NewUserFriendlyError creates a new UserFriendlyError with the given error message and user details -func NewUserFriendlyError(errorMessage, userDetails string) *UserFriendlyError { - return &UserFriendlyError{ - ErrorMessage: errorMessage, - UserDetails: userDetails, - } -} - -// NewUserFriendlyErrorf creates a new UserFriendlyError with a formatted error message and user details -func NewUserFriendlyErrorf(errorMessage string, userDetails string, args ...any) *UserFriendlyError { - return &UserFriendlyError{ - ErrorMessage: errorMessage, - UserDetails: fmt.Sprintf(userDetails, args...), - } -} diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error_test.go deleted file mode 100644 index 3bd54720ea3..00000000000 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/user_friendly_error_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package internal - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestUserFriendlyError_Error(t *testing.T) { - tests := []struct { - name string - err *UserFriendlyError - expected string - }{ - { - name: "BasicMessage", - err: &UserFriendlyError{ - ErrorMessage: "something went wrong", - UserDetails: "Try running the command again", - }, - expected: "something went wrong", - }, - { - name: "EmptyMessage", - err: &UserFriendlyError{ - ErrorMessage: "", - UserDetails: "details only", - }, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, tt.err.Error()) - }) - } -} - -func TestUserFriendlyError_GetUserDetails(t *testing.T) { - tests := []struct { - name string - err *UserFriendlyError - expected string - }{ - { - name: "WithDetails", - err: &UserFriendlyError{ - ErrorMessage: "error", - UserDetails: "Run azd init first", - }, - expected: "Run azd init first", - }, - { - name: "EmptyDetails", - err: &UserFriendlyError{ - ErrorMessage: "error", - UserDetails: "", - }, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, tt.err.GetUserDetails()) - }) - } -} - -func TestUserFriendlyError_ImplementsErrorInterface(t *testing.T) { - ufe := NewUserFriendlyError("test error", "test details") - var err error = ufe - require.Error(t, err) - require.Equal(t, "test error", err.Error()) -} - -func TestNewUserFriendlyError(t *testing.T) { - err := NewUserFriendlyError("the error", "the details") - require.NotNil(t, err) - require.Equal(t, "the error", err.ErrorMessage) - require.Equal(t, "the details", err.UserDetails) - require.Equal(t, "the error", err.Error()) - require.Equal(t, "the details", err.GetUserDetails()) -} - -func TestNewUserFriendlyErrorf(t *testing.T) { - tests := []struct { - name string - errorMessage string - userDetails string - args []any - expectedMessage string - expectedDetails string - }{ - { - name: "WithFormatArgs", - errorMessage: "build failed", - userDetails: "Run %s in %s directory", - args: []any{"go build", "/src"}, - expectedMessage: "build failed", - expectedDetails: "Run go build in /src directory", - }, - { - name: "NoFormatArgs", - errorMessage: "error", - userDetails: "plain details", - args: nil, - expectedMessage: "error", - expectedDetails: "plain details", - }, - { - name: "SingleArg", - errorMessage: "not found", - userDetails: "file %q does not exist", - args: []any{"config.yaml"}, - expectedMessage: "not found", - expectedDetails: `file "config.yaml" does not exist`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := NewUserFriendlyErrorf( - tt.errorMessage, tt.userDetails, tt.args..., - ) - require.NotNil(t, err) - require.Equal(t, tt.expectedMessage, err.Error()) - require.Equal(t, tt.expectedDetails, err.GetUserDetails()) - }) - } -} - -func TestUserFriendlyError_ErrorsAs(t *testing.T) { - ufe := NewUserFriendlyError("wrapped", "details") - wrapped := errors.Join(errors.New("context"), ufe) - - var target *UserFriendlyError - require.True(t, errors.As(wrapped, &target)) - require.Equal(t, "wrapped", target.ErrorMessage) - require.Equal(t, "details", target.UserDetails) -} diff --git a/cli/azd/extensions/microsoft.azd.extensions/main.go b/cli/azd/extensions/microsoft.azd.extensions/main.go index baf5a11045d..44fe3783276 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/main.go +++ b/cli/azd/extensions/microsoft.azd.extensions/main.go @@ -4,57 +4,10 @@ package main import ( - "context" - "errors" - "fmt" - "os" - - "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.extensions/internal" "github.com/azure/azure-dev/cli/azd/extensions/microsoft.azd.extensions/internal/cmd" - "github.com/fatih/color" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" ) -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() - - cwd, err := rootCmd.PersistentFlags().GetString("cwd") - if err != nil { - color.Red("Error: %v", err) - os.Exit(1) - } - - if cwd != "." { - if err := os.Chdir(cwd); err != nil { - color.Red("Error: failed to change directory to %s: %v", cwd, err) - os.Exit(1) - } - } - - if err := rootCmd.ExecuteContext(ctx); err != nil { - // Check if this is our custom UserFriendlyError type - var userFriendlyErr *internal.UserFriendlyError - if errors.As(err, &userFriendlyErr) { - // Display the error message in red - color.Red("Error: %v", userFriendlyErr.Error()) - - // If we have user details, display them in normal text color - if userFriendlyErr.GetUserDetails() != "" { - fmt.Println() - fmt.Println(userFriendlyErr.GetUserDetails()) - } - } else { - // Default error handling for regular errors - color.Red("Error: %v", err) - } - os.Exit(1) - } + azdext.Run(cmd.NewRootCommand()) } diff --git a/cli/azd/extensions/registry.schema.json b/cli/azd/extensions/registry.schema.json index 7b8eac2e246..464834349e6 100644 --- a/cli/azd/extensions/registry.schema.json +++ b/cli/azd/extensions/registry.schema.json @@ -77,11 +77,12 @@ "enum": [ "custom-commands", "lifecycle-events", - "mcp-server", - "service-target-provider", - "framework-service-provider", - "metadata" - ] + "mcp-server", + "service-target-provider", + "framework-service-provider", + "provisioning-provider", + "metadata" + ] } }, "usage": { diff --git a/cli/azd/internal/grpcserver/framework_service.go b/cli/azd/internal/grpcserver/framework_service.go index 8baa8d8ed57..19e16e5172a 100644 --- a/cli/azd/internal/grpcserver/framework_service.go +++ b/cli/azd/internal/grpcserver/framework_service.go @@ -58,9 +58,7 @@ func (s *FrameworkService) Stream(stream azdext.FrameworkService_StreamServer) e return status.Errorf(codes.FailedPrecondition, "failed to get extension: %s", err.Error()) } - // For framework services, we'll create a custom capability check similar to service targets - // Extensions providing framework services should declare this capability - if !extension.HasCapability("framework-service-provider") { + if !extension.HasCapability(extensions.FrameworkServiceProviderCapability) { return status.Errorf(codes.PermissionDenied, "extension does not support framework-service-provider capability") } diff --git a/cli/azd/pkg/azdext/extension_command.go b/cli/azd/pkg/azdext/extension_command.go index 53cbc1af9e7..89b031f2c20 100644 --- a/cli/azd/pkg/azdext/extension_command.go +++ b/cli/azd/pkg/azdext/extension_command.go @@ -26,6 +26,12 @@ const ( // flagDefaultAnnotationPrefix prefixes cobra annotation keys that record // the per-command default for an inherited flag. See [RegisterFlagOptions]. flagDefaultAnnotationPrefix = "azdext.default/" + // flagUsageAnnotationPrefix prefixes cobra annotation keys that record + // the per-command usage text for an inherited flag. See [RegisterFlagOptions]. + flagUsageAnnotationPrefix = "azdext.usage/" + // flagHideDefaultAnnotationPrefix prefixes cobra annotation keys that mark + // inherited flag defaults as hidden for a command. See [RegisterFlagOptions]. + flagHideDefaultAnnotationPrefix = "azdext.hide-default/" defaultOutputFlagUsage = "The output format" ) @@ -231,16 +237,24 @@ type FlagOptions struct { // (e.g. [ExtensionContext.OutputFormat]) when the user does not pass // the flag. Substitution preserves cmd.Flags().Changed(name) == false. Default string + + // Usage, when non-empty, replaces the inherited flag usage text for this + // subcommand's help and metadata. + Usage string + + // HideDefault suppresses the inherited flag default in help and metadata + // without changing the bound runtime value. + HideDefault bool } // RegisterFlagOptions configures per-subcommand behavior for an inherited // persistent flag (typically one registered by [NewExtensionRootCommand], // such as -o/--output). One declaration drives: // -// - help/usage rendering — flag usage gets "(supported: ...)" appended and -// shows the per-command default +// - help/usage rendering — flag usage gets "(supported: ...)" appended, +// and shows the per-command usage/default behavior // - extension metadata (see [GenerateExtensionMetadata]) — populates the -// flag's ValidValues field and overrides its Default field +// flag's ValidValues field and overrides its Default/Description fields // - parse-time validation — values outside AllowedValues are rejected // before the command's RunE runs // - shell completion — AllowedValues are suggested for the flag @@ -248,8 +262,8 @@ type FlagOptions struct { // bound variable is set to Default before RunE runs // // Empty AllowedValues skips validation/completion. Empty Default leaves the -// SDK default in place. Repeat calls for the same flag overwrite. A nil -// command is a no-op. +// SDK default in place unless HideDefault is true. Repeat calls for the same +// flag overwrite. A nil command is a no-op. // // Panics if Name is empty, or if Default is set but not in a non-empty // AllowedValues. Returns an error from PersistentPreRunE if Name does not @@ -294,6 +308,16 @@ func RegisterFlagOptions(cmd *cobra.Command, opts FlagOptions) *cobra.Command { } else { delete(cmd.Annotations, flagDefaultAnnotationPrefix+opts.Name) } + if opts.Usage != "" { + cmd.Annotations[flagUsageAnnotationPrefix+opts.Name] = opts.Usage + } else { + delete(cmd.Annotations, flagUsageAnnotationPrefix+opts.Name) + } + if opts.HideDefault { + cmd.Annotations[flagHideDefaultAnnotationPrefix+opts.Name] = "true" + } else { + delete(cmd.Annotations, flagHideDefaultAnnotationPrefix+opts.Name) + } return cmd } @@ -302,6 +326,8 @@ func RegisterFlagOptions(cmd *cobra.Command, opts FlagOptions) *cobra.Command { type flagOverride struct { AllowedValues []string Default string + Usage string + HideDefault bool } // flagOptionsCompletion returns a delegating completion func for flagName @@ -343,6 +369,16 @@ func flagOverridesForCommand(cmd *cobra.Command) map[string]flagOverride { o := out[name] o.Default = val out[name] = o + case strings.HasPrefix(key, flagUsageAnnotationPrefix): + name := strings.TrimPrefix(key, flagUsageAnnotationPrefix) + o := out[name] + o.Usage = val + out[name] = o + case strings.HasPrefix(key, flagHideDefaultAnnotationPrefix): + name := strings.TrimPrefix(key, flagHideDefaultAnnotationPrefix) + o := out[name] + o.HideDefault = true + out[name] = o } } if len(out) == 0 { @@ -372,10 +408,15 @@ func applyFlagOverridesForCommand(cmd *cobra.Command) func() { continue } savedFlags = append(savedFlags, saved{flag, flag.Usage, flag.DefValue}) + if ov.Usage != "" { + flag.Usage = ov.Usage + } if len(ov.AllowedValues) > 0 { flag.Usage = fmt.Sprintf("%s (supported: %s)", flag.Usage, strings.Join(ov.AllowedValues, ", ")) } - if ov.Default != "" { + if ov.HideDefault { + flag.DefValue = "" + } else if ov.Default != "" { flag.DefValue = ov.Default } } diff --git a/cli/azd/pkg/azdext/extension_commands_test.go b/cli/azd/pkg/azdext/extension_commands_test.go index c8d42ced042..c93b5407bb0 100644 --- a/cli/azd/pkg/azdext/extension_commands_test.go +++ b/cli/azd/pkg/azdext/extension_commands_test.go @@ -29,7 +29,21 @@ func TestRegisterFlagOptions_HelpRendering(t *testing.T) { return nil }, }, FlagOptions{Name: "output", AllowedValues: []string{"json"}, Default: "json"}) - root.AddCommand(showCmd, versionCmd) + pathCmd := RegisterFlagOptions(&cobra.Command{ + Use: "pack", + Short: "pack", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + }, FlagOptions{Name: "output", Usage: "Path to the output directory."}) + hiddenDefaultCmd := RegisterFlagOptions(&cobra.Command{ + Use: "pack-hidden", + Short: "pack-hidden", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + }, FlagOptions{Name: "output", Usage: "Computed output path.", HideDefault: true}) + root.AddCommand(showCmd, versionCmd, pathCmd, hiddenDefaultCmd) showHelp := captureStdout(t, func() { root.SetArgs([]string{"show", "--help"}) @@ -47,6 +61,22 @@ func TestRegisterFlagOptions_HelpRendering(t *testing.T) { require.Contains(t, string(versionHelp), `The output format (supported: json)`) require.NotContains(t, string(versionHelp), `supported: json, table`) + pathHelp := captureStdout(t, func() { + root.SetArgs([]string{"pack", "--help"}) + err := root.Execute() + require.NoError(t, err) + }) + require.Contains(t, string(pathHelp), `Path to the output directory.`) + require.NotContains(t, string(pathHelp), defaultOutputFlagUsage) + + hiddenDefaultHelp := captureStdout(t, func() { + root.SetArgs([]string{"pack-hidden", "--help"}) + err := root.Execute() + require.NoError(t, err) + }) + require.Contains(t, string(hiddenDefaultHelp), `Computed output path.`) + require.NotContains(t, string(hiddenDefaultHelp), `(default "default")`) + rootHelp := captureStdout(t, func() { root.SetArgs([]string{"--help"}) err := root.Execute() diff --git a/cli/azd/pkg/azdext/metadata_generator.go b/cli/azd/pkg/azdext/metadata_generator.go index 7eb887f08e0..fce209539b0 100644 --- a/cli/azd/pkg/azdext/metadata_generator.go +++ b/cli/azd/pkg/azdext/metadata_generator.go @@ -155,10 +155,15 @@ func generateFlags(cmd *cobra.Command) []extensions.Flag { } if ov, ok := overrides[flag.Name]; ok { + if ov.Usage != "" { + flagMeta.Description = ov.Usage + } if len(ov.AllowedValues) > 0 { flagMeta.ValidValues = ov.AllowedValues } - if ov.Default != "" { + if ov.HideDefault { + flagMeta.Default = "" + } else if ov.Default != "" { flagMeta.Default = ov.Default } } diff --git a/cli/azd/pkg/azdext/metadata_generator_test.go b/cli/azd/pkg/azdext/metadata_generator_test.go index 8182932fdd5..813cd03a62d 100644 --- a/cli/azd/pkg/azdext/metadata_generator_test.go +++ b/cli/azd/pkg/azdext/metadata_generator_test.go @@ -344,15 +344,29 @@ func TestGenerateExtensionMetadata_FlagOptionsOverride(t *testing.T) { Use: "list", Short: "List things", RunE: func(cmd *cobra.Command, args []string) error { return nil }, - }, FlagOptions{Name: "output", AllowedValues: []string{"json", "table"}, Default: "json"}) + }, FlagOptions{ + Name: "output", + AllowedValues: []string{"json", "table"}, + Default: "json", + Usage: "Output format for list results", + }) plainCmd := &cobra.Command{ Use: "plain", Short: "Plain command without override", RunE: func(cmd *cobra.Command, args []string) error { return nil }, } + pathCmd := RegisterFlagOptions(&cobra.Command{ + Use: "pack", + Short: "Pack things", + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + }, FlagOptions{ + Name: "output", + Usage: "Path to the output directory.", + HideDefault: true, + }) - rootCmd.AddCommand(listCmd, plainCmd) + rootCmd.AddCommand(listCmd, plainCmd, pathCmd) metadata := GenerateExtensionMetadata("1.0", "test.extension", rootCmd) @@ -360,9 +374,7 @@ func TestGenerateExtensionMetadata_FlagOptionsOverride(t *testing.T) { require.NotNil(t, listMeta) listOutput := findFlag(listMeta.Flags, "output") require.NotNil(t, listOutput) - // The metadata description stays clean; ValidValues carries the allowed - // set so JSON consumers can render or validate it as they prefer. - assert.Equal(t, defaultOutputFlagUsage, listOutput.Description) + assert.Equal(t, "Output format for list results", listOutput.Description) assert.Equal(t, []string{"json", "table"}, listOutput.ValidValues) assert.Equal(t, "json", listOutput.Default) @@ -373,4 +385,11 @@ func TestGenerateExtensionMetadata_FlagOptionsOverride(t *testing.T) { assert.Equal(t, defaultOutputFlagUsage, plainOutput.Description) assert.Empty(t, plainOutput.ValidValues) assert.Equal(t, "default", plainOutput.Default) + + pathMeta := findCommand(metadata.Commands, "pack") + require.NotNil(t, pathMeta) + pathOutput := findFlag(pathMeta.Flags, "output") + require.NotNil(t, pathOutput) + assert.Equal(t, "Path to the output directory.", pathOutput.Description) + assert.Empty(t, pathOutput.Default) } diff --git a/docs/architecture/extension-framework.md b/docs/architecture/extension-framework.md index 8845e5bec64..6f961f5430a 100644 --- a/docs/architecture/extension-framework.md +++ b/docs/architecture/extension-framework.md @@ -56,6 +56,7 @@ Extensions declare their capabilities in `extension.yaml`: | `mcp-server` | Provide Model Context Protocol tools for AI agents | | `framework-service-provider` | Add build/restore support for new languages | | `service-target-provider` | Add deployment support for new hosting targets | +| `provisioning-provider` | Add custom infrastructure provisioning support | | `metadata` | Provide metadata about commands and capabilities | ## Available gRPC Services diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md index bc0f9081ae8..7c176f2961e 100644 --- a/docs/concepts/glossary.md +++ b/docs/concepts/glossary.md @@ -62,7 +62,11 @@ A JSON manifest that lists available extensions, their versions, capabilities, a ### Extension Capabilities -The set of features an extension provides. Valid capabilities are: `custom-commands`, `lifecycle-events`, `mcp-server`, `service-target-provider`, `framework-service-provider`, and `metadata`. See [Extension Framework](../architecture/extension-framework.md) for details. +The set of features an extension provides. Valid capabilities are: `custom-commands`, `lifecycle-events`, `mcp-server`, `service-target-provider`, `framework-service-provider`, `provisioning-provider`, and `metadata`. See [Extension Framework](../architecture/extension-framework.md) for details. + +### Provisioning Provider + +An extension capability that lets an extension provide a custom infrastructure provisioning experience as an alternative to built-in providers such as Bicep and Terraform. ## Architecture diff --git a/docs/guides/creating-an-extension.md b/docs/guides/creating-an-extension.md index 7208ab124b5..c47d92ee047 100644 --- a/docs/guides/creating-an-extension.md +++ b/docs/guides/creating-an-extension.md @@ -11,6 +11,7 @@ azd extensions use a gRPC-based framework to add functionality. Extensions can p - **MCP server** — Provide Model Context Protocol tools for AI agents - **Framework service providers** — Add build/restore support for new languages - **Service target providers** — Add deployment support for new Azure hosting targets +- **Provisioning providers** — Add custom infrastructure provisioning support - **Metadata** — Provide metadata about commands and capabilities ## Prerequisites