Skip to content

Commit 63020ad

Browse files
authored
Merge pull request #113 from shelltime/claude/issue-112-20251002-0939
fix(model): implement custom lookPath for node package managers
2 parents 5d9e389 + 976c666 commit 63020ad

1 file changed

Lines changed: 103 additions & 4 deletions

File tree

model/ccusage_service.go

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os"
1010
"os/exec"
1111
"os/user"
12+
"path/filepath"
13+
"runtime"
1214
"time"
1315
)
1416

@@ -207,14 +209,111 @@ func (s *ccUsageService) getLastSyncTimestamp(ctx context.Context, endpoint Endp
207209
return lastSyncAt, nil
208210
}
209211

212+
// lookPath searches for an executable in common locations, falling back to system PATH.
213+
// This is necessary because when running as a daemon service, the PATH environment
214+
// variable may not include user-specific Node.js installation paths.
215+
func lookPath(name string) (string, error) {
216+
// First, try the standard exec.LookPath which checks system PATH
217+
if path, err := exec.LookPath(name); err == nil {
218+
return path, nil
219+
}
220+
221+
// Get the current user's home directory
222+
currentUser, err := user.Current()
223+
if err != nil {
224+
return "", fmt.Errorf("%s not found in PATH and unable to get user home directory: %w", name, err)
225+
}
226+
homeDir := currentUser.HomeDir
227+
228+
// Common installation locations for node package managers
229+
var searchPaths []string
230+
231+
if runtime.GOOS == "windows" {
232+
// Windows paths
233+
searchPaths = []string{
234+
filepath.Join(homeDir, "AppData", "Roaming", "npm", name+".cmd"),
235+
filepath.Join(homeDir, "AppData", "Roaming", "npm", name+".ps1"),
236+
filepath.Join(homeDir, ".bun", "bin", name+".exe"),
237+
filepath.Join(homeDir, ".bun", "bin", name),
238+
filepath.Join(os.Getenv("ProgramFiles"), "nodejs", name+".cmd"),
239+
filepath.Join(os.Getenv("ProgramFiles(x86)"), "nodejs", name+".cmd"),
240+
}
241+
} else {
242+
// Unix-like systems (Linux, macOS, etc.)
243+
searchPaths = []string{
244+
// User-specific npm global installations
245+
filepath.Join(homeDir, ".npm-global", "bin", name),
246+
filepath.Join(homeDir, ".npm", "bin", name),
247+
// User-specific pnpm installation
248+
filepath.Join(homeDir, ".local", "share", "pnpm", name),
249+
// Bun installation
250+
filepath.Join(homeDir, ".bun", "bin", name),
251+
// NVM (Node Version Manager) current version
252+
filepath.Join(homeDir, ".nvm", "current", "bin", name),
253+
// fnm (Fast Node Manager) installations
254+
filepath.Join(homeDir, ".local", "share", "fnm", "node-versions", "*", "installation", "bin", name),
255+
filepath.Join(homeDir, ".fnm", "node-versions", "*", "installation", "bin", name),
256+
// Homebrew on macOS (Intel)
257+
filepath.Join("/usr/local/bin", name),
258+
// Homebrew on macOS (Apple Silicon)
259+
filepath.Join("/opt/homebrew/bin", name),
260+
// Common system paths
261+
filepath.Join("/usr/bin", name),
262+
filepath.Join("/bin", name),
263+
}
264+
265+
// Add Node.js versions from nvm if NVM_DIR is set
266+
if nvmDir := os.Getenv("NVM_DIR"); nvmDir != "" {
267+
// Try to find the default/current version
268+
searchPaths = append(searchPaths,
269+
filepath.Join(nvmDir, "current", "bin", name),
270+
filepath.Join(nvmDir, "versions", "node", "*", "bin", name),
271+
)
272+
}
273+
274+
// Add Node.js versions from fnm if FNM_DIR is set
275+
if fnmDir := os.Getenv("FNM_DIR"); fnmDir != "" {
276+
searchPaths = append(searchPaths,
277+
filepath.Join(fnmDir, "node-versions", "*", "installation", "bin", name),
278+
)
279+
}
280+
}
281+
282+
// Search each path
283+
for _, path := range searchPaths {
284+
// Handle glob patterns (like nvm versions)
285+
if matches, err := filepath.Glob(path); err == nil && len(matches) > 0 {
286+
// Use the last match, which is likely to be the latest version
287+
// since Glob returns a sorted list.
288+
path = matches[len(matches)-1]
289+
}
290+
291+
// Check if the file exists and is executable
292+
if info, err := os.Stat(path); err == nil {
293+
if !info.IsDir() {
294+
// On Unix-like systems, check if it's executable
295+
if runtime.GOOS != "windows" {
296+
if info.Mode()&0111 == 0 {
297+
continue
298+
}
299+
}
300+
slog.Debug("Found executable", "name", name, "path", path)
301+
return path, nil
302+
}
303+
}
304+
}
305+
306+
return "", fmt.Errorf("%s not found in PATH or common installation locations", name)
307+
}
308+
210309
// collectData collects usage data using bunx or npx ccusage command
211310
func (s *ccUsageService) collectData(ctx context.Context, since time.Time) (*CCUsageData, error) {
212-
// Check if bunx exists
213-
bunxPath, bunxErr := exec.LookPath("bunx")
214-
npxPath, npxErr := exec.LookPath("npx")
311+
// Check if bunx exists using custom lookPath that checks common installation locations
312+
bunxPath, bunxErr := lookPath("bunx")
313+
npxPath, npxErr := lookPath("npx")
215314

216315
if bunxErr != nil && npxErr != nil {
217-
return nil, fmt.Errorf("neither bunx nor npx found in system PATH")
316+
return nil, fmt.Errorf("neither bunx nor npx found in system PATH or common installation locations")
218317
}
219318

220319
// Build command arguments

0 commit comments

Comments
 (0)