@@ -22,6 +22,7 @@ import (
2222 "github.com/azure/azure-dev/cli/azd/pkg/exec"
2323 "github.com/azure/azure-dev/cli/azd/pkg/input"
2424 "github.com/azure/azure-dev/cli/azd/pkg/tools/github"
25+ "github.com/azure/azure-dev/cli/azd/pkg/ux"
2526 "github.com/fatih/color"
2627 "github.com/spf13/cobra"
2728
@@ -32,6 +33,8 @@ type initFlags struct {
3233 rootFlagsDefinition
3334 template string
3435 projectResourceId string
36+ subscriptionId string
37+ projectEndpoint string
3538 jobId string
3639 src string
3740 env string
@@ -137,16 +140,22 @@ func newInitCommand(rootFlags rootFlagsDefinition) *cobra.Command {
137140 cmd .Flags ().StringVarP (& flags .template , "template" , "t" , "" ,
138141 "URL or path to a fine-tune job template" )
139142
140- cmd .Flags ().StringVarP (& flags .projectResourceId , "project" , "p" , "" ,
141- "Existing Microsoft Foundry Project Id to initialize your azd environment with " )
143+ cmd .Flags ().StringVarP (& flags .projectResourceId , "project-resource-id " , "p" , "" ,
144+ "ARM resource ID of the Microsoft Foundry Project (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project}) " )
142145
143- cmd .Flags ().StringVarP (& flags .src , "source" , "s" , "" ,
146+ cmd .Flags ().StringVarP (& flags .subscriptionId , "subscription" , "s" , "" ,
147+ "Azure subscription ID" )
148+
149+ cmd .Flags ().StringVarP (& flags .projectEndpoint , "project-endpoint" , "e" , "" ,
150+ "Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)" )
151+
152+ cmd .Flags ().StringVarP (& flags .src , "working-directory" , "w" , "" ,
144153 "Local path for project output" )
145154
146155 cmd .Flags ().StringVarP (& flags .jobId , "from-job" , "j" , "" ,
147156 "Clone configuration from an existing job ID" )
148157
149- cmd .Flags ().StringVarP (& flags .env , "environment" , "e " , "" , "The name of the azd environment to use." )
158+ cmd .Flags ().StringVarP (& flags .env , "environment" , "n " , "" , "The name of the azd environment to use." )
150159
151160 return cmd
152161}
@@ -181,6 +190,98 @@ func extractProjectDetails(projectResourceId string) (*FoundryProject, error) {
181190 }, nil
182191}
183192
193+ // parseProjectEndpoint extracts account name and project name from an endpoint URL
194+ // Example: https://account-name.services.ai.azure.com/api/projects/project-name
195+ func parseProjectEndpoint (endpoint string ) (accountName string , projectName string , err error ) {
196+ parsedURL , err := url .Parse (endpoint )
197+ if err != nil {
198+ return "" , "" , fmt .Errorf ("failed to parse endpoint URL: %w" , err )
199+ }
200+
201+ // Extract account name from hostname (e.g., "account-name.services.ai.azure.com")
202+ hostname := parsedURL .Hostname ()
203+ hostParts := strings .Split (hostname , "." )
204+ if len (hostParts ) < 1 || hostParts [0 ] == "" {
205+ return "" , "" , fmt .Errorf ("invalid endpoint URL: cannot extract account name from hostname" )
206+ }
207+ accountName = hostParts [0 ]
208+
209+ // Extract project name from path (e.g., "/api/projects/project-name")
210+ pathParts := strings .Split (strings .Trim (parsedURL .Path , "/" ), "/" )
211+ // Expected path: api/projects/{project-name}
212+ if len (pathParts ) >= 3 && pathParts [0 ] == "api" && pathParts [1 ] == "projects" {
213+ projectName = pathParts [2 ]
214+ } else {
215+ return "" , "" , fmt .Errorf ("invalid endpoint URL: cannot extract project name from path. Expected format: /api/projects/{project-name}" )
216+ }
217+
218+ return accountName , projectName , nil
219+ }
220+
221+ // findProjectByEndpoint searches for a Foundry project matching the endpoint URL
222+ func findProjectByEndpoint (
223+ ctx context.Context ,
224+ subscriptionId string ,
225+ accountName string ,
226+ projectName string ,
227+ credential azcore.TokenCredential ,
228+ ) (* FoundryProject , error ) {
229+ // Create Cognitive Services Accounts client to search for the account
230+ accountsClient , err := armcognitiveservices .NewAccountsClient (subscriptionId , credential , nil )
231+ if err != nil {
232+ return nil , fmt .Errorf ("failed to create Cognitive Services Accounts client: %w" , err )
233+ }
234+
235+ // List all accounts in the subscription and find the matching one
236+ pager := accountsClient .NewListPager (nil )
237+ var foundAccount * armcognitiveservices.Account
238+ for pager .More () {
239+ page , err := pager .NextPage (ctx )
240+ if err != nil {
241+ return nil , fmt .Errorf ("failed to list Cognitive Services accounts: %w" , err )
242+ }
243+ for _ , account := range page .Value {
244+ if account .Name != nil && strings .EqualFold (* account .Name , accountName ) {
245+ foundAccount = account
246+ break
247+ }
248+ }
249+ if foundAccount != nil {
250+ break
251+ }
252+ }
253+
254+ if foundAccount == nil {
255+ return nil , fmt .Errorf ("could not find Cognitive Services account '%s' in subscription '%s'" , accountName , subscriptionId )
256+ }
257+
258+ // Parse the account's resource ID to get resource group
259+ accountResourceId , err := arm .ParseResourceID (* foundAccount .ID )
260+ if err != nil {
261+ return nil , fmt .Errorf ("failed to parse account resource ID: %w" , err )
262+ }
263+
264+ // Create Projects client to verify the project exists
265+ projectsClient , err := armcognitiveservices .NewProjectsClient (subscriptionId , credential , nil )
266+ if err != nil {
267+ return nil , fmt .Errorf ("failed to create Cognitive Services Projects client: %w" , err )
268+ }
269+
270+ // Get the project to verify it exists and get its details
271+ projectResp , err := projectsClient .Get (ctx , accountResourceId .ResourceGroupName , accountName , projectName , nil )
272+ if err != nil {
273+ return nil , fmt .Errorf ("could not find project '%s' under account '%s': %w" , projectName , accountName , err )
274+ }
275+
276+ return & FoundryProject {
277+ SubscriptionId : subscriptionId ,
278+ ResourceGroupName : accountResourceId .ResourceGroupName ,
279+ AiAccountName : accountName ,
280+ AiProjectName : projectName ,
281+ Location : * projectResp .Location ,
282+ }, nil
283+ }
284+
184285func getExistingEnvironment (ctx context.Context , name * string , azdClient * azdext.AzdClient ) (* azdext.Environment , error ) {
185286 var env * azdext.Environment
186287 if name == nil || * name == "" {
@@ -205,8 +306,67 @@ func getExistingEnvironment(ctx context.Context, name *string, azdClient *azdext
205306func ensureEnvironment (ctx context.Context , flags * initFlags , azdClient * azdext.AzdClient ) (* azdext.Environment , error ) {
206307 var foundryProject * FoundryProject
207308
208- // Parse the Microsoft Foundry project resource ID if provided & Fetch Tenant Id and Location using parsed information
209- if flags .projectResourceId != "" {
309+ // Handle project endpoint URL - extract account/project names and find the ARM resource
310+ if flags .projectEndpoint != "" {
311+ accountName , projectName , err := parseProjectEndpoint (flags .projectEndpoint )
312+ if err != nil {
313+ return nil , fmt .Errorf ("failed to parse project endpoint: %w" , err )
314+ }
315+
316+ fmt .Printf ("Parsed endpoint - Account: %s, Project: %s\n " , accountName , projectName )
317+
318+ // Get subscription ID - either from flag or prompt
319+ subscriptionId := flags .subscriptionId
320+ var tenantId string
321+
322+ if subscriptionId == "" {
323+ fmt .Println ("Subscription ID is required to find the project. Let's select one." )
324+ subscriptionResponse , err := azdClient .Prompt ().PromptSubscription (ctx , & azdext.PromptSubscriptionRequest {})
325+ if err != nil {
326+ return nil , fmt .Errorf ("failed to prompt for subscription: %w" , err )
327+ }
328+ subscriptionId = subscriptionResponse .Subscription .Id
329+ tenantId = subscriptionResponse .Subscription .TenantId
330+ } else {
331+ // Get tenant ID from subscription
332+ tenantResponse , err := azdClient .Account ().LookupTenant (ctx , & azdext.LookupTenantRequest {
333+ SubscriptionId : subscriptionId ,
334+ })
335+ if err != nil {
336+ return nil , fmt .Errorf ("failed to get tenant ID: %w" , err )
337+ }
338+ tenantId = tenantResponse .TenantId
339+ }
340+
341+ // Create credential
342+ credential , err := azidentity .NewAzureDeveloperCLICredential (& azidentity.AzureDeveloperCLICredentialOptions {
343+ TenantID : tenantId ,
344+ AdditionallyAllowedTenants : []string {"*" },
345+ })
346+ if err != nil {
347+ return nil , fmt .Errorf ("failed to create Azure credential: %w" , err )
348+ }
349+
350+ // Find the project by searching the subscription
351+ spinner := ux .NewSpinner (& ux.SpinnerOptions {
352+ Text : fmt .Sprintf ("Searching for project in subscription %s..." , subscriptionId ),
353+ })
354+ if err := spinner .Start (ctx ); err != nil {
355+ fmt .Printf ("failed to start spinner: %v\n " , err )
356+ }
357+
358+ foundryProject , err = findProjectByEndpoint (ctx , subscriptionId , accountName , projectName , credential )
359+ _ = spinner .Stop (ctx )
360+ if err != nil {
361+ return nil , fmt .Errorf ("failed to find project from endpoint: %w" , err )
362+ }
363+ foundryProject .TenantId = tenantId
364+
365+ fmt .Printf ("Found project - Resource Group: %s, Account: %s, Project: %s\n " ,
366+ foundryProject .ResourceGroupName , foundryProject .AiAccountName , foundryProject .AiProjectName )
367+
368+ } else if flags .projectResourceId != "" {
369+ // Parse the Microsoft Foundry project resource ID if provided & Fetch Tenant Id and Location using parsed information
210370 var err error
211371 foundryProject , err = extractProjectDetails (flags .projectResourceId )
212372 if err != nil {
@@ -258,7 +418,7 @@ func ensureEnvironment(ctx context.Context, flags *initFlags, azdClient *azdext.
258418 envArgs = append (envArgs , flags .env )
259419 }
260420
261- if flags . projectResourceId != "" {
421+ if foundryProject != nil {
262422 envArgs = append (envArgs , "--subscription" , foundryProject .SubscriptionId )
263423 envArgs = append (envArgs , "--location" , foundryProject .Location )
264424 }
@@ -287,7 +447,7 @@ func ensureEnvironment(ctx context.Context, flags *initFlags, azdClient *azdext.
287447 }
288448
289449 // Set TenantId, SubscriptionId, ResourceGroupName, AiAccountName, and Location in the environment
290- if flags . projectResourceId != "" {
450+ if foundryProject != nil {
291451
292452 _ , err := azdClient .Environment ().SetValue (ctx , & azdext.SetEnvRequest {
293453 EnvName : existingEnv .Name ,
0 commit comments