Skip to content

Commit 2d89875

Browse files
committed
feat: improve UX with CLI args, auto-detection, and init command
- Accept directories as CLI arguments: git-scope ~/code ~/work - Auto-detect common dev directories when no config exists - Add 'git-scope init' for interactive setup wizard - Scan current directory with 'git-scope scan .' - Bump version to 0.1.1
1 parent 5cfb5b0 commit 2d89875

File tree

2 files changed

+226
-8
lines changed

2 files changed

+226
-8
lines changed

cmd/git-scope/main.go

Lines changed: 182 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
package main
22

33
import (
4+
"bufio"
45
"flag"
56
"fmt"
67
"log"
78
"os"
9+
"path/filepath"
10+
"strings"
811

912
"github.com/Bharath-code/git-scope/internal/config"
1013
"github.com/Bharath-code/git-scope/internal/scan"
1114
"github.com/Bharath-code/git-scope/internal/tui"
1215
)
1316

14-
const version = "0.1.0"
17+
const version = "0.1.1"
1518

1619
func usage() {
1720
fmt.Fprintf(os.Stderr, `git-scope v%s — A fast TUI to see the status of all git repositories
1821
1922
Usage:
20-
git-scope [command]
23+
git-scope [command] [directories...]
2124
2225
Commands:
2326
(default) Launch TUI dashboard
24-
scan Scan configured roots and print repos (JSON)
25-
tui Launch TUI dashboard (same as default)
27+
scan Scan and print repos (JSON)
28+
init Create config file interactively
2629
help Show this help
2730
31+
Examples:
32+
git-scope # Scan configured dirs or current dir
33+
git-scope ~/code ~/work # Scan specific directories
34+
git-scope scan . # Scan current directory (JSON)
35+
git-scope init # Setup config interactively
36+
2837
Flags:
2938
`, version)
3039
flag.PrintDefaults()
@@ -35,10 +44,21 @@ func main() {
3544
configPath := flag.String("config", config.DefaultConfigPath(), "Path to config file")
3645
flag.Parse()
3746

38-
// Default command is "tui" if no args provided
39-
cmd := "tui"
40-
if flag.NArg() >= 1 {
41-
cmd = flag.Arg(0)
47+
args := flag.Args()
48+
cmd := ""
49+
dirs := []string{}
50+
51+
// Parse command and directories
52+
if len(args) >= 1 {
53+
switch args[0] {
54+
case "scan", "tui", "help", "init", "-h", "--help":
55+
cmd = args[0]
56+
dirs = args[1:]
57+
default:
58+
// Assume it's a directory
59+
cmd = "tui"
60+
dirs = args
61+
}
4262
}
4363

4464
// Handle help early
@@ -47,12 +67,26 @@ func main() {
4767
return
4868
}
4969

70+
// Handle init command
71+
if cmd == "init" {
72+
runInit()
73+
return
74+
}
75+
5076
// Load configuration
5177
cfg, err := config.Load(*configPath)
5278
if err != nil {
5379
log.Fatalf("failed to load config: %v", err)
5480
}
5581

82+
// Override roots if directories provided via CLI
83+
if len(dirs) > 0 {
84+
cfg.Roots = expandDirs(dirs)
85+
} else if !config.ConfigExists(*configPath) {
86+
// No config file and no CLI dirs - use smart defaults
87+
cfg.Roots = getSmartDefaults()
88+
}
89+
5690
switch cmd {
5791
case "scan":
5892
repos, err := scan.ScanRoots(cfg.Roots, cfg.Ignore)
@@ -74,3 +108,143 @@ func main() {
74108
os.Exit(1)
75109
}
76110
}
111+
112+
// expandDirs converts relative paths and ~ to absolute paths
113+
func expandDirs(dirs []string) []string {
114+
result := make([]string, 0, len(dirs))
115+
for _, d := range dirs {
116+
if d == "." {
117+
if cwd, err := os.Getwd(); err == nil {
118+
result = append(result, cwd)
119+
}
120+
} else if strings.HasPrefix(d, "~/") {
121+
if home, err := os.UserHomeDir(); err == nil {
122+
result = append(result, filepath.Join(home, d[2:]))
123+
}
124+
} else if filepath.IsAbs(d) {
125+
result = append(result, d)
126+
} else {
127+
if abs, err := filepath.Abs(d); err == nil {
128+
result = append(result, abs)
129+
}
130+
}
131+
}
132+
return result
133+
}
134+
135+
// getSmartDefaults returns directories that likely contain git repos
136+
func getSmartDefaults() []string {
137+
home, err := os.UserHomeDir()
138+
if err != nil {
139+
cwd, _ := os.Getwd()
140+
return []string{cwd}
141+
}
142+
143+
// Common developer directories to check
144+
candidates := []string{
145+
filepath.Join(home, "code"),
146+
filepath.Join(home, "Code"),
147+
filepath.Join(home, "projects"),
148+
filepath.Join(home, "Projects"),
149+
filepath.Join(home, "dev"),
150+
filepath.Join(home, "Dev"),
151+
filepath.Join(home, "work"),
152+
filepath.Join(home, "Work"),
153+
filepath.Join(home, "repos"),
154+
filepath.Join(home, "Repos"),
155+
filepath.Join(home, "src"),
156+
filepath.Join(home, "Developer"),
157+
filepath.Join(home, "Documents", "GitHub"),
158+
filepath.Join(home, "Desktop", "projects"),
159+
}
160+
161+
found := []string{}
162+
for _, dir := range candidates {
163+
if info, err := os.Stat(dir); err == nil && info.IsDir() {
164+
found = append(found, dir)
165+
}
166+
}
167+
168+
// If no common dirs found, use current directory
169+
if len(found) == 0 {
170+
cwd, _ := os.Getwd()
171+
return []string{cwd}
172+
}
173+
174+
return found
175+
}
176+
177+
// runInit creates a config file interactively
178+
func runInit() {
179+
configPath := config.DefaultConfigPath()
180+
181+
fmt.Println("git-scope init — Setup your configuration")
182+
fmt.Println()
183+
184+
// Check if config already exists
185+
if config.ConfigExists(configPath) {
186+
fmt.Printf("Config file already exists at: %s\n", configPath)
187+
fmt.Print("Overwrite? [y/N]: ")
188+
reader := bufio.NewReader(os.Stdin)
189+
answer, _ := reader.ReadString('\n')
190+
answer = strings.TrimSpace(strings.ToLower(answer))
191+
if answer != "y" && answer != "yes" {
192+
fmt.Println("Aborted.")
193+
return
194+
}
195+
}
196+
197+
reader := bufio.NewReader(os.Stdin)
198+
199+
// Get directories
200+
fmt.Println("Enter directories to scan for git repos (one per line, empty line to finish):")
201+
fmt.Println("Examples: ~/code, ~/projects, ~/work")
202+
fmt.Println()
203+
204+
dirs := []string{}
205+
for {
206+
fmt.Print("> ")
207+
line, _ := reader.ReadString('\n')
208+
line = strings.TrimSpace(line)
209+
if line == "" {
210+
break
211+
}
212+
dirs = append(dirs, line)
213+
}
214+
215+
if len(dirs) == 0 {
216+
// Suggest detected directories
217+
detected := getSmartDefaults()
218+
if len(detected) > 0 {
219+
fmt.Println("\nNo directories entered. Detected these on your system:")
220+
for _, d := range detected {
221+
fmt.Printf(" - %s\n", d)
222+
}
223+
fmt.Print("\nUse these? [Y/n]: ")
224+
answer, _ := reader.ReadString('\n')
225+
answer = strings.TrimSpace(strings.ToLower(answer))
226+
if answer == "" || answer == "y" || answer == "yes" {
227+
dirs = detected
228+
} else {
229+
fmt.Println("No directories configured. Run 'git-scope init' again to set up.")
230+
return
231+
}
232+
}
233+
}
234+
235+
// Get editor
236+
fmt.Print("\nEditor command (default: code): ")
237+
editor, _ := reader.ReadString('\n')
238+
editor = strings.TrimSpace(editor)
239+
if editor == "" {
240+
editor = "code"
241+
}
242+
243+
// Create config
244+
if err := config.CreateConfig(configPath, dirs, editor); err != nil {
245+
log.Fatalf("Failed to create config: %v", err)
246+
}
247+
248+
fmt.Printf("\n✅ Config created at: %s\n", configPath)
249+
fmt.Println("\nRun 'git-scope' to launch the dashboard!")
250+
}

internal/config/config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,47 @@ func DefaultConfigPath() string {
8282
}
8383
return filepath.Join(home, ".config", "git-scope", "config.yml")
8484
}
85+
86+
// ConfigExists checks if a config file exists at the given path
87+
func ConfigExists(path string) bool {
88+
_, err := os.Stat(path)
89+
return err == nil
90+
}
91+
92+
// CreateConfig creates a new config file at the given path
93+
func CreateConfig(path string, roots []string, editor string) error {
94+
// Ensure directory exists
95+
dir := filepath.Dir(path)
96+
if err := os.MkdirAll(dir, 0755); err != nil {
97+
return fmt.Errorf("create config dir: %w", err)
98+
}
99+
100+
cfg := &Config{
101+
Roots: roots,
102+
Ignore: []string{
103+
"node_modules",
104+
".next",
105+
"dist",
106+
"build",
107+
"target",
108+
".venv",
109+
"vendor",
110+
},
111+
Editor: editor,
112+
}
113+
114+
data, err := yaml.Marshal(cfg)
115+
if err != nil {
116+
return fmt.Errorf("marshal config: %w", err)
117+
}
118+
119+
// Add header comment
120+
content := "# git-scope configuration\n# Edit this file to customize scanning behavior\n\n" + string(data)
121+
122+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
123+
return fmt.Errorf("write config: %w", err)
124+
}
125+
126+
return nil
127+
}
128+

0 commit comments

Comments
 (0)