github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/generate_tiltfile.go (about) 1 package cli 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "runtime" 12 "strings" 13 14 "github.com/fatih/color" 15 "golang.org/x/term" 16 ) 17 18 type generateTiltfileResult string 19 20 const ( 21 generateTiltfileError = "error" 22 generateTiltfileUserExit = "exit" 23 generateTiltfileNonInteractive = "non_interactive" 24 generateTiltfileAlreadyExists = "already_exists" 25 generateTiltfileCreated = "created" 26 generateTiltfileDeclined = "declined" 27 ) 28 29 // maybeGenerateTiltfile offers to create a Tiltfile if one does not exist and 30 // the user is running in an interactive (TTY) session. 31 // 32 // The generateTiltfileResult value is ALWAYS set, even on error. 33 func maybeGenerateTiltfile(tfPath string) (generateTiltfileResult, error) { 34 if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) { 35 return generateTiltfileNonInteractive, nil 36 } 37 38 if absPath, err := filepath.Abs(tfPath); err != nil { 39 // the absolute path is really just to improve CLI output, if the path 40 // itself is so invalid this fails, we'll catch + report it via the 41 // logic to determine if a Tiltfile already exists 42 tfPath = absPath 43 } 44 45 if hasTiltfile, err := checkTiltfileExists(tfPath); err != nil { 46 // either Tiltfile path is totally invalid or there's something like 47 // a permissions error, so report it & exit 48 return generateTiltfileError, err 49 } else if hasTiltfile { 50 // Tiltfile exists, so don't prompt to generate one 51 return generateTiltfileAlreadyExists, nil 52 } 53 54 restoreTerm, err := setupTerm(os.Stdin) 55 if err != nil { 56 return generateTiltfileError, nil 57 } 58 59 lineCount := 0 60 var postFinishMessage string 61 defer func() { 62 _ = restoreTerm() 63 64 if postFinishMessage != "" { 65 // NOTE: pre-win10, there's no support for ANSI escape codes, and 66 // it's not worth the headache to deal with Windows console API 67 // for this, so the output isn't cleared there 68 if err == nil && runtime.GOOS != "windows" { 69 // erase our output once done on success 70 // \033[%d -> move cursor up %d rows 71 // \r -> move cursor to first column 72 // \033[J -> clear output from cursor to end of stream 73 fmt.Printf("\033[%dA\r\033[J", lineCount) 74 } 75 76 fmt.Println(postFinishMessage) 77 } 78 }() 79 80 // using os.Stdin directly works on *nix, but Windows needs the reads to 81 // happen from os.Stdin and critically, the writes to be on os.Stdout 82 // (this wrapper also works on *nix) 83 termIO := struct { 84 io.Reader 85 io.Writer 86 }{os.Stdin, os.Stdout} 87 t := term.NewTerminal(termIO, "✨ Create a starter Tiltfile? (y/n) ") 88 89 // Offer to create a Tiltfile 90 var intro bytes.Buffer 91 intro.WriteString(tiltfileDoesNotExistWarning(tfPath)) 92 intro.WriteString(` 93 Tilt can create a sample Tiltfile for you, which includes 94 useful snippets to modify and extend with build and deploy 95 steps for your microservices. 96 `) 97 intro.WriteString("\n") 98 _, err = t.Write(intro.Bytes()) 99 if err != nil { 100 return generateTiltfileError, err 101 } 102 // we track # of lines written to clear the output when done 103 lineCount += bytes.Count(intro.Bytes(), []byte("\n")) 104 105 for { 106 line, err := t.ReadLine() 107 lineCount++ 108 if err != nil { 109 // perform a carriage return to ensure we're back at the beginning 110 // of a new line (if user hit Ctrl-C/Ctrl-D, this is necessary; for 111 // any other errors, better to be safe than leave terminal in a bad 112 // state) 113 fmt.Println("\r") 114 if err == io.EOF { 115 // since we have the terminal in raw mode, no signal will be fired 116 // on Ctrl-C, so we manually propagate it here (sending ourselves 117 // a SIGINT signal is not practical) 118 return generateTiltfileUserExit, userExitError 119 } 120 return generateTiltfileError, err 121 } 122 123 line = strings.ToLower(strings.TrimSpace(line)) 124 if strings.HasPrefix(line, "y") { 125 break 126 } else if strings.HasPrefix(line, "n") { 127 // there's a noticeable delay before further output indicating that 128 // Tilt has started, so we don't want users to think Tilt is hung 129 postFinishMessage = "Starting Tilt...\n" 130 return generateTiltfileDeclined, nil 131 } 132 } 133 134 if err = os.WriteFile(tfPath, starterTiltfile, 0644); err != nil { 135 return generateTiltfileError, fmt.Errorf("could not write to %s: %v", tfPath, err) 136 } 137 138 postFinishMessage = generateTiltfileSuccessMessage(tfPath) 139 return generateTiltfileCreated, nil 140 } 141 142 func setupTerm(f *os.File) (restore func() error, err error) { 143 oldState, err := term.MakeRaw(int(f.Fd())) 144 if err != nil { 145 return nil, err 146 } 147 restore = func() error { 148 return term.Restore(int(f.Fd()), oldState) 149 } 150 return restore, nil 151 } 152 153 func checkTiltfileExists(tfPath string) (bool, error) { 154 if fi, err := os.Stat(tfPath); err == nil { 155 if fi.Mode().IsDir() { 156 return false, fmt.Errorf("could not open Tiltfile at %s: target is a directory", tfPath) 157 } 158 // Tiltfile exists! 159 return true, nil 160 } else if errors.Is(err, fs.ErrNotExist) { 161 return false, nil 162 } else { 163 // likely a permissions issue, bubble up the error and exit Tilt 164 // N.B. os::Stat always returns a PathError which will include the path in its output 165 return false, fmt.Errorf("could not open Tiltfile: %v", err) 166 } 167 } 168 169 func tiltfileDoesNotExistWarning(tfPath string) string { 170 return color.YellowString(` 171 ─────────────────────────────────────────────────────────── 172 ⚠️ No Tiltfile exists at %s 173 ─────────────────────────────────────────────────────────── 174 `, tfPath) 175 } 176 177 func generateTiltfileSuccessMessage(tfPath string) string { 178 return fmt.Sprintf(` 179 ─────────────────────────────────────────────────────────── 180 🎉 Tiltfile generated at %s 181 ─────────────────────────────────────────────────────────── 182 `, color.BlueString(tfPath)) 183 }