/* Copyright © 2025 MAX THE WEB */ package cmd import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "github.com/spf13/cobra" "golang.org/x/term" ) var loginTokenFlag bool // loginCmd represents the login command var loginCmd = &cobra.Command{ Use: "login", Short: "Configure X API credentials", Long: `Opens your editor to configure X API credentials. The configuration file will be saved to ~/.config/xapi/config.toml You'll need your X API Bearer token from developer.x.com Examples: xapi login # Opens editor to configure xapi login --token # Prompts for token (secure, no history)`, Run: func(cmd *cobra.Command, args []string) { runLogin() }, } func init() { rootCmd.AddCommand(loginCmd) loginCmd.Flags().BoolVarP(&loginTokenFlag, "token", "t", false, "Prompt for Bearer token (secure input)") } // findEditor searches for an available text editor func findEditor() string { // First, check if EDITOR is set if editor := os.Getenv("EDITOR"); editor != "" { // Verify it exists if _, err := exec.LookPath(editor); err == nil { return editor } } // Check for VISUAL (some systems use this) if visual := os.Getenv("VISUAL"); visual != "" { if _, err := exec.LookPath(visual); err == nil { return visual } } // Try common editors in order of preference editors := []string{ "nvim", // Neovim (power users) "vim", // Vim (widely available) "vi", // Vi (almost always there) "nano", // Nano (beginner-friendly) "emacs", // Emacs (for the enlightened) "micro", // Micro (modern terminal editor) "code", // VS Code (if they have it in terminal) "subl", // Sublime Text "gedit", // GNOME editor "kate", // KDE editor } for _, editor := range editors { if _, err := exec.LookPath(editor); err == nil { return editor } } return "" // No editor found } func runLogin() { // Get config directory homeDir, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err) os.Exit(1) } configDir := filepath.Join(homeDir, ".config", "xapi") configFile := filepath.Join(configDir, "config.toml") // Create config directory if it doesn't exist if err := os.MkdirAll(configDir, 0755); err != nil { fmt.Fprintf(os.Stderr, "Error creating config directory: %v\n", err) os.Exit(1) } // Handle secure token input if loginTokenFlag { fmt.Print("Enter your X API Bearer token (input hidden): ") // Read password securely (hidden input) tokenBytes, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { // Fallback to regular input if terminal doesn't support hidden input fmt.Print("\n(Warning: Using visible input) Enter token: ") reader := bufio.NewReader(os.Stdin) tokenInput, _ := reader.ReadString('\n') tokenInput = strings.TrimSpace(tokenInput) if tokenInput == "" { fmt.Fprintf(os.Stderr, "\nError: Token cannot be empty\n") os.Exit(1) } tokenBytes = []byte(tokenInput) } fmt.Println() // New line after hidden input token := strings.TrimSpace(string(tokenBytes)) if token == "" { fmt.Fprintf(os.Stderr, "Error: Token cannot be empty\n") os.Exit(1) } // Create config with provided token config := fmt.Sprintf(`# X API Configuration # Get your Bearer token from: https://developer.x.com # # SECURITY NOTE: Never share this file or commit it to version control! # Your X API Bearer Token bearer_token = "%s" # API Settings [api] base_url = "https://api.x.com/2" timeout = 30 # Free Tier Limits (adjust based on your plan) [limits] monthly_reads = 100 monthly_posts = 500 `, token) if err := os.WriteFile(configFile, []byte(config), 0600); err != nil { fmt.Fprintf(os.Stderr, "Error writing config file: %v\n", err) os.Exit(1) } fmt.Println("✅ Token saved to:", configFile) fmt.Println("Run 'xapi status' to verify your connection") return } // Check if config file exists, if not create template if _, err := os.Stat(configFile); os.IsNotExist(err) { template := `# X API Configuration # Get your Bearer token from: https://developer.x.com # # SECURITY NOTE: Never share this file or commit it to version control! # Consider using 'xapi login --token' for secure input without shell history. # Your X API Bearer Token bearer_token = "YOUR_BEARER_TOKEN_HERE" # API Settings [api] base_url = "https://api.x.com/2" timeout = 30 # Free Tier Limits (adjust based on your plan) [limits] monthly_reads = 100 monthly_posts = 500 ` if err := os.WriteFile(configFile, []byte(template), 0600); err != nil { fmt.Fprintf(os.Stderr, "Error creating config file: %v\n", err) os.Exit(1) } } // Get editor using smart detection editor := findEditor() if editor == "" { fmt.Fprintf(os.Stderr, "Error: No text editor found!\n\n") fmt.Println("Please install one of: nvim, vim, vi, nano, emacs") fmt.Println("Or set your preferred editor: export EDITOR=nvim") os.Exit(1) } // Open editor fmt.Printf("Opening %s with %s...\n", configFile, editor) cmd := exec.Command(editor, configFile) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error opening editor: %v\n", err) os.Exit(1) } fmt.Println("\n✅ Configuration saved to:", configFile) fmt.Println("Run 'xapi status' to verify your connection") }