@@ -0,0 +1,182 @@
10+ "charm.land/lipgloss/v2"
11+ "github.com/charmbracelet/crush/internal/client"
12+ "github.com/charmbracelet/crush/internal/config"
13+ "github.com/charmbracelet/x/ansi"
14+ "github.com/spf13/cobra"
18+ logoutHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
19+ logoutItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
20+ logoutPromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
23+var logoutCmd = &cobra.Command{
24+ Aliases: []string{"signout"},
25+ Use: "logout [platform]",
26+ Short: "Logout Crush from a platform",
27+ Long: `Logout Crush from a specified platform, removing stored credentials.
28+The platform should be provided as an argument.
29+If no argument is given, a list of logged-in platforms will be shown.
30+Available platforms are: hyper, copilot.`,
32+# Sign out from Charm Hyper
35+# Sign out from GitHub Copilot
38+ ValidArgs: []cobra.Completion{
44+ Args: cobra.MaximumNArgs(1),
45+ RunE: func(cmd *cobra.Command, args []string) error {
46+ c, ws, cleanup, err := connectToServer(cmd)
52+ progressEnabled := ws.Config.Options.Progress == nil || *ws.Config.Options.Progress
53+ if progressEnabled && supportsProgressBar() {
54+ _, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
55+ defer func() { _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar) }()
60+ provider, err = pickLoggedInProvider(c, ws.ID)
71+ force, _ := cmd.Flags().GetBool("force")
73+ fmt.Print(logoutPromptStyle.Render(fmt.Sprintf("Are you sure you want to logout %s? (y/N) ", provider)))
75+ _, err := fmt.Scanln(&response)
76+ if err != nil || (response != "y" && response != "Y" && response != "yes" && response != "Yes" && response != "YES") {
77+ fmt.Println(logoutHeaderStyle.Render("Logout cancelled."))
84+ return logoutHyper(c, ws.ID)
85+ case "copilot", "github", "github-copilot":
86+ return logoutCopilot(c, ws.ID)
88+ return fmt.Errorf("unknown platform: %s", provider)
93+func logoutHyper(c *client.Client, wsID string) error {
94+ ctx := getLogoutContext()
97+ c.RemoveConfigField(ctx, wsID, config.ScopeGlobal, "providers.hyper.api_key"),
98+ c.RemoveConfigField(ctx, wsID, config.ScopeGlobal, "providers.hyper.oauth"),
103+ fmt.Println(logoutHeaderStyle.Render("Successfully logged out of Hyper."))
107+func logoutCopilot(c *client.Client, wsID string) error {
108+ ctx := getLogoutContext()
111+ c.RemoveConfigField(ctx, wsID, config.ScopeGlobal, "providers.copilot.api_key"),
112+ c.RemoveConfigField(ctx, wsID, config.ScopeGlobal, "providers.copilot.oauth"),
117+ fmt.Println(logoutHeaderStyle.Render("Successfully logged out of GitHub Copilot."))
121+func pickLoggedInProvider(c *client.Client, wsID string) (string, error) {
122+ ctx := getLogoutContext()
124+ cfg, err := c.GetConfig(ctx, wsID)
126+ return "", fmt.Errorf("failed to get config: %w", err)
129+ type loggedInProvider struct {
134+ var loggedIn []loggedInProvider
135+ for p := range cfg.Providers.Seq() {
136+ if p.OAuthToken != nil || p.APIKey != "" {
141+ loggedIn = append(loggedIn, loggedInProvider{id: p.ID, name: name})
145+ if len(loggedIn) == 0 {
146+ fmt.Println(logoutPromptStyle.Render("You are not logged in to any platform."))
150+ if len(loggedIn) == 1 {
151+ return loggedIn[0].id, nil
154+ fmt.Println(logoutHeaderStyle.Render("Logged-in platforms:"))
155+ for i, p := range loggedIn {
156+ fmt.Println(logoutItemStyle.Render(fmt.Sprintf(" %d. %s", i+1, p.name)))
158+ fmt.Print(logoutPromptStyle.Render(fmt.Sprintf("Select a platform to logout (1-%d): ", len(loggedIn))))
161+ _, err = fmt.Scanln(&choice)
162+ if err != nil || choice < 1 || choice > len(loggedIn) {
163+ fmt.Println(logoutHeaderStyle.Render("Logout cancelled."))
167+ return loggedIn[choice-1].id, nil
171+ logoutCmd.Flags().BoolP("force", "f", false, "Skip logout confirmation prompt")
174+func getLogoutContext() context.Context {
175+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)