@@ -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 \n Options:\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 \n Options:\n " )
191+ fmt .Fprintf (fs .Output (), "Usage:\n polycode build <app-path> [options]\n \n Options:\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 \n Options:\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+
483759func printRootUsage () {
484- fmt .Println (`polycode — code generator & extractor
760+ fmt .Println (`polycode — project scaffolding, build & extractor
485761
486762Usage:
487763 polycode <command> [arguments]
488764
489765Commands:
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
493771Run 'polycode <command> -h' for more details.
494772
495773Examples:
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