-
Notifications
You must be signed in to change notification settings - Fork 292
Expand file tree
/
Copy pathauto_install.go
More file actions
752 lines (650 loc) · 27.4 KB
/
auto_install.go
File metadata and controls
752 lines (650 loc) · 27.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmd
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"slices"
"strconv"
"strings"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/runcontext/agentdetect"
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// extractFlagsWithValues extracts flags that take values from a cobra command.
// This ensures we have a single source of truth for flag definitions by
// dynamically inspecting the command's flag definitions instead of
// maintaining a separate hardcoded list.
//
// The function inspects both regular flags and persistent flags, checking
// the flag's value type to determine if it takes an argument:
// - Bool flags don't take values
// - String, Int, StringSlice, etc. flags do take values
func extractFlagsWithValues(cmd *cobra.Command) map[string]bool {
flagsWithValues := make(map[string]bool)
// Extract flags that take values from the command
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
// String, StringSlice, StringArray, Int, Int64, etc. all take values
// Bool flags don't take values
if flag.Value.Type() != "bool" {
flagsWithValues["--"+flag.Name] = true
if flag.Shorthand != "" {
flagsWithValues["-"+flag.Shorthand] = true
}
}
})
// Also check persistent flags (global flags)
// IMPORTANT: cmd.Flags().VisitAll() does NOT include persistent flags.
// In Cobra, cmd.Flags() only returns local flags specific to that command,
// while cmd.PersistentFlags() returns flags that are inherited by subcommands.
// These are separate flag sets, so we must call both VisitAll functions
// to capture all flags that can take values.
cmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
if flag.Value.Type() != "bool" {
flagsWithValues["--"+flag.Name] = true
if flag.Shorthand != "" {
flagsWithValues["-"+flag.Shorthand] = true
}
}
})
return flagsWithValues
}
// findFirstNonFlagArg finds the first argument that doesn't start with '-' and isn't a flag value.
// This function properly handles flags that take values (like --output json) to avoid
// incorrectly identifying flag values as commands.
// Returns the command and any unknown flags encountered before the command.
func findFirstNonFlagArg(args []string, flagsWithValues map[string]bool) (command string, unknownFlags []string) {
// Initialize as empty slice instead of nil for consistent behavior
unknownFlags = []string{}
skipNext := false
for i, arg := range args {
// Skip this argument if it's marked as a flag value from previous iteration
if skipNext {
skipNext = false
continue
}
// If it doesn't start with '-', it's a potential command
if !strings.HasPrefix(arg, "-") {
return arg, unknownFlags
}
// Check if this is a known flag that takes a value
if flagsWithValues[arg] {
// This flag takes a value, so skip the next argument
skipNext = true
continue
}
// Handle flags with '=' syntax like --output=json
if strings.Contains(arg, "=") {
parts := strings.SplitN(arg, "=", 2)
if flagsWithValues[parts[0]] {
// This is a known flag=value format, no need to skip next
continue
}
// Unknown flag with equals - record it
unknownFlags = append(unknownFlags, parts[0])
continue
}
// This is an unknown flag - record it
unknownFlags = append(unknownFlags, arg)
// Conservative heuristic: if the next argument doesn't start with '-'
// and there are more args after it, assume the unknown flag takes a value
if i+1 < len(args) && i+2 < len(args) {
nextArg := args[i+1]
argAfterNext := args[i+2]
if !strings.HasPrefix(nextArg, "-") && !strings.HasPrefix(argAfterNext, "-") {
// Pattern: --unknown value command
// Skip the value, let command be found next
skipNext = true
}
}
}
return "", unknownFlags
}
// checkForMatchingExtensions checks for extensions that match any possible namespace
// from the command arguments. For example, "azd foo demo bar" will check for
// extensions with namespaces: "foo", "foo.demo", "foo.demo.bar"
func checkForMatchingExtensions(
ctx context.Context, extensionManager *extensions.Manager, args []string) ([]*extensions.ExtensionMetadata, error) {
if len(args) == 0 {
return nil, nil
}
options := &extensions.FilterOptions{}
registryExtensions, err := extensionManager.FindExtensions(ctx, options)
if err != nil {
return nil, err
}
var matchingExtensions []*extensions.ExtensionMetadata
// Generate all possible namespace combinations from the command arguments
// For "azd something demo foo" -> check "something", "something.demo", "something.demo.foo"
for i := 1; i <= len(args); i++ {
candidateNamespace := strings.Join(args[:i], ".")
// Check if any extension has this exact namespace
for _, ext := range registryExtensions {
if ext.Namespace == candidateNamespace {
matchingExtensions = append(matchingExtensions, ext)
}
}
}
return matchingExtensions, nil
}
// promptForExtensionChoice prompts the user to choose from multiple matching extensions
func promptForExtensionChoice(
ctx context.Context,
console input.Console,
extensions []*extensions.ExtensionMetadata) (*extensions.ExtensionMetadata, error) {
if len(extensions) == 0 {
return nil, fmt.Errorf("no extensions to choose from")
}
if len(extensions) == 1 {
return extensions[0], nil
}
options := make([]string, len(extensions))
for i, ext := range extensions {
options[i] = fmt.Sprintf("%s (%s) - %s", ext.DisplayName, ext.Source, ext.Description)
}
choice, err := console.Select(ctx, input.ConsoleOptions{
Message: "Which extension would you like to install?",
Options: options,
})
if err != nil {
return nil, err
}
return extensions[choice], nil
}
// isBuiltInCommand checks if the given command is a built-in command by examining
// the root command's command tree. This includes both core azd commands and any
// installed extensions, preventing auto-install from triggering for known commands.
func isBuiltInCommand(rootCmd *cobra.Command, commandName string) bool {
if commandName == "" {
return false
}
// Check if the command exists in the root command's subcommands
for _, cmd := range rootCmd.Commands() {
if cmd.Name() == commandName {
return true
}
// Also check aliases
if slices.Contains(cmd.Aliases, commandName) {
return true
}
}
return false
}
// hasSubcommand checks if a command has a subcommand with the given name or alias.
func hasSubcommand(cmd *cobra.Command, name string) bool {
for _, sub := range cmd.Commands() {
if sub.Name() == name {
return true
}
if slices.Contains(sub.Aliases, name) {
return true
}
}
return false
}
// getCommandPath returns the command path from root to the given command (excluding root).
// For example, if foundCmd is "agent" under "ai", returns ["ai", "agent"].
func getCommandPath(cmd *cobra.Command) []string {
var path []string
for c := cmd; c != nil && c.Parent() != nil; c = c.Parent() {
path = append([]string{c.Name()}, path...)
}
return path
}
// buildNamespaceArgs builds the full namespace argument list by combining
// the found command's path with remaining non-flag arguments.
func buildNamespaceArgs(foundCmd *cobra.Command, remainingArgs []string) []string {
args := getCommandPath(foundCmd)
for _, arg := range remainingArgs {
if !strings.HasPrefix(arg, "-") {
args = append(args, arg)
}
}
return args
}
// tryAutoInstallForPartialNamespace checks if the found command is a partial namespace match
// and prompts for extension installation if an uninstalled extension matches.
// Returns true if an extension was installed, false otherwise.
//
// This handles the scenario where an extension like "ai.foo" is installed (creating the "ai"
// command group), but the user runs "azd ai bar init" where the "ai.bar" extension is not installed.
// Without this check, Cobra would find the "ai" command and show its help instead of prompting to install
// the "ai.bar" extension.
func tryAutoInstallForPartialNamespace(
ctx context.Context,
rootContainer *ioc.NestedContainer,
foundCmd *cobra.Command,
remainingArgs []string,
) bool {
if _, isExtensionCmd := foundCmd.Annotations["extension.id"]; isExtensionCmd {
// Extension commands handle their own args via DisableFlagParsing
return false
}
var firstRemainingArg string
for _, arg := range remainingArgs {
if !strings.HasPrefix(arg, "-") {
firstRemainingArg = arg
break
}
}
if firstRemainingArg == "" || hasSubcommand(foundCmd, firstRemainingArg) {
return false
}
argsForMatching := buildNamespaceArgs(foundCmd, remainingArgs)
if len(argsForMatching) == 0 {
return false
}
var extensionManager *extensions.Manager
var console input.Console
if err := rootContainer.Resolve(&extensionManager); err != nil {
log.Printf("failed to resolve extension manager: %v", err)
return false
}
if err := rootContainer.Resolve(&console); err != nil {
log.Printf("failed to resolve console: %v", err)
return false
}
extensionMatches, err := checkForMatchingExtensions(ctx, extensionManager, argsForMatching)
if err != nil {
log.Printf("failed to check for matching extensions: %v", err)
return false
}
if len(extensionMatches) == 0 {
return false
}
console.Message(ctx,
fmt.Sprintf("Command '%s' was not found, but there's an available extension that provides it\n",
strings.Join(argsForMatching, " ")))
chosenExtension, err := promptForExtensionChoice(ctx, console, extensionMatches)
if err != nil {
console.Message(ctx, fmt.Sprintf("Error selecting extension: %v", err))
return false
}
if chosenExtension == nil {
return false
}
installed, installErr := tryAutoInstallExtension(ctx, console, extensionManager, *chosenExtension)
if installErr != nil {
console.Message(ctx, installErr.Error())
return false
}
return installed
}
// tryAutoInstallExtension attempts to auto-install an extension if the unknown command matches an available
// extension namespace. Returns true if an extension was found and installed, false otherwise.
func tryAutoInstallExtension(
ctx context.Context,
console input.Console,
extensionManager *extensions.Manager,
extension extensions.ExtensionMetadata) (bool, error) {
// Check if the extension is already installed
_, err := extensionManager.GetInstalled(extensions.FilterOptions{
Id: extension.Id,
})
if err == nil {
return false, nil
}
// Return error if running in CI/CD environment
if resource.IsRunningOnCI() {
return false,
fmt.Errorf(
"Auto-installation is not supported in CI/CD environments.\n"+
"Run '%s' to install it manually.",
fmt.Sprintf("azd extension install %s", extension.Id))
}
console.MessageUxItem(ctx, &ux.WarningMessage{
Description: "You are about to install an extension!",
})
console.Message(ctx, fmt.Sprintf("Source: %s", extension.Source))
console.Message(ctx, fmt.Sprintf("Id: %s", extension.Id))
console.Message(ctx, fmt.Sprintf("Name: %s", extension.DisplayName))
console.Message(ctx, fmt.Sprintf("Description: %s", extension.Description))
// Ask user for permission to auto-install the extension
shouldInstall, err := console.Confirm(ctx, input.ConsoleOptions{
DefaultValue: true,
Message: "Confirm installation",
})
if err != nil {
return false, nil
}
if !shouldInstall {
return false, nil
}
// Install the extension
console.Message(ctx, fmt.Sprintf("Installing extension '%s'...\n", extension.Id))
_, err = extensionManager.Install(ctx, &extension, "")
if err != nil {
return false, fmt.Errorf("failed to install extension: %w", err)
}
console.Message(ctx, fmt.Sprintf("Extension '%s' installed successfully!\n", extension.Id))
return true, nil
}
// ExecuteWithAutoInstall executes the command and handles auto-installation of extensions for unknown commands.
func ExecuteWithAutoInstall(ctx context.Context, rootContainer *ioc.NestedContainer) error {
// Parse global flags BEFORE creating the command tree.
// This allows us to access flag values (like --no-prompt, --debug) early for auto-install logic.
// This also enables the global options to be set in the container for support during extension framework callbacks.
globalOpts := &internal.GlobalCommandOptions{}
if err := ParseGlobalFlags(os.Args[1:], globalOpts); err != nil {
return fmt.Errorf("Warning: failed to parse global flags: %w", err)
}
// Register GlobalCommandOptions as a singleton in the container BEFORE building the command tree.
// This ensures all components (FlagsResolver, actions, etc.) get the same pre-parsed instance.
ioc.RegisterInstance(rootContainer, globalOpts)
// Creating the RootCmd takes care of registering common dependencies in rootContainer.
// The command tree will retrieve globalOpts from the container via its FlagsResolver.
rootCmd := NewRootCmd(false, nil, rootContainer)
var extensionManager *extensions.Manager
var console input.Console
// rootCmd.Find() returns error if the command is not identified. Cobra checks all the registered commands
// and returns error if the input command is not registered.
// This allows us to determine if a subcommand was provided or not or if the command is unknown.
foundCmd, originalArgs, err := rootCmd.Find(os.Args[1:])
if err == nil {
// Check for partial namespace match (e.g., "ai" found but "ai.agent" not installed)
if installed := tryAutoInstallForPartialNamespace(
ctx, rootContainer, foundCmd, originalArgs,
); installed {
// Extension was installed, rebuild command tree and execute
rootCmd = NewRootCmd(false, nil, rootContainer)
return rootCmd.ExecuteContext(ctx)
}
// Known command, proceed with normal execution
err := rootCmd.ExecuteContext(ctx)
// Only attempt service-host auto-install when the command failed with that specific error.
// Other command errors (for example, unsupported output formats) should be returned directly.
unsupportedErr, ok := errors.AsType[*project.UnsupportedServiceHostError](err)
if !ok {
return err
}
if err := rootContainer.Resolve(&extensionManager); err != nil {
log.Panic("failed to resolve extension manager for auto-install:", err)
}
if err := rootContainer.Resolve(&console); err != nil {
log.Panic("failed to resolve console for unknown flags error:", err)
}
requiredHost := unsupportedErr.Host
availableExtensionsForHost, err := extensionManager.FindExtensions(ctx, &extensions.FilterOptions{
Capability: extensions.ServiceTargetProviderCapability,
Provider: requiredHost,
})
if err != nil {
// Do not fail if we couldn't check for extensions - just proceed to normal execution
log.Println("Error: check for extensions. Skipping auto-install:", err)
console.Message(ctx, unsupportedErr.ErrorMessage)
return nil
}
// Note: We don't need to filter or check which extensions are installed.
// If any of these extensions would be installed, the auto-install wouldn't have been triggered because
// there would be at least one extensions providing the capability and provider.
if len(availableExtensionsForHost) == 0 {
// did not find an extension with the capability, just print the original error message
console.Message(ctx, unsupportedErr.ErrorMessage)
return nil
}
console.Message(ctx,
fmt.Sprintf("Your project is using host '%s' which is not supported by default.\n", unsupportedErr.Host))
var extensionIdToInstall extensions.ExtensionMetadata
if len(availableExtensionsForHost) == 1 {
extensionIdToInstall = *availableExtensionsForHost[0]
console.Message(ctx, "An extension was found that provides support for this host.")
} else {
console.Message(ctx, "There are multiple extensions that provide support for this host.")
// Multiple matches found, prompt user to choose
chosenExtension, err := promptForExtensionChoice(ctx, console, availableExtensionsForHost)
if err != nil {
console.Message(ctx, fmt.Sprintf("Error selecting extension: %v", err))
return err
}
extensionIdToInstall = *chosenExtension
}
installed, installErr := tryAutoInstallExtension(ctx, console, extensionManager, extensionIdToInstall)
if installErr != nil {
// Error needs to be printed here or else it will be hidden b/c the error printing is handled inside runtime
console.Message(ctx, installErr.Error())
return installErr
}
if installed {
// Extension was installed, build command tree and execute
rootCmd := NewRootCmd(false, nil, rootContainer)
return rootCmd.ExecuteContext(ctx)
}
return err
}
// Extract flags that take values from the root command
flagsWithValues := extractFlagsWithValues(rootCmd)
// Find the first non-flag argument (the actual command) and check for unknown flags
unknownCommand, unknownFlags := findFirstNonFlagArg(originalArgs, flagsWithValues)
// If we have a command, check if it's a built-in command first
if unknownCommand != "" {
// Check if this is a built-in command first (includes core commands and installed extensions)
if isBuiltInCommand(rootCmd, unknownCommand) {
// This is a built-in command, proceed with normal execution without checking for extensions
return rootCmd.ExecuteContext(ctx)
}
if err := rootContainer.Resolve(&extensionManager); err != nil {
log.Panic("failed to resolve extension manager for auto-install:", err)
}
if err := rootContainer.Resolve(&console); err != nil {
log.Panic("failed to resolve console for unknown flags error:", err)
}
// Check for deprecated commands and provide helpful redirection messages
if unknownCommand == "login" {
console.Message(ctx, "Error: The 'azd login' command has been removed.")
console.Message(ctx, "Please use 'azd auth login' instead.")
return fmt.Errorf("unknown command 'login'")
}
if unknownCommand == "logout" {
console.Message(ctx, "Error: The 'azd logout' command has been removed.")
console.Message(ctx, "Please use 'azd auth logout' instead.")
return fmt.Errorf("unknown command 'logout'")
}
// If unknown flags were found before a non-built-in command, return an error with helpful guidance
if len(unknownFlags) > 0 {
flagsList := strings.Join(unknownFlags, ", ")
errorMsg := fmt.Sprintf(
"Unknown flags detected before command '%s': %s\n\n"+
"If you're trying to run an extension command, the extension name must come BEFORE any flags.\n"+
"This is because extension-specific flags are not known until the extension is installed.\n\n"+
"Correct usage:\n"+
" azd %s %s # Extension name first, then flags\n"+
" azd %s --help # Get help for the extension\n\n"+
"If this is not an extension command, please check the flag names for typos.",
unknownCommand, flagsList,
unknownCommand, strings.Join(unknownFlags, " "),
unknownCommand)
console.Message(ctx, errorMsg)
return fmt.Errorf("unknown flags before command: %s", flagsList)
}
// Get all remaining arguments starting from the command for namespace matching
// This allows checking longer namespaces like "something.demo.foo" from "azd something demo foo"
var argsForMatching []string
for i, arg := range originalArgs {
if !strings.HasPrefix(arg, "-") && arg == unknownCommand {
// Found the command, collect all non-flag arguments from here
for j := i; j < len(originalArgs); j++ {
if !strings.HasPrefix(originalArgs[j], "-") {
argsForMatching = append(argsForMatching, originalArgs[j])
}
}
break
}
}
// Check if any commands might match extensions with various namespace lengths
extensionMatches, err := checkForMatchingExtensions(ctx, extensionManager, argsForMatching)
if err != nil {
// Do not fail if we couldn't check for extensions - just proceed to normal execution
log.Println("Error: check for extensions. Skipping auto-install:", err)
return rootCmd.ExecuteContext(ctx)
}
if len(extensionMatches) > 0 {
var console input.Console
if err := rootContainer.Resolve(&console); err != nil {
log.Panic("failed to resolve console for auto-install:", err)
}
console.Message(ctx,
fmt.Sprintf("Command '%s' was not found, but there's an available extension that provides it\n",
strings.Join(argsForMatching, " ")))
// Prompt user to choose if multiple extensions match
chosenExtension, err := promptForExtensionChoice(ctx, console, extensionMatches)
if err != nil {
console.Message(ctx, fmt.Sprintf("Error selecting extension: %v", err))
return rootCmd.ExecuteContext(ctx)
}
if chosenExtension == nil {
// User cancelled selection, proceed to normal execution
return rootCmd.ExecuteContext(ctx)
}
// Try to auto-install the chosen extension
installed, installErr := tryAutoInstallExtension(ctx, console, extensionManager, *chosenExtension)
if installErr != nil {
// Error needs to be printed here or else it will be hidden b/c the error printing is handled inside runtime
console.Message(ctx, installErr.Error())
return installErr
}
if installed {
// Extension was installed, build command tree and execute
rootCmd := NewRootCmd(false, nil, rootContainer)
return rootCmd.ExecuteContext(ctx)
}
}
}
// Normal execution path - either no args, no matching extension, or user declined install
return rootCmd.ExecuteContext(ctx)
}
// CreateGlobalFlagSet creates a new flag set with all global flags defined.
// This is the single source of truth for global flag definitions.
func CreateGlobalFlagSet() *pflag.FlagSet {
globalFlags := pflag.NewFlagSet("global", pflag.ContinueOnError)
globalFlags.StringP("cwd", "C", "", "Sets the current working directory.")
globalFlags.Bool("debug", false, "Enables debugging and diagnostics logging.")
globalFlags.Bool(
"no-prompt",
false,
"Accepts the default value instead of prompting, or it fails if there is no default.")
globalFlags.Bool(
"non-interactive",
false,
"Alias for --no-prompt.")
_ = globalFlags.MarkHidden("non-interactive")
globalFlags.Bool(
"fail-on-prompt",
false,
"Fails with an actionable error whenever a prompt is encountered, even if a default exists."+
" Implies --no-prompt.")
globalFlags.StringP(internal.EnvironmentNameFlagName, "e", "", "The name of the environment to use.")
// The telemetry system is responsible for reading these flags value and using it to configure the telemetry
// system, but we still need to add it to our flag set so that when we parse the command line with Cobra we
// don't error due to an "unknown flag".
globalFlags.String("trace-log-file", "", "Write a diagnostics trace to a file.")
_ = globalFlags.MarkHidden("trace-log-file")
globalFlags.String("trace-log-url", "", "Send traces to an Open Telemetry compatible endpoint.")
_ = globalFlags.MarkHidden("trace-log-url")
return globalFlags
}
// ParseGlobalFlags parses global flags from the provided arguments and populates the GlobalCommandOptions.
// Uses ParseErrorsAllowlist to gracefully ignore unknown flags (like extension-specific flags).
// This function is designed to be called BEFORE Cobra command tree construction to enable
// early access to global flag values for auto-install and other pre-execution logic.
//
// Agent Detection: If --no-prompt is not explicitly set and an AI coding agent (like Claude Code,
// GitHub Copilot CLI, Cursor, etc.) is detected as the caller, NoPrompt is automatically enabled.
func ParseGlobalFlags(args []string, opts *internal.GlobalCommandOptions) error {
globalFlagSet := CreateGlobalFlagSet()
// Set output to io.Discard to suppress any error messages from pflag
// Cobra will handle all user-facing output
globalFlagSet.SetOutput(io.Discard)
// Configure the flag set to ignore unknown flags. This is critical for extension commands
// where extension-specific flags are not yet known and will be handled by the extension's
// command parser after the extension is loaded.
globalFlagSet.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true}
// Parse the arguments - unknown flags will be silently ignored
err := globalFlagSet.Parse(args)
// Ignore help errors - let Cobra handle help requests
if err != nil && !errors.Is(err, pflag.ErrHelp) {
return fmt.Errorf("failed to parse global flags: %w", err)
}
// Bind parsed values to the options struct
if strVal, err := globalFlagSet.GetString("cwd"); err == nil {
opts.Cwd = strVal
}
if boolVal, err := globalFlagSet.GetBool("debug"); err == nil {
opts.EnableDebugLogging = boolVal
}
// --non-interactive is an alias for --no-prompt; either flag sets NoPrompt.
// When both are present, true wins (either flag opting in is sufficient).
noPromptVal, _ := globalFlagSet.GetBool("no-prompt")
nonInteractiveVal, _ := globalFlagSet.GetBool("non-interactive")
opts.NoPrompt = noPromptVal || nonInteractiveVal
// Check if either flag was explicitly provided on the command line
noPromptFlag := globalFlagSet.Lookup("no-prompt")
nonInteractiveFlag := globalFlagSet.Lookup("non-interactive")
flagExplicitlySet := (noPromptFlag != nil && noPromptFlag.Changed) ||
(nonInteractiveFlag != nil && nonInteractiveFlag.Changed)
// Environment variable: AZD_NON_INTERACTIVE enables no-prompt mode when set to a
// truthy value (parsed via strconv.ParseBool: "true", "1", "TRUE", etc.).
// Explicit flags take precedence over this env var.
// When this env var is present (regardless of value), it also suppresses
// agent auto-detection since the user has made an explicit choice.
envVarPresent := false
if !flagExplicitlySet {
if envVal, ok := os.LookupEnv("AZD_NON_INTERACTIVE"); ok {
envVarPresent = true
if parsed, err := strconv.ParseBool(envVal); err == nil && parsed {
opts.NoPrompt = true
} else if err != nil {
log.Printf(
"warning: AZD_NON_INTERACTIVE=%q is not a valid boolean"+
" (expected true/false/1/0), ignoring",
envVal,
)
}
}
}
// Parse -e/--environment with lenient validation.
// Only accept values that look like valid environment names (alphanumeric, hyphens, dots,
// underscores). Values that don't match (e.g., URLs from extensions reusing -e for
// --project-endpoint) are silently ignored — the extension still receives the raw args
// and can parse -e itself. This avoids breaking third-party extensions that use -e
// for their own flags while still fixing the environment leak for valid env names.
if strVal, err := globalFlagSet.GetString(internal.EnvironmentNameFlagName); err == nil && strVal != "" {
if environment.IsValidEnvironmentName(strVal) {
opts.EnvironmentName = strVal
} else if opts.EnableDebugLogging {
log.Printf(
"debug: ignoring invalid environment name %q from -e/--environment flag"+
" (does not match %s pattern)",
strVal, environment.EnvironmentNameRegexp,
)
}
}
if boolVal, err := globalFlagSet.GetBool("fail-on-prompt"); err == nil {
opts.FailOnPrompt = boolVal
if boolVal {
// --fail-on-prompt implies --no-prompt
opts.NoPrompt = true
}
}
// Agent Detection: If --no-prompt was not explicitly set and we detect an AI coding agent
// as the caller, automatically enable fail-on-prompt mode for strict non-interactive execution.
failOnPromptFlag := globalFlagSet.Lookup("fail-on-prompt")
failOnPromptExplicitlySet := failOnPromptFlag != nil && failOnPromptFlag.Changed
if !flagExplicitlySet && !failOnPromptExplicitlySet &&
!envVarPresent && agentdetect.IsRunningInAgent() {
opts.NoPrompt = true
opts.FailOnPrompt = true
}
return nil
}