- 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.
206 lines
5.3 KiB
Go
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")
|
|
} |