From 3b804b83da5733982a9989ecb95e5a3ae611c532 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 10 Sep 2025 14:30:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=8C=E5=B7=A5=E5=85=B7=E7=9A=84=E5=B9=B2=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=92=8C=E8=BE=93=E5=87=BA=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/new.go | 26 ++++----- cmd/new_event.go | 127 +++++++++++++++++++++++++++----------------- cmd/new_job.go | 53 +++++++++++------- cmd/new_project.go | 95 ++++++++++++++++++++------------- cmd/new_provider.go | 59 ++++++++++++-------- 5 files changed, 221 insertions(+), 139 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index eab8017..b7fe0c4 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -5,20 +5,22 @@ import ( ) func CommandInit(root *cobra.Command) { - cmd := &cobra.Command{ - Use: "new [project|module]", - Short: "new project/module", - } + cmd := &cobra.Command{ + Use: "new [project|module]", + Short: "new project/module", + } - cmd.PersistentFlags().BoolP("force", "f", false, "Force init project if exists") + cmd.PersistentFlags().BoolP("force", "f", false, "Force overwrite existing files or directories") + cmd.PersistentFlags().Bool("dry-run", false, "Preview actions without writing files") + cmd.PersistentFlags().String("dir", ".", "Base directory for outputs") - cmds := []func(*cobra.Command){ - CommandNewProject, - // deprecate CommandNewModule, - CommandNewProvider, - CommandNewEvent, - CommandNewJob, - } + cmds := []func(*cobra.Command){ + CommandNewProject, + // deprecate CommandNewModule, + CommandNewProvider, + CommandNewEvent, + CommandNewJob, + } for _, c := range cmds { c(cmd) diff --git a/cmd/new_event.go b/cmd/new_event.go index 06e2f1d..182cc43 100644 --- a/cmd/new_event.go +++ b/cmd/new_event.go @@ -1,11 +1,12 @@ package cmd import ( - "fmt" - "io/fs" - "os" - "path/filepath" - "text/template" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "text/template" "go.ipao.vip/atomctl/pkg/utils/gomod" "go.ipao.vip/atomctl/templates" @@ -15,23 +16,30 @@ import ( // CommandNewProvider 注册 new_provider 命令 func CommandNewEvent(root *cobra.Command) { - cmd := &cobra.Command{ - Use: "event", - Aliases: []string{"e"}, - Short: "创建新的 event publish & subscriber", - Args: cobra.ExactArgs(1), - RunE: commandNewEventE, - } + cmd := &cobra.Command{ + Use: "event", + Aliases: []string{"e"}, + Short: "创建新的 event publish & subscriber", + Args: cobra.ExactArgs(1), + RunE: commandNewEventE, + } - root.AddCommand(cmd) + cmd.Flags().String("only", "", "仅生成: publisher 或 subscriber") + + root.AddCommand(cmd) } func commandNewEventE(cmd *cobra.Command, args []string) error { snakeName := lo.SnakeCase(args[0]) camelName := lo.PascalCase(args[0]) - publisherPath := "app/events/publishers" - subscriberPath := "app/events/subscribers" + // shared flags + dryRun, _ := cmd.Flags().GetBool("dry-run") + baseDir, _ := cmd.Flags().GetString("dir") + only, _ := cmd.Flags().GetString("only") + + publisherPath := filepath.Join(baseDir, "app/events/publishers") + subscriberPath := filepath.Join(baseDir, "app/events/subscribers") path, err := os.Getwd() if err != nil { @@ -44,13 +52,17 @@ func commandNewEventE(cmd *cobra.Command, args []string) error { return err } - if err := os.MkdirAll(publisherPath, os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(subscriberPath, os.ModePerm); err != nil { - return err - } + if dryRun { + fmt.Printf("[dry-run] mkdir -p %s\n", publisherPath) + fmt.Printf("[dry-run] mkdir -p %s\n", subscriberPath) + } else { + if err := os.MkdirAll(publisherPath, os.ModePerm); err != nil { + return err + } + if err := os.MkdirAll(subscriberPath, os.ModePerm); err != nil { + return err + } + } err = fs.WalkDir(templates.Events, "events", func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -65,42 +77,59 @@ func commandNewEventE(cmd *cobra.Command, args []string) error { return err } - var destPath string - if relPath == "publisher.go.tpl" { - destPath = filepath.Join(publisherPath, snakeName+".go") - } else if relPath == "subscriber.go.tpl" { - destPath = filepath.Join(subscriberPath, snakeName+".go") - } + var destPath string + if relPath == "publisher.go.tpl" { + if only == "subscriber" { return nil } + destPath = filepath.Join(publisherPath, snakeName+".go") + } else if relPath == "subscriber.go.tpl" { + if only == "publisher" { return nil } + destPath = filepath.Join(subscriberPath, snakeName+".go") + } else { return nil } tmpl, err := template.ParseFS(templates.Events, path) if err != nil { return err } - destFile, err := os.Create(destPath) - if err != nil { - return err - } - defer destFile.Close() + if dryRun { + fmt.Printf("[dry-run] render > %s\n", destPath) + return nil + } - return tmpl.Execute(destFile, map[string]string{ - "Name": camelName, - "ModuleName": gomod.GetModuleName(), - }) - }) + destFile, err := os.Create(destPath) + if err != nil { + return err + } + defer destFile.Close() - topicStr := fmt.Sprintf("const Topic%s = %q\n", camelName, snakeName) - // 写入到 app/events/topic.go - topicFile, err := os.OpenFile("app/events/topics.go", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer topicFile.Close() + return tmpl.Execute(destFile, map[string]string{ + "Name": camelName, + "ModuleName": gomod.GetModuleName(), + }) + }) - _, err = topicFile.WriteString(topicStr) - if err != nil { - return err - } + // 写入或追加 topic 常量,避免重复。 + topicsPath := filepath.Join(baseDir, "app/events/topics.go") + topicLine := fmt.Sprintf("const Topic%s = %q\n", camelName, snakeName) + + if dryRun { + fmt.Printf("[dry-run] ensure topics file and add constant > %s\n", topicsPath) + } else { + // ensure file exists with basic header + if _, statErr := os.Stat(topicsPath); os.IsNotExist(statErr) { + if err := os.MkdirAll(filepath.Dir(topicsPath), os.ModePerm); err != nil { return err } + header := "package events\n\n// topics generated by atomctl\n\n" + if err := os.WriteFile(topicsPath, []byte(header), 0o644); err != nil { return err } + } + // check duplicate + content, _ := os.ReadFile(topicsPath) + if !strings.Contains(string(content), "Topic"+camelName+" ") && !strings.Contains(string(content), topicLine) { + f, err := os.OpenFile(topicsPath, os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { return err } + defer f.Close() + if _, err := f.WriteString(topicLine); err != nil { return err } + } + } fmt.Printf("event 已创建: %s\n", snakeName) return nil diff --git a/cmd/new_job.go b/cmd/new_job.go index c7c54b6..30547e4 100644 --- a/cmd/new_job.go +++ b/cmd/new_job.go @@ -26,10 +26,14 @@ func CommandNewJob(root *cobra.Command) { } func commandNewJobE(cmd *cobra.Command, args []string) error { - snakeName := lo.SnakeCase(args[0]) - camelName := lo.PascalCase(args[0]) + snakeName := lo.SnakeCase(args[0]) + camelName := lo.PascalCase(args[0]) - destPath := "app/jobs" + // shared flags + dryRun, _ := cmd.Flags().GetBool("dry-run") + baseDir, _ := cmd.Flags().GetString("dir") + + basePath := filepath.Join(baseDir, "app/jobs") path, err := os.Getwd() if err != nil { @@ -42,9 +46,13 @@ func commandNewJobE(cmd *cobra.Command, args []string) error { return err } - if err := os.MkdirAll(destPath, os.ModePerm); err != nil { - return err - } + if dryRun { + fmt.Printf("[dry-run] mkdir -p %s\n", basePath) + } else { + if err := os.MkdirAll(basePath, os.ModePerm); err != nil { + return err + } + } err = fs.WalkDir(templates.Jobs, "jobs", func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -55,22 +63,27 @@ func commandNewJobE(cmd *cobra.Command, args []string) error { return nil } - destPath := filepath.Join(destPath, snakeName+".go") - tmpl, err := template.ParseFS(templates.Jobs, path) - if err != nil { - return err - } + filePath := filepath.Join(basePath, snakeName+".go") + tmpl, err := template.ParseFS(templates.Jobs, path) + if err != nil { + return err + } - destFile, err := os.Create(destPath) - if err != nil { - return err - } - defer destFile.Close() + if dryRun { + fmt.Printf("[dry-run] render > %s\n", filePath) + return nil + } - return tmpl.Execute(destFile, map[string]string{ - "Name": camelName, - "ModuleName": gomod.GetModuleName(), - }) + destFile, err := os.Create(filePath) + if err != nil { + return err + } + defer destFile.Close() + + return tmpl.Execute(destFile, map[string]string{ + "Name": camelName, + "ModuleName": gomod.GetModuleName(), + }) }) if err != nil { return err diff --git a/cmd/new_project.go b/cmd/new_project.go index e2543f5..f402cd5 100644 --- a/cmd/new_project.go +++ b/cmd/new_project.go @@ -36,10 +36,14 @@ func CommandNewProject(root *cobra.Command) { } func commandNewProjectE(cmd *cobra.Command, args []string) error { - var ( - moduleName string - inPlace bool - ) + 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 { @@ -72,22 +76,27 @@ func commandNewProjectE(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") - rootDir := "." - if !inPlace { - rootDir = 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 err := os.RemoveAll(rootDir); err != nil { - return fmt.Errorf("failed to remove existing directory: %v", err) - } - } - if err := os.MkdirAll(rootDir, 0o755); err != nil { - return fmt.Errorf("failed to create project directory: %v", err) - } - } + 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 { @@ -106,10 +115,14 @@ func commandNewProjectE(cmd *cobra.Command, args []string) error { } targetPath := filepath.Join(rootDir, relPath) - if d.IsDir() { - log.Infof("创建目录: %s", targetPath) - return os.MkdirAll(targetPath, 0o755) - } + 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 { @@ -151,20 +164,28 @@ func commandNewProjectE(cmd *cobra.Command, args []string) error { if err != nil { return err } - log.Infof("[渲染] 文件: %s", targetPath) - 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] 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) - return os.WriteFile(targetPath, content, 0o644) - }); err != nil { - return err - } + 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("🎉 项目初始化成功 (当前目录)!") diff --git a/cmd/new_provider.go b/cmd/new_provider.go index b2c1a8a..04509fb 100644 --- a/cmd/new_provider.go +++ b/cmd/new_provider.go @@ -27,16 +27,24 @@ func CommandNewProvider(root *cobra.Command) { } func commandNewProviderE(cmd *cobra.Command, args []string) error { - providerName := args[0] - targetPath := filepath.Join("providers", providerName) + providerName := args[0] + // shared flags + dryRun, _ := cmd.Flags().GetBool("dry-run") + baseDir, _ := cmd.Flags().GetString("dir") - if _, err := os.Stat(targetPath); err == nil { - return fmt.Errorf("目录 %s 已存在", targetPath) - } + targetPath := filepath.Join(baseDir, "providers", providerName) - if err := os.MkdirAll(targetPath, os.ModePerm); err != nil { - return err - } + if _, err := os.Stat(targetPath); err == nil { + return fmt.Errorf("目录 %s 已存在", targetPath) + } + + if dryRun { + fmt.Printf("[dry-run] mkdir -p %s\n", targetPath) + } else { + if err := os.MkdirAll(targetPath, os.ModePerm); err != nil { + return err + } + } err := fs.WalkDir(templates.Provider, "provider", func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -51,26 +59,35 @@ func commandNewProviderE(cmd *cobra.Command, args []string) error { return err } - destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl")) - if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil { - return err - } + destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl")) + if dryRun { + fmt.Printf("[dry-run] mkdir -p %s\n", filepath.Dir(destPath)) + } else { + if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil { + return err + } + } tmpl, err := template.ParseFS(templates.Provider, path) if err != nil { return err } - destFile, err := os.Create(destPath) - if err != nil { - return err - } - defer destFile.Close() + if dryRun { + fmt.Printf("[dry-run] render > %s\n", destPath) + return nil + } - return tmpl.Execute(destFile, map[string]string{ - "Name": providerName, - "CamelName": strcase.ToCamel(providerName), - }) + destFile, err := os.Create(destPath) + if err != nil { + return err + } + defer destFile.Close() + + return tmpl.Execute(destFile, map[string]string{ + "Name": providerName, + "CamelName": strcase.ToCamel(providerName), + }) }) if err != nil { return errors.New("渲染 provider 模板失败")