Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions cmd/entire/cli/plugin/dispatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package plugin

import (
"context"
"errors"
"fmt"
"os"
"os/exec"

"github.com/spf13/cobra"
)

// Dispatch resolves an unknown subcommand to an installed plugin and execs
// it. Returns handled=true if a plugin was found and executed (even if it
// returned a non-zero exit code, which is propagated via err). When handled
// is false, the caller should fall through to its normal unknown-command
// handling.
//
// args is the full argv slice excluding the program name (e.g.
// os.Args[1:]). Cobra resolves built-ins first, so any name registered on
// rootCmd (including aliases) takes precedence over an installed plugin.
func Dispatch(ctx context.Context, rootCmd *cobra.Command, args []string) (bool, error) {
mgr, err := NewManager()
if err != nil {
return false, err
}
return DispatchWith(ctx, rootCmd, args, mgr)
}

// DispatchWith is Dispatch with an explicit Manager (useful for tests so
// they can avoid mutating ENTIRE_PLUGIN_DIR).
func DispatchWith(ctx context.Context, rootCmd *cobra.Command, args []string, mgr *Manager) (bool, error) {
if len(args) == 0 {
return false, nil
}

// Skip flags. The first non-flag arg is the candidate subcommand name.
// We deliberately mirror cobra's "first positional after the root" model:
// `entire --foo bar baz` → the candidate is "bar".
first := -1
for i, a := range args {
if a == "--" {
if i+1 < len(args) {
first = i + 1
}
break
}
if len(a) == 0 || a[0] == '-' {
continue
}
first = i
break
}
if first == -1 {
return false, nil
}

name := args[first]
if !ValidName(name) {
return false, nil
}

// Built-in precedence: cobra resolves built-ins first. If rootCmd.Find()
// returns a non-root command, the user wanted a built-in (potentially
// misspelled) and we should not shadow it.
if isBuiltin(rootCmd, name) {
return false, nil
}

if mgr == nil {
return false, errors.New("plugin: nil manager")
}
p, err := mgr.Find(name)
if err != nil {
return false, err
}
if p == nil {
return false, nil
}

rest := args[first+1:]
if err := Exec(ctx, p, rest, mgr.Root); err != nil {
return true, err
}
return true, nil
}

// Exec runs the plugin executable with the given args, inheriting stdio and
// injecting ENTIRE_* env vars. The caller's exit code should mirror the
// child's; on a non-zero exit Exec returns an *exec.ExitError so callers can
// inspect ProcessState.
func Exec(ctx context.Context, p *Plugin, args []string, pluginRoot string) error {
if p == nil {
return errors.New("plugin: nil plugin")
}
if p.ExecPath == "" {
return fmt.Errorf("plugin %q: missing executable path", p.Name)
}
if _, err := os.Stat(p.ExecPath); err != nil {
return fmt.Errorf("plugin %q: %w", p.Name, err)
}

cmd, err := buildExecCommand(ctx, p, args)
if err != nil {
return err
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = pluginEnv(os.Environ(), p, pluginRoot)
if err := cmd.Run(); err != nil {
// Always wrap, but preserve the *exec.ExitError chain via %w so
// callers can errors.As to recover the child's exit code.
return fmt.Errorf("plugin %q: %w", p.Name, err)
}
return nil
}

// pluginEnv returns the child environment with ENTIRE_* injections.
// - ENTIRE_PLUGIN_DATA_DIR: per-plugin durable storage under the plugins root.
// - ENTIRE_REPO_ROOT: passes through if the parent already set it.
// - ENTIRE_SESSION_ID: passes through if the parent already set it.
//
// Callers (e.g. main.go) are responsible for setting ENTIRE_REPO_ROOT before
// invoking dispatch. We don't compute it here to avoid pulling the paths
// package and its git CLI dependency into the dispatcher.
func pluginEnv(parent []string, p *Plugin, pluginRoot string) []string {
out := make([]string, 0, len(parent)+1)
out = append(out, parent...)
out = append(out, "ENTIRE_PLUGIN_DATA_DIR="+pluginDataDir(pluginRoot, p.Name))
return out
}

// pluginDataDir returns a per-plugin data directory adjacent to the plugin
// install location. Plugins should write durable data here, not into the
// install dir (which gets replaced on upgrade).
func pluginDataDir(root, name string) string {
return root + string(os.PathSeparator) + Prefix + name + string(os.PathSeparator) + "data"
}
Comment on lines +137 to +139

// isBuiltin reports whether name resolves to a built-in command or alias on
// rootCmd. This mirrors gh's install-time conflict check.
func isBuiltin(rootCmd *cobra.Command, name string) bool {
if rootCmd == nil {
return false
}
cmd, _, err := rootCmd.Find([]string{name})
if err != nil {
return false
}
// Find returns the deepest command it could resolve; for an unknown
// subcommand it returns rootCmd itself with no error. So a real built-in
// is anything that resolved past the root.
return cmd != nil && cmd != rootCmd
}

// PropagateExitCode inspects err for an *exec.ExitError and returns the
// child's exit status, or -1 if err is nil or unrelated to a child exit.
// Callers (typically main.go) can use this to mirror the plugin's exit code.
func PropagateExitCode(err error) int {
if err == nil {
return 0
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return exitErr.ExitCode()
}
return -1
}
201 changes: 201 additions & 0 deletions cmd/entire/cli/plugin/dispatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package plugin

import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/spf13/cobra"
)

// newTestManager returns a Manager rooted at a fresh temp dir.
func newTestManager(t *testing.T) *Manager {
t.Helper()
return &Manager{Root: t.TempDir()}
}

// installFakeScript writes a fake script plugin at root/entire-<name>/entire-<name>.
// On Unix the script is made executable; on Windows we skip these tests.
func installFakeScript(t *testing.T, root, name, body string) {
t.Helper()
dir := filepath.Join(root, Prefix+name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
exe := filepath.Join(dir, Prefix+name)
if err := os.WriteFile(exe, []byte(body), 0o755); err != nil {
t.Fatalf("write exe: %v", err)
}
}

func TestDispatch_NoArgs(t *testing.T) {
t.Parallel()
root := &cobra.Command{Use: "entire"}
mgr := newTestManager(t)
handled, err := DispatchWith(context.Background(), root, nil, mgr)
if handled || err != nil {
t.Errorf("Dispatch(nil) = (%v, %v); want (false, nil)", handled, err)
}
}

func TestDispatch_FlagsOnly(t *testing.T) {
t.Parallel()
root := &cobra.Command{Use: "entire"}
mgr := newTestManager(t)
handled, err := DispatchWith(context.Background(), root, []string{"--foo", "--bar"}, mgr)
if handled || err != nil {
t.Errorf("flags-only: handled=%v err=%v; want false,nil", handled, err)
}
}

func TestDispatch_BuiltinTakesPrecedence(t *testing.T) {
t.Parallel()
if runtime.GOOS == osWindows {
t.Skip("uses Unix shebang scripts")
}
mgr := newTestManager(t)
installFakeScript(t, mgr.Root, "status", "#!/bin/sh\nexit 0\n")

rootCmd := &cobra.Command{Use: "entire"}
rootCmd.AddCommand(&cobra.Command{Use: "status"})

handled, err := DispatchWith(context.Background(), rootCmd, []string{"status"}, mgr)
if handled || err != nil {
t.Errorf("built-in shadow: handled=%v err=%v; want false,nil", handled, err)
}
}

func TestDispatch_RunsPlugin(t *testing.T) {
t.Parallel()
if runtime.GOOS == osWindows {
t.Skip("uses Unix shebang scripts")
}
mgr := newTestManager(t)
installFakeScript(t, mgr.Root, "echo", "#!/bin/sh\nif [ \"$1\" = \"ok\" ]; then exit 0; else exit 7; fi\n")

rootCmd := &cobra.Command{Use: "entire"}
handled, err := DispatchWith(context.Background(), rootCmd, []string{"echo", "ok"}, mgr)
if !handled {
t.Fatalf("plugin not handled: err=%v", err)
}
if err != nil {
t.Errorf("Dispatch error: %v", err)
}
}

func TestDispatch_PropagatesExitCode(t *testing.T) {
t.Parallel()
if runtime.GOOS == osWindows {
t.Skip("uses Unix shebang scripts")
}
mgr := newTestManager(t)
installFakeScript(t, mgr.Root, "fail", "#!/bin/sh\nexit 42\n")

rootCmd := &cobra.Command{Use: "entire"}
handled, err := DispatchWith(context.Background(), rootCmd, []string{"fail"}, mgr)
if !handled {
t.Fatalf("expected handled=true; err=%v", err)
}
if err == nil {
t.Fatalf("expected non-nil err for non-zero exit")
}
var exitErr *exec.ExitError
if !asExitError(err, &exitErr) {
t.Fatalf("err = %v; want *exec.ExitError", err)
}
if got := exitErr.ExitCode(); got != 42 {
t.Errorf("exit code = %d; want 42", got)
}
if got := PropagateExitCode(err); got != 42 {
t.Errorf("PropagateExitCode = %d; want 42", got)
}
}

func TestDispatch_InjectsPluginDataDir(t *testing.T) {
t.Parallel()
if runtime.GOOS == osWindows {
t.Skip("uses Unix shebang scripts")
}
mgr := newTestManager(t)
out := filepath.Join(t.TempDir(), "out")
body := "#!/bin/sh\nprintf '%s' \"$ENTIRE_PLUGIN_DATA_DIR\" > " + out + "\n"
installFakeScript(t, mgr.Root, "envprobe", body)

rootCmd := &cobra.Command{Use: "entire"}
if _, err := DispatchWith(context.Background(), rootCmd, []string{"envprobe"}, mgr); err != nil {
t.Fatalf("Dispatch: %v", err)
}
got, err := os.ReadFile(out)
if err != nil {
t.Fatalf("read out: %v", err)
}
want := filepath.Join(mgr.Root, Prefix+"envprobe", "data")
if string(got) != want {
t.Errorf("ENTIRE_PLUGIN_DATA_DIR = %q; want %q", got, want)
}
}

func TestDispatch_UnknownPluginFallsThrough(t *testing.T) {
t.Parallel()
mgr := newTestManager(t)
rootCmd := &cobra.Command{Use: "entire"}
handled, err := DispatchWith(context.Background(), rootCmd, []string{"does-not-exist"}, mgr)
if handled || err != nil {
t.Errorf("unknown plugin: handled=%v err=%v; want false,nil", handled, err)
}
}

func TestDispatch_RejectsInvalidName(t *testing.T) {
t.Parallel()
mgr := newTestManager(t)
rootCmd := &cobra.Command{Use: "entire"}
handled, err := DispatchWith(context.Background(), rootCmd, []string{"BadName"}, mgr)
if handled || err != nil {
t.Errorf("invalid name: handled=%v err=%v; want false,nil", handled, err)
}
}

func TestDispatch_FirstNonFlagIsCandidate(t *testing.T) {
t.Parallel()
if runtime.GOOS == osWindows {
t.Skip("uses Unix shebang scripts")
}
mgr := newTestManager(t)
out := filepath.Join(t.TempDir(), "out")
body := "#!/bin/sh\nprintf '%s|' \"$@\" > " + out + "\n"
installFakeScript(t, mgr.Root, "args", body)

rootCmd := &cobra.Command{Use: "entire"}
if _, err := DispatchWith(context.Background(), rootCmd, []string{"--global-flag", "args", "--child-flag", "value"}, mgr); err != nil {
t.Fatalf("Dispatch: %v", err)
}
got, err := os.ReadFile(out)
if err != nil {
t.Fatalf("read out: %v", err)
}
if !strings.Contains(string(got), "--child-flag|value|") {
t.Errorf("child args = %q; want to contain --child-flag|value|", got)
}
}

func asExitError(err error, target **exec.ExitError) bool {
for cur := err; cur != nil; {
ee := &exec.ExitError{}
if errors.As(cur, &ee) {
*target = ee
return true
}
type unwrapper interface{ Unwrap() error }
u, ok := cur.(unwrapper)
if !ok {
return false
}
cur = u.Unwrap()
}
return false
}
14 changes: 14 additions & 0 deletions cmd/entire/cli/plugin/dispatch_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build unix

package plugin

import (
"context"
"os/exec"
)

// buildExecCommand returns an *exec.Cmd that runs the plugin directly. The
// shebang of script plugins is honored by the OS.
func buildExecCommand(ctx context.Context, p *Plugin, args []string) (*exec.Cmd, error) {
return exec.CommandContext(ctx, p.ExecPath, args...), nil
}
Loading
Loading