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()))
}