package run import ( "bufio" "context" "crypto/md5" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "os/signal" "path/filepath" "strings" "sync" "syscall" "time" "github.com/urfave/cli/v2" "go-micro.dev/v6/ai" clt "go-micro.dev/v6/client" "go-micro.dev/v6/cmd" "go-micro.dev/v6/cmd/micro/cli/generate" "go-micro.dev/v6/cmd/micro/run/config" "go-micro.dev/v6/cmd/micro/run/watcher" "go-micro.dev/v6/cmd/micro/server" "go-micro.dev/v6/registry" _ "go-micro.dev/v6/ai/anthropic" _ "go-micro.dev/v6/ai/atlascloud" _ "go-micro.dev/v6/ai/gemini" _ "go-micro.dev/v6/ai/groq" _ "go-micro.dev/v6/ai/mistral" _ "go-micro.dev/v6/ai/openai" _ "go-micro.dev/v6/ai/together" ) // Color codes for log output var colors = []string{ "\033[31m", // red "\033[32m", // green "\033[33m", // yellow "\033[34m", // blue "\033[35m", // magenta "\033[36m", // cyan } const colorReset = "\033[0m" func colorFor(idx int) string { return colors[idx%len(colors)] } // serviceProcess tracks a running service type serviceProcess struct { name string dir string binPath string pidFile string logFile string cmd *exec.Cmd pipeWriter *io.PipeWriter color string port int env []string mu sync.Mutex running bool } func (s *serviceProcess) start(logDir string) error { s.mu.Lock() defer s.mu.Unlock() if s.running { return nil } // Build buildCmd := exec.Command("go", "build", "-o", s.binPath, ".") buildCmd.Dir = s.dir buildOut, buildErr := buildCmd.CombinedOutput() if buildErr != nil { return fmt.Errorf("build failed: %s\n%s", buildErr, string(buildOut)) } // Open log file logFile, err := os.OpenFile(s.logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return fmt.Errorf("failed to open log file: %w", err) } // Start process s.cmd = exec.Command(s.binPath) s.cmd.Dir = s.dir s.cmd.Env = append(os.Environ(), s.env...) pr, pw := io.Pipe() s.pipeWriter = pw s.cmd.Stdout = pw s.cmd.Stderr = pw // Stream output go func(name string, color string, pr *io.PipeReader, logFile *os.File) { defer logFile.Close() scanner := bufio.NewScanner(pr) for scanner.Scan() { line := scanner.Text() fmt.Printf("%s[%s]%s %s\n", color, name, colorReset, line) _, _ = logFile.WriteString("[" + name + "] " + line + "\n") } }(s.name, s.color, pr, logFile) if err := s.cmd.Start(); err != nil { pw.Close() return fmt.Errorf("failed to start: %w", err) } // Write PID file _ = os.WriteFile(s.pidFile, []byte(fmt.Sprintf("%d\n%s\n%s\n%s\n", s.cmd.Process.Pid, s.dir, s.name, time.Now().Format(time.RFC3339))), 0644) s.running = true fmt.Printf("%s[%s]%s started (pid %d)\n", s.color, s.name, colorReset, s.cmd.Process.Pid) return nil } func (s *serviceProcess) stop() { s.mu.Lock() defer s.mu.Unlock() if !s.running || s.cmd == nil || s.cmd.Process == nil { return } fmt.Printf("%s[%s]%s stopping...\n", s.color, s.name, colorReset) // Graceful shutdown _ = s.cmd.Process.Signal(syscall.SIGTERM) // Wait with timeout done := make(chan error, 1) go func() { done <- s.cmd.Wait() }() select { case <-done: case <-time.After(5 * time.Second): _ = s.cmd.Process.Kill() <-done } if s.pipeWriter != nil { s.pipeWriter.Close() } os.Remove(s.pidFile) s.running = false } func (s *serviceProcess) restart(logDir string) error { s.stop() return s.start(logDir) } // waitForHealth waits for a service's health endpoint to respond func waitForHealth(port int, timeout time.Duration) bool { if port == 0 { return true // No port configured, assume ready } deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", port)) if err == nil { resp.Body.Close() if resp.StatusCode == http.StatusOK { return true } } time.Sleep(100 * time.Millisecond) } return false } func Run(c *cli.Context) error { // Handle --prompt: generate services first, then run them if prompt := c.String("prompt"); prompt != "" { return runWithPrompt(c, prompt) } dir := c.Args().Get(0) if dir == "" { dir = "." } // Handle git URLs if strings.HasPrefix(dir, "github.com/") || strings.HasPrefix(dir, "https://github.com/") { repo := strings.TrimPrefix(dir, "https://") tmp, err := os.MkdirTemp("", "micro-run-") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } defer os.RemoveAll(tmp) cloneURL := "https://" + repo cloneCmd := exec.Command("git", "clone", "--depth", "1", cloneURL, tmp) cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr if err := cloneCmd.Run(); err != nil { return fmt.Errorf("failed to clone %s: %w", cloneURL, err) } dir = tmp } absDir, err := filepath.Abs(dir) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } // Setup directories homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home dir: %w", err) } logsDir := filepath.Join(homeDir, "micro", "logs") runDir := filepath.Join(homeDir, "micro", "run") binDir := filepath.Join(homeDir, "micro", "bin") for _, d := range []string{logsDir, runDir, binDir} { if err := os.MkdirAll(d, 0755); err != nil { return fmt.Errorf("failed to create %s: %w", d, err) } } // Load configuration cfg, err := config.Load(absDir) if err != nil { return fmt.Errorf("failed to load config: %w", err) } // Get environment envName := c.String("env") if envName == "" { envName = os.Getenv("MICRO_ENV") } if envName == "" { envName = "development" } var envVars []string if cfg != nil { if envMap := cfg.GetEnv(envName); envMap != nil { for k, v := range envMap { envVars = append(envVars, k+"="+v) } } } // Discover services var services []*serviceProcess servicesByDir := make(map[string]*serviceProcess) if cfg != nil && len(cfg.Services) > 0 { // Use configured services in dependency order sorted, err := cfg.TopologicalSort() if err != nil { return fmt.Errorf("dependency error: %w", err) } for i, svc := range sorted { svcDir := filepath.Join(absDir, svc.Path) absSvcDir, _ := filepath.Abs(svcDir) hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8] sp := &serviceProcess{ name: svc.Name, dir: absSvcDir, binPath: filepath.Join(binDir, svc.Name+"-"+hash), pidFile: filepath.Join(runDir, svc.Name+"-"+hash+".pid"), logFile: filepath.Join(logsDir, svc.Name+"-"+hash+".log"), color: colorFor(i), port: svc.Port, env: envVars, } services = append(services, sp) servicesByDir[absSvcDir] = sp } } else { // Auto-discover from main.go files var mainFiles []string _ = filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if info.Name() == "main.go" { mainFiles = append(mainFiles, path) } return nil }) if len(mainFiles) == 0 { return fmt.Errorf("no main.go files found in %s", absDir) } for i, mainFile := range mainFiles { svcDir := filepath.Dir(mainFile) absSvcDir, _ := filepath.Abs(svcDir) var name string if absSvcDir == absDir { name = filepath.Base(absDir) } else { name = filepath.Base(svcDir) } hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8] sp := &serviceProcess{ name: name, dir: absSvcDir, binPath: filepath.Join(binDir, name+"-"+hash), pidFile: filepath.Join(runDir, name+"-"+hash+".pid"), logFile: filepath.Join(logsDir, name+"-"+hash+".log"), color: colorFor(i), env: envVars, } services = append(services, sp) servicesByDir[absSvcDir] = sp } } if len(services) == 0 { return fmt.Errorf("no services found") } // Start gateway unless disabled var gw *server.Gateway gatewayAddr := c.String("address") if gatewayAddr == "" { gatewayAddr = ":8080" } if !c.Bool("no-gateway") { var err error mcpAddr := c.String("mcp-address") gw, err = server.StartGateway(server.GatewayOptions{ Address: gatewayAddr, AuthEnabled: true, // Auth enabled with default admin/micro user Context: context.Background(), MCPEnabled: mcpAddr != "", MCPAddress: mcpAddr, }) if err != nil { return fmt.Errorf("failed to start gateway: %w", err) } } // Start services for _, svc := range services { if err := svc.start(logsDir); err != nil { fmt.Fprintf(os.Stderr, "[%s] %v\n", svc.name, err) continue } // Wait for health if port configured if svc.port > 0 { if !waitForHealth(svc.port, 10*time.Second) { fmt.Fprintf(os.Stderr, "[%s] health check timeout\n", svc.name) } } } // Print startup banner printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address")) // Setup signal handling sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) // Watch mode watchEnabled := !c.Bool("no-watch") var watch *watcher.Watcher if watchEnabled { var dirs []string for _, svc := range services { dirs = append(dirs, svc.dir) } watch = watcher.New(dirs) watch.Start() go func() { for event := range watch.Events() { if svc, ok := servicesByDir[event.Dir]; ok { fmt.Printf("%s[%s]%s rebuilding...\n", svc.color, svc.name, colorReset) if err := svc.restart(logsDir); err != nil { fmt.Fprintf(os.Stderr, "%s[%s]%s restart failed: %v\n", svc.color, svc.name, colorReset, err) } } } }() // Scan for new services added by micro chat or micro new go func() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-sigCh: return case <-ticker.C: newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services)) for _, sp := range newSvcs { services = append(services, sp) servicesByDir[sp.dir] = sp watch.AddDir(sp.dir) if err := sp.start(logsDir); err != nil { fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err) continue } fmt.Printf("\n \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name) } } } }() } // Interactive console or wait for signal if c.Bool("detach") { <-sigCh } else { runConsole(sigCh) } fmt.Println("\nShutting down...") if watch != nil { watch.Stop() } if gw != nil { _ = gw.Stop() } // Stop services in reverse order for i := len(services) - 1; i >= 0; i-- { services[i].stop() } return nil } func discoverNewServices(baseDir string, known map[string]*serviceProcess, binDir, runDir, logsDir string, envVars []string, colorOffset int) []*serviceProcess { var newSvcs []*serviceProcess entries, err := os.ReadDir(baseDir) if err != nil { return nil } for _, e := range entries { if !e.IsDir() { continue } svcDir := filepath.Join(baseDir, e.Name()) absSvcDir, _ := filepath.Abs(svcDir) if _, exists := known[absSvcDir]; exists { continue } mainFile := filepath.Join(svcDir, "main.go") if _, err := os.Stat(mainFile); err != nil { continue } name := e.Name() hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8] sp := &serviceProcess{ name: name, dir: absSvcDir, binPath: filepath.Join(binDir, name+"-"+hash), pidFile: filepath.Join(runDir, name+"-"+hash+".pid"), logFile: filepath.Join(logsDir, name+"-"+hash+".log"), color: colorFor(colorOffset + len(newSvcs)), env: envVars, } newSvcs = append(newSvcs, sp) } return newSvcs } func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) { fmt.Println() fmt.Println(" \033[1mMicro\033[0m") fmt.Println() if gw != nil { fmt.Printf(" Dashboard \033[36mhttp://localhost%s\033[0m\n", gw.Addr()) fmt.Printf(" API \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr()) fmt.Printf(" Agent \033[36mhttp://localhost%s/agent\033[0m\n", gw.Addr()) // MCP tools are served on the gateway by default — every endpoint is an // AI-callable tool, so surface it rather than hiding it behind a flag. fmt.Printf(" MCP Tools \033[36mhttp://localhost%s/mcp/tools\033[0m\n", gw.Addr()) fmt.Printf(" Health \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr()) if mcpAddr != "" { // Optional standalone MCP protocol server (e.g. for MCP clients). fmt.Printf(" MCP Server \033[36mhttp://localhost%s\033[0m (full MCP protocol)\n", mcpAddr) fmt.Printf(" WebSocket \033[36mws://localhost%s/mcp/ws\033[0m\n", mcpAddr) } } var agents, svcs []*serviceProcess for _, s := range services { if s.name == "agent" { agents = append(agents, s) } else { svcs = append(svcs, s) } } fmt.Println() fmt.Println(" Services:") for _, svc := range svcs { status := "\033[32m●\033[0m" if !svc.running { status = "\033[31m●\033[0m" } fmt.Printf(" %s %s\n", status, svc.name) } if len(agents) > 0 { fmt.Println() fmt.Println(" Agents:") for _, a := range agents { status := "\033[35m◆\033[0m" if !a.running { status = "\033[31m◆\033[0m" } fmt.Printf(" %s %s\n", status, a.name) } } fmt.Println() fmt.Println(" Auth: \033[32menabled\033[0m (admin / micro)") if watching { fmt.Println(" \033[33mWatching for changes...\033[0m") } fmt.Println() } func runConsole(sigCh chan os.Signal) { // Detect provider and API key from environment provider := os.Getenv("MICRO_AI_PROVIDER") apiKey := os.Getenv("MICRO_AI_API_KEY") if apiKey == "" { for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY"} { if v := os.Getenv(env); v != "" { apiKey = v break } } } if provider == "" { provider = ai.AutoDetectProvider("") } if apiKey == "" { fmt.Println(" \033[2mSet MICRO_AI_API_KEY to enable the interactive console.\033[0m") fmt.Println(" \033[2mCtrl-C to stop.\033[0m") fmt.Println() <-sigCh return } // Wait a moment for services to register time.Sleep(2 * time.Second) // Set up tools and model reg := registry.DefaultRegistry cl := clt.DefaultClient tools := ai.NewTools(reg, ai.ToolClient(cl)) var modelOpts []ai.Option modelOpts = append(modelOpts, ai.WithAPIKey(apiKey)) modelOpts = append(modelOpts, ai.WithToolHandler(tools.Handler())) m := ai.New(provider, modelOpts...) hist := ai.NewHistory(50) // Build system prompt with service list discovered, _ := tools.Discover() serviceNames := make(map[string]bool) for _, t := range discovered { parts := strings.SplitN(t.OriginalName, ".", 2) if len(parts) == 2 { serviceNames[parts[0]] = true } } var svcList []string for name := range serviceNames { svcList = append(svcList, name) } sysPrompt := fmt.Sprintf("You are an agent that orchestrates microservices. Available services: %s. "+ "Use the available tools to fulfill requests. When you call a tool, explain what you are doing. "+ "If a capability doesn't exist, say so.", strings.Join(svcList, ", ")) fmt.Printf(" \033[2m%d tools from %d services. Type a message or Ctrl-C to stop.\033[0m\n\n", len(discovered), len(serviceNames)) // Interactive REPL scanner := bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 0, 4096), 1024*1024) done := make(chan struct{}) go func() { for { fmt.Print("\033[1;36m>\033[0m ") if !scanner.Scan() { close(done) return } line := strings.TrimSpace(scanner.Text()) if line == "" { continue } hist.Add("user", line) resp, err := m.Generate(context.Background(), &ai.Request{ Prompt: line, SystemPrompt: sysPrompt, Tools: discovered, Messages: hist.Messages(), }) if err != nil { fmt.Printf("\033[31merror:\033[0m %v\n\n", err) continue } if resp.Reply != "" { hist.Add("assistant", resp.Reply) fmt.Println(resp.Reply) } for _, tc := range resp.ToolCalls { args, _ := json.Marshal(tc.Input) fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args) if tc.Result != "" { result := tc.Result if len(result) > 200 { result = result[:200] + "..." } fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", result) } } if resp.Answer != "" { hist.Add("assistant", resp.Answer) fmt.Println() fmt.Println(resp.Answer) } fmt.Println() } }() select { case <-sigCh: case <-done: } } func init() { cmd.Register(&cli.Command{ Name: "run", Usage: "Development mode: run services with hot reload and API gateway", Description: `Run discovers and runs services in a directory (development mode). Starts an HTTP gateway on :8080 providing: - Web dashboard at / - Agent playground at /agent (AI chat with MCP tools) - API explorer at /api - API proxy at /api/{service}/{endpoint} - MCP tools at /mcp/tools - Health checks at /health With a micro.mu or micro.json config file, services start in dependency order. Without config, all main.go files are discovered and run. Examples: micro run # Run with gateway on :8080 micro run --address :3000 # Gateway on custom port micro run --no-gateway # Services only, no HTTP gateway micro run --no-watch # Disable hot reload micro run --env production # Use production environment micro run --mcp-address :3000 # Enable MCP protocol gateway micro run --prompt "an order system for dropshipping" # Generate and run`, Action: Run, Flags: []cli.Flag{ &cli.StringFlag{ Name: "address", Aliases: []string{"a"}, Usage: "Gateway address (default :8080)", Value: ":8080", }, &cli.BoolFlag{ Name: "no-gateway", Usage: "Disable HTTP gateway", }, &cli.BoolFlag{ Name: "no-watch", Usage: "Disable hot reload (file watching)", }, &cli.BoolFlag{ Name: "detach", Aliases: []string{"d"}, Usage: "Run without interactive console (background mode)", }, &cli.StringFlag{ Name: "env", Aliases: []string{"e"}, Usage: "Environment to use (default: development)", EnvVars: []string{"MICRO_ENV"}, }, &cli.StringFlag{ Name: "mcp-address", Usage: "MCP gateway address (e.g., :3000). Enables MCP protocol for AI tools.", EnvVars: []string{"MICRO_MCP_ADDRESS"}, }, &cli.StringFlag{ Name: "prompt", Usage: "Describe a system to generate and run (AI designs, builds, and starts services)", EnvVars: []string{"MICRO_RUN_PROMPT"}, }, &cli.StringFlag{ Name: "provider", Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)", EnvVars: []string{"MICRO_AI_PROVIDER"}, }, &cli.StringFlag{ Name: "api_key", Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)", EnvVars: []string{"MICRO_AI_API_KEY"}, }, }, }) } func runWithPrompt(c *cli.Context, prompt string) error { provider := c.String("provider") apiKey := c.String("api_key") if apiKey == "" { for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} { if v := os.Getenv(env); v != "" { apiKey = v break } } } if apiKey == "" { return fmt.Errorf("--api_key or a provider API key env var is required for --prompt") } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() fmt.Println() fmt.Println(" \033[1mmicro run --prompt\033[0m") fmt.Println() fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt) design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt) if err != nil { return fmt.Errorf("design failed: %w", err) } fmt.Println(" Services:") for _, svc := range design.Services { fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description) for _, ep := range svc.Endpoints { fmt.Printf(" %s: %s\n", ep.Name, ep.Description) } } fmt.Println() if !confirmGenerate() { fmt.Println(" Canceled.") return nil } fmt.Println(" Generating code...") if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) } for _, svc := range design.Services { fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name) } fmt.Println() // Set env vars so the agent process can pick them up if provider != "" { os.Setenv("MICRO_AI_PROVIDER", provider) } os.Setenv("MICRO_AI_API_KEY", apiKey) // Now run normally — micro run discovers the generated services + agent fmt.Println(" Starting services...") fmt.Println() cancel() _ = c.Set("prompt", "") return Run(c) } func confirmGenerate() bool { fmt.Print(" Generate? [Y/n] ") scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() { return false } answer := strings.TrimSpace(strings.ToLower(scanner.Text())) return answer == "" || answer == "y" || answer == "yes" }