diff --git a/src/cmd/install.go b/src/cmd/install.go index 8b6245a..c738417 100644 --- a/src/cmd/install.go +++ b/src/cmd/install.go @@ -51,14 +51,20 @@ func init() { // installSingle installs a single runtime/version func installSingle(runtimeName, version string) { + ui.Debug("Installing single runtime: %s version %s", runtimeName, version) + provider, err := runtime.Get(runtimeName) if err != nil { + ui.Debug("Provider lookup failed: %v", err) ui.Error("%v", err) ui.Info("Available runtimes: %v", runtime.List()) return } + ui.Debug("Using provider: %s (%s)", provider.Name(), provider.DisplayName()) + if err := provider.Install(version); err != nil { + ui.Debug("Installation failed: %v", err) ui.Error("%v", err) return } diff --git a/src/cmd/list.go b/src/cmd/list.go index 6b46aee..2861779 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -37,7 +37,10 @@ Examples: // listAllRuntimes lists installed versions for all runtimes func listAllRuntimes() { + ui.Debug("Listing installed versions for all runtimes") + providers := runtime.GetAll() + ui.Debug("Found %d registered providers", len(providers)) if len(providers) == 0 { ui.Info("No runtime providers registered") @@ -46,11 +49,14 @@ func listAllRuntimes() { hasAny := false for _, provider := range providers { + ui.Debug("Checking provider: %s", provider.Name()) versions, err := provider.ListInstalled() if err != nil { + ui.Debug("Error listing versions for %s: %v", provider.Name(), err) ui.Error(" %s: %v", provider.DisplayName(), err) continue } + ui.Debug("Found %d installed versions for %s", len(versions), provider.Name()) if len(versions) == 0 { continue diff --git a/src/cmd/root.go b/src/cmd/root.go index 55519c5..4d79297 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -6,12 +6,18 @@ import ( "os" "github.com/dtvem/dtvem/src/internal/tui" + "github.com/dtvem/dtvem/src/internal/ui" "github.com/spf13/cobra" ) +var verbose bool + var rootCmd = &cobra.Command{ Use: "dtvem", Short: "Developer Tools Virtual Environment Manager", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + ui.SetVerbose(verbose) + }, } func Execute() { @@ -33,6 +39,9 @@ func init() { // Hide the completion command until we implement it rootCmd.CompletionOptions.HiddenDefaultCmd = true + // Add global verbose flag + rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Enable verbose output for debugging") + // Set custom usage and help functions with TUI table for commands rootCmd.SetUsageFunc(customUsage) rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { diff --git a/src/cmd/shim/main.go b/src/cmd/shim/main.go index a54121d..72a6ad8 100644 --- a/src/cmd/shim/main.go +++ b/src/cmd/shim/main.go @@ -22,6 +22,9 @@ import ( ) func main() { + // Check for DTVEM_VERBOSE environment variable + ui.CheckVerboseEnv() + if err := runShim(); err != nil { fmt.Fprintf(os.Stderr, "dtvem shim error: %v\n", err) os.Exit(1) @@ -31,22 +34,28 @@ func main() { func runShim() error { // Get the name of this shim (e.g., "python", "node", "npm") shimName := getShimName() + ui.Debug("Shim invoked: %s", shimName) + ui.Debug("Arguments: %v", os.Args[1:]) // Determine which runtime this shim belongs to runtimeName := mapShimToRuntime(shimName) + ui.Debug("Mapped to runtime: %s", runtimeName) // Get the runtime provider (using ShimProvider interface for minimal dependencies) provider, err := runtime.GetShimProvider(runtimeName) if err != nil { + ui.Debug("Provider lookup failed: %v", err) return fmt.Errorf("runtime provider not found: %w", err) } // Resolve which version to use version, err := config.ResolveVersion(runtimeName) if err != nil { + ui.Debug("Version resolution failed: %v", err) // No dtvem version configured - try to fallback to system PATH return handleNoConfiguredVersion(shimName, runtimeName, provider) } + ui.Debug("Resolved version: %s", version) // Check if the version is installed installed, err := provider.IsInstalled(version) @@ -55,6 +64,7 @@ func runShim() error { } if !installed { + ui.Debug("Version %s is not installed", version) ui.Error("%s %s is configured but not installed", provider.DisplayName(), version) ui.Info("To install, run: dtvem install %s %s", runtimeName, version) return fmt.Errorf("version not installed") @@ -65,11 +75,13 @@ func runShim() error { if err != nil { return fmt.Errorf("could not find %s %s executable: %w", runtimeName, version, err) } + ui.Debug("Base executable path: %s", execPath) // If the shim name differs from the base runtime name, // we might need to adjust the executable path // (e.g., python3 -> python3, pip -> pip, npm -> npm) execPath = adjustExecutablePath(execPath, shimName, runtimeName) + ui.Debug("Final executable path: %s", execPath) // Check if this command should trigger a reshim after execution needsReshim := provider.ShouldReshimAfter(shimName, os.Args[1:]) diff --git a/src/internal/download/download.go b/src/internal/download/download.go index 6c27d6a..7431062 100644 --- a/src/internal/download/download.go +++ b/src/internal/download/download.go @@ -8,11 +8,15 @@ import ( "os" "path/filepath" + "github.com/dtvem/dtvem/src/internal/ui" "github.com/schollz/progressbar/v3" ) // File downloads a file from a URL to a destination path with a progress bar func File(url, destPath string) error { + ui.Debug("Starting download: %s", url) + ui.Debug("Destination: %s", destPath) + // Create destination directory if it doesn't exist destDir := filepath.Dir(destPath) if err := os.MkdirAll(destDir, 0755); err != nil { @@ -27,19 +31,24 @@ func File(url, destPath string) error { defer func() { _ = out.Close() }() // Make HTTP request + ui.Debug("Making HTTP GET request...") resp, err := http.Get(url) if err != nil { - return err + ui.Debug("HTTP request failed: %v", err) + return fmt.Errorf("failed to connect: %w (URL: %s)", err, url) } defer func() { _ = resp.Body.Close() }() + ui.Debug("HTTP response: %s", resp.Status) + // Check response status if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("download failed (HTTP %s): %s", resp.Status, url) } // Get file size for progress bar size := resp.ContentLength + ui.Debug("Content-Length: %d bytes", size) // Create progress bar bar := progressbar.DefaultBytes( @@ -50,10 +59,12 @@ func File(url, destPath string) error { // Copy data with progress bar _, err = io.Copy(io.MultiWriter(out, bar), resp.Body) if err != nil { + ui.Debug("Download failed: %v", err) return err } fmt.Println() // New line after progress bar + ui.Debug("Download complete: %s", destPath) return nil } @@ -75,13 +86,13 @@ func FileWithProgress(url, destPath string, progress func(current, total int64)) // Make HTTP request resp, err := http.Get(url) if err != nil { - return err + return fmt.Errorf("failed to connect: %w (URL: %s)", err, url) } defer func() { _ = resp.Body.Close() }() // Check response status if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("download failed (HTTP %s): %s", resp.Status, url) } // Get total size diff --git a/src/internal/download/extract.go b/src/internal/download/extract.go index 8e7e14e..128a15c 100644 --- a/src/internal/download/extract.go +++ b/src/internal/download/extract.go @@ -9,17 +9,25 @@ import ( "os" "path/filepath" "strings" + + "github.com/dtvem/dtvem/src/internal/ui" ) // ExtractZip extracts a zip archive to a destination directory func ExtractZip(zipPath, destDir string) error { + ui.Debug("Extracting ZIP: %s", zipPath) + ui.Debug("Destination: %s", destDir) + // Open zip file reader, err := zip.OpenReader(zipPath) if err != nil { - return err + ui.Debug("Failed to open ZIP: %v", err) + return fmt.Errorf("failed to open archive: %w (file: %s)", err, zipPath) } defer func() { _ = reader.Close() }() + ui.Debug("ZIP contains %d files", len(reader.File)) + // Create destination directory if err := os.MkdirAll(destDir, 0755); err != nil { return err @@ -32,6 +40,7 @@ func ExtractZip(zipPath, destDir string) error { } } + ui.Debug("ZIP extraction complete") return nil } @@ -75,17 +84,22 @@ func extractZipFile(file *zip.File, destDir string) error { // ExtractTarGz extracts a tar.gz archive to a destination directory func ExtractTarGz(tarGzPath, destDir string) error { + ui.Debug("Extracting tar.gz: %s", tarGzPath) + ui.Debug("Destination: %s", destDir) + // Open tar.gz file file, err := os.Open(tarGzPath) if err != nil { - return err + ui.Debug("Failed to open tar.gz: %v", err) + return fmt.Errorf("failed to open archive: %w (file: %s)", err, tarGzPath) } defer func() { _ = file.Close() }() // Create gzip reader gzReader, err := gzip.NewReader(file) if err != nil { - return err + ui.Debug("Failed to create gzip reader: %v", err) + return fmt.Errorf("invalid gzip archive: %w (file: %s)", err, tarGzPath) } defer func() { _ = gzReader.Close() }() @@ -98,6 +112,7 @@ func ExtractTarGz(tarGzPath, destDir string) error { } // Extract each file + fileCount := 0 for { header, err := tarReader.Next() if err == io.EOF { @@ -110,8 +125,10 @@ func ExtractTarGz(tarGzPath, destDir string) error { if err := extractTarFile(header, tarReader, destDir); err != nil { return fmt.Errorf("failed to extract %s: %w", header.Name, err) } + fileCount++ } + ui.Debug("tar.gz extraction complete: %d files extracted", fileCount) return nil } diff --git a/src/internal/ui/output.go b/src/internal/ui/output.go index 63c34df..63103fd 100644 --- a/src/internal/ui/output.go +++ b/src/internal/ui/output.go @@ -5,10 +5,17 @@ import ( "fmt" "os" "strings" + "time" "github.com/fatih/color" ) +// Environment variable values +const ( + envTrue = "true" + envFalse = "false" +) + var ( // Color functions for different message types successColor = color.New(color.FgGreen, color.Bold) @@ -16,12 +23,17 @@ var ( warningColor = color.New(color.FgYellow, color.Bold) infoColor = color.New(color.FgCyan) progressColor = color.New(color.FgBlue) + debugColor = color.New(color.Faint) // Symbols successSymbol = "✓" errorSymbol = "✗" warningSymbol = "⚠" infoSymbol = "→" + debugSymbol = "·" + + // Verbose mode flag - controls debug output visibility + verboseMode = false ) // Success prints a success message in green with a checkmark @@ -54,6 +66,41 @@ func Progress(format string, args ...interface{}) { _, _ = progressColor.Printf(" %s %s\n", infoSymbol, message) } +// Debug prints a debug message only when verbose mode is enabled +// Messages are dimmed and include a timestamp for debugging +func Debug(format string, args ...interface{}) { + if !verboseMode { + return + } + message := fmt.Sprintf(format, args...) + timestamp := time.Now().Format("15:04:05.000") + _, _ = debugColor.Printf("%s %s %s\n", debugSymbol, timestamp, message) +} + +// Debugf is an alias for Debug (for consistency with fmt.Printf naming) +func Debugf(format string, args ...interface{}) { + Debug(format, args...) +} + +// SetVerbose enables or disables verbose mode +func SetVerbose(enabled bool) { + verboseMode = enabled +} + +// IsVerbose returns whether verbose mode is enabled +func IsVerbose() bool { + return verboseMode +} + +// CheckVerboseEnv checks if DTVEM_VERBOSE environment variable is set +// This is useful for the shim which doesn't have access to CLI flags +func CheckVerboseEnv() { + val := os.Getenv("DTVEM_VERBOSE") + if val == "1" || val == envTrue { + verboseMode = true + } +} + // Println prints a regular message without color func Println(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) @@ -99,12 +146,12 @@ func DimText(text string) string { // - unset: prompt interactively func PromptInstall(displayName, version string) bool { // Check if running in non-interactive mode (CI/automation) - if os.Getenv("DTVEM_AUTO_INSTALL") == "false" { + if os.Getenv("DTVEM_AUTO_INSTALL") == envFalse { return false } // If DTVEM_AUTO_INSTALL=true, auto-install without prompting - if os.Getenv("DTVEM_AUTO_INSTALL") == "true" { + if os.Getenv("DTVEM_AUTO_INSTALL") == envTrue { return true } @@ -134,12 +181,12 @@ func PromptInstallMissing[T any](missing []T) bool { } // Check if running in non-interactive mode (CI/automation) - if os.Getenv("DTVEM_AUTO_INSTALL") == "false" { + if os.Getenv("DTVEM_AUTO_INSTALL") == envFalse { return false } // If DTVEM_AUTO_INSTALL=true, auto-install without prompting - if os.Getenv("DTVEM_AUTO_INSTALL") == "true" { + if os.Getenv("DTVEM_AUTO_INSTALL") == envTrue { return true } diff --git a/src/internal/ui/output_test.go b/src/internal/ui/output_test.go index 5aad514..de97a4c 100644 --- a/src/internal/ui/output_test.go +++ b/src/internal/ui/output_test.go @@ -138,4 +138,88 @@ func TestHighlight_Symbols(t *testing.T) { if infoSymbol == "" { t.Error("infoSymbol should not be empty") } + if debugSymbol == "" { + t.Error("debugSymbol should not be empty") + } +} + +func TestVerboseMode(t *testing.T) { + // Test that verbose mode can be toggled + // First ensure verbose mode is off + SetVerbose(false) + if IsVerbose() { + t.Error("Verbose mode should be off after SetVerbose(false)") + } + + // Enable verbose mode + SetVerbose(true) + if !IsVerbose() { + t.Error("Verbose mode should be on after SetVerbose(true)") + } + + // Disable verbose mode again + SetVerbose(false) + if IsVerbose() { + t.Error("Verbose mode should be off after SetVerbose(false)") + } +} + +func TestCheckVerboseEnv(t *testing.T) { + // Save original state + originalVerbose := verboseMode + + // Test with DTVEM_VERBOSE=1 + SetVerbose(false) + t.Setenv("DTVEM_VERBOSE", "1") + CheckVerboseEnv() + if !IsVerbose() { + t.Error("Verbose mode should be on when DTVEM_VERBOSE=1") + } + + // Test with DTVEM_VERBOSE=true + SetVerbose(false) + t.Setenv("DTVEM_VERBOSE", "true") + CheckVerboseEnv() + if !IsVerbose() { + t.Error("Verbose mode should be on when DTVEM_VERBOSE=true") + } + + // Test with DTVEM_VERBOSE=false (should not enable) + SetVerbose(false) + t.Setenv("DTVEM_VERBOSE", "false") + CheckVerboseEnv() + if IsVerbose() { + t.Error("Verbose mode should remain off when DTVEM_VERBOSE=false") + } + + // Test with DTVEM_VERBOSE unset + SetVerbose(false) + t.Setenv("DTVEM_VERBOSE", "") + CheckVerboseEnv() + if IsVerbose() { + t.Error("Verbose mode should remain off when DTVEM_VERBOSE is empty") + } + + // Restore original state + verboseMode = originalVerbose +} + +func TestDebugOutput(t *testing.T) { + // Save original state + originalVerbose := verboseMode + + // Debug should not output anything when verbose is off + SetVerbose(false) + // We can't easily capture output in this test framework, + // but we can at least verify the function doesn't panic + Debug("test message %s", "arg") + Debugf("test message %s", "arg") + + // Enable verbose and verify Debug runs without panic + SetVerbose(true) + Debug("test message %s", "arg") + Debugf("test message %s", "arg") + + // Restore original state + verboseMode = originalVerbose } diff --git a/src/runtimes/python/provider.go b/src/runtimes/python/provider.go index 4ccc6b3..dff2aa3 100644 --- a/src/runtimes/python/provider.go +++ b/src/runtimes/python/provider.go @@ -128,6 +128,8 @@ func (p *Provider) installPipIfNeeded(version string) { } func (p *Provider) Install(version string) error { + ui.Debug("Starting Python installation for version %s", version) + // Ensure dtvem directories exist if err := config.EnsureDirectories(); err != nil { return fmt.Errorf("failed to create dtvem directories: %w", err) @@ -145,6 +147,8 @@ func (p *Provider) Install(version string) error { if err != nil { return fmt.Errorf("failed to get download URL: %w", err) } + ui.Debug("Download URL: %s", downloadURL) + ui.Debug("Archive name: %s", archiveName) // Download and extract extractDir, cleanup, err := p.downloadAndExtract(version, downloadURL, archiveName) @@ -155,13 +159,17 @@ func (p *Provider) Install(version string) error { // Determine source directory sourceDir := determineSourceDir(extractDir) + ui.Debug("Source directory: %s", sourceDir) // Get install path and move files installPath := config.RuntimeVersionPath("python", version) + ui.Debug("Install path: %s", installPath) + if err := os.MkdirAll(filepath.Dir(installPath), 0755); err != nil { return fmt.Errorf("failed to create install directory: %w", err) } + ui.Debug("Moving files from %s to %s", sourceDir, installPath) if err := os.Rename(sourceDir, installPath); err != nil { return fmt.Errorf("failed to move to install location: %w", err) }