Skip to content
Merged
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
107 changes: 103 additions & 4 deletions model/ccusage_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"time"
)

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

// lookPath searches for an executable in common locations, falling back to system PATH.
// This is necessary because when running as a daemon service, the PATH environment
// variable may not include user-specific Node.js installation paths.
func lookPath(name string) (string, error) {
// First, try the standard exec.LookPath which checks system PATH
if path, err := exec.LookPath(name); err == nil {
return path, nil
}

// Get the current user's home directory
currentUser, err := user.Current()
if err != nil {
return "", fmt.Errorf("%s not found in PATH and unable to get user home directory: %w", name, err)
}
homeDir := currentUser.HomeDir

// Common installation locations for node package managers
var searchPaths []string

if runtime.GOOS == "windows" {
// Windows paths
searchPaths = []string{
filepath.Join(homeDir, "AppData", "Roaming", "npm", name+".cmd"),
filepath.Join(homeDir, "AppData", "Roaming", "npm", name+".ps1"),
filepath.Join(homeDir, ".bun", "bin", name+".exe"),
filepath.Join(homeDir, ".bun", "bin", name),
filepath.Join(os.Getenv("ProgramFiles"), "nodejs", name+".cmd"),
filepath.Join(os.Getenv("ProgramFiles(x86)"), "nodejs", name+".cmd"),
}
} else {
// Unix-like systems (Linux, macOS, etc.)
searchPaths = []string{
// User-specific npm global installations
filepath.Join(homeDir, ".npm-global", "bin", name),
filepath.Join(homeDir, ".npm", "bin", name),
// User-specific pnpm installation
filepath.Join(homeDir, ".local", "share", "pnpm", name),
// Bun installation
filepath.Join(homeDir, ".bun", "bin", name),
// NVM (Node Version Manager) current version
filepath.Join(homeDir, ".nvm", "current", "bin", name),
// fnm (Fast Node Manager) installations
filepath.Join(homeDir, ".local", "share", "fnm", "node-versions", "*", "installation", "bin", name),
filepath.Join(homeDir, ".fnm", "node-versions", "*", "installation", "bin", name),
// Homebrew on macOS (Intel)
filepath.Join("/usr/local/bin", name),
// Homebrew on macOS (Apple Silicon)
filepath.Join("/opt/homebrew/bin", name),
// Common system paths
filepath.Join("/usr/bin", name),
filepath.Join("/bin", name),
}

// Add Node.js versions from nvm if NVM_DIR is set
if nvmDir := os.Getenv("NVM_DIR"); nvmDir != "" {
// Try to find the default/current version
searchPaths = append(searchPaths,
filepath.Join(nvmDir, "current", "bin", name),
filepath.Join(nvmDir, "versions", "node", "*", "bin", name),
)
}

// Add Node.js versions from fnm if FNM_DIR is set
if fnmDir := os.Getenv("FNM_DIR"); fnmDir != "" {
searchPaths = append(searchPaths,
filepath.Join(fnmDir, "node-versions", "*", "installation", "bin", name),
)
}
}

// Search each path
for _, path := range searchPaths {
// Handle glob patterns (like nvm versions)
if matches, err := filepath.Glob(path); err == nil && len(matches) > 0 {
// Use the last match, which is likely to be the latest version
// since Glob returns a sorted list.
path = matches[len(matches)-1]
}
Comment thread
AnnatarHe marked this conversation as resolved.

// Check if the file exists and is executable
if info, err := os.Stat(path); err == nil {
if !info.IsDir() {
// On Unix-like systems, check if it's executable
if runtime.GOOS != "windows" {
if info.Mode()&0111 == 0 {
continue
}
}
slog.Debug("Found executable", "name", name, "path", path)
return path, nil
}
}
}

return "", fmt.Errorf("%s not found in PATH or common installation locations", name)
}

// collectData collects usage data using bunx or npx ccusage command
func (s *ccUsageService) collectData(ctx context.Context, since time.Time) (*CCUsageData, error) {
// Check if bunx exists
bunxPath, bunxErr := exec.LookPath("bunx")
npxPath, npxErr := exec.LookPath("npx")
// Check if bunx exists using custom lookPath that checks common installation locations
bunxPath, bunxErr := lookPath("bunx")
npxPath, npxErr := lookPath("npx")

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

// Build command arguments
Expand Down
Loading