Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This is the official command line tool for [Boot.dev](https://www.boot.dev/). It
- [1. Install Go](#1-install-go)
- [2. Install the Boot.dev CLI](#2-install-the-bootdev-cli)
- [3. Login to the CLI](#3-login-to-the-cli)
- [Running and Submitting Lessons](#running-and-submitting-lessons)
- [Configuration](#configuration)
- [Base URL for HTTP tests](#base-url-for-http-tests)
- [CLI colors](#cli-colors)
Expand Down Expand Up @@ -114,6 +115,34 @@ fish_add_path $HOME/go/bin

Run `bootdev login` to authenticate with your Boot.dev account. After authenticating, you're ready to go!

## Running and Submitting Lessons

When you reach a CLI lesson, the Boot.dev CLI can detect it from your course progress. Run the lesson's checks without submitting it with:

```sh
bootdev run
```

Submit the lesson for completion with:

```sh
bootdev submit
```

The lesson UUID is optional. You can still provide it explicitly to either command:

```sh
bootdev run UUID
bootdev submit UUID
```

To run the checks and submit the lesson in one command, use the `--submit` (`-s`) flag. The UUID is optional here as well:

```sh
bootdev run -s
bootdev run -s UUID
```

## Configuration

The Boot.dev CLI offers a couple of configuration options that are stored in a config file (default is `~/.bootdev.yaml`, or `$XDG_CONFIG_HOME/bootdev/config.yaml` if `XDG_CONFIG_HOME` is set).
Expand Down Expand Up @@ -151,7 +180,7 @@ The CLI text output is rendered with extra colors: green (e.g., success messages
- To customize these colors, run:

```sh
bootdev config colors --red VALUE --green VALUE --gray VALUE
bootdev config colors --red VALUE --green VALUE --gray VALUE --magenta VALUE --yellow VALUE
```

_You can use an [ANSI color code](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) or a hex string as the `VALUE`._
Expand Down
1 change: 1 addition & 0 deletions cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var defaultColors = map[string]string{
"red": "1",
"green": "2",
"magenta": "5",
"yellow": "#f6bd45",
}

// configureColorsCmd represents the `configure colors` command for changing
Expand Down
2 changes: 1 addition & 1 deletion cmd/localtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func localTestHandler(cmd *cobra.Command, args []string) error {
fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n")
}

send, finish := render.StartRenderer(true, verboseOutput)
send, finish := render.StartRenderer(true, verboseOutput, false)
submissionEvent := api.LessonSubmissionEvent{}
defer func() {
finish(submissionEvent)
Expand Down
2 changes: 1 addition & 1 deletion cmd/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func submissionHandler(cmd *cobra.Command, args []string) error {
fmt.Printf("You can reset to the default with `bootdev config base_url --reset`\n\n")
}

send, finish := render.StartRenderer(isSubmit, verboseOutput)
send, finish := render.StartRenderer(isSubmit, verboseOutput, isSubmit && len(args) > 0)
finalEvent := api.LessonSubmissionEvent{}
var debugPath string
var debugWriteErr error
Expand Down
21 changes: 11 additions & 10 deletions render/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ type stepModel struct {
}

type rootModel struct {
steps []stepModel
spinner spinner.Model
result api.VerificationResultSlug
failure *api.StructuredErrCLI
xpReward int
xpBreakdown []api.XPBreakdownItem
isSubmit bool
verbose bool
finalized bool
clear bool
steps []stepModel
spinner spinner.Model
result api.VerificationResultSlug
failure *api.StructuredErrCLI
xpReward int
xpBreakdown []api.XPBreakdownItem
isSubmit bool
verbose bool
showOmitLessonIDTip bool
finalized bool
clear bool
}

func initModel(isSubmit bool, verbose bool) rootModel {
Expand Down
8 changes: 6 additions & 2 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
green lipgloss.Style
red lipgloss.Style
magenta lipgloss.Style
yellow lipgloss.Style
gray lipgloss.Style
white lipgloss.Style
borderBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
Expand All @@ -25,6 +26,7 @@ func (m rootModel) Init() tea.Cmd {
green = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.green")))
red = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.red")))
magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.magenta")))
yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.yellow")))
gray = lipgloss.NewStyle().Foreground(lipgloss.Color(viper.GetString("color.gray")))
white = lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
return m.spinner.Tick
Expand Down Expand Up @@ -102,8 +104,10 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

func StartRenderer(isSubmit bool, verbose bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) {
p := tea.NewProgram(initModel(isSubmit, verbose), tea.WithoutSignalHandler())
func StartRenderer(isSubmit bool, verbose bool, showOmitLessonIDTip bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) {
m := initModel(isSubmit, verbose)
m.showOmitLessonIDTip = showOmitLessonIDTip
p := tea.NewProgram(m, tea.WithoutSignalHandler())
done := make(chan struct{})

go func() {
Expand Down
7 changes: 7 additions & 0 deletions render/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ func (m rootModel) View() string {
str.WriteString(green.Render("Return to your browser to continue with the next lesson."))
str.WriteByte('\n')
str.WriteByte('\n')
if m.showOmitLessonIDTip {
str.WriteString(yellow.Render("Tip:"))
str.WriteString(" When you reach your next CLI lesson, you can skip copying its ID:\n ")
str.WriteString(yellow.Render("bootdev run"))
str.WriteByte('\n')
str.WriteByte('\n')
}
} else if m.result == api.VerificationResultSlugSystemError {
str.WriteByte('\n')
str.WriteByte('\n')
Expand Down
45 changes: 45 additions & 0 deletions render/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,51 @@ func TestSystemErrorViewDoesNotShowStepsAsPassed(t *testing.T) {
}
}

func TestOmitLessonIDTipOnlyAppearsAfterExplicitSuccessfulSubmission(t *testing.T) {
tests := []struct {
name string
isSubmit bool
showTip bool
result api.VerificationResultSlug
wantTip bool
}{
{name: "explicit successful submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSuccess, wantTip: true},
{name: "UUID-less successful submission", isSubmit: true, result: api.VerificationResultSlugSuccess},
{name: "ordinary run with UUID", showTip: true, result: api.VerificationResultSlugSuccess},
{name: "failed submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugFailure},
{name: "noop submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugNoop},
{name: "canceled or pre-submission error", isSubmit: true, showTip: true},
{name: "system-error submission", isSubmit: true, showTip: true, result: api.VerificationResultSlugSystemError},
}

const tip = "Tip: When you reach your next CLI lesson, you can skip copying its ID:\n bootdev run"
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := initModel(tt.isSubmit, false)
m.finalized = true
m.showOmitLessonIDTip = tt.showTip
m.result = tt.result
if tt.result == api.VerificationResultSlugFailure || tt.result == api.VerificationResultSlugNoop {
m.failure = &api.StructuredErrCLI{FailedStepIndex: 0, ErrorMessage: "failed"}
}

view := m.View()
if got := strings.Contains(view, tip); got != tt.wantTip {
t.Fatalf("tip present = %v, want %v\n%s", got, tt.wantTip, view)
}
if tt.wantTip {
if strings.Contains(view, "detect") {
t.Errorf("tip should not explain automatic detection\n%s", view)
}
browserInstruction := "Return to your browser to continue with the next lesson."
if !strings.Contains(view, browserInstruction) || strings.Index(view, browserInstruction) > strings.Index(view, tip) {
t.Fatalf("browser instruction must appear before tip\n%s", view)
}
}
})
}
}

func TestStartStepFallsBackToTechnicalDescription(t *testing.T) {
m := initModel(true, false)
updated, _ := m.Update(messages.StartStepMsg{CMD: "go test ./..."})
Expand Down