All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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>
242 lines
7.3 KiB
Go
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
|
|
}
|