feat: fun new 项目脚手架——一条命令生成可运行 hello world

- fun new <名字|模块路径>:go.mod + HelloSvc.Say 示例 + README + .gitignore
- -local 写 replace 指向本地 fun 源码(离线/未发布版本也能跑)
- -port 指定示例端口;flag 手动解析可放任意位置
- 自动 go mod tidy,失败给出手动提示

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-09-19 16:14:25 +08:00
co-authored by Devin
parent 759f5f65a2
commit 7d5c6b06f7
3 changed files with 174 additions and 1 deletions
+154
View File
@@ -0,0 +1,154 @@
package main
// fun new:脚手架——一条命令生成可运行的 fun 项目骨架
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var reModuleName = regexp.MustCompile(`^[a-zA-Z0-9][\w.\-/]*$`)
func cmdNew(args []string) {
// 手动解析:允许 flag 放任意位置(go flag 遇位置参数即停,不友好)
port, local, module := 18080, "", ""
for i := 0; i < len(args); i++ {
a := args[i]
if v, ok := strings.CutPrefix(a, "-port="); ok {
fmt.Sscanf(v, "%d", &port)
continue
}
if v, ok := strings.CutPrefix(a, "-local="); ok {
local = v
continue
}
if (a == "-port" || a == "-local") && i+1 < len(args) {
i++
if a == "-port" {
fmt.Sscanf(args[i], "%d", &port)
} else {
local = args[i]
}
continue
}
if strings.HasPrefix(a, "-") {
fmt.Fprintln(os.Stderr, "fun: 未知参数:", a)
os.Exit(2)
}
if module != "" {
fmt.Fprintln(os.Stderr, "用法: fun new <项目名|模块路径> [-port 端口] [-local fun源码路径]")
os.Exit(2)
}
module = a
}
if module == "" {
fmt.Fprintln(os.Stderr, "用法: fun new <项目名|模块路径> [-port 端口] [-local fun源码路径]")
fmt.Fprintln(os.Stderr, "示例: fun new myapp fun new github.com/me/myapp")
os.Exit(2)
}
if !reModuleName.MatchString(module) || strings.Contains(module, "..") {
fmt.Fprintln(os.Stderr, "fun: 非法项目名:", module)
os.Exit(1)
}
// 模块路径形如 github.com/me/app 时,目录取最后一段
dir := module
if i := strings.LastIndex(module, "/"); i >= 0 {
dir = module[i+1:]
}
if _, err := os.Stat(dir); err == nil {
fmt.Fprintf(os.Stderr, "fun: 目录 %s 已存在\n", dir)
os.Exit(1)
}
if err := os.MkdirAll(dir, 0o755); err != nil {
fmt.Fprintln(os.Stderr, "fun: 创建目录失败:", err)
os.Exit(1)
}
gomod := fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire github.com/cyi-cc/fun v1.4.1\n", module)
if local != "" {
abs, err := filepath.Abs(local)
if err != nil {
abs = local
}
gomod += fmt.Sprintf("\nreplace github.com/cyi-cc/fun => %s\n", filepath.ToSlash(abs))
}
files := map[string]string{
"go.mod": gomod,
"main.go": fmt.Sprintf(`package main
import "github.com/cyi-cc/fun"
type HelloDto struct {
Name string
}
type HelloSvc struct {
fun.Ctx
}
// hello world:最小可调用的服务方法
func (s *HelloSvc) Say(dto HelloDto) (string, error) {
return "hello, " + dto.Name + "!", nil
}
func main() {
f := fun.New()
f.BindService(&HelloSvc{})
f.Start(%d)
}
`, port),
".gitignore": "/gen\n/log\nfun-docs-edits.json\n",
"README.md": fmt.Sprintf(`# %s
fun 框架项目。[文档](https://fungo.ink)
## 启动
`+"```bash"+`
go run .
`+"```"+`
## 工具链(fun-cli)
`+"```bash"+`
fun docs # API 文档站 + 在线调测(热更新)
fun run # 热更新开发循环
fun gen ts # 生成 TypeScript 客户端
`+"```"+`
`, module),
}
for name, body := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "fun: 写入 %s 失败: %v\n", name, err)
os.Exit(1)
}
}
tidy := exec.Command("go", "mod", "tidy")
tidy.Dir = dir
tidy.Stdout, tidy.Stderr = os.Stderr, os.Stderr
tidied := tidy.Run() == nil
next := "go run ."
if !tidied {
next = "go mod tidy && go run ."
}
fmt.Printf(`
已创建 %s(module %s)
─────────────────────────────────────────
%s/go.mod 依赖 github.com/cyi-cc/fun
%s/main.go HelloSvc.Say 示例服务
─────────────────────────────────────────
下一步:
cd %s
%s # 启动服务(:%d)
fun docs # 文档站在线调测
`, dir, module, dir, dir, dir, next, port)
if !tidied {
fmt.Println("\n ⚠ go mod tidy 失败(可能离线),进入目录后手动执行一次即可")
}
}