init.go

184 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
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-frontend          Exclude React islands
  --no-assistant         Exclude AI assistant package
  --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-assistant --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")
	noFrontend := fs.Bool("no-frontend", false, "Exclude React islands")
	noAssistant := fs.Bool("no-assistant", false, "Exclude AI assistant package")
	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
	withFrontend := !*noFrontend && !*appOnly
	withAssistant := !*noAssistant && !*appOnly
	withPlatform := !*noPlatform && !*appOnly

	packages := []string{"application"}
	if withDatabase {
		packages = append(packages, "database")
	}
	if withFrontend {
		packages = append(packages, "frontend")
	}
	if withAssistant {
		packages = append(packages, "assistant")
	}
	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,
		WithFrontend:  withFrontend,
		WithAssistant: withAssistant,
		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 !withFrontend {
		for _, f := range []string{"index.ts", "package.json", "components"} {
			os.RemoveAll(filepath.Join(appDir, f))
		}
	}
	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)
	}

	if withFrontend {
		if _, err := exec.LookPath("npm"); err == nil {
			fmt.Println("  Installing frontend dependencies...")
			npm := exec.Command("npm", "install")
			npm.Dir = appDir
			npm.Stdout = os.Stdout
			npm.Stderr = os.Stderr
			if err := npm.Run(); err != nil {
				fmt.Println("  Warning: npm install failed. Run 'npm install' manually.")
			}
		} else {
			fmt.Printf("  Note: npm not found. Run 'npm install' in %s/.\n", *dir)
		}
	}

	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()
}