feat(secretstore): add animated bitwarden wait loader
This commit is contained in:
parent
7d159bfdbd
commit
98f07f557d
2 changed files with 155 additions and 0 deletions
|
|
@ -10,6 +10,9 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -18,11 +21,19 @@ const (
|
||||||
bitwardenSecretFieldName = "mcp-secret"
|
bitwardenSecretFieldName = "mcp-secret"
|
||||||
bitwardenServiceFieldName = "mcp-service"
|
bitwardenServiceFieldName = "mcp-service"
|
||||||
bitwardenSecretNameFieldName = "mcp-secret-name"
|
bitwardenSecretNameFieldName = "mcp-secret-name"
|
||||||
|
bitwardenLoaderMessage = "Waiting BitWarden..."
|
||||||
|
bitwardenLoaderInterval = 90 * time.Millisecond
|
||||||
|
bitwardenLoaderColorBase = "\033[38;5;39m"
|
||||||
|
bitwardenLoaderColorWave = "\033[38;5;81m"
|
||||||
|
bitwardenLoaderColorFocus = "\033[38;5;117m"
|
||||||
|
bitwardenLoaderResetColor = "\033[0m"
|
||||||
|
bitwardenLoaderClearLine = "\r\033[2K"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bitwardenRunner func(command string, stdin []byte, args ...string) ([]byte, error)
|
type bitwardenRunner func(command string, stdin []byte, args ...string) ([]byte, error)
|
||||||
|
|
||||||
var runBitwardenCLI bitwardenRunner = executeBitwardenCLI
|
var runBitwardenCLI bitwardenRunner = executeBitwardenCLI
|
||||||
|
var bitwardenLoaderActive atomic.Bool
|
||||||
|
|
||||||
type bitwardenStore struct {
|
type bitwardenStore struct {
|
||||||
command string
|
command string
|
||||||
|
|
@ -511,6 +522,9 @@ func bitwardenItemMatchesMarkers(payload map[string]any, serviceName, secretName
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeBitwardenCLI(command string, stdin []byte, args ...string) ([]byte, error) {
|
func executeBitwardenCLI(command string, stdin []byte, args ...string) ([]byte, error) {
|
||||||
|
stopLoader := startBitwardenLoader()
|
||||||
|
defer stopLoader()
|
||||||
|
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
if stdin != nil {
|
if stdin != nil {
|
||||||
cmd.Stdin = bytes.NewReader(stdin)
|
cmd.Stdin = bytes.NewReader(stdin)
|
||||||
|
|
@ -528,6 +542,108 @@ func executeBitwardenCLI(command string, stdin []byte, args ...string) ([]byte,
|
||||||
return stdout.Bytes(), nil
|
return stdout.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startBitwardenLoader() func() {
|
||||||
|
if !shouldShowBitwardenLoader() {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
if !bitwardenLoaderActive.CompareAndSwap(false, true) {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(stopped)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(bitwardenLoaderInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
phase := 0
|
||||||
|
for {
|
||||||
|
_, _ = fmt.Fprint(os.Stdout, bitwardenLoaderFrame(phase))
|
||||||
|
phase++
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
_, _ = fmt.Fprint(os.Stdout, bitwardenLoaderClearLine)
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var stopOnce sync.Once
|
||||||
|
return func() {
|
||||||
|
stopOnce.Do(func() {
|
||||||
|
close(done)
|
||||||
|
<-stopped
|
||||||
|
bitwardenLoaderActive.Store(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldShowBitwardenLoader() bool {
|
||||||
|
term := strings.TrimSpace(os.Getenv("TERM"))
|
||||||
|
if term == "" || strings.EqualFold(term, "dumb") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stdout.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (info.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func bitwardenLoaderFrame(phase int) string {
|
||||||
|
chars := []rune(bitwardenLoaderMessage)
|
||||||
|
if len(chars) == 0 {
|
||||||
|
return bitwardenLoaderClearLine
|
||||||
|
}
|
||||||
|
|
||||||
|
waveIndex := phase % len(chars)
|
||||||
|
if waveIndex < 0 {
|
||||||
|
waveIndex += len(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame strings.Builder
|
||||||
|
frame.Grow(len(chars)*14 + len(bitwardenLoaderClearLine) + len(bitwardenLoaderResetColor))
|
||||||
|
frame.WriteString(bitwardenLoaderClearLine)
|
||||||
|
|
||||||
|
for idx, char := range chars {
|
||||||
|
frame.WriteString(bitwardenLoaderColorForIndex(idx, waveIndex, len(chars)))
|
||||||
|
frame.WriteRune(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.WriteString(bitwardenLoaderResetColor)
|
||||||
|
return frame.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func bitwardenLoaderColorForIndex(idx, waveIndex, length int) string {
|
||||||
|
if length <= 1 {
|
||||||
|
return bitwardenLoaderColorFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
distance := idx - waveIndex
|
||||||
|
if distance < 0 {
|
||||||
|
distance = -distance
|
||||||
|
}
|
||||||
|
if wrapped := length - distance; wrapped < distance {
|
||||||
|
distance = wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
switch distance {
|
||||||
|
case 0:
|
||||||
|
return bitwardenLoaderColorFocus
|
||||||
|
case 1:
|
||||||
|
return bitwardenLoaderColorWave
|
||||||
|
default:
|
||||||
|
return bitwardenLoaderColorBase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeBitwardenExecutionError(err error, stderrText, stdoutText string) error {
|
func normalizeBitwardenExecutionError(err error, stderrText, stdoutText string) error {
|
||||||
detail := sanitizeBitwardenErrorDetail(stderrText, stdoutText)
|
detail := sanitizeBitwardenErrorDetail(stderrText, stdoutText)
|
||||||
classification := classifyBitwardenError(detail)
|
classification := classifyBitwardenError(detail)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gitea.lclr.dev/AI/mcp-framework/manifest"
|
"gitea.lclr.dev/AI/mcp-framework/manifest"
|
||||||
)
|
)
|
||||||
|
|
@ -365,6 +367,43 @@ func TestExecuteBitwardenCLIClassifiesLoginError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBitwardenLoaderFrameUsesSingleLineRewriteAndMessage(t *testing.T) {
|
||||||
|
frame := bitwardenLoaderFrame(0)
|
||||||
|
if !strings.HasPrefix(frame, "\r\033[2K") {
|
||||||
|
t.Fatalf("frame prefix = %q, want carriage return + clear line", frame)
|
||||||
|
}
|
||||||
|
if !strings.Contains(frame, "\033[38;5;117mW") {
|
||||||
|
t.Fatalf("frame = %q, want highlighted first rune", frame)
|
||||||
|
}
|
||||||
|
cleaned := stripANSIControlSequences(frame)
|
||||||
|
if cleaned != bitwardenLoaderMessage {
|
||||||
|
t.Fatalf("cleaned frame = %q, want %q", cleaned, bitwardenLoaderMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitwardenLoaderFrameMovesAndWrapsTheWave(t *testing.T) {
|
||||||
|
firstFrame := bitwardenLoaderFrame(0)
|
||||||
|
secondFrame := bitwardenLoaderFrame(1)
|
||||||
|
if firstFrame == secondFrame {
|
||||||
|
t.Fatal("expected different frames between two ticks")
|
||||||
|
}
|
||||||
|
if !strings.Contains(secondFrame, "\033[38;5;117ma") {
|
||||||
|
t.Fatalf("second frame = %q, want highlighted second rune", secondFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped := bitwardenLoaderFrame(utf8.RuneCountInString(bitwardenLoaderMessage))
|
||||||
|
if !strings.Contains(wrapped, "\033[38;5;117mW") {
|
||||||
|
t.Fatalf("wrapped frame = %q, want wave to wrap to first rune", wrapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ansiControlSequencePattern = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
|
||||||
|
|
||||||
|
func stripANSIControlSequences(value string) string {
|
||||||
|
noANSI := ansiControlSequencePattern.ReplaceAllString(value, "")
|
||||||
|
return strings.ReplaceAll(noANSI, "\r", "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenFromManifestSupportsBitwardenCLIBackendPolicy(t *testing.T) {
|
func TestOpenFromManifestSupportsBitwardenCLIBackendPolicy(t *testing.T) {
|
||||||
withBitwardenSession(t)
|
withBitwardenSession(t)
|
||||||
fakeCLI := newFakeBitwardenCLI("bw")
|
fakeCLI := newFakeBitwardenCLI("bw")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue