Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions cli/azd/docs/extensions/extension-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -652,15 +652,15 @@ 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).

---

`pack` - Package your extension to prepare for publishing.

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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 16 additions & 10 deletions cli/azd/extensions/extension.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type buildFlags struct {
skipInstall bool
}

func newBuildCommand() *cobra.Command {
func newBuildCommand(outputPath *string) *cobra.Command {
flags := &buildFlags{}

buildCmd := &cobra.Command{
Expand All @@ -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 {
Expand All @@ -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.",
Expand Down
102 changes: 55 additions & 47 deletions cli/azd/extensions/microsoft.azd.extensions/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"text/template"

Expand All @@ -38,7 +39,7 @@ type initFlags struct {
namespace string
}

func newInitCommand() *cobra.Command {
func newInitCommand(noPrompt *bool) *cobra.Command {
flags := &initFlags{}

initCmd := &cobra.Command{
Expand All @@ -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
Expand Down Expand Up @@ -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", "",
Expand All @@ -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(
Expand Down Expand Up @@ -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, ", "),
)
}
}
Expand Down Expand Up @@ -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. " +
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type packageFlags struct {
rebuild bool
}

func newPackCommand() *cobra.Command {
func newPackCommand(outputPath *string) *cobra.Command {
flags := &packageFlags{}

packageCmd := &cobra.Command{
Expand All @@ -40,6 +40,9 @@ func newPackCommand() *cobra.Command {
"Packages the azd extension project and updates the registry",
)

if outputPath != nil && cmd.Flags().Changed("output") {
flags.outputPath = *outputPath
}
defaultPackageFlags(flags)
err := runPackageAction(cmd.Context(), flags)
if err != nil {
Expand All @@ -51,11 +54,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading