package cli import ( "bufio" "fmt" "os" "strings" "syscall" "git.eeqj.de/sneak/secret/internal/secret" "github.com/spf13/afero" "golang.org/x/term" ) // Global scanner for consistent stdin reading var stdinScanner *bufio.Scanner // CLIInstance encapsulates all CLI functionality and state type CLIInstance struct { fs afero.Fs stateDir string } // NewCLIInstance creates a new CLI instance with the real filesystem func NewCLIInstance() *CLIInstance { fs := afero.NewOsFs() stateDir := secret.DetermineStateDir("") return &CLIInstance{ fs: fs, stateDir: stateDir, } } // NewCLIInstanceWithFs creates a new CLI instance with the given filesystem (for testing) func NewCLIInstanceWithFs(fs afero.Fs) *CLIInstance { stateDir := secret.DetermineStateDir("") return &CLIInstance{ fs: fs, stateDir: stateDir, } } // NewCLIInstanceWithStateDir creates a new CLI instance with custom state directory (for testing) func NewCLIInstanceWithStateDir(fs afero.Fs, stateDir string) *CLIInstance { return &CLIInstance{ fs: fs, stateDir: stateDir, } } // SetFilesystem sets the filesystem for this CLI instance (for testing) func (cli *CLIInstance) SetFilesystem(fs afero.Fs) { cli.fs = fs } // SetStateDir sets the state directory for this CLI instance (for testing) func (cli *CLIInstance) SetStateDir(stateDir string) { cli.stateDir = stateDir } // GetStateDir returns the state directory for this CLI instance func (cli *CLIInstance) GetStateDir() string { return cli.stateDir } // getStdinScanner returns a shared scanner for stdin to avoid buffering issues func getStdinScanner() *bufio.Scanner { if stdinScanner == nil { stdinScanner = bufio.NewScanner(os.Stdin) } return stdinScanner } // readLineFromStdin reads a single line from stdin with a prompt // Uses a shared scanner to avoid buffering issues between multiple calls func readLineFromStdin(prompt string) (string, error) { // Check if stderr is a terminal - if not, we can't prompt interactively if !term.IsTerminal(int(syscall.Stderr)) { return "", fmt.Errorf("cannot prompt for input: stderr is not a terminal (running in non-interactive mode)") } fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout scanner := getStdinScanner() if !scanner.Scan() { if err := scanner.Err(); err != nil { return "", fmt.Errorf("failed to read from stdin: %w", err) } return "", fmt.Errorf("failed to read from stdin: EOF") } return strings.TrimSpace(scanner.Text()), nil }