Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions internal/awsconfig/awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/url"
"os"
"path/filepath"
"strings"

"gopkg.in/ini.v1"

Expand Down Expand Up @@ -77,17 +76,6 @@ func (s profileStatus) anyNeeded() bool {
return s.configNeeded || s.credsNeeded
}

func (s profileStatus) filesToModify() []string {
var files []string
if s.configNeeded {
files = append(files, "~/.aws/config")
}
if s.credsNeeded {
files = append(files, "~/.aws/credentials")
}
return files
}

// checkProfileStatus determines which AWS profile files need to be written or updated.
func checkProfileStatus(configPath, credsPath, resolvedHost string) (profileStatus, error) {
configNeeded, err := configNeedsWrite(configPath, resolvedHost)
Expand Down Expand Up @@ -220,10 +208,9 @@ func Setup(ctx context.Context, sink output.Sink, interactive bool, resolvedHost
return nil
}

files := strings.Join(status.filesToModify(), " and ")
responseCh := make(chan output.InputResponse, 1)
output.EmitUserInputRequest(sink, output.UserInputRequestEvent{
Prompt: fmt.Sprintf("Set up LocalStack AWS profile in %s?", files),
Prompt: "Configure AWS profile in ~/.aws/?",
Options: []output.InputOption{{Key: "y", Label: "Y"}, {Key: "n", Label: "n"}},
ResponseCh: responseCh,
})
Expand All @@ -245,7 +232,7 @@ func Setup(ctx context.Context, sink output.Sink, interactive bool, resolvedHost
return nil
}
}
output.EmitSuccess(sink, fmt.Sprintf("LocalStack AWS profile written to %s", files))
output.EmitSuccess(sink, "AWS profile successfully configured")
output.EmitNote(sink, fmt.Sprintf("Try: aws s3 mb s3://test --profile %s", profileName))
case <-ctx.Done():
return ctx.Err()
Expand Down
36 changes: 0 additions & 36 deletions internal/awsconfig/awsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,39 +341,3 @@ func TestIsValidLocalStackEndpoint(t *testing.T) {
}
}

func TestFilesToModify(t *testing.T) {
tests := []struct {
name string
status profileStatus
wantFiles []string
}{
{
name: "both needed",
status: profileStatus{configNeeded: true, credsNeeded: true},
wantFiles: []string{"~/.aws/config", "~/.aws/credentials"},
},
{
name: "config only",
status: profileStatus{configNeeded: true, credsNeeded: false},
wantFiles: []string{"~/.aws/config"},
},
{
name: "credentials only",
status: profileStatus{configNeeded: false, credsNeeded: true},
wantFiles: []string{"~/.aws/credentials"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := tc.status.filesToModify()
if len(got) != len(tc.wantFiles) {
t.Fatalf("got %v, want %v", got, tc.wantFiles)
}
for i, want := range tc.wantFiles {
if got[i] != want {
t.Errorf("files[%d]: got %q, want %q", i, got[i], want)
}
}
})
}
}
24 changes: 13 additions & 11 deletions internal/output/plain_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@ func formatUserInputRequest(e UserInputRequestEvent) string {
return FormatPrompt(e.Prompt, e.Options)
}

// FormatPrompt formats a prompt string with its options into a display line.
func FormatPrompt(prompt string, options []InputOption) string {
lines := strings.Split(prompt, "\n")
firstLine := lines[0]
rest := lines[1:]
// FormatPromptLabels formats option labels into a suffix string.
// Returns " (label)" for a single option, " [a/b]" for multiple, or "" for none.
func FormatPromptLabels(options []InputOption) string {
labels := make([]string, 0, len(options))
for _, opt := range options {
if opt.Label != "" {
Expand All @@ -72,15 +70,19 @@ func FormatPrompt(prompt string, options []InputOption) string {

switch len(labels) {
case 0:
if len(rest) == 0 {
return firstLine
}
return strings.Join(append([]string{firstLine}, rest...), "\n")
return ""
case 1:
firstLine = fmt.Sprintf("%s (%s)", firstLine, labels[0])
return fmt.Sprintf(" (%s)", labels[0])
default:
firstLine = fmt.Sprintf("%s [%s]", firstLine, strings.Join(labels, "/"))
return fmt.Sprintf(" [%s]", strings.Join(labels, "/"))
}
}

// FormatPrompt formats a prompt string with its options into a display line.
func FormatPrompt(prompt string, options []InputOption) string {
lines := strings.Split(prompt, "\n")
firstLine := lines[0] + FormatPromptLabels(options)
rest := lines[1:]

if len(rest) == 0 {
return firstLine
Expand Down
23 changes: 22 additions & 1 deletion internal/ui/components/input_prompt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package components

import (
"strings"

"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/ui/styles"
)
Expand Down Expand Up @@ -36,5 +38,24 @@ func (p InputPrompt) View() string {
return ""
}

return styles.SecondaryMessage.Render(output.FormatPrompt(p.prompt, p.options))
lines := strings.Split(p.prompt, "\n")
firstLine := lines[0]

var sb strings.Builder

// "?" prefix in secondary color
sb.WriteString(styles.Secondary.Render("? "))

sb.WriteString(styles.Message.Render(firstLine))

if suffix := output.FormatPromptLabels(p.options); suffix != "" {
sb.WriteString(styles.Secondary.Render(suffix))
}

if len(lines) > 1 {
sb.WriteString("\n")
sb.WriteString(styles.SecondaryMessage.Render(strings.Join(lines[1:], "\n")))
}

return sb.String()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: it looks like the logic to format the labels is duplicated in plain_format.go:62-89. Can we introduce a new helper FormatPromptLabels that's used in both places?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an attempt for this in fa61ef1.

}
85 changes: 85 additions & 0 deletions internal/ui/components/input_prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package components

import (
"strings"
"testing"

"github.com/localstack/lstk/internal/output"
)

func TestInputPromptView(t *testing.T) {
t.Parallel()

tests := []struct {
name string
prompt string
options []output.InputOption
contains []string
excludes []string
}{
{
name: "hidden returns empty",
prompt: "",
options: nil,
contains: nil,
},
{
name: "no options",
prompt: "Continue?",
options: nil,
contains: []string{"?", "Continue?"},
excludes: []string{"(", "["},
},
{
name: "single option shows parentheses",
prompt: "Continue?",
options: []output.InputOption{{Key: "enter", Label: "Press ENTER"}},
contains: []string{"?", "Continue?", "(Press ENTER)"},
},
{
name: "multiple options shows brackets",
prompt: "Configure AWS profile?",
options: []output.InputOption{
{Key: "y", Label: "Y"},
{Key: "n", Label: "n"},
},
contains: []string{"?", "Configure AWS profile?", "[Y/n]"},
},
{
name: "multi-line prompt renders trailing lines",
prompt: "First line\nSecond line\nThird line",
options: []output.InputOption{{Key: "y", Label: "Y"}},
contains: []string{"?", "First line", "Second line", "Third line", "(Y)"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

p := NewInputPrompt()

if tc.prompt == "" && tc.options == nil {
view := p.View()
if view != "" {
t.Fatalf("expected empty view when hidden, got: %q", view)
}
return
}

p = p.Show(tc.prompt, tc.options)
view := p.View()

for _, s := range tc.contains {
if !strings.Contains(view, s) {
t.Errorf("expected view to contain %q, got: %q", s, view)
}
}
for _, s := range tc.excludes {
if strings.Contains(view, s) {
t.Errorf("expected view NOT to contain %q, got: %q", s, view)
}
}
})
}
}
6 changes: 3 additions & 3 deletions test/integration/awsconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestStartPromptsToCreateAWSProfileWhenMissing(t *testing.T) {

// Wait for the AWS profile prompt.
require.Eventually(t, func() bool {
return bytes.Contains(out.Bytes(), []byte("Set up LocalStack AWS profile"))
return bytes.Contains(out.Bytes(), []byte("Configure AWS profile in"))
}, 2*time.Minute, 200*time.Millisecond, "AWS profile prompt should appear")

// Press Y to confirm.
Expand All @@ -66,7 +66,7 @@ func TestStartPromptsToCreateAWSProfileWhenMissing(t *testing.T) {

// Wait for the success message.
require.Eventually(t, func() bool {
return bytes.Contains(out.Bytes(), []byte("LocalStack AWS profile written"))
return bytes.Contains(out.Bytes(), []byte("AWS profile successfully configured"))
}, 10*time.Second, 200*time.Millisecond, "success message should appear")

// Verify files were written to the isolated home dir, not the real one.
Expand Down Expand Up @@ -133,7 +133,7 @@ func TestStartSkipsAWSProfilePromptWhenAlreadyConfigured(t *testing.T) {
_ = cmd.Wait()
<-outputCh

assert.NotContains(t, out.String(), "Set up LocalStack AWS profile",
assert.NotContains(t, out.String(), "Configure AWS profile in",
"profile prompt should not appear when profile is already correctly configured")
}

Expand Down
Loading