From 3212ab968388c9fed82e619c5a3b07e4352929c7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 07:57:02 +0000 Subject: [PATCH] fix(model): use user shell for ccusage command execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed npx/bunx command execution to use the user's shell environment instead of minimal Go environment. This ensures proper PATH and environment variables are available for node package managers. - Added getUserShell() to detect user's shell from $SHELL with fallbacks - Added shellEscapeArgs() for safe shell argument escaping - Modified exec.CommandContext to use shell -c for command execution Fixes #127 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Le He --- model/ccusage_service.go | 50 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/model/ccusage_service.go b/model/ccusage_service.go index 29d2f15..97a93f8 100644 --- a/model/ccusage_service.go +++ b/model/ccusage_service.go @@ -9,6 +9,8 @@ import ( "os" "os/exec" "os/user" + "runtime" + "strings" "time" ) @@ -231,16 +233,21 @@ func (s *ccUsageService) collectData(ctx context.Context, since time.Time) (*CCU slog.Debug("Using since parameter", "sinceDate", sinceDate, "since", since) } + // Get user's shell to run command with proper environment + shell := getUserShell() + var cmd *exec.Cmd if bunxErr == nil { // Use bunx if available - cmd = exec.CommandContext(ctx, bunxPath, args...) - slog.Debug("Using bunx to collect ccusage data") + cmdStr := bunxPath + " " + shellEscapeArgs(args) + cmd = exec.CommandContext(ctx, shell, "-c", cmdStr) + slog.Debug("Using bunx to collect ccusage data", "shell", shell) } else { // Fall back to npx with --yes flag to auto-accept prompts npxArgs := append([]string{"--yes"}, args...) - cmd = exec.CommandContext(ctx, npxPath, npxArgs...) - slog.Debug("Using npx to collect ccusage data") + cmdStr := npxPath + " " + shellEscapeArgs(npxArgs) + cmd = exec.CommandContext(ctx, shell, "-c", cmdStr) + slog.Debug("Using npx to collect ccusage data", "shell", shell) } // Execute the command @@ -410,3 +417,38 @@ func (s *ccUsageService) sendData(ctx context.Context, endpoint Endpoint, data * slog.Debug("CCUsage data sent successfully", "successCount", resp.SuccessCount, "totalCount", resp.TotalCount) return nil } + +// getUserShell returns the user's shell executable path +// It checks the SHELL environment variable first, then falls back to sensible defaults +func getUserShell() string { + // Try to get the shell from environment variable + shell := os.Getenv("SHELL") + if shell != "" { + return shell + } + + // Fall back to platform-specific defaults + if runtime.GOOS == "windows" { + // On Windows, prefer PowerShell, fall back to cmd + if pwsh, err := exec.LookPath("pwsh"); err == nil { + return pwsh + } + if powershell, err := exec.LookPath("powershell"); err == nil { + return powershell + } + return "cmd" + } + + // On Unix-like systems, default to sh (POSIX shell) + return "/bin/sh" +} + +// shellEscapeArgs joins arguments with spaces and escapes them for safe shell execution +func shellEscapeArgs(args []string) string { + escaped := make([]string, len(args)) + for i, arg := range args { + // Simple shell escaping: wrap in single quotes and escape single quotes + escaped[i] = "'" + strings.ReplaceAll(arg, "'", "'\"'\"'") + "'" + } + return strings.Join(escaped, " ") +}