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
109 lines
4 KiB
Go
109 lines
4 KiB
Go
package toolingplan
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type nodeDetector struct{}
|
|
|
|
func (nodeDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
|
|
result := detectionResult{}
|
|
|
|
nodeVersion, nodeSource, nodeManaged, nodeSkip := detectNodeVersion(repoRoot, managedTools)
|
|
if nodeManaged {
|
|
result.Skips = append(result.Skips, SkipNote{Target: "node", Reason: "already managed by repo mise declarations"})
|
|
} else if nodeVersion != "" {
|
|
result.Steps = append(result.Steps, InstallStep{Tool: "node", Version: nodeVersion, Source: nodeSource, Reason: "exact runtime version"})
|
|
} else {
|
|
result.Skips = append(result.Skips, SkipNote{Target: "node", Reason: nodeSkip})
|
|
}
|
|
|
|
packageManagerVersion, packageManagerTool, packageManagerSource, packageManagerSkip := detectNodePackageManager(repoRoot)
|
|
if packageManagerTool == "" {
|
|
if packageManagerSkip != "" {
|
|
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: packageManagerSkip})
|
|
}
|
|
return result
|
|
}
|
|
if alreadyManaged(packageManagerTool, managedTools) {
|
|
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: packageManagerTool + " is already managed by repo mise declarations"})
|
|
return result
|
|
}
|
|
if nodeVersion == "" && !alreadyManaged("node", managedTools) {
|
|
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: "packageManager is pinned but node is not pinned"})
|
|
return result
|
|
}
|
|
result.Steps = append(result.Steps, InstallStep{Tool: packageManagerTool, Version: packageManagerVersion, Source: packageManagerSource, Reason: "exact packageManager version"})
|
|
return result
|
|
}
|
|
|
|
func detectNodeVersion(repoRoot string, managedTools map[string]struct{}) (version string, source string, managed bool, skip string) {
|
|
if alreadyManaged("node", managedTools) {
|
|
return "", "", true, ""
|
|
}
|
|
for _, candidate := range []string{".node-version", ".nvmrc"} {
|
|
value, ok, err := readRepoFile(repoRoot, candidate)
|
|
if err != nil {
|
|
return "", "", false, fmt.Sprintf("could not read %s: %v", candidate, err)
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
version, ok := normalizeExactVersion(strings.TrimSpace(value))
|
|
if ok {
|
|
return version, candidate, false, ""
|
|
}
|
|
return "", "", false, candidate + " does not pin an exact version"
|
|
}
|
|
packageJSON, ok, err := readRepoFile(repoRoot, "package.json")
|
|
if err != nil {
|
|
return "", "", false, fmt.Sprintf("could not read package.json: %v", err)
|
|
}
|
|
if !ok {
|
|
return "", "", false, "no pinned node version file"
|
|
}
|
|
meta, err := parsePackageJSON(packageJSON)
|
|
if err != nil {
|
|
return "", "", false, fmt.Sprintf("could not parse package.json: %v", err)
|
|
}
|
|
if version, ok := normalizeExactVersion(meta.Volta.Node); ok {
|
|
return version, "package.json#volta.node", false, ""
|
|
}
|
|
if strings.TrimSpace(meta.Volta.Node) != "" {
|
|
return "", "", false, "package.json#volta.node is not an exact version"
|
|
}
|
|
return "", "", false, "no pinned node version file"
|
|
}
|
|
|
|
func detectNodePackageManager(repoRoot string) (version string, tool string, source string, skip string) {
|
|
packageJSON, ok, err := readRepoFile(repoRoot, "package.json")
|
|
if err != nil {
|
|
return "", "", "", fmt.Sprintf("could not read package.json: %v", err)
|
|
}
|
|
if !ok {
|
|
return "", "", "", ""
|
|
}
|
|
meta, err := parsePackageJSON(packageJSON)
|
|
if err != nil {
|
|
return "", "", "", fmt.Sprintf("could not parse package.json: %v", err)
|
|
}
|
|
value := strings.TrimSpace(meta.PackageManager)
|
|
if value == "" {
|
|
return "", "", "", ""
|
|
}
|
|
parts := strings.SplitN(value, "@", 2)
|
|
if len(parts) != 2 {
|
|
return "", "", "", "packageManager is not in tool@version form"
|
|
}
|
|
tool = strings.TrimSpace(parts[0])
|
|
if tool != "pnpm" && tool != "yarn" && tool != "npm" && tool != "bun" {
|
|
return "", "", "", "packageManager is not a supported exact installer target"
|
|
}
|
|
version, ok = normalizeExactVersion(parts[1])
|
|
if !ok {
|
|
return "", "", "", "packageManager version is not exact"
|
|
}
|
|
return version, tool, "package.json#packageManager", ""
|
|
}
|