xapi-cli/cmd/login.go
Soldier ee459c1f8e feat: Initial MVP release with authentication and quota tracking
- Add secure login command with smart editor detection
- Implement Bearer token authentication with hidden input option
- Add status command showing monthly quota and rate limits
- Support for Free/Basic/Pro tier detection
- Smart editor detection (nvim, vim, nano, etc.)
- Secure token storage in ~/.config/xapi/config.toml
- Add comprehensive README and documentation
- Add .gitignore for binaries and IDE files

Features:
- xapi login: Configure credentials securely
- xapi login --token: Hidden input mode (no bash history)
- xapi status: Check quota usage and recommendations
- xapi status --days N: View N-day usage history

Security:
- Token input without shell history exposure
- Config file with 0600 permissions
- HTTPS-only API communication
- Clear security warnings in config

Built with Cobra CLI framework for a professional CLI experience.
Next: Search command with dry-run mode to preview before burning quota.
2025-11-13 16:19:30 +00:00

206 lines
5.3 KiB
Go

/*
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")
}