|
9 | 9 | "os" |
10 | 10 | "os/exec" |
11 | 11 | "os/user" |
| 12 | + "runtime" |
| 13 | + "strings" |
12 | 14 | "time" |
13 | 15 | ) |
14 | 16 |
|
@@ -231,16 +233,21 @@ func (s *ccUsageService) collectData(ctx context.Context, since time.Time) (*CCU |
231 | 233 | slog.Debug("Using since parameter", "sinceDate", sinceDate, "since", since) |
232 | 234 | } |
233 | 235 |
|
| 236 | + // Get user's shell to run command with proper environment |
| 237 | + shell := getUserShell() |
| 238 | + |
234 | 239 | var cmd *exec.Cmd |
235 | 240 | if bunxErr == nil { |
236 | 241 | // Use bunx if available |
237 | | - cmd = exec.CommandContext(ctx, bunxPath, args...) |
238 | | - slog.Debug("Using bunx to collect ccusage data") |
| 242 | + cmdStr := bunxPath + " " + shellEscapeArgs(args) |
| 243 | + cmd = exec.CommandContext(ctx, shell, "-c", cmdStr) |
| 244 | + slog.Debug("Using bunx to collect ccusage data", "shell", shell) |
239 | 245 | } else { |
240 | 246 | // Fall back to npx with --yes flag to auto-accept prompts |
241 | 247 | npxArgs := append([]string{"--yes"}, args...) |
242 | | - cmd = exec.CommandContext(ctx, npxPath, npxArgs...) |
243 | | - slog.Debug("Using npx to collect ccusage data") |
| 248 | + cmdStr := npxPath + " " + shellEscapeArgs(npxArgs) |
| 249 | + cmd = exec.CommandContext(ctx, shell, "-c", cmdStr) |
| 250 | + slog.Debug("Using npx to collect ccusage data", "shell", shell) |
244 | 251 | } |
245 | 252 |
|
246 | 253 | // Execute the command |
@@ -410,3 +417,38 @@ func (s *ccUsageService) sendData(ctx context.Context, endpoint Endpoint, data * |
410 | 417 | slog.Debug("CCUsage data sent successfully", "successCount", resp.SuccessCount, "totalCount", resp.TotalCount) |
411 | 418 | return nil |
412 | 419 | } |
| 420 | + |
| 421 | +// getUserShell returns the user's shell executable path |
| 422 | +// It checks the SHELL environment variable first, then falls back to sensible defaults |
| 423 | +func getUserShell() string { |
| 424 | + // Try to get the shell from environment variable |
| 425 | + shell := os.Getenv("SHELL") |
| 426 | + if shell != "" { |
| 427 | + return shell |
| 428 | + } |
| 429 | + |
| 430 | + // Fall back to platform-specific defaults |
| 431 | + if runtime.GOOS == "windows" { |
| 432 | + // On Windows, prefer PowerShell, fall back to cmd |
| 433 | + if pwsh, err := exec.LookPath("pwsh"); err == nil { |
| 434 | + return pwsh |
| 435 | + } |
| 436 | + if powershell, err := exec.LookPath("powershell"); err == nil { |
| 437 | + return powershell |
| 438 | + } |
| 439 | + return "cmd" |
| 440 | + } |
| 441 | + |
| 442 | + // On Unix-like systems, default to sh (POSIX shell) |
| 443 | + return "/bin/sh" |
| 444 | +} |
| 445 | + |
| 446 | +// shellEscapeArgs joins arguments with spaces and escapes them for safe shell execution |
| 447 | +func shellEscapeArgs(args []string) string { |
| 448 | + escaped := make([]string, len(args)) |
| 449 | + for i, arg := range args { |
| 450 | + // Simple shell escaping: wrap in single quotes and escape single quotes |
| 451 | + escaped[i] = "'" + strings.ReplaceAll(arg, "'", "'\"'\"'") + "'" |
| 452 | + } |
| 453 | + return strings.Join(escaped, " ") |
| 454 | +} |
0 commit comments