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

279 lines
8.4 KiB
Go

package agentstatus
import (
"context"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/tutti-os/tutti/packages/agent/daemon/runtimecmd"
)
// codexRuntimeCandidateSource records how a launcher entered a discovery
// snapshot. A logical installation can have more than one source when, for
// example, its launcher is both on PATH and returned by `bun pm bin -g`.
type codexRuntimeCandidateSource string
const (
codexRuntimeCandidateSourcePath codexRuntimeCandidateSource = "path"
codexRuntimeCandidateSourceBunGlobal codexRuntimeCandidateSource = "bun_global"
codexRuntimeCandidateSourcePNPMGlobal codexRuntimeCandidateSource = "pnpm_global"
codexRuntimeCandidateSourceNPMGlobal codexRuntimeCandidateSource = "npm_global"
codexRuntimeCandidateSourceHomebrew codexRuntimeCandidateSource = "homebrew"
)
// codexRuntimeCandidate is discovery evidence only. Phase two adds version
// and app-server validation; phase one must not use these candidates to alter
// selection or launch behavior.
type codexRuntimeCandidate struct {
LauncherPath string
RealPath string
PackageRoot string
Sources []codexRuntimeCandidateSource
}
// discoverCodexRuntimeCandidates enumerates every known Codex launcher source
// instead of stopping at the first PATH match. It intentionally does not
// perform version or app-server probes and is not connected to runtime
// selection yet.
func (s Service) discoverCodexRuntimeCandidates(ctx context.Context, spec ProviderSpec) []codexRuntimeCandidate {
if !isCodexStatusSpec(spec) {
return nil
}
resolver := s.commandResolver()
env := resolver.Env(spec.AdapterEnv)
result := codexRuntimeCandidateCollector{}
result.addAll(resolveCodexLaunchers(resolver, env), codexRuntimeCandidateSourcePath)
if path := resolver.ResolveBinary([]string{"codex"}, spec.AdapterEnv); path != "" {
result.add(path, codexRuntimeCandidateSourcePath)
}
globalBins := s.discoverCodexGlobalBins(ctx, resolver, spec.AdapterEnv)
for _, discovery := range []struct {
binDir string
source codexRuntimeCandidateSource
}{
{globalBins.bun, codexRuntimeCandidateSourceBunGlobal},
{globalBins.pnpm, codexRuntimeCandidateSourcePNPMGlobal},
{globalBins.npm, codexRuntimeCandidateSourceNPMGlobal},
{globalBins.homebrew, codexRuntimeCandidateSourceHomebrew},
} {
if discovery.binDir != "" {
result.addAll(resolveCodexLaunchersInDir(resolver, discovery.binDir), discovery.source)
}
}
return result.candidates
}
type codexGlobalBins struct {
bun string
pnpm string
npm string
homebrew string
}
// discoverCodexGlobalBins runs independent package-manager queries together so
// one slow manager does not hide later valid Codex installations. Candidates
// are still added in a stable source order after every query completes.
func (s Service) discoverCodexGlobalBins(
ctx context.Context,
resolver runtimecmd.Resolver,
overrides []string,
) codexGlobalBins {
var result codexGlobalBins
var queries sync.WaitGroup
queries.Add(4)
go func() {
defer queries.Done()
result.bun = s.discoverBunGlobalBinDir(ctx, resolver, overrides)
}()
go func() {
defer queries.Done()
result.pnpm = s.discoverPnpmGlobalBinDir(ctx, resolver, overrides)
}()
go func() {
defer queries.Done()
result.npm = s.discoverNpmGlobalBinDir(ctx, resolver, overrides)
}()
go func() {
defer queries.Done()
result.homebrew = s.discoverHomebrewBinDir(ctx, resolver, overrides)
}()
queries.Wait()
return result
}
func resolveCodexLaunchersInDir(resolver runtimecmd.Resolver, dir string) []string {
dir = strings.TrimSpace(dir)
if dir == "" {
return nil
}
return resolveCodexLaunchers(resolver, []string{"PATH=" + dir})
}
func resolveCodexLaunchers(resolver runtimecmd.Resolver, env []string) []string {
result := []string{}
seen := map[string]struct{}{}
for _, path := range resolver.ResolveAllNames(codexLauncherNames(), env) {
key := codexRuntimeCandidateIdentity(path, path, "")
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, path)
}
return result
}
func codexLauncherNames() []string {
if runtime.GOOS == "windows" {
// Windows package managers expose executable shims with one of these
// extensions. An extensionless file in an AppX resources directory is
// the non-Windows companion shipped beside codex.exe and cannot be
// launched by CreateProcess on Windows.
return []string{"codex.exe", "codex.cmd", "codex.bat"}
}
return []string{"codex"}
}
func (s Service) discoverPnpmGlobalBinDir(ctx context.Context, resolver runtimecmd.Resolver, overrides []string) string {
return s.runGlobalBinDirCommand(ctx, resolver, overrides, pnpmCommandNames(), []string{"bin", "-g"})
}
func (s Service) discoverNpmGlobalBinDir(ctx context.Context, resolver runtimecmd.Resolver, overrides []string) string {
prefix := s.runGlobalBinDirCommand(ctx, resolver, overrides, npmCommandNames(), []string{"prefix", "-g"})
if prefix == "" {
return ""
}
if runtime.GOOS == "windows" {
return prefix
}
return filepath.Join(prefix, "bin")
}
func (s Service) discoverHomebrewBinDir(ctx context.Context, resolver runtimecmd.Resolver, overrides []string) string {
if runtime.GOOS == "windows" {
return ""
}
prefix := s.runGlobalBinDirCommand(ctx, resolver, overrides, []string{"brew"}, []string{"--prefix"})
if prefix == "" {
return ""
}
return filepath.Join(prefix, "bin")
}
func (s Service) runGlobalBinDirCommand(
ctx context.Context,
resolver runtimecmd.Resolver,
overrides []string,
binaryNames []string,
args []string,
) string {
binaryPath := resolver.ResolveBinary(binaryNames, overrides)
if binaryPath == "" {
return ""
}
return s.GlobalBinDiscoveryCache.load(binaryPath, strings.Join(args, "\x00"), func() string {
release, acquired := s.DetectionCommands.acquire(ctx)
if !acquired {
return ""
}
defer release()
commandCtx, cancel := context.WithTimeout(baseContext(ctx), globalBinDiscoveryTimeout)
defer cancel()
command := exec.CommandContext(commandCtx, binaryPath, args...)
command.Env = resolver.Env(overrides)
output := &boundedCommandOutput{limit: bunGlobalBinOutputLimit}
command.Stdout = output
if err := command.Run(); err != nil {
return ""
}
value := strings.TrimSpace(output.String())
if !filepath.IsAbs(value) {
return ""
}
return filepath.Clean(value)
})
}
func pnpmCommandNames() []string {
if runtime.GOOS == "windows" {
return []string{"pnpm.cmd", "pnpm"}
}
return []string{"pnpm"}
}
func npmCommandNames() []string {
if runtime.GOOS == "windows" {
return []string{"npm.cmd", "npm"}
}
return []string{"npm"}
}
type codexRuntimeCandidateCollector struct {
candidates []codexRuntimeCandidate
byIdentity map[string]int
}
func (c *codexRuntimeCandidateCollector) addAll(paths []string, source codexRuntimeCandidateSource) {
for _, path := range paths {
c.add(path, source)
}
}
func (c *codexRuntimeCandidateCollector) add(path string, source codexRuntimeCandidateSource) {
path = strings.TrimSpace(path)
if path == "" {
return
}
path = materializeCodexWindowsAppsLauncher(path)
realPath := resolvedCodexLauncherPath(path)
packageRoot := codexPackageDirForBinary(realPath)
identity := codexRuntimeCandidateIdentity(path, realPath, packageRoot)
if c.byIdentity == nil {
c.byIdentity = map[string]int{}
}
if index, found := c.byIdentity[identity]; found {
candidate := &c.candidates[index]
candidate.Sources = appendCodexRuntimeCandidateSource(candidate.Sources, source)
return
}
c.byIdentity[identity] = len(c.candidates)
c.candidates = append(c.candidates, codexRuntimeCandidate{
LauncherPath: filepath.Clean(path),
RealPath: realPath,
PackageRoot: packageRoot,
Sources: []codexRuntimeCandidateSource{source},
})
}
func resolvedCodexLauncherPath(path string) string {
if resolved, err := filepath.EvalSymlinks(path); err == nil {
return filepath.Clean(resolved)
}
return filepath.Clean(path)
}
func codexRuntimeCandidateIdentity(launcherPath, realPath, packageRoot string) string {
identity := firstNonBlank(packageRoot, realPath, launcherPath)
if runtime.GOOS == "windows" {
return strings.ToLower(filepath.Clean(identity))
}
return filepath.Clean(identity)
}
func appendCodexRuntimeCandidateSource(
sources []codexRuntimeCandidateSource,
source codexRuntimeCandidateSource,
) []codexRuntimeCandidateSource {
for _, existing := range sources {
if existing == source {
return sources
}
}
return append(sources, source)
}