diff --git a/cmd/root/root.go b/cmd/root/root.go index 9f2993396..c1f2ee1b2 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -204,25 +204,26 @@ func (e RuntimeError) Unwrap() error { return e.Err } -// isFirstRun checks if this is the first time cagent is being run -// It creates a marker file in the user's config directory +// isFirstRun checks if this is the first time cagent is being run. +// It atomically creates a marker file in the user's config directory +// using os.O_EXCL to avoid a race condition when multiple processes +// start concurrently. func isFirstRun() bool { configDir := paths.GetConfigDir() markerFile := filepath.Join(configDir, ".cagent_first_run") - // Check if marker file exists - if _, err := os.Stat(markerFile); err == nil { - return false // File exists, not first run - } - - // Create marker file to indicate this run has happened + // Ensure the config directory exists before trying to create the marker file if err := os.MkdirAll(configDir, 0o755); err != nil { - return false // Can't create config dir, assume not first run + slog.Warn("Failed to create config directory for first run marker", "error", err) + return false } - if err := os.WriteFile(markerFile, []byte(""), 0o644); err != nil { - return false // Can't create marker file, assume not first run + // Atomically create the marker file. If it already exists, OpenFile returns an error. + f, err := os.OpenFile(markerFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return false // File already exists or other error, not first run } + f.Close() - return true // Successfully created marker, this is first run + return true } diff --git a/pkg/app/app.go b/pkg/app/app.go index c49eed8b7..12893bea5 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -446,8 +446,18 @@ func (a *App) RunWithMessage(ctx context.Context, cancel context.CancelFunc, msg } func (a *App) RunBangCommand(ctx context.Context, command string) { - out, _ := exec.CommandContext(ctx, "/bin/sh", "-c", command).CombinedOutput() - a.events <- runtime.ShellOutput("$ " + command + "\n" + string(out)) + command = strings.TrimSpace(command) + if command == "" { + a.events <- runtime.ShellOutput("Error: empty command") + return + } + + out, err := exec.CommandContext(ctx, "/bin/sh", "-c", command).CombinedOutput() + output := "$ " + command + "\n" + string(out) + if err != nil && len(out) == 0 { + output = "$ " + command + "\nError: " + err.Error() + } + a.events <- runtime.ShellOutput(output) } func (a *App) Subscribe(ctx context.Context, program *tea.Program) { diff --git a/pkg/tools/builtin/shell.go b/pkg/tools/builtin/shell.go index 91978074d..83c1d2300 100644 --- a/pkg/tools/builtin/shell.go +++ b/pkg/tools/builtin/shell.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "os/exec" "runtime" @@ -137,6 +138,10 @@ func statusToString(status int32) string { } func (h *shellHandler) RunShell(ctx context.Context, params RunShellArgs) (*tools.ToolCallResult, error) { + if strings.TrimSpace(params.Cmd) == "" { + return tools.ResultError("Error: empty command"), nil + } + timeout := h.timeout if params.Timeout > 0 { timeout = time.Duration(params.Timeout) * time.Second @@ -152,6 +157,8 @@ func (h *shellHandler) RunShell(ctx context.Context, params RunShellArgs) (*tool return h.sandbox.runCommand(timeoutCtx, ctx, params.Cmd, cwd, timeout), nil } + slog.Debug("Executing native shell command", "command", params.Cmd, "cwd", cwd) + return h.runNativeCommand(timeoutCtx, ctx, params.Cmd, cwd, timeout), nil }