@@ -21,6 +21,7 @@ import (
2121 "os"
2222 "path/filepath"
2323 "slices"
24+ "sort"
2425 "strings"
2526 "text/template"
2627
@@ -49,43 +50,49 @@ func HasDockerfile(dir string) (bool, error) {
4950}
5051
5152func CreateDockerfile (dir string , projectType ProjectType , settingsMap map [string ]string ) error {
52- if len (settingsMap ) == 0 {
53- return fmt .Errorf ("unable to fetch client settings from server, please try again later" )
53+ dockerfileContent , dockerIgnoreContent , err := GenerateDockerArtifacts (dir , projectType , settingsMap )
54+ if err != nil {
55+ return err
5456 }
5557
56- var dockerfileContent []byte
57- var dockerIgnoreContent []byte
58- var err error
59-
60- dockerfileContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".Dockerfile" )
61- if err != nil {
58+ if err := os .WriteFile (filepath .Join (dir , "Dockerfile" ), dockerfileContent , 0644 ); err != nil {
6259 return err
6360 }
6461
65- dockerIgnoreContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".dockerignore" )
66- if err != nil {
62+ if err := os .WriteFile (filepath .Join (dir , ".dockerignore" ), dockerIgnoreContent , 0644 ); err != nil {
6763 return err
6864 }
6965
70- // TODO: (@rektdeckard) support Node entrypoint validation
71- if projectType .IsPython () {
72- dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType , settingsMap )
73- if err != nil {
74- return err
75- }
66+ return nil
67+ }
68+
69+ // GenerateDockerArtifacts returns the Dockerfile and .dockerignore contents for the
70+ // provided project type without writing them to disk. The Dockerfile content may be
71+ // templated/validated (e.g., Python entrypoint).
72+ func GenerateDockerArtifacts (dir string , projectType ProjectType , settingsMap map [string ]string ) ([]byte , []byte , error ) {
73+ if len (settingsMap ) == 0 {
74+ return nil , nil , fmt .Errorf ("unable to fetch client settings from server, please try again later" )
7675 }
7776
78- err = os . WriteFile ( filepath . Join ( dir , "Dockerfile" ), dockerfileContent , 0644 )
77+ dockerfileContent , err := fs . ReadFile ( "examples/" + string ( projectType ) + ".Dockerfile" )
7978 if err != nil {
80- return err
79+ return nil , nil , err
8180 }
8281
83- err = os . WriteFile ( filepath . Join ( dir , ".dockerignore" ), dockerIgnoreContent , 0644 )
82+ dockerIgnoreContent , err := fs . ReadFile ( "examples/" + string ( projectType ) + ".dockerignore" )
8483 if err != nil {
85- return err
84+ return nil , nil , err
8685 }
8786
88- return nil
87+ // TODO: (@rektdeckard) support Node entrypoint validation
88+ if projectType .IsPython () {
89+ dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType , settingsMap )
90+ if err != nil {
91+ return nil , nil , err
92+ }
93+ }
94+
95+ return dockerfileContent , dockerIgnoreContent , nil
8996}
9097
9198func validateEntrypoint (dir string , dockerfileContent []byte , dockerignoreContent []byte , projectType ProjectType , settingsMap map [string ]string ) ([]byte , error ) {
@@ -115,13 +122,16 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
115122 return err
116123 }
117124 if ! d .IsDir () && strings .HasSuffix (d .Name (), projectType .FileExt ()) {
125+ // Exclude files like __init__.py which cannot be entrypoints
126+ if d .Name () == "__init__.py" {
127+ return nil
128+ }
118129 fileList = append (fileList , path )
119130 }
120131 return nil
121132 }); err != nil {
122133 return "" , fmt .Errorf ("error walking directory %s: %w" , dir , err )
123134 }
124-
125135 if slices .Contains (fileList , fileName ) {
126136 return fileName , nil
127137 }
@@ -131,11 +141,39 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
131141 return "" , nil
132142 }
133143
144+ // Prioritize common entrypoint filenames at the top of the list
145+ if len (fileList ) > 1 {
146+ priority := func (p string ) int {
147+ name := filepath .Base (p )
148+ switch name {
149+ case "main.py" :
150+ return 0
151+ case "agent.py" :
152+ return 1
153+ default :
154+ return 2
155+ }
156+ }
157+ sort .SliceStable (fileList , func (i , j int ) bool {
158+ pi := priority (fileList [i ])
159+ pj := priority (fileList [j ])
160+ if pi != pj {
161+ return pi < pj
162+ }
163+ return fileList [i ] < fileList [j ]
164+ })
165+ }
166+
167+ // If there's only one candidate, select it automatically
168+ if len (fileList ) == 1 {
169+ return fileList [0 ], nil
170+ }
171+
134172 var selected string
135173 form := huh .NewForm (
136174 huh .NewGroup (
137175 huh .NewSelect [string ]().
138- Title (fmt .Sprintf ("Select %s file to use as entrypoint" , projectType .Lang ())).
176+ Title (fmt .Sprintf ("Select the %s file which contains your agent's entrypoint" , projectType .Lang ())).
139177 Options (huh .NewOptions (fileList ... )... ).
140178 Value (& selected ).
141179 WithTheme (util .Theme ),
@@ -145,7 +183,6 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
145183 if err := form .Run (); err != nil {
146184 return "" , err
147185 }
148-
149186 return selected , nil
150187 }
151188
@@ -159,6 +196,11 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
159196 return nil , err
160197 }
161198
199+ if newEntrypoint == "" {
200+ newEntrypoint = pythonEntrypoint
201+ }
202+ fmt .Printf ("Using entrypoint file [%s]\n " , util .Accented (newEntrypoint ))
203+
162204 tpl := template .Must (template .New ("Dockerfile" ).Parse (string (dockerfileContent )))
163205 buf := & bytes.Buffer {}
164206 tpl .Execute (buf , map [string ]string {
0 commit comments