diff options
Diffstat (limited to 'internal/askcli/taskexec.go')
| -rw-r--r-- | internal/askcli/taskexec.go | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/internal/askcli/taskexec.go b/internal/askcli/taskexec.go new file mode 100644 index 0000000..9c9aa69 --- /dev/null +++ b/internal/askcli/taskexec.go @@ -0,0 +1,129 @@ +package askcli + +import ( + "context" + "errors" + "fmt" + "io" + "os/exec" + "path/filepath" + "strings" +) + +type binaryFinder func() (string, error) + +type repoTopLevelDetector func(context.Context) (string, error) + +type commandRunner func(context.Context, string, []string, io.Reader, io.Writer, io.Writer) error + +type Executor struct { + commandName string + findBinary binaryFinder + detectRepoRoot repoTopLevelDetector + runCommand commandRunner +} + +func NewExecutor(commandName string) Executor { + return Executor{ + commandName: strings.TrimSpace(commandName), + findBinary: findTaskBinary, + detectRepoRoot: detectRepoRoot, + runCommand: runTaskCommand, + } +} + +func (e Executor) taskArgs(repoRoot string, args []string) ([]string, error) { + projectName, err := projectNameFromRoot(repoRoot) + if err != nil { + return nil, err + } + return append([]string{"project:" + projectName, "+agent"}, args...), nil +} + +func (e Executor) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) { + executor := normalizeExecutor(e) + taskPath, err := executor.findBinary() + if err != nil { + return 1, fmt.Errorf("%s: task binary lookup failed: %w", executor.label(), err) + } + repoRoot, err := executor.detectRepoRoot(ctx) + if err != nil { + return 1, fmt.Errorf("%s: must be run inside a git repository: %w", executor.label(), err) + } + taskArgs, err := executor.taskArgs(repoRoot, args) + if err != nil { + return 1, fmt.Errorf("%s: %w", executor.label(), err) + } + if err := executor.runCommand(ctx, taskPath, taskArgs, stdin, stdout, stderr); err != nil { + return exitCodeFor(err), nil + } + return 0, nil +} + +func (e Executor) label() string { + label := strings.TrimSpace(e.commandName) + if label == "" { + return "ask" + } + return label +} + +func normalizeExecutor(e Executor) Executor { + if e.commandName == "" { + e.commandName = "ask" + } + if e.findBinary == nil { + e.findBinary = findTaskBinary + } + if e.detectRepoRoot == nil { + e.detectRepoRoot = detectRepoRoot + } + if e.runCommand == nil { + e.runCommand = runTaskCommand + } + return e +} + +func projectNameFromRoot(repoRoot string) (string, error) { + projectName := filepath.Base(strings.TrimSpace(repoRoot)) + if projectName == "" || projectName == "." || projectName == string(filepath.Separator) { + return "", fmt.Errorf("could not derive project name from git root %q", repoRoot) + } + return projectName, nil +} + +func findTaskBinary() (string, error) { + path, err := exec.LookPath("task") + if err != nil { + return "", fmt.Errorf("task binary 'task' not found in PATH; install task and retry") + } + return path, nil +} + +func detectRepoRoot(ctx context.Context) (string, error) { + out, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output() + if err != nil { + return "", fmt.Errorf("must be run inside a git repository so project name can be derived") + } + root := strings.TrimSpace(string(out)) + if root == "" { + return "", fmt.Errorf("git returned an empty repository root") + } + return root, nil +} + +func runTaskCommand(ctx context.Context, name string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd.Run() +} + +func exitCodeFor(err error) int { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return exitErr.ExitCode() + } + return 1 +} |
