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
3 changes: 3 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ AWSリソース管理のためのターミナルUI
- **インタラクティブTUI** - vimスタイルのキーバインドでAWSリソースを操作できます
- **70サービス、175リソース** - EC2、S3、Lambda、RDS、ECS、EKSなど多数に対応しています
- **マルチプロファイル&マルチリージョン** - 複数のアカウント/リージョンを並列でクエリできます
- **プロファイルログイン補助** - プロファイル選択画面からAWS SSOログインやコンソールログインを実行できます
- **リソースアクション** - インスタンスの起動/停止、リソースの削除、ログのテールが可能です
- **クロスリソースナビゲーション** - VPCからサブネット、LambdaからCloudWatchへジャンプできます
- **フィルタリング&ソート** - あいまい検索、タグフィルタリング、カラムソートに対応しています
Expand Down Expand Up @@ -109,6 +110,8 @@ claws --read-only
| `A` | AIチャット(リスト/詳細/差分ビュー) |
| `R` | リージョンを選択します |
| `P` | プロファイルを選択します |
| `l` | 選択中のプロファイルでSSOログインします(プロファイル選択画面) |
| `L` | 選択中のプロファイルでコンソールログインします(プロファイル選択画面) |
| `?` | ヘルプを表示します |
| `q` | 終了します |

Expand Down
3 changes: 3 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ AWS 리소스 관리를 위한 터미널 UI
- **인터랙티브 TUI** - vim 스타일 키 바인딩으로 AWS 리소스를 탐색할 수 있습니다
- **70개 서비스, 175개 리소스** - EC2, S3, Lambda, RDS, ECS, EKS 등 다양한 서비스를 지원합니다
- **멀티 프로필 및 멀티 리전** - 여러 계정/리전을 병렬로 조회할 수 있습니다
- **프로필 로그인 도우미** - 프로필 선택기에서 AWS SSO 로그인 또는 콘솔 로그인을 실행할 수 있습니다
- **리소스 액션** - 인스턴스 시작/중지, 리소스 삭제, 로그 테일링이 가능합니다
- **크로스 리소스 탐색** - VPC에서 서브넷으로, Lambda에서 CloudWatch로 이동할 수 있습니다
- **필터링 및 정렬** - 퍼지 검색, 태그 필터링, 컬럼 정렬을 지원합니다
Expand Down Expand Up @@ -109,6 +110,8 @@ claws --read-only
| `A` | AI 채팅 (리스트/상세/비교 뷰) |
| `R` | 리전을 선택합니다 |
| `P` | 프로필을 선택합니다 |
| `l` | 선택된 프로필로 SSO 로그인합니다 (프로필 선택기) |
| `L` | 선택된 프로필로 콘솔 로그인합니다 (프로필 선택기) |
| `?` | 도움말을 표시합니다 |
| `q` | 종료합니다 |

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A terminal UI for AWS resource management
- **Interactive TUI** - Navigate AWS resources with vim-style keybindings
- **70 services, 175 resources** - EC2, S3, Lambda, RDS, ECS, EKS, and more
- **Multi-profile & Multi-region** - Query multiple accounts/regions in parallel
- **Profile login helpers** - Run AWS SSO login or open console login from the profile selector
- **Resource actions** - Start/stop instances, delete resources, tail logs
- **Cross-resource navigation** - Jump from VPC to subnets, Lambda to CloudWatch
- **Filtering & sorting** - Fuzzy search, tag filtering, column sorting
Expand Down Expand Up @@ -109,6 +110,8 @@ claws --read-only
| `A` | AI Chat (in list/detail/diff views) |
| `R` | Select region(s) |
| `P` | Select profile(s) |
| `l` | SSO login for selected profile (in profile selector) |
| `L` | Console login for selected profile (in profile selector) |
| `?` | Show help |
| `q` | Quit |

Expand Down
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ AWS 资源管理终端 UI
- **交互式 TUI** - 使用 vim 风格的快捷键浏览 AWS 资源
- **70 个服务、175 个资源** - 支持 EC2、S3、Lambda、RDS、ECS、EKS 等众多服务
- **多配置文件与多区域** - 并行查询多个账户和区域
- **配置文件登录辅助** - 可从配置文件选择器执行 AWS SSO 登录或控制台登录
- **资源操作** - 启动/停止实例、删除资源、追踪日志
- **跨资源导航** - 从 VPC 跳转到子网,从 Lambda 跳转到 CloudWatch
- **筛选与排序** - 模糊搜索、标签筛选、列排序
Expand Down Expand Up @@ -109,6 +110,8 @@ claws --read-only
| `A` | AI 聊天(列表/详情/差异视图) |
| `R` | 选择区域 |
| `P` | 选择配置文件 |
| `l` | 对选中的配置文件进行 SSO 登录(配置文件选择器) |
| `L` | 对选中的配置文件进行控制台登录(配置文件选择器) |
| `?` | 显示帮助 |
| `q` | 退出 |

Expand Down
92 changes: 92 additions & 0 deletions cmd/claws/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"slices"
"testing"

"github.com/clawscli/claws/internal/config"
)

func TestParseFlags_Profiles(t *testing.T) {
Expand Down Expand Up @@ -146,3 +148,93 @@ func TestParseFlags_ConfigFile(t *testing.T) {
})
}
}

func TestParseFlags_EnvCreds(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "short flag", args: []string{"-e"}},
{name: "long flag", args: []string{"--env"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := parseFlagsFromArgs(tt.args)
if !opts.envCreds {
t.Error("envCreds should be true")
}
})
}
}

func TestApplyStartupConfig_ProfilePrecedence(t *testing.T) {
tests := []struct {
name string
opts cliOptions
startup []string
wantProfile []string
}{
{
name: "saved startup profiles used when no CLI override",
opts: cliOptions{},
startup: []string{"saved"},
wantProfile: []string{"saved"},
},
{
name: "profile flag overrides saved startup profiles",
opts: cliOptions{profiles: []string{"cli"}},
startup: []string{"saved"},
wantProfile: []string{"cli"},
},
{
name: "env flag overrides profile flag and saved startup profiles",
opts: cliOptions{envCreds: true, profiles: []string{"cli"}},
startup: []string{"saved"},
wantProfile: []string{config.ProfileIDEnvOnly},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fileCfg := &config.FileConfig{Startup: config.StartupConfig{Profiles: tt.startup}}
cfg := &config.Config{}

applyStartupConfig(tt.opts, fileCfg, cfg)

if got := selectionIDs(cfg.Selections()); !slices.Equal(got, tt.wantProfile) {
t.Errorf("selections = %v, want %v", got, tt.wantProfile)
}
})
}
}

func TestApplyStartupConfig_EnvOverrideDoesNotMutateSavedProfiles(t *testing.T) {
fileCfg := &config.FileConfig{Startup: config.StartupConfig{Profiles: []string{"personal"}}}
cfg := &config.Config{}

applyStartupConfig(cliOptions{envCreds: true}, fileCfg, cfg)

if got := cfg.Selection().ID(); got != config.ProfileIDEnvOnly {
t.Fatalf("selection = %q, want env-only", got)
}
_, savedProfiles := fileCfg.GetStartup()
if !slices.Equal(savedProfiles, []string{"personal"}) {
t.Fatalf("saved profiles = %v, want [personal]", savedProfiles)
}

nextCfg := &config.Config{}
applyStartupConfig(cliOptions{}, fileCfg, nextCfg)

if got := nextCfg.Selection().ID(); got != "personal" {
t.Errorf("next launch selection = %q, want personal", got)
}
}

func selectionIDs(selections []config.ProfileSelection) []string {
ids := make([]string, len(selections))
for i, sel := range selections {
ids[i] = sel.ID()
}
return ids
}
18 changes: 16 additions & 2 deletions internal/view/profile_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ func (p *ProfileSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case loginResultMsg:
p.loginResult = &msg
if msg.success {
if msg.isConsoleLogin {
selected := p.selector.Selected()
for id := range selected {
delete(selected, id)
}
selected[msg.profileID] = true

sel := config.NamedProfile(msg.profileID)
selections := []config.ProfileSelection{sel}
config.Global().SetSelections(selections)
p.selector.ClearResult()
p.updateExtraHeight()
return p, func() tea.Msg {
return navmsg.ProfilesChangedMsg{Selections: selections}
}
}
p.selector.Selected()[msg.profileID] = true
p.selector.ClearResult()
}
Expand Down Expand Up @@ -277,8 +293,6 @@ func (p *ProfileSelector) consoleLoginCurrentProfile() (tea.Model, tea.Cmd) {
if err != nil {
return loginResultMsg{profileID: profileID, success: false, err: err, isConsoleLogin: true}
}
sel := config.NamedProfile(profileID)
config.Global().SetSelection(sel)
return loginResultMsg{profileID: profileID, success: true, isConsoleLogin: true}
})
}
Expand Down
39 changes: 39 additions & 0 deletions internal/view/profile_selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"testing"

tea "charm.land/bubbletea/v2"

"github.com/clawscli/claws/internal/config"
navmsg "github.com/clawscli/claws/internal/msg"
)

func testProfiles() []profileItem {
Expand Down Expand Up @@ -173,3 +176,39 @@ func TestProfileSelectorToggle(t *testing.T) {
t.Error("Expected both profiles to be selected")
}
}

func TestProfileSelectorConsoleLoginSuccessSwitchesAndEmitsProfileChange(t *testing.T) {
config.Global().UseEnvOnly()
t.Cleanup(func() { config.Global().UseSDKDefault() })

selector := NewProfileSelector()
selector.SetSize(100, 50)
selector.Update(profilesLoadedMsg{profiles: []profileItem{
{id: config.ProfileIDEnvOnly, display: "Env/IMDS Only", isSSO: false},
{id: "dev", display: "dev", isSSO: false},
}})

_, cmd := selector.Update(loginResultMsg{profileID: "dev", success: true, isConsoleLogin: true})
if cmd == nil {
t.Fatal("Expected console login success to emit profile change command")
}

msg := cmd()
profileMsg, ok := msg.(navmsg.ProfilesChangedMsg)
if !ok {
t.Fatalf("command message = %T, want ProfilesChangedMsg", msg)
}
if len(profileMsg.Selections) != 1 || profileMsg.Selections[0].ID() != "dev" {
t.Fatalf("ProfilesChangedMsg selections = %v, want [dev]", profileMsg.Selections)
}

if got := config.Global().Selection().ID(); got != "dev" {
t.Errorf("global selection = %q, want dev", got)
}
if selector.selector.Selected()[config.ProfileIDEnvOnly] {
t.Error("env-only selection should be cleared after console login switches profile")
}
if !selector.selector.Selected()["dev"] {
t.Error("dev profile should be selected after console login")
}
}