package dev import ( "context" "fmt" "sort" "strings" "forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/cli/pkg/i18n" ) // Health command flags var ( healthRegistryPath string healthVerbose bool ) // AddHealthCommand adds the 'health' command to the given parent command. func AddHealthCommand(parent *cli.Command) { healthCmd := &cli.Command{ Use: "health", Short: i18n.T("cmd.dev.health.short"), Long: i18n.T("cmd.dev.health.long"), RunE: func(cmd *cli.Command, args []string) error { return runHealth(healthRegistryPath, healthVerbose) }, } healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", i18n.T("common.flag.registry")) healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, i18n.T("cmd.dev.health.flag.verbose")) parent.AddCommand(healthCmd) } func runHealth(registryPath string, verbose bool) error { ctx := context.Background() // Load registry and get paths reg, _, err := loadRegistryWithConfig(registryPath) if err != nil { return err } // Build paths and names for git operations var paths []string names := make(map[string]string) for _, repo := range reg.List() { if repo.IsGitRepo() { paths = append(paths, repo.Path) names[repo.Path] = repo.Name } } if len(paths) == 0 { cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } // Get status for all repos statuses := git.Status(ctx, git.StatusOptions{ Paths: paths, Names: names, }) // Sort for consistent verbose output sort.Slice(statuses, func(i, j int) bool { return statuses[i].Name < statuses[j].Name }) // Aggregate stats var ( totalRepos = len(statuses) dirtyRepos []string aheadRepos []string behindRepos []string errorRepos []string ) for _, s := range statuses { if s.Error != nil { errorRepos = append(errorRepos, s.Name) continue } if s.IsDirty() { dirtyRepos = append(dirtyRepos, s.Name) } if s.HasUnpushed() { aheadRepos = append(aheadRepos, s.Name) } if s.HasUnpulled() { behindRepos = append(behindRepos, s.Name) } } // Print summary line cli.Blank() printHealthSummary(totalRepos, dirtyRepos, aheadRepos, behindRepos, errorRepos) cli.Blank() // Verbose output if verbose { if len(dirtyRepos) > 0 { cli.Print("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.dirty_label")), formatRepoList(dirtyRepos)) } if len(aheadRepos) > 0 { cli.Print("%s %s\n", successStyle.Render(i18n.T("cmd.dev.health.ahead_label")), formatRepoList(aheadRepos)) } if len(behindRepos) > 0 { cli.Print("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.behind_label")), formatRepoList(behindRepos)) } if len(errorRepos) > 0 { cli.Print("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos)) } cli.Blank() } return nil } func printHealthSummary(total int, dirty, ahead, behind, errors []string) { parts := []string{ statusPart(total, i18n.T("cmd.dev.health.repos"), cli.ValueStyle), } // Dirty status if len(dirty) > 0 { parts = append(parts, statusPart(len(dirty), i18n.T("common.status.dirty"), cli.WarningStyle)) } else { parts = append(parts, statusText(i18n.T("cmd.dev.status.clean"), cli.SuccessStyle)) } // Push status if len(ahead) > 0 { parts = append(parts, statusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), cli.ValueStyle)) } else { parts = append(parts, statusText(i18n.T("common.status.synced"), cli.SuccessStyle)) } // Pull status if len(behind) > 0 { parts = append(parts, statusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), cli.WarningStyle)) } else { parts = append(parts, statusText(i18n.T("common.status.up_to_date"), cli.SuccessStyle)) } // Errors (only if any) if len(errors) > 0 { parts = append(parts, statusPart(len(errors), i18n.T("cmd.dev.health.errors"), cli.ErrorStyle)) } cli.Text(statusLine(parts...)) } func formatRepoList(reposList []string) string { if len(reposList) <= 5 { return joinRepos(reposList) } return joinRepos(reposList[:5]) + " " + i18n.T("cmd.dev.health.more", map[string]interface{}{"Count": len(reposList) - 5}) } func joinRepos(reposList []string) string { result := "" for i, r := range reposList { if i > 0 { result += ", " } result += r } return result } func statusPart(count int, label string, style *cli.AnsiStyle) string { return style.Render(fmt.Sprintf("%d %s", count, label)) } func statusText(text string, style *cli.AnsiStyle) string { return style.Render(text) } func statusLine(parts ...string) string { return strings.Join(parts, " | ") }