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,240 @@
|
||||
package new
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// TestZeroToOneContract locks the documented getting-started path:
|
||||
// `micro new helloworld` must produce an ordinary Go service that the Go
|
||||
// toolchain can build, run long enough to start, and call through its generated
|
||||
// handler. The generated module is pointed back at this checkout so the
|
||||
// contract stays local and deterministic in CI.
|
||||
//
|
||||
// It shells out to `micro new` (which runs `go mod tidy`) and `go build`, so
|
||||
// it needs the Go toolchain and module access; it is skipped under `-short`.
|
||||
func TestZeroToOneContract(t *testing.T) {
|
||||
generated := generateService(t, "helloworld")
|
||||
|
||||
for _, rel := range []string{"go.mod", "main.go", "handler/helloworld.go", "README.md", "Makefile"} {
|
||||
if _, err := os.Stat(filepath.Join(generated.dir, rel)); err != nil {
|
||||
t.Fatalf("generated file %s: %v", rel, err)
|
||||
}
|
||||
}
|
||||
|
||||
generated.assertLocalModule(t)
|
||||
generated.build(t)
|
||||
generated.run(t)
|
||||
generated.call(t, "Alice", "Hello Alice")
|
||||
}
|
||||
|
||||
// TestZeroToOneNoMCPContract keeps the MCP opt-out path honest. Some services
|
||||
// intentionally run without the local MCP listener, but that variant must still
|
||||
// satisfy the same 0→1 contract: scaffold, tidy, and build without additional
|
||||
// toolchain dependencies.
|
||||
func TestZeroToOneNoMCPContract(t *testing.T) {
|
||||
generated := generateService(t, "worker", "--no-mcp")
|
||||
|
||||
main, err := os.ReadFile(filepath.Join(generated.dir, "main.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(main), "gateway/mcp") || strings.Contains(string(main), "WithMCP") {
|
||||
t.Fatalf("--no-mcp generated main.go with MCP wiring:\n%s", main)
|
||||
}
|
||||
|
||||
generated.assertLocalModule(t)
|
||||
generated.build(t)
|
||||
generated.run(t)
|
||||
generated.call(t, "Bob", "Hello Bob")
|
||||
}
|
||||
|
||||
func TestPrintNextStepsSurfacesFirstAgentPath(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
printNextSteps(&out, "helloworld", false)
|
||||
|
||||
for _, want := range []string{
|
||||
"cd helloworld",
|
||||
"micro agent preflight",
|
||||
"go run .",
|
||||
"micro chat",
|
||||
"micro inspect agent <name>",
|
||||
"micro agent demo",
|
||||
"micro docs",
|
||||
"your-first-agent.html",
|
||||
"zero-to-hero.html",
|
||||
"http://localhost:3001/mcp/tools",
|
||||
} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("next steps missing %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintNextStepsNoMCPSkipsMCPHints(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
printNextSteps(&out, "worker", true)
|
||||
|
||||
for _, want := range []string{"micro agent preflight", "micro chat", "micro inspect agent <name>", "micro agent demo", "micro docs"} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("--no-mcp next steps missing %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
for _, notWant := range []string{"http://localhost:3001/mcp/tools", "micro mcp serve"} {
|
||||
if strings.Contains(out.String(), notWant) {
|
||||
t.Fatalf("--no-mcp next steps should not include %q:\n%s", notWant, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type generatedService struct {
|
||||
dir string
|
||||
repoRoot string
|
||||
}
|
||||
|
||||
func generateService(t *testing.T, name string, args ...string) generatedService {
|
||||
t.Helper()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("contract test shells out to the Go toolchain; skipped with -short")
|
||||
}
|
||||
|
||||
repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MICRO_NEW_GO_MICRO_REPLACE", repoRoot)
|
||||
|
||||
tmp := t.TempDir()
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chdir(oldwd)
|
||||
|
||||
set := flag.NewFlagSet("micro-new", flag.ContinueOnError)
|
||||
set.Bool("no-mcp", false, "")
|
||||
set.Bool("proto", false, "")
|
||||
set.String("template", "", "")
|
||||
set.String("prompt", "", "")
|
||||
set.String("provider", "", "")
|
||||
set.String("api_key", "", "")
|
||||
if err := set.Parse(append(args, name)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := cli.NewContext(cli.NewApp(), set, nil)
|
||||
|
||||
if err := Run(ctx); err != nil {
|
||||
t.Fatalf("micro new %s %s: %v", strings.Join(args, " "), name, err)
|
||||
}
|
||||
|
||||
return generatedService{dir: filepath.Join(tmp, name), repoRoot: repoRoot}
|
||||
}
|
||||
|
||||
func (g generatedService) assertLocalModule(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
modPath := filepath.Join(g.dir, "go.mod")
|
||||
mod, err := os.ReadFile(modPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "replace go-micro.dev/v6 => " + filepath.ToSlash(g.repoRoot)
|
||||
if !strings.Contains(string(mod), want) {
|
||||
t.Fatalf("generated go.mod missing local replace %q:\n%s", want, mod)
|
||||
}
|
||||
}
|
||||
|
||||
func (g generatedService) build(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command("go", "build", "./...")
|
||||
cmd.Dir = g.dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("generated service go build ./... failed: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func (g generatedService) run(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
bin := filepath.Join(g.dir, "service-contract")
|
||||
build := exec.Command("go", "build", "-o", bin, ".")
|
||||
build.Dir = g.dir
|
||||
if out, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("generated service go build -o service-contract . failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin)
|
||||
cmd.Dir = g.dir
|
||||
var out strings.Builder
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("generated service failed to start: %v\n%s", err, out.String())
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("generated service exited early: %v\n%s", err, out.String())
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||
t.Fatalf("failed to stop generated service: %v\n%s", err, out.String())
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatalf("generated service did not stop after kill\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (g generatedService) call(t *testing.T, name, want string) {
|
||||
t.Helper()
|
||||
|
||||
testPath := filepath.Join(g.dir, "handler", "contract_test.go")
|
||||
testSrc := `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeneratedCallContract(t *testing.T) {
|
||||
rsp := new(Response)
|
||||
if err := New().Call(context.Background(), &Request{Name: "` + name + `"}, rsp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rsp.Msg != "` + want + `" {
|
||||
t.Fatalf("Call response = %q, want %q", rsp.Msg, "` + want + `")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(testPath, []byte(testSrc), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "test", "./handler", "-run", "TestGeneratedCallContract", "-count=1")
|
||||
cmd.Dir = g.dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("generated service call contract failed: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
// 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"
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
ApiProtoSRV = `syntax = "proto3";
|
||||
|
||||
package {{dehyphen .Alias}};
|
||||
|
||||
option go_package = "./proto;{{dehyphen .Alias}}";
|
||||
|
||||
service {{title .Alias}} {
|
||||
rpc Health(HealthRequest) returns (HealthResponse) {}
|
||||
rpc Endpoint(EndpointRequest) returns (EndpointResponse) {}
|
||||
}
|
||||
|
||||
message HealthRequest {}
|
||||
|
||||
message HealthResponse {
|
||||
string status = 1;
|
||||
int64 uptime = 2;
|
||||
}
|
||||
|
||||
message EndpointRequest {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
string body = 3;
|
||||
map<string, string> headers = 4;
|
||||
}
|
||||
|
||||
message EndpointResponse {
|
||||
int32 status_code = 1;
|
||||
string body = 2;
|
||||
map<string, string> headers = 3;
|
||||
}
|
||||
`
|
||||
|
||||
ApiHandlerSRV = `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "go-micro.dev/v6/logger"
|
||||
|
||||
pb "{{.Dir}}/proto"
|
||||
)
|
||||
|
||||
type {{title .Alias}} struct {
|
||||
started time.Time
|
||||
routes map[string]http.HandlerFunc
|
||||
}
|
||||
|
||||
func New() *{{title .Alias}} {
|
||||
h := &{{title .Alias}}{
|
||||
started: time.Now(),
|
||||
routes: make(map[string]http.HandlerFunc),
|
||||
}
|
||||
h.registerRoutes()
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *{{title .Alias}}) registerRoutes() {
|
||||
h.routes["GET /hello"] = func(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
name = "World"
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": fmt.Sprintf("Hello %s", name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Health returns the service health status and uptime.
|
||||
//
|
||||
// @example {}
|
||||
func (h *{{title .Alias}}) Health(ctx context.Context, req *pb.HealthRequest, rsp *pb.HealthResponse) error {
|
||||
rsp.Status = "ok"
|
||||
rsp.Uptime = int64(time.Since(h.started).Seconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Endpoint handles proxied HTTP requests. The method and path fields
|
||||
// select the route; body and headers are forwarded.
|
||||
//
|
||||
// @example {"method": "GET", "path": "/hello", "body": "", "headers": {}}
|
||||
func (h *{{title .Alias}}) Endpoint(ctx context.Context, req *pb.EndpointRequest, rsp *pb.EndpointResponse) error {
|
||||
key := fmt.Sprintf("%s %s", req.Method, req.Path)
|
||||
handler, ok := h.routes[key]
|
||||
if !ok {
|
||||
log.Infof("Route not found: %s", key)
|
||||
rsp.StatusCode = 404
|
||||
rsp.Body = ` + "`" + `{"error":"not found"}` + "`" + `
|
||||
return nil
|
||||
}
|
||||
|
||||
rec := &responseRecorder{headers: make(map[string]string), statusCode: 200}
|
||||
fakeReq, _ := http.NewRequestWithContext(ctx, req.Method, req.Path, nil)
|
||||
handler(rec, fakeReq)
|
||||
|
||||
rsp.StatusCode = int32(rec.statusCode)
|
||||
rsp.Body = rec.body
|
||||
rsp.Headers = rec.headers
|
||||
return nil
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
headers map[string]string
|
||||
body string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (r *responseRecorder) Header() http.Header { return http.Header{} }
|
||||
func (r *responseRecorder) WriteHeader(statusCode int) { r.statusCode = statusCode }
|
||||
func (r *responseRecorder) Write(b []byte) (int, error) {
|
||||
r.body = string(b)
|
||||
return len(b), nil
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,225 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
CrudProtoSRV = `syntax = "proto3";
|
||||
|
||||
package {{dehyphen .Alias}};
|
||||
|
||||
option go_package = "./proto;{{dehyphen .Alias}}";
|
||||
|
||||
service {{title .Alias}} {
|
||||
rpc Create(CreateRequest) returns (CreateResponse) {}
|
||||
rpc Read(ReadRequest) returns (ReadResponse) {}
|
||||
rpc Update(UpdateRequest) returns (UpdateResponse) {}
|
||||
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
|
||||
rpc List(ListRequest) returns (ListResponse) {}
|
||||
}
|
||||
|
||||
message {{title .Alias}}Record {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
string phone = 4;
|
||||
string company = 5;
|
||||
int64 created = 6;
|
||||
int64 updated = 7;
|
||||
}
|
||||
|
||||
message CreateRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
string phone = 3;
|
||||
string company = 4;
|
||||
}
|
||||
|
||||
message CreateResponse {
|
||||
{{title .Alias}}Record record = 1;
|
||||
}
|
||||
|
||||
message ReadRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ReadResponse {
|
||||
{{title .Alias}}Record record = 1;
|
||||
}
|
||||
|
||||
message UpdateRequest {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
string phone = 4;
|
||||
string company = 5;
|
||||
}
|
||||
|
||||
message UpdateResponse {
|
||||
{{title .Alias}}Record record = 1;
|
||||
}
|
||||
|
||||
message DeleteRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteResponse {
|
||||
bool deleted = 1;
|
||||
}
|
||||
|
||||
message ListRequest {
|
||||
int64 limit = 1;
|
||||
int64 offset = 2;
|
||||
}
|
||||
|
||||
message ListResponse {
|
||||
repeated {{title .Alias}}Record records = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
`
|
||||
|
||||
CrudHandlerSRV = `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "go-micro.dev/v6/logger"
|
||||
|
||||
pb "{{.Dir}}/proto"
|
||||
)
|
||||
|
||||
type {{title .Alias}} struct {
|
||||
mu sync.RWMutex
|
||||
records map[string]*pb.{{title .Alias}}Record
|
||||
}
|
||||
|
||||
func New() *{{title .Alias}} {
|
||||
return &{{title .Alias}}{
|
||||
records: make(map[string]*pb.{{title .Alias}}Record),
|
||||
}
|
||||
}
|
||||
|
||||
// Create adds a new record and returns it with a generated ID.
|
||||
//
|
||||
// @example {"name": "Alice Smith", "email": "alice@example.com", "phone": "+1-555-0100", "company": "Acme Inc"}
|
||||
func (h *{{title .Alias}}) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
|
||||
log.Infof("Creating record: %s", req.Name)
|
||||
|
||||
now := time.Now().Unix()
|
||||
record := &pb.{{title .Alias}}Record{
|
||||
Id: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Phone: req.Phone,
|
||||
Company: req.Company,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
h.records[record.Id] = record
|
||||
h.mu.Unlock()
|
||||
|
||||
rsp.Record = record
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read retrieves a record by ID.
|
||||
//
|
||||
// @example {"id": "some-uuid"}
|
||||
func (h *{{title .Alias}}) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
|
||||
h.mu.RLock()
|
||||
record, ok := h.records[req.Id]
|
||||
h.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("record %s not found", req.Id)
|
||||
}
|
||||
|
||||
rsp.Record = record
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies an existing record. Only non-empty fields are updated.
|
||||
//
|
||||
// @example {"id": "some-uuid", "name": "Alice Johnson", "email": "alice.j@example.com"}
|
||||
func (h *{{title .Alias}}) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
record, ok := h.records[req.Id]
|
||||
if !ok {
|
||||
return fmt.Errorf("record %s not found", req.Id)
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
record.Name = req.Name
|
||||
}
|
||||
if req.Email != "" {
|
||||
record.Email = req.Email
|
||||
}
|
||||
if req.Phone != "" {
|
||||
record.Phone = req.Phone
|
||||
}
|
||||
if req.Company != "" {
|
||||
record.Company = req.Company
|
||||
}
|
||||
record.Updated = time.Now().Unix()
|
||||
|
||||
rsp.Record = record
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a record by ID.
|
||||
//
|
||||
// @example {"id": "some-uuid"}
|
||||
func (h *{{title .Alias}}) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
|
||||
h.mu.Lock()
|
||||
_, ok := h.records[req.Id]
|
||||
if ok {
|
||||
delete(h.records, req.Id)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
rsp.Deleted = ok
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all records with optional pagination.
|
||||
//
|
||||
// @example {"limit": 10, "offset": 0}
|
||||
func (h *{{title .Alias}}) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
all := make([]*pb.{{title .Alias}}Record, 0, len(h.records))
|
||||
for _, r := range h.records {
|
||||
all = append(all, r)
|
||||
}
|
||||
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].Created > all[j].Created
|
||||
})
|
||||
|
||||
rsp.Total = int64(len(all))
|
||||
|
||||
offset := int(req.Offset)
|
||||
if offset > len(all) {
|
||||
offset = len(all)
|
||||
}
|
||||
limit := int(req.Limit)
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(all) {
|
||||
end = len(all)
|
||||
}
|
||||
|
||||
rsp.Records = all[offset:end]
|
||||
return nil
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
HandlerSRV = `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "go-micro.dev/v6/logger"
|
||||
|
||||
pb "{{.Dir}}/proto"
|
||||
)
|
||||
|
||||
type {{title .Alias}} struct{}
|
||||
|
||||
// Return a new handler.
|
||||
func New() *{{title .Alias}} {
|
||||
return &{{title .Alias}}{}
|
||||
}
|
||||
|
||||
// Call greets a person by name and returns a welcome message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
||||
log.Info("Received {{title .Alias}}.Call request")
|
||||
rsp.Msg = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream sends a sequence of numbered responses back to the caller.
|
||||
// Use this for streaming large result sets or real-time updates.
|
||||
//
|
||||
// @example {"count": 5}
|
||||
func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error {
|
||||
log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count)
|
||||
|
||||
for i := 0; i < int(req.Count); i++ {
|
||||
log.Infof("Responding: %d", i)
|
||||
if err := stream.Send(&pb.StreamingResponse{
|
||||
Count: int64(i),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
// HandlerNoProto is the default handler: plain Go request/response types
|
||||
// registered by reflection. No generated protobuf code, so no protoc.
|
||||
HandlerNoProto = `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
// Request is the input to {{title .Alias}}.Call.
|
||||
type Request struct {
|
||||
Name string ` + "`json:\"name\" description:\"Name to greet (required)\"`" + `
|
||||
}
|
||||
|
||||
// Response is the output of {{title .Alias}}.Call.
|
||||
type Response struct {
|
||||
Msg string ` + "`json:\"msg\"`" + `
|
||||
}
|
||||
|
||||
type {{title .Alias}} struct{}
|
||||
|
||||
// Return a new handler.
|
||||
func New() *{{title .Alias}} {
|
||||
return &{{title .Alias}}{}
|
||||
}
|
||||
|
||||
// Call greets a person by name and returns a welcome message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (e *{{title .Alias}}) Call(ctx context.Context, req *Request, rsp *Response) error {
|
||||
log.Info("Received {{title .Alias}}.Call request")
|
||||
rsp.Msg = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
SubscriberSRV = `package subscriber
|
||||
|
||||
import (
|
||||
"context"
|
||||
log "go-micro.dev/v6/logger"
|
||||
|
||||
pb "{{.Dir}}/proto"
|
||||
)
|
||||
|
||||
type {{title .Alias}} struct{}
|
||||
|
||||
func (e *{{title .Alias}}) Handle(ctx context.Context, msg *pb.Message) error {
|
||||
log.Info("Handler Received message: ", msg.Say)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Handler(ctx context.Context, msg *pb.Message) error {
|
||||
log.Info("Function Received message: ", msg.Say)
|
||||
return nil
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
GitIgnore = `
|
||||
{{.Alias}}
|
||||
.micro
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
MainSRV = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
pb "{{.Dir}}/proto"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("{{lower .Alias}}",
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
|
||||
// Initialize service
|
||||
service.Init()
|
||||
|
||||
// Register handler
|
||||
pb.Register{{title .Alias}}Handler(service.Server(), handler.New())
|
||||
|
||||
// Run service
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
|
||||
MainSRVNoMCP = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
pb "{{.Dir}}/proto"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("{{lower .Alias}}")
|
||||
|
||||
// Initialize service
|
||||
service.Init()
|
||||
|
||||
// Register handler
|
||||
pb.Register{{title .Alias}}Handler(service.Server(), handler.New())
|
||||
|
||||
// Run service
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
|
||||
// MainNoProto is the default template: handlers are registered by
|
||||
// reflection, so the service builds and runs with no protoc toolchain.
|
||||
MainNoProto = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("{{lower .Alias}}",
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
|
||||
// Initialize service
|
||||
service.Init()
|
||||
|
||||
// Register handler (reflection-based — no protoc required)
|
||||
if err := service.Handle(handler.New()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Run service
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
|
||||
MainNoProtoNoMCP = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("{{lower .Alias}}")
|
||||
|
||||
// Initialize service
|
||||
service.Init()
|
||||
|
||||
// Register handler (reflection-based — no protoc required)
|
||||
if err := service.Handle(handler.New()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Run service
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,119 @@
|
||||
package template
|
||||
|
||||
// MakefileNoProto is the default Makefile: no proto target, nothing to
|
||||
// generate, so the service builds and runs with no protoc toolchain.
|
||||
var MakefileNoProto = `.PHONY: build run dev test test-coverage clean docker lint fmt deps
|
||||
|
||||
# Build the service
|
||||
build:
|
||||
go build -o bin/{{.Alias}} .
|
||||
|
||||
# Run the service
|
||||
run:
|
||||
go run .
|
||||
|
||||
# Run with hot reload (requires air: go install github.com/air-verse/air@latest)
|
||||
dev:
|
||||
air
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
go test -v -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# List MCP tools exposed by this service
|
||||
mcp-tools:
|
||||
micro mcp list
|
||||
|
||||
# Start MCP server for Claude Code
|
||||
mcp-serve:
|
||||
micro mcp serve
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin/ coverage.out coverage.html
|
||||
|
||||
# Build Docker image
|
||||
docker:
|
||||
docker build -t {{.Alias}}:latest .
|
||||
|
||||
# Lint code
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
go fmt ./...
|
||||
goimports -w .
|
||||
|
||||
# Update dependencies
|
||||
deps:
|
||||
go mod tidy
|
||||
go mod download
|
||||
`
|
||||
|
||||
var Makefile = `.PHONY: proto build run test clean docker
|
||||
|
||||
# Generate protobuf files
|
||||
proto:
|
||||
protoc --proto_path=. --micro_out=. --go_out=. proto/*.proto
|
||||
|
||||
# Build the service
|
||||
build:
|
||||
go build -o bin/{{.Alias}} .
|
||||
|
||||
# Run the service
|
||||
run:
|
||||
go run .
|
||||
|
||||
# Run with hot reload (requires air: go install github.com/air-verse/air@latest)
|
||||
dev:
|
||||
air
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
go test -v -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# List MCP tools exposed by this service
|
||||
mcp-tools:
|
||||
micro mcp list
|
||||
|
||||
# Test an MCP tool interactively
|
||||
mcp-test:
|
||||
micro mcp test
|
||||
|
||||
# Start MCP server for Claude Code
|
||||
mcp-serve:
|
||||
micro mcp serve
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin/ coverage.out coverage.html
|
||||
|
||||
# Build Docker image
|
||||
docker:
|
||||
docker build -t {{.Alias}}:latest .
|
||||
|
||||
# Lint code
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
go fmt ./...
|
||||
goimports -w .
|
||||
|
||||
# Update dependencies
|
||||
deps:
|
||||
go mod tidy
|
||||
go mod download
|
||||
`
|
||||
@@ -0,0 +1,28 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
Module = `module {{.Dir}}
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
go-micro.dev/v6 {{.MicroVersion}}
|
||||
github.com/golang/protobuf latest
|
||||
google.golang.org/protobuf latest
|
||||
)
|
||||
{{if .MicroReplace}}
|
||||
replace go-micro.dev/v6 => {{.MicroReplace}}
|
||||
{{end}}`
|
||||
|
||||
// ModuleNoProto is the default go.mod: no protobuf dependencies.
|
||||
// MicroVersion is the version this CLI was built from (or "latest"), so a
|
||||
// generated service tracks the framework the user is actually running.
|
||||
ModuleNoProto = `module {{.Dir}}
|
||||
|
||||
go 1.23
|
||||
|
||||
require go-micro.dev/v6 {{.MicroVersion}}
|
||||
{{if .MicroReplace}}
|
||||
replace go-micro.dev/v6 => {{.MicroReplace}}
|
||||
{{end}}`
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
ProtoSRV = `syntax = "proto3";
|
||||
|
||||
package {{dehyphen .Alias}};
|
||||
|
||||
option go_package = "./proto;{{dehyphen .Alias}}";
|
||||
|
||||
service {{title .Alias}} {
|
||||
rpc Call(Request) returns (Response) {}
|
||||
rpc Stream(StreamingRequest) returns (stream StreamingResponse) {}
|
||||
}
|
||||
|
||||
message Message {
|
||||
string say = 1;
|
||||
}
|
||||
|
||||
message Request {
|
||||
// Name of the person to greet
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message Response {
|
||||
// Greeting message
|
||||
string msg = 1;
|
||||
}
|
||||
|
||||
message StreamingRequest {
|
||||
// Number of responses to stream back
|
||||
int64 count = 1;
|
||||
}
|
||||
|
||||
message StreamingResponse {
|
||||
// Current sequence number in the stream
|
||||
int64 count = 1;
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,184 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
PubsubProtoSRV = `syntax = "proto3";
|
||||
|
||||
package {{dehyphen .Alias}};
|
||||
|
||||
option go_package = "./proto;{{dehyphen .Alias}}";
|
||||
|
||||
service {{title .Alias}} {
|
||||
rpc Publish(PublishRequest) returns (PublishResponse) {}
|
||||
rpc Stats(StatsRequest) returns (StatsResponse) {}
|
||||
}
|
||||
|
||||
message Event {
|
||||
string id = 1;
|
||||
string type = 2;
|
||||
string source = 3;
|
||||
string data = 4;
|
||||
int64 timestamp = 5;
|
||||
}
|
||||
|
||||
message PublishRequest {
|
||||
string type = 1;
|
||||
string data = 2;
|
||||
}
|
||||
|
||||
message PublishResponse {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message StatsRequest {}
|
||||
|
||||
message StatsResponse {
|
||||
int64 published = 1;
|
||||
int64 received = 2;
|
||||
}
|
||||
`
|
||||
|
||||
PubsubHandlerSRV = `package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v6/broker"
|
||||
log "go-micro.dev/v6/logger"
|
||||
|
||||
pb "{{.Dir}}/proto"
|
||||
)
|
||||
|
||||
const Topic = "{{lower .Alias}}.events"
|
||||
|
||||
type {{title .Alias}} struct {
|
||||
broker broker.Broker
|
||||
published atomic.Int64
|
||||
received atomic.Int64
|
||||
}
|
||||
|
||||
func New(b broker.Broker) *{{title .Alias}} {
|
||||
return &{{title .Alias}}{broker: b}
|
||||
}
|
||||
|
||||
// Publish sends an event to the message broker.
|
||||
//
|
||||
// @example {"type": "user.created", "data": "{\"id\": \"123\", \"name\": \"Alice\"}"}
|
||||
func (h *{{title .Alias}}) Publish(ctx context.Context, req *pb.PublishRequest, rsp *pb.PublishResponse) error {
|
||||
event := &pb.Event{
|
||||
Id: uuid.New().String(),
|
||||
Type: req.Type,
|
||||
Source: "{{lower .Alias}}",
|
||||
Data: req.Data,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
body, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.broker.Publish(Topic, &broker.Message{Body: body}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.published.Add(1)
|
||||
log.Infof("Published event %s type=%s", event.Id, event.Type)
|
||||
|
||||
rsp.Id = event.Id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns the number of events published and received.
|
||||
//
|
||||
// @example {}
|
||||
func (h *{{title .Alias}}) Stats(ctx context.Context, req *pb.StatsRequest, rsp *pb.StatsResponse) error {
|
||||
rsp.Published = h.published.Load()
|
||||
rsp.Received = h.received.Load()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe sets up a subscription to the event topic. Call this
|
||||
// after the service has started.
|
||||
func (h *{{title .Alias}}) Subscribe() error {
|
||||
_, err := h.broker.Subscribe(Topic, func(p broker.Event) error {
|
||||
h.received.Add(1)
|
||||
|
||||
var event pb.Event
|
||||
if err := json.Unmarshal(p.Message().Body, &event); err != nil {
|
||||
log.Errorf("Failed to unmarshal event: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Received event %s type=%s data=%s", event.Id, event.Type, event.Data)
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
`
|
||||
|
||||
PubsubMainSRV = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
pb "{{.Dir}}/proto"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("{{lower .Alias}}",
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
h := handler.New(service.Options().Broker)
|
||||
pb.Register{{title .Alias}}Handler(service.Server(), h)
|
||||
|
||||
// Subscribe to events after service starts
|
||||
go func() {
|
||||
if err := h.Subscribe(); err != nil {
|
||||
log.Fatalf("Failed to subscribe: %v", err)
|
||||
}
|
||||
log.Info("Subscribed to ", handler.Topic)
|
||||
}()
|
||||
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
|
||||
PubsubMainSRVNoMCP = `package main
|
||||
|
||||
import (
|
||||
"{{.Dir}}/handler"
|
||||
pb "{{.Dir}}/proto"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("{{lower .Alias}}")
|
||||
|
||||
service.Init()
|
||||
|
||||
h := handler.New(service.Options().Broker)
|
||||
pb.Register{{title .Alias}}Handler(service.Server(), h)
|
||||
|
||||
go func() {
|
||||
if err := h.Subscribe(); err != nil {
|
||||
log.Fatalf("Failed to subscribe: %v", err)
|
||||
}
|
||||
log.Info("Subscribed to ", handler.Topic)
|
||||
}()
|
||||
|
||||
service.Run()
|
||||
}
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
package template
|
||||
|
||||
var (
|
||||
// ReadmeNoProto is the default README: handlers are reflection-based, so
|
||||
// there is no proto generation step and no protoc prerequisite.
|
||||
ReadmeNoProto = `# {{title .Alias}} Service
|
||||
|
||||
Generated with
|
||||
|
||||
` + "```" + `
|
||||
micro new {{.Alias}}
|
||||
` + "```" + `
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run the service — no code generation, no protoc, nothing else to install:
|
||||
|
||||
` + "```bash" + `
|
||||
go run .
|
||||
` + "```" + `
|
||||
|
||||
Handlers are registered by reflection from plain Go types, so request and
|
||||
response structs are defined directly in ` + "`handler/`" + `. To use Protocol
|
||||
Buffers instead, regenerate with ` + "`micro new {{.Alias}} --proto`" + `.
|
||||
|
||||
## MCP & AI Agents
|
||||
|
||||
This service is MCP-enabled by default. When running, AI agents can discover
|
||||
and call your service endpoints automatically.
|
||||
|
||||
**MCP tools endpoint:** http://localhost:3001/mcp/tools
|
||||
|
||||
### Test with curl
|
||||
|
||||
` + "```bash" + `
|
||||
# List available tools
|
||||
curl http://localhost:3001/mcp/tools | jq
|
||||
|
||||
# Call the service via MCP
|
||||
curl -X POST http://localhost:3001/mcp/call \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"tool": "{{lower .Alias}}.{{title .Alias}}.Call", "arguments": {"name": "Alice"}}'
|
||||
` + "```" + `
|
||||
|
||||
### Use with Claude Code
|
||||
|
||||
` + "```bash" + `
|
||||
# Start MCP server for Claude Code
|
||||
micro mcp serve
|
||||
` + "```" + `
|
||||
|
||||
### Writing Good Tool Descriptions
|
||||
|
||||
AI agents work best when your handler methods have clear doc comments:
|
||||
|
||||
` + "```go" + `
|
||||
// CreateUser registers a new user account with the given email and name.
|
||||
// Returns the created user with their assigned ID.
|
||||
//
|
||||
// @example {"email": "alice@example.com", "name": "Alice Smith"}
|
||||
func (s *Users) CreateUser(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
||||
// ...
|
||||
}
|
||||
` + "```" + `
|
||||
|
||||
See the [tool descriptions guide](https://go-micro.dev/docs/guides/tool-descriptions) for more tips.
|
||||
|
||||
## Development
|
||||
|
||||
` + "```bash" + `
|
||||
make build # Build binary
|
||||
make test # Run tests
|
||||
make dev # Run with hot reload (requires air)
|
||||
` + "```" + `
|
||||
`
|
||||
|
||||
Readme = `# {{title .Alias}} Service
|
||||
|
||||
Generated with
|
||||
|
||||
` + "```" + `
|
||||
micro new {{.Alias}}
|
||||
` + "```" + `
|
||||
|
||||
## Getting Started
|
||||
|
||||
Generate the proto code:
|
||||
|
||||
` + "```bash" + `
|
||||
make proto
|
||||
` + "```" + `
|
||||
|
||||
Run the service:
|
||||
|
||||
` + "```bash" + `
|
||||
go run .
|
||||
` + "```" + `
|
||||
|
||||
## MCP & AI Agents
|
||||
|
||||
This service is MCP-enabled by default. When running, AI agents can discover
|
||||
and call your service endpoints automatically.
|
||||
|
||||
**MCP tools endpoint:** http://localhost:3001/mcp/tools
|
||||
|
||||
### Test with curl
|
||||
|
||||
` + "```bash" + `
|
||||
# List available tools
|
||||
curl http://localhost:3001/mcp/tools | jq
|
||||
|
||||
# Call the service via MCP
|
||||
curl -X POST http://localhost:3001/mcp/call \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"tool": "{{lower .Alias}}.{{title .Alias}}.Call", "arguments": {"name": "Alice"}}'
|
||||
` + "```" + `
|
||||
|
||||
### Use with Claude Code
|
||||
|
||||
` + "```bash" + `
|
||||
# Start MCP server for Claude Code
|
||||
micro mcp serve
|
||||
` + "```" + `
|
||||
|
||||
Or add to your Claude Code config:
|
||||
|
||||
` + "```json" + `
|
||||
{
|
||||
"mcpServers": {
|
||||
"{{lower .Alias}}": {
|
||||
"command": "micro",
|
||||
"args": ["mcp", "serve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
` + "```" + `
|
||||
|
||||
### Writing Good Tool Descriptions
|
||||
|
||||
AI agents work best when your handler methods have clear doc comments:
|
||||
|
||||
` + "```go" + `
|
||||
// CreateUser registers a new user account with the given email and name.
|
||||
// Returns the created user with their assigned ID.
|
||||
//
|
||||
// @example {"email": "alice@example.com", "name": "Alice Smith"}
|
||||
func (s *Users) CreateUser(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
||||
// ...
|
||||
}
|
||||
` + "```" + `
|
||||
|
||||
See the [tool descriptions guide](https://go-micro.dev/docs/guides/tool-descriptions) for more tips.
|
||||
|
||||
## Development
|
||||
|
||||
` + "```bash" + `
|
||||
make proto # Regenerate proto code
|
||||
make build # Build binary
|
||||
make test # Run tests
|
||||
make dev # Run with hot reload (requires air)
|
||||
` + "```" + `
|
||||
`
|
||||
)
|
||||
Reference in New Issue
Block a user