ag/main.go
2024-06-07 18:35:15 +08:00

722 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"archive/zip"
_ "embed"
"fmt"
"github.com/ssgo/httpclient"
"github.com/ssgo/u"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"time"
)
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{
{"init", "i", "", "init a new project for empty dir", initProject},
{"init plugin", "i p", "", "init a new plugin project for empty dir", initPluginProject},
//{"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},
{"tags", "", "", "show git tags", showGitTags},
{"commit", "co", "comment", "commit git repo and push, comment is need", commitGitRepo},
{"tag+", "t+", "[version]", "add git tag push, if no new tag specified will use last tag +1", addGitTag},
//{"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 从 plugins 中读取信息增加命令例如dao
// TODO 支持 build
// TODO 支持 publish 到 apigo.cloud
}
var hc = httpclient.GetClient(30 * time.Second)
var cachePath = "c:\\.ag\\cache"
func init() {
hc.SetGlobalHeader("Content-Type", "application/json")
if homePath, err := os.UserHomeDir(); err == nil {
cachePath = path.Join(homePath, ".ag", "cache")
}
}
func showGitTags(args []string) {
_ = runCommand("git", "tag", "-l", "v*", "--sort=-taggerdate", "--format=%(refname:short) %(taggerdate:short) %(*objectname:short)")
}
func commitGitRepo(args []string) {
comment := strings.Join(args, " ")
if comment != "" {
if err := runCommand("git", "commit", "-a", "-m", comment); err == nil {
if err := runCommand("git", "push"); err != nil {
fmt.Println("git push failed:", err.Error())
}
} else {
fmt.Println("git commit failed:", err.Error())
}
} else {
fmt.Println("commit message is empty")
}
}
func addGitTag(args []string) {
newVer := ""
if len(args) > 0 {
newVer = args[0]
}
if newVer == "" {
if outs, err := u.RunCommand("git", "tag", "-l", "v*", "--sort=taggerdate"); err == nil {
oldVer := "v0.0.0"
for i := len(outs) - 1; i >= 0; i-- {
if outs[i][0] == 'v' && strings.IndexByte(outs[i], '.') != -1 {
oldVer = outs[len(outs)-1]
break
}
}
if len(os.Args) > 2 {
newVer = os.Args[2]
} else {
versionParts := strings.Split(oldVer, ".")
v, err := strconv.Atoi(versionParts[len(versionParts)-1])
if err != nil {
v = 0
}
versionParts[len(versionParts)-1] = strconv.Itoa(v + 1)
newVer = strings.Join(versionParts, ".")
}
} else {
fmt.Println("get last tag failed:", err.Error())
}
}
if newVer != "" {
if err := runCommand("git", "tag", "-a", newVer, "-m", "update tag by 'ag tag+'"); err == nil {
if err := runCommand("git", "push", "origin", newVer); err != nil {
fmt.Println("git push failed:", err.Error())
}
} else {
fmt.Println("git add tag failed:", err.Error())
}
}
}
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() 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 len(u.ReadDirN(".")) != 0 {
fmt.Println(u.BRed("current path is not empty"))
return ""
}
if pathname, err := os.Getwd(); err != nil {
fmt.Println(u.BRed("getwd error: " + err.Error()))
return ""
} else {
return path.Base(pathname)
}
//}
}
func parseRepo(url string) (apiUrl, owner, repo string) {
name := ""
if strings.HasPrefix(url, "github.com/") {
apiUrl = "https://api.github.com/"
name = url[11:]
} else if strings.HasPrefix(url, "gitee.com/") {
apiUrl = "https://gitee.com/api/v5/"
name = url[10:]
} else if strings.HasPrefix(url, "apigo.cloud/git/") {
apiUrl = "https://apigo.cloud/git/api/v1/"
name = url[16:]
} else {
apiUrl = "https://apigo.cloud/git/api/v1/"
if strings.ContainsRune(url, '/') {
name = url
} else {
name = "apigo/" + url
}
}
if !strings.ContainsRune(name, '/') {
name = name + "/" + name
}
a := strings.Split(name, "/")
owner = a[0]
repo = a[1]
return
}
type Tag struct {
Name string
Commit struct {
Sha string
Url string
}
Zipball_url string
Tarball_url string
}
func fetchRepo(name string) (repoPath string) {
apiUrl, owner, repo := parseRepo(name)
tagsUrl := apiUrl + path.Join("repos", owner, repo, "tags")
tags := make([]Tag, 0)
fmt.Println("try to fetch tags", tagsUrl)
r := hc.Get(tagsUrl)
if r.Error == nil {
r.To(&tags)
} else if apiUrl != "https://api.github.com/" {
tagsUrl = "https://api.github.com/" + path.Join("repos", owner, repo, "tags")
fmt.Println("try to fetch tags", tagsUrl)
r = hc.Get(tagsUrl)
if r.Error == nil {
r.To(&tags)
} else {
fmt.Println("fetch tags error", r.Error)
}
}
if len(tags) > 0 {
lastVersion := tags[0].Name
zipUrl := tags[0].Zipball_url
tagPath := filepath.Join(cachePath, owner, repo, lastVersion)
if !u.FileExists(tagPath) {
zipFile := filepath.Join(cachePath, owner, repo, lastVersion+".zip")
fmt.Println("Download file ", zipUrl)
_, err := hc.Download(zipFile, zipUrl, func(start, end int64, ok bool, finished, total int64) {
fmt.Printf(" %.2f%%", float32(finished)/float32(total)*100)
})
fmt.Println()
if err != nil {
fmt.Println(u.BRed("download file failed"))
return
}
if u.FileExists(zipFile) {
if reader, err := zip.OpenReader(zipFile); err == nil {
for _, f := range reader.File {
toFile := filepath.Join(tagPath, f.Name)
if f.FileInfo().IsDir() {
//fmt.Println("extract dir", f.Name, toFile)
os.MkdirAll(toFile, 0755)
} else {
//fmt.Println("extract file", f.Name, toFile)
u.CheckPath(toFile)
if fp, err := os.OpenFile(toFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644); err == nil {
if rd, err := f.Open(); err == nil {
if _, err := io.Copy(fp, rd); err != nil {
fmt.Println(u.BRed("write file failed"), toFile, err.Error())
} else {
//fmt.Println(u.BGreen("write file success"), toFile)
}
_ = rd.Close()
} else {
fmt.Println(u.BRed("open file in zip failed"), f.Name, err.Error())
}
_ = fp.Close()
} else {
fmt.Println(u.BRed("open dst file failed"), toFile, err.Error())
}
}
}
_ = reader.Close()
} else {
fmt.Println(u.BRed("open zip file failed"), zipFile, err.Error())
}
_ = os.Remove(zipFile)
}
}
return filepath.Join(tagPath, repo)
} else {
fmt.Println(u.BRed("no repo found for"), name)
return ""
}
}
func initProject(args []string) {
if name := checkProjectPath(); name != "" {
projectOK := false
if len(args) > 0 {
repoPath := fetchRepo(args[0])
projectPath := filepath.Join(repoPath, "_project")
if u.FileExists(projectPath) {
if err := CopyFile(projectPath, "."); err == nil {
projectOK = true
fmt.Println(u.Green("make project files success"))
} else {
fmt.Println(u.BRed("make project files failed"), projectPath, err.Error())
}
}
}
if len(args) == 0 || !projectOK {
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})
}
_ = 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(".gitignore", gitignoreTPL, map[string]any{"name": name})
_ = runCommand("go", "mod", "tidy")
checkSSGOTool()
fmt.Println(u.BGreen("new project " + name + " created"))
}
}
func CopyFile(from, to string) error {
fromStat, _ := os.Stat(from)
if fromStat.IsDir() {
// copy dir
for _, f := range u.ReadDirN(from) {
err := CopyFile(filepath.Join(from, f.Name), filepath.Join(to, f.Name))
if err != nil {
return err
}
}
return nil
} else {
// copy file
toStat, err := os.Stat(to)
if err == nil && toStat.IsDir() {
to = filepath.Join(to, filepath.Base(from))
}
u.CheckPath(to)
if writer, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil {
defer writer.Close()
if reader, err := os.OpenFile(from, os.O_RDONLY, 0644); err == nil {
defer reader.Close()
_, err = io.Copy(writer, reader)
return err
} else {
return err
}
} else {
return err
}
}
}
func initPluginProject(args []string) {
if name := checkProjectPath(); 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 _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 {
if currentPkgName+"_test" != m[1] {
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()
}