// Package main provides a CLI tool for rendering skeleton templates locally. // // This is used for testing templates without needing Gitea/rdev infrastructure. // The tool renders the monorepo skeleton plus all component types to a local directory. // // Usage: // // go run ./cmd/render-skeleton -output ./examples/full-monorepo package main import ( "flag" "fmt" "maps" "os" "path/filepath" "strings" "github.com/orchard9/rdev/internal/adapter/templates" ) // Component defines a component to render. type Component struct { Type string Name string Port string DestDir string // "services", "workers", "apps", or "cli" } // defaultComponents are the components to render in a full example project. var defaultComponents = []Component{ {Type: "service", Name: "example-api", Port: "8001", DestDir: "services"}, {Type: "worker", Name: "example-worker", Port: "", DestDir: "workers"}, {Type: "app-astro", Name: "example-astro", Port: "4321", DestDir: "apps"}, {Type: "app-react", Name: "example-react", Port: "5173", DestDir: "apps"}, {Type: "app-nextjs", Name: "example-nextjs", Port: "3000", DestDir: "apps"}, {Type: "cli", Name: "example-cli", Port: "", DestDir: "cli"}, } func main() { outputDir := flag.String("output", "", "Output directory for rendered skeleton") projectName := flag.String("project", "test-project", "Project name") goModule := flag.String("module", "git.threesix.ai/threesix/test-project", "Go module path") domain := flag.String("domain", "test.threesix.ai", "Domain for the project") flag.Parse() if *outputDir == "" { fmt.Fprintln(os.Stderr, "Usage: render-skeleton -output [-project ] [-module ] [-domain ]") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Example:") fmt.Fprintln(os.Stderr, " go run ./cmd/render-skeleton -output ./examples/full-monorepo") os.Exit(1) } // Base variables for the project vars := map[string]string{ "PROJECT_NAME": *projectName, "GO_MODULE": *goModule, "DOMAIN": *domain, "DESCRIPTION": "Test project for skeleton verification", "GIT_URL": "https://" + strings.TrimPrefix(*goModule, "git."), } fmt.Printf("Rendering skeleton to %s\n", *outputDir) fmt.Printf(" PROJECT_NAME: %s\n", vars["PROJECT_NAME"]) fmt.Printf(" GO_MODULE: %s\n", vars["GO_MODULE"]) fmt.Printf(" DOMAIN: %s\n", vars["DOMAIN"]) fmt.Println() // Clean output directory if it exists (but preserve .git if present) if err := cleanOutputDir(*outputDir); err != nil { fmt.Fprintf(os.Stderr, "Error cleaning output directory: %v\n", err) os.Exit(1) } // Render skeleton fmt.Println("Rendering skeleton...") if err := templates.RenderSkeletonToDir(*outputDir, vars); err != nil { fmt.Fprintf(os.Stderr, "Error rendering skeleton: %v\n", err) os.Exit(1) } // Render all components for _, c := range defaultComponents { fmt.Printf("Rendering component: %s (%s)\n", c.Name, c.Type) // Component-specific variables componentVars := copyVars(vars) componentVars["COMPONENT_NAME"] = c.Name componentVars["PORT"] = c.Port // For frontend apps, inject the primary service name/port for API proxy if strings.HasPrefix(c.Type, "app-") { if svc := findFirstService(defaultComponents); svc != nil { componentVars["SERVICE_NAME"] = svc.Name componentVars["SERVICE_PORT"] = svc.Port } } // Determine destination path destPath := filepath.Join(c.DestDir, c.Name) if err := templates.RenderComponentToDir(*outputDir, c.Type, destPath, componentVars); err != nil { fmt.Fprintf(os.Stderr, "Error rendering component %s: %v\n", c.Name, err) os.Exit(1) } } // Update monorepo files fmt.Println("\nUpdating monorepo files...") if err := updateGoWork(*outputDir, defaultComponents); err != nil { fmt.Fprintf(os.Stderr, "Error updating go.work: %v\n", err) os.Exit(1) } if err := updateProcfile(*outputDir, defaultComponents); err != nil { fmt.Fprintf(os.Stderr, "Error updating Procfile: %v\n", err) os.Exit(1) } // Remove .gitkeep files from directories that now have content if err := removeGitkeeps(*outputDir); err != nil { fmt.Fprintf(os.Stderr, "Error removing .gitkeep files: %v\n", err) os.Exit(1) } fmt.Println("\nDone!") fmt.Printf("Rendered %d components to %s\n", len(defaultComponents), *outputDir) fmt.Println("\nNext steps:") fmt.Println(" cd " + *outputDir) fmt.Println(" go work sync") fmt.Println(" go build ./...") fmt.Println(" pnpm install") fmt.Println(" pnpm -r typecheck") } // cleanOutputDir removes existing content but preserves .git directory. func cleanOutputDir(outputDir string) error { entries, err := os.ReadDir(outputDir) if os.IsNotExist(err) { return os.MkdirAll(outputDir, 0755) } if err != nil { return err } for _, entry := range entries { if entry.Name() == ".git" { continue // preserve .git } path := filepath.Join(outputDir, entry.Name()) if err := os.RemoveAll(path); err != nil { return fmt.Errorf("failed to remove %s: %w", path, err) } } return nil } // copyVars creates a copy of a variables map. func copyVars(vars map[string]string) map[string]string { return maps.Clone(vars) } // updateGoWork adds component modules to go.work. func updateGoWork(outputDir string, components []Component) error { var sb strings.Builder sb.WriteString("go 1.25\n\n") sb.WriteString("use ./pkg\n") for _, c := range components { // Only Go components get added to go.work switch c.Type { case "service", "worker", "cli": fmt.Fprintf(&sb, "use ./%s/%s\n", c.DestDir, c.Name) } } return os.WriteFile(filepath.Join(outputDir, "go.work"), []byte(sb.String()), 0644) } // updateProcfile adds component processes to Procfile. func updateProcfile(outputDir string, components []Component) error { var lines []string lines = append(lines, "# Local development processes") lines = append(lines, "") for _, c := range components { var cmd string switch c.Type { case "service": cmd = fmt.Sprintf("%s: cd %s/%s && make dev", c.Name, c.DestDir, c.Name) case "worker": cmd = fmt.Sprintf("%s: cd %s/%s && make dev", c.Name, c.DestDir, c.Name) case "cli": // CLIs don't run as processes continue case "app-astro", "app-react": cmd = fmt.Sprintf("%s: cd %s/%s && pnpm dev", c.Name, c.DestDir, c.Name) case "app-nextjs": cmd = fmt.Sprintf("%s: cd %s/%s && pnpm dev", c.Name, c.DestDir, c.Name) default: continue } lines = append(lines, cmd) } lines = append(lines, "") content := strings.Join(lines, "\n") return os.WriteFile(filepath.Join(outputDir, "Procfile"), []byte(content), 0644) } // findFirstService returns the first service component, or nil if none. func findFirstService(components []Component) *Component { for i := range components { if components[i].Type == "service" { return &components[i] } } return nil } // removeGitkeeps removes .gitkeep files from directories that have other content. func removeGitkeeps(outputDir string) error { keepDirs := []string{"services", "workers", "apps", "cli", "packages"} for _, dir := range keepDirs { gitkeepPath := filepath.Join(outputDir, dir, ".gitkeep") dirPath := filepath.Join(outputDir, dir) entries, err := os.ReadDir(dirPath) if err != nil { continue // directory might not exist } // If there's more than just .gitkeep, remove it if len(entries) > 1 { _ = os.Remove(gitkeepPath) // Ignore error - file may not exist } } return nil }