chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,825 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user