banger/internal/toolingplan/rust.go
Thales Maciel 4813e844e2
Bootstrap vm run tooling before attach
Speed up first use of repo backed VMs by bootstrapping obvious tools before
the best effort LLM harness runs.

Add a host side tooling plan for pinned Go, Node, Python, and Rust versions,
summarize that plan in the uploaded prompt, and run repo mise install plus
guest global mise use -g --pin steps before the bounded opencode inspection.

Keep the harness non fatal, prefer host opencode attach when the client
supports it, fall back to guest opencode over SSH for older clients, and
cover the new flow with CLI plus planner tests.

Validation:
- go test ./internal/cli ./internal/toolingplan
- GOCACHE=/tmp/banger-gocache go test ./...
- make build
2026-03-29 11:38:05 -03:00

70 lines
2.4 KiB
Go

package toolingplan
import (
"context"
"fmt"
"strings"
toml "github.com/pelletier/go-toml"
)
type rustDetector struct{}
func (rustDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
if alreadyManaged("rust", managedTools) {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "already managed by repo mise declarations"}}}
}
if version, ok, reason := parseRustToolchainToml(repoRoot); ok {
return detectionResult{Steps: []InstallStep{{Tool: "rust", Version: version, Source: "rust-toolchain.toml", Reason: "exact toolchain channel"}}}
} else if reason != "" {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: reason}}}
}
value, ok, err := readRepoFile(repoRoot, "rust-toolchain")
if err != nil {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: fmt.Sprintf("could not read rust-toolchain: %v", err)}}}
}
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "no rust-toolchain or rust-toolchain.toml"}}}
}
version := firstMeaningfulLine(value)
if normalized, ok := normalizeExactVersion(version); ok {
return detectionResult{Steps: []InstallStep{{Tool: "rust", Version: normalized, Source: "rust-toolchain", Reason: "exact toolchain channel"}}}
}
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "rust-toolchain does not pin an exact version"}}}
}
func parseRustToolchainToml(repoRoot string) (version string, ok bool, reason string) {
data, found, err := readRepoFile(repoRoot, "rust-toolchain.toml")
if err != nil {
return "", false, fmt.Sprintf("could not read rust-toolchain.toml: %v", err)
}
if !found {
return "", false, ""
}
tree, err := toml.Load(data)
if err != nil {
return "", false, fmt.Sprintf("could not parse rust-toolchain.toml: %v", err)
}
channelValue := tree.GetDefault("toolchain.channel", "")
channel, _ := channelValue.(string)
channel = strings.TrimSpace(channel)
if channel == "" {
return "", false, "rust-toolchain.toml has no toolchain.channel"
}
version, ok = normalizeExactVersion(channel)
if !ok {
return "", false, "rust-toolchain.toml channel is not an exact version"
}
return version, true, ""
}
func firstMeaningfulLine(value string) string {
for _, line := range strings.Split(value, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
return trimmed
}
return ""
}