feat: add & list commands with deadlock fix

- Add 'add' command: register hosts via flags or interactive prompts
  Supports password/key/both auth, groups, tags, notes, port override
- Add 'list' command: display hosts with filtering and formatting
  Supports --group, --tag filters, --sort, table/json/wide output
- Fix deadlock bug in JSON storage (RLock within Lock)
  Introduced internal list functions that don't lock
  Affects: ListHosts/GetHost/SaveHost/DeleteHost + KeyPair + Snippet
- Add comprehensive tests for add and list commands
- Update PROJECT_STATE.md (Tasks 1-8 complete, ~55% done)
This commit is contained in:
Swana Diva Borneos
2026-06-23 10:56:04 +07:00
parent 7dfe181b97
commit 4087c5fdc7
8 changed files with 770 additions and 16 deletions
+33 -9
View File
@@ -62,6 +62,11 @@ func (s *JSONStorage) ListHosts(ctx context.Context) ([]*models.Host, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.listHostsInternal()
}
// listHostsInternal reads hosts WITHOUT locking (caller must hold lock)
func (s *JSONStorage) listHostsInternal() ([]*models.Host, error) {
var data struct {
Hosts []*models.Host `json:"hosts"`
}
@@ -74,7 +79,10 @@ func (s *JSONStorage) ListHosts(ctx context.Context) ([]*models.Host, error) {
}
func (s *JSONStorage) GetHost(ctx context.Context, id string) (*models.Host, error) {
hosts, err := s.ListHosts(ctx)
s.mu.RLock()
defer s.mu.RUnlock()
hosts, err := s.listHostsInternal()
if err != nil {
return nil, err
}
@@ -92,7 +100,7 @@ func (s *JSONStorage) SaveHost(ctx context.Context, host *models.Host) error {
s.mu.Lock()
defer s.mu.Unlock()
hosts, err := s.ListHosts(ctx)
hosts, err := s.listHostsInternal()
if err != nil {
return err
}
@@ -117,7 +125,7 @@ func (s *JSONStorage) DeleteHost(ctx context.Context, id string) error {
s.mu.Lock()
defer s.mu.Unlock()
hosts, err := s.ListHosts(ctx)
hosts, err := s.listHostsInternal()
if err != nil {
return err
}
@@ -156,6 +164,11 @@ func (s *JSONStorage) ListKeyPairs(ctx context.Context) ([]*models.KeyPair, erro
s.mu.RLock()
defer s.mu.RUnlock()
return s.listKeyPairsInternal()
}
// listKeyPairsInternal reads key pairs WITHOUT locking (caller must hold lock)
func (s *JSONStorage) listKeyPairsInternal() ([]*models.KeyPair, error) {
var data struct {
KeyPairs []*models.KeyPair `json:"key_pairs"`
}
@@ -168,7 +181,10 @@ func (s *JSONStorage) ListKeyPairs(ctx context.Context) ([]*models.KeyPair, erro
}
func (s *JSONStorage) GetKeyPair(ctx context.Context, id string) (*models.KeyPair, error) {
keys, err := s.ListKeyPairs(ctx)
s.mu.RLock()
defer s.mu.RUnlock()
keys, err := s.listKeyPairsInternal()
if err != nil {
return nil, err
}
@@ -186,7 +202,7 @@ func (s *JSONStorage) SaveKeyPair(ctx context.Context, keyPair *models.KeyPair)
s.mu.Lock()
defer s.mu.Unlock()
keys, err := s.ListKeyPairs(ctx)
keys, err := s.listKeyPairsInternal()
if err != nil {
return err
}
@@ -211,7 +227,7 @@ func (s *JSONStorage) DeleteKeyPair(ctx context.Context, id string) error {
s.mu.Lock()
defer s.mu.Unlock()
keys, err := s.ListKeyPairs(ctx)
keys, err := s.listKeyPairsInternal()
if err != nil {
return err
}
@@ -250,6 +266,11 @@ func (s *JSONStorage) ListSnippets(ctx context.Context) ([]*models.Snippet, erro
s.mu.RLock()
defer s.mu.RUnlock()
return s.listSnippetsInternal()
}
// listSnippetsInternal reads snippets WITHOUT locking (caller must hold lock)
func (s *JSONStorage) listSnippetsInternal() ([]*models.Snippet, error) {
var data struct {
Snippets []*models.Snippet `json:"snippets"`
}
@@ -262,7 +283,10 @@ func (s *JSONStorage) ListSnippets(ctx context.Context) ([]*models.Snippet, erro
}
func (s *JSONStorage) GetSnippet(ctx context.Context, id string) (*models.Snippet, error) {
snippets, err := s.ListSnippets(ctx)
s.mu.RLock()
defer s.mu.RUnlock()
snippets, err := s.listSnippetsInternal()
if err != nil {
return nil, err
}
@@ -280,7 +304,7 @@ func (s *JSONStorage) SaveSnippet(ctx context.Context, snippet *models.Snippet)
s.mu.Lock()
defer s.mu.Unlock()
snippets, err := s.ListSnippets(ctx)
snippets, err := s.listSnippetsInternal()
if err != nil {
return err
}
@@ -305,7 +329,7 @@ func (s *JSONStorage) DeleteSnippet(ctx context.Context, id string) error {
s.mu.Lock()
defer s.mu.Unlock()
snippets, err := s.ListSnippets(ctx)
snippets, err := s.listSnippetsInternal()
if err != nil {
return err
}