e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
826 lines
21 KiB
Go
826 lines
21 KiB
Go
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"
|
|
}
|