refactor: migrate all pkg/* to cli abstraction

- Replaces lipgloss/fmt with cli.* functions
- Adds unit tests for new cli components
- Fixes all build errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-31 23:36:43 +00:00
parent f07e5bb3ff
commit cdcb489d7b
34 changed files with 348 additions and 228 deletions

View file

@ -17,20 +17,20 @@ var (
// Task priority/status styles from shared // Task priority/status styles from shared
var ( var (
taskPriorityHighStyle = cli.PriorityHighStyle taskPriorityHighStyle = cli.NewStyle().Foreground(cli.ColourRed500)
taskPriorityMediumStyle = cli.PriorityMediumStyle taskPriorityMediumStyle = cli.NewStyle().Foreground(cli.ColourAmber500)
taskPriorityLowStyle = cli.PriorityLowStyle taskPriorityLowStyle = cli.NewStyle().Foreground(cli.ColourBlue400)
taskStatusPendingStyle = cli.StatusPendingStyle taskStatusPendingStyle = cli.DimStyle
taskStatusInProgressStyle = cli.StatusRunningStyle taskStatusInProgressStyle = cli.NewStyle().Foreground(cli.ColourBlue500)
taskStatusCompletedStyle = cli.StatusSuccessStyle taskStatusCompletedStyle = cli.SuccessStyle
taskStatusBlockedStyle = cli.StatusErrorStyle taskStatusBlockedStyle = cli.ErrorStyle
) )
// Task-specific styles (aliases to shared where possible) // Task-specific styles (aliases to shared where possible)
var ( var (
taskIDStyle = cli.TitleStyle // Bold + blue taskIDStyle = cli.TitleStyle // Bold + blue
taskTitleStyle = cli.ValueStyle // Light gray taskTitleStyle = cli.ValueStyle // Light gray
taskLabelStyle = cli.AccentLabelStyle // Violet for labels taskLabelStyle = cli.NewStyle().Foreground(cli.ColourViolet500) // Violet for labels
) )
// AddAgenticCommands adds the agentic task management commands to the ai command. // AddAgenticCommands adds the agentic task management commands to the ai command.

View file

@ -80,7 +80,7 @@ var taskCommitCmd = &cli.Command{
} }
if !hasChanges { if !hasChanges {
cli.Text("No changes to commit") cli.Println("No changes to commit")
return nil return nil
} }

View file

@ -134,7 +134,7 @@ var taskCmd = &cli.Command{
taskClaim = true // Auto-select implies claiming taskClaim = true // Auto-select implies claiming
} else { } else {
if taskID == "" { if taskID == "" {
return cli.Err(i18n.T("cmd.ai.task.id_required")) return cli.Err("%s", i18n.T("cmd.ai.task.id_required"))
} }
task, err = client.GetTask(ctx, taskID) task, err = client.GetTask(ctx, taskID)
@ -157,7 +157,7 @@ var taskCmd = &cli.Command{
} }
if taskClaim && task.Status == agentic.StatusPending { if taskClaim && task.Status == agentic.StatusPending {
cli.Line("") cli.Blank()
cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming")) cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming"))
claimedTask, err := client.ClaimTask(ctx, task.ID) claimedTask, err := client.ClaimTask(ctx, task.ID)
@ -215,12 +215,12 @@ func printTaskList(tasks []agentic.Task) {
cli.Text(line) cli.Text(line)
} }
cli.Line("") cli.Blank()
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint"))) cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint")))
} }
func printTaskDetails(task *agentic.Task) { func printTaskDetails(task *agentic.Task) {
cli.Line("") cli.Blank()
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID))
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title))
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority))
@ -240,12 +240,12 @@ func printTaskDetails(task *agentic.Task) {
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt))
cli.Line("") cli.Blank()
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description"))) cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description")))
cli.Text(task.Description) cli.Text(task.Description)
if len(task.Files) > 0 { if len(task.Files) > 0 {
cli.Line("") cli.Blank()
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files"))) cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files")))
for _, f := range task.Files { for _, f := range task.Files {
cli.Print(" - %s\n", f) cli.Print(" - %s\n", f)
@ -253,7 +253,7 @@ func printTaskDetails(task *agentic.Task) {
} }
if len(task.Dependencies) > 0 { if len(task.Dependencies) > 0 {
cli.Line("") cli.Blank()
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", ")) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", "))
} }
} }
@ -286,4 +286,4 @@ func formatTaskStatus(s agentic.TaskStatus) string {
default: default:
return dimStyle.Render(string(s)) return dimStyle.Render(string(s))
} }
} }

View file

@ -34,7 +34,7 @@ var taskUpdateCmd = &cli.Command{
taskID := args[0] taskID := args[0]
if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
return cli.Err(i18n.T("cmd.ai.task_update.flag_required")) return cli.Err("%s", i18n.T("cmd.ai.task_update.flag_required"))
} }
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")

View file

@ -2,30 +2,56 @@ package ci
import ( import (
"os" "os"
"os/exec"
"strings"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
// runChangelog generates and prints a changelog.
func runChangelog(fromRef, toRef string) error { func runChangelog(fromRef, toRef string) error {
projectDir, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return cli.WrapVerb(err, "get", "working directory") return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
} }
// Load config for changelog settings // Auto-detect refs if not provided
cfg, err := release.LoadConfig(projectDir) if fromRef == "" || toRef == "" {
if err != nil { tag, err := latestTag(cwd)
return cli.WrapVerb(err, "load", "config") if err == nil {
if fromRef == "" {
fromRef = tag
}
if toRef == "" {
toRef = "HEAD"
}
} else {
// No tags, use initial commit? Or just HEAD?
cli.Text(i18n.T("cmd.ci.changelog.no_tags"))
return nil
}
} }
cli.Print("%s %s..%s\n\n", releaseDimStyle.Render(i18n.T("cmd.ci.changelog.generating")), fromRef, toRef)
// Generate changelog // Generate changelog
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog) changelog, err := release.Generate(cwd, fromRef, toRef)
if err != nil { if err != nil {
return cli.WrapVerb(err, "generate", "changelog") return cli.Err("%s: %w", i18n.T("i18n.fail.generate", "changelog"), err)
} }
cli.Text(changelog) cli.Text(changelog)
return nil return nil
} }
func latestTag(dir string) (string, error) {
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

View file

@ -8,7 +8,7 @@ import (
// Style aliases from shared // Style aliases from shared
var ( var (
releaseHeaderStyle = cli.RepoNameStyle releaseHeaderStyle = cli.RepoStyle
releaseSuccessStyle = cli.SuccessStyle releaseSuccessStyle = cli.SuccessStyle
releaseErrorStyle = cli.ErrorStyle releaseErrorStyle = cli.ErrorStyle
releaseDimStyle = cli.DimStyle releaseDimStyle = cli.DimStyle

View file

@ -1,74 +1,43 @@
package ci package ci
import ( import (
"bufio"
"os" "os"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
// runCIReleaseInit creates a release configuration interactively.
func runCIReleaseInit() error { func runCIReleaseInit() error {
projectDir, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return cli.WrapVerb(err, "get", "working directory") return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
} }
// Check if config already exists cli.Print("%s %s\n\n", releaseDimStyle.Render(i18n.Label("init")), i18n.T("cmd.ci.init.initializing"))
if release.ConfigExists(projectDir) {
cli.Print("%s %s %s\n",
releaseDimStyle.Render(i18n.Label("note")),
i18n.T("cmd.ci.init.config_exists"),
release.ConfigPath(projectDir))
reader := bufio.NewReader(os.Stdin) // Check if already initialized
cli.Print("%s", i18n.T("cmd.ci.init.overwrite_prompt")) if release.ConfigExists(cwd) {
response, _ := reader.ReadString('\n') cli.Text(i18n.T("cmd.ci.init.already_initialized"))
response = strings.TrimSpace(strings.ToLower(response)) return nil
if response != "y" && response != "yes" {
cli.Text(i18n.T("common.prompt.abort"))
return nil
}
} }
cli.Print("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.init")), i18n.T("cmd.ci.init.creating")) // Create release config
cli.Line("")
reader := bufio.NewReader(os.Stdin)
// Project name
defaultName := filepath.Base(projectDir)
cli.Print("%s [%s]: ", i18n.T("cmd.ci.init.project_name"), defaultName)
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
if name == "" {
name = defaultName
}
// Repository
cli.Print("%s ", i18n.T("cmd.ci.init.github_repo"))
repo, _ := reader.ReadString('\n')
repo = strings.TrimSpace(repo)
// Create config
cfg := release.DefaultConfig() cfg := release.DefaultConfig()
cfg.Project.Name = name if err := release.WriteConfig(cfg, cwd); err != nil {
cfg.Project.Repository = repo return cli.Err("%s: %w", i18n.T("i18n.fail.create", "config"), err)
// Write config
if err := release.WriteConfig(cfg, projectDir); err != nil {
return cli.WrapVerb(err, "write", "config")
} }
cli.Line("") cli.Blank()
cli.Print("%s %s %s\n", cli.Print("%s %s\n", releaseSuccessStyle.Render("v"), i18n.T("cmd.ci.init.created_config"))
releaseSuccessStyle.Render(i18n.T("i18n.done.pass")),
i18n.T("cmd.ci.init.config_written"), // Templates init removed as functionality not exposed
release.ConfigPath(projectDir))
cli.Blank()
cli.Text(i18n.T("cmd.ci.init.next_steps"))
cli.Print(" %s\n", i18n.T("cmd.ci.init.edit_config"))
cli.Print(" %s\n", i18n.T("cmd.ci.init.run_ci"))
return nil return nil
} }

View file

@ -51,7 +51,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
} else { } else {
cli.Print(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch"))) cli.Print(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch")))
} }
cli.Line("") cli.Blank()
// Check for publishers // Check for publishers
if len(cfg.Publishers) == 0 { if len(cfg.Publishers) == 0 {
@ -66,7 +66,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
} }
// Print summary // Print summary
cli.Line("") cli.Blank()
cli.Print("%s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed")) cli.Print("%s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed"))
cli.Print(" %s %s\n", i18n.Label("version"), releaseValueStyle.Render(rel.Version)) cli.Print(" %s %s\n", i18n.Label("version"), releaseValueStyle.Render(rel.Version))
cli.Print(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts)) cli.Print(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts))

20
pkg/cli/ansi_test.go Normal file
View file

@ -0,0 +1,20 @@
package cli
import (
"strings"
"testing"
)
func TestAnsiStyle_Render(t *testing.T) {
s := NewStyle().Bold().Foreground("#ff0000")
got := s.Render("test")
if got == "test" {
t.Error("Expected styled output")
}
if !strings.Contains(got, "test") {
t.Error("Output should contain text")
}
if !strings.Contains(got, "[1m") {
t.Error("Output should contain bold code")
}
}

23
pkg/cli/glyph_test.go Normal file
View file

@ -0,0 +1,23 @@
package cli
import "testing"
func TestGlyph(t *testing.T) {
UseUnicode()
if Glyph(":check:") != "✓" {
t.Errorf("Expected ✓, got %s", Glyph(":check:"))
}
UseASCII()
if Glyph(":check:") != "[OK]" {
t.Errorf("Expected [OK], got %s", Glyph(":check:"))
}
}
func TestCompileGlyphs(t *testing.T) {
UseUnicode()
got := compileGlyphs("Status: :check:")
if got != "Status: ✓" {
t.Errorf("Expected Status: ✓, got %s", got)
}
}

25
pkg/cli/layout_test.go Normal file
View file

@ -0,0 +1,25 @@
package cli
import "testing"
func TestParseVariant(t *testing.T) {
c, err := ParseVariant("H[LC]F")
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if _, ok := c.regions[RegionHeader]; !ok {
t.Error("Expected Header region")
}
if _, ok := c.regions[RegionFooter]; !ok {
t.Error("Expected Footer region")
}
hSlot := c.regions[RegionHeader]
if hSlot.child == nil {
t.Error("Header should have child layout")
} else {
if _, ok := hSlot.child.regions[RegionLeft]; !ok {
t.Error("Child should have Left region")
}
}
}

View file

@ -30,6 +30,11 @@ func Println(format string, args ...any) {
fmt.Println(compileGlyphs(fmt.Sprintf(format, args...))) fmt.Println(compileGlyphs(fmt.Sprintf(format, args...)))
} }
// Text prints arguments like fmt.Println, but handling glyphs.
func Text(args ...any) {
fmt.Println(compileGlyphs(fmt.Sprint(args...)))
}
// Success prints a success message with checkmark (green). // Success prints a success message with checkmark (green).
func Success(msg string) { func Success(msg string) {
fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg)) fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg))

View file

@ -17,7 +17,7 @@ import (
var ( var (
ciSuccessStyle = cli.SuccessStyle ciSuccessStyle = cli.SuccessStyle
ciFailureStyle = cli.ErrorStyle ciFailureStyle = cli.ErrorStyle
ciPendingStyle = cli.StatusWarningStyle ciPendingStyle = cli.WarningStyle
ciSkippedStyle = cli.DimStyle ciSkippedStyle = cli.DimStyle
) )
@ -144,7 +144,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
} }
// Print summary // Print summary
cli.Line("") cli.Blank()
cli.Print("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)})) cli.Print("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)}))
if success > 0 { if success > 0 {
cli.Print(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success}))) cli.Print(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success})))
@ -158,8 +158,8 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
if len(noCI) > 0 { if len(noCI) > 0 {
cli.Print(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)}))) cli.Print(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)})))
} }
cli.Line("") cli.Blank()
cli.Line("") cli.Blank()
// Filter if needed // Filter if needed
displayRuns := allRuns displayRuns := allRuns
@ -179,7 +179,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
// Print errors // Print errors
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
cli.Line("") cli.Blank()
for _, err := range fetchErrors { for _, err := range fetchErrors {
cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err)
} }

View file

@ -120,19 +120,19 @@ func runCommit(registryPath string, all bool) error {
if s.Staged > 0 { if s.Staged > 0 {
cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged})))
} }
cli.Line("") cli.Blank()
} }
// Confirm unless --all // Confirm unless --all
if !all { if !all {
cli.Line("") cli.Blank()
if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) { if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
cli.Text(i18n.T("cli.aborted")) cli.Text(i18n.T("cli.aborted"))
return nil return nil
} }
} }
cli.Line("") cli.Blank()
// Commit each dirty repo // Commit each dirty repo
var succeeded, failed int var succeeded, failed int
@ -146,7 +146,7 @@ func runCommit(registryPath string, all bool) error {
cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed"))
succeeded++ succeeded++
} }
cli.Line("") cli.Blank()
} }
// Summary // Summary
@ -154,7 +154,7 @@ func runCommit(registryPath string, all bool) error {
if failed > 0 { if failed > 0 {
cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed})))
} }
cli.Line("") cli.Blank()
return nil return nil
} }
@ -200,18 +200,18 @@ func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error {
if s.Staged > 0 { if s.Staged > 0 {
cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged})))
} }
cli.Line("") cli.Blank()
// Confirm unless --all // Confirm unless --all
if !all { if !all {
cli.Line("") cli.Blank()
if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) { if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
cli.Text(i18n.T("cli.aborted")) cli.Text(i18n.T("cli.aborted"))
return nil return nil
} }
} }
cli.Line("") cli.Blank()
// Commit // Commit
if err := claudeCommit(ctx, repoPath, repoName, ""); err != nil { if err := claudeCommit(ctx, repoPath, repoName, ""); err != nil {
@ -220,4 +220,4 @@ func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error {
} }
cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed"))
return nil return nil
} }

View file

@ -45,14 +45,14 @@ var (
dimStyle = cli.DimStyle dimStyle = cli.DimStyle
valueStyle = cli.ValueStyle valueStyle = cli.ValueStyle
headerStyle = cli.HeaderStyle headerStyle = cli.HeaderStyle
repoNameStyle = cli.RepoNameStyle repoNameStyle = cli.RepoStyle
) )
// Table styles for status display (extends shared styles with cell padding) // Table styles for status display (extends shared styles with cell padding)
var ( var (
dirtyStyle = cli.GitDirtyStyle.Padding(0, 1) dirtyStyle = cli.NewStyle().Foreground(cli.ColourRed500)
aheadStyle = cli.GitAheadStyle.Padding(0, 1) aheadStyle = cli.NewStyle().Foreground(cli.ColourAmber500)
cleanStyle = cli.GitCleanStyle.Padding(0, 1) cleanStyle = cli.NewStyle().Foreground(cli.ColourGreen500)
) )
// AddDevCommands registers the 'dev' command and all subcommands. // AddDevCommands registers the 'dev' command and all subcommands.

View file

@ -2,8 +2,10 @@ package dev
import ( import (
"context" "context"
"fmt"
"os" "os"
"sort" "sort"
"strings"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
@ -116,9 +118,9 @@ func runHealth(registryPath string, verbose bool) error {
} }
// Print summary line // Print summary line
cli.Line("") cli.Blank()
printHealthSummary(totalRepos, dirtyRepos, aheadRepos, behindRepos, errorRepos) printHealthSummary(totalRepos, dirtyRepos, aheadRepos, behindRepos, errorRepos)
cli.Line("") cli.Blank()
// Verbose output // Verbose output
if verbose { if verbose {
@ -134,7 +136,7 @@ func runHealth(registryPath string, verbose bool) error {
if len(errorRepos) > 0 { if len(errorRepos) > 0 {
cli.Print("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos)) cli.Print("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos))
} }
cli.Line("") cli.Blank()
} }
return nil return nil
@ -142,36 +144,36 @@ func runHealth(registryPath string, verbose bool) error {
func printHealthSummary(total int, dirty, ahead, behind, errors []string) { func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
parts := []string{ parts := []string{
cli.StatusPart(total, i18n.T("cmd.dev.health.repos"), cli.ValueStyle), statusPart(total, i18n.T("cmd.dev.health.repos"), cli.ValueStyle),
} }
// Dirty status // Dirty status
if len(dirty) > 0 { if len(dirty) > 0 {
parts = append(parts, cli.StatusPart(len(dirty), i18n.T("common.status.dirty"), cli.WarningStyle)) parts = append(parts, statusPart(len(dirty), i18n.T("common.status.dirty"), cli.WarningStyle))
} else { } else {
parts = append(parts, cli.StatusText(i18n.T("cmd.dev.status.clean"), cli.SuccessStyle)) parts = append(parts, statusText(i18n.T("cmd.dev.status.clean"), cli.SuccessStyle))
} }
// Push status // Push status
if len(ahead) > 0 { if len(ahead) > 0 {
parts = append(parts, cli.StatusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), cli.ValueStyle)) parts = append(parts, statusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), cli.ValueStyle))
} else { } else {
parts = append(parts, cli.StatusText(i18n.T("common.status.synced"), cli.SuccessStyle)) parts = append(parts, statusText(i18n.T("common.status.synced"), cli.SuccessStyle))
} }
// Pull status // Pull status
if len(behind) > 0 { if len(behind) > 0 {
parts = append(parts, cli.StatusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), cli.WarningStyle)) parts = append(parts, statusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), cli.WarningStyle))
} else { } else {
parts = append(parts, cli.StatusText(i18n.T("common.status.up_to_date"), cli.SuccessStyle)) parts = append(parts, statusText(i18n.T("common.status.up_to_date"), cli.SuccessStyle))
} }
// Errors (only if any) // Errors (only if any)
if len(errors) > 0 { if len(errors) > 0 {
parts = append(parts, cli.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), cli.ErrorStyle)) parts = append(parts, statusPart(len(errors), i18n.T("cmd.dev.health.errors"), cli.ErrorStyle))
} }
cli.Text(cli.StatusLine(parts...)) cli.Text(statusLine(parts...))
} }
func formatRepoList(reposList []string) string { func formatRepoList(reposList []string) string {
@ -191,3 +193,15 @@ func joinRepos(reposList []string) string {
} }
return result 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, " | ")
}

View file

@ -12,8 +12,8 @@ import (
// Impact-specific styles (aliases to shared) // Impact-specific styles (aliases to shared)
var ( var (
impactDirectStyle = cli.ErrorStyle impactDirectStyle = cli.ErrorStyle
impactIndirectStyle = cli.StatusWarningStyle impactIndirectStyle = cli.WarningStyle
impactSafeStyle = cli.StatusSuccessStyle impactSafeStyle = cli.SuccessStyle
) )
// Impact command flags // Impact command flags
@ -89,12 +89,12 @@ func runImpact(registryPath string, repoName string) error {
sort.Strings(indirect) sort.Strings(indirect)
// Print results // Print results
cli.Line("") cli.Blank()
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName))
if repo.Description != "" { if repo.Description != "" {
cli.Print("%s\n", dimStyle.Render(repo.Description)) cli.Print("%s\n", dimStyle.Render(repo.Description))
} }
cli.Line("") cli.Blank()
if len(allAffected) == 0 { if len(allAffected) == 0 {
cli.Print("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName})) cli.Print("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName}))
@ -115,7 +115,7 @@ func runImpact(registryPath string, repoName string) error {
} }
cli.Print(" %s%s\n", d, desc) cli.Print(" %s%s\n", d, desc)
} }
cli.Line("") cli.Blank()
} }
// Indirect dependents // Indirect dependents
@ -132,7 +132,7 @@ func runImpact(registryPath string, repoName string) error {
} }
cli.Print(" %s%s\n", d, desc) cli.Print(" %s%s\n", d, desc)
} }
cli.Line("") cli.Blank()
} }
// Summary // Summary

View file

@ -20,7 +20,7 @@ var (
issueNumberStyle = cli.TitleStyle issueNumberStyle = cli.TitleStyle
issueTitleStyle = cli.ValueStyle issueTitleStyle = cli.ValueStyle
issueLabelStyle = cli.WarningStyle issueLabelStyle = cli.WarningStyle
issueAssigneeStyle = cli.StatusSuccessStyle issueAssigneeStyle = cli.SuccessStyle
issueAgeStyle = cli.DimStyle issueAgeStyle = cli.DimStyle
) )
@ -135,7 +135,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
// Print issues // Print issues
if len(allIssues) == 0 { if len(allIssues) == 0 {
cli.Text(i18n.T("cmd.dev.issues.no_issues")) cli.Text(i18n.T("cmd.dev.issues.no_issues"))
return nil return nil
} }
@ -147,7 +147,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
// Print any errors // Print any errors
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
cli.Line("") cli.Blank()
for _, err := range fetchErrors { for _, err := range fetchErrors {
cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err)
} }
@ -226,5 +226,5 @@ func printIssue(issue GitHubIssue) {
age := cli.FormatAge(issue.CreatedAt) age := cli.FormatAge(issue.CreatedAt)
line += " " + issueAgeStyle.Render(age) line += " " + issueAgeStyle.Render(age)
cli.Text(line) cli.Text(line)
} }

View file

@ -115,7 +115,7 @@ func runPull(registryPath string, all bool) error {
dimStyle.Render(i18n.T("cmd.dev.pull.commits_behind", map[string]interface{}{"Count": s.Behind})), dimStyle.Render(i18n.T("cmd.dev.pull.commits_behind", map[string]interface{}{"Count": s.Behind})),
) )
} }
cli.Line("") cli.Blank()
} }
// Pull each repo // Pull each repo
@ -134,12 +134,12 @@ func runPull(registryPath string, all bool) error {
} }
// Summary // Summary
cli.Line("") cli.Blank()
cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded}))) cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded})))
if failed > 0 { if failed > 0 {
cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed})))
} }
cli.Line("") cli.Blank()
return nil return nil
} }

View file

@ -119,14 +119,14 @@ func runPush(registryPath string, force bool) error {
// Confirm unless --force // Confirm unless --force
if !force { if !force {
cli.Line("") cli.Blank()
if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) { if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
cli.Text(i18n.T("cli.aborted")) cli.Text(i18n.T("cli.aborted"))
return nil return nil
} }
} }
cli.Line("") cli.Blank()
// Push sequentially (SSH passphrase needs interaction) // Push sequentially (SSH passphrase needs interaction)
var pushPaths []string var pushPaths []string
@ -157,10 +157,10 @@ func runPush(registryPath string, force bool) error {
// Handle diverged repos - offer to pull and retry // Handle diverged repos - offer to pull and retry
if len(divergedRepos) > 0 { if len(divergedRepos) > 0 {
cli.Line("") cli.Blank()
cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
cli.Line("") cli.Blank()
for _, r := range divergedRepos { for _, r := range divergedRepos {
cli.Print(" %s %s...\n", dimStyle.Render("↓"), r.Name) cli.Print(" %s %s...\n", dimStyle.Render("↓"), r.Name)
if err := git.Pull(ctx, r.Path); err != nil { if err := git.Pull(ctx, r.Path); err != nil {
@ -180,12 +180,12 @@ func runPush(registryPath string, force bool) error {
} }
// Summary // Summary
cli.Line("") cli.Blank()
cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded}))) cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded})))
if failed > 0 { if failed > 0 {
cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed})))
} }
cli.Line("") cli.Blank()
return nil return nil
} }
@ -222,10 +222,10 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error {
if s.Staged > 0 { if s.Staged > 0 {
cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged})))
} }
cli.Line("") cli.Blank()
cli.Line("") cli.Blank()
if cli.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) { if cli.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) {
cli.Line("") cli.Blank()
// Use edit-enabled commit if only untracked files (may need .gitignore fix) // Use edit-enabled commit if only untracked files (may need .gitignore fix)
var err error var err error
if s.Modified == 0 && s.Staged == 0 && s.Untracked > 0 { if s.Modified == 0 && s.Staged == 0 && s.Untracked > 0 {
@ -257,24 +257,24 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error {
// Confirm unless --force // Confirm unless --force
if !force { if !force {
cli.Line("") cli.Blank()
if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) { if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) {
cli.Text(i18n.T("cli.aborted")) cli.Text(i18n.T("cli.aborted"))
return nil return nil
} }
} }
cli.Line("") cli.Blank()
// Push // Push
err := git.Push(ctx, repoPath) err := git.Push(ctx, repoPath)
if err != nil { if err != nil {
if git.IsNonFastForward(err) { if git.IsNonFastForward(err) {
cli.Print(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged")) cli.Print(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged"))
cli.Line("") cli.Blank()
cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
cli.Line("") cli.Blank()
cli.Print(" %s %s...\n", dimStyle.Render("↓"), repoName) cli.Print(" %s %s...\n", dimStyle.Render("↓"), repoName)
if pullErr := git.Pull(ctx, repoPath); pullErr != nil { if pullErr := git.Pull(ctx, repoPath); pullErr != nil {
cli.Print(" %s %s: %s\n", errorStyle.Render("x"), repoName, pullErr) cli.Print(" %s %s: %s\n", errorStyle.Render("x"), repoName, pullErr)

View file

@ -16,7 +16,7 @@ import (
// PR-specific styles (aliases to shared) // PR-specific styles (aliases to shared)
var ( var (
prNumberStyle = cli.PrNumberStyle prNumberStyle = cli.NumberStyle
prTitleStyle = cli.ValueStyle prTitleStyle = cli.ValueStyle
prAuthorStyle = cli.InfoStyle prAuthorStyle = cli.InfoStyle
prApprovedStyle = cli.SuccessStyle prApprovedStyle = cli.SuccessStyle
@ -162,7 +162,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
} }
} }
cli.Line("") cli.Blank()
cli.Print("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)})) cli.Print("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)}))
if pending > 0 { if pending > 0 {
cli.Print(" * %s", prPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending}))) cli.Print(" * %s", prPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending})))
@ -173,8 +173,8 @@ func runReviews(registryPath string, author string, showAll bool) error {
if changesRequested > 0 { if changesRequested > 0 {
cli.Print(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested}))) cli.Print(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested})))
} }
cli.Line("") cli.Blank()
cli.Line("") cli.Blank()
for _, pr := range allPRs { for _, pr := range allPRs {
printPR(pr) printPR(pr)
@ -182,7 +182,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
// Print any errors // Print any errors
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
cli.Line("") cli.Blank()
for _, err := range fetchErrors { for _, err := range fetchErrors {
cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err)
} }

View file

@ -47,15 +47,15 @@ func runVMInstall() error {
if d.IsInstalled() { if d.IsInstalled() {
cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.already_installed"))) cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.already_installed")))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")})) cli.Text(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")}))
return nil return nil
} }
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("image")), devops.ImageName()) cli.Print("%s %s\n", dimStyle.Render(i18n.Label("image")), devops.ImageName())
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.downloading")) cli.Text(i18n.T("cmd.dev.vm.downloading"))
cli.Line("") cli.Blank()
ctx := context.Background() ctx := context.Background()
start := time.Now() start := time.Now()
@ -71,16 +71,16 @@ func runVMInstall() error {
} }
}) })
cli.Line("") // Clear progress line cli.Blank() // Clear progress line
if err != nil { if err != nil {
return cli.Wrap(err, "install failed") return cli.Wrap(err, "install failed")
} }
elapsed := time.Since(start).Round(time.Second) elapsed := time.Since(start).Round(time.Second)
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed})) cli.Text(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed}))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
return nil return nil
@ -131,7 +131,7 @@ func runVMBoot(memory, cpus int, fresh bool) error {
opts.Fresh = fresh opts.Fresh = fresh
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs})) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs}))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.booting")) cli.Text(i18n.T("cmd.dev.vm.booting"))
ctx := context.Background() ctx := context.Background()
@ -139,9 +139,9 @@ func runVMBoot(memory, cpus int, fresh bool) error {
return err return err
} }
cli.Line("") cli.Blank()
cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.running"))) cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.running")))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")})) cli.Text(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")}))
cli.Print("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222")) cli.Print("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222"))
@ -216,7 +216,7 @@ func runVMStatus() error {
} }
cli.Text(headerStyle.Render(i18n.T("cmd.dev.vm.status_title"))) cli.Text(headerStyle.Render(i18n.T("cmd.dev.vm.status_title")))
cli.Line("") cli.Blank()
// Installation status // Installation status
if status.Installed { if status.Installed {
@ -226,12 +226,12 @@ func runVMStatus() error {
} }
} else { } else {
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no"))) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no")))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")})) cli.Text(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")}))
return nil return nil
} }
cli.Line("") cli.Blank()
// Running status // Running status
if status.Running { if status.Running {
@ -243,7 +243,7 @@ func runVMStatus() error {
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime))
} else { } else {
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), dimStyle.Render(i18n.T("common.status.stopped"))) cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), dimStyle.Render(i18n.T("common.status.stopped")))
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
} }
@ -453,7 +453,7 @@ func runVMUpdate(apply bool) error {
ctx := context.Background() ctx := context.Background()
cli.Text(i18n.T("common.progress.checking_updates")) cli.Text(i18n.T("common.progress.checking_updates"))
cli.Line("") cli.Blank()
current, latest, hasUpdate, err := d.CheckUpdate(ctx) current, latest, hasUpdate, err := d.CheckUpdate(ctx)
if err != nil { if err != nil {
@ -462,7 +462,7 @@ func runVMUpdate(apply bool) error {
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("current")), valueStyle.Render(current)) cli.Print("%s %s\n", dimStyle.Render(i18n.Label("current")), valueStyle.Render(current))
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest)) cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest))
cli.Line("") cli.Blank()
if !hasUpdate { if !hasUpdate {
cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date"))) cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date")))
@ -470,7 +470,7 @@ func runVMUpdate(apply bool) error {
} }
cli.Text(warningStyle.Render(i18n.T("cmd.dev.vm.update_available"))) cli.Text(warningStyle.Render(i18n.T("cmd.dev.vm.update_available")))
cli.Line("") cli.Blank()
if !apply { if !apply {
cli.Text(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")})) cli.Text(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")}))
@ -485,7 +485,7 @@ func runVMUpdate(apply bool) error {
} }
cli.Text(i18n.T("cmd.dev.vm.downloading_update")) cli.Text(i18n.T("cmd.dev.vm.downloading_update"))
cli.Line("") cli.Blank()
start := time.Now() start := time.Now()
err = d.Install(ctx, func(downloaded, total int64) { err = d.Install(ctx, func(downloaded, total int64) {
@ -495,14 +495,14 @@ func runVMUpdate(apply bool) error {
} }
}) })
cli.Line("") cli.Blank()
if err != nil { if err != nil {
return cli.Wrap(err, "update failed") return cli.Wrap(err, "update failed")
} }
elapsed := time.Since(start).Round(time.Second) elapsed := time.Since(start).Round(time.Second)
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed})) cli.Text(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed}))
return nil return nil

View file

@ -106,9 +106,9 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Auto-commit dirty repos if requested // Auto-commit dirty repos if requested
if autoCommit && len(dirtyRepos) > 0 { if autoCommit && len(dirtyRepos) > 0 {
cli.Line("") cli.Blank()
cli.Print("%s\n", cli.TitleStyle.Render(i18n.T("cmd.dev.commit.committing"))) cli.Print("%s\n", cli.TitleStyle.Render(i18n.T("cmd.dev.commit.committing")))
cli.Line("") cli.Blank()
for _, s := range dirtyRepos { for _, s := range dirtyRepos {
// PERFORM commit via agentic service // PERFORM commit via agentic service
@ -146,7 +146,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// If status only, we're done // If status only, we're done
if statusOnly { if statusOnly {
if len(dirtyRepos) > 0 && !autoCommit { if len(dirtyRepos) > 0 && !autoCommit {
cli.Line("") cli.Blank()
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag"))) cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag")))
} }
return nil return nil
@ -154,24 +154,24 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Push repos with unpushed commits // Push repos with unpushed commits
if len(aheadRepos) == 0 { if len(aheadRepos) == 0 {
cli.Line("") cli.Blank()
cli.Text(i18n.T("cmd.dev.work.all_up_to_date")) cli.Text(i18n.T("cmd.dev.work.all_up_to_date"))
return nil return nil
} }
cli.Line("") cli.Blank()
cli.Print("%s\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)})) cli.Print("%s\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)}))
for _, s := range aheadRepos { for _, s := range aheadRepos {
cli.Print(" %s: %s\n", s.Name, i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead})) cli.Print(" %s: %s\n", s.Name, i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead}))
} }
cli.Line("") cli.Blank()
if !cli.Confirm(i18n.T("cmd.dev.push.confirm")) { if !cli.Confirm(i18n.T("cmd.dev.push.confirm")) {
cli.Text(i18n.T("cli.aborted")) cli.Text(i18n.T("cli.aborted"))
return nil return nil
} }
cli.Line("") cli.Blank()
// PERFORM push for each repo // PERFORM push for each repo
var divergedRepos []git.RepoStatus var divergedRepos []git.RepoStatus
@ -199,10 +199,10 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Handle diverged repos - offer to pull and retry // Handle diverged repos - offer to pull and retry
if len(divergedRepos) > 0 { if len(divergedRepos) > 0 {
cli.Line("") cli.Blank()
cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
cli.Line("") cli.Blank()
for _, s := range divergedRepos { for _, s := range divergedRepos {
cli.Print(" %s %s...\n", dimStyle.Render("↓"), s.Name) cli.Print(" %s %s...\n", dimStyle.Render("↓"), s.Name)

View file

@ -73,7 +73,7 @@ func (s *Service) runWork(task TaskWork) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
cli.Text("No git repositories found") cli.Println("No git repositories found")
return nil return nil
} }
@ -116,9 +116,9 @@ func (s *Service) runWork(task TaskWork) error {
// Auto-commit dirty repos if requested // Auto-commit dirty repos if requested
if task.AutoCommit && len(dirtyRepos) > 0 { if task.AutoCommit && len(dirtyRepos) > 0 {
cli.Line("") cli.Blank()
cli.Text("Committing changes...") cli.Println("Committing changes...")
cli.Line("") cli.Blank()
for _, repo := range dirtyRepos { for _, repo := range dirtyRepos {
_, handled, err := s.Core().PERFORM(agentic.TaskCommit{ _, handled, err := s.Core().PERFORM(agentic.TaskCommit{
@ -156,35 +156,35 @@ func (s *Service) runWork(task TaskWork) error {
// If status only, we're done // If status only, we're done
if task.StatusOnly { if task.StatusOnly {
if len(dirtyRepos) > 0 && !task.AutoCommit { if len(dirtyRepos) > 0 && !task.AutoCommit {
cli.Line("") cli.Blank()
cli.Text("Use --commit flag to auto-commit dirty repos") cli.Println("Use --commit flag to auto-commit dirty repos")
} }
return nil return nil
} }
// Push repos with unpushed commits // Push repos with unpushed commits
if len(aheadRepos) == 0 { if len(aheadRepos) == 0 {
cli.Line("") cli.Blank()
cli.Text("All repositories are up to date") cli.Println("All repositories are up to date")
return nil return nil
} }
cli.Line("") cli.Blank()
cli.Print("%d repos with unpushed commits:\n", len(aheadRepos)) cli.Print("%d repos with unpushed commits:\n", len(aheadRepos))
for _, st := range aheadRepos { for _, st := range aheadRepos {
cli.Print(" %s: %d commits\n", st.Name, st.Ahead) cli.Print(" %s: %d commits\n", st.Name, st.Ahead)
} }
cli.Line("") cli.Blank()
cli.Print("Push all? [y/N] ") cli.Print("Push all? [y/N] ")
var answer string var answer string
cli.Scanln(&answer) cli.Scanln(&answer)
if strings.ToLower(answer) != "y" { if strings.ToLower(answer) != "y" {
cli.Text("Aborted") cli.Println("Aborted")
return nil return nil
} }
cli.Line("") cli.Blank()
// Push each repo // Push each repo
for _, st := range aheadRepos { for _, st := range aheadRepos {
@ -217,7 +217,7 @@ func (s *Service) runStatus(task TaskStatus) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
cli.Text("No git repositories found") cli.Println("No git repositories found")
return nil return nil
} }

View file

@ -8,7 +8,7 @@ import (
// Style and utility aliases from shared // Style and utility aliases from shared
var ( var (
repoNameStyle = cli.RepoNameStyle repoNameStyle = cli.RepoStyle
successStyle = cli.SuccessStyle successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle dimStyle = cli.DimStyle

View file

@ -42,11 +42,11 @@ func runDocsList(registryPath string) error {
for _, repo := range reg.List() { for _, repo := range reg.List() {
info := scanRepoDocs(repo) info := scanRepoDocs(repo)
readme := cli.CheckMark(info.Readme != "") readme := checkMark(info.Readme != "")
claude := cli.CheckMark(info.ClaudeMd != "") claude := checkMark(info.ClaudeMd != "")
changelog := cli.CheckMark(info.Changelog != "") changelog := checkMark(info.Changelog != "")
docsDir := cli.CheckMark(false) docsDir := checkMark(false)
if len(info.DocsFiles) > 0 { if len(info.DocsFiles) > 0 {
docsDir = docsFoundStyle.Render(i18n.T("common.count.files", map[string]interface{}{"Count": len(info.DocsFiles)})) docsDir = docsFoundStyle.Render(i18n.T("common.count.files", map[string]interface{}{"Count": len(info.DocsFiles)}))
} }
@ -66,11 +66,18 @@ func runDocsList(registryPath string) error {
} }
} }
cli.Line("") cli.Blank()
cli.Print("%s %s\n", cli.Print("%s %s\n",
cli.Label(i18n.Label("coverage")), cli.KeyStyle.Render(i18n.Label("coverage")),
i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}), i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}),
) )
return nil return nil
} }
func checkMark(ok bool) string {
if ok {
return cli.Glyph(":check:")
}
return cli.Glyph(":cross:")
}

View file

@ -113,14 +113,14 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
} }
// Confirm // Confirm
cli.Line("") cli.Blank()
if !confirm(i18n.T("cmd.docs.sync.confirm")) { if !confirm(i18n.T("cmd.docs.sync.confirm")) {
cli.Text(i18n.T("common.prompt.abort")) cli.Text(i18n.T("common.prompt.abort"))
return nil return nil
} }
// Sync docs // Sync docs
cli.Line("") cli.Blank()
var synced int var synced int
for _, info := range docsInfo { for _, info := range docsInfo {
outName := packageOutputName(info.Name) outName := packageOutputName(info.Name)
@ -152,4 +152,4 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.sync")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced})) cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.sync")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced}))
return nil return nil
} }

View file

@ -44,13 +44,13 @@ func runDoctor(verbose bool) error {
ok, version := runCheck(c) ok, version := runCheck(c)
if ok { if ok {
if verbose { if verbose {
fmt.Println(cli.CheckResult(true, c.name, version)) fmt.Println(formatCheckResult(true, c.name, version))
} else { } else {
fmt.Println(cli.CheckResult(true, c.name, "")) fmt.Println(formatCheckResult(true, c.name, ""))
} }
passed++ passed++
} else { } else {
fmt.Printf(" %s %s - %s\n", errorStyle.Render(cli.SymbolCross), c.name, c.description) fmt.Printf(" %s %s - %s\n", errorStyle.Render(cli.Glyph(":cross:")), c.name, c.description)
failed++ failed++
} }
} }
@ -61,13 +61,13 @@ func runDoctor(verbose bool) error {
ok, version := runCheck(c) ok, version := runCheck(c)
if ok { if ok {
if verbose { if verbose {
fmt.Println(cli.CheckResult(true, c.name, version)) fmt.Println(formatCheckResult(true, c.name, version))
} else { } else {
fmt.Println(cli.CheckResult(true, c.name, "")) fmt.Println(formatCheckResult(true, c.name, ""))
} }
passed++ passed++
} else { } else {
fmt.Printf(" %s %s - %s\n", dimStyle.Render(cli.SymbolSkip), c.name, dimStyle.Render(c.description)) fmt.Printf(" %s %s - %s\n", dimStyle.Render(cli.Glyph(":skip:")), c.name, dimStyle.Render(c.description))
optional++ optional++
} }
} }
@ -75,16 +75,16 @@ func runDoctor(verbose bool) error {
// Check GitHub access // Check GitHub access
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github")) fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github"))
if checkGitHubSSH() { if checkGitHubSSH() {
fmt.Println(cli.CheckResult(true, i18n.T("cmd.doctor.ssh_found"), "")) fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
} else { } else {
fmt.Printf(" %s %s\n", errorStyle.Render(cli.SymbolCross), i18n.T("cmd.doctor.ssh_missing")) fmt.Printf(" %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.ssh_missing"))
failed++ failed++
} }
if checkGitHubCLI() { if checkGitHubCLI() {
fmt.Println(cli.CheckResult(true, i18n.T("cmd.doctor.cli_auth"), "")) fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
} else { } else {
fmt.Printf(" %s %s\n", errorStyle.Render(cli.SymbolCross), i18n.T("cmd.doctor.cli_auth_missing")) fmt.Printf(" %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.cli_auth_missing"))
failed++ failed++
} }
@ -104,3 +104,18 @@ func runDoctor(verbose bool) error {
cli.Success(i18n.T("cmd.doctor.ready")) cli.Success(i18n.T("cmd.doctor.ready"))
return nil return nil
} }
func formatCheckResult(ok bool, name, detail string) string {
check := cli.Check(name)
if ok {
check.Pass()
} else {
check.Fail()
}
if detail != "" {
check.Message(detail)
} else {
check.Message("")
}
return check.String()
}

View file

@ -75,7 +75,7 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
if !jsonOut { if !jsonOut {
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) cli.Print("%s %s\n", dimStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests"))
cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), pkg) cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), pkg)
cli.Line("") cli.Blank()
} }
cmd := exec.Command("go", args...) cmd := exec.Command("go", args...)
@ -102,7 +102,7 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
if jsonOut { if jsonOut {
cli.Print(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`, cli.Print(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`,
passed, failed, skipped, cov, cmd.ProcessState.ExitCode()) passed, failed, skipped, cov, cmd.ProcessState.ExitCode())
cli.Line("") cli.Blank()
return err return err
} }
@ -113,15 +113,15 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
// Summary // Summary
if err == nil { if err == nil {
cli.Print(" %s %s\n", successStyle.Render(cli.SymbolCheck), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass")) cli.Print(" %s %s\n", successStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass"))
} else { } else {
cli.Print(" %s %s, %s\n", errorStyle.Render(cli.SymbolCross), cli.Print(" %s %s, %s\n", errorStyle.Render(cli.Glyph(":cross:")),
i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass"), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass"),
i18n.T("i18n.count.test", failed)+" "+i18n.T("i18n.done.fail")) i18n.T("i18n.count.test", failed)+" "+i18n.T("i18n.done.fail"))
} }
if cov > 0 { if cov > 0 {
cli.Print("\n %s %s\n", cli.ProgressLabel(i18n.Label("coverage")), cli.FormatCoverage(cov)) cli.Print("\n %s %s\n", cli.KeyStyle.Render(i18n.Label("coverage")), formatCoverage(cov))
} }
if err == nil { if err == nil {
@ -202,7 +202,7 @@ func addGoCovCommand(parent *cli.Command) {
displayPkg = displayPkg[:57] + "..." displayPkg = displayPkg[:57] + "..."
} }
cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), displayPkg) cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), displayPkg)
cli.Line("") cli.Blank()
// Run tests with coverage // Run tests with coverage
// We need to split pkg into individual arguments if it contains spaces // We need to split pkg into individual arguments if it contains spaces
@ -242,8 +242,8 @@ func addGoCovCommand(parent *cli.Command) {
} }
// Print coverage summary // Print coverage summary
cli.Line("") cli.Blank()
cli.Print(" %s %s\n", cli.ProgressLabel(i18n.Label("total")), cli.FormatCoverage(totalCov)) cli.Print(" %s %s\n", cli.KeyStyle.Render(i18n.Label("total")), formatCoverage(totalCov))
// Generate HTML if requested // Generate HTML if requested
if covHTML || covOpen { if covHTML || covOpen {
@ -319,3 +319,13 @@ func findTestPackages(root string) ([]string, error) {
} }
return pkgs, nil return pkgs, nil
} }
func formatCoverage(cov float64) string {
s := fmt.Sprintf("%.1f%%", cov)
if cov >= 80 {
return cli.SuccessStyle.Render(s)
} else if cov >= 50 {
return cli.WarningStyle.Render(s)
}
return cli.ErrorStyle.Render(s)
}

View file

@ -118,21 +118,21 @@ func runQAChecks(checkNames []string) error {
cli.Print("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name)) cli.Print("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name))
if err := runCheck(ctx, cwd, check); err != nil { if err := runCheck(ctx, cwd, check); err != nil {
cli.Print(" %s %s\n", cli.ErrorStyle.Render(cli.SymbolCross), err.Error()) cli.Print(" %s %s\n", cli.ErrorStyle.Render(cli.Glyph(":cross:")), err.Error())
failed++ failed++
} else { } else {
cli.Print(" %s %s\n", cli.SuccessStyle.Render(cli.SymbolCheck), i18n.T("i18n.done.pass")) cli.Print(" %s %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.done.pass"))
passed++ passed++
} }
} }
// Summary // Summary
cli.Line("") cli.Blank()
duration := time.Since(startTime).Round(time.Millisecond) duration := time.Since(startTime).Round(time.Millisecond)
if failed > 0 { if failed > 0 {
cli.Print("%s %s, %s (%s)\n", cli.Print("%s %s, %s (%s)\n",
cli.ErrorStyle.Render(cli.SymbolCross), cli.ErrorStyle.Render(cli.Glyph(":cross:")),
i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"),
i18n.T("i18n.count.check", failed)+" "+i18n.T("i18n.done.fail"), i18n.T("i18n.count.check", failed)+" "+i18n.T("i18n.done.fail"),
duration) duration)
@ -140,7 +140,7 @@ func runQAChecks(checkNames []string) error {
} }
cli.Print("%s %s (%s)\n", cli.Print("%s %s (%s)\n",
cli.SuccessStyle.Render(cli.SymbolCheck), cli.SuccessStyle.Render(cli.Glyph(":check:")),
i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"),
duration) duration)
@ -228,7 +228,7 @@ func runCheck(ctx context.Context, dir string, check QACheck) error {
} }
if len(output) > 0 { if len(output) > 0 {
// Show files that need formatting // Show files that need formatting
cli.Print(string(output)) cli.Text(string(output))
return cli.Err("%s (use --fix)", i18n.T("i18n.fail.format", i18n.T("i18n.count.file", len(output)))) return cli.Err("%s (use --fix)", i18n.T("i18n.fail.format", i18n.T("i18n.count.file", len(output))))
} }
return nil return nil

View file

@ -13,7 +13,7 @@ func init() {
// Style and utility aliases // Style and utility aliases
var ( var (
repoNameStyle = cli.RepoNameStyle repoNameStyle = cli.RepoStyle
successStyle = cli.SuccessStyle successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle dimStyle = cli.DimStyle

View file

@ -11,14 +11,14 @@ import (
// Style aliases from shared // Style aliases from shared
var ( var (
testHeaderStyle = cli.RepoNameStyle testHeaderStyle = cli.RepoStyle
testPassStyle = cli.SuccessStyle testPassStyle = cli.SuccessStyle
testFailStyle = cli.ErrorStyle testFailStyle = cli.ErrorStyle
testSkipStyle = cli.WarningStyle testSkipStyle = cli.WarningStyle
testDimStyle = cli.DimStyle testDimStyle = cli.DimStyle
testCovHighStyle = cli.CoverageHighStyle testCovHighStyle = cli.NewStyle().Foreground(cli.ColourGreen500)
testCovMedStyle = cli.CoverageMedStyle testCovMedStyle = cli.NewStyle().Foreground(cli.ColourAmber500)
testCovLowStyle = cli.CoverageLowStyle testCovLowStyle = cli.NewStyle().Foreground(cli.ColourRed500)
) )
// Flag variables for test command // Flag variables for test command

View file

@ -9,7 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
) )
@ -153,7 +153,13 @@ func printCoverageSummary(results testResults) {
} }
func formatCoverage(cov float64) string { func formatCoverage(cov float64) string {
return cli.FormatCoverage(cov) s := fmt.Sprintf("%.1f%%", cov)
if cov >= 80 {
return testCovHighStyle.Render(s)
} else if cov >= 50 {
return testCovMedStyle.Render(s)
}
return testCovLowStyle.Render(s)
} }
func shortenPackageName(name string) string { func shortenPackageName(name string) string {

View file

@ -221,10 +221,10 @@ func buildLinuxKitImage(yamlPath, outputPath string) error {
} }
// Build the image // Build the image
// linuxkit build -format iso-bios -name <output> <yaml> // linuxkit build --format iso-bios --name <output> <yaml>
cmd := exec.Command(lkPath, "build", cmd := exec.Command(lkPath, "build",
"-format", "iso-bios", "--format", "iso-bios",
"-name", outputPath, "--name", outputPath,
yamlPath) yamlPath)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout