package toolingplan import ( "context" "os" "path/filepath" "sort" ) type InstallStep struct { Tool string Version string Source string Reason string } type SkipNote struct { Target string Reason string } type Plan struct { RepoManagedTools []string Steps []InstallStep Skips []SkipNote } type detector interface { detect(context.Context, string, map[string]struct{}) detectionResult } type detectionResult struct { Steps []InstallStep Skips []SkipNote } var detectors = []detector{ goDetector{}, nodeDetector{}, pythonDetector{}, rustDetector{}, } func Build(ctx context.Context, repoRoot string) Plan { managedTools, managedSkips := repoManagedTools(repoRoot) steps := make([]InstallStep, 0) skips := append([]SkipNote(nil), managedSkips...) for _, detector := range detectors { result := detector.detect(ctx, repoRoot, managedTools) steps = append(steps, result.Steps...) skips = append(skips, result.Skips...) } sort.Slice(steps, func(i, j int) bool { if steps[i].Tool != steps[j].Tool { return steps[i].Tool < steps[j].Tool } if steps[i].Version != steps[j].Version { return steps[i].Version < steps[j].Version } return steps[i].Source < steps[j].Source }) sort.Slice(skips, func(i, j int) bool { if skips[i].Target != skips[j].Target { return skips[i].Target < skips[j].Target } return skips[i].Reason < skips[j].Reason }) repoManagedList := make([]string, 0, len(managedTools)) for tool := range managedTools { repoManagedList = append(repoManagedList, tool) } sort.Strings(repoManagedList) return Plan{ RepoManagedTools: repoManagedList, Steps: steps, Skips: skips, } } func readRepoFile(repoRoot, relativePath string) (string, bool, error) { data, err := os.ReadFile(filepath.Join(repoRoot, relativePath)) if err != nil { if os.IsNotExist(err) { return "", false, nil } return "", false, err } return string(data), true, nil } func alreadyManaged(tool string, managedTools map[string]struct{}) bool { _, ok := managedTools[tool] return ok }