Skip to content

Commit d4f8728

Browse files
authored
feat(logging): add verbose mode and debug output (#109)
1 parent 19e25b1 commit d4f8728

9 files changed

Lines changed: 211 additions & 11 deletions

File tree

src/cmd/install.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,20 @@ func init() {
5151

5252
// installSingle installs a single runtime/version
5353
func installSingle(runtimeName, version string) {
54+
ui.Debug("Installing single runtime: %s version %s", runtimeName, version)
55+
5456
provider, err := runtime.Get(runtimeName)
5557
if err != nil {
58+
ui.Debug("Provider lookup failed: %v", err)
5659
ui.Error("%v", err)
5760
ui.Info("Available runtimes: %v", runtime.List())
5861
return
5962
}
6063

64+
ui.Debug("Using provider: %s (%s)", provider.Name(), provider.DisplayName())
65+
6166
if err := provider.Install(version); err != nil {
67+
ui.Debug("Installation failed: %v", err)
6268
ui.Error("%v", err)
6369
return
6470
}

src/cmd/list.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ Examples:
3737

3838
// listAllRuntimes lists installed versions for all runtimes
3939
func listAllRuntimes() {
40+
ui.Debug("Listing installed versions for all runtimes")
41+
4042
providers := runtime.GetAll()
43+
ui.Debug("Found %d registered providers", len(providers))
4144

4245
if len(providers) == 0 {
4346
ui.Info("No runtime providers registered")
@@ -46,11 +49,14 @@ func listAllRuntimes() {
4649

4750
hasAny := false
4851
for _, provider := range providers {
52+
ui.Debug("Checking provider: %s", provider.Name())
4953
versions, err := provider.ListInstalled()
5054
if err != nil {
55+
ui.Debug("Error listing versions for %s: %v", provider.Name(), err)
5156
ui.Error(" %s: %v", provider.DisplayName(), err)
5257
continue
5358
}
59+
ui.Debug("Found %d installed versions for %s", len(versions), provider.Name())
5460

5561
if len(versions) == 0 {
5662
continue

src/cmd/root.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ import (
66
"os"
77

88
"github.com/dtvem/dtvem/src/internal/tui"
9+
"github.com/dtvem/dtvem/src/internal/ui"
910
"github.com/spf13/cobra"
1011
)
1112

13+
var verbose bool
14+
1215
var rootCmd = &cobra.Command{
1316
Use: "dtvem",
1417
Short: "Developer Tools Virtual Environment Manager",
18+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
19+
ui.SetVerbose(verbose)
20+
},
1521
}
1622

1723
func Execute() {
@@ -33,6 +39,9 @@ func init() {
3339
// Hide the completion command until we implement it
3440
rootCmd.CompletionOptions.HiddenDefaultCmd = true
3541

42+
// Add global verbose flag
43+
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Enable verbose output for debugging")
44+
3645
// Set custom usage and help functions with TUI table for commands
3746
rootCmd.SetUsageFunc(customUsage)
3847
rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {

src/cmd/shim/main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
)
2323

2424
func main() {
25+
// Check for DTVEM_VERBOSE environment variable
26+
ui.CheckVerboseEnv()
27+
2528
if err := runShim(); err != nil {
2629
fmt.Fprintf(os.Stderr, "dtvem shim error: %v\n", err)
2730
os.Exit(1)
@@ -31,22 +34,28 @@ func main() {
3134
func runShim() error {
3235
// Get the name of this shim (e.g., "python", "node", "npm")
3336
shimName := getShimName()
37+
ui.Debug("Shim invoked: %s", shimName)
38+
ui.Debug("Arguments: %v", os.Args[1:])
3439

3540
// Determine which runtime this shim belongs to
3641
runtimeName := mapShimToRuntime(shimName)
42+
ui.Debug("Mapped to runtime: %s", runtimeName)
3743

3844
// Get the runtime provider (using ShimProvider interface for minimal dependencies)
3945
provider, err := runtime.GetShimProvider(runtimeName)
4046
if err != nil {
47+
ui.Debug("Provider lookup failed: %v", err)
4148
return fmt.Errorf("runtime provider not found: %w", err)
4249
}
4350

4451
// Resolve which version to use
4552
version, err := config.ResolveVersion(runtimeName)
4653
if err != nil {
54+
ui.Debug("Version resolution failed: %v", err)
4755
// No dtvem version configured - try to fallback to system PATH
4856
return handleNoConfiguredVersion(shimName, runtimeName, provider)
4957
}
58+
ui.Debug("Resolved version: %s", version)
5059

5160
// Check if the version is installed
5261
installed, err := provider.IsInstalled(version)
@@ -55,6 +64,7 @@ func runShim() error {
5564
}
5665

5766
if !installed {
67+
ui.Debug("Version %s is not installed", version)
5868
ui.Error("%s %s is configured but not installed", provider.DisplayName(), version)
5969
ui.Info("To install, run: dtvem install %s %s", runtimeName, version)
6070
return fmt.Errorf("version not installed")
@@ -65,11 +75,13 @@ func runShim() error {
6575
if err != nil {
6676
return fmt.Errorf("could not find %s %s executable: %w", runtimeName, version, err)
6777
}
78+
ui.Debug("Base executable path: %s", execPath)
6879

6980
// If the shim name differs from the base runtime name,
7081
// we might need to adjust the executable path
7182
// (e.g., python3 -> python3, pip -> pip, npm -> npm)
7283
execPath = adjustExecutablePath(execPath, shimName, runtimeName)
84+
ui.Debug("Final executable path: %s", execPath)
7385

7486
// Check if this command should trigger a reshim after execution
7587
needsReshim := provider.ShouldReshimAfter(shimName, os.Args[1:])

src/internal/download/download.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import (
88
"os"
99
"path/filepath"
1010

11+
"github.com/dtvem/dtvem/src/internal/ui"
1112
"github.com/schollz/progressbar/v3"
1213
)
1314

1415
// File downloads a file from a URL to a destination path with a progress bar
1516
func File(url, destPath string) error {
17+
ui.Debug("Starting download: %s", url)
18+
ui.Debug("Destination: %s", destPath)
19+
1620
// Create destination directory if it doesn't exist
1721
destDir := filepath.Dir(destPath)
1822
if err := os.MkdirAll(destDir, 0755); err != nil {
@@ -27,19 +31,24 @@ func File(url, destPath string) error {
2731
defer func() { _ = out.Close() }()
2832

2933
// Make HTTP request
34+
ui.Debug("Making HTTP GET request...")
3035
resp, err := http.Get(url)
3136
if err != nil {
32-
return err
37+
ui.Debug("HTTP request failed: %v", err)
38+
return fmt.Errorf("failed to connect: %w (URL: %s)", err, url)
3339
}
3440
defer func() { _ = resp.Body.Close() }()
3541

42+
ui.Debug("HTTP response: %s", resp.Status)
43+
3644
// Check response status
3745
if resp.StatusCode != http.StatusOK {
38-
return fmt.Errorf("bad status: %s", resp.Status)
46+
return fmt.Errorf("download failed (HTTP %s): %s", resp.Status, url)
3947
}
4048

4149
// Get file size for progress bar
4250
size := resp.ContentLength
51+
ui.Debug("Content-Length: %d bytes", size)
4352

4453
// Create progress bar
4554
bar := progressbar.DefaultBytes(
@@ -50,10 +59,12 @@ func File(url, destPath string) error {
5059
// Copy data with progress bar
5160
_, err = io.Copy(io.MultiWriter(out, bar), resp.Body)
5261
if err != nil {
62+
ui.Debug("Download failed: %v", err)
5363
return err
5464
}
5565

5666
fmt.Println() // New line after progress bar
67+
ui.Debug("Download complete: %s", destPath)
5768
return nil
5869
}
5970

@@ -75,13 +86,13 @@ func FileWithProgress(url, destPath string, progress func(current, total int64))
7586
// Make HTTP request
7687
resp, err := http.Get(url)
7788
if err != nil {
78-
return err
89+
return fmt.Errorf("failed to connect: %w (URL: %s)", err, url)
7990
}
8091
defer func() { _ = resp.Body.Close() }()
8192

8293
// Check response status
8394
if resp.StatusCode != http.StatusOK {
84-
return fmt.Errorf("bad status: %s", resp.Status)
95+
return fmt.Errorf("download failed (HTTP %s): %s", resp.Status, url)
8596
}
8697

8798
// Get total size

src/internal/download/extract.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@ import (
99
"os"
1010
"path/filepath"
1111
"strings"
12+
13+
"github.com/dtvem/dtvem/src/internal/ui"
1214
)
1315

1416
// ExtractZip extracts a zip archive to a destination directory
1517
func ExtractZip(zipPath, destDir string) error {
18+
ui.Debug("Extracting ZIP: %s", zipPath)
19+
ui.Debug("Destination: %s", destDir)
20+
1621
// Open zip file
1722
reader, err := zip.OpenReader(zipPath)
1823
if err != nil {
19-
return err
24+
ui.Debug("Failed to open ZIP: %v", err)
25+
return fmt.Errorf("failed to open archive: %w (file: %s)", err, zipPath)
2026
}
2127
defer func() { _ = reader.Close() }()
2228

29+
ui.Debug("ZIP contains %d files", len(reader.File))
30+
2331
// Create destination directory
2432
if err := os.MkdirAll(destDir, 0755); err != nil {
2533
return err
@@ -32,6 +40,7 @@ func ExtractZip(zipPath, destDir string) error {
3240
}
3341
}
3442

43+
ui.Debug("ZIP extraction complete")
3544
return nil
3645
}
3746

@@ -75,17 +84,22 @@ func extractZipFile(file *zip.File, destDir string) error {
7584

7685
// ExtractTarGz extracts a tar.gz archive to a destination directory
7786
func ExtractTarGz(tarGzPath, destDir string) error {
87+
ui.Debug("Extracting tar.gz: %s", tarGzPath)
88+
ui.Debug("Destination: %s", destDir)
89+
7890
// Open tar.gz file
7991
file, err := os.Open(tarGzPath)
8092
if err != nil {
81-
return err
93+
ui.Debug("Failed to open tar.gz: %v", err)
94+
return fmt.Errorf("failed to open archive: %w (file: %s)", err, tarGzPath)
8295
}
8396
defer func() { _ = file.Close() }()
8497

8598
// Create gzip reader
8699
gzReader, err := gzip.NewReader(file)
87100
if err != nil {
88-
return err
101+
ui.Debug("Failed to create gzip reader: %v", err)
102+
return fmt.Errorf("invalid gzip archive: %w (file: %s)", err, tarGzPath)
89103
}
90104
defer func() { _ = gzReader.Close() }()
91105

@@ -98,6 +112,7 @@ func ExtractTarGz(tarGzPath, destDir string) error {
98112
}
99113

100114
// Extract each file
115+
fileCount := 0
101116
for {
102117
header, err := tarReader.Next()
103118
if err == io.EOF {
@@ -110,8 +125,10 @@ func ExtractTarGz(tarGzPath, destDir string) error {
110125
if err := extractTarFile(header, tarReader, destDir); err != nil {
111126
return fmt.Errorf("failed to extract %s: %w", header.Name, err)
112127
}
128+
fileCount++
113129
}
114130

131+
ui.Debug("tar.gz extraction complete: %d files extracted", fileCount)
115132
return nil
116133
}
117134

src/internal/ui/output.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,35 @@ import (
55
"fmt"
66
"os"
77
"strings"
8+
"time"
89

910
"github.com/fatih/color"
1011
)
1112

13+
// Environment variable values
14+
const (
15+
envTrue = "true"
16+
envFalse = "false"
17+
)
18+
1219
var (
1320
// Color functions for different message types
1421
successColor = color.New(color.FgGreen, color.Bold)
1522
errorColor = color.New(color.FgRed, color.Bold)
1623
warningColor = color.New(color.FgYellow, color.Bold)
1724
infoColor = color.New(color.FgCyan)
1825
progressColor = color.New(color.FgBlue)
26+
debugColor = color.New(color.Faint)
1927

2028
// Symbols
2129
successSymbol = "✓"
2230
errorSymbol = "✗"
2331
warningSymbol = "⚠"
2432
infoSymbol = "→"
33+
debugSymbol = "·"
34+
35+
// Verbose mode flag - controls debug output visibility
36+
verboseMode = false
2537
)
2638

2739
// Success prints a success message in green with a checkmark
@@ -54,6 +66,41 @@ func Progress(format string, args ...interface{}) {
5466
_, _ = progressColor.Printf(" %s %s\n", infoSymbol, message)
5567
}
5668

69+
// Debug prints a debug message only when verbose mode is enabled
70+
// Messages are dimmed and include a timestamp for debugging
71+
func Debug(format string, args ...interface{}) {
72+
if !verboseMode {
73+
return
74+
}
75+
message := fmt.Sprintf(format, args...)
76+
timestamp := time.Now().Format("15:04:05.000")
77+
_, _ = debugColor.Printf("%s %s %s\n", debugSymbol, timestamp, message)
78+
}
79+
80+
// Debugf is an alias for Debug (for consistency with fmt.Printf naming)
81+
func Debugf(format string, args ...interface{}) {
82+
Debug(format, args...)
83+
}
84+
85+
// SetVerbose enables or disables verbose mode
86+
func SetVerbose(enabled bool) {
87+
verboseMode = enabled
88+
}
89+
90+
// IsVerbose returns whether verbose mode is enabled
91+
func IsVerbose() bool {
92+
return verboseMode
93+
}
94+
95+
// CheckVerboseEnv checks if DTVEM_VERBOSE environment variable is set
96+
// This is useful for the shim which doesn't have access to CLI flags
97+
func CheckVerboseEnv() {
98+
val := os.Getenv("DTVEM_VERBOSE")
99+
if val == "1" || val == envTrue {
100+
verboseMode = true
101+
}
102+
}
103+
57104
// Println prints a regular message without color
58105
func Println(format string, args ...interface{}) {
59106
fmt.Printf(format+"\n", args...)
@@ -99,12 +146,12 @@ func DimText(text string) string {
99146
// - unset: prompt interactively
100147
func PromptInstall(displayName, version string) bool {
101148
// Check if running in non-interactive mode (CI/automation)
102-
if os.Getenv("DTVEM_AUTO_INSTALL") == "false" {
149+
if os.Getenv("DTVEM_AUTO_INSTALL") == envFalse {
103150
return false
104151
}
105152

106153
// If DTVEM_AUTO_INSTALL=true, auto-install without prompting
107-
if os.Getenv("DTVEM_AUTO_INSTALL") == "true" {
154+
if os.Getenv("DTVEM_AUTO_INSTALL") == envTrue {
108155
return true
109156
}
110157

@@ -134,12 +181,12 @@ func PromptInstallMissing[T any](missing []T) bool {
134181
}
135182

136183
// Check if running in non-interactive mode (CI/automation)
137-
if os.Getenv("DTVEM_AUTO_INSTALL") == "false" {
184+
if os.Getenv("DTVEM_AUTO_INSTALL") == envFalse {
138185
return false
139186
}
140187

141188
// If DTVEM_AUTO_INSTALL=true, auto-install without prompting
142-
if os.Getenv("DTVEM_AUTO_INSTALL") == "true" {
189+
if os.Getenv("DTVEM_AUTO_INSTALL") == envTrue {
143190
return true
144191
}
145192

0 commit comments

Comments
 (0)