-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpath.go
More file actions
200 lines (173 loc) · 5.58 KB
/
path.go
File metadata and controls
200 lines (173 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Package path provides utilities for PATH environment variable manipulation
package path
import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/CodingWithCalvin/dtvem.cli/src/internal/constants"
)
// IsInPath checks if a directory is in the system PATH
func IsInPath(dir string) bool {
pathEnv := os.Getenv("PATH")
// Get the path separator for this OS
separator := ":"
if runtime.GOOS == constants.OSWindows {
separator = ";"
}
// Split PATH into individual directories
paths := strings.Split(pathEnv, separator)
// Normalize the directory path for comparison
dir = filepath.Clean(dir)
for _, p := range paths {
p = filepath.Clean(p)
if p == dir {
return true
}
}
return false
}
// IsDtvemShimsPath reports whether path looks like a dtvem shims directory.
// It matches the standard installation patterns:
// - <anything>/dtvem/shims (e.g., ~/.local/share/dtvem/shims under XDG_DATA_HOME)
// - <anything>/.dtvem/shims (the default Windows/macOS layout, leading dot)
//
// Comparison is case-insensitive on Windows. Custom DTVEM_ROOT layouts whose
// final two components don't match these patterns are not detected.
func IsDtvemShimsPath(path string) bool {
if path == "" {
return false
}
cleaned := filepath.Clean(path)
leaf := filepath.Base(cleaned)
parent := filepath.Base(filepath.Dir(cleaned))
leafEq := func(a, b string) bool { return a == b }
if runtime.GOOS == constants.OSWindows {
leafEq = strings.EqualFold
}
if !leafEq(leaf, "shims") {
return false
}
return leafEq(parent, "dtvem") || leafEq(parent, ".dtvem")
}
// FindStaleShimsEntries scans pathEntries for entries that look like dtvem
// shims directories but do not match currentShimsDir. The returned slice
// preserves the order of appearance in pathEntries and has the original
// (un-cleaned) entry strings, so callers can match them against registry
// or config-file content.
//
// Comparison against currentShimsDir is case-insensitive on Windows.
func FindStaleShimsEntries(pathEntries []string, currentShimsDir string) []string {
if currentShimsDir == "" {
return nil
}
currentClean := filepath.Clean(currentShimsDir)
var stale []string
for _, entry := range pathEntries {
trimmed := strings.TrimSpace(entry)
if trimmed == "" {
continue
}
if !IsDtvemShimsPath(trimmed) {
continue
}
entryClean := filepath.Clean(trimmed)
if runtime.GOOS == constants.OSWindows {
if strings.EqualFold(entryClean, currentClean) {
continue
}
} else {
if entryClean == currentClean {
continue
}
}
stale = append(stale, entry)
}
return stale
}
// SplitPath splits the PATH environment variable using the OS-appropriate separator.
func SplitPath(pathEnv string) []string {
separator := ":"
if runtime.GOOS == constants.OSWindows {
separator = ";"
}
return strings.Split(pathEnv, separator)
}
// ShimsDir returns the path to the shims directory
// This replicates the root directory logic from config package to avoid circular dependencies.
// Must stay in sync with config.getRootDir().
func ShimsDir() string {
// Check for DTVEM_ROOT environment variable first (overrides all)
if root := os.Getenv("DTVEM_ROOT"); root != "" {
return filepath.Join(root, "shims")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
// On Linux, respect XDG Base Directory specification
if runtime.GOOS == "linux" {
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
return filepath.Join(xdgDataHome, "dtvem", "shims")
}
// XDG default: ~/.local/share
return filepath.Join(home, ".local", "share", "dtvem", "shims")
}
// On macOS and Windows, use XDG_DATA_HOME if explicitly set (opt-in)
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
return filepath.Join(xdgDataHome, "dtvem", "shims")
}
// Default for macOS and Windows: ~/.dtvem
return filepath.Join(home, ".dtvem", "shims")
}
// LookPathExcludingShims searches for an executable in PATH, excluding dtvem's shims directory.
// This prevents detecting our own shims as "system" installations during migration detection.
// Returns the full path to the executable, or empty string if not found.
func LookPathExcludingShims(execName string) string {
// Get the shims directory to exclude it from search
shimsDir := ShimsDir()
// Get PATH environment variable
pathEnv := os.Getenv("PATH")
if pathEnv == "" {
return ""
}
// Split PATH into directories
pathDirs := filepath.SplitList(pathEnv)
// Search each directory
for _, dir := range pathDirs {
// Skip the dtvem shims directory (case-insensitive on Windows)
if strings.EqualFold(dir, shimsDir) {
continue
}
// Try to find the executable in this directory
candidatePath := findExecutableInDir(dir, execName)
if candidatePath != "" {
return candidatePath
}
}
return ""
}
// findExecutableInDir looks for an executable with the given name in a directory.
// On Windows, it tries .exe, .cmd, .bat extensions.
// On Unix, it checks if the file exists and has execute permission.
func findExecutableInDir(dir, execName string) string {
if runtime.GOOS == constants.OSWindows {
// Windows: try .exe, .cmd, .bat extensions
for _, ext := range []string{".exe", ".cmd", ".bat"} {
candidate := filepath.Join(dir, execName+ext)
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
return candidate
}
}
} else {
// Unix: check if file exists and is executable
candidate := filepath.Join(dir, execName)
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
// Check if executable (has execute permission)
if info.Mode()&0111 != 0 {
return candidate
}
}
}
return ""
}