Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

486 lines
12 KiB
Go

// 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 <name>")
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"
}