Skip to content

Commit 32867bb

Browse files
added new project creation command
1 parent 9be4582 commit 32867bb

File tree

1 file changed

+301
-13
lines changed

1 file changed

+301
-13
lines changed

main.go

Lines changed: 301 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const (
2626
capturePath = "/v1/system/app/start"
2727
timeout = 60 * time.Second
2828
grace = 1500 * time.Millisecond
29+
30+
gettingStartedRepo = "https://github.com/cloudimpl/polycode-getting-started.git"
31+
defaultGettingBranch = "main"
2932
)
3033

3134
// Language -> Generator
@@ -46,23 +49,133 @@ func main() {
4649
printRootUsage()
4750
return
4851

49-
case "generate":
50-
cmdGenerate(os.Args[2:])
52+
case "new":
53+
cmdNew(os.Args[2:])
54+
55+
case "build":
56+
cmdBuild(os.Args[2:])
57+
58+
case "run":
59+
cmdRun(os.Args[2:])
5160

5261
case "extract":
5362
cmdExtract(os.Args[2:])
5463

64+
// Optional compatibility alias (uncomment if you want to keep it)
65+
// case "generate":
66+
// fmt.Fprintln(os.Stderr, "warning: 'generate' is deprecated; use 'build' instead")
67+
// cmdBuild(os.Args[2:])
68+
5569
default:
5670
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
5771
printRootUsage()
5872
os.Exit(2)
5973
}
6074
}
6175

62-
// ========================= generate =========================
76+
// ========================= new =========================
6377

64-
func cmdGenerate(args []string) {
65-
fs := flag.NewFlagSet("generate", flag.ContinueOnError)
78+
// polycode new <project-name> [options]
79+
func cmdNew(args []string) {
80+
fs := flag.NewFlagSet("new", flag.ContinueOnError)
81+
fs.SetOutput(os.Stderr)
82+
83+
var (
84+
projectName string
85+
lang string
86+
repo string
87+
branch string
88+
)
89+
fs.StringVar(&lang, "language", "go", "Template language: go | java | python")
90+
fs.StringVar(&repo, "repo", gettingStartedRepo, "Getting started repo (advanced)")
91+
fs.StringVar(&branch, "branch", defaultGettingBranch, "Repo branch (advanced)")
92+
fs.Usage = func() {
93+
fmt.Fprintf(fs.Output(), "Usage:\n polycode new <project-name> [options]\n\nOptions:\n")
94+
fs.PrintDefaults()
95+
}
96+
97+
if err := fs.Parse(args); err != nil {
98+
if err == flag.ErrHelp {
99+
return
100+
}
101+
os.Exit(2)
102+
}
103+
if fs.NArg() < 1 {
104+
fmt.Fprintln(os.Stderr, "missing required <project-name>")
105+
fs.Usage()
106+
os.Exit(2)
107+
}
108+
projectName = fs.Arg(0)
109+
110+
lang = strings.ToLower(strings.TrimSpace(lang))
111+
switch lang {
112+
case "go", "java", "python":
113+
default:
114+
log.Fatalf("unsupported language %q (use: go | java | python)", lang)
115+
}
116+
117+
dest := projectName
118+
if _, err := os.Stat(dest); err == nil {
119+
log.Fatalf("destination folder %q already exists", dest)
120+
}
121+
122+
// 1) clone into a temp dir
123+
tmpDir, err := os.MkdirTemp("", "polycode-gs-*")
124+
if err != nil {
125+
log.Fatalf("failed to create temp dir: %v", err)
126+
}
127+
defer os.RemoveAll(tmpDir)
128+
129+
log.Printf("Cloning %s (branch %s)...", repo, branch)
130+
if err := runCmd(".", "git", "clone", "--depth", "1", "--branch", branch, repo, tmpDir); err != nil {
131+
log.Fatalf("git clone failed: %v", err)
132+
}
133+
134+
// 2) copy the language subfolder into projectName
135+
src := filepath.Join(tmpDir, lang)
136+
if st, err := os.Stat(src); err != nil || !st.IsDir() {
137+
log.Fatalf("template subfolder not found: %s", src)
138+
}
139+
if err := copyTree(src, dest); err != nil {
140+
log.Fatalf("failed to copy template: %v", err)
141+
}
142+
143+
// 3) language-specific tweaks
144+
switch lang {
145+
case "go":
146+
// replace _getting_started in go.mod and .go files
147+
if err := replaceInFiles(dest, []string{".go", ".mod"}, "_getting_started", projectName); err != nil {
148+
log.Fatalf("failed to apply replacements: %v", err)
149+
}
150+
// run go mod tidy
151+
if err := runCmd(dest, "go", "mod", "tidy"); err != nil {
152+
log.Printf("warning: go mod tidy failed: %v", err)
153+
}
154+
case "java":
155+
// add Java-specific replacements if your template needs it
156+
case "python":
157+
// add Python-specific replacements if your template needs it
158+
}
159+
160+
fmt.Printf("\n✅ Created %q from %s/%s template.\n\n", projectName, filepath.Base(repo), lang)
161+
fmt.Println("Next steps:")
162+
fmt.Printf(" cd %s\n", projectName)
163+
switch lang {
164+
case "go":
165+
fmt.Println(" polycode build .")
166+
fmt.Println(" go run ./app # or: go build -o appbin ./app && ./appbin")
167+
case "java":
168+
fmt.Println(" # TODO: build & run steps for Java template")
169+
case "python":
170+
fmt.Println(" # TODO: build & run steps for Python template")
171+
}
172+
fmt.Println()
173+
}
174+
175+
// ========================= build =========================
176+
177+
func cmdBuild(args []string) {
178+
fs := flag.NewFlagSet("build", flag.ContinueOnError)
66179
fs.SetOutput(os.Stderr)
67180

68181
var (
@@ -73,9 +186,9 @@ func cmdGenerate(args []string) {
73186

74187
fs.StringVar(&appLanguage, "language", "auto", "Application language (supported: go)")
75188
fs.StringVar(&outputPath, "out", "", "Output path for generated code (default: <app-path>/app)")
76-
fs.BoolVar(&watchFlag, "watch", false, "Watch <app-path>/services and regenerate on changes")
189+
fs.BoolVar(&watchFlag, "watch", false, "Watch <app-path>/services and rebuild on changes")
77190
fs.Usage = func() {
78-
fmt.Fprintf(fs.Output(), "Usage:\n polycode generate <app-path> [options]\n\nOptions:\n")
191+
fmt.Fprintf(fs.Output(), "Usage:\n polycode build <app-path> [options]\n\nOptions:\n")
79192
fs.PrintDefaults()
80193
}
81194

@@ -127,7 +240,94 @@ func cmdGenerate(args []string) {
127240
}
128241

129242
if err := g.Generate(appPath, outputPath); err != nil {
130-
log.Fatalf("failed to generate code: %v", err)
243+
log.Fatalf("failed to build: %v", err)
244+
}
245+
}
246+
247+
// ========================= run =========================
248+
249+
// polycode run <app-path>
250+
// - Builds into <app-path>/app (like build)
251+
// - For Go: builds binary into .polycode/app and runs it
252+
func cmdRun(args []string) {
253+
fs := flag.NewFlagSet("run", flag.ContinueOnError)
254+
fs.SetOutput(os.Stderr)
255+
256+
var (
257+
appLanguage string
258+
)
259+
fs.StringVar(&appLanguage, "language", "auto", "Application language (supported: go)")
260+
fs.Usage = func() {
261+
fmt.Fprintf(fs.Output(), "Usage:\n polycode run <app-path> [options]\n\nOptions:\n")
262+
fs.PrintDefaults()
263+
}
264+
265+
if err := fs.Parse(args); err != nil {
266+
if err == flag.ErrHelp {
267+
return
268+
}
269+
os.Exit(2)
270+
}
271+
if fs.NArg() < 1 {
272+
fmt.Fprintln(os.Stderr, "missing required <app-path>")
273+
fs.Usage()
274+
os.Exit(2)
275+
}
276+
appPath := fs.Arg(0)
277+
278+
// detect language
279+
if appLanguage == "" || appLanguage == "auto" {
280+
appLanguage = detectLanguage(appPath)
281+
if appLanguage == "" {
282+
log.Fatalf("unable to detect language for %s — please specify with -language", appPath)
283+
}
284+
fmt.Println("Detected language:", appLanguage)
285+
}
286+
287+
// 1) build
288+
if err := os.MkdirAll(filepath.Join(appPath, "app"), 0o755); err != nil {
289+
log.Fatalf("failed to create app folder: %v", err)
290+
}
291+
cmdBuild([]string{appPath, "-language", appLanguage})
292+
293+
// 2) build & run (language-specific)
294+
switch appLanguage {
295+
case "go":
296+
binDir := filepath.Join(appPath, ".polycode")
297+
_ = os.MkdirAll(binDir, 0o755)
298+
bin := filepath.Join(binDir, "app")
299+
// go mod tidy at root (in case builder added deps)
300+
_ = runCmd(appPath, "go", "mod", "tidy")
301+
// build the generated app
302+
if err := runCmd(appPath, "go", "build", "-o", bin, "./app"); err != nil {
303+
log.Fatalf("build binary failed: %v", err)
304+
}
305+
fmt.Printf("▶ Running %s\n\n", bin)
306+
// exec the app and proxy signals
307+
ctx, cancel := context.WithCancel(context.Background())
308+
defer cancel()
309+
310+
cmd := exec.CommandContext(ctx, bin)
311+
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
312+
if err := cmd.Start(); err != nil {
313+
log.Fatalf("failed to start: %v", err)
314+
}
315+
done := make(chan struct{})
316+
go func() {
317+
handleSignals(func() {
318+
gracefulStop(cmd.Process)
319+
})
320+
_ = cmd.Wait()
321+
close(done)
322+
}()
323+
<-done
324+
325+
case "java":
326+
log.Fatalf("run: java pipeline not implemented yet")
327+
case "python":
328+
log.Fatalf("run: python pipeline not implemented yet")
329+
default:
330+
log.Fatalf("run: unsupported language %q", appLanguage)
131331
}
132332
}
133333

@@ -480,23 +680,111 @@ func detectLanguage(appPath string) string {
480680
}
481681
}
482682

683+
// ---------- local file ops / utilities ----------
684+
685+
func runCmd(dir string, name string, args ...string) error {
686+
cmd := exec.Command(name, args...)
687+
cmd.Dir = dir
688+
cmd.Stdout = os.Stdout
689+
cmd.Stderr = os.Stderr
690+
return cmd.Run()
691+
}
692+
693+
func copyTree(src, dst string) error {
694+
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
695+
if err != nil {
696+
return err
697+
}
698+
rel, _ := filepath.Rel(src, path)
699+
target := filepath.Join(dst, rel)
700+
if info.IsDir() {
701+
return os.MkdirAll(target, info.Mode().Perm())
702+
}
703+
// file
704+
data, err := os.ReadFile(path)
705+
if err != nil {
706+
return err
707+
}
708+
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
709+
return err
710+
}
711+
return os.WriteFile(target, data, info.Mode().Perm())
712+
})
713+
}
714+
715+
func replaceInFiles(root string, exts []string, old, new string) error {
716+
extSet := map[string]struct{}{}
717+
for _, e := range exts {
718+
extSet[e] = struct{}{}
719+
}
720+
return filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
721+
if err != nil {
722+
return err
723+
}
724+
if info.IsDir() {
725+
return nil
726+
}
727+
ext := filepath.Ext(p)
728+
if _, ok := extSet[ext]; !ok && !(strings.HasSuffix(p, "go.mod") && contains(exts, ".mod")) {
729+
return nil
730+
}
731+
b, err := os.ReadFile(p)
732+
if err != nil {
733+
return err
734+
}
735+
nb := []byte(strings.ReplaceAll(string(b), old, new))
736+
if !bytes.Equal(b, nb) {
737+
// atomic-ish write
738+
tmp := p + ".tmp"
739+
if err := os.WriteFile(tmp, nb, info.Mode().Perm()); err != nil {
740+
return err
741+
}
742+
if err := os.Rename(tmp, p); err != nil {
743+
return err
744+
}
745+
}
746+
return nil
747+
})
748+
}
749+
750+
func contains(ss []string, s string) bool {
751+
for _, x := range ss {
752+
if x == s {
753+
return true
754+
}
755+
}
756+
return false
757+
}
758+
483759
func printRootUsage() {
484-
fmt.Println(`polycode — code generator & extractor
760+
fmt.Println(`polycode — project scaffolding, build & extractor
485761
486762
Usage:
487763
polycode <command> [arguments]
488764
489765
Commands:
490-
generate Generate code from an app folder (with optional watch mode)
766+
new Create a new project from the getting-started repo
767+
build Build code from an app folder (with optional watch mode)
768+
run Build then run the app (Go supported)
491769
extract Run a client binary and capture its startup POST payload
492770
493771
Run 'polycode <command> -h' for more details.
494772
495773
Examples:
496-
polycode generate ./myapp -language go -out ./myapp/app
497-
polycode generate ./myapp -watch
774+
# Create a new project
775+
polycode new myapp -language go
776+
cd myapp
777+
polycode build .
778+
go run ./app
779+
780+
# Build in-place
781+
polycode build ./myapp -language go -out ./myapp/app
782+
polycode build ./myapp -watch
783+
784+
# Run (build + binary + execute)
785+
polycode run ./myapp
498786
499-
polycode extract ./bin/myclient
787+
# Extract startup metadata from an app binary
500788
polycode extract ./bin/myclient -out ./meta.json -callback http://localhost:8080/hook -cwd ./sandbox
501789
`)
502790
}

0 commit comments

Comments
 (0)