@@ -68,9 +68,10 @@ type InitAction struct {
6868 flags * initFlags
6969 models * modelSelector
7070
71- deploymentDetails []project.Deployment
72- containerSettings * project.ContainerSettings
73- httpClient * http.Client
71+ deploymentDetails []project.Deployment
72+ containerSettings * project.ContainerSettings
73+ httpClient * http.Client
74+ serviceNameOverride string // when set, addToProject uses this instead of the manifest name
7475}
7576
7677// modelSelector encapsulates the dependencies needed for model selection and
@@ -574,6 +575,22 @@ func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdC
574575 }
575576
576577 fmt .Println ()
578+ } else if projectResponse .Project != nil {
579+ // An existing azd project was found — tell the user so the skipped template
580+ // download isn't a mystery. Also warn if the project lacks an infra/ directory,
581+ // since deployment may require infrastructure scaffolding.
582+ fmt .Println (output .WithGrayFormat (
583+ "Found existing azd project at %q. Adding agent to it." , projectResponse .Project .Path ,
584+ ))
585+
586+ infraDir := filepath .Join (projectResponse .Project .Path , "infra" )
587+ if _ , statErr := os .Stat (infraDir ); os .IsNotExist (statErr ) {
588+ fmt .Printf ("%s" , output .WithWarningFormat (
589+ "No infra/ directory found in the project. If you need Azure infrastructure " +
590+ "for deployment, run 'azd init -t Azure-Samples/azd-ai-starter-basic' in an empty " +
591+ "directory first, then re-run this command from there.\n " ,
592+ ))
593+ }
577594 }
578595
579596 if projectResponse .Project == nil {
@@ -630,8 +647,25 @@ func manifestHasModelResources(manifest *agent_yaml.AgentManifest) bool {
630647func (a * InitAction ) configureModelChoice (
631648 ctx context.Context , agentManifest * agent_yaml.AgentManifest ,
632649) (* agent_yaml.AgentManifest , error ) {
633- // If --project-id is provided, validate the ARM format and extract the subscription ID
634- // so ensureSubscription can skip the prompt and just resolve the tenant
650+ // When no --project-id flag was given, check whether the azd environment already
651+ // has a Foundry project configured from a previous init. If so, reuse it so the
652+ // user isn't prompted to select a project they already chose.
653+ if a .flags .projectResourceId == "" {
654+ if existing , err := a .azdClient .Environment ().GetValue (ctx , & azdext.GetEnvRequest {
655+ EnvName : a .environment .Name ,
656+ Key : "AZURE_AI_PROJECT_ID" ,
657+ }); err == nil && existing .Value != "" {
658+ a .flags .projectResourceId = existing .Value
659+ log .Printf ("Reusing existing Foundry project from environment: %s" , existing .Value )
660+ fmt .Println (output .WithGrayFormat (
661+ "Using Foundry project from environment: %s" , existing .Value ,
662+ ))
663+ }
664+ }
665+
666+ // If --project-id is provided (or reused from environment), validate the ARM
667+ // format and extract the subscription ID so ensureSubscription can skip the
668+ // prompt and just resolve the tenant.
635669 if a .flags .projectResourceId != "" {
636670 projectDetails , err := extractProjectDetails (a .flags .projectResourceId )
637671 if err != nil {
@@ -1213,12 +1247,28 @@ func (a *InitAction) downloadAgentYaml(
12131247 fmt .Println (output .WithGrayFormat ("✓ Manifest validated successfully" ))
12141248
12151249 agentId := agentManifest .Name
1250+ serviceName := strings .ReplaceAll (agentId , " " , "" )
12161251
12171252 // Use targetDir if provided, otherwise default to "src/{agentId}"
1218- if targetDir == "" {
1253+ autoDir := targetDir == ""
1254+ if autoDir {
12191255 targetDir = filepath .Join ("src" , agentId )
12201256 }
12211257
1258+ // When the target directory was auto-computed (no --src flag), check for
1259+ // collisions with an existing directory or an existing azure.yaml service.
1260+ // If a collision is found, prompt for a new service name (or auto-suffix
1261+ // in no-prompt mode).
1262+ if autoDir {
1263+ targetDir , serviceName , err = a .resolveCollisions (
1264+ ctx , agentId , targetDir , serviceName ,
1265+ )
1266+ if err != nil {
1267+ return nil , "" , err
1268+ }
1269+ }
1270+ a .serviceNameOverride = serviceName
1271+
12221272 // Safety checks for local container-based agents should happen before prompting for model SKU, etc.
12231273 if a .isLocalFilePath (manifestPointer ) {
12241274 if _ , isContainerAgent := agentManifest .Template .(agent_yaml.ContainerAgent ); isContainerAgent {
@@ -1396,7 +1446,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
13961446 }
13971447
13981448 serviceConfig := & azdext.ServiceConfig {
1399- Name : strings . ReplaceAll ( agentDef . Name , " " , "" ) ,
1449+ Name : a . serviceNameOverride ,
14001450 RelativePath : targetDir ,
14011451 Host : AiAgentHost ,
14021452 Language : "docker" ,
@@ -1416,18 +1466,172 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
14161466 return fmt .Errorf ("adding agent service to project: %w" , err )
14171467 }
14181468
1419- fmt .Printf ("\n Added your agent as a service entry named '%s' under the file azure.yaml.\n " , agentDef .Name )
1469+ fmt .Printf (
1470+ "\n Added your agent as a service entry named '%s' under the file azure.yaml.\n " ,
1471+ a .serviceNameOverride ,
1472+ )
14201473 if projectID , _ := a .azdClient .Environment ().GetValue (ctx , & azdext.GetEnvRequest {
14211474 EnvName : a .environment .Name ,
14221475 Key : "AZURE_AI_PROJECT_ID" ,
14231476 }); projectID != nil && projectID .Value != "" {
1424- fmt .Printf ("To deploy your agent, use %s.\n " , color .HiBlueString ("azd deploy %s" , agentDef .Name ))
1477+ fmt .Printf ("To deploy your agent, use %s.\n " ,
1478+ color .HiBlueString ("azd deploy %s" , a .serviceNameOverride ))
14251479 } else {
1426- fmt .Printf ("To provision and deploy the whole solution, use %s.\n " , color .HiBlueString ("azd up" ))
1480+ fmt .Printf (
1481+ "To provision and deploy the whole solution, use %s.\n " ,
1482+ color .HiBlueString ("azd up" ),
1483+ )
14271484 }
14281485 return nil
14291486}
14301487
1488+ // resolveCollisions checks whether the auto-computed target directory or
1489+ // service name already exist. When a collision is detected, the user is
1490+ // prompted for a new name (or a numeric suffix is appended in no-prompt
1491+ // mode). Returns the (possibly adjusted) targetDir and serviceName.
1492+ func (a * InitAction ) resolveCollisions (
1493+ ctx context.Context ,
1494+ agentId string ,
1495+ targetDir string ,
1496+ serviceName string ,
1497+ ) (string , string , error ) {
1498+ dirExists := fileExists (targetDir )
1499+
1500+ serviceExists := false
1501+ if a .projectConfig != nil {
1502+ for _ , svc := range a .projectConfig .Services {
1503+ if svc .Name == serviceName {
1504+ serviceExists = true
1505+ break
1506+ }
1507+ }
1508+ }
1509+
1510+ if ! dirExists && ! serviceExists {
1511+ return targetDir , serviceName , nil
1512+ }
1513+
1514+ // Find the next available name for use as the default suggestion
1515+ // (interactive) or the final answer (no-prompt).
1516+ suggestion , suggestionDir , suggestionSvc , err :=
1517+ a .nextAvailableName (agentId )
1518+ if err != nil {
1519+ return "" , "" , err
1520+ }
1521+
1522+ if a .flags .NoPrompt {
1523+ log .Printf (
1524+ "Collision on %q; using %q" , agentId , suggestion ,
1525+ )
1526+ return suggestionDir , suggestionSvc , nil
1527+ }
1528+
1529+ // Interactive mode: let the user choose.
1530+ choices := []* azdext.SelectChoice {
1531+ {
1532+ Label : "Overwrite existing" ,
1533+ Value : "overwrite" ,
1534+ },
1535+ {
1536+ Label : "Use a different service name" ,
1537+ Value : "rename" ,
1538+ },
1539+ }
1540+
1541+ defaultIdx := int32 (1 )
1542+ resp , err := a .azdClient .Prompt ().Select (ctx , & azdext.SelectRequest {
1543+ Options : & azdext.SelectOptions {
1544+ Message : fmt .Sprintf (
1545+ "An agent named '%s' already exists in your azure.yaml." +
1546+ " Overwrite it or use a different name?" ,
1547+ serviceName ,
1548+ ),
1549+ Choices : choices ,
1550+ SelectedIndex : & defaultIdx ,
1551+ },
1552+ })
1553+ if err != nil {
1554+ if exterrors .IsCancellation (err ) {
1555+ return "" , "" , exterrors .Cancelled (
1556+ "initialization was cancelled" ,
1557+ )
1558+ }
1559+ return "" , "" , fmt .Errorf (
1560+ "prompting for collision resolution: %w" , err ,
1561+ )
1562+ }
1563+
1564+ if choices [* resp .Value ].Value == "overwrite" {
1565+ return targetDir , serviceName , nil
1566+ }
1567+
1568+ // Prompt for a new name — default to the next available suffix.
1569+ nameResp , err := a .azdClient .Prompt ().Prompt (ctx , & azdext.PromptRequest {
1570+ Options : & azdext.PromptOptions {
1571+ Message : "Enter a new service name for this agent" ,
1572+ DefaultValue : suggestion ,
1573+ IgnoreHintKeys : true ,
1574+ },
1575+ })
1576+ if err != nil {
1577+ if exterrors .IsCancellation (err ) {
1578+ return "" , "" , exterrors .Cancelled (
1579+ "initialization was cancelled" ,
1580+ )
1581+ }
1582+ return "" , "" , fmt .Errorf (
1583+ "prompting for new service name: %w" , err ,
1584+ )
1585+ }
1586+
1587+ newName := strings .TrimSpace (nameResp .Value )
1588+ if newName == "" {
1589+ newName = suggestion
1590+ }
1591+
1592+ newDir := filepath .Join ("src" , newName )
1593+ newSvc := strings .ReplaceAll (newName , " " , "" )
1594+ return newDir , newSvc , nil
1595+ }
1596+
1597+ // nextAvailableName finds the next unused name by appending -2, -3, etc.
1598+ // Returns the candidate name, directory, and service name.
1599+ func (a * InitAction ) nextAvailableName (
1600+ agentId string ,
1601+ ) (string , string , string , error ) {
1602+ const maxAttempts = 100
1603+ for i := 2 ; i <= maxAttempts ; i ++ {
1604+ candidate := fmt .Sprintf ("%s-%d" , agentId , i )
1605+ candidateDir := filepath .Join ("src" , candidate )
1606+ candidateSvc := strings .ReplaceAll (candidate , " " , "" )
1607+
1608+ if fileExists (candidateDir ) {
1609+ continue
1610+ }
1611+
1612+ svcTaken := false
1613+ if a .projectConfig != nil {
1614+ for _ , svc := range a .projectConfig .Services {
1615+ if svc .Name == candidateSvc {
1616+ svcTaken = true
1617+ break
1618+ }
1619+ }
1620+ }
1621+ if svcTaken {
1622+ continue
1623+ }
1624+
1625+ return candidate , candidateDir , candidateSvc , nil
1626+ }
1627+
1628+ return "" , "" , "" , fmt .Errorf (
1629+ "could not find a unique name after %d attempts " +
1630+ "(tried %s-2 through %s-%d)" ,
1631+ maxAttempts - 1 , agentId , agentId , maxAttempts ,
1632+ )
1633+ }
1634+
14311635func (a * InitAction ) populateContainerSettings (
14321636 ctx context.Context ,
14331637 manifestResources * agent_yaml.ContainerResources ,
0 commit comments