| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- package cmd
- import (
- "bufio"
- "context"
- "fmt"
- "net/url"
- "os"
- "strings"
- "gogs.dmsc.dev/arp/arp_cli/config"
- "github.com/AlecAivazis/survey/v2"
- "github.com/urfave/cli/v3"
- )
- // REPL represents a Read-Eval-Print Loop for interactive CLI usage
- type REPL struct {
- app *cli.Command
- prompt string
- history []string
- serverURL string // Session-specific server URL
- token string // Session-specific token
- }
- // NewREPL creates a new REPL instance
- func NewREPL(app *cli.Command) *REPL {
- return &REPL{
- app: app,
- prompt: "arp> ",
- }
- }
- // Run starts the REPL loop
- func (r *REPL) Run(ctx context.Context) error {
- // Load saved config for defaults
- cfg, err := config.Load()
- if err != nil {
- return fmt.Errorf("failed to load config: %w", err)
- }
- // Prompt for server URL
- serverURL, err := r.promptServerURL(cfg)
- if err != nil {
- return err
- }
- r.serverURL = serverURL
- r.token = cfg.Token // Use saved token as starting point
- // Update prompt to show connection info
- r.updatePrompt()
- fmt.Println("ARP CLI - Interactive Mode")
- fmt.Printf("Connected to: %s\n", r.serverURL)
- fmt.Println("Type 'help' for available commands, 'exit' or 'quit' to leave.")
- fmt.Println()
- scanner := bufio.NewScanner(os.Stdin)
- for {
- // Show prompt
- fmt.Print(r.prompt)
- // Read line
- if !scanner.Scan() {
- // EOF (Ctrl+D)
- fmt.Println()
- break
- }
- line := strings.TrimSpace(scanner.Text())
- if line == "" {
- continue
- }
- // Check for exit commands
- if line == "exit" || line == "quit" || line == "q" {
- fmt.Println("Goodbye!")
- break
- }
- // Parse the line into args
- args, err := parseArgs(line)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing command: %v\n", err)
- continue
- }
- // Prepend the app name to match CLI expectations
- args = append([]string{"arp_cli"}, args...)
- // Inject session URL if not already specified
- args = r.injectSessionURL(args)
- // Execute the command
- if err := r.app.Run(ctx, args); err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- }
- // Store in history
- r.history = append(r.history, line)
- // Reload config to pick up any token changes from login
- r.reloadToken()
- }
- if err := scanner.Err(); err != nil {
- return fmt.Errorf("reading input: %w", err)
- }
- return nil
- }
- // promptServerURL prompts the user for a server URL
- func (r *REPL) promptServerURL(cfg *config.Config) (string, error) {
- // Determine default URL
- defaultURL := cfg.ServerURL
- if defaultURL == "" {
- defaultURL = "http://localhost:8080/query"
- }
- var serverURL string
- prompt := &survey.Input{
- Message: "Server URL",
- Default: defaultURL,
- Help: "The ARP server URL to connect to for this session",
- }
- if err := survey.AskOne(prompt, &serverURL); err != nil {
- return "", err
- }
- // Ensure URL has the /query endpoint
- serverURL = ensureQueryEndpoint(serverURL)
- return serverURL, nil
- }
- // updatePrompt updates the prompt to show connection info
- func (r *REPL) updatePrompt() {
- // Extract a short identifier from the URL for the prompt
- shortName := r.getShortServerName()
- r.prompt = fmt.Sprintf("arp[%s]> ", shortName)
- }
- // getShortServerName extracts a short name from the server URL for the prompt
- func (r *REPL) getShortServerName() string {
- if r.serverURL == "" {
- return "local"
- }
- parsed, err := url.Parse(r.serverURL)
- if err != nil {
- return "server"
- }
- host := parsed.Host
- // Remove port if present
- if idx := strings.Index(host, ":"); idx != -1 {
- host = host[:idx]
- }
- // Shorten common patterns
- if host == "localhost" || host == "127.0.0.1" {
- return "local"
- }
- // Truncate if too long
- if len(host) > 15 {
- host = host[:12] + "..."
- }
- return host
- }
- // injectSessionURL adds the --url flag to commands if not already present
- func (r *REPL) injectSessionURL(args []string) []string {
- if r.serverURL == "" {
- return args
- }
- // Don't inject URL for help commands (help treats args as topics)
- if len(args) > 1 && args[1] == "help" {
- return args
- }
- // Check if --url or -u is already in args
- for i, arg := range args {
- if arg == "--url" || arg == "-u" {
- // URL already specified, don't inject
- return args
- }
- // Check for --url=value format
- if strings.HasPrefix(arg, "--url=") || strings.HasPrefix(arg, "-u=") {
- return args
- }
- // Also check if previous arg was --url/-u and this is the value
- if i > 0 && (args[i-1] == "--url" || args[i-1] == "-u") {
- return args
- }
- }
- // Insert --url flag right after the app name, before any subcommands
- // This ensures global flags are processed correctly by urfave/cli/v3
- insertPos := 1
- // Insert --url flag with value
- newArgs := make([]string, 0, len(args)+2)
- newArgs = append(newArgs, args[:insertPos]...)
- newArgs = append(newArgs, "--url", r.serverURL)
- newArgs = append(newArgs, args[insertPos:]...)
- return newArgs
- }
- // reloadToken reloads the token from config (called after commands that might change auth)
- func (r *REPL) reloadToken() {
- cfg, err := config.Load()
- if err == nil {
- r.token = cfg.Token
- }
- }
- // parseArgs parses a command line string into arguments
- // Handles quoted strings and basic escaping
- func parseArgs(line string) ([]string, error) {
- var args []string
- var current strings.Builder
- inQuote := false
- quoteChar := rune(0)
- for i, ch := range line {
- switch {
- case ch == '\\' && i+1 < len(line):
- // Handle escape sequences
- next := rune(line[i+1])
- if next == '"' || next == '\'' || next == '\\' || next == ' ' {
- current.WriteRune(next)
- // Skip the next character
- line = line[:i] + line[i+1:]
- } else {
- current.WriteRune(ch)
- }
- case (ch == '"' || ch == '\'') && !inQuote:
- inQuote = true
- quoteChar = ch
- case ch == quoteChar && inQuote:
- inQuote = false
- quoteChar = 0
- case ch == ' ' && !inQuote:
- if current.Len() > 0 {
- args = append(args, current.String())
- current.Reset()
- }
- default:
- current.WriteRune(ch)
- }
- }
- // Add the last argument
- if current.Len() > 0 {
- args = append(args, current.String())
- }
- return args, nil
- }
|