From 841fe03b85c46e1137987701de2075e372549645 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 15 May 2026 13:16:32 -0400 Subject: [PATCH 01/19] feat(init): create project folder during initialization --- .../azure.ai.agents/internal/cmd/init.go | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 3441ca67ee4..253e5e55c24 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -530,9 +530,10 @@ func runInitFromManifest( flags *initFlags, azdClient *azdext.AzdClient, httpClient *http.Client, + targetDir string, ) error { // Ensure project and environment exist (no subscription/location prompting yet) - projectConfig, err := ensureProject(ctx, flags, azdClient) + projectConfig, err := ensureProject(ctx, flags, azdClient, targetDir) if err != nil { return err } @@ -669,6 +670,10 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, Timeout: 30 * time.Second, } + // Track whether a new project folder was created so we can + // print a follow-up cd hint at the end of the command. + createdFolder := "" + // Auto-detect an existing agent manifest in the target directory // when no --manifest flag was provided. // @@ -763,7 +768,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, return err } - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest(ctx, flags, azdClient, httpClient, "."); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -793,17 +798,19 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, switch selectedTemplate.EffectiveType() { case TemplateTypeAzd: // Full azd template - dispatch azd init -t - initArgs := []string{"init", "-t", selectedTemplate.Source, "."} + // Create project in a new subdirectory derived from the template title. + title := selectedTemplate.Title + if idx := strings.IndexByte(title, '('); idx >= 0 { + title = strings.TrimSpace(title[:idx]) + } + folderName := sanitizeAgentName(title) + initArgs := []string{"init", "-t", selectedTemplate.Source, folderName} if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - cwd, err := os.Getwd() - if err == nil { - sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd)) - initArgs = append( - initArgs, "--environment", sanitizedDirectoryName+"-dev", - ) - } + initArgs = append( + initArgs, "--environment", folderName+"-dev", + ) } workflow := &azdext.Workflow{ @@ -834,6 +841,16 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, selectedTemplate.Title, ) + // Sync the extension process into the new project directory. + // The azd host already chdir'd when it processed the init command. + if err := os.Chdir(folderName); err != nil { + return fmt.Errorf( + "changing to project directory %q: %w", + folderName, err, + ) + } + createdFolder = folderName + // Search for an agent manifest in the scaffolded project cwd, err := os.Getwd() if err != nil { @@ -847,7 +864,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if manifestPath != "" { flags.manifestPointer = manifestPath - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest(ctx, flags, azdClient, httpClient, "."); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -858,14 +875,23 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } default: - // Agent manifest template - use existing -m flow + // Agent manifest template - use existing -m flow. + // Create project in a new subdirectory derived from the template title. + title := selectedTemplate.Title + if idx := strings.IndexByte(title, '('); idx >= 0 { + title = strings.TrimSpace(title[:idx]) + } + folderName := sanitizeAgentName(title) flags.manifestPointer = selectedTemplate.Source - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest( + ctx, flags, azdClient, httpClient, folderName, + ); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } return err } + createdFolder = folderName } default: @@ -885,6 +911,10 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } } + if createdFolder != "" { + fmt.Printf("\nYour project has been created in ./%s\n", createdFolder) + } + return nil }, } @@ -1093,21 +1123,35 @@ func (a *InitAction) Run(ctx context.Context) error { return nil } -func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdClient) (*azdext.ProjectConfig, error) { +func ensureProject( + ctx context.Context, + flags *initFlags, + azdClient *azdext.AzdClient, + targetDir string, +) (*azdext.ProjectConfig, error) { projectResponse, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { fmt.Println("Let's get your project initialized.") // Environment creation is handled separately in ensureEnvironment - initArgs := []string{"init", "-t", "Azure-Samples/azd-ai-starter-basic", "."} + initArgs := []string{ + "init", "-t", "Azure-Samples/azd-ai-starter-basic", targetDir, + } if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - cwd, err := os.Getwd() - if err == nil { - sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd)) - initArgs = append(initArgs, "--environment", sanitizedDirectoryName+"-dev") + // Derive environment name from target folder + envBase := targetDir + if targetDir == "." { + cwd, cwdErr := os.Getwd() + if cwdErr == nil { + envBase = filepath.Base(cwd) + } } + sanitizedDirectoryName := sanitizeAgentName(envBase) + initArgs = append( + initArgs, "--environment", sanitizedDirectoryName+"-dev", + ) } // We don't have a project yet @@ -1134,6 +1178,17 @@ func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdC ) } + // Sync the extension process into the new project directory so that + // subsequent local file operations see the scaffolded project. + if targetDir != "." { + if chdirErr := os.Chdir(targetDir); chdirErr != nil { + return nil, fmt.Errorf( + "changing to project directory %q: %w", + targetDir, chdirErr, + ) + } + } + projectResponse, err = azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { return nil, exterrors.Dependency( From 377e3eaa981def012daacc2660917e1ee1e63e6a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 19 May 2026 12:14:49 -0400 Subject: [PATCH 02/19] The environment name needed to be sanitized to avoid running over 63 chars. --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 253e5e55c24..5ee2e8ddbb0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -808,8 +808,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { + defaultEnvName := sanitizeAgentName(title + "-dev") initArgs = append( - initArgs, "--environment", folderName+"-dev", + initArgs, "--environment", defaultEnvName, ) } @@ -1150,7 +1151,8 @@ func ensureProject( } sanitizedDirectoryName := sanitizeAgentName(envBase) initArgs = append( - initArgs, "--environment", sanitizedDirectoryName+"-dev", + sanitizedEnvName := sanitizeAgentName(envBase + "-dev") + initArgs, "--environment", sanitizedEnvName, ) } From 5c5286767a25c0a3772bf6ab60302410a0168575 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 19 May 2026 12:19:02 -0400 Subject: [PATCH 03/19] Capturing original working directory during project initialization and adjusting folder creation hints --- .../azure.ai.agents/internal/cmd/init.go | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 5ee2e8ddbb0..8481ff7a380 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -670,8 +670,15 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, Timeout: 30 * time.Second, } - // Track whether a new project folder was created so we can - // print a follow-up cd hint at the end of the command. + // Capture the original working directory so we can print an + // accurate cd hint after the process has chdir'd. + originalCwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + + // Track the absolute path of a newly created project folder so we + // can print a follow-up cd hint at the end of the command. createdFolder := "" // Auto-detect an existing agent manifest in the target directory @@ -850,7 +857,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName, err, ) } - createdFolder = folderName + createdFolder = filepath.Join(originalCwd, folderName) // Search for an agent manifest in the scaffolded project cwd, err := os.Getwd() @@ -883,6 +890,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, title = strings.TrimSpace(title[:idx]) } folderName := sanitizeAgentName(title) + // Check whether the target directory already exists so we + // only report "created" when a new directory was made. + _, dirExisted := os.Stat(folderName) flags.manifestPointer = selectedTemplate.Source if err := runInitFromManifest( ctx, flags, azdClient, httpClient, folderName, @@ -892,7 +902,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } return err } - createdFolder = folderName + if dirExisted != nil { + createdFolder = filepath.Join(originalCwd, folderName) + } } default: @@ -913,7 +925,12 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } if createdFolder != "" { - fmt.Printf("\nYour project has been created in ./%s\n", createdFolder) + // Print relative to where the user invoked the command. + relPath, relErr := filepath.Rel(originalCwd, createdFolder) + if relErr != nil { + relPath = createdFolder + } + fmt.Printf("\nYour project has been created in ./%s\n", relPath) } return nil From 87bf84880bcd54475f93d3b4ad4f86c40e50a762 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 19 May 2026 14:08:26 -0400 Subject: [PATCH 04/19] Refactor environment name sanitization and add tests for created folder path logic --- .../azure.ai.agents/internal/cmd/init.go | 7 +- .../azure.ai.agents/internal/cmd/init_test.go | 225 ++++++++++++++++++ 2 files changed, 227 insertions(+), 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 8481ff7a380..98892453c2f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -1166,11 +1166,8 @@ func ensureProject( envBase = filepath.Base(cwd) } } - sanitizedDirectoryName := sanitizeAgentName(envBase) - initArgs = append( - sanitizedEnvName := sanitizeAgentName(envBase + "-dev") - initArgs, "--environment", sanitizedEnvName, - ) + sanitizedEnvName := sanitizeAgentName(envBase + "-dev") + initArgs = append(initArgs, "--environment", sanitizedEnvName) } // We don't have a project yet diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index ee9a619f43a..381e52c5332 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2335,3 +2335,228 @@ func TestCodeDeployFlagValidation(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// createdFolder path computation after Chdir +// (covers PR review — directory creation tracking and message accuracy) +// --------------------------------------------------------------------------- + +// TestCreatedFolderPath_AfterChdir verifies that the createdFolder path logic +// produces the correct relative path for the user-facing message, even after +// the process has chdir'd into the new project directory. +func TestCreatedFolderPath_AfterChdir(t *testing.T) { + tests := []struct { + name string + folderName string + wantRelPath string + }{ + { + name: "simple folder name", + folderName: "my-agent", + wantRelPath: "my-agent", + }, + { + name: "sanitized folder name", + folderName: sanitizeAgentName("Hello World (Python)"), + wantRelPath: sanitizeAgentName("Hello World (Python)"), + }, + { + name: "folder with numbers", + folderName: "agent-v2", + wantRelPath: "agent-v2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + // Create the subdirectory (simulates azd init creating it) + folderPath := filepath.Join(originalCwd, tt.folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(folderPath, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + + // Simulate the chdir that happens after azd init + if err := os.Chdir(folderPath); err != nil { + t.Fatalf("Chdir: %v", err) + } + + // This mirrors the logic in the init command: + // createdFolder = filepath.Join(originalCwd, folderName) + createdFolder := filepath.Join(originalCwd, tt.folderName) + + // Verify that filepath.Rel produces the right path from originalCwd + relPath, err := filepath.Rel(originalCwd, createdFolder) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + + if relPath != tt.wantRelPath { + t.Errorf("relPath = %q, want %q", relPath, tt.wantRelPath) + } + + // Verify that ./ resolves to the created directory + // from the original cwd perspective. + resolvedAbs := filepath.Join(originalCwd, relPath) + if resolvedAbs != folderPath { + t.Errorf("resolved = %q, want %q", resolvedAbs, folderPath) + } + }) + } +} + +// TestCreatedFolderPath_NotSetWhenDirectoryExists verifies that the +// createdFolder variable is not set when the target directory already exists +// (i.e., ensureProject found an existing project). +func TestCreatedFolderPath_NotSetWhenDirectoryExists(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "existing-project" + + // Pre-create the directory (simulates an existing project) + existingDir := filepath.Join(originalCwd, folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(existingDir, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + + // This mirrors the logic in the init command's default template case: + // _, dirExisted := os.Stat(folderName) + _, dirExisted := os.Stat(folderName) + + createdFolder := "" + if dirExisted != nil { + createdFolder = filepath.Join(originalCwd, folderName) + } + + // Since the directory already existed, createdFolder should remain empty + if createdFolder != "" { + t.Errorf("createdFolder should be empty when dir exists, got %q", createdFolder) + } +} + +// TestCreatedFolderPath_SetWhenDirectoryDoesNotExist verifies that the +// createdFolder variable IS set when the target directory doesn't exist before +// runInitFromManifest is called. +func TestCreatedFolderPath_SetWhenDirectoryDoesNotExist(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "new-agent-project" + + // Do NOT create the directory — simulates fresh init + _, dirExisted := os.Stat(folderName) + + createdFolder := "" + if dirExisted != nil { + createdFolder = filepath.Join(originalCwd, folderName) + } + + // Since the directory did not exist, createdFolder should be set + wantPath := filepath.Join(originalCwd, folderName) + if createdFolder != wantPath { + t.Errorf("createdFolder = %q, want %q", createdFolder, wantPath) + } + + // Verify the relative path is correct for the output message + relPath, err := filepath.Rel(originalCwd, createdFolder) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + if relPath != folderName { + t.Errorf("relPath = %q, want %q", relPath, folderName) + } +} + +// TestCreatedFolderPath_AzdTemplateCase verifies the full flow for the +// TemplateTypeAzd case: azd init creates the folder, the process chdir's in, +// and the message correctly refers back to the original location. +func TestCreatedFolderPath_AzdTemplateCase(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + // Simulate deriving folder name from template title + templateTitle := "Basic Agent (Python)" + folderName := sanitizeAgentName(templateTitle) + + // Simulate azd init creating the directory + folderPath := filepath.Join(originalCwd, folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(folderPath, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + + // Simulate the chdir that happens after the workflow completes + if err := os.Chdir(folderPath); err != nil { + t.Fatalf("Chdir: %v", err) + } + + // The key line from init.go: + createdFolder := filepath.Join(originalCwd, folderName) + + // Now verify the message would print correctly + relPath, err := filepath.Rel(originalCwd, createdFolder) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + + // The message printed would be: "Your project has been created in ./" + expectedMsg := "\nYour project has been created in ./" + relPath + "\n" + wantMsg := "\nYour project has been created in ./" + folderName + "\n" + if expectedMsg != wantMsg { + t.Errorf("message = %q, want %q", expectedMsg, wantMsg) + } + + // Verify that from the user's shell (still in originalCwd), the path makes sense + expectedAbsPath := filepath.Join(originalCwd, relPath) + if expectedAbsPath != folderPath { + t.Errorf("absolute path = %q, want %q", expectedAbsPath, folderPath) + } +} + +// TestCreatedFolderPath_ManifestTemplateExistingProject verifies that no +// "created" message is produced when an existing project is found for the +// agent manifest template flow. +func TestCreatedFolderPath_ManifestTemplateExistingProject(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "my-agent" + + // Pre-create directory and azure.yaml to simulate existing project + projectDir := filepath.Join(originalCwd, folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(projectDir, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + //nolint:gosec // test fixture file permissions are intentional + if err := os.WriteFile( + filepath.Join(projectDir, "azure.yaml"), + []byte("name: my-agent\n"), + 0o644, + ); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + // Check before "runInitFromManifest" — directory exists + _, dirExisted := os.Stat(folderName) + + // Simulate runInitFromManifest finding the existing project and chdir'ing + if err := os.Chdir(projectDir); err != nil { + t.Fatalf("Chdir: %v", err) + } + + createdFolder := "" + if dirExisted != nil { + createdFolder = filepath.Join(originalCwd, folderName) + } + + // No message should be printed since the directory already existed + if createdFolder != "" { + t.Errorf("createdFolder should be empty for existing project, got %q", createdFolder) + } +} From a4525d814d9944ba85971923822ad48901fd5dc0 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 19 May 2026 15:43:18 -0400 Subject: [PATCH 05/19] Refactor folder name handling during project initialization to improve clarity and consistency --- .../azure.ai.agents/internal/cmd/init.go | 14 +++----------- .../azure.ai.agents/internal/cmd/init_from_code.go | 7 +++++++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 98892453c2f..ef0dc2723ca 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -806,16 +806,12 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, case TemplateTypeAzd: // Full azd template - dispatch azd init -t // Create project in a new subdirectory derived from the template title. - title := selectedTemplate.Title - if idx := strings.IndexByte(title, '('); idx >= 0 { - title = strings.TrimSpace(title[:idx]) - } - folderName := sanitizeAgentName(title) + folderName := sanitizeAgentName(selectedTemplate.Title) initArgs := []string{"init", "-t", selectedTemplate.Source, folderName} if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - defaultEnvName := sanitizeAgentName(title + "-dev") + defaultEnvName := sanitizeAgentName(selectedTemplate.Title + "-dev") initArgs = append( initArgs, "--environment", defaultEnvName, ) @@ -885,11 +881,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, default: // Agent manifest template - use existing -m flow. // Create project in a new subdirectory derived from the template title. - title := selectedTemplate.Title - if idx := strings.IndexByte(title, '('); idx >= 0 { - title = strings.TrimSpace(title[:idx]) - } - folderName := sanitizeAgentName(title) + folderName := folderNameFromTitle(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. _, dirExisted := os.Stat(folderName) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 266358d2928..4f8bf29be50 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -869,6 +869,13 @@ func sanitizeAgentName(name string) string { return name } +func folderNameFromTitle(title string) string { + if idx := strings.IndexByte(title, '('); idx >= 0 { + title = strings.TrimSpace(title[:idx]) + } + return sanitizeAgentName(title) +} + // normalizeForFuzzyMatch strips common separator characters (hyphens, dots, spaces, underscores) // and lowercases the string for fuzzy comparison. func normalizeForFuzzyMatch(s string) string { From 4637d82b3f6028ddea4c39c55f9eb2c4eecb30ed Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:37:04 -0400 Subject: [PATCH 06/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index 381e52c5332..bddf5f29683 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2380,9 +2380,7 @@ func TestCreatedFolderPath_AfterChdir(t *testing.T) { } // Simulate the chdir that happens after azd init - if err := os.Chdir(folderPath); err != nil { - t.Fatalf("Chdir: %v", err) - } + t.Chdir(folderPath) // This mirrors the logic in the init command: // createdFolder = filepath.Join(originalCwd, folderName) From 2ca5f2ad9b707b1e566294aa79b34b5c569fccee Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:27:57 -0400 Subject: [PATCH 07/19] Improve project creation message to display correct folder path --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index ef0dc2723ca..5f1e45ac801 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -918,11 +918,12 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if createdFolder != "" { // Print relative to where the user invoked the command. + displayPath := createdFolder relPath, relErr := filepath.Rel(originalCwd, createdFolder) if relErr != nil { - relPath = createdFolder + displayPath = "./" + relPath } - fmt.Printf("\nYour project has been created in ./%s\n", relPath) + fmt.Printf("\nYour project has been created in %s\n", displayPath) } return nil From bf1310992d121c07c744f0902681fb78e883b8bb Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:39:43 -0400 Subject: [PATCH 08/19] Check if target directory exists before reporting creation during project initialization --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 5f1e45ac801..d799a1cf36e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -807,6 +807,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, // Full azd template - dispatch azd init -t // Create project in a new subdirectory derived from the template title. folderName := sanitizeAgentName(selectedTemplate.Title) + // Check whether the target directory already exists so we + // only report "created" when a new directory was made. + _, dirExisted := os.Stat(folderName) initArgs := []string{"init", "-t", selectedTemplate.Source, folderName} if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) @@ -853,7 +856,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName, err, ) } - createdFolder = filepath.Join(originalCwd, folderName) + if dirExisted != nil { + createdFolder = filepath.Join(originalCwd, folderName) + } // Search for an agent manifest in the scaffolded project cwd, err := os.Getwd() From bd739c138b593e22b458bc2a967962cd04d17b5f Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:42:19 -0400 Subject: [PATCH 09/19] address wbreza #1 issue: 1. folderNameFromTitle is only used in one of the two template branches --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index d799a1cf36e..f1ba10a7ead 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -806,7 +806,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, case TemplateTypeAzd: // Full azd template - dispatch azd init -t // Create project in a new subdirectory derived from the template title. - folderName := sanitizeAgentName(selectedTemplate.Title) + folderName := folderNameFromTitle(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. _, dirExisted := os.Stat(folderName) From ac856e968e352721ca9a4b515b6908324e2a7e67 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:43:03 -0400 Subject: [PATCH 10/19] address wbreza comment 2. Env-name derivation is inconsistent between branches --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index f1ba10a7ead..f85f6cc470e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -814,7 +814,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - defaultEnvName := sanitizeAgentName(selectedTemplate.Title + "-dev") + defaultEnvName := sanitizeAgentName(folderName + "-dev") initArgs = append( initArgs, "--environment", defaultEnvName, ) From 4e9ca9183373ddd3e6f6f7b8eb320a299906dbf7 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:48:12 -0400 Subject: [PATCH 11/19] Address wbreza issue 3: Add notice for folder name discrepancies during project initialization --- .../azure.ai.agents/internal/cmd/init.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index f85f6cc470e..408b085f48e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -680,6 +680,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, // Track the absolute path of a newly created project folder so we // can print a follow-up cd hint at the end of the command. createdFolder := "" + // Original template title, used to surface a notice when the + // sanitized folder name differs significantly from what the user selected. + createdFromTitle := "" // Auto-detect an existing agent manifest in the target directory // when no --manifest flag was provided. @@ -858,6 +861,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } if dirExisted != nil { createdFolder = filepath.Join(originalCwd, folderName) + createdFromTitle = selectedTemplate.Title } // Search for an agent manifest in the scaffolded project @@ -901,6 +905,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } if dirExisted != nil { createdFolder = filepath.Join(originalCwd, folderName) + createdFromTitle = selectedTemplate.Title } } @@ -925,10 +930,20 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, // Print relative to where the user invoked the command. displayPath := createdFolder relPath, relErr := filepath.Rel(originalCwd, createdFolder) - if relErr != nil { + if relErr == nil { displayPath = "./" + relPath } fmt.Printf("\nYour project has been created in %s\n", displayPath) + + // Surface a notice when the folder name differs from the + // original template title (e.g. non-ASCII characters were + // stripped during sanitization). + if createdFromTitle != "" && filepath.Base(createdFolder) != createdFromTitle { + fmt.Printf( + " (folder name derived from template %q)\n", + createdFromTitle, + ) + } } return nil From 4ab73ad8ea313485cb73971c92945e6e7092eecd Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 18:51:01 -0400 Subject: [PATCH 12/19] wbreza issue 4 Fix path display for created folder to ensure consistent formatting across platforms --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 408b085f48e..b4b25004015 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -928,10 +928,10 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if createdFolder != "" { // Print relative to where the user invoked the command. + // Use ToSlash so the path is consistently forward-slash on all platforms. displayPath := createdFolder - relPath, relErr := filepath.Rel(originalCwd, createdFolder) - if relErr == nil { - displayPath = "./" + relPath + if relPath, relErr := filepath.Rel(originalCwd, createdFolder); relErr == nil { + displayPath = filepath.ToSlash(relPath) } fmt.Printf("\nYour project has been created in %s\n", displayPath) From 48518be200c10c6c855fa6ea32189f220b47f5f1 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 19:17:56 -0400 Subject: [PATCH 13/19] wbreza 4/5 Improve project creation message format and include folder navigation instructions --- .../extensions/azure.ai.agents/internal/cmd/init.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index b4b25004015..462f092b8cf 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -933,17 +933,14 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if relPath, relErr := filepath.Rel(originalCwd, createdFolder); relErr == nil { displayPath = filepath.ToSlash(relPath) } - fmt.Printf("\nYour project has been created in %s\n", displayPath) - // Surface a notice when the folder name differs from the - // original template title (e.g. non-ASCII characters were - // stripped during sanitization). + msg := fmt.Sprintf("\nYour project has been created in %s", displayPath) if createdFromTitle != "" && filepath.Base(createdFolder) != createdFromTitle { - fmt.Printf( - " (folder name derived from template %q)\n", - createdFromTitle, - ) + msg += fmt.Sprintf(" (from template %q)", createdFromTitle) } + msg += fmt.Sprintf("\n cd %s\n", displayPath) + + fmt.Print(msg) } return nil From f0f78c893ceab5c475180107799a20687871539a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 19:29:30 -0400 Subject: [PATCH 14/19] 6. Long titles can truncate -dev off the env name --- .../azure.ai.agents/internal/cmd/init.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 462f092b8cf..cdec0dd5692 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -817,7 +817,11 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - defaultEnvName := sanitizeAgentName(folderName + "-dev") + base := sanitizeAgentName(folderName) + if len(base) > 59 { + base = strings.TrimRight(base[:59], "-") + } + defaultEnvName := base + "-dev" initArgs = append( initArgs, "--environment", defaultEnvName, ) @@ -1176,8 +1180,12 @@ func ensureProject( envBase = filepath.Base(cwd) } } - sanitizedEnvName := sanitizeAgentName(envBase + "-dev") - initArgs = append(initArgs, "--environment", sanitizedEnvName) + base := sanitizeAgentName(envBase) + if len(base) > 59 { + base = strings.TrimRight(base[:59], "-") + } + envName := base + "-dev" + initArgs = append(initArgs, "--environment", envName) } // We don't have a project yet From c510ae3dc8b847a0bdb4873bab89eda3fd518554 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 19:38:21 -0400 Subject: [PATCH 15/19] Refactor folder creation logic and improve user-facing messages for project initialization --- .../azure.ai.agents/internal/cmd/init.go | 44 +++-- .../azure.ai.agents/internal/cmd/init_test.go | 171 ++++++------------ 2 files changed, 83 insertions(+), 132 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index cdec0dd5692..f7c80c33819 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "maps" "net/http" @@ -812,7 +813,8 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName := folderNameFromTitle(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. - _, dirExisted := os.Stat(folderName) + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) initArgs := []string{"init", "-t", selectedTemplate.Source, folderName} if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) @@ -863,7 +865,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName, err, ) } - if dirExisted != nil { + if newlyCreated { createdFolder = filepath.Join(originalCwd, folderName) createdFromTitle = selectedTemplate.Title } @@ -897,7 +899,8 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName := folderNameFromTitle(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. - _, dirExisted := os.Stat(folderName) + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) flags.manifestPointer = selectedTemplate.Source if err := runInitFromManifest( ctx, flags, azdClient, httpClient, folderName, @@ -907,7 +910,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } return err } - if dirExisted != nil { + if newlyCreated { createdFolder = filepath.Join(originalCwd, folderName) createdFromTitle = selectedTemplate.Title } @@ -931,20 +934,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } if createdFolder != "" { - // Print relative to where the user invoked the command. - // Use ToSlash so the path is consistently forward-slash on all platforms. - displayPath := createdFolder - if relPath, relErr := filepath.Rel(originalCwd, createdFolder); relErr == nil { - displayPath = filepath.ToSlash(relPath) - } - - msg := fmt.Sprintf("\nYour project has been created in %s", displayPath) - if createdFromTitle != "" && filepath.Base(createdFolder) != createdFromTitle { - msg += fmt.Sprintf(" (from template %q)", createdFromTitle) - } - msg += fmt.Sprintf("\n cd %s\n", displayPath) - - fmt.Print(msg) + fmt.Print(formatCreatedFolderMessage(originalCwd, createdFolder, createdFromTitle)) } return nil @@ -3205,3 +3195,21 @@ func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint, dep } return nil } + +// formatCreatedFolderMessage builds the user-facing message shown after a new +// project folder is created. It computes a cross-platform relative display path +// and optionally notes the original template title when the folder name differs. +func formatCreatedFolderMessage(originalCwd, createdFolder, createdFromTitle string) string { + displayPath := createdFolder + if relPath, err := filepath.Rel(originalCwd, createdFolder); err == nil { + displayPath = filepath.ToSlash(relPath) + } + + msg := fmt.Sprintf("\nYour project has been created in %s", displayPath) + if createdFromTitle != "" && filepath.Base(createdFolder) != createdFromTitle { + msg += fmt.Sprintf(" (from template %q)", createdFromTitle) + } + msg += fmt.Sprintf("\n cd %s\n", displayPath) + + return msg +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index bddf5f29683..44f10fcf2ab 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -6,6 +6,7 @@ package cmd import ( "context" "errors" + "io/fs" "net/http" "os" "path/filepath" @@ -2341,39 +2342,38 @@ func TestCodeDeployFlagValidation(t *testing.T) { // (covers PR review — directory creation tracking and message accuracy) // --------------------------------------------------------------------------- -// TestCreatedFolderPath_AfterChdir verifies that the createdFolder path logic -// produces the correct relative path for the user-facing message, even after -// the process has chdir'd into the new project directory. +// TestCreatedFolderPath_AfterChdir verifies that formatCreatedFolderMessage +// produces the correct relative display path even after the process has +// chdir'd into the new project directory. func TestCreatedFolderPath_AfterChdir(t *testing.T) { tests := []struct { - name string - folderName string - wantRelPath string + name string + folder string + wantPath string }{ { - name: "simple folder name", - folderName: "my-agent", - wantRelPath: "my-agent", + name: "simple folder name", + folder: "my-agent", + wantPath: "my-agent", }, { - name: "sanitized folder name", - folderName: sanitizeAgentName("Hello World (Python)"), - wantRelPath: sanitizeAgentName("Hello World (Python)"), + name: "sanitized folder name", + folder: folderNameFromTitle("Hello World (Python)"), + wantPath: "hello-world", }, { - name: "folder with numbers", - folderName: "agent-v2", - wantRelPath: "agent-v2", + name: "folder with numbers", + folder: "agent-v2", + wantPath: "agent-v2", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { originalCwd := t.TempDir() - t.Chdir(originalCwd) // Create the subdirectory (simulates azd init creating it) - folderPath := filepath.Join(originalCwd, tt.folderName) + folderPath := filepath.Join(originalCwd, tt.folder) //nolint:gosec // test fixture directory permissions are intentional if err := os.MkdirAll(folderPath, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) @@ -2382,33 +2382,20 @@ func TestCreatedFolderPath_AfterChdir(t *testing.T) { // Simulate the chdir that happens after azd init t.Chdir(folderPath) - // This mirrors the logic in the init command: - // createdFolder = filepath.Join(originalCwd, folderName) - createdFolder := filepath.Join(originalCwd, tt.folderName) - - // Verify that filepath.Rel produces the right path from originalCwd - relPath, err := filepath.Rel(originalCwd, createdFolder) - if err != nil { - t.Fatalf("filepath.Rel: %v", err) + msg := formatCreatedFolderMessage(originalCwd, folderPath, "") + wantSuffix := "cd " + tt.wantPath + "\n" + if !strings.Contains(msg, tt.wantPath) { + t.Errorf("message missing display path %q:\n%s", tt.wantPath, msg) } - - if relPath != tt.wantRelPath { - t.Errorf("relPath = %q, want %q", relPath, tt.wantRelPath) - } - - // Verify that ./ resolves to the created directory - // from the original cwd perspective. - resolvedAbs := filepath.Join(originalCwd, relPath) - if resolvedAbs != folderPath { - t.Errorf("resolved = %q, want %q", resolvedAbs, folderPath) + if !strings.HasSuffix(msg, wantSuffix) { + t.Errorf("message should end with %q, got:\n%s", wantSuffix, msg) } }) } } // TestCreatedFolderPath_NotSetWhenDirectoryExists verifies that the -// createdFolder variable is not set when the target directory already exists -// (i.e., ensureProject found an existing project). +// newlyCreated check correctly identifies an existing directory. func TestCreatedFolderPath_NotSetWhenDirectoryExists(t *testing.T) { originalCwd := t.TempDir() t.Chdir(originalCwd) @@ -2422,24 +2409,17 @@ func TestCreatedFolderPath_NotSetWhenDirectoryExists(t *testing.T) { t.Fatalf("MkdirAll: %v", err) } - // This mirrors the logic in the init command's default template case: - // _, dirExisted := os.Stat(folderName) - _, dirExisted := os.Stat(folderName) + // Mirror production logic: stat + errors.Is + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) - createdFolder := "" - if dirExisted != nil { - createdFolder = filepath.Join(originalCwd, folderName) - } - - // Since the directory already existed, createdFolder should remain empty - if createdFolder != "" { - t.Errorf("createdFolder should be empty when dir exists, got %q", createdFolder) + if newlyCreated { + t.Error("newlyCreated should be false when directory already exists") } } // TestCreatedFolderPath_SetWhenDirectoryDoesNotExist verifies that the -// createdFolder variable IS set when the target directory doesn't exist before -// runInitFromManifest is called. +// newlyCreated check correctly identifies a missing directory. func TestCreatedFolderPath_SetWhenDirectoryDoesNotExist(t *testing.T) { originalCwd := t.TempDir() t.Chdir(originalCwd) @@ -2447,72 +2427,45 @@ func TestCreatedFolderPath_SetWhenDirectoryDoesNotExist(t *testing.T) { folderName := "new-agent-project" // Do NOT create the directory — simulates fresh init - _, dirExisted := os.Stat(folderName) - - createdFolder := "" - if dirExisted != nil { - createdFolder = filepath.Join(originalCwd, folderName) - } + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) - // Since the directory did not exist, createdFolder should be set - wantPath := filepath.Join(originalCwd, folderName) - if createdFolder != wantPath { - t.Errorf("createdFolder = %q, want %q", createdFolder, wantPath) + if !newlyCreated { + t.Error("newlyCreated should be true when directory does not exist") } - // Verify the relative path is correct for the output message - relPath, err := filepath.Rel(originalCwd, createdFolder) - if err != nil { - t.Fatalf("filepath.Rel: %v", err) - } - if relPath != folderName { - t.Errorf("relPath = %q, want %q", relPath, folderName) + // Verify formatCreatedFolderMessage produces valid output + createdFolder := filepath.Join(originalCwd, folderName) + msg := formatCreatedFolderMessage(originalCwd, createdFolder, "") + if !strings.Contains(msg, folderName) { + t.Errorf("message should contain %q:\n%s", folderName, msg) } } // TestCreatedFolderPath_AzdTemplateCase verifies the full flow for the -// TemplateTypeAzd case: azd init creates the folder, the process chdir's in, -// and the message correctly refers back to the original location. +// TemplateTypeAzd case: folderNameFromTitle derives the name, and the message +// includes a template-title notice when the name changed. func TestCreatedFolderPath_AzdTemplateCase(t *testing.T) { originalCwd := t.TempDir() - t.Chdir(originalCwd) - // Simulate deriving folder name from template title templateTitle := "Basic Agent (Python)" - folderName := sanitizeAgentName(templateTitle) + folderName := folderNameFromTitle(templateTitle) - // Simulate azd init creating the directory - folderPath := filepath.Join(originalCwd, folderName) - //nolint:gosec // test fixture directory permissions are intentional - if err := os.MkdirAll(folderPath, 0o755); err != nil { - t.Fatalf("MkdirAll: %v", err) + // folderNameFromTitle should strip parenthetical suffix + if strings.Contains(folderName, "python") { + t.Errorf("folderName should not contain parenthetical suffix, got %q", folderName) } - // Simulate the chdir that happens after the workflow completes - if err := os.Chdir(folderPath); err != nil { - t.Fatalf("Chdir: %v", err) - } - - // The key line from init.go: createdFolder := filepath.Join(originalCwd, folderName) + msg := formatCreatedFolderMessage(originalCwd, createdFolder, templateTitle) - // Now verify the message would print correctly - relPath, err := filepath.Rel(originalCwd, createdFolder) - if err != nil { - t.Fatalf("filepath.Rel: %v", err) + // Should contain the template notice since name differs from title + if !strings.Contains(msg, templateTitle) { + t.Errorf("message should reference original title %q:\n%s", templateTitle, msg) } - - // The message printed would be: "Your project has been created in ./" - expectedMsg := "\nYour project has been created in ./" + relPath + "\n" - wantMsg := "\nYour project has been created in ./" + folderName + "\n" - if expectedMsg != wantMsg { - t.Errorf("message = %q, want %q", expectedMsg, wantMsg) - } - - // Verify that from the user's shell (still in originalCwd), the path makes sense - expectedAbsPath := filepath.Join(originalCwd, relPath) - if expectedAbsPath != folderPath { - t.Errorf("absolute path = %q, want %q", expectedAbsPath, folderPath) + // Should contain the cd hint + if !strings.Contains(msg, "cd "+folderName) { + t.Errorf("message should contain cd hint:\n%s", msg) } } @@ -2540,21 +2493,11 @@ func TestCreatedFolderPath_ManifestTemplateExistingProject(t *testing.T) { t.Fatalf("WriteFile: %v", err) } - // Check before "runInitFromManifest" — directory exists - _, dirExisted := os.Stat(folderName) - - // Simulate runInitFromManifest finding the existing project and chdir'ing - if err := os.Chdir(projectDir); err != nil { - t.Fatalf("Chdir: %v", err) - } - - createdFolder := "" - if dirExisted != nil { - createdFolder = filepath.Join(originalCwd, folderName) - } + // Mirror production logic: directory exists, so newlyCreated is false + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) - // No message should be printed since the directory already existed - if createdFolder != "" { - t.Errorf("createdFolder should be empty for existing project, got %q", createdFolder) + if newlyCreated { + t.Error("newlyCreated should be false for existing project directory") } } From 9fa8c62a6fe839d8972b1a17f06794bbc3c204f7 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 19:41:06 -0400 Subject: [PATCH 16/19] Add tests for folder name generation and non-ASCII character handling --- .../internal/cmd/init_from_code_test.go | 10 +++++++ .../azure.ai.agents/internal/cmd/init_test.go | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go index 84e6d151669..14629b0028c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go @@ -100,6 +100,16 @@ func TestSanitizeAgentName(t *testing.T) { input: "---", expected: "my-agent", }, + { + name: "non-ASCII characters stripped", + input: "Ünö Ägent", + expected: "n-gent", + }, + { + name: "all non-ASCII falls back to default", + input: "日本語エージェント", + expected: "my-agent", + }, } for _, tt := range tests { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index 44f10fcf2ab..2af048ebbec 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2501,3 +2501,31 @@ func TestCreatedFolderPath_ManifestTemplateExistingProject(t *testing.T) { t.Error("newlyCreated should be false for existing project directory") } } + +func TestFolderNameFromTitle(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + title string + want string + }{ + {name: "strips parenthetical suffix", title: "Basic Agent (Python)", want: "basic-agent"}, + {name: "no parenthetical", title: "My Cool Agent", want: "my-cool-agent"}, + {name: "parenthetical with spaces", title: "Agent ( Preview )", want: "agent"}, + {name: "non-ASCII title", title: "Ünö Agent (Test)", want: "n-agent"}, + {name: "all non-ASCII before paren", title: "日本語 (Python)", want: "my-agent"}, + {name: "empty title", title: "", want: "my-agent"}, + {name: "only parenthetical", title: "(Python)", want: "my-agent"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := folderNameFromTitle(tt.title) + if got != tt.want { + t.Errorf("folderNameFromTitle(%q) = %q, want %q", tt.title, got, tt.want) + } + }) + } +} From 74d3d3cbfe877faa2e01069d80d53ecaced71851 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 May 2026 19:42:11 -0400 Subject: [PATCH 17/19] Rename folderNameFromTitle to folderNameStrippingParenSuffix for clarity and update references in init and test files --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 4 ++-- .../azure.ai.agents/internal/cmd/init_from_code.go | 2 +- .../extensions/azure.ai.agents/internal/cmd/init_test.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index f7c80c33819..d2ef9234b7b 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -810,7 +810,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, case TemplateTypeAzd: // Full azd template - dispatch azd init -t // Create project in a new subdirectory derived from the template title. - folderName := folderNameFromTitle(selectedTemplate.Title) + folderName := folderNameStrippingParenSuffix(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. _, statErr := os.Stat(folderName) @@ -896,7 +896,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, default: // Agent manifest template - use existing -m flow. // Create project in a new subdirectory derived from the template title. - folderName := folderNameFromTitle(selectedTemplate.Title) + folderName := folderNameStrippingParenSuffix(selectedTemplate.Title) // Check whether the target directory already exists so we // only report "created" when a new directory was made. _, statErr := os.Stat(folderName) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 4f8bf29be50..c8cea56c18d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -869,7 +869,7 @@ func sanitizeAgentName(name string) string { return name } -func folderNameFromTitle(title string) string { +func folderNameStrippingParenSuffix(title string) string { if idx := strings.IndexByte(title, '('); idx >= 0 { title = strings.TrimSpace(title[:idx]) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index 2af048ebbec..273dbfad3da 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2358,7 +2358,7 @@ func TestCreatedFolderPath_AfterChdir(t *testing.T) { }, { name: "sanitized folder name", - folder: folderNameFromTitle("Hello World (Python)"), + folder: folderNameStrippingParenSuffix("Hello World (Python)"), wantPath: "hello-world", }, { @@ -2449,7 +2449,7 @@ func TestCreatedFolderPath_AzdTemplateCase(t *testing.T) { originalCwd := t.TempDir() templateTitle := "Basic Agent (Python)" - folderName := folderNameFromTitle(templateTitle) + folderName := folderNameStrippingParenSuffix(templateTitle) // folderNameFromTitle should strip parenthetical suffix if strings.Contains(folderName, "python") { @@ -2522,7 +2522,7 @@ func TestFolderNameFromTitle(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := folderNameFromTitle(tt.title) + got := folderNameStrippingParenSuffix(tt.title) if got != tt.want { t.Errorf("folderNameFromTitle(%q) = %q, want %q", tt.title, got, tt.want) } From ae97712871d62cde28feb662a86f707e90ecdf12 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 26 May 2026 17:31:29 -0400 Subject: [PATCH 18/19] Changing to use the nextsteps code for suggesting the cd command --- .gitignore | 1 + .../azure.ai.agents/internal/cmd/init.go | 71 +++++++++---------- .../internal/cmd/init_from_code_reuse.go | 2 +- .../internal/cmd/nextstep/resolver.go | 11 +++ .../internal/cmd/nextstep/resolver_test.go | 42 +++++++++++ .../internal/cmd/nextstep/state.go | 14 ++++ .../internal/cmd/nextstep/types.go | 6 ++ 7 files changed, 110 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 9f93e561951..afa2f898725 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ cli/azd/extensions/azure.coding-agent/azurecodingagent cli/azd/extensions/azure.coding-agent/azurecodingagent.exe cli/azd/extensions/azure.ai.agents/azureaiagent cli/azd/extensions/azure.ai.agents/azureaiagent.exe +cli/azd/extensions/azure.ai.agents/azure.ai.agents cli/azd/extensions/azure.ai.finetune/azure.ai.finetune cli/azd/extensions/azure.ai.finetune/azure.ai.finetune.exe cli/azd/extensions/microsoft.azd.extensions/microsoft.azd.extensions diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index d2ef9234b7b..5a609d5c124 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -93,6 +93,7 @@ type InitAction struct { isCodeDeploy bool // true when user selects code deploy mode; skips ACR config httpClient *http.Client serviceNameOverride string // when set, addToProject uses this instead of the manifest name + createdFolderDisplay string // pre-computed relative display path for the created folder } // modelSelector encapsulates the dependencies needed for model selection and @@ -532,6 +533,7 @@ func runInitFromManifest( azdClient *azdext.AzdClient, httpClient *http.Client, targetDir string, + createdFolderDisplay string, ) error { // Ensure project and environment exist (no subscription/location prompting yet) projectConfig, err := ensureProject(ctx, flags, azdClient, targetDir) @@ -584,14 +586,15 @@ func runInitFromManifest( ) action := &InitAction{ - azdClient: azdClient, - azureContext: azureContext, - console: console, - credential: credential, - projectConfig: projectConfig, - environment: env, - flags: flags, - httpClient: httpClient, + azdClient: azdClient, + azureContext: azureContext, + console: console, + credential: credential, + projectConfig: projectConfig, + environment: env, + flags: flags, + httpClient: httpClient, + createdFolderDisplay: createdFolderDisplay, } return action.Run(ctx) @@ -671,19 +674,10 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, Timeout: 30 * time.Second, } - // Capture the original working directory so we can print an - // accurate cd hint after the process has chdir'd. - originalCwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("getting current directory: %w", err) - } - - // Track the absolute path of a newly created project folder so we - // can print a follow-up cd hint at the end of the command. - createdFolder := "" - // Original template title, used to surface a notice when the - // sanitized folder name differs significantly from what the user selected. - createdFromTitle := "" + // Track whether a project already exists so the cd hint is + // only shown for brand-new top-level project folders, not + // when a template adds a subfolder to an existing project. + existingProject := fileExists("azure.yaml") // Auto-detect an existing agent manifest in the target directory // when no --manifest flag was provided. @@ -779,7 +773,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, return err } - if err := runInitFromManifest(ctx, flags, azdClient, httpClient, "."); err != nil { + if err := runInitFromManifest(ctx, flags, azdClient, httpClient, ".", ""); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -865,9 +859,12 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, folderName, err, ) } - if newlyCreated { - createdFolder = filepath.Join(originalCwd, folderName) - createdFromTitle = selectedTemplate.Title + // Compute display path for created folder (used in nextstep). + // Only show cd hint for brand-new projects, not when adding + // a template subfolder to an existing project. + var folderDisplay string + if newlyCreated && !existingProject { + folderDisplay = filepath.ToSlash(folderName) } // Search for an agent manifest in the scaffolded project @@ -883,7 +880,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if manifestPath != "" { flags.manifestPointer = manifestPath - if err := runInitFromManifest(ctx, flags, azdClient, httpClient, "."); err != nil { + if err := runInitFromManifest( + ctx, flags, azdClient, httpClient, ".", folderDisplay, + ); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -901,19 +900,19 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, // only report "created" when a new directory was made. _, statErr := os.Stat(folderName) newlyCreated := errors.Is(statErr, fs.ErrNotExist) + var folderDisplay string + if newlyCreated && !existingProject { + folderDisplay = filepath.ToSlash(folderName) + } flags.manifestPointer = selectedTemplate.Source if err := runInitFromManifest( - ctx, flags, azdClient, httpClient, folderName, + ctx, flags, azdClient, httpClient, folderName, folderDisplay, ); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } return err } - if newlyCreated { - createdFolder = filepath.Join(originalCwd, folderName) - createdFromTitle = selectedTemplate.Title - } } default: @@ -933,10 +932,6 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } } - if createdFolder != "" { - fmt.Print(formatCreatedFolderMessage(originalCwd, createdFolder, createdFromTitle)) - } - return nil }, } @@ -2287,7 +2282,11 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa // everything is configured. All paths append the deploy hint as the // trailing line. State-assembly errors are intentionally ignored: the // resolver degrades gracefully on partial state per the design spec. - state, _ := nextstep.AssembleState(ctx, a.azdClient) + var stateOpts []nextstep.Option + if a.createdFolderDisplay != "" { + stateOpts = append(stateOpts, nextstep.WithCreatedFolder(a.createdFolderDisplay)) + } + state, _ := nextstep.AssembleState(ctx, a.azdClient, stateOpts...) _ = printAllNextIfTerminal(os.Stdout, nextstep.ResolveAfterInit(state)) return nil } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go index 1694f25f0c7..d528889d09c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go @@ -85,7 +85,7 @@ func runReuseDefinition( displayPath, def.Name, )) - projectConfig, err := ensureProject(ctx, flags, azdClient) + projectConfig, err := ensureProject(ctx, flags, azdClient, ".") if err != nil { return err } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go index e571094b12e..b7274b19812 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go @@ -93,6 +93,17 @@ func ResolveAfterInit(state *State) []Suggestion { out := make([]Suggestion, 0, 4) priority := 5 + // When init created a new project folder, the user's shell is still + // in the original directory. A leading `cd` suggestion lets them + // navigate before running any subsequent commands. + if state.CreatedFolderDisplay != "" { + out = append(out, Suggestion{ + Command: fmt.Sprintf("cd %s", state.CreatedFolderDisplay), + Description: "enter your new project folder", + Priority: 0, + }) + } + // Placeholder fix-ups always come first when present: they are broken // state in agent.yaml itself and block both `run` and `deploy`. The // user has to edit agent.yaml (or define a matching parameter in diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go index 41972ff6b21..5c15a6e23f5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go @@ -181,6 +181,48 @@ func TestResolveAfterInit_NilState(t *testing.T) { assert.Nil(t, ResolveAfterInit(nil)) } +func TestResolveAfterInit_CreatedFolder(t *testing.T) { + t.Parallel() + + t.Run("cd suggestion prepended when folder was created", func(t *testing.T) { + t.Parallel() + state := &State{ + HasProjectEndpoint: true, + CreatedFolderDisplay: "my-agent", + } + out := ResolveAfterInit(state) + require.NotEmpty(t, out) + assert.Equal(t, "cd my-agent", out[0].Command) + assert.Equal(t, "enter your new project folder", out[0].Description) + assert.Equal(t, 0, out[0].Priority, "cd suggestion should have highest priority") + // Next primary is run, trailing is deploy + assert.Contains(t, out[1].Command, "azd ai agent run") + assert.Equal(t, "azd deploy", out[len(out)-1].Command) + }) + + t.Run("no cd suggestion when no folder created", func(t *testing.T) { + t.Parallel() + state := &State{HasProjectEndpoint: true} + out := ResolveAfterInit(state) + require.NotEmpty(t, out) + for _, s := range out { + assert.False(t, strings.HasPrefix(s.Command, "cd "), + "should not contain cd suggestion, got %q", s.Command) + } + }) + + t.Run("cd suggestion before provision when infra missing", func(t *testing.T) { + t.Parallel() + state := &State{ + CreatedFolderDisplay: "hello-world", + } + out := ResolveAfterInit(state) + require.True(t, len(out) >= 2) + assert.Equal(t, "cd hello-world", out[0].Command) + assert.Equal(t, "azd provision", out[1].Command) + }) +} + // TestResolveAfterInit_ManualVarsSingleEmitsEnrichedShape locks the // single-missing-manual-var case end-to-end. Three asserts: the env-set // line has the enriched "referenced by agent.yaml but not set in azd diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go index 49a05eeb092..79e2c4f5590 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go @@ -159,6 +159,11 @@ type config struct { // `azd ai agent run` to surface a fresh sample without making the // on-disk cache the source of truth. openAPILiveFetch func(context.Context) ([]byte, error) + + // createdFolderDisplay is a pre-computed relative display path for + // the folder created during init (e.g., "my-agent"). Empty when + // init did not create a new directory. + createdFolderDisplay string } // WithOpenAPIProbe enables a cache-only OpenAPI lookup for (agentName, suffix). @@ -186,6 +191,14 @@ func WithLiveOpenAPIProbe(fetch func(context.Context) ([]byte, error)) Option { return func(c *config) { c.openAPILiveFetch = fetch } } +// WithCreatedFolder passes a pre-computed display path for the folder +// created during init (e.g., "my-agent"). The resolver prepends a +// `cd ` suggestion when this is non-empty. The caller is +// responsible for computing the relative/slash-normalized path. +func WithCreatedFolder(displayPath string) Option { + return func(c *config) { c.createdFolderDisplay = displayPath } +} + // AssembleState builds a State snapshot for the current azd environment. // // All probes are best-effort: transport or parse errors are collected @@ -222,6 +235,7 @@ func assembleState(ctx context.Context, src Source, opts ...Option) (*State, []e } state := &State{} + state.CreatedFolderDisplay = cfg.createdFolderDisplay var errs []error envName, err := src.CurrentEnvName(ctx) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go index 1f6ddacc69e..fb6262c56c2 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go @@ -109,6 +109,12 @@ type State struct { // example. Empty when HasOpenAPI is false. OpenAPIPayload string + // CreatedFolderDisplay is a pre-computed, user-friendly relative path + // to the project folder created during init (e.g., "my-agent"). Empty + // when init did not create a new directory. The resolver uses it to + // prepend a `cd ` suggestion to the Next: block. + CreatedFolderDisplay string + // HasModels, HasToolboxes, HasConnections are aggregate flags // derived from each azure.ai.agent service's agent.manifest.yaml // (when present). They are true when at least one resource of the From dcf8ac280c4987b39963ff9a6b8a246756c975ea Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 26 May 2026 18:55:58 -0400 Subject: [PATCH 19/19] address comments from @trangevi --- .gitignore | 1 - .../azure.ai.agents/internal/cmd/init.go | 17 ++++++++++++----- .../internal/cmd/init_from_code.go | 7 ------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index afa2f898725..9f93e561951 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,6 @@ cli/azd/extensions/azure.coding-agent/azurecodingagent cli/azd/extensions/azure.coding-agent/azurecodingagent.exe cli/azd/extensions/azure.ai.agents/azureaiagent cli/azd/extensions/azure.ai.agents/azureaiagent.exe -cli/azd/extensions/azure.ai.agents/azure.ai.agents cli/azd/extensions/azure.ai.finetune/azure.ai.finetune cli/azd/extensions/azure.ai.finetune/azure.ai.finetune.exe cli/azd/extensions/microsoft.azd.extensions/microsoft.azd.extensions diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 5a609d5c124..ac2e26fc74c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -88,11 +88,11 @@ type InitAction struct { flags *initFlags models *modelSelector - deploymentDetails []project.Deployment - containerSettings *project.ContainerSettings - isCodeDeploy bool // true when user selects code deploy mode; skips ACR config - httpClient *http.Client - serviceNameOverride string // when set, addToProject uses this instead of the manifest name + deploymentDetails []project.Deployment + containerSettings *project.ContainerSettings + isCodeDeploy bool // true when user selects code deploy mode; skips ACR config + httpClient *http.Client + serviceNameOverride string // when set, addToProject uses this instead of the manifest name createdFolderDisplay string // pre-computed relative display path for the created folder } @@ -306,6 +306,13 @@ func setAgentNameOnTemplate(agentManifest *agent_yaml.AgentManifest, agentName s return nil } +func folderNameStrippingParenSuffix(title string) string { + if idx := strings.IndexByte(title, '('); idx >= 0 { + title = strings.TrimSpace(title[:idx]) + } + return sanitizeAgentName(title) +} + func updateAgentDefinition( template any, update func(*agent_yaml.AgentDefinition), diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index c8cea56c18d..266358d2928 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -869,13 +869,6 @@ func sanitizeAgentName(name string) string { return name } -func folderNameStrippingParenSuffix(title string) string { - if idx := strings.IndexByte(title, '('); idx >= 0 { - title = strings.TrimSpace(title[:idx]) - } - return sanitizeAgentName(title) -} - // normalizeForFuzzyMatch strips common separator characters (hyphens, dots, spaces, underscores) // and lowercases the string for fuzzy comparison. func normalizeForFuzzyMatch(s string) string {