| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- // Package runner starts the claude subprocess with a pre-built environment,
- // transparently forwards stdio and signals, and returns claude's exit code as
- // cc-switch's own.
- //
- // The contract (spec: provider-launch):
- // - stdin/stdout/stderr are inherited verbatim.
- // - SIGINT/SIGTERM/SIGHUP are forwarded to the child; cc-switch waits for
- // the child to finish and then returns.
- // - Exit code passthrough: normal exit -> child's code; signal termination
- // -> 128 + signum (POSIX convention).
- // - We never mutate os.Environ; the supplied childEnv is the whole
- // environment the child sees.
- package runner
- import (
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "github.com/kotoyuuko/cc-switch-cli/internal/config"
- )
- // ResolveClaudePath returns the absolute path of the claude executable to
- // launch. Precedence: cfg.ClaudePath (if non-empty) > exec.LookPath("claude").
- // If neither succeeds, an error is returned that suggests the `config set
- // claude-path` escape hatch.
- func ResolveClaudePath(cfg config.Config) (string, error) {
- if cfg.ClaudePath != "" {
- expanded, err := config.ExpandUser(cfg.ClaudePath)
- if err != nil {
- return "", fmt.Errorf("expand claude_path %q: %w", cfg.ClaudePath, err)
- }
- abs, err := filepath.Abs(expanded)
- if err != nil {
- return "", fmt.Errorf("resolve claude_path %q: %w", cfg.ClaudePath, err)
- }
- info, err := os.Stat(abs)
- if err != nil {
- return "", fmt.Errorf("claude_path %q: %w", abs, err)
- }
- if !info.Mode().IsRegular() {
- return "", fmt.Errorf("claude_path %q is not a regular file", abs)
- }
- if info.Mode().Perm()&0o111 == 0 {
- return "", fmt.Errorf("claude_path %q is not executable", abs)
- }
- return abs, nil
- }
- p, err := exec.LookPath("claude")
- if err != nil {
- return "", fmt.Errorf(
- "claude not found in PATH and no claude_path configured; try `cc-switch config set claude-path <path>`",
- )
- }
- return p, nil
- }
- // ExitError is returned by Run when the child was killed by a signal. Its
- // ExitCode already follows the 128+signum convention.
- type ExitError struct {
- Signal os.Signal // nil if exited normally
- ExitCode int
- }
- func (e *ExitError) Error() string {
- if e.Signal != nil {
- return fmt.Sprintf("claude terminated by signal %v (exit code %d)", e.Signal, e.ExitCode)
- }
- return fmt.Sprintf("claude exited with code %d", e.ExitCode)
- }
- // asExitError converts a cmd.Wait error into an ExitCode value consistent with
- // the spec: normal exit -> err.ExitCode(); signal kill -> 128+signum.
- func asExitError(err error) (int, error) {
- if err == nil {
- return 0, nil
- }
- var exitErr *exec.ExitError
- if !errors.As(err, &exitErr) {
- // Something went wrong that isn't the child reporting an exit (e.g.
- // we couldn't wait on it at all). Surface the raw error and a
- // generic 1 so callers can still print something useful.
- return 1, err
- }
- if code := exitErr.ExitCode(); code >= 0 {
- return code, nil
- }
- // code == -1 means the child was killed by a signal. Look up which.
- if sig, ok := extractSignal(exitErr); ok {
- return 128 + int(sig), nil
- }
- return 1, err
- }
- // Run starts claudePath with the given env and args and returns its exit code.
- // It installs the signal-forwarding handler as configured per platform
- // (see runner_unix.go / runner_other.go).
- func Run(claudePath string, childEnv []string, args []string) (int, error) {
- cmd := exec.Command(claudePath, args...)
- cmd.Env = childEnv
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Start(); err != nil {
- return 1, fmt.Errorf("start claude: %w", err)
- }
- stopForward := startSignalForwarding(cmd.Process)
- defer stopForward()
- return asExitError(cmd.Wait())
- }
|