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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/src/cmd/app/commands/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func listAvailableServices() error {
sort.Strings(names)

if cliout.IsJSON() {
type ServiceInfo struct {
type AvailableServiceInfo struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Expand All @@ -164,10 +164,10 @@ func listAvailableServices() error {
Ports []string `json:"ports"`
ConnStrings map[string]string `json:"connectionStrings"`
}
services := make([]ServiceInfo, 0, len(names))
services := make([]AvailableServiceInfo, 0, len(names))
for _, name := range names {
def := wellknown.Get(name)
services = append(services, ServiceInfo{
services = append(services, AvailableServiceInfo{
Name: def.Name,
DisplayName: def.DisplayName,
Description: def.Description,
Expand Down
75 changes: 31 additions & 44 deletions cli/src/cmd/app/commands/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,37 @@ import (
"github.com/jongio/azd-core/cliout"
)

// Global orchestrator instance shared across all commands.
var cmdOrchestrator *orchestrator.Orchestrator
func newCommandOrchestrator() *orchestrator.Orchestrator {
cmdOrchestrator := orchestrator.NewOrchestrator()

register := func(command *orchestrator.Command) {
if err := cmdOrchestrator.Register(command); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to register %s command: %v\n", command.Name, err)
}
}

register(&orchestrator.Command{
Name: "reqs",
Execute: executeReqs,
})
register(&orchestrator.Command{
Name: "deps",
Dependencies: []string{"reqs"},
Execute: executeDeps,
})
register(&orchestrator.Command{
Name: "run",
Dependencies: []string{"deps"},
Execute: executeRun,
})
register(&orchestrator.Command{
Name: "test",
Dependencies: []string{"deps"},
Execute: executeTest,
})

return cmdOrchestrator
}

// ExecutionContext holds runtime configuration for command execution.
type ExecutionContext struct {
Expand Down Expand Up @@ -65,48 +94,6 @@ func SetCacheEnabled(enabled bool) {
execContext.CacheEnabled = enabled
}

// init initializes the command orchestrator and registers all commands.
func init() {
cmdOrchestrator = orchestrator.NewOrchestrator()

// Register commands with their dependencies
// reqs has no dependencies
if err := cmdOrchestrator.Register(&orchestrator.Command{
Name: "reqs",
Execute: executeReqs,
}); err != nil {
// Log error but don't exit - let the app handle it gracefully
fmt.Fprintf(os.Stderr, "Warning: Failed to register reqs command: %v\n", err)
}

// deps depends on reqs
if err := cmdOrchestrator.Register(&orchestrator.Command{
Name: "deps",
Dependencies: []string{"reqs"},
Execute: executeDeps,
}); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to register deps command: %v\n", err)
}

// run depends on deps (which depends on reqs)
if err := cmdOrchestrator.Register(&orchestrator.Command{
Name: "run",
Dependencies: []string{"deps"},
Execute: executeRun,
}); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to register run command: %v\n", err)
}

// test depends on deps (test tools like jest/vitest must be installed first)
if err := cmdOrchestrator.Register(&orchestrator.Command{
Name: "test",
Dependencies: []string{"deps"},
Execute: executeTest,
}); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to register test command: %v\n", err)
}
}

// executeReqs is the core logic for the reqs command.
func executeReqs() error {
cliout.CommandHeader("reqs", "Check required prerequisites")
Expand Down
114 changes: 62 additions & 52 deletions cli/src/cmd/app/commands/core_deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
// DependencyInstaller handles installation of project dependencies.
type DependencyInstaller struct {
searchRoot string
nodeProjects []types.NodeProject // Pre-filtered Node.js projects (optional)
pythonProjects []types.PythonProject // Pre-filtered Python projects (optional)
dotnetProjects []types.DotnetProject // Pre-filtered .NET projects (optional)
projects DetectedProjects // Pre-filtered projects (optional)
nodeProjects []types.NodeProject
pythonProjects []types.PythonProject
dotnetProjects []types.DotnetProject
}

// NewDependencyInstaller creates a new dependency installer.
Expand Down Expand Up @@ -84,22 +85,27 @@ func (di *DependencyInstaller) InstallAll() ([]InstallResult, error) {
func (di *DependencyInstaller) InstallAllFiltered() ([]InstallResult, error) {
var results []InstallResult

// Install Node.js dependencies from pre-filtered list
if len(di.nodeProjects) > 0 {
nodeResults := di.installNodeProjectList(di.nodeProjects)
results = append(results, nodeResults...)
nodeProjects := di.projects.Node
pythonProjects := di.projects.Python
dotnetProjects := di.projects.Dotnet
if nodeProjects == nil {
nodeProjects = di.nodeProjects
}

// Install Python dependencies from pre-filtered list
if len(di.pythonProjects) > 0 {
pythonResults := di.installPythonProjectList(di.pythonProjects)
results = append(results, pythonResults...)
if pythonProjects == nil {
pythonProjects = di.pythonProjects
}
if dotnetProjects == nil {
dotnetProjects = di.dotnetProjects
}

// Install .NET dependencies from pre-filtered list
if len(di.dotnetProjects) > 0 {
dotnetResults := di.installDotnetProjectList(di.dotnetProjects)
results = append(results, dotnetResults...)
if len(nodeProjects) > 0 {
results = append(results, di.installNodeProjectList(nodeProjects)...)
}
if len(pythonProjects) > 0 {
results = append(results, di.installPythonProjectList(pythonProjects)...)
}
if len(dotnetProjects) > 0 {
results = append(results, di.installDotnetProjectList(dotnetProjects)...)
}

return results, nil
Expand Down Expand Up @@ -261,53 +267,44 @@ func (di *DependencyInstaller) installProject(projectType, dir, manager string,
return result
}

// filterProjectsByService filters projects to only include those matching the specified service names.
func filterProjectsByService(
nodeProjects []types.NodeProject,
pythonProjects []types.PythonProject,
dotnetProjects []types.DotnetProject,
services []string,
searchRoot string,
) ([]types.NodeProject, []types.PythonProject, []types.DotnetProject) {
// filterDetectedProjectsByService filters grouped projects to only include those matching the specified service names.
func filterDetectedProjectsByService(projects DetectedProjects, services []string, searchRoot string) DetectedProjects {
// Build a set of service paths from azure.yaml
servicePaths := make(map[string]bool)

azureYamlPath, err := detector.FindAzureYaml(searchRoot)
if err != nil || azureYamlPath == "" {
// No azure.yaml found, can't filter by service
return nodeProjects, pythonProjects, dotnetProjects
return projects
}

azureYaml, err := parseAzureYaml(azureYamlPath)
if err != nil {
return nodeProjects, pythonProjects, dotnetProjects
return projects
}

azureYamlDir := filepath.Dir(azureYamlPath)

// Build map of service name to absolute path
for name, svc := range azureYaml.Services {
// Check if this service is in the filter list
for _, filterName := range services {
if name == filterName {
svcPath := filepath.Join(azureYamlDir, svc.Project)
absPath, err := filepath.Abs(svcPath)
if err != nil {
// Log warning but continue processing other services
if !cliout.IsJSON() {
cliout.Warning("Failed to resolve absolute path for service %s: %v", name, err)
}
continue
if name != filterName {
continue
}

absPath, err := filepath.Abs(svc.Project)
if err != nil {
if !cliout.IsJSON() {
cliout.Warning("Failed to resolve absolute path for service %s: %v", name, err)
}
servicePaths[absPath] = true
break
continue
}
servicePaths[absPath] = true
break
}
}

// Filter Node.js projects
var filteredNode []types.NodeProject
for _, p := range nodeProjects {
for _, p := range projects.Node {
absDir, _ := filepath.Abs(p.Dir)
if servicePaths[absDir] || isSubdirectory(absDir, servicePaths) {
filteredNode = append(filteredNode, p)
Expand All @@ -316,7 +313,7 @@ func filterProjectsByService(

// Filter Python projects
var filteredPython []types.PythonProject
for _, p := range pythonProjects {
for _, p := range projects.Python {
absDir, _ := filepath.Abs(p.Dir)
if servicePaths[absDir] || isSubdirectory(absDir, servicePaths) {
filteredPython = append(filteredPython, p)
Expand All @@ -325,15 +322,30 @@ func filterProjectsByService(

// Filter .NET projects
var filteredDotnet []types.DotnetProject
for _, p := range dotnetProjects {
for _, p := range projects.Dotnet {
absPath, _ := filepath.Abs(p.Path)
absDir := filepath.Dir(absPath)
if servicePaths[absDir] || isSubdirectory(absDir, servicePaths) {
filteredDotnet = append(filteredDotnet, p)
}
}

return filteredNode, filteredPython, filteredDotnet
return DetectedProjects{
Node: filteredNode,
Python: filteredPython,
Dotnet: filteredDotnet,
}
}

// filterProjectsByService preserves the legacy test-facing signature while delegating to grouped project filtering.
func filterProjectsByService(nodeProjects []types.NodeProject, pythonProjects []types.PythonProject, dotnetProjects []types.DotnetProject, services []string, searchRoot string) ([]types.NodeProject, []types.PythonProject, []types.DotnetProject) {
filtered := filterDetectedProjectsByService(DetectedProjects{
Node: nodeProjects,
Python: pythonProjects,
Dotnet: dotnetProjects,
}, services, searchRoot)

return filtered.Node, filtered.Python, filtered.Dotnet
}

// detectProjectsFromAzureYaml reads azure.yaml and detects project types directly from
Expand Down Expand Up @@ -462,23 +474,23 @@ func isSubdirectory(path string, parentPaths map[string]bool) bool {
}

// runParallelInstallation runs the parallel installer for non-JSON mode.
func runParallelInstallation(nodeProjects []types.NodeProject, pythonProjects []types.PythonProject, dotnetProjects []types.DotnetProject, verbose bool) error {
func runParallelInstallation(projects DetectedProjects, verbose bool) error {
parallelInstaller := installer.NewParallelInstaller()
parallelInstaller.Verbose = verbose

// Handle npm/yarn/pnpm workspace scenarios using workspace handler
// When a workspace root exists, only install at the root level to avoid race conditions
// on Windows where parallel npm installs compete for the same node_modules directory
workspaceHandler := workspace.NewHandler()
filteredNodeProjects := workspaceHandler.FilterNodeProjects(nodeProjects)
filteredNodeProjects := workspaceHandler.FilterNodeProjects(projects.Node)

for _, project := range filteredNodeProjects {
parallelInstaller.AddNodeProject(project)
}
for _, project := range pythonProjects {
for _, project := range projects.Python {
parallelInstaller.AddPythonProject(project)
}
for _, project := range dotnetProjects {
for _, project := range projects.Dotnet {
parallelInstaller.AddDotnetProject(project)
}

Expand All @@ -500,11 +512,9 @@ func runParallelInstallation(nodeProjects []types.NodeProject, pythonProjects []
}

// runJSONInstallation runs installation in JSON mode with sequential cliout.
func runJSONInstallation(searchRoot string, nodeProjects []types.NodeProject, pythonProjects []types.PythonProject, dotnetProjects []types.DotnetProject) error {
func runJSONInstallation(searchRoot string, projects DetectedProjects) error {
depInstaller := NewDependencyInstaller(searchRoot)
depInstaller.nodeProjects = nodeProjects
depInstaller.pythonProjects = pythonProjects
depInstaller.dotnetProjects = dotnetProjects
depInstaller.projects = projects

results, err := depInstaller.InstallAllFiltered()
if err != nil {
Expand Down
Loading
Loading