rdev/cmd/render-skeleton/main.go
jordan a8c8a0a14d
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: add GCS-based persistent media storage, AI generation pipeline, and composable skeleton packages
Adds complete media storage pipeline with GCS presigned uploads, AI image/video/text generation
via queue-based workers, realtime SSE event streaming, and comprehensive skeleton packages
(storage, mediagen, textgen, generation, realtime, persona, routing, ai-client). Includes
security fixes for media delete authorization, nil pointer guards in handlers, video persistence
via download-then-upload, consistent signed URLs, and Image→ImageIcon rename to avoid DOM collision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:29:09 -07:00

242 lines
7.3 KiB
Go

// 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 <dir> [-project <name>] [-module <path>] [-domain <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
}