Files
atomctl/cmd/new_project.go

214 lines
5.8 KiB
Go
Raw 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 cmd
import (
"bytes"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
"go.ipao.vip/atomctl/v2/templates"
)
// 验证包名是否合法:支持域名、路径分隔符和常见字符
var goPackageRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9](/[a-zA-Z0-9][a-zA-Z0-9-_.]*)*$`)
func isValidGoPackageName(name string) bool {
return goPackageRegexp.MatchString(name)
}
func CommandNewProject(root *cobra.Command) {
cmd := &cobra.Command{
Use: "project",
Aliases: []string{"p"},
Short: "new project",
Long: `基于内置模板创建新项目,或在已有 go.mod 的目录中就地初始化。
行为:
- 在空目录:需传入模块名(如 github.com/acme/app
- 在已有 go.mod可省略模块名自动解析为就地初始化
- 模板渲染规则:支持 .tpl/.raw或通过文件内 atomctl:mode=tpl|raw 指示
- 隐藏文件占位:模板文件名以 - 开头会渲染为 . 前缀(-.gitignore -> .gitignore
- --force可覆盖存在的文件/目录;--dry-run仅打印渲染与写入动作
示例:
atomctl new project github.com/acme/demo
atomctl new -f --dir ./playground project github.com/acme/demo
atomctl new project # 在已有 go.mod 的项目中就地初始化`,
RunE: commandNewProjectE,
}
root.AddCommand(cmd)
}
func commandNewProjectE(cmd *cobra.Command, args []string) error {
var (
moduleName string
inPlace bool
)
// shared flags
dryRun, _ := cmd.Flags().GetBool("dry-run")
baseDir, _ := cmd.Flags().GetString("dir")
if len(args) == 0 {
if _, err := os.Stat("go.mod"); err == nil {
pwd, _ := os.Getwd()
if err := gomod.Parse(filepath.Join(pwd, "go.mod")); err != nil {
return fmt.Errorf("parse go.mod failed: %v", err)
}
moduleName = gomod.GetModuleName()
inPlace = true
} else {
return fmt.Errorf("module name required or run inside an existing module (go.mod)")
}
} else {
moduleName = args[0]
if !isValidGoPackageName(moduleName) {
return fmt.Errorf("invalid module name: %s, should be a valid go package name", moduleName)
}
}
log.Info("创建项目: ", moduleName)
var projectInfo struct {
ModuleName string
ProjectName string
}
projectInfo.ModuleName = moduleName
moduleSplitInfo := strings.Split(projectInfo.ModuleName, "/")
projectInfo.ProjectName = moduleSplitInfo[len(moduleSplitInfo)-1]
force, _ := cmd.Flags().GetBool("force")
rootDir := "."
if !inPlace {
// honor base dir when creating a new project
rootDir = filepath.Join(baseDir, projectInfo.ProjectName)
if _, err := os.Stat(rootDir); err == nil {
if !force {
return fmt.Errorf("project directory %s already exists", rootDir)
}
log.Warnf("强制删除已存在的目录: %s", rootDir)
if dryRun {
log.Infof("[dry-run] 将删除目录: %s", rootDir)
} else if err := os.RemoveAll(rootDir); err != nil {
return fmt.Errorf("failed to remove existing directory: %v", err)
}
}
if dryRun {
log.Infof("[dry-run] 将创建目录: %s", rootDir)
} else if err := os.MkdirAll(rootDir, 0o755); err != nil {
return fmt.Errorf("failed to create project directory: %v", err)
}
}
if err := fs.WalkDir(templates.Project, "project", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel("project", path)
if err != nil {
return err
}
fileName := filepath.Base(relPath)
if strings.HasPrefix(fileName, "-") {
fileName = "." + strings.TrimPrefix(fileName, "-")
relPath = filepath.Join(filepath.Dir(relPath), fileName)
}
targetPath := filepath.Join(rootDir, relPath)
if d.IsDir() {
log.Infof("创建目录: %s", targetPath)
if dryRun {
log.Infof("[dry-run] mkdir -p %s", targetPath)
return nil
}
return os.MkdirAll(targetPath, 0o755)
}
content, err := templates.Project.ReadFile(path)
if err != nil {
return err
}
// 渲染判定:优先(.tpl/.raw) > 行内指令 > 模板分隔符
isTpl := false
if strings.HasSuffix(path, ".tpl") {
isTpl = true
targetPath = strings.TrimSuffix(targetPath, ".tpl")
} else if strings.HasSuffix(path, ".raw") {
isTpl = false
targetPath = strings.TrimSuffix(targetPath, ".raw")
} else if bytes.Contains(content, []byte("atomctl:mode=tpl")) {
isTpl = true
} else if bytes.Contains(content, []byte("atomctl:mode=raw")) {
isTpl = false
} else if bytes.Contains(content, []byte("{{")) && bytes.Contains(content, []byte("}}")) {
isTpl = true
}
if inPlace && strings.HasSuffix(path, string(os.PathSeparator)+"go.mod.tpl") {
if _, err := os.Stat(filepath.Join(rootDir, "go.mod")); err == nil {
log.Infof("跳过已有文件: %s", filepath.Join(rootDir, "go.mod"))
return nil
}
}
if !force {
if _, err := os.Stat(targetPath); err == nil {
log.Warnf("文件已存在,跳过: %s", targetPath)
return nil
}
}
if isTpl {
tmpl, err := template.New(filepath.Base(path)).Parse(string(content))
if err != nil {
return err
}
log.Infof("[渲染] 文件: %s", targetPath)
if dryRun {
log.Infof("[dry-run] render > %s", targetPath)
return nil
}
f, err := os.Create(targetPath)
if err != nil {
return errors.Wrapf(err, "创建文件失败 %s", targetPath)
}
defer f.Close()
return tmpl.Execute(f, projectInfo)
}
log.Infof("[复制] 文件: %s", targetPath)
if dryRun {
log.Infof("[dry-run] write > %s", targetPath)
return nil
}
return os.WriteFile(targetPath, content, 0o644)
}); err != nil {
return err
}
if inPlace {
log.Info("🎉 项目初始化成功 (当前目录)!")
} else {
log.Info("🎉 项目创建成功!")
log.Info("后续步骤:")
log.Infof(" cd %s", projectInfo.ProjectName)
}
log.Info(" go mod tidy")
return nil
}