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
63 changes: 0 additions & 63 deletions pkg/cmd/agentskill/agentskill.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@
package agentskill

import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/terminal"
"github.com/fatih/color"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -199,45 +195,6 @@ func IsSkillInstalled(homeDir string) bool {
return false
}

// PromptInstallSkill asks the user if they want to install the agent skill
// Returns true if they want to install, false otherwise
func PromptInstallSkill(t *terminal.Terminal, homeDir string) bool {
// Skip if skill is already installed
if IsSkillInstalled(homeDir) {
return false
}

// Check if Claude Code appears to be installed
if !IsClaudeInstalled(homeDir) {
return false
}

fmt.Println()
caretType := color.New(color.FgCyan, color.Bold).SprintFunc()
fmt.Println(" ", caretType("▸"), " AI Agent Integration")
fmt.Println()
fmt.Println(" We detected an AI coding agent on your system.")
fmt.Println(" Would you like to install the Brev CLI skill?")
fmt.Println()
fmt.Println(" This enables natural language commands like:")
fmt.Println(t.Yellow(" \"Create an A100 instance for ML training\""))
fmt.Println(t.Yellow(" \"Search for GPUs with 40GB VRAM\""))
fmt.Println(t.Yellow(" \"Stop all my running instances\""))
fmt.Println()

prompt := promptui.Select{
Label: "Install agent skill",
Items: []string{"Yes, install it", "No, skip for now"},
}

idx, _, err := prompt.Run()
if err != nil {
return false
}

return idx == 0
}

// InstallSkill downloads and installs the agent skill to all install paths
func InstallSkill(t *terminal.Terminal, homeDir string, quiet bool) error {
skillDirs := GetSkillDirs(homeDir)
Expand Down Expand Up @@ -322,18 +279,6 @@ func UninstallSkill(t *terminal.Terminal, homeDir string) error {
return nil
}

// RunInstallSkillIfWanted prompts and installs if user wants it
// This is called from the login flow
func RunInstallSkillIfWanted(t *terminal.Terminal, homeDir string) {
if PromptInstallSkill(t, homeDir) {
err := InstallSkill(t, homeDir, false)
if err != nil {
// Don't fail login for skill install errors
fmt.Printf(" %s Failed to install skill: %v\n", t.Yellow("Warning:"), err)
}
}
}

// downloadAndInstallFile downloads a single file and writes it to all skill dirs.
// Returns true on success, false if the download or any write failed.
func downloadAndInstallFile(client *http.Client, baseURL, file string, skillDirs []string, t *terminal.Terminal, quiet bool) bool {
Expand Down Expand Up @@ -387,11 +332,3 @@ func downloadBytes(client *http.Client, url string) ([]byte, error) {
return body, nil
}

// PromptInstallSkillSimple is a simpler yes/no prompt for the login flow
func PromptInstallSkillSimple() bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Install agent skill? [y/N]: ")
response, _ := reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes"
}
87 changes: 87 additions & 0 deletions pkg/cmd/analytics/analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Package analytics exposes the `brev analytics` command for managing the
// user's opt-in preference for anonymous usage analytics.
package analytics

import (
"fmt"

analyticspkg "github.com/brevdev/brev-cli/pkg/analytics"
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/terminal"
"github.com/spf13/cobra"
)

// NewCmdAnalytics returns the `brev analytics` command with enable/disable/status subcommands.
func NewCmdAnalytics(t *terminal.Terminal) *cobra.Command {
cmd := &cobra.Command{
Annotations: map[string]string{"configuration": ""},
Use: "analytics",
DisableFlagsInUseLine: true,
Short: "Manage anonymous usage analytics",
Long: `Enable, disable, or check the status of anonymous usage analytics.

Analytics are opt-in. When enabled, Brev reports command usage and error
rates to help the team prioritize fixes and improvements. No command
arguments, file contents, or credentials are ever captured.`,
Comment on lines +24 to +25
Example: "brev analytics enable\nbrev analytics disable\nbrev analytics status",
RunE: func(cmd *cobra.Command, args []string) error {
return runStatus(t)
},
}
Comment on lines +16 to +30

cmd.AddCommand(&cobra.Command{
Use: "enable",
Short: "Opt in to anonymous usage analytics",
Example: "brev analytics enable",
RunE: func(cmd *cobra.Command, args []string) error {
return runSet(t, true)
},
})
cmd.AddCommand(&cobra.Command{
Use: "disable",
Short: "Opt out of anonymous usage analytics",
Example: "brev analytics disable",
RunE: func(cmd *cobra.Command, args []string) error {
return runSet(t, false)
},
})
cmd.AddCommand(&cobra.Command{
Use: "status",
Short: "Show whether anonymous usage analytics are enabled",
Example: "brev analytics status",
RunE: func(cmd *cobra.Command, args []string) error {
return runStatus(t)
},
})

return cmd
}

func runSet(t *terminal.Terminal, enabled bool) error {
if err := analyticspkg.SetAnalyticsPreference(enabled); err != nil {
return breverrors.WrapAndTrace(err)
}
analyticspkg.CaptureAnalyticsOptIn(enabled)
if enabled {
t.Vprintf("%s Analytics enabled. Thanks for helping improve Brev.\n", t.Green("✓"))
} else {
t.Vprintf("%s Analytics disabled.\n", t.Green("✓"))
}
return nil
}

func runStatus(t *terminal.Terminal) error {
enabled, asked := analyticspkg.IsAnalyticsEnabled()
switch {
case !asked:
fmt.Println("Analytics: not configured (off by default).")
t.Vprintf("Run %s to opt in.\n", t.Yellow("brev analytics enable"))
case enabled:
fmt.Println("Analytics: enabled.")
t.Vprintf("Run %s to opt out.\n", t.Yellow("brev analytics disable"))
default:
fmt.Println("Analytics: disabled.")
t.Vprintf("Run %s to opt in.\n", t.Yellow("brev analytics enable"))
}
return nil
}
15 changes: 2 additions & 13 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/brevdev/brev-cli/pkg/analytics"
"github.com/brevdev/brev-cli/pkg/auth"
"github.com/brevdev/brev-cli/pkg/cmd/agentskill"
analyticscmd "github.com/brevdev/brev-cli/pkg/cmd/analytics"
"github.com/brevdev/brev-cli/pkg/cmd/background"
"github.com/brevdev/brev-cli/pkg/cmd/clipboard"
"github.com/brevdev/brev-cli/pkg/cmd/configureenvvars"
Expand Down Expand Up @@ -153,19 +154,6 @@ func NewBrevCommand() *cobra.Command { //nolint:funlen,gocognit,gocyclo // defin

Find more information at:
https://brev.nvidia.com`,
PostRun: func(cmd *cobra.Command, args []string) {
shouldWe := hello.ShouldWeRunOnboarding(noLoginCmdStore)
if shouldWe {
user, err := loginCmdStore.GetCurrentUser()
if err != nil {
return
}
err = hello.CanWeOnboard(t, user, loginCmdStore)
if err != nil {
return
}
}
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
analytics.CaptureCommand(analytics.GetOrCreateAnalyticsID(), cmd, args)
return nil
Expand Down Expand Up @@ -330,6 +318,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
cmd.AddCommand(open.NewCmdOpen(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(ollama.NewCmdOllama(t, loginCmdStore))
cmd.AddCommand(agentskill.NewCmdAgentSkill(t, noLoginCmdStore))
cmd.AddCommand(analyticscmd.NewCmdAnalytics(t))
cmd.AddCommand(background.NewCmdBackground(t, loginCmdStore))
cmd.AddCommand(status.NewCmdStatus(t, loginCmdStore))
cmd.AddCommand(sshkeys.NewCmdSSHKeys(t, loginCmdStore))
Expand Down
89 changes: 0 additions & 89 deletions pkg/cmd/hello/onboarding_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import (
"path/filepath"
"strings"

"github.com/brevdev/brev-cli/pkg/entity"
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/files"
"github.com/brevdev/brev-cli/pkg/terminal"
"github.com/brevdev/brev-cli/pkg/util"
"github.com/spf13/afero"
)

Expand All @@ -22,92 +19,6 @@ func GetFirstName(name string) string {
return name
}

// The LS step should get the GetOnboardingData from the user
// and use that to check the step "FinishedOnboarding"
// Either way. It should set it to True
func ShouldWeRunOnboardingLSStep(s HelloStore) bool {
user, err := s.GetCurrentUser()
if err != nil {
return false
}

ob, err := user.GetOnboardingData()
if err != nil {
return false
}

if ob.FinishedOnboarding {
return false
} else {
// set the value and return true
newOnboardingStatus := make(map[string]interface{})
newOnboardingStatus["finishedOnboarding"] = true

user, err = s.UpdateUser(user.ID, &entity.UpdateUser{
// username, name, and email are required fields, but we only care about onboarding status
Username: user.Username,
Name: user.Name,
Email: user.Email,
OnboardingData: util.MapAppend(user.OnboardingData, newOnboardingStatus),
})
if err != nil {
// TODO: what should we do here?
return true
}

return true
}
}

func ShouldWeRunOnboarding(s HelloStore) bool {
workspaceID, err := s.GetCurrentWorkspaceID()
if err != nil {
return false
}
if workspaceID != "" {
return false
}

oo, err := GetOnboardingObject()
if err != nil {
return true
}
if oo.Step == 0 && !oo.HasRunBrevOpen && !oo.HasRunBrevShell {
return true
} else {
return false
}
}

func CanWeOnboard(t *terminal.Terminal, user *entity.User, store HelloStore) error {
s := t.Green("\n\nHi " + GetFirstName(user.Name) + "! Looks like it's your first time using Brev!\n")

TypeItToMeUnskippable(s)

res := terminal.PromptSelectInput(terminal.PromptSelectContent{
Label: "Want a quick tour?",
ErrorMsg: "Please pick yes or no",
Items: []string{"Yes!", "No, I'll read docs later"},
})
if res == "Yes!" {
err := RunOnboarding(t, user, store)
if err != nil {
return breverrors.WrapAndTrace(err)
}
} else {
_ = SetOnboardingObject(OnboardingObject{
Step: 1,
HasRunBrevOpen: true,
HasRunBrevShell: true,
})

_ = SkippedOnboarding(user, store)

t.Vprintf("\nOkay, you can always read the docs at %s\n\n", t.Yellow("https://brev.dev/docs"))
}
return nil
}

func GetOnboardingFilePath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
Expand Down
Loading
Loading