go-process/program.go
Claude a09ca4f408
chore: migrate to dappco.re vanity import path
Module path: forge.lthn.ai/core/go-process -> dappco.re/go/core/process

Import path updates:
- forge.lthn.ai/core/go-log -> dappco.re/go/core/log
- forge.lthn.ai/core/go-io -> dappco.re/go/core/io
- forge.lthn.ai/core/go-ws -> dappco.re/go/core/ws
- forge.lthn.ai/core/go-process (self) -> dappco.re/go/core/process
- forge.lthn.ai/core/api left as-is (not yet migrated)

Local replace directives added until vanity URL server is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:49:08 +00:00

68 lines
2 KiB
Go

package process
import (
"bytes"
"context"
"fmt"
"os/exec"
"strings"
coreerr "dappco.re/go/core/log"
)
// ErrProgramNotFound is returned when Find cannot locate the binary on PATH.
// Callers may use errors.Is to detect this condition.
var ErrProgramNotFound = coreerr.E("", "program: binary not found in PATH", nil)
// Program represents a named executable located on the system PATH.
// Create one with a Name, call Find to resolve its path, then Run or RunDir.
type Program struct {
// Name is the binary name (e.g. "go", "node", "git").
Name string
// Path is the absolute path resolved by Find.
// If empty, Run and RunDir fall back to Name for OS PATH resolution.
Path string
}
// Find resolves the program's absolute path using exec.LookPath.
// Returns ErrProgramNotFound (wrapped) if the binary is not on PATH.
func (p *Program) Find() error {
if p.Name == "" {
return coreerr.E("Program.Find", "program name is empty", nil)
}
path, err := exec.LookPath(p.Name)
if err != nil {
return coreerr.E("Program.Find", fmt.Sprintf("%q: not found in PATH", p.Name), ErrProgramNotFound)
}
p.Path = path
return nil
}
// Run executes the program with args in the current working directory.
// Returns trimmed combined stdout+stderr output and any error.
func (p *Program) Run(ctx context.Context, args ...string) (string, error) {
return p.RunDir(ctx, "", args...)
}
// RunDir executes the program with args in dir.
// Returns trimmed combined stdout+stderr output and any error.
// If dir is empty, the process inherits the caller's working directory.
func (p *Program) RunDir(ctx context.Context, dir string, args ...string) (string, error) {
binary := p.Path
if binary == "" {
binary = p.Name
}
var out bytes.Buffer
cmd := exec.CommandContext(ctx, binary, args...)
cmd.Stdout = &out
cmd.Stderr = &out
if dir != "" {
cmd.Dir = dir
}
if err := cmd.Run(); err != nil {
return strings.TrimSpace(out.String()), coreerr.E("Program.RunDir", fmt.Sprintf("%q: command failed", p.Name), err)
}
return strings.TrimSpace(out.String()), nil
}