Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

448 lines
13 KiB
Go

package runtimecmd
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
)
type Resolver struct {
Environ func() []string
HomeDir func() (string, error)
IsExecutableFile func(string) bool
LookPath func(string) (string, error)
// ScutilProxy returns the raw output of `scutil --proxy` (and whether it is
// available). It is injectable for legacy macOS parser tests; production
// reads the platform system proxy through the build-tagged adapter.
ScutilProxy func() (string, bool)
}
// nestingGuardEnvKeys are the environment variables a parent Claude Code
// session exports to detect (and refuse) nested launches. When tuttid itself
// runs inside a Claude Code session these leak into spawned agent subprocesses, causing
// a child `claude` to abort with "cannot be launched inside another Claude Code
// session". They are stripped from the base environment so each spawned agent
// starts as a fresh session; they are CLAUDE-specific and harmless to other runtimes.
var nestingGuardEnvKeys = []string{
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_SESSION_ID",
"CLAUDE_CODE_CHILD_SESSION",
}
func (r Resolver) Env(overrides []string) []string {
baseEnv := stripEnvKeys(r.environ(), nestingGuardEnvKeys)
env := append(baseEnv, overrides...)
pathKey := pathEnvKey(env)
pathGroups := [][]string{}
if overridePathGroups := pathGroupsFromEnv(overrides, pathKey, envValue(baseEnv, pathKey)); len(overridePathGroups) > 0 {
pathGroups = append(pathGroups, overridePathGroups...)
pathGroups = append(pathGroups, preferredExecutableDirs(env))
pathGroups = append(pathGroups, r.fallbackExecutableDirs())
} else {
pathGroups = append(pathGroups, preferredExecutableDirs(env))
pathGroups = append(pathGroups, splitPathList(envValue(baseEnv, pathKey)))
pathGroups = append(pathGroups, r.fallbackExecutableDirs())
}
pathDirs := mergePathDirs(pathGroups...)
if len(pathDirs) > 0 {
env = setEnvValue(env, pathKey, strings.Join(pathDirs, string(os.PathListSeparator)))
}
return r.injectSystemProxyEnv(env)
}
func (r Resolver) Resolve(command string, env []string) string {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, `/\`) {
return command
}
if candidates := r.ResolveAll(command, env); len(candidates) > 0 {
return candidates[0]
}
return command
}
// ResolveAll returns every executable match for command in the effective PATH,
// preserving directory order and removing duplicate paths. It deliberately
// does not call LookPath: callers that need an OS-specific fallback can append
// that result explicitly, while discovery callers get a complete, stable view
// of the resolver's own search plan.
func (r Resolver) ResolveAll(command string, env []string) []string {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, "/\\") {
return nil
}
return r.ResolveAllNames([]string{command}, env)
}
// ResolveAllNames returns every executable match for a set of command names.
// Directory order is primary and name order is secondary, which lets callers
// enumerate platform launcher variants without changing PATH precedence.
func (r Resolver) ResolveAllNames(commandNames []string, env []string) []string {
result := []string{}
seen := map[string]struct{}{}
for _, dir := range splitPathList(envValue(env, pathEnvKey(env))) {
for _, command := range commandNames {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, `/\\`) {
continue
}
for _, executableName := range executableNameCandidates(command, env) {
candidate := filepath.Join(dir, executableName)
if !r.isExecutableFile(candidate) {
continue
}
key := executablePathKey(candidate)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, candidate)
}
}
}
return result
}
func (r Resolver) ResolveBinary(binaryNames []string, overrides []string) string {
env := r.Env(overrides)
for _, binaryName := range binaryNames {
binaryName = strings.TrimSpace(binaryName)
if binaryName == "" {
continue
}
path := r.Resolve(binaryName, env)
if path != binaryName {
return path
}
if path := r.lookPath(binaryName); path != "" {
return path
}
}
return ""
}
func (r Resolver) UserBinInstallDirs(overrides []string) []string {
baseEnv := r.environ()
env := append([]string{}, baseEnv...)
env = append(env, overrides...)
pathValue := envValue(baseEnv, pathEnvKey(baseEnv))
if overridePath, ok := envValueFrom(overrides, pathEnvKey(env)); ok {
pathValue = overridePath
}
candidates := [][]string{
splitPathList(pathValue),
}
home, err := r.homeDir()
if err == nil && strings.TrimSpace(home) != "" {
managedNPMDirs := UserManagedNPMExecutableDirs(home)
candidates = append(candidates, managedNPMDirs, []string{filepath.Join(home, "bin")})
}
return mergePathDirs(candidates...)
}
func preferredExecutableDirs(env []string) []string {
dirs := []string{}
if nPrefix := strings.TrimSpace(envValue(env, "N_PREFIX")); nPrefix != "" {
dirs = append([]string{filepath.Join(nPrefix, "bin")}, dirs...)
}
if pnpmHome := strings.TrimSpace(envValue(env, "PNPM_HOME")); pnpmHome != "" {
dirs = append([]string{pnpmHome}, dirs...)
}
if voltaHome := strings.TrimSpace(envValue(env, "VOLTA_HOME")); voltaHome != "" {
dirs = append([]string{filepath.Join(voltaHome, "bin")}, dirs...)
}
if asdfDataDir := strings.TrimSpace(envValue(env, "ASDF_DATA_DIR")); asdfDataDir != "" {
dirs = append([]string{filepath.Join(asdfDataDir, "shims")}, dirs...)
}
if miseDataDir := strings.TrimSpace(envValue(env, "MISE_DATA_DIR")); miseDataDir != "" {
dirs = append([]string{filepath.Join(miseDataDir, "shims")}, dirs...)
}
if fnmDir := strings.TrimSpace(envValue(env, "FNM_DIR")); fnmDir != "" {
dirs = append(fnmNodeBinDirs(fnmDir), dirs...)
}
return dirs
}
func (r Resolver) fallbackExecutableDirs() []string {
dirs := []string{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
}
homeDirs := []string{}
home, err := r.homeDir()
if err == nil && strings.TrimSpace(home) != "" {
homeDirs = []string{
filepath.Join(home, ".tutti", "bin"),
filepath.Join(home, ".opencode", "bin"),
}
homeDirs = append(homeDirs, UserManagedNPMExecutableDirs(home)...)
homeDirs = append(homeDirs,
filepath.Join(home, "bin"),
filepath.Join(home, ".npm-global", "bin"),
filepath.Join(home, ".n", "bin"),
filepath.Join(home, "n", "bin"),
filepath.Join(home, ".volta", "bin"),
filepath.Join(home, ".asdf", "shims"),
filepath.Join(home, ".mise", "shims"),
filepath.Join(home, ".bun", "bin"),
filepath.Join(home, "Library", "pnpm"),
)
homeDirs = append(homeDirs, nvmNodeBinDirs(home)...)
homeDirs = append(homeDirs, fnmNodeBinDirs(filepath.Join(home, ".fnm"))...)
// Cursor's native Windows installer places agent.ps1 and its versioned
// payload under %LOCALAPPDATA%\\cursor-agent rather than on PATH.
homeDirs = append(homeDirs, filepath.Join(home, "AppData", "Local", "cursor-agent"))
}
return append(homeDirs, dirs...)
}
func (r Resolver) environ() []string {
if r.Environ != nil {
return r.Environ()
}
return os.Environ()
}
func (r Resolver) homeDir() (string, error) {
if r.HomeDir != nil {
return r.HomeDir()
}
return os.UserHomeDir()
}
func (r Resolver) isExecutableFile(path string) bool {
if r.IsExecutableFile != nil {
return r.IsExecutableFile(path)
}
return isExecutableFile(path)
}
func (r Resolver) lookPath(binaryName string) string {
lookPath := r.LookPath
if lookPath == nil {
lookPath = exec.LookPath
}
path, err := lookPath(binaryName)
if err == nil && strings.TrimSpace(path) != "" {
return strings.TrimSpace(path)
}
return ""
}
func nvmNodeBinDirs(home string) []string {
matches, err := filepath.Glob(filepath.Join(home, ".nvm", "versions", "node", "*", "bin"))
if err != nil {
return nil
}
sort.SliceStable(matches, func(i, j int) bool {
return compareNVMNodeBinDirs(matches[i], matches[j]) > 0
})
return matches
}
func compareNVMNodeBinDirs(left string, right string) int {
leftVersion := nvmNodeVersion(left)
rightVersion := nvmNodeVersion(right)
for i := 0; i < max(len(leftVersion), len(rightVersion)); i++ {
leftPart := versionPart(leftVersion, i)
rightPart := versionPart(rightVersion, i)
if leftPart > rightPart {
return 1
}
if leftPart < rightPart {
return -1
}
}
return strings.Compare(left, right)
}
func nvmNodeVersion(binDir string) []int {
versionDir := filepath.Base(filepath.Dir(binDir))
version := strings.TrimPrefix(versionDir, "v")
parts := strings.Split(version, ".")
result := make([]int, 0, len(parts))
for _, part := range parts {
value, err := strconv.Atoi(part)
if err != nil {
return nil
}
result = append(result, value)
}
return result
}
func versionPart(version []int, index int) int {
if index < 0 || index >= len(version) {
return 0
}
return version[index]
}
func fnmNodeBinDirs(fnmDir string) []string {
matches, err := filepath.Glob(filepath.Join(fnmDir, "node-versions", "*", "installation", "bin"))
if err != nil {
return nil
}
return matches
}
func mergePathDirs(groups ...[]string) []string {
result := []string{}
seen := map[string]bool{}
for _, group := range groups {
for _, dir := range group {
normalized := strings.TrimSpace(dir)
if normalized == "" {
continue
}
key := pathDirKey(normalized)
if seen[key] {
continue
}
seen[key] = true
result = append(result, normalized)
}
}
return result
}
func pathDirKey(dir string) string {
key := filepath.Clean(strings.TrimSpace(dir))
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
return key
}
func executablePathKey(path string) string {
return pathDirKey(path)
}
func pathEnvKey(env []string) string {
for i := len(env) - 1; i >= 0; i-- {
key, _, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(key, "PATH") {
return key
}
}
return "PATH"
}
func envValue(env []string, key string) string {
value, _ := envValueFrom(env, key)
return value
}
func envValueFrom(env []string, key string) (string, bool) {
for i := len(env) - 1; i >= 0; i-- {
candidateKey, value, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(candidateKey, key) {
return value, true
}
}
return "", false
}
func pathGroupsFromEnv(env []string, key string, basePath string) [][]string {
groups := [][]string{}
baseDirs := splitPathList(basePath)
inheritedBaseDirs := []string{}
for i := len(env) - 1; i >= 0; i-- {
candidateKey, value, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(candidateKey, key) {
dirs := splitPathList(value)
if prefix, inherited, ok := splitInheritedPath(dirs, baseDirs); ok {
groups = append(groups, prefix)
if len(inheritedBaseDirs) == 0 {
inheritedBaseDirs = inherited
}
continue
}
groups = append(groups, dirs)
}
}
if len(inheritedBaseDirs) > 0 {
groups = append(groups, inheritedBaseDirs)
}
return groups
}
// splitPathList follows the host separator but also accepts the colon form
// emitted by MSYS/Unix-like test and launcher environments on Windows. A
// native Windows path containing a drive letter or backslashes remains on the
// normal semicolon path-list path.
func splitPathList(value string) []string {
if runtime.GOOS == "windows" && !strings.Contains(value, ";") &&
strings.Contains(value, ":") && !strings.Contains(value, `\`) {
return strings.Split(value, ":")
}
return filepath.SplitList(value)
}
func splitInheritedPath(dirs []string, baseDirs []string) ([]string, []string, bool) {
if len(baseDirs) == 0 || len(dirs) < len(baseDirs) {
return nil, nil, false
}
for i := 0; i+len(baseDirs) <= len(dirs); i++ {
if pathDirSlicesEqual(dirs[i:i+len(baseDirs)], baseDirs) {
return dirs[:i], dirs[i:], true
}
}
return nil, nil, false
}
func pathDirSlicesEqual(a []string, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if pathDirKey(a[i]) != pathDirKey(b[i]) {
return false
}
}
return true
}
func stripEnvKeys(env []string, keys []string) []string {
if len(keys) == 0 {
return env
}
next := make([]string, 0, len(env))
for _, item := range env {
candidateKey, _, ok := strings.Cut(item, "=")
if ok {
drop := false
for _, key := range keys {
if strings.EqualFold(candidateKey, key) {
drop = true
break
}
}
if drop {
continue
}
}
next = append(next, item)
}
return next
}
func setEnvValue(env []string, key string, value string) []string {
next := make([]string, 0, len(env)+1)
for _, item := range env {
candidateKey, _, ok := strings.Cut(item, "=")
if ok && strings.EqualFold(candidateKey, key) {
continue
}
next = append(next, item)
}
return append(next, key+"="+value)
}