package create import ( . "ersteller-lib" "ersteller-lib/starter/env" "fmt" "log" "os" "os/exec" "path" "path/filepath" "runtime" "strings" ) type DatabaseType string const ( DatabaseTypePostgres DatabaseType = "postgres" DataBaseTypeSqlite DatabaseType = "sqlite" ) type Params struct { DbType DatabaseType ProjectDir string ModuleName string GoPath string OverwriteEnv bool } type StarterCreator struct { params Params thisDir string } func NewStarterCreator(params Params) StarterCreator { // Validate target directory parameter if params.ProjectDir == "" { log.Printf("StarterCreator.Create: ProjectDir is empty; nothing to do") panic("ProjectDir is empty; nothing to do") } thisDir := GetPathToThisDir() return StarterCreator{ params: params, thisDir: thisDir, } } func GetPathToThisDir() string { // Resolve the path to the starter main.go located at ../main.go relative to this file _, thisFile, _, ok := runtime.Caller(0) if !ok { log.Printf("StarterCreator.Create: unable to resolve current file location via runtime.Caller") panic("unable to resolve current file location via runtime.Caller") } thisDir := filepath.Dir(thisFile) return thisDir } // Create reads the starter main.go file bundled with this package // and writes it into the target project directory as main.go. // Minimal, side-effecting function with internal error logging to keep API unchanged. func (s StarterCreator) Create() { Debug("StarterCreator.Create") starterMainPath := filepath.Join(s.thisDir, "..", "main.go") // Read starter main.go content, err := os.ReadFile(starterMainPath) if err != nil { log.Printf("StarterCreator.Create: failed to read starter main.go at %s: %v", starterMainPath, err) return } // Ensure target directory exists if err := os.MkdirAll(s.params.ProjectDir, 0o755); err != nil { log.Printf("StarterCreator.Create: failed to create target directory %s: %v", s.params.ProjectDir, err) return } // Write to /main.go targetPath := filepath.Join(s.params.ProjectDir, "main.go") content = s.replaceImports(content) must(os.WriteFile(targetPath, content, 0o644)) log.Printf("StarterCreator.Create: wrote starter main.go to %s", targetPath) s.copyFile("../go.work.template", "go.work") s.copyFile("../../template.air.toml", ".air.toml") //s.copyGoMod() //s.executeGoModTidy() s.executeGetPrismaClient() if s.params.DbType == DataBaseTypeSqlite { must(os.MkdirAll(filepath.Join(s.params.ProjectDir, "db"), 0o755)) } s.copySchema() must(os.MkdirAll(filepath.Join(s.params.ProjectDir, "scripts"), 0o755)) s.copyFile("../../scripts/db-push.sh", "scripts/db-push.sh") if s.params.DbType == DataBaseTypeSqlite { s.executeDbPush() } s.copyFile("../.gitignore", ".gitignore") s.createEnvironment() directories := []string{ "index", "page", "routes", "static", "ent", } for _, dir := range directories { s.copyDirectoryRecursive(path.Join(s.thisDir, "..", dir), path.Join(s.params.ProjectDir, dir)) } s.copySchemaFiles() } func (s StarterCreator) copySchemaFiles() { s.copyFile("../../schema/ent/pagination_query.tmpl", "ent/pagination_query.tmpl") content, err := os.ReadFile(filepath.Join(s.thisDir, "../generate_schema.go")) must(err) //content = s.replaceImports(content) must(os.WriteFile(filepath.Join(s.params.ProjectDir, "generate_schema.go"), content, 0o644)) } func (s StarterCreator) createEnvironment() { if err := os.MkdirAll(s.params.ProjectDir+"/env", 0o755); err != nil { log.Printf("StarterCreator.Create: failed to create env target directory%v", err) return } //s.copyFile("../env/environment.go", "env/environment.go") content, err := os.ReadFile(filepath.Join(s.thisDir, "../env/environment.go")) must(err) must(os.WriteFile(filepath.Join(s.params.ProjectDir, "env/environment.go"), content, 0o644)) must(env.GenerateEnvFile(s.params.ProjectDir, s.params.OverwriteEnv)) } func (s StarterCreator) executeDbPush() { // After copying create_db.sh, run it with the project directory as the root scriptPath := filepath.Join(s.params.ProjectDir, "scripts", "db-push.sh") if runtime.GOOS == "windows" { log.Printf("StarterCreator.Create: skipping create_db.sh execution on Windows") return } if err := os.Chmod(scriptPath, 0o755); err != nil { log.Printf("StarterCreator.Create: failed to make create_db.sh executable: %v", err) return } cmd := exec.Command("/bin/sh", scriptPath) cmd.Dir = s.params.ProjectDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Printf("StarterCreator.Create: failed to run create_db.sh: %v", err) } else { log.Printf("StarterCreator.Create: successfully ran create_db.sh in %s", s.params.ProjectDir) } } func (s StarterCreator) copyFile(src string, dst string) { content, err := os.ReadFile(filepath.Join(s.thisDir, src)) must(err) must(os.WriteFile(filepath.Join(s.params.ProjectDir, dst), content, 0o644)) } func (s StarterCreator) copyDirectoryRecursive(src string, dst string) { must(os.MkdirAll(dst, 0o755)) entries, err := os.ReadDir(src) must(err) for _, entry := range entries { srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) Debug("copying srcPath: ", srcPath, " to dstPath: ", dstPath) if entry.IsDir() { s.copyDirectoryRecursive(srcPath, dstPath) continue } content, err := os.ReadFile(srcPath) content = s.replaceImports(content) must(err) must(os.WriteFile(filepath.Join(dstPath), content, 0o644)) } } func (s StarterCreator) copySchema() { content, err := os.ReadFile(filepath.Join(s.thisDir, "../../schema_template.prisma")) must(err) if s.params.DbType == DataBaseTypeSqlite { pgBlock := "datasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n" sqliteBlock := "datasource db {\n provider = \"sqlite\"\n url = \"file:./db/sqlite.db\"\n}\n" contentStr := string(content) contentStr = strings.Replace(contentStr, pgBlock, sqliteBlock, 1) contentStr = strings.ReplaceAll(contentStr, " @db.Timestamptz(3)", "") contentStr = strings.ReplaceAll(contentStr, " @db.Timestamp(6)", "") content = []byte(contentStr) } must(os.WriteFile(filepath.Join(s.params.ProjectDir, "schema.prisma"), content, 0o644)) } func (s StarterCreator) copyGoMod() { content, err := os.ReadFile(filepath.Join(s.thisDir, "../go.mod.template")) must(err) contentString := InlineTemplate(string(content), struct { ModuleName string }{ ModuleName: s.params.ModuleName, }) must(os.WriteFile(filepath.Join(s.params.ProjectDir, "go.mod"), []byte(contentString), 0o644)) } func (s StarterCreator) executeGoModTidy() { goCommand := s.getGoCommand() cmd := exec.Command(goCommand, "mod", "tidy") cmd.Dir = s.params.ProjectDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(cmd.Run()) } func (s StarterCreator) getGoCommand() string { goCommand := "go" if s.params.GoPath != "" { goCommand = s.params.GoPath } return goCommand } func (s StarterCreator) executeGetPrismaClient() { goCommand := s.getGoCommand() cmd := exec.Command(goCommand, "get", "github.com/steebchen/prisma-client-go") cmd.Dir = s.params.ProjectDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(cmd.Run()) } func (s StarterCreator) replaceImports(content []byte) []byte { contentString := string(content) contentString = strings.ReplaceAll(contentString, "\"ersteller-lib/starter/", fmt.Sprint("\"", s.params.ModuleName, "/")) return []byte(contentString) } func must(err error) { if err != nil { panic(err) } }