package main import ( "fmt" "os" "preflight/src/command" ) func main() { if err := command.PreflightCommand().Execute(); err != nil { fmt.Println(err) os.Exit(1) } }
package command import ( "fmt" "preflight/src/styles" "strings" "github.com/spf13/cobra" ) var ( Remote bool Checklist []string ) func PreflightCommand() *cobra.Command { var rootCmd = &cobra.Command{ Use: "preflight [flags] [checklist file]", Short: fmt.Sprintf("Automate checklist to ensure you are ready to %s 🛫", styles.Golor), Args: ValidateArgs, Run: Run, } // Add long description rootCmd.Long = makeLongDescription() // Add flags rootCmd. Flags(). BoolVarP(&Remote, "remote", "r", false, "Fetch your checklist file from a remote server.") rootCmd. Flags(). StringArrayVarP(&Checklist, "checklists", "c", nil, "Use predefined checklists") return rootCmd } func makeLongDescription() string { builder := strings.Builder{} builder.WriteString("A small CLI that will run some commands for you, depending on the chosen config, to make sure you are ready to go 🛫") builder.WriteString("\n\n") builder.WriteString(fmt.Sprintf("Written with %s in %s", styles.Heart, styles.Golor)) return builder.String() }
package command import ( "fmt" "os" "preflight/src/io" "preflight/src/programs" "preflight/src/systemcheck" ) func ReadFile(filePath string) []systemcheck.SystemCheck { var ( dataBytes []byte err error ) if Remote { dataBytes, err = programs.LoadHttpFileFrom(filePath) } else { dataBytes, err = io.ReadFile(filePath) } if err != nil { fmt.Println(err) os.Exit(1) } systemChecks, err := io.ReadChecklist(dataBytes) if err != nil { fmt.Println(err) os.Exit(1) } return systemChecks }
package command import ( "fmt" "os" "preflight/presets" "preflight/src/preflight" "preflight/src/programs" "preflight/src/styles" "preflight/src/systemcheck" "sort" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) func Run(cmd *cobra.Command, args []string) { fmt.Println() var systemChecks []systemcheck.SystemCheck if cmd.Flag("checklists").Changed { systemChecks = programs.UsePresets(strings.Split(Checklist[0], ","), presets.Presets) } else { systemChecks = ReadFile(args[0]) } sort.SliceStable(systemChecks, func(a, b int) bool { return systemChecks[a].Name < systemChecks[b].Name }) if len(systemChecks) == 0 { fmt.Println(styles.WarningMark.String() + styles.WarningMark.Render(" Your checklist is empty! Weird but why not?")) fmt.Println(styles.CheckMark.Render("Done! You're good to go 🛫")) os.Exit(0) } if _, err := tea.NewProgram(preflight.InitPreflightModel(systemChecks)).Run(); err != nil { fmt.Println(err) os.Exit(1) } }
package command import ( "errors" "github.com/spf13/cobra" ) func ValidateArgs(cmd *cobra.Command, args []string) error { checklists := cmd.Flag("checklists").Value.String() if len(args) < 1 && len(checklists) <= 2 { return errors.New("requires a path to the checklist file or a list of presets") } return nil }
package io import ( "fmt" ) type OSInterpreter struct { Interpreter string InterpreterArgs string InterpreterInteractiveArgs string Command string CommandArgs string } func GetInterpreterCommand(os string) (OSInterpreter, error) { switch os { case "windows": return OSInterpreter{ Interpreter: "powershell.exe", InterpreterArgs: "", InterpreterInteractiveArgs: "", Command: "command", CommandArgs: "", }, nil case "darwin": return OSInterpreter{ Interpreter: "bash", InterpreterArgs: "-c", InterpreterInteractiveArgs: "-ic", Command: "command", CommandArgs: "-v", }, nil case "linux": return OSInterpreter{ Interpreter: "bash", InterpreterArgs: "-c", InterpreterInteractiveArgs: "-ic", Command: "command", CommandArgs: "-v", }, nil default: return OSInterpreter{}, fmt.Errorf("OS %s is not currently supported", os) } }
package io import ( "io" "io/ioutil" "net/http" "preflight/src/systemcheck" "gopkg.in/yaml.v3" ) func ReadFile(path string) ([]byte, error) { buf, err := ioutil.ReadFile(path) return buf, err } func ReadHttpFile(path string) ([]byte, error) { resp, err := http.Get(path) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } func ReadChecklist(checklist []byte) ([]systemcheck.SystemCheck, error) { data := []systemcheck.SystemCheck{} err := yaml.Unmarshal(checklist, &data) if err != nil { return []systemcheck.SystemCheck{}, err } return data, nil }
package preflight import ( "fmt" "preflight/src/styles" "preflight/src/systemcheck" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/lipgloss" ) type PreflightModel struct { checks []systemcheck.SystemCheck spinner spinner.Model progress progress.Model activeIndex int activeCheckpointIndex int done bool } func (p PreflightModel) getActive() *systemcheck.SystemCheck { return &p.checks[p.activeIndex] } func (p PreflightModel) getActiveCheckpoint() systemcheck.Checkpoint { return p.getActive().Checkpoints[p.activeCheckpointIndex] } func InitPreflightModel(systemCheck []systemcheck.SystemCheck) PreflightModel { fmt.Println(styles.Greetings.String()) p := progress.New( progress.WithGradient(string(styles.Ocean), string(styles.White)), ) s := spinner.New() s.Spinner = spinner.Jump s.Style = lipgloss.NewStyle().Foreground(styles.Honey) return PreflightModel{ checks: systemCheck, spinner: s, progress: p, } }
package preflight import ( "strings" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" ) func (p PreflightModel) Init() tea.Cmd { return tea.Batch( p.runCheckpoint(), p.spinner.Tick, ) } func (p PreflightModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: p.progress.Width = msg.Width case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc", "q": return p, tea.Quit } case systemCheckMsg: return p.UpdateInternalState(msg) case progress.FrameMsg: return p.UpdateProgress(msg) default: p.spinner, cmd = p.spinner.Update(msg) } return p, cmd } func (p PreflightModel) UpdateProgress(msg progress.FrameMsg) (PreflightModel, tea.Cmd) { newModel, cmd := p.progress.Update(msg) if newModel, ok := newModel.(progress.Model); ok { p.progress = newModel } return p, cmd } func (p PreflightModel) View() string { view := strings.Builder{} if p.done { view.WriteString(p.checks[len(p.checks)-1].RenderResult()) view.WriteString(p.RenderConclusion()) return view.String() } for i := p.activeIndex; i < len(p.checks); i++ { view.WriteString(p.checks[i].RenderSystemCheck(i == p.activeIndex, p.spinner)) } view.WriteString("\n") view.WriteString(p.progress.View()) return view.String() }
package preflight import "preflight/src/styles" func (p PreflightModel) RenderConclusion() string { hasFail := false hasWarning := false for _, systemCheck := range p.checks { if !systemCheck.Check { if systemCheck.Optional { hasWarning = true } else { hasFail = true break } } } if hasFail { return styles.KoMark.Render("\n\n No go, no go! Check above for more details. 🛬\n") } if hasWarning { return styles.WarningMark.Render("\n\n You're good to go, but check above, some checks were unsuccessful 🎫\n") } return styles.CheckMark.Render("\n\nDone! You're good to go 🛫\n") }
package preflight import ( "fmt" "os/exec" "preflight/src/io" "runtime" "time" tea "github.com/charmbracelet/bubbletea" ) type systemCheckMsg struct{ check bool } func (p PreflightModel) runCheckpoint() tea.Cmd { checkpoint := p.getActiveCheckpoint() interpreter, err := io.GetInterpreterCommand(runtime.GOOS) if err != nil { fmt.Println(err) return tea.Quit } interpreterArg := interpreter.InterpreterArgs if checkpoint.UseInteractive { interpreterArg = interpreter.InterpreterInteractiveArgs } arg := fmt.Sprintf("%s %s %s", interpreter.Command, interpreter.CommandArgs, checkpoint.Command) command := exec.Command(interpreter.Interpreter, interpreterArg, arg) // Run check only return func() tea.Msg { err := command.Run() return systemCheckMsg{check: err == nil} } } func (p PreflightModel) UpdateInternalState(msg systemCheckMsg) (PreflightModel, tea.Cmd) { p.activeCheckpointIndex++ if msg.check { p.getActive().Check = msg.check } result := p.getActive().RenderResult() if p.activeCheckpointIndex >= len(p.getActive().Checkpoints) { p.activeCheckpointIndex = 0 p.activeIndex++ if p.activeIndex >= len(p.checks) { // Everything's been installed. We're done! p.done = true return p, tea.Quit } progressCmd := p.progress.SetPercent(float64(p.activeIndex) / float64(len(p.checks))) return p, tea.Batch( progressCmd, tea.Tick(time.Millisecond*time.Duration(150), func(t time.Time) tea.Msg { return p.runCheckpoint()() }), tea.Printf(result), ) } return p, p.runCheckpoint() }
package programs import ( "fmt" "preflight/src/io" "preflight/src/styles" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type responseMsg []byte type errMsg struct{ error } type loadOverHttpModel struct { url string spinner spinner.Model body []byte quitting bool err error } func (m loadOverHttpModel) getSentence(prefix interface{}, done bool) string { verb := "Fetching" if done { verb = "Fetched" } sentence := styles.PkgNameStyle.Render(fmt.Sprintf("%s file from %s", verb, m.url)) return styles.PkgNameStyle.Render(fmt.Sprintf("%s %s\n", prefix, sentence)) } func initialModel(url string) loadOverHttpModel { s := spinner.New() s.Spinner = spinner.MiniDot s.Style = lipgloss.NewStyle().Foreground(styles.Honey) return loadOverHttpModel{spinner: s, url: url} } func (m loadOverHttpModel) Init() tea.Cmd { return tea.Batch(m.checkServer, m.spinner.Tick) } func (m loadOverHttpModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case responseMsg: m.body = msg m.quitting = true return m, tea.Printf(m.getSentence(styles.CheckMark, true)) case errMsg: m.err = msg m.quitting = true return m, nil default: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) if m.quitting { return m, tea.Quit } return m, cmd } } func (m loadOverHttpModel) View() string { str := m.getSentence(m.spinner.View(), false) if m.err != nil { return m.err.Error() } if len(m.body) != 0 { return "" } return str } func (m loadOverHttpModel) checkServer() tea.Msg { body, err := io.ReadHttpFile(m.url) if err != nil { return errMsg{err} } return responseMsg(body) } func LoadHttpFileFrom(url string) ([]byte, error) { model, err := tea.NewProgram(initialModel(url)).Run() if err != nil { return nil, err } return model.(loadOverHttpModel).body, nil }
package programs import ( "fmt" p "preflight/presets" "preflight/src/styles" "preflight/src/systemcheck" "sort" "strings" ) func displayPresetError(err_presets []string) { fmt.Println(styles.KoMark.Render("Unknown presets:")) for _, err := range err_presets { fmt.Println(styles.KoMark.Render(" - " + err)) } fmt.Println(styles.KoMark.Render("\n" + AvailablePresets(p.Presets))) fmt.Println(styles.PkgNameStyle.Render("Thinks that something is missing? Please open an issue on https://github.com/delni/preflight/issues/new")) fmt.Println() } func UsePresets(presetsCandidate []string, knownPresets map[string]systemcheck.SystemCheck) []systemcheck.SystemCheck { var err_presets = []string{} var systemcheck = []systemcheck.SystemCheck{} for _, presetName := range presetsCandidate { preset, exists := knownPresets[presetName] if !exists { err_presets = append(err_presets, presetName) } else { systemcheck = append(systemcheck, preset) } } if len(err_presets) > 0 { displayPresetError(err_presets) } return systemcheck } func AvailablePresets(knownPresets map[string]systemcheck.SystemCheck) string { var ( builder = strings.Builder{} keys = []string{} ) for name := range knownPresets { keys = append(keys, name) } sort.Strings(keys) builder.WriteString("Available presets are: ") builder.WriteString(strings.Join(keys, ", ")) return builder.String() }
package systemcheck import ( "fmt" "preflight/src/styles" "strings" "github.com/charmbracelet/bubbles/spinner" ) func (s SystemCheck) RenderSystemCheck(active bool, spinner spinner.Model) string { icon := styles.PkgNameStyle.Render("-") checkName := styles.PkgNameStyle.Render(s.Name) if active { icon = spinner.View() checkName = styles.CurrentPkgNameStyle.Render(s.Name) } return fmt.Sprintf("%s %s\n", icon, checkName) } func (s SystemCheck) RenderResult() string { icon := styles.CheckMark.String() name := styles.CheckMark.Render(s.Name) desc := strings.Builder{} if !s.Check { style := styles.KoMark if s.Optional { style = styles.WarningMark } icon = style.String() name = style.Render(s.Name) desc.WriteString(fmt.Sprintf("\n\t%s", s.Description)) for _, checkpoint := range s.Checkpoints { desc.WriteString(fmt.Sprintf("\n\t%s\t%s", checkpoint.Name, checkpoint.Documentation)) } } return fmt.Sprintf("%s %s%s", icon, name, styles.PkgNameStyle.Render(desc.String())) }