init.go

151 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
package commands

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"congo.gg/cmd/internal"
)

var validName = regexp.MustCompile(`^[a-z][a-z0-9_-]*$`)

const initUsage = `Create a new Congo project

Usage:
  congo init <name> [flags]

Flags:
  --module <path>        Go module path (default: project name)
  --dir <name>           App directory name (default: web)
  --no-platform          Exclude cloud platform package
  --no-database          Exclude database ORM
  --application-only     Only include the application framework

Examples:
  congo init myapp
  congo init myapp --module github.com/user/myapp
  congo init myapp --no-platform
  congo init myapp --application-only
`

func Init() {
	fs := flag.NewFlagSet("init", flag.ContinueOnError)
	fs.Usage = func() { fmt.Print(initUsage) }

	module := fs.String("module", "", "Go module path")
	dir := fs.String("dir", "web", "App directory name")
	noPlatform := fs.Bool("no-platform", false, "Exclude cloud platform package")
	noDatabase := fs.Bool("no-database", false, "Exclude database ORM")
	appOnly := fs.Bool("application-only", false, "Only include application framework")

	// Partition args so flags work in any position.
	var flagArgs, posArgs []string
	for i, args := 0, os.Args[2:]; i < len(args); i++ {
		if args[i] == "--" {
			posArgs = append(posArgs, args[i+1:]...)
			break
		}
		if len(args[i]) > 0 && args[i][0] == '-' {
			flagArgs = append(flagArgs, args[i])
			if f := fs.Lookup(strings.TrimLeft(args[i], "-")); f != nil {
				if _, ok := f.Value.(interface{ IsBoolFlag() bool }); !ok {
					if i+1 < len(args) {
						i++
						flagArgs = append(flagArgs, args[i])
					}
				}
			}
		} else {
			posArgs = append(posArgs, args[i])
		}
	}
	if err := fs.Parse(flagArgs); err != nil {
		if err == flag.ErrHelp {
			os.Exit(0)
		}
		os.Exit(1)
	}

	if len(posArgs) < 1 {
		fs.Usage()
		os.Exit(1)
	}

	name := posArgs[0]
	if !validName.MatchString(name) {
		log.Fatalf("invalid project name %q (lowercase letters, numbers, hyphens, underscores)", name)
	}

	if *module == "" {
		*module = name
	}

	if _, err := os.Stat(name); err == nil {
		log.Fatalf("directory %q already exists", name)
	}

	// All packages included by default; --application-only overrides everything.
	withDatabase := !*noDatabase && !*appOnly
	withPlatform := !*noPlatform && !*appOnly

	// router and security are always included — application depends on them.
	packages := []string{"router", "security", "application"}
	if withDatabase {
		packages = append(packages, "database")
	}
	if withPlatform {
		packages = append(packages, "platform")
	}

	fmt.Printf("Creating %s...\n", name)

	if err := os.MkdirAll(name, 0755); err != nil {
		log.Fatalf("create directory: %v", err)
	}

	fmt.Println("  Extracting framework packages...")
	if err := scaffold.ExtractPackages(name, *module, packages); err != nil {
		log.Fatalf("extract packages: %v", err)
	}

	fmt.Println("  Generating project files...")
	data := scaffold.Data{
		Name:         name,
		Module:       *module,
		Dir:          *dir,
		WithPlatform: withPlatform,
		WithDatabase: withDatabase,
	}
	if err := scaffold.RenderProject(name, data); err != nil {
		log.Fatalf("render scaffold: %v", err)
	}

	// Remove optional scaffold files for excluded packages.
	appDir := filepath.Join(name, *dir)
	if !withDatabase {
		os.RemoveAll(filepath.Join(appDir, "models"))
	}

	fmt.Println("  Resolving Go dependencies...")
	tidy := exec.Command("go", "mod", "tidy")
	tidy.Dir = name
	tidy.Stdout = os.Stdout
	tidy.Stderr = os.Stderr
	if err := tidy.Run(); err != nil {
		log.Fatalf("go mod tidy: %v", err)
	}

	fmt.Printf("\nDone! Project ready at ./%s\n\n", name)
	fmt.Println("Next steps:")
	fmt.Printf("  cd %s\n", name)
	fmt.Println("  congo dev         # start development server")
	fmt.Println("  congo claude      # AI-assisted development")
	fmt.Println("  congo new <name>  # add another app")
	fmt.Println()
}