@@ -94,6 +94,13 @@ func (p *GeneratePlugin) Commands() []cli.Command {
9494 cli .WithFlag (cli .NewStringFlag ("name" , "n" , "Migration name" , "" )),
9595 ))
9696
97+ generateCmd .AddSubcommand (cli .NewCommand (
98+ "cli-app" ,
99+ "Generate a CLI application" ,
100+ p .generateCLIApp ,
101+ cli .WithFlag (cli .NewStringFlag ("name" , "n" , "CLI app name" , "" )),
102+ ))
103+
97104 generateCmd .AddSubcommand (cli .NewCommand (
98105 "command" ,
99106 "Generate a CLI command (for CLI apps only)" ,
@@ -135,14 +142,6 @@ func (p *GeneratePlugin) generateApp(ctx cli.CommandContext) error {
135142
136143 var appPath string
137144
138- isCLI := template == "cli"
139-
140- // Determine the internal directory based on app type
141- internalDir := "handlers"
142- if isCLI {
143- internalDir = "commands"
144- }
145-
146145 if p .config .IsSingleModule () {
147146 // Single-module: create cmd/app-name and apps/app-name
148147 structure := p .config .Project .GetStructure ()
@@ -156,7 +155,7 @@ func (p *GeneratePlugin) generateApp(ctx cli.CommandContext) error {
156155 return err
157156 }
158157
159- if err := os .MkdirAll (filepath .Join (appPath , "internal" , internalDir ), 0755 ); err != nil {
158+ if err := os .MkdirAll (filepath .Join (appPath , "internal" , "handlers" ), 0755 ); err != nil {
160159 spinner .Stop (cli .Red ("✗ Failed" ))
161160
162161 return err
@@ -171,32 +170,23 @@ func (p *GeneratePlugin) generateApp(ctx cli.CommandContext) error {
171170 }
172171
173172 // Create .forge.yaml for app
174- appConfig := p .generateAppConfig (name , template )
173+ appConfig := p .generateAppConfig (name )
175174 if err := os .WriteFile (filepath .Join (appPath , ".forge.yaml" ), []byte (appConfig ), 0644 ); err != nil {
176175 spinner .Stop (cli .Red ("✗ Failed" ))
177176
178177 return err
179178 }
180179
181- // Create app-specific config.yaml (skip for CLI apps)
182- if ! isCLI {
183- if err := p .createAppConfig (appPath , name , false ); err != nil {
184- spinner .Stop (cli .Red ("✗ Failed" ))
180+ // Create app-specific config.yaml
181+ if err := p .createAppConfig (appPath , name , false ); err != nil {
182+ spinner .Stop (cli .Red ("✗ Failed" ))
185183
186- return err
187- }
184+ return err
188185 }
189186 } else {
190187 // Multi-module: create apps/app-name with go.mod
191188 appPath = filepath .Join (p .config .RootDir , "apps" , name )
192-
193- // CLI apps use cmd/{name} instead of cmd/server
194- cmdSubDir := "server"
195- if isCLI {
196- cmdSubDir = name
197- }
198-
199- cmdPath := filepath .Join (appPath , "cmd" , cmdSubDir )
189+ cmdPath := filepath .Join (appPath , "cmd" , "server" )
200190
201191 // Create directories
202192 if err := os .MkdirAll (cmdPath , 0755 ); err != nil {
@@ -205,7 +195,7 @@ func (p *GeneratePlugin) generateApp(ctx cli.CommandContext) error {
205195 return err
206196 }
207197
208- if err := os .MkdirAll (filepath .Join (appPath , "internal" , internalDir ), 0755 ); err != nil {
198+ if err := os .MkdirAll (filepath .Join (appPath , "internal" , "handlers" ), 0755 ); err != nil {
209199 spinner .Stop (cli .Red ("✗ Failed" ))
210200
211201 return err
@@ -231,38 +221,145 @@ func (p *GeneratePlugin) generateApp(ctx cli.CommandContext) error {
231221 }
232222
233223 // Create .forge.yaml for app
234- appConfig := p .generateAppConfig (name , template )
224+ appConfig := p .generateAppConfig (name )
235225 if err := os .WriteFile (filepath .Join (appPath , ".forge.yaml" ), []byte (appConfig ), 0644 ); err != nil {
236226 spinner .Stop (cli .Red ("✗ Failed" ))
237227
238228 return err
239229 }
240230
241- // Create app-specific config.yaml (skip for CLI apps)
242- if ! isCLI {
243- if err := p .createAppConfig (appPath , name , true ); err != nil {
244- spinner .Stop (cli .Red ("✗ Failed" ))
231+ // Create app-specific config.yaml
232+ if err := p .createAppConfig (appPath , name , true ); err != nil {
233+ spinner .Stop (cli .Red ("✗ Failed" ))
245234
246- return err
247- }
235+ return err
248236 }
249237 }
250238
251239 spinner .Stop (cli .Green (fmt .Sprintf ("✓ App %s created successfully!" , name )))
252240
253241 ctx .Println ("" )
254242 ctx .Success ("Next steps:" )
243+ ctx .Println (" 1. Review app config at apps/" + name + "/config.yaml" )
244+ ctx .Println (" 2. (Optional) Create config.local.yaml for local overrides" )
245+ ctx .Println (" 3. Run: forge dev -a" , name )
246+
247+ return nil
248+ }
249+
250+ func (p * GeneratePlugin ) generateCLIApp (ctx cli.CommandContext ) error {
251+ if p .config == nil {
252+ ctx .Error (errors .New ("no .forge.yaml found in current directory or any parent" ))
253+ ctx .Println ("" )
254+ ctx .Info ("This doesn't appear to be a Forge project." )
255+ ctx .Info ("To initialize a new project, run:" )
256+ ctx .Println (" forge init" )
257+
258+ return errors .New ("not a forge project" )
259+ }
260+
261+ name := ctx .String ("name" )
262+ if name == "" {
263+ var err error
264+
265+ name , err = ctx .Prompt ("CLI app name:" )
266+ if err != nil {
267+ return err
268+ }
269+ }
270+
271+ spinner := ctx .Spinner (fmt .Sprintf ("Generating CLI app %s..." , name ))
272+
273+ var appPath string
274+
275+ if p .config .IsSingleModule () {
276+ // Single-module: create cmd/app-name and apps/app-name
277+ structure := p .config .Project .GetStructure ()
278+ cmdPath := filepath .Join (p .config .RootDir , structure .Cmd , name )
279+ appPath = filepath .Join (p .config .RootDir , structure .Apps , name )
280+
281+ // Create directories
282+ if err := os .MkdirAll (cmdPath , 0755 ); err != nil {
283+ spinner .Stop (cli .Red ("✗ Failed" ))
284+
285+ return err
286+ }
287+
288+ if err := os .MkdirAll (filepath .Join (appPath , "internal" , "commands" ), 0755 ); err != nil {
289+ spinner .Stop (cli .Red ("✗ Failed" ))
290+
291+ return err
292+ }
255293
256- if isCLI {
257- ctx .Println (" 1. Review the generated main.go" )
258- ctx .Println (" 2. Add commands using: forge generate command --app " + name )
259- ctx .Println (" 3. Build: go build -o " + name )
294+ // Create main.go
295+ mainContent := p .generateCLIMainFile (name )
296+ if err := os .WriteFile (filepath .Join (cmdPath , "main.go" ), []byte (mainContent ), 0644 ); err != nil {
297+ spinner .Stop (cli .Red ("✗ Failed" ))
298+
299+ return err
300+ }
301+
302+ // Create .forge.yaml for CLI app
303+ appConfig := p .generateCLIAppConfig (name )
304+ if err := os .WriteFile (filepath .Join (appPath , ".forge.yaml" ), []byte (appConfig ), 0644 ); err != nil {
305+ spinner .Stop (cli .Red ("✗ Failed" ))
306+
307+ return err
308+ }
260309 } else {
261- ctx .Println (" 1. Review app config at apps/" + name + "/config.yaml" )
262- ctx .Println (" 2. (Optional) Create config.local.yaml for local overrides" )
263- ctx .Println (" 3. Run: forge dev -a" , name )
310+ // Multi-module: create apps/app-name with go.mod
311+ appPath = filepath .Join (p .config .RootDir , "apps" , name )
312+ cmdPath := filepath .Join (appPath , "cmd" , name )
313+
314+ // Create directories
315+ if err := os .MkdirAll (cmdPath , 0755 ); err != nil {
316+ spinner .Stop (cli .Red ("✗ Failed" ))
317+
318+ return err
319+ }
320+
321+ if err := os .MkdirAll (filepath .Join (appPath , "internal" , "commands" ), 0755 ); err != nil {
322+ spinner .Stop (cli .Red ("✗ Failed" ))
323+
324+ return err
325+ }
326+
327+ // Create go.mod
328+ modulePath := fmt .Sprintf ("%s/apps/%s" , p .config .Project .Module , name )
329+ version := getLatestForgeVersion ()
330+
331+ goModContent := fmt .Sprintf ("module %s\n \n go 1.24.0\n \n require github.com/xraph/forge v%s\n " , modulePath , version )
332+ if err := os .WriteFile (filepath .Join (appPath , "go.mod" ), []byte (goModContent ), 0644 ); err != nil {
333+ spinner .Stop (cli .Red ("✗ Failed" ))
334+
335+ return err
336+ }
337+
338+ // Create main.go
339+ mainContent := p .generateCLIMainFile (name )
340+ if err := os .WriteFile (filepath .Join (cmdPath , "main.go" ), []byte (mainContent ), 0644 ); err != nil {
341+ spinner .Stop (cli .Red ("✗ Failed" ))
342+
343+ return err
344+ }
345+
346+ // Create .forge.yaml for CLI app
347+ appConfig := p .generateCLIAppConfig (name )
348+ if err := os .WriteFile (filepath .Join (appPath , ".forge.yaml" ), []byte (appConfig ), 0644 ); err != nil {
349+ spinner .Stop (cli .Red ("✗ Failed" ))
350+
351+ return err
352+ }
264353 }
265354
355+ spinner .Stop (cli .Green (fmt .Sprintf ("✓ CLI app %s created successfully!" , name )))
356+
357+ ctx .Println ("" )
358+ ctx .Success ("Next steps:" )
359+ ctx .Println (" 1. Review the generated main.go" )
360+ ctx .Println (" 2. Add commands using: forge generate command --app " + name )
361+ ctx .Println (" 3. Build: go build -o " + name )
362+
266363 return nil
267364}
268365
@@ -892,10 +989,6 @@ func (p *GeneratePlugin) printModelInfo(ctx cli.CommandContext, baseType string)
892989// Template generation functions
893990
894991func (p * GeneratePlugin ) generateMainFile (name , template string ) string {
895- if template == "cli" {
896- return p .generateCLIMainFile (name )
897- }
898-
899992 return fmt .Sprintf (`package main
900993
901994import (
@@ -970,17 +1063,17 @@ func main() {
9701063` , name , name )
9711064}
9721065
973- func (p * GeneratePlugin ) generateAppConfig (name , template string ) string {
974- if template == "cli" {
975- return fmt .Sprintf (`app:
1066+ func (p * GeneratePlugin ) generateCLIAppConfig (name string ) string {
1067+ return fmt .Sprintf (`app:
9761068 name: "%s"
9771069 type: "cli"
9781070
9791071build:
9801072 output: "%s"
9811073` , name , name )
982- }
1074+ }
9831075
1076+ func (p * GeneratePlugin ) generateAppConfig (name string ) string {
9841077 return fmt .Sprintf (`app:
9851078 name: "%s"
9861079 type: "web"
0 commit comments