package create import ( "fmt" "log" "os" "os/exec" "path" "path/filepath" "runtime" "strings" . "git.gorlug.de/code/ersteller" "git.gorlug.de/code/ersteller/starter/env" ) 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("../template.air.toml", ".air.toml") //s.copyGoMod() //s.executeGoModTidy() if s.params.DbType == DatabaseTypeSqlite { must(os.MkdirAll(filepath.Join(s.params.ProjectDir, "db"), 0o755)) } if s.params.DbType == DatabaseTypeSqlite { s.executeDbPush() } s.copyFile("../.gitignore", ".gitignore") s.copyFile("../AGENTS.md", "AGENTS.md") s.createEnvironment() directories := []string{ "index", "page", "routes", "static", "ent", "google", "about", "contact", "login", "todos", } for _, dir := range directories { s.copyDirectoryRecursive(path.Join(s.thisDir, "..", dir), path.Join(s.params.ProjectDir, dir)) } s.copySchemaFiles() } func (s StarterCreator) copySchemaFiles() { 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) 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) replaceImports(content []byte) []byte { contentString := string(content) contentString = strings.ReplaceAll(contentString, "\"git.gorlug.de/code/ersteller/starter/", fmt.Sprint("\"", s.params.ModuleName, "/")) return []byte(contentString) } func must(err error) { if err != nil { panic(err) } }