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.
This commit is contained in:
parent
6991a1727c
commit
ee459c1f8e
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Binaries
|
||||
xapi
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Go vendor directory
|
||||
vendor/
|
||||
|
||||
# IDE directories
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Config files with secrets (user's local config)
|
||||
# Note: We don't ignore the entire .config directory
|
||||
# as users might want to share example configs
|
||||
206
cmd/login.go
Normal file
206
cmd/login.go
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
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")
|
||||
}
|
||||
51
cmd/root.go
Normal file
51
cmd/root.go
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "xapi",
|
||||
Short: "The quota-intelligent X API search tool",
|
||||
Long: `xapi is a command-line tool for the X API that respects your quota limits.
|
||||
|
||||
Preview searches before executing them. Know exactly what you're spending.
|
||||
Built for developers on Free and Basic tiers who need to maximize every API call.
|
||||
|
||||
by MAX THE WEB | git.maxtheweb.com/maxtheweb/xapi-cli`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.xapi-cli.yaml)")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
// We'll add global flags here later
|
||||
}
|
||||
|
||||
|
||||
264
cmd/status.go
Normal file
264
cmd/status.go
Normal file
@ -0,0 +1,264 @@
|
||||
/*
|
||||
Copyright © 2025 MAX THE WEB
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Config structure
|
||||
type Config struct {
|
||||
BearerToken string `toml:"bearer_token"`
|
||||
API struct {
|
||||
BaseURL string `toml:"base_url"`
|
||||
Timeout int `toml:"timeout"`
|
||||
} `toml:"api"`
|
||||
Limits struct {
|
||||
MonthlyReads int `toml:"monthly_reads"`
|
||||
MonthlyPosts int `toml:"monthly_posts"`
|
||||
} `toml:"limits"`
|
||||
}
|
||||
|
||||
// IntOrString handles JSON fields that can be either int or string
|
||||
type IntOrString int
|
||||
|
||||
func (i *IntOrString) UnmarshalJSON(data []byte) error {
|
||||
// Try to unmarshal as int first
|
||||
var intVal int
|
||||
if err := json.Unmarshal(data, &intVal); err == nil {
|
||||
*i = IntOrString(intVal)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If that fails, try string
|
||||
var strVal string
|
||||
if err := json.Unmarshal(data, &strVal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert string to int
|
||||
parsed, err := strconv.Atoi(strVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = IntOrString(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UsageResponse from X API
|
||||
type UsageResponse struct {
|
||||
Data struct {
|
||||
ProjectCap IntOrString `json:"project_cap"` // Total monthly quota
|
||||
ProjectUsage IntOrString `json:"project_usage"` // Already consumed
|
||||
CapResetDay int `json:"cap_reset_day"` // Days until reset
|
||||
ProjectID string `json:"project_id"` // Project identifier
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
var statusDays int
|
||||
|
||||
// statusCmd represents the status command
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check X API quota and rate limits",
|
||||
Long: `Shows your current X API usage status including:
|
||||
- Monthly quota usage (total API operations)
|
||||
- Time until quota reset
|
||||
- Current rate limit window status`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runStatus()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
statusCmd.Flags().IntVarP(&statusDays, "days", "d", 7, "Number of days of usage history (1-90)")
|
||||
}
|
||||
|
||||
func runStatus() {
|
||||
// Load configuration
|
||||
config, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
fmt.Println("Run 'xapi login' to configure your credentials")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if token is configured
|
||||
if config.BearerToken == "" || config.BearerToken == "YOUR_BEARER_TOKEN_HERE" {
|
||||
fmt.Fprintf(os.Stderr, "Bearer token not configured\n")
|
||||
fmt.Println("Run 'xapi login' to configure your credentials")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create HTTP client
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(config.API.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
// Validate days parameter
|
||||
if statusDays < 1 || statusDays > 90 {
|
||||
fmt.Fprintf(os.Stderr, "Error: --days must be between 1 and 90\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Build API URL with days parameter
|
||||
apiURL := fmt.Sprintf("%s/usage/tweets?days=%d", config.API.BaseURL, statusDays)
|
||||
|
||||
// Make API request to usage endpoint
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
|
||||
|
||||
// Execute request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error making API request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Fprintf(os.Stderr, "API error (status %d): %s\n", resp.StatusCode, string(body))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var usage UsageResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&usage); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Display status
|
||||
displayStatus(usage, resp.Header, config)
|
||||
}
|
||||
|
||||
func loadConfig() (*Config, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configFile := filepath.Join(homeDir, ".config", "xapi", "config.toml")
|
||||
|
||||
var config Config
|
||||
if _, err := toml.DecodeFile(configFile, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set defaults if not specified
|
||||
if config.API.BaseURL == "" {
|
||||
config.API.BaseURL = "https://api.x.com/2"
|
||||
}
|
||||
if config.API.Timeout == 0 {
|
||||
config.API.Timeout = 30
|
||||
}
|
||||
if config.Limits.MonthlyReads == 0 {
|
||||
config.Limits.MonthlyReads = 100
|
||||
}
|
||||
if config.Limits.MonthlyPosts == 0 {
|
||||
config.Limits.MonthlyPosts = 500
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func displayStatus(usage UsageResponse, headers http.Header, config *Config) {
|
||||
fmt.Println("\n📊 MONTHLY QUOTA STATUS")
|
||||
fmt.Println("═══════════════════════")
|
||||
|
||||
// Calculate usage (convert IntOrString to int)
|
||||
cap := int(usage.Data.ProjectCap)
|
||||
used := int(usage.Data.ProjectUsage)
|
||||
remaining := cap - used
|
||||
percentage := float64(used) / float64(cap) * 100
|
||||
|
||||
// Display quota (clarify these are total API operations)
|
||||
fmt.Printf("├─ Type: %s Tier (%d total operations/month)\n", getAccessTier(cap), cap)
|
||||
fmt.Printf("├─ Used: %d / %d operations (%.1f%%)\n", used, cap, percentage)
|
||||
fmt.Printf("├─ Remaining: %d operations\n", remaining)
|
||||
fmt.Printf("└─ Resets in: %d days\n", usage.Data.CapResetDay)
|
||||
|
||||
// Display rate limit headers
|
||||
fmt.Println("\n⏱️ 15-MIN RATE WINDOW")
|
||||
fmt.Println("═════════════════════")
|
||||
|
||||
limit := headers.Get("x-rate-limit-limit")
|
||||
remainingRate := headers.Get("x-rate-limit-remaining")
|
||||
reset := headers.Get("x-rate-limit-reset")
|
||||
|
||||
if limit != "" {
|
||||
fmt.Printf("├─ Requests: %s / %s available\n", remainingRate, limit)
|
||||
}
|
||||
|
||||
if reset != "" {
|
||||
resetTime, _ := strconv.ParseInt(reset, 10, 64)
|
||||
resetAt := time.Unix(resetTime, 0)
|
||||
timeUntil := time.Until(resetAt)
|
||||
fmt.Printf("└─ Resets in: %s\n", formatDuration(timeUntil))
|
||||
}
|
||||
|
||||
// Display recommendations
|
||||
fmt.Println("\n💡 RECOMMENDATIONS")
|
||||
fmt.Println("═════════════════")
|
||||
|
||||
if remaining > 0 {
|
||||
daysLeft := usage.Data.CapResetDay
|
||||
if daysLeft > 0 {
|
||||
dailyBudget := float64(remaining) / float64(daysLeft)
|
||||
fmt.Printf("├─ You have %d operations left for %d days\n", remaining, daysLeft)
|
||||
fmt.Printf("├─ That's ~%.1f operations per day\n", dailyBudget)
|
||||
fmt.Printf("└─ Each search query will consume 1 operation\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("└─ ⚠️ No operations remaining! Consider upgrading your plan.")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// getAccessTier determines the access tier based on monthly cap
|
||||
func getAccessTier(cap int) string {
|
||||
switch {
|
||||
case cap <= 100:
|
||||
return "Free"
|
||||
case cap <= 10000:
|
||||
return "Basic"
|
||||
case cap <= 1000000:
|
||||
return "Pro"
|
||||
default:
|
||||
return "Enterprise"
|
||||
}
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < 0 {
|
||||
return "0 seconds"
|
||||
}
|
||||
|
||||
minutes := int(d.Minutes())
|
||||
seconds := int(d.Seconds()) % 60
|
||||
|
||||
if minutes > 0 {
|
||||
return fmt.Sprintf("%d minutes %d seconds", minutes, seconds)
|
||||
}
|
||||
return fmt.Sprintf("%d seconds", seconds)
|
||||
}
|
||||
12
go.mod
Normal file
12
go.mod
Normal file
@ -0,0 +1,12 @@
|
||||
module git.maxtheweb.com/maxtheweb/xapi-cli
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/cobra v1.10.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
)
|
||||
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Loading…
Reference in New Issue
Block a user