diff --git a/internal/cli/up.go b/internal/cli/up.go index 933ccf9..3ac6ae0 100644 --- a/internal/cli/up.go +++ b/internal/cli/up.go @@ -7,6 +7,8 @@ import ( "github.com/ork-cli/ork/internal/config" "github.com/ork-cli/ork/internal/docker" "github.com/ork-cli/ork/internal/service" + "github.com/ork-cli/ork/internal/ui" + "github.com/ork-cli/ork/pkg/utils" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ var upCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), // Require at least one service name Run: func(cmd *cobra.Command, args []string) { if err := runUp(args); err != nil { - fmt.Printf("āŒ Error: %v\n", err) + handleUpError(err) return } }, @@ -68,30 +70,44 @@ func runUp(serviceNames []string) error { } defer func() { if closeErr := dockerClient.Close(); closeErr != nil { - fmt.Printf("āŒ Error closing Docker client: %v\n", closeErr) + ui.Warning(fmt.Sprintf("Failed to close Docker client: %v", closeErr)) } }() // Resolve dependencies and get services in the correct start order orderedServices, err := service.ResolveDependencies(cfg.Services, serviceNames) if err != nil { - return fmt.Errorf("failed to resolve dependencies: %w", err) + return utils.ServiceError( + "up.dependencies", + "Failed to resolve service dependencies", + "Check your service dependencies in ork.yml", + err, + ) } // Create a project network for service communication ctx := context.Background() + spinner := ui.ShowSpinner("Creating project network...") networkID, err := dockerClient.CreateNetwork(ctx, cfg.Project) if err != nil { - return fmt.Errorf("failed to create project network: %w", err) + spinner.Error("Failed to create network") + return utils.NetworkError( + "up.network", + "Failed to create project network", + "Check if Docker is running and you have permissions", + err, + ) } - fmt.Printf("🌐 Created network: ork-%s-network\n", cfg.Project) + spinner.Success(fmt.Sprintf("Created network: ork-%s-network", cfg.Project)) - // Show startup message - fmt.Printf("āœ… Loaded project: %s (version %s)\n", cfg.Project, cfg.Version) - fmt.Printf("šŸš€ Starting services: %v\n", serviceNames) + // Show startup summary + ui.EmptyLine() + ui.Info(fmt.Sprintf("Project: %s (v%s)", ui.Bold(cfg.Project), cfg.Version)) + ui.Info(fmt.Sprintf("Starting: %s", ui.Highlight(fmt.Sprintf("%v", serviceNames)))) if len(orderedServices) > len(serviceNames) { - fmt.Printf("šŸ“¦ Including dependencies: %v\n", orderedServices) + ui.Info(fmt.Sprintf("Dependencies: %s", ui.Dim(fmt.Sprintf("%v", orderedServices)))) } + ui.EmptyLine() // Create an orchestrator for parallel service management orchestrator := service.NewOrchestrator(cfg.Project, dockerClient, networkID) @@ -106,7 +122,8 @@ func runUp(serviceNames []string) error { return err } - fmt.Printf("āœ… All services started successfully!\n") + ui.EmptyLine() + ui.SuccessBox(fmt.Sprintf("All services started successfully! %s", ui.SymbolRocket)) return nil } @@ -118,11 +135,21 @@ func runUp(serviceNames []string) error { func loadAndValidateConfig() (*config.Config, error) { cfg, err := config.Load() if err != nil { - return nil, fmt.Errorf("failed to load config: %w", err) + return nil, utils.ConfigError( + "up.load", + "Failed to load configuration", + "Make sure ork.yml exists in the current directory", + err, + ) } if err := cfg.Validate(); err != nil { - return nil, fmt.Errorf("invalid configuration: %w", err) + return nil, utils.ConfigError( + "up.validate", + "Invalid configuration", + "Check your ork.yml for errors", + err, + ) } return cfg, nil @@ -136,18 +163,24 @@ func loadAndValidateConfig() (*config.Config, error) { func validateServiceNames(serviceNames []string, cfg *config.Config) error { for _, serviceName := range serviceNames { if _, exists := cfg.Services[serviceName]; !exists { - return fmt.Errorf("service '%s' not found in ork.yml\nšŸ’” Available services: %s", - serviceName, getAvailableServicesList(cfg)) + availableServices := getAvailableServicesList(cfg) + suggestions := utils.FindSuggestions(serviceName, availableServices, 3) + + err := utils.ErrServiceNotFound(serviceName, suggestions) + err.Details = []string{ + fmt.Sprintf("Available services: %s", ui.Dim(fmt.Sprintf("%v", availableServices))), + } + return err } } return nil } -// getAvailableServicesList returns a formatted string of available services -func getAvailableServicesList(cfg *config.Config) string { - services := "" +// getAvailableServicesList returns a slice of available service names +func getAvailableServicesList(cfg *config.Config) []string { + services := make([]string, 0, len(cfg.Services)) for name := range cfg.Services { - services += name + " " + services = append(services, name) } return services } @@ -160,7 +193,39 @@ func getAvailableServicesList(cfg *config.Config) string { func createDockerClient() (*docker.Client, error) { client, err := docker.NewClient() if err != nil { - return nil, fmt.Errorf("failed to create Docker client: %w", err) + return nil, utils.DockerError( + "up.docker", + "Failed to connect to Docker", + "Make sure Docker is running. Try 'docker ps' or run 'ork doctor'", + err, + ) } return client, nil } + +// handleUpError formats and displays errors with hints +func handleUpError(err error) { + if orkErr, ok := err.(*utils.OrkError); ok { + // Display structured error with hints + ui.Error(orkErr.Message) + if orkErr.Hint != "" { + ui.Hint(orkErr.Hint) + } + if len(orkErr.Details) > 0 { + ui.EmptyLine() + for _, detail := range orkErr.Details { + ui.List(detail) + } + } + if len(orkErr.Suggestions) > 0 { + ui.EmptyLine() + ui.Info("Did you mean:") + for _, suggestion := range orkErr.Suggestions { + ui.ListItem(ui.SymbolArrow, ui.Highlight(suggestion)) + } + } + } else { + // Fallback for non-Ork errors + ui.Error(fmt.Sprintf("Error: %v", err)) + } +} diff --git a/internal/service/orchestrator.go b/internal/service/orchestrator.go index aa05d4b..a151488 100644 --- a/internal/service/orchestrator.go +++ b/internal/service/orchestrator.go @@ -8,6 +8,7 @@ import ( "github.com/ork-cli/ork/internal/config" "github.com/ork-cli/ork/internal/docker" + "github.com/ork-cli/ork/internal/ui" ) // ============================================================================ @@ -67,12 +68,12 @@ func (o *Orchestrator) StartServicesInOrder(ctx context.Context, orderedServiceN // Start services level by level for levelNum, levelServices := range levels { - fmt.Printf("šŸ“¦ Starting level %d: %v\n", levelNum+1, levelServices) + ui.Subheader(fmt.Sprintf("Level %d: %s", levelNum+1, ui.Dim(fmt.Sprintf("%v", levelServices)))) // Start all services in this level in parallel if err := o.startServicesInParallel(ctx, levelServices, &startedServices); err != nil { // Rollback on failure - fmt.Printf("āŒ Failed to start services: %v\n", err) + ui.Error(fmt.Sprintf("Failed to start services: %v", err)) o.rollbackStartedServices(ctx, startedServices) return err } @@ -80,7 +81,7 @@ func (o *Orchestrator) StartServicesInOrder(ctx context.Context, orderedServiceN // Wait for all services in this level to become healthy if err := o.waitForHealthy(ctx, levelServices); err != nil { // Rollback on health check failure - fmt.Printf("āŒ Health check failed: %v\n", err) + ui.Error(fmt.Sprintf("Health check failed: %v", err)) o.rollbackStartedServices(ctx, startedServices) return err } @@ -179,7 +180,7 @@ func (o *Orchestrator) calculateServiceLevel(serviceName string, graph map[strin func (o *Orchestrator) startServicesInParallel(ctx context.Context, serviceNames []string, startedServices *[]*Service) error { // Use a wait group to track parallel starts var wg sync.WaitGroup - var mu sync.Mutex // Protects concurrent access to startedServices slice + var mu sync.Mutex // Protects concurrent access to the startedServices slice errChan := make(chan error, len(serviceNames)) // Start each service in a separate goroutine @@ -195,14 +196,19 @@ func (o *Orchestrator) startServicesInParallel(ctx context.Context, serviceNames return } - // Start the service - fmt.Printf("🐳 Starting %s...\n", serviceName) + // Start the service with a spinner + spinner := ui.ShowSpinner(fmt.Sprintf("Starting %s", ui.Bold(serviceName))) if err := svc.Start(ctx, o.dockerClient, o.networkID); err != nil { + spinner.Error(fmt.Sprintf("Failed to start %s", serviceName)) errChan <- fmt.Errorf("failed to start %s: %w", serviceName, err) return } - fmt.Printf("āœ… Started %s (container: %s)\n", serviceName, svc.GetContainerID()[:12]) + containerID := svc.GetContainerID() + if len(containerID) > 12 { + containerID = containerID[:12] + } + spinner.Success(fmt.Sprintf("Started %s %s", ui.Bold(serviceName), ui.Dim(containerID))) // Track successfully started service (protected by mutex) mu.Lock() @@ -251,7 +257,7 @@ func (o *Orchestrator) waitForHealthy(ctx context.Context, serviceNames []string return nil } - fmt.Printf("šŸ„ Waiting for services to become healthy...\n") + ui.Info(fmt.Sprintf("%s Waiting for health checks...", ui.SymbolDoctor)) // Wait for each service with a health check var wg sync.WaitGroup @@ -278,7 +284,7 @@ func (o *Orchestrator) waitForHealthy(ctx context.Context, serviceNames []string return } - fmt.Printf("āœ… %s is healthy\n", service.Name) + ui.Success(fmt.Sprintf("%s is healthy", service.Name)) }(svc) } @@ -344,17 +350,18 @@ func (o *Orchestrator) rollbackStartedServices(ctx context.Context, startedServi return } - fmt.Printf("šŸ”„ Rolling back %d started service(s)...\n", len(startedServices)) + ui.EmptyLine() + ui.Warning(fmt.Sprintf("Rolling back %d started service(s)...", len(startedServices))) // Stop services in reverse order for i := len(startedServices) - 1; i >= 0; i-- { svc := startedServices[i] - fmt.Printf("šŸ›‘ Rolling back %s...\n", svc.Name) + spinner := ui.ShowSpinner(fmt.Sprintf("Rolling back %s", svc.Name)) if err := svc.Stop(ctx, o.dockerClient); err != nil { - fmt.Printf("āš ļø Warning: failed to rollback %s: %v\n", svc.Name, err) + spinner.Warning(fmt.Sprintf("Failed to rollback %s: %v", svc.Name, err)) } else { - fmt.Printf("āœ… Rolled back %s\n", svc.Name) + spinner.Success(fmt.Sprintf("Rolled back %s", svc.Name)) } } } @@ -386,12 +393,13 @@ func (o *Orchestrator) StopAll(ctx context.Context) error { go func(service *Service) { defer wg.Done() - fmt.Printf("šŸ›‘ Stopping %s...\n", service.Name) + spinner := ui.ShowSpinner(fmt.Sprintf("Stopping %s", service.Name)) if err := service.Stop(ctx, o.dockerClient); err != nil { + spinner.Error(fmt.Sprintf("Failed to stop %s", service.Name)) errChan <- fmt.Errorf("failed to stop %s: %w", service.Name, err) return } - fmt.Printf("āœ… Stopped %s\n", service.Name) + spinner.Success(fmt.Sprintf("Stopped %s", service.Name)) }(svc) }