@@ -34,6 +34,20 @@ import (
3434//go:embed examples/*
3535var fs embed.FS
3636
37+ type GenerationStatus int
38+
39+ const (
40+ GenerationStatusGenerated GenerationStatus = iota
41+ GenerationStatusSkipped // File exists and no force flag
42+ GenerationStatusFailed // Error during generation
43+ )
44+
45+ type GenerationResult struct {
46+ Status GenerationStatus
47+ Message string
48+ Error error
49+ }
50+
3751func HasDockerfile (dir string ) (bool , error ) {
3852 entries , err := os .ReadDir (dir )
3953 if err != nil {
@@ -48,20 +62,22 @@ func HasDockerfile(dir string) (bool, error) {
4862 return false , nil
4963}
5064
51- func CreateDockerfile (dir string , settingsMap map [string ]string , silent bool ) error {
65+ // Returns dockerfile content, dockerignore content, and error
66+ func PrepareDockerfileContent (dir string , settingsMap map [string ]string , silent bool ) ([]byte , []byte , error ) {
5267 if len (settingsMap ) == 0 {
53- return fmt .Errorf ("unable to fetch client settings from server, please try again later" )
68+ return nil , nil , fmt .Errorf ("unable to fetch client settings from server, please try again later" )
5469 }
5570
5671 projectType , err := DetectProjectType (dir )
5772 if err != nil {
58- return fmt .Errorf (`× Unable to determine project type
73+ return nil , nil , fmt .Errorf (`× Unable to determine project type
5974
6075Supported project types:
61- • Python: requires requirements.txt or pyproject.toml
62- • Node.js: requires package.json
76+ • Python: pip, uv, pdm, hatch, poetry, pipenv
77+ • Node.js: npm, pnpm, yarn, yarn-berry, bun
6378
64- Please ensure your project has the appropriate dependency file, or create a Dockerfile manually in the current directory` )
79+ Please ensure your project has the appropriate project files (node projects may
80+ need to be buit first), or create a Dockerfile manually in the current directory` )
6581 }
6682
6783 if ! silent {
@@ -142,47 +158,136 @@ Please ensure your project has the appropriate dependency file, or create a Dock
142158
143159 dockerfileContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".Dockerfile" )
144160 if err != nil {
145- return fmt .Errorf ("failed to load Dockerfile template '%s': %w" , string (projectType ), err )
161+ return nil , nil , fmt .Errorf ("failed to load Dockerfile template '%s': %w" , string (projectType ), err )
146162 }
147163
148164 dockerIgnoreContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".dockerignore" )
149165 if err != nil {
150- return fmt .Errorf ("failed to load .dockerignore template for '%s': %w" , string (projectType ), err )
166+ return nil , nil , fmt .Errorf ("failed to load .dockerignore template for '%s': %w" , string (projectType ), err )
151167 }
152168
153169 // Validate entrypoint for both Python and Node.js projects
154170 if projectType .IsPython () {
155171 dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType , settingsMap , silent )
156172 if err != nil {
157- return fmt .Errorf ("failed to validate Python entry point: %w" , err )
173+ return nil , nil , fmt .Errorf ("failed to validate Python entry point: %w" , err )
158174 }
159175 } else if projectType .IsNode () {
160176 dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType , settingsMap , silent )
161177 if err != nil {
162- return fmt .Errorf ("failed to validate Node.js entry point: %w" , err )
178+ return nil , nil , fmt .Errorf ("failed to validate Node.js entry point: %w" , err )
163179 }
164180 }
165181
166- err = os .WriteFile (filepath .Join (dir , "Dockerfile" ), dockerfileContent , 0644 )
182+ return dockerfileContent , dockerIgnoreContent , nil
183+ }
184+
185+ // GenerateDockerfile generates only the Dockerfile with status reporting
186+ // If preparedDockerfileContent is provided (non-nil), it will be used instead of calling PrepareDockerfileContent
187+ func GenerateDockerfile (dir string , settingsMap map [string ]string , silent bool , force bool , preparedDockerfileContent []byte ) GenerationResult {
188+ dockerfilePath := filepath .Join (dir , "Dockerfile" )
189+
190+ // Check if file exists
191+ if ! force {
192+ if _ , err := os .Stat (dockerfilePath ); err == nil {
193+ return GenerationResult {
194+ Status : GenerationStatusSkipped ,
195+ Message : "Dockerfile already exists. Use --force to overwrite" ,
196+ }
197+ }
198+ }
199+
200+ // Get content - use prepared content if provided, otherwise generate it
201+ var dockerfileContent []byte
202+ if preparedDockerfileContent != nil {
203+ dockerfileContent = preparedDockerfileContent
204+ } else {
205+ var err error
206+ dockerfileContent , _ , err = PrepareDockerfileContent (dir , settingsMap , silent )
207+ if err != nil {
208+ return GenerationResult {
209+ Status : GenerationStatusFailed ,
210+ Error : err ,
211+ Message : fmt .Sprintf ("Failed to prepare Dockerfile: %v" , err ),
212+ }
213+ }
214+ }
215+
216+ // Write file
217+ err := os .WriteFile (dockerfilePath , dockerfileContent , 0644 )
167218 if err != nil {
168- return fmt .Errorf ("failed to write Dockerfile: %w" , err )
219+ return GenerationResult {
220+ Status : GenerationStatusFailed ,
221+ Error : fmt .Errorf ("failed to write Dockerfile: %w" , err ),
222+ Message : fmt .Sprintf ("Failed to write Dockerfile: %v" , err ),
223+ }
224+ }
225+
226+ message := "Generated Dockerfile"
227+ if force {
228+ if _ , err := os .Stat (dockerfilePath ); err == nil {
229+ message = "Overwrote existing Dockerfile"
230+ }
231+ }
232+
233+ return GenerationResult {
234+ Status : GenerationStatusGenerated ,
235+ Message : message ,
236+ }
237+ }
238+
239+ // GenerateDockerIgnore generates only the .dockerignore with status reporting
240+ // If preparedDockerignoreContent is provided (non-nil), it will be used instead of calling PrepareDockerfileContent
241+ func GenerateDockerIgnore (dir string , settingsMap map [string ]string , silent bool , force bool , preparedDockerignoreContent []byte ) GenerationResult {
242+ dockerignorePath := filepath .Join (dir , ".dockerignore" )
243+
244+ // Check if file exists
245+ if ! force {
246+ if _ , err := os .Stat (dockerignorePath ); err == nil {
247+ return GenerationResult {
248+ Status : GenerationStatusSkipped ,
249+ Message : ".dockerignore already exists. Use --force to overwrite" ,
250+ }
251+ }
169252 }
170253
171- err = os .WriteFile (filepath .Join (dir , ".dockerignore" ), dockerIgnoreContent , 0644 )
254+ // Get content - use prepared content if provided, otherwise generate it
255+ var dockerignoreContent []byte
256+ if preparedDockerignoreContent != nil {
257+ dockerignoreContent = preparedDockerignoreContent
258+ } else {
259+ var err error
260+ _ , dockerignoreContent , err = PrepareDockerfileContent (dir , settingsMap , silent )
261+ if err != nil {
262+ return GenerationResult {
263+ Status : GenerationStatusFailed ,
264+ Error : err ,
265+ Message : fmt .Sprintf ("Failed to prepare .dockerignore: %v" , err ),
266+ }
267+ }
268+ }
269+
270+ // Write file
271+ err := os .WriteFile (dockerignorePath , dockerignoreContent , 0644 )
172272 if err != nil {
173- return fmt .Errorf ("failed to write .dockerignore: %w" , err )
273+ return GenerationResult {
274+ Status : GenerationStatusFailed ,
275+ Error : fmt .Errorf ("failed to write .dockerignore: %w" , err ),
276+ Message : fmt .Sprintf ("Failed to write .dockerignore: %v" , err ),
277+ }
174278 }
175279
176- if ! silent {
177- fmt .Printf ("\n ✔ Successfully generated Docker files:\n " )
178- fmt .Printf (" %s - Container build instructions\n " , util .Accented ("Dockerfile" ))
179- fmt .Printf (" %s - Files excluded from build context\n " , util .Accented (".dockerignore" ))
180- fmt .Printf ("\n Next steps:\n " )
181- fmt .Printf (" ► Review the %s and uncomment/update any needed packages\n " , util .Accented ("Dockerfile" ))
182- fmt .Printf (" ► Build your agent: docker build -t my-agent .\n " )
280+ message := "Generated .dockerignore"
281+ if force {
282+ if _ , err := os .Stat (dockerignorePath ); err == nil {
283+ message = "Overwrote existing .dockerignore"
284+ }
183285 }
184286
185- return nil
287+ return GenerationResult {
288+ Status : GenerationStatusGenerated ,
289+ Message : message ,
290+ }
186291}
187292
188293func validateUVProject (dir string , silent bool ) {
@@ -339,7 +444,7 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
339444 allowedPaths ["dist" ] = true
340445 allowedPaths ["out" ] = true
341446 }
342-
447+
343448 // TODO: Parse tsconfig.json "outDir" if it exists to get the actual output directory
344449 // For now, we're using common conventions
345450 }
0 commit comments