✨ Better log viewer in launchpad
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/logview"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -22,20 +23,25 @@ var colors = []string{
|
||||
"\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", "\033[31m",
|
||||
}
|
||||
|
||||
const colorReset = "\033[0m"
|
||||
|
||||
// RunDev starts all services defined in the config in development mode.
|
||||
func RunDev(cfg config.LaunchpadConfig) {
|
||||
// RunDev starts selected services defined in the config in development mode.
|
||||
func RunDev(cfg config.LaunchpadConfig, servicesToStart []config.Service) {
|
||||
log.Info().Msg("Starting services in development mode with dependency checks...")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
defer func() {
|
||||
log.Info().Msg("Shutting down all services...")
|
||||
cancel()
|
||||
wg.Wait()
|
||||
log.Info().Msg("All services have been shut down.")
|
||||
}()
|
||||
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
log.Info().Msgf("Received signal: %v. Shutting down all services...", sig)
|
||||
<-sigChan
|
||||
log.Info().Msg("Shutdown signal received.")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
@@ -48,33 +54,43 @@ func RunDev(cfg config.LaunchpadConfig) {
|
||||
}
|
||||
createDockerNetwork(devNetwork)
|
||||
|
||||
// --- Dependency-aware startup ---
|
||||
serviceMap := make(map[string]config.Service)
|
||||
for _, s := range cfg.Services {
|
||||
serviceMap[s.Name] = s
|
||||
}
|
||||
|
||||
started := make(map[string]chan bool)
|
||||
for _, s := range cfg.Services {
|
||||
serviceNamesToStart := []string{}
|
||||
for _, s := range servicesToStart {
|
||||
serviceNamesToStart = append(serviceNamesToStart, s.Name)
|
||||
}
|
||||
log.Info().Msgf("Attempting to start: %s", strings.Join(serviceNamesToStart, ", "))
|
||||
|
||||
logChan := make(chan logview.LogMessage, 100)
|
||||
|
||||
for i, s := range servicesToStart {
|
||||
if _, exists := started[s.Name]; !exists {
|
||||
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork)
|
||||
color := colors[i%len(colors)]
|
||||
startServiceWithDeps(ctx, &wg, s, serviceMap, started, devNetwork, color, logChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't start the log viewer if no services were selected to run
|
||||
if len(servicesToStart) > 0 {
|
||||
if err := logview.Start(logChan, serviceNamesToStart); err != nil {
|
||||
log.Fatal().Err(err).Msg("Log viewer failed")
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
log.Info().Msg("All services have been shut down.")
|
||||
}
|
||||
|
||||
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network string) {
|
||||
func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Service, serviceMap map[string]config.Service, started map[string]chan bool, network, color string, logChan chan<- logview.LogMessage) {
|
||||
if _, exists := started[s.Name]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a channel that will be closed when this service is healthy
|
||||
healthyChan := make(chan bool)
|
||||
started[s.Name] = healthyChan
|
||||
|
||||
// First, recursively start dependencies
|
||||
var depNames []string
|
||||
for depName := range s.Prod.DependsOn {
|
||||
depNames = append(depNames, depName)
|
||||
@@ -82,11 +98,11 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
|
||||
|
||||
for _, depName := range depNames {
|
||||
if dep, ok := serviceMap[depName]; ok {
|
||||
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network)
|
||||
// Dependencies get a default color for now
|
||||
startServiceWithDeps(ctx, wg, dep, serviceMap, started, network, "\033[37m", logChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for dependencies to be healthy
|
||||
log.Info().Str("service", s.Name).Msgf("Waiting for dependencies to be healthy: %v", depNames)
|
||||
for _, depName := range depNames {
|
||||
if depChan, ok := started[depName]; ok {
|
||||
@@ -101,33 +117,29 @@ func startServiceWithDeps(ctx context.Context, wg *sync.WaitGroup, s config.Serv
|
||||
}
|
||||
}
|
||||
|
||||
// Now, start the actual service
|
||||
wg.Add(1)
|
||||
go func(s config.Service, color string) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
var healthCheckPorts []int
|
||||
if s.Type == "docker" {
|
||||
// For docker, we use the dev healthcheck ports to also map them
|
||||
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
||||
startDockerService(ctx, s, color, network, healthCheckPorts)
|
||||
startDockerService(ctx, s, color, network, healthCheckPorts, logChan)
|
||||
} else if s.Dev.Command != "" {
|
||||
healthCheckPorts = s.Dev.Healthcheck.TcpPorts
|
||||
startSourceService(ctx, s, color)
|
||||
startSourceService(ctx, s, color, logChan)
|
||||
} else {
|
||||
log.Warn().Str("service", s.Name).Msg("No dev.command or docker type, skipping.")
|
||||
close(healthyChan) // Mark as "healthy" so other things can proceed
|
||||
close(healthyChan)
|
||||
return
|
||||
}
|
||||
|
||||
// Perform health check on the service we just started
|
||||
waitForHealth(ctx, s.Name, healthCheckPorts)
|
||||
close(healthyChan) // Signal that this service is now healthy
|
||||
close(healthyChan)
|
||||
|
||||
// Block until context is cancelled to keep the service running
|
||||
// and ensure wg.Done is called at the right time.
|
||||
<-ctx.Done()
|
||||
}(s, colors[len(started)%len(colors)])
|
||||
}(
|
||||
)
|
||||
}
|
||||
|
||||
func waitForHealth(ctx context.Context, serviceName string, ports []int) {
|
||||
@@ -158,9 +170,7 @@ func waitForHealth(ctx context.Context, serviceName string, ports []int) {
|
||||
}
|
||||
}
|
||||
|
||||
// startSourceService runs a service from its source code.
|
||||
func startSourceService(ctx context.Context, s config.Service, color string) {
|
||||
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
|
||||
func startSourceService(ctx context.Context, s config.Service, color string, logChan chan<- logview.LogMessage) {
|
||||
log.Info().Str("service", s.Name).Str("command", s.Dev.Command).Msg("Starting from source")
|
||||
|
||||
parts := strings.Fields(s.Dev.Command)
|
||||
@@ -173,12 +183,10 @@ func startSourceService(ctx context.Context, s config.Service, color string) {
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
|
||||
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
|
||||
}
|
||||
|
||||
// startDockerService runs a pre-built Docker image.
|
||||
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int) {
|
||||
prefix := fmt.Sprintf("%s[%-10s]%s ", color, s.Name, colorReset)
|
||||
func startDockerService(ctx context.Context, s config.Service, color string, network string, portsToMap []int, logChan chan<- logview.LogMessage) {
|
||||
log.Info().Str("service", s.Name).Str("image", s.Prod.Image).Msg("Starting from Docker image")
|
||||
|
||||
containerName := fmt.Sprintf("%s-dev", s.Name)
|
||||
@@ -218,10 +226,10 @@ func startDockerService(ctx context.Context, s config.Service, color string, net
|
||||
}()
|
||||
|
||||
cmd := exec.Command("docker", args...)
|
||||
runAndMonitorCommand(ctx, cmd, s.Name, prefix)
|
||||
runAndMonitorCommand(ctx, cmd, s.Name, color, logChan)
|
||||
}
|
||||
|
||||
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefix string) {
|
||||
func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, color string, logChan chan<- logview.LogMessage) {
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
|
||||
@@ -230,14 +238,14 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
|
||||
return
|
||||
}
|
||||
|
||||
go streamOutput(stdout, prefix)
|
||||
go streamOutput(stderr, prefix)
|
||||
go streamOutput(stdout, serviceName, color, logChan)
|
||||
go streamOutput(stderr, serviceName, color, logChan)
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Str("service", serviceName).Msg("Process stopped.")
|
||||
// This is expected on clean shutdown
|
||||
} else if err != nil {
|
||||
log.Error().Err(err).Str("service", serviceName).Msg("Exited with error")
|
||||
} else {
|
||||
@@ -246,10 +254,10 @@ func runAndMonitorCommand(ctx context.Context, cmd *exec.Cmd, serviceName, prefi
|
||||
}()
|
||||
}
|
||||
|
||||
func streamOutput(pipe io.ReadCloser, prefix string) {
|
||||
func streamOutput(pipe io.ReadCloser, serviceName, color string, logChan chan<- logview.LogMessage) {
|
||||
scanner := bufio.NewScanner(pipe)
|
||||
for scanner.Scan() {
|
||||
fmt.Printf("%s%s\n", prefix, scanner.Text())
|
||||
logChan <- logview.LogMessage{ServiceName: serviceName, Line: scanner.Text(), Color: color}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
126
pkg/launchpad/interactive/dev_mode.go
Normal file
126
pkg/launchpad/interactive/dev_mode.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package interactive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("62")).
|
||||
Foreground(lipgloss.Color("230")).
|
||||
Padding(0, 1)
|
||||
|
||||
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
)
|
||||
|
||||
type model struct {
|
||||
services []config.Service
|
||||
cursor int
|
||||
selected map[int]struct{}
|
||||
startServices bool
|
||||
}
|
||||
|
||||
func initialModel(services []config.Service) model {
|
||||
selected := make(map[int]struct{})
|
||||
for i := range services {
|
||||
selected[i] = struct{}{}
|
||||
}
|
||||
return model{
|
||||
services: services,
|
||||
selected: selected,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "up", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.cursor < len(m.services)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
|
||||
case " ":
|
||||
if _, ok := m.selected[m.cursor]; ok {
|
||||
delete(m.selected, m.cursor)
|
||||
} else {
|
||||
m.selected[m.cursor] = struct{}{}
|
||||
}
|
||||
case "enter":
|
||||
m.startServices = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(titleStyle.Render("Select services to start"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, service := range m.services {
|
||||
cursor := " "
|
||||
if m.cursor == i {
|
||||
cursor = ">"
|
||||
}
|
||||
|
||||
checked := " "
|
||||
if _, ok := m.selected[i]; ok {
|
||||
checked = "x"
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("%s [%s] %s\n", cursor, checked, service.Name))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(helpStyle.Render("Use up/down to navigate, space to select, enter to start, q to quit."))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m model) SelectedServices() []config.Service {
|
||||
var selectedServices []config.Service
|
||||
for i := range m.selected {
|
||||
selectedServices = append(selectedServices, m.services[i])
|
||||
}
|
||||
return selectedServices
|
||||
}
|
||||
|
||||
func SelectServices(services []config.Service) ([]config.Service, error) {
|
||||
if len(services) == 0 {
|
||||
return nil, fmt.Errorf("no services defined in launchpad.toml")
|
||||
}
|
||||
m := initialModel(services)
|
||||
p := tea.NewProgram(m)
|
||||
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fm := finalModel.(model)
|
||||
if fm.startServices {
|
||||
return fm.SelectedServices(), nil
|
||||
}
|
||||
|
||||
return nil, nil // User quit
|
||||
}
|
||||
171
pkg/launchpad/logview/viewer.go
Normal file
171
pkg/launchpad/logview/viewer.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package logview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// LogMessage is a message containing a line from a service's log.
|
||||
type LogMessage struct {
|
||||
ServiceName string
|
||||
Line string
|
||||
Color string
|
||||
}
|
||||
|
||||
type model struct {
|
||||
services []string
|
||||
logChan <-chan LogMessage
|
||||
logs map[string][]string // Key: service name, Value: log lines
|
||||
allLogs []string
|
||||
viewports map[string]viewport.Model
|
||||
activeTab int
|
||||
ready bool
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
var (
|
||||
tabStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).Padding(0, 1)
|
||||
activeTabStyle = tabStyle.Copy().Border(lipgloss.ThickBorder(), false, false, true, false).Foreground(lipgloss.Color("62"))
|
||||
inactiveTabStyle = tabStyle.Copy()
|
||||
windowStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1, 0)
|
||||
)
|
||||
|
||||
func NewModel(logChan <-chan LogMessage, services []string) model {
|
||||
m := model{
|
||||
services: append([]string{"All"}, services...),
|
||||
logChan: logChan,
|
||||
logs: make(map[string][]string),
|
||||
viewports: make(map[string]viewport.Model),
|
||||
activeTab: 0,
|
||||
}
|
||||
for _, s := range m.services {
|
||||
m.logs[s] = []string{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return m.waitForLogs()
|
||||
}
|
||||
|
||||
// waitForLogs waits for the next log message from the channel.
|
||||
func (m model) waitForLogs() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.logChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var (
|
||||
cmd tea.Cmd
|
||||
cmds []tea.Cmd
|
||||
)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "right", "l":
|
||||
m.activeTab = (m.activeTab + 1) % len(m.services)
|
||||
m.updateViewportContent()
|
||||
case "left", "h":
|
||||
m.activeTab--
|
||||
if m.activeTab < 0 {
|
||||
m.activeTab = len(m.services) - 1
|
||||
}
|
||||
m.updateViewportContent()
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
headerHeight := lipgloss.Height(m.renderHeader())
|
||||
vpHeight := m.height - headerHeight - windowStyle.GetVerticalFrameSize()
|
||||
|
||||
if !m.ready {
|
||||
for _, serviceName := range m.services {
|
||||
m.viewports[serviceName] = viewport.New(m.width-windowStyle.GetHorizontalFrameSize(), vpHeight)
|
||||
}
|
||||
m.ready = true
|
||||
} else {
|
||||
for _, serviceName := range m.services {
|
||||
vp := m.viewports[serviceName]
|
||||
vp.Width = m.width - windowStyle.GetHorizontalFrameSize()
|
||||
vp.Height = vpHeight
|
||||
m.viewports[serviceName] = vp
|
||||
}
|
||||
}
|
||||
m.updateViewportContent()
|
||||
|
||||
case LogMessage:
|
||||
logLine := fmt.Sprintf("%s%s%s", msg.Color, msg.Line, "\033[0m")
|
||||
allLogLine := fmt.Sprintf("%s[%-10s]%s %s", msg.Color, msg.ServiceName, "\033[0m", msg.Line)
|
||||
|
||||
m.logs[msg.ServiceName] = append(m.logs[msg.ServiceName], logLine)
|
||||
m.allLogs = append(m.allLogs, allLogLine)
|
||||
|
||||
m.updateViewportContent()
|
||||
cmds = append(cmds, m.waitForLogs())
|
||||
}
|
||||
|
||||
activeViewport := m.viewports[m.services[m.activeTab]]
|
||||
activeViewport, cmd = activeViewport.Update(msg)
|
||||
m.viewports[m.services[m.activeTab]] = activeViewport
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *model) updateViewportContent() {
|
||||
if !m.ready {
|
||||
return
|
||||
}
|
||||
activeServiceName := m.services[m.activeTab]
|
||||
var content string
|
||||
if activeServiceName == "All" {
|
||||
content = strings.Join(m.allLogs, "\n")
|
||||
} else {
|
||||
content = strings.Join(m.logs[activeServiceName], "\n")
|
||||
}
|
||||
|
||||
vp := m.viewports[activeServiceName]
|
||||
vp.SetContent(content)
|
||||
vp.GotoBottom()
|
||||
m.viewports[activeServiceName] = vp
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if !m.ready {
|
||||
return "Initializing..."
|
||||
}
|
||||
header := m.renderHeader()
|
||||
activeService := m.services[m.activeTab]
|
||||
viewport := m.viewports[activeService]
|
||||
|
||||
return fmt.Sprintf("%s\n%s", header, windowStyle.Render(viewport.View()))
|
||||
}
|
||||
|
||||
func (m model) renderHeader() string {
|
||||
var tabs []string
|
||||
for i, service := range m.services {
|
||||
if i == m.activeTab {
|
||||
tabs = append(tabs, activeTabStyle.Render(service))
|
||||
} else {
|
||||
tabs = append(tabs, inactiveTabStyle.Render(service))
|
||||
}
|
||||
}
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
||||
}
|
||||
|
||||
// Start runs the log viewer program.
|
||||
func Start(logChan <-chan LogMessage, services []string) error {
|
||||
m := NewModel(logChan, services)
|
||||
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
return p.Start()
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/config"
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/deploy"
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/dev"
|
||||
|
||||
"git.solsynth.dev/goatworks/turbine/pkg/launchpad/interactive"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func init() {
|
||||
// Initialize logging
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -35,7 +35,7 @@ func main() {
|
||||
// Dispatch to the correct handler
|
||||
switch command {
|
||||
case "dev":
|
||||
dev.RunDev(cfg)
|
||||
handleDev(cfg, os.Args[2:])
|
||||
case "deploy":
|
||||
log.Info().Msg("Generating docker-compose.yml for production deployment...")
|
||||
deploy.GenerateDockerCompose(cfg)
|
||||
@@ -43,3 +43,41 @@ func main() {
|
||||
log.Fatal().Msgf("Unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDev(cfg config.LaunchpadConfig, args []string) {
|
||||
var servicesToRun []config.Service
|
||||
|
||||
if len(args) == 0 {
|
||||
// Interactive mode
|
||||
selectedServices, err := interactive.SelectServices(cfg.Services)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Could not start interactive selection")
|
||||
}
|
||||
if len(selectedServices) == 0 {
|
||||
log.Info().Msg("No services selected. Exiting.")
|
||||
return
|
||||
}
|
||||
servicesToRun = selectedServices
|
||||
} else if len(args) == 1 && args[0] == "all" {
|
||||
log.Info().Msg("Starting all services.")
|
||||
servicesToRun = cfg.Services
|
||||
} else {
|
||||
// Start specific services from args
|
||||
serviceMap := make(map[string]config.Service)
|
||||
for _, s := range cfg.Services {
|
||||
serviceMap[s.Name] = s
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
if service, ok := serviceMap[arg]; ok {
|
||||
servicesToRun = append(servicesToRun, service)
|
||||
} else {
|
||||
log.Fatal().Msgf("Service '%s' not found in launchpad.toml", arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(servicesToRun) > 0 {
|
||||
dev.RunDev(cfg, servicesToRun)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user