package main import ( _ "embed" "fmt" "github.com/ssgo/u" "io" "os" "os/exec" "path" "path/filepath" "regexp" "strings" "text/template" ) var version = "v0.0.6" //go:embed templates/_makePluginCode.go var makePluginCodeTPL string //go:embed templates/_main.go var mainCodeTPL string //go:embed templates/_main_test.go var mainTestCodeTPL string //go:embed templates/_main.js var mainJSCodeTPL string //go:embed templates/_plugin.go var pluginCodeTPL string //go:embed templates/_plugin_test.go var pluginTestCodeTPL string //go:embed templates/_plugin_test.js var pluginTestJSCodeTPL string //go:embed templates/_gitignore var gitignoreTPL string //go:embed templates/_gitignore_server var gitignoreServerTPL string type Command struct { Name string ShortName string Args string Comment string Func func([]string) } var commands = []Command{ {"new", "+", "[name]", "create a new project, will create in the current directory if no name is specified", newProject}, {"new plugin", "+p", "[name]", "create a new plugin project, will create in the current directory if no name is specified", newPluginProject}, //{"new server", "+s", "[name]", "create a new server project, will create in the current directory if no name is specified", newServerProject}, //{"new api", "+a", "[path] [method]", "create a new api for server project, will use restful api if specified http method", newServerAPI}, //{"new game", "+g", "[name]", "create a new game project, will create in the current directory if no name is specified", newServerProject}, //{"new webkit", "+w", "[name]", "create a new webkit project, will create in the current directory if no name is specified", newServerProject}, //{"download", "dl", "[plugin name]", "will fetch plugin into project", downloadPlugin}, {"run", "r", "", "will exec `go run .`", runProject}, {"watch run", "rr", "[...]", "run project use gowatch, if project files changed will restart auto, ... args see gowatch help https://github.com/ssgo/tool", devProject}, {"test", "t", "", "will exec `go test -v .`, will exec into tests if exists tests dir", testProject}, {"watch test", "tt", "[...]", "test project use gowatch, if project files changed will restart auto, ... args see gowatch help https://github.com/ssgo/tool", devTestProject}, //{"export plugins", "ep", "", "export typescript code for used plugins into \"plugins/\"", makePluginCode}, {"tidy", "td", "", "tidy project, find imported plugins from .js files add import code to jsImports.go, export typescript code for used plugins into \"plugins/\"", tidy}, //{"git login", "/lg", "", "login to apigo.cloud/git", }, //{"git new", "/+", "name", "create new public repository from apigo.cloud/git", }, //{"git new pri", "/++", "name", "create new private repository from apigo.cloud/git", }, //{"git clone", "/cl", "name", "clone repository to current dir from apigo.cloud/git", }, //{"git list", "/l", "", "list current repository tags from apigo.cloud/git", }, //{"git commit", "/c", "comment", "commit current repository to apigo.cloud/git", }, //{"git tag", "/t", "tag", "create new tag for current repository and push to apigo.cloud/git", }, //{"build", "b", "[-m]", "build for current os, output to build/, -m will mix js files into exec file", }, //{"build mac", "bm", "", "build", }, //{"build macarm", "bma", "", "build", }, //{"build win", "bw", "", "build", }, //{"build win32", "bw32", "", "build", }, //{"build linux", "bl", "", "build", }, //{"build linuxarm", "bla", "", "build", }, //{"build all", "ba", "", "build", }, //{"publish", "p", "", "publish project to target server", }, // TODO 从 apigo.cloud/git 中读取信息增加命令,例如:server // TODO 从 plugins 中读取信息增加命令,例如:dao } func findTool() (goWatchPath string, logVPath string) { goWatchPath = "gowatch" logVPath = "logv" if binPath, err := exec.LookPath("gowatch"); err == nil && binPath != "" { goWatchPath = binPath } if binPath, err := exec.LookPath("logv"); err == nil && binPath != "" { logVPath = binPath } return goWatchPath, logVPath } func checkSSGOTool() { goWatchPath, logVPath := findTool() if goWatchPath == "gowatch" || logVPath == "logv" { _ = runCommand("go", "get", "-u", "github.com/ssgo/tool") if goWatchPath == "gowatch" { _ = runCommand("go", "install", "github.com/ssgo/tool/gowatch") } if logVPath == "logv" { _ = runCommand("go", "install", "github.com/ssgo/tool/logv") } } } func checkProjectPath(args []string) string { if len(args) > 0 { if err := os.Mkdir(args[0], 0755); err != nil { fmt.Println(u.BRed("mkdir error: " + err.Error())) return "" } if err := os.Chdir(args[0]); err != nil { fmt.Println(u.BRed("chdir error: " + err.Error())) return "" } return args[0] } else { if pathname, err := os.Getwd(); err != nil { fmt.Println(u.BRed("getwd error: " + err.Error())) return "" } else { return path.Base(pathname) } } } func newProject(args []string) { if name := checkProjectPath(args); name != "" { _ = runCommand("go", "mod", "init", name) _ = runCommand("go", "mod", "edit", "-go=1.18") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/gojs") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/plugins") writeFile("main.go", mainCodeTPL, map[string]any{"name": name}) writeFile("main_test.go", mainTestCodeTPL, map[string]any{"name": name}) writeFile("main.js", mainJSCodeTPL, map[string]any{"name": name}) writeFile(".gitignore", gitignoreTPL, map[string]any{"name": name}) _ = runCommand("go", "mod", "tidy") checkSSGOTool() fmt.Println(u.BGreen("new project " + name + " created")) } } func newPluginProject(args []string) { if name := checkProjectPath(args); name != "" { _ = runCommand("go", "mod", "init", name) _ = runCommand("go", "mod", "edit", "-go=1.18") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/plugin") writeFile("plugin.go", pluginCodeTPL, map[string]any{"name": name}) writeFile(".gitignore", gitignoreTPL, map[string]any{"name": name}) _ = runCommand("go", "mod", "tidy") _ = os.Mkdir("tests", 0755) _ = os.Chdir("tests") _ = runCommand("go", "mod", "init", "tests") _ = runCommand("go", "mod", "edit", "-go=1.18") _ = runCommand("go", "mod", "edit", "-require=current-plugin@v0.0.0") _ = runCommand("go", "mod", "edit", "-replace=current-plugin@v0.0.0=../") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/plugin") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/gojs") writeFile("plugin_test.go", pluginTestCodeTPL, map[string]any{"name": name}) writeFile("plugin_test.js", pluginTestJSCodeTPL, map[string]any{"name": name}) _ = runCommand("go", "mod", "tidy") checkSSGOTool() fmt.Println(u.BGreen("new plugin " + name + " created")) } } func newServerProject(args []string) { } func newServerAPI(args []string) { } func _runProject(args []string, isWatch bool) { _ = runCommand("go", "mod", "tidy") goBinPath, logVPath := findTool() if isWatch { args = append(args, "-pt", ".go,.js,.yml", "run", ".") } else { goBinPath = "go" args = append(args, "run", ".") } _ = runCommandPipe(logVPath, goBinPath, args...) } func runProject(args []string) { _runProject(args, false) } func devProject(args []string) { _runProject(args, true) } func testProject(args []string) { _testProject(args, false) } func devTestProject(args []string) { _testProject(args, true) } func _testProject(args []string, isWatch bool) { if u.FileExists("tests") { if u.FileExists(filepath.Join("tests", "go.mod")) { _ = os.Chdir("tests") if isWatch { args = append(args, "-p", "..") } isRun := false for _, f := range u.ReadDirN(".") { if strings.HasSuffix(f.Name, ".go") { goStr := u.ReadFileN(f.FullName) if strings.Contains(goStr, "package main") && !strings.Contains(goStr, "package main_test") { isRun = true } break } } if !isRun { args = append(args, "test", "-v", ".") } else { args = append(args, "run", ".") } } else { args = append(args, "test", "-v", "tests") } } else { args = append(args, "test", "-v", ".") } _ = runCommand("go", "mod", "tidy") goWatchPath, logVPath := findTool() if isWatch { args2 := append([]string{"-pt", ".go,.js,.yml"}, args...) _ = runCommandPipe(logVPath, goWatchPath, args2...) } else { _ = runCommandPipe(logVPath, "go", args...) } } var pkgMatcher = regexp.MustCompile(`(?im)^\s*(import)?\s*_\s+"([\w-_/.]+)"`) var importMatcher = regexp.MustCompile(`(?im)^\s*import\s+([\w{}, ]+)\s+from\s+['"]([\w./\\\- ]+)['"]`) func makeJsImports(path string, jsImports *[]string, pkgName string) { if u.FileExists(filepath.Join(path, "go.mod")) { if m := modNameMatcher.FindStringSubmatch(u.ReadFileN(filepath.Join(path, "go.mod"))); m != nil { if pkgName != m[1] { return } } } for _, f := range u.ReadDirN(path) { filename := f.Name fullName := filepath.Join(path, filename) if !strings.HasPrefix(filename, ".") && !strings.HasPrefix(filename, "_") && filename != "_makePluginCode" && filename != "node_modules" && filename != "www" && filename != "src" { if f.IsDir { makeJsImports(fullName, jsImports, pkgName) } else if strings.HasSuffix(filename, ".js") { if code, err := u.ReadFile(fullName); err == nil { for _, m := range importMatcher.FindAllStringSubmatch(code, 1024) { if !strings.HasPrefix(m[2], ".") && !strings.HasPrefix(m[2], "_") && m[2] != "current-plugin" && m[2] != "console" && m[2] != "logger" { if !u.FileExists(filepath.Join(path, m[2])) { *jsImports = u.AppendUniqueString(*jsImports, m[2]) } } } } } } } } func makePluginCodeImports(from string, imports *[]string, parentModuleName string) { jsImports := make([]string, 0) currentParentModuleName := parentModuleName if u.FileExists(filepath.Join(from, "go.mod")) { pkgName := "" if m := modNameMatcher.FindStringSubmatch(u.ReadFileN(filepath.Join(from, "go.mod"))); m != nil { pkgName = m[1] } makeJsImports(from, &jsImports, pkgName) parentModuleName = pkgName } currentPkgName := "" for _, f := range u.ReadDirN(from) { if f.IsDir { if !strings.HasPrefix(f.Name, ".") { makePluginCodeImports(filepath.Join(from, f.Name), imports, parentModuleName) } } else { if strings.HasSuffix(f.Name, ".go") && !strings.HasPrefix(f.Name, ".") && f.Name != "makePluginCode.go" { if code, err := u.ReadFile(filepath.Join(from, f.Name)); err == nil { if m := pkgNameMatcher.FindStringSubmatch(code); m != nil { currentPkgName = m[1] } for _, m := range pkgMatcher.FindAllStringSubmatch(code, 1024) { if m[2] != "current-plugin" && !strings.HasPrefix(f.Name, "jsImports") { *imports = u.AppendUniqueString(*imports, m[2]) } } } } } } if currentPkgName != "" && u.FileExists(filepath.Join(from, "go.mod")) { pkgList := make([]string, 0) for _, plgPkg := range jsImports { if plgPkg != currentParentModuleName && !u.StringIn(*imports, plgPkg) { pkgList = append(pkgList, plgPkg) *imports = u.AppendUniqueString(*imports, plgPkg) } } if len(pkgList) > 0 { _ = u.WriteFile(filepath.Join(from, u.StringIf(strings.HasSuffix(currentPkgName, "_test"), "jsImports_test.go", "jsImports.go")), `package `+currentPkgName+` import _ "`+strings.Join(pkgList, "\"\nimport _ \"")+`" `) } } } func writeFile(filename string, fileContent string, data any) { if fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600); err == nil { tpl, _ := template.New(filename).Parse(fileContent) if err = tpl.Execute(fp, data); err != nil { fmt.Println(u.Red(err.Error())) } _ = fp.Close() } else { fmt.Println(u.Red(err.Error())) } } var replaceMatcher = regexp.MustCompile(`([a-zA-Z0-9._\-/]+)\s+(v[0-9.]+)\s+=>\s+([a-zA-Z0-9._\-/\\]+)`) var modNameMatcher = regexp.MustCompile(`(?m)^module\s+([a-zA-Z0-9._\-/]+)$`) var pkgNameMatcher = regexp.MustCompile(`(?m)^package\s+([a-zA-Z0-9._\-/]+)$`) func tidy(args []string) { // 判断是否可执行项目(不创建指向项目本身的 import _) isMainProject := false isEmptyProject := true if files, err := os.ReadDir("."); err == nil { for _, f := range files { if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") { isEmptyProject = false code, _ := u.ReadFile(f.Name()) if strings.Contains(code, "package main") || strings.Contains(code, "func main(") { isMainProject = true break } } } } // 扫描用到的插件(import _) imports := make([]string, 0) makePluginCodeImports(".", &imports, "") findGoModCode, _ := u.ReadFile("go.mod") goModCode := "module main\ngo 1.18\n" currentModuleName := "current-project" if !isMainProject { if m := modNameMatcher.FindStringSubmatch(findGoModCode); m != nil { currentModuleName = m[1] } goModCode += "require " + currentModuleName + " v0.0.0 // indirect\nreplace " + currentModuleName + " v0.0.0 => ../\n" } // 扫描 replace,处理路径后加入到 _makePluginCode/go.mod for _, m := range replaceMatcher.FindAllStringSubmatch(findGoModCode, 100) { replacePath := m[3] if absPath, err := filepath.Abs(m[3]); err == nil { replacePath = absPath } goModCode += fmt.Sprintln("replace", m[1], m[2], "=>", replacePath) } if !isMainProject && !isEmptyProject { imports = append(imports, currentModuleName) } _ = u.WriteFile("_makePluginCode/go.mod", goModCode) writeFile("_makePluginCode/main.go", makePluginCodeTPL, map[string]any{"imports": imports}) _ = os.Chdir("_makePluginCode") defer func() { _ = os.Chdir("..") _ = os.RemoveAll("_makePluginCode") }() _ = runCommand("go", "mod", "tidy") if err := runCommand("go", "run", "."); err != nil { fmt.Println(u.Red(err.Error())) } } func runCommand(name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func runCommandPipe(pipeCommandName, commandName string, args ...string) error { cmd1 := exec.Command(commandName, args...) cmd2 := exec.Command(pipeCommandName) r, w := io.Pipe() wClosed := false defer func() { if !wClosed { w.Close() } }() cmd1.Stdin = os.Stdin cmd1.Stdout = w cmd1.Stderr = w cmd2.Stdin = r cmd2.Stdout = os.Stdout cmd2.Stderr = os.Stderr if err := cmd2.Start(); err != nil { return err } if err := cmd1.Start(); err != nil { return err } if err := cmd1.Wait(); err != nil { return err } w.Close() wClosed = true // 等待第二个命令完成 if err := cmd2.Wait(); err != nil { return err } return nil } func main() { if len(os.Args) > 1 { cmd1 := os.Args[1] cmd2 := cmd1 if len(os.Args) > 2 { cmd2 += " " + os.Args[2] } for _, cmdInfo := range commands { if cmd1 == cmdInfo.Name || cmd1 == cmdInfo.ShortName { cmdInfo.Func(os.Args[2:]) return } else if cmd2 == cmdInfo.Name { cmdInfo.Func(os.Args[3:]) return } } } fmt.Println("tools for apigo.cloud") fmt.Println("version ", version) fmt.Println() fmt.Println("Usage:") fmt.Println(" ", u.Cyan("ag [command] [...]")) fmt.Println(" ", u.Magenta("ag [short command] [...]")) fmt.Println() fmt.Println("Commands:") for _, cmdInfo := range commands { padStr := "" padN := 30 - len(cmdInfo.Name) - len(cmdInfo.ShortName) - len(cmdInfo.Args) if padN > 0 { padStr = strings.Repeat(" ", padN) } fmt.Println(" ", u.Cyan(cmdInfo.Name), u.Dim("[")+u.Magenta(cmdInfo.ShortName)+u.Dim("]"), cmdInfo.Args, padStr, cmdInfo.Comment) } fmt.Println() fmt.Println("Examples:") fmt.Println(" ", u.Magenta("ag +"), " create a new simple project with Hello World") fmt.Println(" ", u.Magenta("ag +p ali"), " create a new plugin project named ali for use aliyun services") fmt.Println() }