Documentation
¶
Overview ¶
Package clipath answers one question in one place: where is the sx CLI?
Client hooks and MCP server entries are configuration that some other program executes later, in an environment sx does not control. Writing a bare "sx" into them assumes the CLI is on that program's PATH — true when sx was installed with Homebrew or install.sh, false when the user only installed the desktop app, and unreliable for GUI clients launched from Finder or a Dock (which inherit launchd's minimal PATH, not a shell's).
Writing os.Executable() instead is wrong from the desktop app: that is the Wails GUI binary, which has no subcommands at all.
So resolution walks from most to least specific: an explicit override, the running binary when it is already the CLI, a CLI shipped alongside the app, PATH, and finally the directories installers use.
Index ¶
- Constants
- Variables
- func AppManaged() bool
- func Command(args ...string) (string, error)
- func CommandOrBare(args ...string) string
- func MCPEntryNeedsRewrite(entry any) bool
- func Managed(cmd string, subcommands ...string) bool
- func ManagedArgv(argv []string, subcommands ...string) bool
- func NeedsRepair(cmd string) bool
- func Resolve() (string, error)
- func ResolveOrBare() string
- func ShouldRewrite(cmd string) bool
Constants ¶
const EnvOverride = "SX_CLI_PATH"
EnvOverride names the escape hatch. Set it when the CLI lives somewhere the search below cannot reasonably guess.
Variables ¶
var ErrNotFound = errors.New("clipath: no sx CLI binary found")
ErrNotFound means no sx CLI could be located. Callers should degrade rather than fail: a hook carrying a bare "sx" still works for anyone whose PATH has it, which is strictly better than refusing to install the hook.
Functions ¶
func AppManaged ¶
func AppManaged() bool
AppManaged reports whether the running binary is the CLI copy that ships inside the desktop app.
Such a copy must not self-update. The CLI's updater overwrites its own executable in place, and that executable lives inside a signed, notarized .app — rewriting a file in there invalidates the bundle signature, which on macOS can stop the app from launching at all. /Applications is typically writable by admin users, so this would tend to succeed at breaking things.
It would also be pointless: the app's updater swaps the whole bundle, so a CLI that updated itself gets replaced on the app's next update anyway. And it would reintroduce exactly the app/CLI version skew that bundling exists to prevent. The app updates this copy; the CLI stays out of it.
func Command ¶
Command builds the string form a hook config needs: an absolute CLI path followed by args, shell-quoted when the path contains spaces (an app bundle the user renamed, a home directory with a space in it).
When no CLI can be found it returns the bare-"sx" form and ErrNotFound, so a caller can log the degradation and still write a hook that works for anyone with sx on PATH.
func CommandOrBare ¶
CommandOrBare is Command with the not-found error folded away, for the many call sites that cannot do anything useful about a missing CLI. The bare "sx" form it falls back to is exactly what these configs contained before, so the worst case is the old behavior rather than a failed install.
func MCPEntryNeedsRewrite ¶
MCPEntryNeedsRewrite reports whether an existing MCP server entry should be replaced with a freshly resolved one.
entry is the decoded JSON object for a single server: a "command" plus "args". A broken command is replaced whatever its arguments, since it cannot work as it stands. The bare-"sx" upgrade is held to a stricter test — that entry does work, only its path is being improved — so it must leave a hand-written invocation of the same binary alone, which means its arguments have to be the ones sx itself writes.
func Managed ¶
Managed reports whether cmd is an sx-written invocation of one of the given subcommands. It accepts both the legacy bare-"sx" form and the absolute-path form Command produces, so hook detection, upgrade, and removal keep working across the change.
subcommands are matched against the argument list, so "install" matches "sx install --hook-mode --client=cline" and "report-usage" matches "/abs/sx report-usage --client=github-copilot".
func ManagedArgv ¶
ManagedArgv is Managed for configs that store a command as an argument array rather than a shell string — Codex's config.toml "notify", for instance.
Joining such an array with spaces and handing it to Managed is wrong: a CLI path containing a space would be re-split in the wrong place, and the entry would go unrecognized on uninstall.
func NeedsRepair ¶
NeedsRepair reports whether an existing config command was written by sx but can no longer work, and should therefore be overwritten.
Two cases qualify. An entry naming the desktop app's GUI binary was written by a version that used os.Executable() from the app — it can never serve as the CLI, since that binary has no subcommands. An entry naming an sx CLI at a path that no longer exists was valid when written and went stale, typically because the app moved or a separately installed CLI was removed.
Anything else returns false. A command sx did not write is the user's, and a hand-written "skills" MCP entry pointing at their own server must survive an sx install untouched.
func Resolve ¶
Resolve returns an absolute path to an sx CLI binary that can run subcommands.
Precedence, and what it does and does not guarantee: the binary already running wins first, then a CLI shipped with the app, then PATH. So an install initiated *by the app* always names the app's own bundled CLI — which is the property that matters, because the on-disk vault format is versioned and a hook invoking an older separately-installed CLI could read and write vaults the app manages with code predating the current layout. App-initiated skew stays forward-only.
It is not a global guarantee. Running your own `sx install` from a terminal bakes *that* CLI's path in — more precisely, the most stable known spelling of the binary you invoked (a versioned Homebrew Cellar target is recorded as the stable bin/ symlink that points at it, never the Cellar path an upgrade deletes). Alternating between a terminal install and an app install still rewrites the stored path each way. Both paths work; only the version they pin differs. SX_CLI_PATH overrides all of it.
This only decides what goes into hook and MCP configuration. What happens when the user types "sx" in a terminal is their shell's PATH, untouched.
The result is memoized for the process lifetime: Managed and friends call this once per config entry they inspect, and the probe work (PATH lookups, stats, symlink resolution) is not free.
func ResolveOrBare ¶
func ResolveOrBare() string
ResolveOrBare returns the resolved CLI path, or the bare name "sx" when none can be found.
For argv-style fields — an MCP server's "command", which the client execs directly rather than through a shell — so the result is deliberately not shell-quoted, and the bare fallback defers to the client's own PATH lookup. Failing to write an MCP entry at all would be worse than writing one that depends on PATH.
func ShouldRewrite ¶
ShouldRewrite reports whether an existing config command should be replaced with the current one.
That is NeedsRepair plus two upgrade cases it deliberately excludes. A bare "sx" was written when no CLI could be found; it still works wherever PATH has one, so it is not broken — but once a CLI is resolvable, an absolute path is strictly better, and without this a degraded first install stays degraded forever. And an sx path inside a versioned package-manager tree (a Homebrew Cellar keg, recorded before the stable-alias preference existed) still works today but dies on the next upgrade, so it is upgraded while its stable alias exists. Every other absolute spelling — including a different spelling of the same binary — is left alone to avoid rewrite thrash.
Types ¶
This section is empty.