Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions src/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions src/cmd/shim/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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:])
Expand Down
19 changes: 15 additions & 4 deletions src/internal/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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
}

Expand All @@ -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
Expand Down
23 changes: 20 additions & 3 deletions src/internal/download/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +40,7 @@ func ExtractZip(zipPath, destDir string) error {
}
}

ui.Debug("ZIP extraction complete")
return nil
}

Expand Down Expand Up @@ -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() }()

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

// Extract each file
fileCount := 0
for {
header, err := tarReader.Next()
if err == io.EOF {
Expand All @@ -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
}

Expand Down
55 changes: 51 additions & 4 deletions src/internal/ui/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,35 @@ 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)
errorColor = color.New(color.FgRed, color.Bold)
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
Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
Loading