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
+13 -1
View File
@@ -17,7 +17,19 @@ fun 的服务注册全在 `BindService`/`BindServiceForGen` 调用里。fun-cli
(服务定义在 `package main` 也不怕)——执行后立即删除。生成由 fun 公开的 (服务定义在 `package main` 也不怕)——执行后立即删除。生成由 fun 公开的
`BindServiceForGen + SetOutput + GenCode` 完成,产物与内置生成器完全一致。 `BindServiceForGen + SetOutput + GenCode` 完成,产物与内置生成器完全一致。
## 三个命令 ## 四个命令
### fun new —— 项目脚手架
```bash
fun new myapp # 生成 go.mod + hello world 服务
fun new github.com/me/myapp # 模块路径形式,目录取末段
fun new myapp -local ../fun # replace 指向本地 fun 源码(离线/未发布)
fun new myapp -port 9000 # 指定示例服务端口(默认 18080)
```
生成 `go.mod`、`main.go`(HelloSvc.Say 示例)、`README.md`、`.gitignore`,
自动 `go mod tidy` 后即可 `go run .` 启动。
### fun gen —— 生成客户端代码 ### fun gen —— 生成客户端代码
+7
View File
@@ -2,6 +2,7 @@ package main
// fun-cli:fun 框架的配套命令行工具 // fun-cli:fun 框架的配套命令行工具
// //
// fun new 脚手架:生成可运行的 fun 项目骨架
// fun gen [ts|go] 生成 TypeScript / Go 客户端 // fun gen [ts|go] 生成 TypeScript / Go 客户端
// fun run 热更新开发循环 // fun run 热更新开发循环
// fun docs 拉起 API 文档站(参数表单 + 在线发送测试) // fun docs 拉起 API 文档站(参数表单 + 在线发送测试)
@@ -16,6 +17,9 @@ const version = "1.4.1"
const usage = `fun-cli ` + version + ` — fun 框架配套工具 const usage = `fun-cli ` + version + ` — fun 框架配套工具
用法: 用法:
fun new <项目名|模块路径> [-port 端口]
脚手架:生成 go.mod + 示例服务 + README,go mod tidy 后即可 go run。
fun gen [ts|go] [-o 目录] [-p 模块路径] [-from meta.json] fun gen [ts|go] [-o 目录] [-p 模块路径] [-from meta.json]
生成客户端代码(fun 核心直接产出,与内置 GenCode 完全一致)。 生成客户端代码(fun 核心直接产出,与内置 GenCode 完全一致)。
扫描模块内 BindService/BindServiceForGen 调用,在对应包目录临时 扫描模块内 BindService/BindServiceForGen 调用,在对应包目录临时
@@ -30,6 +34,7 @@ const usage = `fun-cli ` + version + ` — fun 框架配套工具
流式方法实时展示 NDJSON 行。默认自动 go run 拉起后端并反向代理 /cell。 流式方法实时展示 NDJSON 行。默认自动 go run 拉起后端并反向代理 /cell。
示例: 示例:
fun new myapp
fun gen ts -o frontend/src/api fun gen ts -o frontend/src/api
fun run -p ./cmd/server fun run -p ./cmd/server
fun docs -upstream http://127.0.0.1:9000 -p . fun docs -upstream http://127.0.0.1:9000 -p .
@@ -44,6 +49,8 @@ func main() {
os.Exit(2) os.Exit(2)
} }
switch os.Args[1] { switch os.Args[1] {
case "new":
cmdNew(os.Args[2:])
case "gen": case "gen":
cmdGen(os.Args[2:]) cmdGen(os.Args[2:])
case "run": case "run":
+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 失败(可能离线),进入目录后手动执行一次即可")
}
}