micro--go-micro
9bed04ced0
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
* perf: compress hero image — 1.4MB to 80KB Resized from 1536px to 1200px, converted to JPEG at quality 80. 80KB loads instantly vs 1.4MB stalling on slower connections. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd * docs: micro run drops into interactive console One command does everything — generate, start, and chat. No separate micro chat step. The landing page shows micro run dropping straight into the > prompt. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd * feat: interactive console in micro run, -d for detached mode micro run now drops into an interactive chat console after services start. The console discovers services, exposes them as tools, and lets you talk to them through an LLM — same as micro chat but built into the run experience. - Detects MICRO_AI_PROVIDER and MICRO_AI_API_KEY from environment - Falls back to provider-specific env vars (ANTHROPIC_API_KEY, etc.) - If no API key, prints hint and blocks on Ctrl-C (no console) - -d / --detach flag skips the console (background mode) - Ctrl-C always shuts everything down Removed adopters section from README. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --------- Co-authored-by: Claude <noreply@anthropic.com>
842 行
21 KiB
Go
842 行
21 KiB
Go
package run
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"go-micro.dev/v5/ai"
|
|
clt "go-micro.dev/v5/client"
|
|
"go-micro.dev/v5/cmd"
|
|
"go-micro.dev/v5/cmd/micro/cli/generate"
|
|
"go-micro.dev/v5/cmd/micro/run/config"
|
|
"go-micro.dev/v5/cmd/micro/run/watcher"
|
|
"go-micro.dev/v5/registry"
|
|
"go-micro.dev/v5/cmd/micro/server"
|
|
|
|
_ "go-micro.dev/v5/ai/anthropic"
|
|
_ "go-micro.dev/v5/ai/atlascloud"
|
|
_ "go-micro.dev/v5/ai/gemini"
|
|
_ "go-micro.dev/v5/ai/groq"
|
|
_ "go-micro.dev/v5/ai/mistral"
|
|
_ "go-micro.dev/v5/ai/openai"
|
|
_ "go-micro.dev/v5/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 == 200 {
|
|
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
|
|
}
|
|
|
|
// Helper functions
|
|
func parsePid(pidStr string) int {
|
|
pid, _ := strconv.Atoi(pidStr)
|
|
return pid
|
|
}
|
|
|
|
func processRunning(pidStr string) bool {
|
|
pid := parsePid(pidStr)
|
|
if pid <= 0 {
|
|
return false
|
|
}
|
|
proc, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return proc.Signal(syscall.Signal(0)) == 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())
|
|
fmt.Printf(" Health \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
|
|
if mcpAddr != "" {
|
|
fmt.Printf(" MCP \033[36mhttp://localhost%s\033[0m\n", mcpAddr)
|
|
fmt.Printf(" MCP Tools \033[36mhttp://localhost%s/mcp/tools\033[0m\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(" Cancelled.")
|
|
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"
|
|
}
|