github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/tutorial/tutorial.go (about)

     1  package tutorial
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  
     7  	"github.com/ActiveState/cli/internal/analytics"
     8  	"github.com/skratchdot/open-golang/open"
     9  
    10  	anaConsts "github.com/ActiveState/cli/internal/analytics/constants"
    11  	"github.com/ActiveState/cli/internal/constants"
    12  	"github.com/ActiveState/cli/internal/fileutils"
    13  	"github.com/ActiveState/cli/internal/language"
    14  	"github.com/ActiveState/cli/internal/locale"
    15  	"github.com/ActiveState/cli/internal/osutils/user"
    16  	"github.com/ActiveState/cli/internal/output"
    17  	"github.com/ActiveState/cli/internal/primer"
    18  	"github.com/ActiveState/cli/internal/prompt"
    19  	"github.com/ActiveState/cli/internal/runbits"
    20  	"github.com/ActiveState/cli/pkg/platform/api"
    21  	"github.com/ActiveState/cli/pkg/platform/authentication"
    22  )
    23  
    24  type Tutorial struct {
    25  	outputer  output.Outputer
    26  	auth      *authentication.Auth
    27  	prompt    prompt.Prompter
    28  	analytics analytics.Dispatcher
    29  }
    30  
    31  type primeable interface {
    32  	primer.Outputer
    33  	primer.Prompter
    34  	primer.Auther
    35  	primer.Configurer
    36  	primer.Analyticer
    37  }
    38  
    39  func New(primer primeable) *Tutorial {
    40  	return &Tutorial{primer.Output(), primer.Auth(), primer.Prompt(), primer.Analytics()}
    41  }
    42  
    43  type NewProjectParams struct {
    44  	SkipIntro bool
    45  	Language  language.Language
    46  }
    47  
    48  func (t *Tutorial) RunNewProject(params NewProjectParams) error {
    49  	t.analytics.EventWithLabel(anaConsts.CatTutorial, "run", fmt.Sprintf("skipIntro=%v,language=%v", params.SkipIntro, params.Language.String()))
    50  
    51  	// Print intro
    52  	if !params.SkipIntro {
    53  		t.outputer.Print(locale.Tt("tutorial_newproject_intro"))
    54  	}
    55  
    56  	// Prompt for authentication
    57  	if !t.auth.Authenticated() {
    58  		if err := t.authFlow(); err != nil {
    59  			return err
    60  		}
    61  	}
    62  
    63  	// Prompt for language
    64  	lang := params.Language
    65  	if lang == language.Unset {
    66  		choice, err := t.prompt.Select(
    67  			"",
    68  			locale.Tl("tutorial_language", "What language would you like to use for your new virtual environment?"),
    69  			[]string{language.Perl.Text(), language.Python3.Text(), language.Python2.Text()},
    70  			new(string),
    71  		)
    72  		if err != nil {
    73  			return locale.WrapInputError(err, "err_tutorial_prompt_language", "Invalid response received.")
    74  		}
    75  		lang = language.MakeByText(choice)
    76  		if lang == language.Unknown || lang == language.Unset {
    77  			return locale.NewError("err_tutorial_language_unknown", "Invalid language selected: {{.V0}}.", choice)
    78  		}
    79  		t.analytics.EventWithLabel(anaConsts.CatTutorial, "choose-language", lang.String())
    80  	}
    81  
    82  	// Prompt for project name
    83  	defProjectInput := lang.Text()
    84  	name, err := t.prompt.Input("", locale.Tl("tutorial_prompt_projectname", "What do you want to name your project?"), &defProjectInput)
    85  	if err != nil {
    86  		return locale.WrapInputError(err, "err_tutorial_prompt_projectname", "Invalid response received.")
    87  	}
    88  
    89  	// Prompt for project dir
    90  	homeDir, _ := user.HomeDir()
    91  	dir, err := t.prompt.Input("", locale.Tl(
    92  		"tutorial_prompt_projectdir",
    93  		"Where would you like your project directory to be mapped? This is usually the root of your repository, or the place where you have your project dotfiles."), &homeDir)
    94  	if err != nil {
    95  		return locale.WrapInputError(err, "err_tutorial_prompt_projectdir", "Invalid response received.")
    96  	}
    97  
    98  	// Create dir and switch to it
    99  	if err := fileutils.MkdirUnlessExists(dir); err != nil {
   100  		return locale.WrapExternalError(err, "err_tutorial_mkdir", "Could not create directory: {{.V0}}.", dir)
   101  	}
   102  	if err := os.Chdir(dir); err != nil {
   103  		return locale.WrapExternalError(err, "err_tutorial_chdir", "Could not change directory to: {{.V0}}", dir)
   104  	}
   105  
   106  	// Run state init
   107  	if err := runbits.Invoke(t.outputer, "init", t.auth.WhoAmI()+"/"+name, lang.String(), "--path", dir); err != nil {
   108  		return locale.WrapExternalError(err, "err_tutorial_state_init", "Could not initialize project.")
   109  	}
   110  
   111  	// Run state push
   112  	if err := runbits.Invoke(t.outputer, "push"); err != nil {
   113  		return locale.WrapExternalError(err, "err_tutorial_state_push", "Could not push project to ActiveState Platform, try manually running '[ACTIONABLE]state push[/RESET]' from your project directory at {{.V0}}.", dir)
   114  	}
   115  
   116  	// Print outro
   117  	t.outputer.Print(locale.Tt(
   118  		"tutorial_newproject_outro", map[string]interface{}{
   119  			"Dir":  dir,
   120  			"URL":  api.GetPlatformURL(fmt.Sprintf("%s/%s", t.auth.WhoAmI(), name)),
   121  			"Docs": constants.DocumentationURL,
   122  		}))
   123  
   124  	return nil
   125  }
   126  
   127  // authFlow is invoked when the user is not authenticated, it will prompt for sign in or sign up
   128  func (t *Tutorial) authFlow() error {
   129  	t.analytics.Event(anaConsts.CatTutorial, "authentication-flow")
   130  
   131  	// Sign in / Sign up choices
   132  	signIn := locale.Tl("tutorial_signin", "Sign In")
   133  	signUpCLI := locale.Tl("tutorial_createcli", "Create Account via Command Line")
   134  	signUpBrowser := locale.Tl("tutorial_createbrowser", "Create Account via Browser")
   135  	choices := []string{signIn, signUpCLI, signUpBrowser}
   136  
   137  	// Prompt for auth
   138  	choice, err := t.prompt.Select(
   139  		"",
   140  		locale.Tl("tutorial_need_account", "In order to create a virtual environment you must have an ActiveState Platform account"),
   141  		choices,
   142  		&signIn,
   143  	)
   144  	if err != nil {
   145  		return locale.WrapInputError(err, "err_tutorial_prompt_account", "Invalid response received.")
   146  	}
   147  
   148  	// Evaluate user selection
   149  	switch choice {
   150  	case signIn:
   151  		t.analytics.EventWithLabel(anaConsts.CatTutorial, "authentication-action", "sign-in")
   152  		if err := runbits.Invoke(t.outputer, "auth"); err != nil {
   153  			return locale.WrapExternalError(err, "err_tutorial_signin", "Sign in failed. You could try manually signing in by running '[ACTIONABLE]state auth[/RESET]'.")
   154  		}
   155  	case signUpCLI:
   156  		t.analytics.EventWithLabel(anaConsts.CatTutorial, "authentication-action", "sign-up")
   157  		if err := runbits.Invoke(t.outputer, "auth", "signup"); err != nil {
   158  			return locale.WrapExternalError(err, "err_tutorial_signup", "Sign up failed. You could try manually signing up by running '[ACTIONABLE]state auth signup[/RESET]'.")
   159  		}
   160  	case signUpBrowser:
   161  		t.analytics.EventWithLabel(anaConsts.CatTutorial, "authentication-action", "sign-up-browser")
   162  		signupURL := api.GetPlatformURL(constants.PlatformSignupPath).String()
   163  		err := open.Run(signupURL)
   164  		if err != nil {
   165  			return locale.WrapExternalError(err, "err_tutorial_browser", "Could not open browser, please manually navigate to {{.V0}}.", signupURL)
   166  		}
   167  		t.outputer.Notice(locale.Tl("tutorial_signing_ready", "[NOTICE]Please sign in once you have finished signing up via your browser.[/RESET]"))
   168  		if err := runbits.Invoke(t.outputer, "auth"); err != nil {
   169  			return locale.WrapExternalError(err, "err_tutorial_signin", "Sign in failed. You could try manually signing in by running '[ACTIONABLE]state auth[/RESET]'.")
   170  		}
   171  	}
   172  
   173  	if err := t.auth.Authenticate(); err != nil {
   174  		return locale.WrapError(err, "err_tutorial_auth", "Could not authenticate after invoking `state auth ..`.")
   175  	}
   176  
   177  	t.analytics.Event(anaConsts.CatTutorial, "authentication-flow-complete")
   178  
   179  	return nil
   180  }