diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 773b5e6..3b08f08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -205,13 +205,22 @@ For detailed guidelines and examples, see [Commit Convention Guide](docs/COMMIT_ To add support for a new runtime (e.g., Ruby): 1. Create `src/runtimes/ruby/provider.go` -2. Implement the `runtime.Provider` interface: +2. Implement the `runtime.Provider` interface (which embeds `runtime.ShimProvider`): ```go type Provider struct {} + // ShimProvider methods (used by the shim binary) func (p *Provider) Name() string { return "ruby" } func (p *Provider) DisplayName() string { return "Ruby" } - // ... implement all other methods + func (p *Provider) Shims() []string { return []string{"ruby", "gem", "bundle"} } + func (p *Provider) ExecutablePath(version string) (string, error) { ... } + func (p *Provider) IsInstalled(version string) (bool, error) { ... } + func (p *Provider) ShouldReshimAfter(shimName string, args []string) bool { ... } + + // Provider-only methods (used by CLI, can include net/http) + func (p *Provider) Install(version string) error { ... } + func (p *Provider) ListAvailable() ([]runtime.AvailableVersion, error) { ... } + // ... implement remaining methods ``` 3. Register the provider in the `init()` function: ```go @@ -219,14 +228,23 @@ To add support for a new runtime (e.g., Ruby): runtime.Register(NewProvider()) } ``` -4. Import the provider in `src/main.go`: +4. Import the provider in **both** files: + - `src/main.go` (for the CLI) + - `src/cmd/shim/main.go` (for the shim) ```go _ "github.com/dtvem/dtvem/src/runtimes/ruby" ``` -5. Implement the `Shims()` method to define which executables this runtime provides -6. Implement the `ShouldReshimAfter()` method for automatic reshim detection -7. Add tests using the provider contract harness -8. Update README.md to reflect the new runtime support +5. Add tests using the provider contract harness +6. Update README.md to reflect the new runtime support + +### ShimProvider vs Provider + +The `Provider` interface embeds `ShimProvider`. This separation exists for performance: + +- **ShimProvider**: Minimal interface used by the shim binary. Methods here should NOT import `net/http` or other heavy dependencies. +- **Provider**: Full interface for CLI operations. Can use any dependencies. + +Go's linker eliminates unused code, so keeping `ShimProvider` methods dependency-free results in a smaller, faster shim binary (~4.5MB vs ~10MB). ## Testing Guidelines diff --git a/README.md b/README.md index c2a93bb..62dc837 100644 --- a/README.md +++ b/README.md @@ -208,25 +208,23 @@ Reads .dtvem/runtimes.json → {"python": "3.11.0"} Executes: ~/.dtvem/versions/python/3.11.0/bin/python --version ``` -### Auto-Install Missing Versions +### Missing Version Handling -When you run a command that requires a version that isn't installed yet, dtvem will offer to install it automatically: +When you run a command that requires a version that isn't installed yet, dtvem will tell you how to install it: ```bash $ python --version -⚠ Python 3.11.0 is not installed -→ Install it now? [Y/n]: y -→ Installing Python 3.11.0... -✓ Successfully installed Python 3.11.0 -Python 3.11.0 +✗ Python 3.11.0 is configured but not installed +→ To install, run: dtvem install python 3.11.0 ``` -This works seamlessly with bulk install - just clone a repo with a `.dtvem/runtimes.json` file and start using it. The first time you run a command, dtvem will offer to install the required version. - -**Environment Variable Control:** -- `DTVEM_AUTO_INSTALL=false` - Disable auto-install in CI/automation -- `DTVEM_AUTO_INSTALL=true` - Auto-install without prompting -- Default - Interactive prompt +**Bulk Install from Config:** +Clone a repo with a `.dtvem/runtimes.json` file and run: +```bash +dtvem install # Prompts for confirmation +dtvem install --yes # Skip confirmation +``` +This installs all configured versions at once. ### System PATH Fallback @@ -369,10 +367,6 @@ System-wide default versions (JSON format): ### Environment Variables - `DTVEM_ROOT`: Override default dtvem directory (default: `~/.dtvem`) -- `DTVEM_AUTO_INSTALL`: Control auto-install behavior when a configured version is missing - - `false` - Disable auto-install prompts (useful for CI/automation) - - `true` - Auto-install without prompting - - Not set - Interactive prompt (default) ## Supported Runtimes @@ -462,31 +456,49 @@ Remove this installation? [y/N]: y ## Plugin System -dtvem uses an embedded plugin system with a registry pattern. Each runtime provider owns its shim definitions (executables it provides), making the architecture fully decentralized: +dtvem uses an embedded plugin system with a registry pattern. Each runtime provider owns its shim definitions (executables it provides), making the architecture fully decentralized. + +### Provider Interfaces + +Providers implement two interfaces - a lightweight `ShimProvider` for the shim binary, and the full `Provider` for the CLI: ```go -// Each runtime implements the Provider interface (19 methods) -type Provider interface { +// ShimProvider - minimal interface used by the shim binary +// Excludes heavy dependencies like net/http to keep shim fast and small +type ShimProvider interface { Name() string // e.g., "python" DisplayName() string // e.g., "Python" Shims() []string // e.g., ["python", "python3", "pip", "pip3"] + ExecutablePath(version string) (string, error) + IsInstalled(version string) (bool, error) ShouldReshimAfter(shimName, args) bool // e.g., detect "npm install -g" +} + +// Provider - full interface for CLI operations (embeds ShimProvider) +type Provider interface { + ShimProvider Install(version string) error + ListAvailable() ([]AvailableVersion, error) // requires net/http DetectInstalled() ([]DetectedVersion, error) - // ... 13 more methods + // ... more methods } +``` + +**Why two interfaces?** The shim binary runs on every `node`, `python`, `npm` command. By using `ShimProvider`, Go's linker eliminates unused code (like `net/http`), reducing shim size from ~10MB to ~4.5MB and improving startup time. +### Adding a New Runtime + +1. Create `src/runtimes//provider.go` +2. Implement the `Provider` interface (which includes all `ShimProvider` methods) +3. Import in `src/main.go` and `src/cmd/shim/main.go` + +```go // Runtimes auto-register on startup func init() { runtime.Register(NewProvider()) } ``` -**Adding a new runtime is as simple as:** -1. Create `src/runtimes//provider.go` -2. Implement the `Provider` interface (including `Shims()` and `ShouldReshimAfter()`) -3. Import in `src/main.go` - **That's it!** The shim mappings are automatically registered via the `Shims()` method, and automatic reshim detection works via `ShouldReshimAfter()`. No need to modify central mapping files. ## Development diff --git a/src/cmd/shim/main.go b/src/cmd/shim/main.go index 6bf10f2..13c5356 100644 --- a/src/cmd/shim/main.go +++ b/src/cmd/shim/main.go @@ -34,8 +34,8 @@ func runShim() error { // Determine which runtime this shim belongs to runtimeName := mapShimToRuntime(shimName) - // Get the runtime provider - provider, err := runtime.Get(runtimeName) + // Get the runtime provider (using ShimProvider interface for minimal dependencies) + provider, err := runtime.GetShimProvider(runtimeName) if err != nil { return fmt.Errorf("runtime provider not found: %w", err) } @@ -47,35 +47,22 @@ func runShim() error { return handleNoConfiguredVersion(shimName, runtimeName, provider) } - // Get the path to the actual executable - execPath, err := provider.ExecutablePath(version) + // Check if the version is installed + installed, err := provider.IsInstalled(version) if err != nil { - // Check if the version is not installed - if strings.Contains(err.Error(), "not found") { - // Offer to install it - if shouldAutoInstall(provider.DisplayName(), version) { - ui.Info("Installing %s %s...", provider.DisplayName(), version) - - if installErr := provider.Install(version); installErr != nil { - ui.Error("Failed to install %s %s: %v", provider.DisplayName(), version, installErr) - ui.Info("Please install manually with: dtvem install %s %s", runtimeName, version) - return fmt.Errorf("installation failed") - } + return fmt.Errorf("could not check if %s %s is installed: %w", runtimeName, version, err) + } - ui.Success("Successfully installed %s %s", provider.DisplayName(), version) + if !installed { + 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") + } - // Retry getting the executable path - execPath, err = provider.ExecutablePath(version) - if err != nil { - return fmt.Errorf("could not find %s %s after installation: %w", runtimeName, version, err) - } - } else { - ui.Info("To install manually, run: dtvem install %s %s", runtimeName, version) - return fmt.Errorf("installation declined") - } - } else { - return fmt.Errorf("could not find %s %s: %w", runtimeName, version, err) - } + // Get the path to the actual executable + execPath, err := provider.ExecutablePath(version) + if err != nil { + return fmt.Errorf("could not find %s %s executable: %w", runtimeName, version, err) } // If the shim name differs from the base runtime name, @@ -109,7 +96,7 @@ func runShim() error { // handleNoConfiguredVersion handles the case when no dtvem version is configured // It attempts to fallback to system PATH or prompts for installation -func handleNoConfiguredVersion(shimName, runtimeName string, provider runtime.Provider) error { +func handleNoConfiguredVersion(shimName, runtimeName string, provider runtime.ShimProvider) error { // Try to find the executable deeper in PATH (system installation) systemPath := findInSystemPath(shimName) @@ -194,12 +181,6 @@ func findInSystemPath(execName string) string { return "" } -// shouldAutoInstall prompts the user to install a missing version. -// Delegates to ui.PromptInstall for consistent behavior across CLI and shim. -func shouldAutoInstall(displayName, version string) bool { - return ui.PromptInstall(displayName, version) -} - // getShimName returns the name of this shim binary func getShimName() string { shimPath := os.Args[0] @@ -216,8 +197,8 @@ func getShimName() string { // This queries all registered providers for their shims, eliminating the need // for a central hardcoded mapping. func mapShimToRuntime(shimName string) string { - // Get all registered providers - providers := runtime.GetAll() + // Get all registered providers (using ShimProvider interface) + providers := runtime.GetAllShimProviders() // Check each provider's shims for an exact match first for _, provider := range providers { diff --git a/src/internal/runtime/provider.go b/src/internal/runtime/provider.go index 55b0eae..5f80684 100644 --- a/src/internal/runtime/provider.go +++ b/src/internal/runtime/provider.go @@ -1,8 +1,10 @@ // Package runtime defines the provider interface and registry for runtime managers package runtime -// Provider defines the interface that all runtime providers must implement -type Provider interface { +// ShimProvider defines the minimal interface needed by the shim executable. +// This interface excludes heavy operations like Install() and ListAvailable() +// that require net/http and other dependencies not needed for shim execution. +type ShimProvider interface { // Name returns the name of the runtime (e.g., "python", "node", "ruby") Name() string @@ -14,6 +16,25 @@ type Provider interface { // Node.js returns ["node", "npm", "npx"] Shims() []string + // ExecutablePath returns the path to the main executable for a given version + // For example, for Python 3.11.0, this might return "/path/to/python3.11" + ExecutablePath(version string) (string, error) + + // IsInstalled checks if a specific version is installed + IsInstalled(version string) (bool, error) + + // ShouldReshimAfter checks if the given command should trigger a reshim. + // Returns true if the command installs or uninstalls global packages that add/remove executables. + // The shimName parameter indicates which shim was invoked (e.g., "npm", "pip") + // The args parameter contains the command arguments (e.g., ["install", "-g", "typescript"]) + ShouldReshimAfter(shimName string, args []string) bool +} + +// Provider defines the full interface that all runtime providers must implement. +// It embeds ShimProvider and adds operations that require heavier dependencies. +type Provider interface { + ShimProvider + // Install downloads and installs a specific version of the runtime Install(version string) error @@ -27,13 +48,6 @@ type Provider interface { // This might query online sources or use cached data ListAvailable() ([]AvailableVersion, error) - // ExecutablePath returns the path to the main executable for a given version - // For example, for Python 3.11.0, this might return "/path/to/python3.11" - ExecutablePath(version string) (string, error) - - // IsInstalled checks if a specific version is installed - IsInstalled(version string) (bool, error) - // InstallPath returns the installation directory for a given version InstallPath(version string) (string, error) @@ -72,10 +86,4 @@ type Provider interface { // Used to provide help text to users if automatic package installation fails // Returns empty string if the runtime doesn't support global packages ManualPackageInstallCommand(packages []string) string - - // ShouldReshimAfter checks if the given command should trigger a reshim. - // Returns true if the command installs or uninstalls global packages that add/remove executables. - // The shimName parameter indicates which shim was invoked (e.g., "npm", "pip") - // The args parameter contains the command arguments (e.g., ["install", "-g", "typescript"]) - ShouldReshimAfter(shimName string, args []string) bool } diff --git a/src/internal/runtime/registry.go b/src/internal/runtime/registry.go index 687499a..db9c319 100644 --- a/src/internal/runtime/registry.go +++ b/src/internal/runtime/registry.go @@ -132,3 +132,20 @@ func Unregister(name string) error { func GetRegistry() *Registry { return globalRegistry } + +// GetShimProvider retrieves a provider as ShimProvider from the global registry. +// This returns only the minimal interface needed by the shim. +func GetShimProvider(name string) (ShimProvider, error) { + return globalRegistry.Get(name) +} + +// GetAllShimProviders returns all providers as ShimProviders from the global registry. +// This returns only the minimal interface needed by the shim. +func GetAllShimProviders() []ShimProvider { + providers := globalRegistry.GetAll() + shimProviders := make([]ShimProvider, len(providers)) + for i, p := range providers { + shimProviders[i] = p + } + return shimProviders +}