// Package new generates micro service templates package new import ( "bufio" "context" "fmt" "go/build" "io" "os" "os/exec" "os/signal" "path" "path/filepath" "runtime" "runtime/debug" "strings" "syscall" "go-micro.dev/v6/cmd/micro/cli/generate" "text/template" "time" "github.com/urfave/cli/v2" "github.com/xlab/treeprint" tmpl "go-micro.dev/v6/cmd/micro/cli/new/template" ) type config struct { // foo Alias string // github.com/micro/foo Dir string // $GOPATH/src/github.com/micro/foo GoDir string // $GOPATH GoPath string // UseGoPath UseGoPath bool // MicroVersion is the go-micro version to require in go.mod MicroVersion string // MicroReplace optionally points generated services at a local go-micro checkout. MicroReplace string // Files Files []file // Comments Comments []string } // microVersion returns the go-micro version this CLI was built from, so a // generated service requires the same framework version the user is running. // Falls back to "latest" for local/dev builds (resolved by 'go mod tidy'). func microVersion() string { bi, ok := debug.ReadBuildInfo() if !ok { return "latest" } isRelease := func(v string) bool { return strings.HasPrefix(v, "v") && !strings.Contains(v, "devel") } // cmd/micro is part of the go-micro.dev/v6 module, so for an installed // binary the main module version is the framework version. if bi.Main.Path == "go-micro.dev/v6" && isRelease(bi.Main.Version) { return bi.Main.Version } for _, dep := range bi.Deps { if dep.Path == "go-micro.dev/v6" && isRelease(dep.Version) { return dep.Version } } return "latest" } func microReplace() string { return filepath.ToSlash(os.Getenv("MICRO_NEW_GO_MICRO_REPLACE")) } type file struct { Path string Tmpl string } func write(c config, file, tmpl string) error { fn := template.FuncMap{ "title": func(s string) string { return strings.ReplaceAll(strings.Title(s), "-", "") }, "dehyphen": func(s string) string { return strings.ReplaceAll(s, "-", "") }, "lower": func(s string) string { return strings.ToLower(s) }, } f, err := os.Create(file) if err != nil { return err } defer f.Close() t, err := template.New("f").Funcs(fn).Parse(tmpl) if err != nil { return err } return t.Execute(f, c) } func create(c config) error { // check if dir exists if _, err := os.Stat(c.Dir); !os.IsNotExist(err) { return fmt.Errorf("%s already exists", c.Dir) } fmt.Println() fmt.Println(" \033[1mmicro new\033[0m") fmt.Println() fmt.Printf(" Creating \033[36m%s\033[0m\n\n", c.Alias) t := treeprint.New() // write the files for _, file := range c.Files { f := filepath.Join(c.Dir, file.Path) dir := filepath.Dir(f) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { return err } } addFileToTree(t, file.Path) if err := write(c, f, file.Tmpl); err != nil { return err } } // print tree fmt.Println(t.String()) for _, comment := range c.Comments { fmt.Println(comment) } // just wait <-time.After(time.Millisecond * 250) return nil } func addFileToTree(root treeprint.Tree, file string) { split := strings.Split(file, "/") curr := root for i := 0; i < len(split)-1; i++ { n := curr.FindByValue(split[i]) if n != nil { curr = n } else { curr = curr.AddBranch(split[i]) } } if curr.FindByValue(split[len(split)-1]) == nil { curr.AddNode(split[len(split)-1]) } } func Run(ctx *cli.Context) error { // Handle --prompt: design services with AI, then generate each one if prompt := ctx.String("prompt"); prompt != "" { return runPrompt(ctx, prompt) } dir := ctx.Args().First() if len(dir) == 0 { fmt.Println("specify service name") return nil } // check if the path is absolute, we don't want this // we want to a relative path so we can install in GOPATH if path.IsAbs(dir) { fmt.Println("require relative path as service will be installed in GOPATH") return nil } var goPath string var goDir string goPath = build.Default.GOPATH // don't know GOPATH, runaway.... if len(goPath) == 0 { fmt.Println("unknown GOPATH") return nil } // attempt to split path if not windows if runtime.GOOS == "windows" { goPath = strings.Split(goPath, ";")[0] } else { goPath = strings.Split(goPath, ":")[0] } goDir = filepath.Join(goPath, "src", path.Clean(dir)) noMCP := ctx.Bool("no-mcp") templateName := ctx.String("template") // The default template is protoless: handlers are registered by // reflection, so the service builds and runs with no protoc toolchain. // --proto opts into Protocol Buffers; the named templates (crud, pubsub, // api) are proto-based and imply it. useProto := ctx.Bool("proto") || (templateName != "" && templateName != "default") c := config{ Alias: dir, Comments: nil, Dir: dir, GoDir: goDir, GoPath: goPath, UseGoPath: false, MicroVersion: microVersion(), MicroReplace: microReplace(), } if useProto { mainTmpl, handlerTmpl, protoTmpl := selectTemplates(templateName, noMCP) c.Files = []file{ {"main.go", mainTmpl}, {"handler/" + dir + ".go", handlerTmpl}, {"proto/" + dir + ".proto", protoTmpl}, {"Makefile", tmpl.Makefile}, {"README.md", tmpl.Readme}, {".gitignore", tmpl.GitIgnore}, } } else { mainTmpl := tmpl.MainNoProto if noMCP { mainTmpl = tmpl.MainNoProtoNoMCP } c.Files = []file{ {"main.go", mainTmpl}, {"handler/" + dir + ".go", tmpl.HandlerNoProto}, {"Makefile", tmpl.MakefileNoProto}, {"README.md", tmpl.ReadmeNoProto}, {".gitignore", tmpl.GitIgnore}, } } // set gomodule if os.Getenv("GO111MODULE") != "off" { mod := tmpl.ModuleNoProto if useProto { mod = tmpl.Module } c.Files = append(c.Files, file{"go.mod", mod}) } // create the files if err := create(c); err != nil { return err } // Resolve dependencies. fmt.Println("\nRunning 'go mod tidy'...") if err := runInDir(dir, "go mod tidy"); err != nil { fmt.Printf("Error running 'go mod tidy': %v\n", err) } // Generate protobuf code only when the proto workflow is used, and only // when the toolchain is present. Otherwise print install instructions // rather than failing with a cryptic error. if useProto { if missing := missingProtoTools(); len(missing) > 0 { printProtoInstall(dir, missing) } else { fmt.Println("Running 'make proto'...") if err := runInDir(dir, "make proto"); err != nil { fmt.Printf("Error running 'make proto': %v\n", err) } } } // Print updated tree including generated files fmt.Println("\nProject structure:") printTree(dir) fmt.Println() fmt.Printf(" \033[32m✓\033[0m Service \033[36m%s\033[0m created\n\n", dir) printNextSteps(os.Stdout, dir, noMCP) return nil } func printNextSteps(w io.Writer, dir string, noMCP bool) { fmt.Fprintln(w, " Next steps:") fmt.Fprintf(w, " cd %s\n", dir) fmt.Fprintln(w, " micro agent preflight") fmt.Fprintln(w, " go run .") fmt.Fprintln(w, " micro chat") fmt.Fprintln(w, " micro inspect agent ") fmt.Fprintln(w) fmt.Fprintln(w, " First-agent path:") fmt.Fprintln(w, " micro agent demo") fmt.Fprintln(w, " micro docs") fmt.Fprintln(w, " https://go-micro.dev/docs/guides/your-first-agent.html") fmt.Fprintln(w, " https://go-micro.dev/docs/guides/zero-to-hero.html") if !noMCP { fmt.Fprintln(w) fmt.Fprintf(w, " MCP tools \033[36mhttp://localhost:3001/mcp/tools\033[0m\n") fmt.Fprintln(w, " Claude Code \033[2mmicro mcp serve\033[0m") } fmt.Fprintln(w) } func selectTemplates(name string, noMCP bool) (mainTmpl, handlerTmpl, protoTmpl string) { switch name { case "crud": if noMCP { mainTmpl = tmpl.MainSRVNoMCP } else { mainTmpl = tmpl.MainSRV } return mainTmpl, tmpl.CrudHandlerSRV, tmpl.CrudProtoSRV case "pubsub": if noMCP { mainTmpl = tmpl.PubsubMainSRVNoMCP } else { mainTmpl = tmpl.PubsubMainSRV } return mainTmpl, tmpl.PubsubHandlerSRV, tmpl.PubsubProtoSRV case "api": if noMCP { mainTmpl = tmpl.MainSRVNoMCP } else { mainTmpl = tmpl.MainSRV } return mainTmpl, tmpl.ApiHandlerSRV, tmpl.ApiProtoSRV default: if noMCP { mainTmpl = tmpl.MainSRVNoMCP } else { mainTmpl = tmpl.MainSRV } return mainTmpl, tmpl.HandlerSRV, tmpl.ProtoSRV } } // missingProtoTools returns the protobuf tools needed by `make proto` that // are not on the PATH. func missingProtoTools() []string { var missing []string for _, tool := range []string{"protoc", "protoc-gen-go", "protoc-gen-micro"} { if _, err := exec.LookPath(tool); err != nil { missing = append(missing, tool) } } return missing } // printProtoInstall tells the user exactly what to install to generate the // protobuf code, instead of failing with a cryptic plugin error. func printProtoInstall(dir string, missing []string) { fmt.Println() fmt.Printf(" \033[33m!\033[0m This service uses Protocol Buffers, but these tools are missing: %s\n", strings.Join(missing, ", ")) fmt.Println() fmt.Println(" Install them:") fmt.Println(" protoc https://github.com/protocolbuffers/protobuf/releases (or via your package manager)") fmt.Println(" protoc-gen-go go install google.golang.org/protobuf/cmd/protoc-gen-go@latest") fmt.Println(" protoc-gen-micro go install go-micro.dev/v6/cmd/protoc-gen-micro@latest") fmt.Println() fmt.Printf(" Then generate the code:\n cd %s && make proto && go run .\n", dir) fmt.Println() } func runInDir(dir, cmd string) error { parts := strings.Fields(cmd) c := exec.Command(parts[0], parts[1:]...) c.Dir = dir c.Stdout = os.Stdout c.Stderr = os.Stderr return c.Run() } func printTree(dir string) { t := treeprint.New() walk := func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, _ := filepath.Rel(dir, path) if rel == "." { return nil } parts := strings.Split(rel, string(os.PathSeparator)) curr := t for i := 0; i < len(parts)-1; i++ { n := curr.FindByValue(parts[i]) if n != nil { curr = n } else { curr = curr.AddBranch(parts[i]) } } if !info.IsDir() { curr.AddNode(parts[len(parts)-1]) } return nil } _ = filepath.Walk(dir, walk) fmt.Println(t.String()) } func runPrompt(cliCtx *cli.Context, prompt string) error { provider := cliCtx.String("provider") apiKey := cliCtx.String("api_key") if apiKey == "" { // Try provider-specific env vars 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 new --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() fmt.Println(" \033[32m✓\033[0m All services generated") fmt.Println() fmt.Println(" Next steps:") fmt.Println(" micro run \033[2m# start all services\033[0m") fmt.Println(" micro chat --provider anthropic \033[2m# talk to them\033[0m") fmt.Println() return nil } 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" }