Download internal/shell/shell.go from SaylorTwift/crush: direct link, hf CLI and curl.
- Browser
- Download file 8.03 kB
-
https://huggingface.co/SaylorTwift/crush/resolve/main/internal/shell/shell.go
- Command line
-
hf download hf://SaylorTwift/crush/internal/shell/shell.go
-
curl -L -o shell.go https://huggingface.co/SaylorTwift/crush/resolve/main/internal/shell/shell.go
8.03 kB
| // Package shell provides cross-platform shell execution capabilities. | |
| // | |
| // This package provides Shell instances for executing commands with their own | |
| // working directory and environment. Each shell execution is independent. | |
| // | |
| // WINDOWS COMPATIBILITY: | |
| // This implementation provides POSIX shell emulation (mvdan.cc/sh/v3) even on | |
| // Windows. Commands should use forward slashes (/) as path separators to work | |
| // correctly on all platforms. | |
| package shell | |
| import ( | |
| "bytes" | |
| "context" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "os" | |
| "slices" | |
| "strings" | |
| "sync" | |
| "github.com/charmbracelet/x/exp/slice" | |
| "mvdan.cc/sh/v3/interp" | |
| "mvdan.cc/sh/v3/syntax" | |
| ) | |
| // ShellType represents the type of shell to use | |
| type ShellType int | |
| const ( | |
| ShellTypePOSIX ShellType = iota | |
| ShellTypeCmd | |
| ShellTypePowerShell | |
| ) | |
| // CrushEnvMarkers returns a fresh slice of the environment variables that | |
| // Crush unconditionally sets on every shell it spawns — both the interactive | |
| // bash tool's [Shell] and the hook runner's [Run] calls. Tools that want to | |
| // detect "am I being invoked by an AI agent?" can check any of these. | |
| // Keeping them in one place guarantees the two shell surfaces cannot drift. | |
| // A fresh slice is returned on every call so callers may append freely. | |
| func CrushEnvMarkers() []string { | |
| return []string{ | |
| "CRUSH=1", | |
| "AGENT=crush", | |
| "AI_AGENT=crush", | |
| } | |
| } | |
| // Logger interface for optional logging | |
| type Logger interface { | |
| InfoPersist(msg string, keysAndValues ...any) | |
| } | |
| // noopLogger is a logger that does nothing | |
| type noopLogger struct{} | |
| func (noopLogger) InfoPersist(msg string, keysAndValues ...any) {} | |
| // BlockFunc is a function that determines if a command should be blocked | |
| type BlockFunc func(args []string) bool | |
| // Shell provides cross-platform shell execution with optional state persistence | |
| type Shell struct { | |
| env []string | |
| cwd string | |
| mu sync.Mutex | |
| logger Logger | |
| blockFuncs []BlockFunc | |
| } | |
| // Options for creating a new shell | |
| type Options struct { | |
| WorkingDir string | |
| Env []string | |
| Logger Logger | |
| BlockFuncs []BlockFunc | |
| } | |
| // NewShell creates a new shell instance with the given options | |
| func NewShell(opts *Options) *Shell { | |
| if opts == nil { | |
| opts = &Options{} | |
| } | |
| cwd := opts.WorkingDir | |
| if cwd == "" { | |
| cwd, _ = os.Getwd() | |
| } | |
| env := opts.Env | |
| if env == nil { | |
| env = os.Environ() | |
| } | |
| // Strip herdr pane-ownership vars so subprocesses (including test | |
| // binaries and nested crush instances) can't attach to or release | |
| // the parent pane's agent authority. | |
| env = withoutHerdrEnv(env) | |
| // Allow tools to detect execution by Crush. | |
| env = append(env, CrushEnvMarkers()...) | |
| logger := opts.Logger | |
| if logger == nil { | |
| logger = noopLogger{} | |
| } | |
| return &Shell{ | |
| cwd: cwd, | |
| env: env, | |
| logger: logger, | |
| blockFuncs: opts.BlockFuncs, | |
| } | |
| } | |
| // Exec executes a command in the shell | |
| func (s *Shell) Exec(ctx context.Context, command string) (string, string, error) { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| return s.exec(ctx, command) | |
| } | |
| // ExecStream executes a command in the shell with streaming output to provided writers | |
| func (s *Shell) ExecStream(ctx context.Context, command string, stdout, stderr io.Writer) error { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| return s.execStream(ctx, command, stdout, stderr) | |
| } | |
| // GetWorkingDir returns the current working directory | |
| func (s *Shell) GetWorkingDir() string { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| return s.cwd | |
| } | |
| // SetWorkingDir sets the working directory | |
| func (s *Shell) SetWorkingDir(dir string) error { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| // Verify the directory exists | |
| if _, err := os.Stat(dir); err != nil { | |
| return fmt.Errorf("directory does not exist: %w", err) | |
| } | |
| s.cwd = dir | |
| return nil | |
| } | |
| // GetEnv returns a copy of the environment variables | |
| func (s *Shell) GetEnv() []string { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| env := make([]string, len(s.env)) | |
| copy(env, s.env) | |
| return env | |
| } | |
| // SetEnv sets an environment variable | |
| func (s *Shell) SetEnv(key, value string) { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| // Update or add the environment variable | |
| keyPrefix := key + "=" | |
| for i, env := range s.env { | |
| if strings.HasPrefix(env, keyPrefix) { | |
| s.env[i] = keyPrefix + value | |
| return | |
| } | |
| } | |
| s.env = append(s.env, keyPrefix+value) | |
| } | |
| // SetBlockFuncs sets the command block functions for the shell | |
| func (s *Shell) SetBlockFuncs(blockFuncs []BlockFunc) { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| s.blockFuncs = blockFuncs | |
| } | |
| // CommandsBlocker creates a BlockFunc that blocks exact command matches | |
| func CommandsBlocker(cmds []string) BlockFunc { | |
| bannedSet := make(map[string]struct{}) | |
| for _, cmd := range cmds { | |
| bannedSet[cmd] = struct{}{} | |
| } | |
| return func(args []string) bool { | |
| if len(args) == 0 { | |
| return false | |
| } | |
| _, ok := bannedSet[args[0]] | |
| return ok | |
| } | |
| } | |
| // ArgumentsBlocker creates a BlockFunc that blocks specific subcommand | |
| func ArgumentsBlocker(cmd string, args []string, flags []string) BlockFunc { | |
| return func(parts []string) bool { | |
| if len(parts) == 0 || parts[0] != cmd { | |
| return false | |
| } | |
| argParts, flagParts := splitArgsFlags(parts[1:]) | |
| if len(argParts) < len(args) || len(flagParts) < len(flags) { | |
| return false | |
| } | |
| argsMatch := slices.Equal(argParts[:len(args)], args) | |
| flagsMatch := slice.IsSubset(flags, flagParts) | |
| return argsMatch && flagsMatch | |
| } | |
| } | |
| func splitArgsFlags(parts []string) (args []string, flags []string) { | |
| args = make([]string, 0, len(parts)) | |
| flags = make([]string, 0, len(parts)) | |
| for _, part := range parts { | |
| if strings.HasPrefix(part, "-") { | |
| // Extract flag name before '=' if present | |
| flag := part | |
| if before, _, ok := strings.Cut(part, "="); ok { | |
| flag = before | |
| } | |
| flags = append(flags, flag) | |
| } else { | |
| args = append(args, part) | |
| } | |
| } | |
| return args, flags | |
| } | |
| // newInterp creates a new interpreter with the current shell state. A nil | |
| // stdin is equivalent to an empty input stream. | |
| func (s *Shell) newInterp(stdin io.Reader, stdout, stderr io.Writer) (*interp.Runner, error) { | |
| return newRunner(s.cwd, s.env, stdin, stdout, stderr, s.blockFuncs) | |
| } | |
| // updateShellFromRunner updates the shell from the interpreter after execution. | |
| func (s *Shell) updateShellFromRunner(runner *interp.Runner) { | |
| s.cwd = runner.Dir | |
| s.env = s.env[:0] | |
| for name, vr := range runner.Vars { | |
| if vr.Exported { | |
| s.env = append(s.env, name+"="+vr.Str) | |
| } | |
| } | |
| } | |
| // execCommon is the shared implementation for executing commands | |
| func (s *Shell) execCommon(ctx context.Context, command string, stdout, stderr io.Writer) (err error) { | |
| var runner *interp.Runner | |
| defer func() { | |
| if r := recover(); r != nil { | |
| err = fmt.Errorf("command execution panic: %v", r) | |
| } | |
| if runner != nil { | |
| s.updateShellFromRunner(runner) | |
| } | |
| s.logger.InfoPersist("command finished", "command", command, "err", err) | |
| }() | |
| line, err := syntax.NewParser().Parse(strings.NewReader(command), "") | |
| if err != nil { | |
| return fmt.Errorf("could not parse command: %w", err) | |
| } | |
| runner, err = s.newInterp(nil, stdout, stderr) | |
| if err != nil { | |
| return fmt.Errorf("could not run command: %w", err) | |
| } | |
| err = runner.Run(ctx, line) | |
| return err | |
| } | |
| // exec executes commands using a cross-platform shell interpreter. | |
| func (s *Shell) exec(ctx context.Context, command string) (string, string, error) { | |
| var stdout, stderr bytes.Buffer | |
| err := s.execCommon(ctx, command, &stdout, &stderr) | |
| return stdout.String(), stderr.String(), err | |
| } | |
| // execStream executes commands using POSIX shell emulation with streaming output | |
| func (s *Shell) execStream(ctx context.Context, command string, stdout, stderr io.Writer) error { | |
| return s.execCommon(ctx, command, stdout, stderr) | |
| } | |
| // IsInterrupt checks if an error is due to interruption | |
| func IsInterrupt(err error) bool { | |
| return errors.Is(err, context.Canceled) || | |
| errors.Is(err, context.DeadlineExceeded) | |
| } | |
| // ExitCode extracts the exit code from an error | |
| func ExitCode(err error) int { | |
| if err == nil { | |
| return 0 | |
| } | |
| if exitErr, ok := errors.AsType[interp.ExitStatus](err); ok { | |
| return int(exitErr) | |
| } | |
| return 1 | |
| } | |