Skip to content

Commit 05cc88c

Browse files
committed
feat(cli): add dingo clean command
Add new `dingo clean` command to remove build artifacts: - Remove shadow build directory (default: build/) - --all flag to also remove .dmap/ source maps - --verbose flag for detailed cleanup progress - --dry-run flag to preview what would be deleted - Reports freed disk space in human-readable format - Respects dingo.toml output directory configuration Bump version to 0.12.0
1 parent fdf1ae8 commit 05cc88c

4 files changed

Lines changed: 216 additions & 1 deletion

File tree

cmd/dingo/clean.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/charmbracelet/lipgloss"
9+
"github.com/spf13/cobra"
10+
11+
"github.com/MadAppGang/dingo/pkg/config"
12+
)
13+
14+
// cleanCmd creates the "dingo clean" command
15+
func cleanCmd() *cobra.Command {
16+
var (
17+
all bool
18+
verbose bool
19+
dryRun bool
20+
)
21+
22+
cmd := &cobra.Command{
23+
Use: "clean",
24+
Short: "Remove build artifacts and generated files",
25+
Long: `Clean removes the shadow build directory and optionally source maps.
26+
27+
By default, only the build directory is removed (default: build/).
28+
Use --all to also remove source maps (.dmap/).
29+
30+
Examples:
31+
dingo clean # Remove build directory
32+
dingo clean --all # Remove build and source maps
33+
dingo clean --dry-run # Show what would be removed
34+
dingo clean --verbose # Show detailed cleanup progress`,
35+
RunE: func(cmd *cobra.Command, args []string) error {
36+
return runClean(all, verbose, dryRun)
37+
},
38+
}
39+
40+
cmd.Flags().BoolVarP(&all, "all", "a", false, "Also remove .dmap/ directory")
41+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose output")
42+
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be deleted without deleting")
43+
44+
return cmd
45+
}
46+
47+
// runClean executes the clean operation
48+
func runClean(all, verbose, dryRun bool) error {
49+
// Define styles
50+
successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#5AF78E"))
51+
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#6C7086"))
52+
warningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#F7DC6F"))
53+
54+
// Detect workspace root
55+
cwd, err := os.Getwd()
56+
if err != nil {
57+
return fmt.Errorf("failed to get current directory: %w", err)
58+
}
59+
60+
workspaceRoot, err := DetectWorkspaceRoot(cwd)
61+
if err != nil {
62+
// If no workspace root found, use current directory
63+
workspaceRoot = cwd
64+
}
65+
66+
// Load configuration
67+
cfg, err := config.LoadFromDir(workspaceRoot)
68+
if err != nil {
69+
// Use defaults if no config or config invalid
70+
cfg = config.DefaultConfig()
71+
}
72+
73+
// Determine directories to clean
74+
outDir := cfg.Build.OutDir
75+
if outDir == "" {
76+
outDir = "build"
77+
}
78+
shadowPath := filepath.Join(workspaceRoot, outDir)
79+
dmapPath := filepath.Join(workspaceRoot, ".dmap")
80+
81+
// Track cleanup stats
82+
var cleanedCount int
83+
var totalSize int64
84+
85+
// Clean shadow/build directory
86+
if err := cleanDirectory(shadowPath, verbose, dryRun, &cleanedCount, &totalSize, successStyle, dimStyle, warningStyle); err != nil {
87+
return err
88+
}
89+
90+
// Clean .dmap directory if --all flag is set
91+
if all {
92+
if err := cleanDirectory(dmapPath, verbose, dryRun, &cleanedCount, &totalSize, successStyle, dimStyle, warningStyle); err != nil {
93+
return err
94+
}
95+
}
96+
97+
// Print summary
98+
printCleanSummary(cleanedCount, totalSize, dryRun, successStyle, dimStyle)
99+
100+
return nil
101+
}
102+
103+
// cleanDirectory removes a directory and reports progress
104+
func cleanDirectory(path string, verbose, dryRun bool, count *int, size *int64, successStyle, dimStyle, warningStyle lipgloss.Style) error {
105+
// Check if directory exists
106+
info, err := os.Stat(path)
107+
if err != nil {
108+
if os.IsNotExist(err) {
109+
if verbose {
110+
fmt.Printf(" %s %s %s\n",
111+
dimStyle.Render("-"),
112+
path,
113+
dimStyle.Render("(does not exist)"))
114+
}
115+
return nil
116+
}
117+
return fmt.Errorf("failed to stat %s: %w", path, err)
118+
}
119+
120+
if !info.IsDir() {
121+
return fmt.Errorf("%s is not a directory", path)
122+
}
123+
124+
// Calculate size
125+
dirSize, err := calculateDirSize(path)
126+
if err != nil {
127+
// Non-fatal: warn but continue
128+
if verbose {
129+
fmt.Printf(" %s Could not calculate size for %s: %v\n",
130+
warningStyle.Render("!"),
131+
path,
132+
err)
133+
}
134+
dirSize = 0
135+
}
136+
137+
// Remove or report
138+
if dryRun {
139+
fmt.Printf("Would remove: %s %s\n",
140+
path,
141+
dimStyle.Render("("+formatSize(dirSize)+")"))
142+
} else {
143+
if verbose {
144+
fmt.Printf("Removing: %s\n", path)
145+
}
146+
if err := os.RemoveAll(path); err != nil {
147+
return fmt.Errorf("failed to remove %s: %w", path, err)
148+
}
149+
if verbose {
150+
fmt.Printf(" %s Removed %s %s\n",
151+
successStyle.Render("OK"),
152+
path,
153+
dimStyle.Render("("+formatSize(dirSize)+")"))
154+
}
155+
}
156+
157+
*count++
158+
*size += dirSize
159+
return nil
160+
}
161+
162+
// calculateDirSize calculates total size of directory contents
163+
func calculateDirSize(path string) (int64, error) {
164+
var size int64
165+
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
166+
if err != nil {
167+
return err
168+
}
169+
if !info.IsDir() {
170+
size += info.Size()
171+
}
172+
return nil
173+
})
174+
return size, err
175+
}
176+
177+
// formatSize formats bytes in human-readable form
178+
func formatSize(bytes int64) string {
179+
const unit = 1024
180+
if bytes < unit {
181+
return fmt.Sprintf("%d B", bytes)
182+
}
183+
div, exp := int64(unit), 0
184+
for n := bytes / unit; n >= unit; n /= unit {
185+
div *= unit
186+
exp++
187+
}
188+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
189+
}
190+
191+
// printCleanSummary prints cleanup summary with styled output
192+
func printCleanSummary(count int, size int64, dryRun bool, successStyle, dimStyle lipgloss.Style) {
193+
if dryRun {
194+
fmt.Printf("\n%s\n", dimStyle.Render("Dry run - no files deleted"))
195+
return
196+
}
197+
198+
if count == 0 {
199+
fmt.Println(dimStyle.Render("No build artifacts found"))
200+
return
201+
}
202+
203+
suffix := "y"
204+
if count != 1 {
205+
suffix = "ies"
206+
}
207+
208+
fmt.Printf("\n%s Cleaned %d director%s (%s freed)\n",
209+
successStyle.Render("OK"),
210+
count,
211+
suffix,
212+
formatSize(size))
213+
}

cmd/dingo/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ and other quality-of-life features while maintaining 100% Go ecosystem compatibi
5656
rootCmd.AddCommand(goCmd()) // dingo go - transpile only
5757
rootCmd.AddCommand(lintCmd()) // dingo lint - run linter
5858
rootCmd.AddCommand(fmtCmd()) // dingo fmt - format files
59+
rootCmd.AddCommand(cleanCmd()) // dingo clean - remove build artifacts
5960
rootCmd.AddCommand(newVersionCmd()) // dingo version - show version with mascot and update check
6061
rootCmd.AddCommand(updateCmd()) // dingo update - auto-update from GitHub
6162
rootCmd.AddCommand(mascotCmd())

pkg/ui/styles.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ func PrintDingoHelp(version string) {
431431
{"go", "Transpile Dingo to Go only (no compilation)"},
432432
{"lint", "Run Dingo linter on source files"},
433433
{"fmt", "Format Dingo source files"},
434+
{"clean", "Remove build artifacts and generated files"},
434435
{"version", "Print the version number of Dingo"},
435436
{"help", "Help about any command"},
436437
}

pkg/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
package version
33

44
// Version is the current version of Dingo
5-
const Version = "0.11.6"
5+
const Version = "0.12.0"

0 commit comments

Comments
 (0)