feat(cli): add session deletion command and TUI support
- commit
- f95e23c233930dc583a9548a1d21fcec86ddc5f4
- author
- brice · · 2025-12-25 09:42 UTC
- parent
- 9f01528
- signature
- unsigned
3 files · +40 -41
M internal/cli/app.go +14 -15
| @@ -33,9 +33,7 @@ type App struct { | |||
| 33 | 33 | SessionUC *usecase.ManageSessionUseCase | |
| 34 | 34 | ListSessionsUC *usecase.ListSessionsUseCase | |
| 35 | 35 | RestoreUC *usecase.RestoreSessionUseCase | |
| 36 | - | ||
| 37 | - | // Repositories | |
| 38 | - | SessionStateRepo repository.SessionStateRepository | |
| 36 | + | DeleteSessionUC *usecase.DeleteSessionUseCase | |
| 39 | 37 | ||
| 40 | 38 | // Services | |
| 41 | 39 | FaviconService *favicon.Service | |
| @@ -116,24 +114,25 @@ func NewApp() (*App, error) { | |||
| 116 | 114 | searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 117 | 115 | listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir) | |
| 118 | 116 | restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo) | |
| 117 | + | deleteSessionUC := usecase.NewDeleteSessionUseCase(sessionStateRepo, sessionRepo, cfg.Logging.LogDir) | |
| 119 | 118 | ||
| 120 | 119 | // Create favicon service for CLI (path resolution for dmenu/fuzzel) | |
| 121 | 120 | faviconCacheDir, _ := config.GetFaviconCacheDir() | |
| 122 | 121 | faviconService := favicon.NewService(faviconCacheDir) | |
| 123 | 122 | ||
| 124 | 123 | return &App{ | |
| 125 | - | Config: cfg, | |
| 126 | - | Theme: theme, | |
| 127 | - | db: db, | |
| 128 | - | History: historyRepo, | |
| 129 | - | SearchHistoryUC: searchHistoryUC, | |
| 130 | - | SessionUC: sessionUC, | |
| 131 | - | ListSessionsUC: listSessionsUC, | |
| 132 | - | RestoreUC: restoreUC, | |
| 133 | - | SessionStateRepo: sessionStateRepo, | |
| 134 | - | FaviconService: faviconService, | |
| 135 | - | ctx: ctx, | |
| 136 | - | logCleanup: logCleanup, | |
| 124 | + | Config: cfg, | |
| 125 | + | Theme: theme, | |
| 126 | + | db: db, | |
| 127 | + | History: historyRepo, | |
| 128 | + | SearchHistoryUC: searchHistoryUC, | |
| 129 | + | SessionUC: sessionUC, | |
| 130 | + | ListSessionsUC: listSessionsUC, | |
| 131 | + | RestoreUC: restoreUC, | |
| 132 | + | DeleteSessionUC: deleteSessionUC, | |
| 133 | + | FaviconService: faviconService, | |
| 134 | + | ctx: ctx, | |
| 135 | + | logCleanup: logCleanup, | |
| 137 | 136 | }, nil | |
| 138 | 137 | } | |
| 139 | 138 | ||
| @@ -33,9 +33,7 @@ type App struct { | |||||
| 33 | SessionUC *usecase.ManageSessionUseCase | 33 | SessionUC *usecase.ManageSessionUseCase | ||
| 34 | ListSessionsUC *usecase.ListSessionsUseCase | 34 | ListSessionsUC *usecase.ListSessionsUseCase | ||
| 35 | RestoreUC *usecase.RestoreSessionUseCase | 35 | RestoreUC *usecase.RestoreSessionUseCase | ||
| 36 | - | 36 | + | DeleteSessionUC *usecase.DeleteSessionUseCase | |
| 37 | - | // Repositories | |||
| 38 | - | SessionStateRepo repository.SessionStateRepository | |||
| 39 | 37 | ||||
| 40 | // Services | 38 | // Services | ||
| 41 | FaviconService *favicon.Service | 39 | FaviconService *favicon.Service | ||
| @@ -116,24 +114,25 @@ func NewApp() (*App, error) { | |||||
| 116 | searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo) | 114 | searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo) | ||
| 117 | listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir) | 115 | listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir) | ||
| 118 | restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo) | 116 | restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo) | ||
| 117 | + | deleteSessionUC := usecase.NewDeleteSessionUseCase(sessionStateRepo, sessionRepo, cfg.Logging.LogDir) | |||
| 119 | 118 | ||||
| 120 | // Create favicon service for CLI (path resolution for dmenu/fuzzel) | 119 | // Create favicon service for CLI (path resolution for dmenu/fuzzel) | ||
| 121 | faviconCacheDir, _ := config.GetFaviconCacheDir() | 120 | faviconCacheDir, _ := config.GetFaviconCacheDir() | ||
| 122 | faviconService := favicon.NewService(faviconCacheDir) | 121 | faviconService := favicon.NewService(faviconCacheDir) | ||
| 123 | 122 | ||||
| 124 | return &App{ | 123 | return &App{ | ||
| 125 | - | Config: cfg, | 124 | + | Config: cfg, |
| 126 | - | Theme: theme, | 125 | + | Theme: theme, |
| 127 | - | db: db, | 126 | + | db: db, |
| 128 | - | History: historyRepo, | 127 | + | History: historyRepo, |
| 129 | - | SearchHistoryUC: searchHistoryUC, | 128 | + | SearchHistoryUC: searchHistoryUC, |
| 130 | - | SessionUC: sessionUC, | 129 | + | SessionUC: sessionUC, |
| 131 | - | ListSessionsUC: listSessionsUC, | 130 | + | ListSessionsUC: listSessionsUC, |
| 132 | - | RestoreUC: restoreUC, | 131 | + | RestoreUC: restoreUC, |
| 133 | - | SessionStateRepo: sessionStateRepo, | 132 | + | DeleteSessionUC: deleteSessionUC, |
| 134 | - | FaviconService: faviconService, | 133 | + | FaviconService: faviconService, |
| 135 | - | ctx: ctx, | 134 | + | ctx: ctx, |
| 136 | - | logCleanup: logCleanup, | 135 | + | logCleanup: logCleanup, |
| 137 | }, nil | 136 | }, nil | ||
| 138 | } | 137 | } | ||
| 139 | 138 | ||||
M internal/cli/cmd/sessions.go +11 -7
| @@ -59,7 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error { | |||
| 59 | 59 | m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{ | |
| 60 | 60 | ListSessionsUC: app.ListSessionsUC, | |
| 61 | 61 | RestoreUC: app.RestoreUC, | |
| 62 | - | SessionStateRepo: app.SessionStateRepo, | |
| 62 | + | DeleteSessionUC: app.DeleteSessionUC, | |
| 63 | 63 | CurrentSession: currentSessionID, | |
| 64 | 64 | MaxListedSessions: app.Config.Session.MaxListedSessions, | |
| 65 | 65 | }) | |
| @@ -237,7 +237,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error { | |||
| 237 | 237 | return fmt.Errorf("app not initialized") | |
| 238 | 238 | } | |
| 239 | 239 | ||
| 240 | - | if app.SessionStateRepo == nil || app.ListSessionsUC == nil { | |
| 240 | + | if app.DeleteSessionUC == nil || app.ListSessionsUC == nil { | |
| 241 | 241 | return fmt.Errorf("session management not available") | |
| 242 | 242 | } | |
| 243 | 243 | ||
| @@ -247,13 +247,17 @@ func runSessionsDelete(_ *cobra.Command, args []string) error { | |||
| 247 | 247 | return err | |
| 248 | 248 | } | |
| 249 | 249 | ||
| 250 | - | // Check if session is active | |
| 251 | - | if sessionInfo.IsActive || sessionInfo.IsCurrent { | |
| 252 | - | return fmt.Errorf("cannot delete active session - close the browser first") | |
| 250 | + | // Get current session ID | |
| 251 | + | var currentSessionID entity.SessionID | |
| 252 | + | if active, activeErr := app.SessionUC.GetActiveSession(app.Ctx()); activeErr == nil && active != nil { | |
| 253 | + | currentSessionID = active.ID | |
| 253 | 254 | } | |
| 254 | 255 | ||
| 255 | - | // Delete the session state | |
| 256 | - | if err := app.SessionStateRepo.DeleteSnapshot(app.Ctx(), sessionInfo.Session.ID); err != nil { | |
| 256 | + | // Delete using use case (handles validation internally) | |
| 257 | + | if err := app.DeleteSessionUC.Execute(app.Ctx(), usecase.DeleteSessionInput{ | |
| 258 | + | SessionID: sessionInfo.Session.ID, | |
| 259 | + | CurrentSessionID: currentSessionID, | |
| 260 | + | }); err != nil { | |
| 257 | 261 | return fmt.Errorf("delete session: %w", err) | |
| 258 | 262 | } | |
| 259 | 263 | ||
| @@ -59,7 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error { | |||||
| 59 | m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{ | 59 | m := model.NewSessionsModel(app.Ctx(), app.Theme, model.SessionsModelConfig{ | ||
| 60 | ListSessionsUC: app.ListSessionsUC, | 60 | ListSessionsUC: app.ListSessionsUC, | ||
| 61 | RestoreUC: app.RestoreUC, | 61 | RestoreUC: app.RestoreUC, | ||
| 62 | - | SessionStateRepo: app.SessionStateRepo, | 62 | + | DeleteSessionUC: app.DeleteSessionUC, |
| 63 | CurrentSession: currentSessionID, | 63 | CurrentSession: currentSessionID, | ||
| 64 | MaxListedSessions: app.Config.Session.MaxListedSessions, | 64 | MaxListedSessions: app.Config.Session.MaxListedSessions, | ||
| 65 | }) | 65 | }) | ||
| @@ -237,7 +237,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error { | |||||
| 237 | return fmt.Errorf("app not initialized") | 237 | return fmt.Errorf("app not initialized") | ||
| 238 | } | 238 | } | ||
| 239 | 239 | ||||
| 240 | - | if app.SessionStateRepo == nil || app.ListSessionsUC == nil { | 240 | + | if app.DeleteSessionUC == nil || app.ListSessionsUC == nil { |
| 241 | return fmt.Errorf("session management not available") | 241 | return fmt.Errorf("session management not available") | ||
| 242 | } | 242 | } | ||
| 243 | 243 | ||||
| @@ -247,13 +247,17 @@ func runSessionsDelete(_ *cobra.Command, args []string) error { | |||||
| 247 | return err | 247 | return err | ||
| 248 | } | 248 | } | ||
| 249 | 249 | ||||
| 250 | - | // Check if session is active | 250 | + | // Get current session ID |
| 251 | - | if sessionInfo.IsActive || sessionInfo.IsCurrent { | 251 | + | var currentSessionID entity.SessionID |
| 252 | - | return fmt.Errorf("cannot delete active session - close the browser first") | 252 | + | if active, activeErr := app.SessionUC.GetActiveSession(app.Ctx()); activeErr == nil && active != nil { |
| 253 | + | currentSessionID = active.ID | |||
| 253 | } | 254 | } | ||
| 254 | 255 | ||||
| 255 | - | // Delete the session state | 256 | + | // Delete using use case (handles validation internally) |
| 256 | - | if err := app.SessionStateRepo.DeleteSnapshot(app.Ctx(), sessionInfo.Session.ID); err != nil { | 257 | + | if err := app.DeleteSessionUC.Execute(app.Ctx(), usecase.DeleteSessionInput{ |
| 258 | + | SessionID: sessionInfo.Session.ID, | |||
| 259 | + | CurrentSessionID: currentSessionID, | |||
| 260 | + | }); err != nil { | |||
| 257 | return fmt.Errorf("delete session: %w", err) | 261 | return fmt.Errorf("delete session: %w", err) | ||
| 258 | } | 262 | } | ||
| 259 | 263 | ||||
M internal/cli/model/sessions.go +15 -19
| @@ -39,13 +39,11 @@ type SessionsModel struct { | |||
| 39 | 39 | maxListedSessions int | |
| 40 | 40 | ||
| 41 | 41 | // Dependencies | |
| 42 | - | ctx context.Context | |
| 43 | - | listSessionsUC *usecase.ListSessionsUseCase | |
| 44 | - | restoreUC *usecase.RestoreSessionUseCase | |
| 45 | - | sessionStateRepo interface { | |
| 46 | - | DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error | |
| 47 | - | } | |
| 48 | - | theme *styles.Theme | |
| 42 | + | ctx context.Context | |
| 43 | + | listSessionsUC *usecase.ListSessionsUseCase | |
| 44 | + | restoreUC *usecase.RestoreSessionUseCase | |
| 45 | + | deleteSessionUC *usecase.DeleteSessionUseCase | |
| 46 | + | theme *styles.Theme | |
| 49 | 47 | } | |
| 50 | 48 | ||
| 51 | 49 | // sessionsKeyMap defines keybindings for the sessions browser. | |
| @@ -113,11 +111,9 @@ func defaultSessionsKeyMap() sessionsKeyMap { | |||
| 113 | 111 | ||
| 114 | 112 | // SessionsModelConfig holds configuration for the sessions model. | |
| 115 | 113 | type SessionsModelConfig struct { | |
| 116 | - | ListSessionsUC *usecase.ListSessionsUseCase | |
| 117 | - | RestoreUC *usecase.RestoreSessionUseCase | |
| 118 | - | SessionStateRepo interface { | |
| 119 | - | DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error | |
| 120 | - | } | |
| 114 | + | ListSessionsUC *usecase.ListSessionsUseCase | |
| 115 | + | RestoreUC *usecase.RestoreSessionUseCase | |
| 116 | + | DeleteSessionUC *usecase.DeleteSessionUseCase | |
| 121 | 117 | CurrentSession entity.SessionID | |
| 122 | 118 | MaxListedSessions int | |
| 123 | 119 | } | |
| @@ -140,7 +136,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode | |||
| 140 | 136 | ctx: ctx, | |
| 141 | 137 | listSessionsUC: cfg.ListSessionsUC, | |
| 142 | 138 | restoreUC: cfg.RestoreUC, | |
| 143 | - | sessionStateRepo: cfg.SessionStateRepo, | |
| 139 | + | deleteSessionUC: cfg.DeleteSessionUC, | |
| 144 | 140 | currentSession: cfg.CurrentSession, | |
| 145 | 141 | theme: theme, | |
| 146 | 142 | } | |
| @@ -312,14 +308,14 @@ func (m SessionsModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 312 | 308 | ||
| 313 | 309 | func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd { | |
| 314 | 310 | return func() tea.Msg { | |
| 315 | - | log := logging.FromContext(m.ctx) | |
| 316 | - | log.Info().Str("session_id", string(sessionID)).Msg("deleting session") | |
| 317 | - | ||
| 318 | - | if m.sessionStateRepo == nil { | |
| 319 | - | return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("session state repo not available")} | |
| 311 | + | if m.deleteSessionUC == nil { | |
| 312 | + | return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("delete session use case not available")} | |
| 320 | 313 | } | |
| 321 | 314 | ||
| 322 | - | err := m.sessionStateRepo.DeleteSnapshot(m.ctx, sessionID) | |
| 315 | + | err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{ | |
| 316 | + | SessionID: sessionID, | |
| 317 | + | CurrentSessionID: m.currentSession, | |
| 318 | + | }) | |
| 323 | 319 | return sessionDeletedMsg{sessionID: sessionID, err: err} | |
| 324 | 320 | } | |
| 325 | 321 | } | |
| @@ -39,13 +39,11 @@ type SessionsModel struct { | |||||
| 39 | maxListedSessions int | 39 | maxListedSessions int | ||
| 40 | 40 | ||||
| 41 | // Dependencies | 41 | // Dependencies | ||
| 42 | - | ctx context.Context | 42 | + | ctx context.Context |
| 43 | - | listSessionsUC *usecase.ListSessionsUseCase | 43 | + | listSessionsUC *usecase.ListSessionsUseCase |
| 44 | - | restoreUC *usecase.RestoreSessionUseCase | 44 | + | restoreUC *usecase.RestoreSessionUseCase |
| 45 | - | sessionStateRepo interface { | 45 | + | deleteSessionUC *usecase.DeleteSessionUseCase |
| 46 | - | DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error | 46 | + | theme *styles.Theme |
| 47 | - | } | |||
| 48 | - | theme *styles.Theme | |||
| 49 | } | 47 | } | ||
| 50 | 48 | ||||
| 51 | // sessionsKeyMap defines keybindings for the sessions browser. | 49 | // sessionsKeyMap defines keybindings for the sessions browser. | ||
| @@ -113,11 +111,9 @@ func defaultSessionsKeyMap() sessionsKeyMap { | |||||
| 113 | 111 | ||||
| 114 | // SessionsModelConfig holds configuration for the sessions model. | 112 | // SessionsModelConfig holds configuration for the sessions model. | ||
| 115 | type SessionsModelConfig struct { | 113 | type SessionsModelConfig struct { | ||
| 116 | - | ListSessionsUC *usecase.ListSessionsUseCase | 114 | + | ListSessionsUC *usecase.ListSessionsUseCase |
| 117 | - | RestoreUC *usecase.RestoreSessionUseCase | 115 | + | RestoreUC *usecase.RestoreSessionUseCase |
| 118 | - | SessionStateRepo interface { | 116 | + | DeleteSessionUC *usecase.DeleteSessionUseCase |
| 119 | - | DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error | |||
| 120 | - | } | |||
| 121 | CurrentSession entity.SessionID | 117 | CurrentSession entity.SessionID | ||
| 122 | MaxListedSessions int | 118 | MaxListedSessions int | ||
| 123 | } | 119 | } | ||
| @@ -140,7 +136,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode | |||||
| 140 | ctx: ctx, | 136 | ctx: ctx, | ||
| 141 | listSessionsUC: cfg.ListSessionsUC, | 137 | listSessionsUC: cfg.ListSessionsUC, | ||
| 142 | restoreUC: cfg.RestoreUC, | 138 | restoreUC: cfg.RestoreUC, | ||
| 143 | - | sessionStateRepo: cfg.SessionStateRepo, | 139 | + | deleteSessionUC: cfg.DeleteSessionUC, |
| 144 | currentSession: cfg.CurrentSession, | 140 | currentSession: cfg.CurrentSession, | ||
| 145 | theme: theme, | 141 | theme: theme, | ||
| 146 | } | 142 | } | ||
| @@ -312,14 +308,14 @@ func (m SessionsModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||||
| 312 | 308 | ||||
| 313 | func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd { | 309 | func (m SessionsModel) deleteSession(sessionID entity.SessionID) tea.Cmd { | ||
| 314 | return func() tea.Msg { | 310 | return func() tea.Msg { | ||
| 315 | - | log := logging.FromContext(m.ctx) | 311 | + | if m.deleteSessionUC == nil { |
| 316 | - | log.Info().Str("session_id", string(sessionID)).Msg("deleting session") | 312 | + | return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("delete session use case not available")} |
| 317 | - | ||||
| 318 | - | if m.sessionStateRepo == nil { | |||
| 319 | - | return sessionDeletedMsg{sessionID: sessionID, err: fmt.Errorf("session state repo not available")} | |||
| 320 | } | 313 | } | ||
| 321 | 314 | ||||
| 322 | - | err := m.sessionStateRepo.DeleteSnapshot(m.ctx, sessionID) | 315 | + | err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{ |
| 316 | + | SessionID: sessionID, | |||
| 317 | + | CurrentSessionID: m.currentSession, | |||
| 318 | + | }) | |||
| 323 | return sessionDeletedMsg{sessionID: sessionID, err: err} | 319 | return sessionDeletedMsg{sessionID: sessionID, err: err} | ||
| 324 | } | 320 | } | ||
| 325 | } | 321 | } | ||