1package cmd
2
3import (
4 "fmt"
5
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/spf13/cobra"
8
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/bootstrap"
11 "github.com/bnema/dumber/internal/cli"
12 "github.com/bnema/dumber/internal/cli/model"
13 "github.com/bnema/dumber/internal/cli/styles"
14 "github.com/bnema/dumber/internal/infrastructure/desktop"
15 "github.com/bnema/dumber/internal/infrastructure/filesystem"
16 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
17 xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg"
18)
19
20var purgeForce bool
21
22var purgeCmd = &cobra.Command{
23 Use: "purge",
24 Short: "Remove dumber data and configuration",
25 Long: `Interactively select and remove dumber data directories and files.
26
27This can remove:
28 - Config directory
29 - Data directory (includes database)
30 - State directory
31 - Cache directory
32 - Content filter caches
33 - Desktop integration files
34 - Inactive browser sessions
35
36Use --force to remove everything without prompting.`,
37 RunE: runPurge,
38}
39
40func init() {
41 rootCmd.AddCommand(purgeCmd)
42 purgeCmd.Flags().BoolVarP(&purgeForce, "force", "f", false, "remove all items without prompting")
43}
44
45func runPurge(_ *cobra.Command, _ []string) error {
46 app := GetApp()
47 if app == nil {
48 return fmt.Errorf("app not initialized")
49 }
50
51 fsAdapter := filesystem.New()
52 profile, err := bootstrap.ResolveRuntimeProfile(app.Config)
53 if err != nil {
54 return fmt.Errorf("resolve runtime profile: %w", err)
55 }
56 xdgAdapter := xdgadapter.New(
57 profile.Mode == runtimeprofile.ModeDev,
58 bootstrap.ResolveXDGRuntimeDir(profile),
59 )
60 desktopAdapter := desktop.New()
61 purgeUC := usecase.NewPurgeDataUseCase(fsAdapter, xdgAdapter, desktopAdapter)
62
63 if purgeForce {
64 return runPurgeForce(app, purgeUC)
65 }
66
67 // Build session config from app dependencies
68 cfg := model.PurgeModelConfig{
69 ListSessionsUC: app.ListSessionsUC,
70 DeleteSessionUC: app.DeleteSessionUC,
71 }
72
73 m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg)
74 p := tea.NewProgram(m, tea.WithAltScreen())
75 _, err = p.Run()
76 return err
77}
78
79func runPurgeForce(app *cli.App, purgeUC *usecase.PurgeDataUseCase) error {
80 ctx := app.Ctx()
81 theme := app.Theme
82
83 // First, delete all inactive sessions (before we delete the database)
84 if app.ListSessionsUC != nil && app.DeleteSessionUC != nil {
85 output, err := app.ListSessionsUC.GetPurgeableSessions(ctx)
86 if err == nil && output != nil {
87 for _, s := range output.Sessions {
88 sessionID := s.Info.Session.ID
89 if delErr := app.DeleteSessionUC.Execute(ctx, usecase.DeleteSessionInput{
90 SessionID: sessionID,
91 CurrentSessionID: "", // CLI has no current session
92 }); delErr != nil {
93 fmt.Printf("%s Session %s: %v\n", theme.ErrorStyle.Render(styles.IconX), sessionID, delErr)
94 } else {
95 fmt.Printf("%s Session %s\n", theme.SuccessStyle.Render(styles.IconCheck), sessionID)
96 }
97 }
98 }
99 }
100
101 // Then purge all directories
102 out, err := purgeUC.PurgeAll(ctx)
103 if out != nil {
104 for _, r := range out.Results {
105 if r.Success {
106 fmt.Printf("%s %s\n", theme.SuccessStyle.Render(styles.IconCheck), r.Target.Path)
107 } else {
108 fmt.Printf("%s %s: %v\n", theme.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error)
109 }
110 }
111 }
112 return err
113}