@@ -3,6 +3,7 @@ package cmd
33import (
44 "bytes"
55 "context"
6+ "errors"
67 "fmt"
78 "os"
89 "os/exec"
@@ -80,6 +81,54 @@ type WorkflowInitRunner struct {
8081 cmd * cobra.Command
8182}
8283
84+ func truncateDisplayWidth (s string , maxWidth int ) string {
85+ if maxWidth <= 0 {
86+ return ""
87+ }
88+ if lipgloss .Width (s ) <= maxWidth {
89+ return s
90+ }
91+
92+ const ellipsis = "…"
93+ ellipsisWidth := lipgloss .Width (ellipsis )
94+ if maxWidth <= ellipsisWidth {
95+ return ellipsis
96+ }
97+
98+ available := maxWidth - ellipsisWidth
99+ var b strings.Builder
100+ used := 0
101+ for _ , r := range s {
102+ rw := lipgloss .Width (string (r ))
103+ if used + rw > available {
104+ break
105+ }
106+ b .WriteRune (r )
107+ used += rw
108+ }
109+
110+ return b .String () + ellipsis
111+ }
112+
113+ func expandHomePath (path string ) (string , error ) {
114+ if path == "" || path [0 ] != '~' {
115+ return path , nil
116+ }
117+ if path != "~" && len (path ) > 1 && path [1 ] != '/' && path [1 ] != '\\' {
118+ // Keep "~user/..." unchanged; we only support current-user home expansion.
119+ return path , nil
120+ }
121+
122+ home , err := os .UserHomeDir ()
123+ if err != nil {
124+ return "" , fmt .Errorf ("failed to resolve home directory: %w" , err )
125+ }
126+ if path == "~" {
127+ return home , nil
128+ }
129+ return filepath .Join (home , path [2 :]), nil
130+ }
131+
83132// prePrompt prints a blank line before an interactive prompt for visual spacing.
84133func (r * WorkflowInitRunner ) prePrompt () {
85134 command .Println (r .cmd , "" )
@@ -94,6 +143,14 @@ func (r *WorkflowInitRunner) postPrompt() {
94143 }
95144}
96145
146+ func (r * WorkflowInitRunner ) handlePromptError (err error ) error {
147+ if errors .Is (err , huh .ErrUserAborted ) {
148+ command .Println (r .cmd , "Setup canceled." )
149+ return nil
150+ }
151+ return err
152+ }
153+
97154// Run executes the full init flow: resolve templates, prompt for options
98155// (in interactive mode), scaffold, install deps, init git, and print results.
99156func (r * WorkflowInitRunner ) Run (ctx context.Context , input WorkflowInitInput ) error {
@@ -126,7 +183,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
126183 ),
127184 )
128185 if err := form .Run (); err != nil {
129- return err
186+ return r . handlePromptError ( err )
130187 }
131188 r .postPrompt ()
132189 input .Language = language
@@ -198,11 +255,27 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
198255 input .Template = templates [0 ].DirName
199256 } else if r .interactive && ! skipPrompts {
200257 r .prePrompt ()
258+ const maxTemplateNameWidth = 20
259+ const templateDescriptionGap = " "
260+ templateNameStyle := lipgloss .NewStyle ().Bold (true )
261+ maxTemplateLabelWidth := 0
262+ for _ , t := range templates {
263+ name := truncateDisplayWidth (t .Name , maxTemplateNameWidth )
264+ if w := lipgloss .Width (name ); w > maxTemplateLabelWidth {
265+ maxTemplateLabelWidth = w
266+ }
267+ }
268+
201269 var templateOptions []huh.Option [string ]
202270 for _ , t := range templates {
203- label := t .Name
271+ name := truncateDisplayWidth (t .Name , maxTemplateNameWidth )
272+ label := templateNameStyle .Render (name )
204273 if t .Description != "" {
205- label = fmt .Sprintf ("%s — %s" , t .Name , t .Description )
274+ padding := ""
275+ if pad := maxTemplateLabelWidth - lipgloss .Width (name ); pad > 0 {
276+ padding = strings .Repeat (" " , pad )
277+ }
278+ label = fmt .Sprintf ("%s%s%s%s" , label , padding , templateDescriptionGap , t .Description )
206279 }
207280 templateOptions = append (templateOptions , huh .NewOption (label , t .DirName ))
208281 }
@@ -217,7 +290,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
217290 ),
218291 )
219292 if err := form .Run (); err != nil {
220- return err
293+ return r . handlePromptError ( err )
221294 }
222295 r .postPrompt ()
223296 input .Template = selected
@@ -264,17 +337,21 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
264337 form := huh .NewForm (
265338 huh .NewGroup (
266339 huh .NewInput ().
267- Title ("Where should we create your workflows project? " ).
340+ Title ("Specify a project directory (must be new or empty) " ).
268341 Value (& dir ),
269342 ),
270343 )
271344 if err := form .Run (); err != nil {
272- return err
345+ return r . handlePromptError ( err )
273346 }
274347 r .postPrompt ()
275348 input .Dir = dir
276349
277- absDir , err := filepath .Abs (input .Dir )
350+ expandedDir , err := expandHomePath (input .Dir )
351+ if err != nil {
352+ return err
353+ }
354+ absDir , err := filepath .Abs (expandedDir )
278355 if err != nil {
279356 return fmt .Errorf ("failed to resolve path: %w" , err )
280357 }
@@ -298,7 +375,11 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
298375
299376 input .Dir = strings .TrimPrefix (input .Dir , "./" )
300377
301- absDir , err := filepath .Abs (input .Dir )
378+ expandedDir , err := expandHomePath (input .Dir )
379+ if err != nil {
380+ return err
381+ }
382+ absDir , err := filepath .Abs (expandedDir )
302383 if err != nil {
303384 return fmt .Errorf ("failed to resolve path: %w" , err )
304385 }
@@ -319,7 +400,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
319400 form := huh .NewForm (
320401 huh .NewGroup (
321402 huh .NewSelect [string ]().
322- Title ("Install dependencies?" ).
403+ Title ("Install project dependencies?" ).
323404 Description ("(recommended)" ).
324405 Options (
325406 huh .NewOption ("Yes" , "yes" ),
@@ -329,7 +410,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
329410 ),
330411 )
331412 if err := form .Run (); err != nil {
332- return err
413+ return r . handlePromptError ( err )
333414 }
334415 r .postPrompt ()
335416 wantDeps = installDeps == "yes"
@@ -357,7 +438,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
357438 huh .NewGroup (
358439 huh .NewSelect [string ]().
359440 Title ("Initialize a new Git repository?" ).
360- Description ("(optional )" ).
441+ Description ("(recommended )" ).
361442 Options (
362443 huh .NewOption ("Yes" , "yes" ),
363444 huh .NewOption ("No" , "no" ),
@@ -366,7 +447,7 @@ func (r *WorkflowInitRunner) Run(ctx context.Context, input WorkflowInitInput) e
366447 ),
367448 )
368449 if err := form .Run (); err != nil {
369- return err
450+ return r . handlePromptError ( err )
370451 }
371452 r .postPrompt ()
372453 wantGit = initGit == "yes"
0 commit comments