secret/internal/cli/cli.go
2025-05-29 11:02:22 -07:00

92 lines
2.5 KiB
Go

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
}