micro--go-micro
ffc3447893
The README and examples teach a simple pattern using plain Go structs and JSON encoding, but `micro new` only generated protobuf services requiring protoc. This mismatch was the biggest DX gap for new users. Now `micro new helloworld` generates a single-file service using plain Go structs — no protobuf, no code generation, no external tools. Just Go. Use `micro new --proto helloworld` for the previous protobuf behavior. - Add simple mode templates (main.go, Makefile, README, go.mod) - Make simple mode the default, add --proto flag for protobuf - Add tests for template parsing and service generation - Move protoc check to only run when --proto is used https://claude.ai/code/session_01VwPw7hMaVhFfT69oCE6x1D
344 行
7.5 KiB
Go
344 行
7.5 KiB
Go
// Package new generates micro service templates
|
|
package new
|
|
|
|
import (
|
|
"fmt"
|
|
"go/build"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"github.com/xlab/treeprint"
|
|
tmpl "go-micro.dev/v5/cmd/micro/cli/new/template"
|
|
)
|
|
|
|
func protoComments(goDir, alias string) []string {
|
|
return []string{
|
|
"\ndownload protoc zip packages (protoc-$VERSION-$PLATFORM.zip) and install:\n",
|
|
"visit https://github.com/protocolbuffers/protobuf/releases",
|
|
"\ncompile the proto file " + alias + ".proto:\n",
|
|
"cd " + alias,
|
|
"go mod tidy",
|
|
"make proto\n",
|
|
}
|
|
}
|
|
|
|
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
|
|
// Files
|
|
Files []file
|
|
// Comments
|
|
Comments []string
|
|
}
|
|
|
|
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.Printf("Creating service %s\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 {
|
|
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")
|
|
useProto := ctx.Bool("proto")
|
|
|
|
if useProto {
|
|
return runProto(ctx, dir, goDir, goPath, noMCP)
|
|
}
|
|
|
|
return runSimple(dir, goDir, goPath, noMCP)
|
|
}
|
|
|
|
// runSimple generates a service using plain Go structs and JSON encoding.
|
|
// No protobuf, no external tools — just Go.
|
|
func runSimple(dir, goDir, goPath string, noMCP bool) error {
|
|
mainTmpl := tmpl.SimpleMainMCP
|
|
readmeTmpl := tmpl.SimpleReadmeMCP
|
|
moduleTmpl := tmpl.SimpleModule
|
|
if noMCP {
|
|
mainTmpl = tmpl.SimpleMain
|
|
readmeTmpl = tmpl.SimpleReadme
|
|
moduleTmpl = tmpl.SimpleModule
|
|
}
|
|
|
|
c := config{
|
|
Alias: dir,
|
|
Dir: dir,
|
|
GoDir: goDir,
|
|
GoPath: goPath,
|
|
UseGoPath: false,
|
|
Files: []file{
|
|
{"main.go", mainTmpl},
|
|
{"Makefile", tmpl.SimpleMakefile},
|
|
{"README.md", readmeTmpl},
|
|
{".gitignore", tmpl.GitIgnore},
|
|
},
|
|
}
|
|
|
|
if os.Getenv("GO111MODULE") != "off" {
|
|
c.Files = append(c.Files, file{"go.mod", moduleTmpl})
|
|
}
|
|
|
|
if err := create(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Run go mod tidy
|
|
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)
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf("Service %s created successfully!\n\n", dir)
|
|
fmt.Println("Next steps:")
|
|
fmt.Printf(" cd %s\n", dir)
|
|
fmt.Println(" go run .")
|
|
if !noMCP {
|
|
fmt.Println()
|
|
fmt.Println("Your service is MCP-enabled. Once running:")
|
|
fmt.Println(" MCP tools: http://localhost:3001/mcp/tools")
|
|
fmt.Println(" Claude Code: micro mcp serve")
|
|
}
|
|
fmt.Println()
|
|
fmt.Println("To generate a protobuf service instead, use:")
|
|
fmt.Printf(" micro new --proto %s\n", dir)
|
|
fmt.Println()
|
|
return nil
|
|
}
|
|
|
|
// runProto generates a protobuf-based service with code generation.
|
|
func runProto(ctx *cli.Context, dir, goDir, goPath string, noMCP bool) error {
|
|
// Check for protoc
|
|
if _, err := exec.LookPath("protoc"); err != nil {
|
|
fmt.Println("WARNING: protoc is not installed or not in your PATH.")
|
|
fmt.Println("Please install protoc from https://github.com/protocolbuffers/protobuf/releases")
|
|
fmt.Println("After installing, re-run 'make proto' in your service directory if needed.")
|
|
}
|
|
|
|
mainTmpl := tmpl.MainSRV
|
|
if noMCP {
|
|
mainTmpl = tmpl.MainSRVNoMCP
|
|
}
|
|
|
|
c := config{
|
|
Alias: dir,
|
|
Comments: nil,
|
|
Dir: dir,
|
|
GoDir: goDir,
|
|
GoPath: goPath,
|
|
UseGoPath: false,
|
|
Files: []file{
|
|
{"main.go", mainTmpl},
|
|
{"handler/" + dir + ".go", tmpl.HandlerSRV},
|
|
{"proto/" + dir + ".proto", tmpl.ProtoSRV},
|
|
{"Makefile", tmpl.Makefile},
|
|
{"README.md", tmpl.Readme},
|
|
{".gitignore", tmpl.GitIgnore},
|
|
},
|
|
}
|
|
|
|
// set gomodule
|
|
if os.Getenv("GO111MODULE") != "off" {
|
|
c.Files = append(c.Files, file{"go.mod", tmpl.Module})
|
|
}
|
|
|
|
// create the files
|
|
if err := create(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Run go mod tidy and make proto
|
|
fmt.Println("\nRunning 'go mod tidy' and 'make proto'...")
|
|
if err := runInDir(dir, "go mod tidy"); err != nil {
|
|
fmt.Printf("Error running 'go mod tidy': %v\n", err)
|
|
}
|
|
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 after 'make proto':")
|
|
printTree(dir)
|
|
|
|
fmt.Println()
|
|
fmt.Printf("Service %s created successfully!\n\n", dir)
|
|
fmt.Println("Next steps:")
|
|
fmt.Printf(" cd %s\n", dir)
|
|
fmt.Println(" go run .")
|
|
if !noMCP {
|
|
fmt.Println()
|
|
fmt.Println("Your service is MCP-enabled. Once running:")
|
|
fmt.Println(" MCP tools: http://localhost:3001/mcp/tools")
|
|
fmt.Println(" Claude Code: micro mcp serve")
|
|
}
|
|
fmt.Println()
|
|
return nil
|
|
}
|
|
|
|
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())
|
|
}
|