|
9 | 9 | "os" |
10 | 10 | "os/exec" |
11 | 11 | "os/user" |
| 12 | + "path/filepath" |
| 13 | + "runtime" |
12 | 14 | "time" |
13 | 15 | ) |
14 | 16 |
|
@@ -207,14 +209,111 @@ func (s *ccUsageService) getLastSyncTimestamp(ctx context.Context, endpoint Endp |
207 | 209 | return lastSyncAt, nil |
208 | 210 | } |
209 | 211 |
|
| 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 | + |
210 | 309 | // collectData collects usage data using bunx or npx ccusage command |
211 | 310 | 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") |
215 | 314 |
|
216 | 315 | 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") |
218 | 317 | } |
219 | 318 |
|
220 | 319 | // Build command arguments |
|
0 commit comments