cli/pkg/cli/styles.go
Snider 0072650fd9 feat: git command, build improvements, and go fmt git-aware (#74)
* feat(go): make go fmt git-aware by default

- By default, only check changed Go files (modified, staged, untracked)
- Add --all flag to check all files (previous behaviour)
- Reduces noise when running fmt on large codebases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(build): minimal output by default, add missing i18n

- Default output now shows single line: "Success Built N artifacts (dir)"
- Add --verbose/-v flag to show full detailed output
- Add all missing i18n translations for build commands
- Errors still show failure reason in minimal mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add root-level `core git` command

- Create pkg/gitcmd with git workflow commands as root menu
- Export command builders from pkg/dev (AddCommitCommand, etc.)
- Commands available under both `core git` and `core dev` for compatibility
- Git commands: health, commit, push, pull, work, sync, apply
- GitHub orchestration stays in dev: issues, reviews, ci, impact

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(qa): add docblock coverage checking

Implement docblock/docstring coverage analysis for Go code:
- New `core qa docblock` command to check coverage
- Shows compact file:line list when under threshold
- Integrate with `core go qa` as a default check
- Add --docblock-threshold flag (default 80%)

The checker uses Go AST parsing to find exported symbols
(functions, types, consts, vars) without documentation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- Fix doc comment: "status" → "health" in gitcmd package
- Implement --check flag for `core go fmt` (exits non-zero if files need formatting)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add docstrings for 100% coverage

Add documentation comments to all exported symbols:
- pkg/build: ProjectType constants
- pkg/cli: LogLevel, RenderStyle, TableStyle
- pkg/framework: ServiceFor, MustServiceFor, Core.Core
- pkg/git: GitError.Error, GitError.Unwrap
- pkg/i18n: Handler Match/Handle methods
- pkg/log: Level constants
- pkg/mcp: Tool input/output types
- pkg/php: Service constants, QA types, service methods
- pkg/process: ServiceError.Error
- pkg/repos: RepoType constants
- pkg/setup: ChangeType, ChangeCategory constants
- pkg/workspace: AddWorkspaceCommands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: standardize line endings to LF

Add .gitattributes to enforce LF line endings for all text files.
Normalize all existing files to use Unix-style line endings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- cmd_format.go: validate --check/--fix mutual exclusivity, capture stderr
- cmd_docblock.go: return error instead of os.Exit(1) for proper error handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 2)

- linuxkit.go: propagate state update errors, handle cmd.Wait() errors in waitForExit
- mcp.go: guard against empty old_string in editDiff to prevent runaway edits
- cmd_docblock.go: log parse errors instead of silently skipping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 10:48:44 +00:00

210 lines
5 KiB
Go

// Package cli provides semantic CLI output with zero external dependencies.
package cli
import (
"fmt"
"strings"
"time"
)
// Tailwind colour palette (hex strings)
const (
ColourBlue50 = "#eff6ff"
ColourBlue100 = "#dbeafe"
ColourBlue200 = "#bfdbfe"
ColourBlue300 = "#93c5fd"
ColourBlue400 = "#60a5fa"
ColourBlue500 = "#3b82f6"
ColourBlue600 = "#2563eb"
ColourBlue700 = "#1d4ed8"
ColourGreen400 = "#4ade80"
ColourGreen500 = "#22c55e"
ColourGreen600 = "#16a34a"
ColourRed400 = "#f87171"
ColourRed500 = "#ef4444"
ColourRed600 = "#dc2626"
ColourAmber400 = "#fbbf24"
ColourAmber500 = "#f59e0b"
ColourAmber600 = "#d97706"
ColourOrange500 = "#f97316"
ColourYellow500 = "#eab308"
ColourEmerald500 = "#10b981"
ColourPurple500 = "#a855f7"
ColourViolet400 = "#a78bfa"
ColourViolet500 = "#8b5cf6"
ColourIndigo500 = "#6366f1"
ColourCyan500 = "#06b6d4"
ColourGray50 = "#f9fafb"
ColourGray100 = "#f3f4f6"
ColourGray200 = "#e5e7eb"
ColourGray300 = "#d1d5db"
ColourGray400 = "#9ca3af"
ColourGray500 = "#6b7280"
ColourGray600 = "#4b5563"
ColourGray700 = "#374151"
ColourGray800 = "#1f2937"
ColourGray900 = "#111827"
)
// Core styles
var (
SuccessStyle = NewStyle().Bold().Foreground(ColourGreen500)
ErrorStyle = NewStyle().Bold().Foreground(ColourRed500)
WarningStyle = NewStyle().Bold().Foreground(ColourAmber500)
InfoStyle = NewStyle().Foreground(ColourBlue400)
DimStyle = NewStyle().Dim().Foreground(ColourGray500)
MutedStyle = NewStyle().Foreground(ColourGray600)
BoldStyle = NewStyle().Bold()
KeyStyle = NewStyle().Foreground(ColourGray400)
ValueStyle = NewStyle().Foreground(ColourGray200)
AccentStyle = NewStyle().Foreground(ColourCyan500)
LinkStyle = NewStyle().Foreground(ColourBlue500).Underline()
HeaderStyle = NewStyle().Bold().Foreground(ColourGray200)
TitleStyle = NewStyle().Bold().Foreground(ColourBlue500)
CodeStyle = NewStyle().Foreground(ColourGray300)
NumberStyle = NewStyle().Foreground(ColourBlue300)
RepoStyle = NewStyle().Bold().Foreground(ColourBlue500)
)
// Truncate shortens a string to max length with ellipsis.
func Truncate(s string, max int) string {
if len(s) <= max {
return s
}
if max <= 3 {
return s[:max]
}
return s[:max-3] + "..."
}
// Pad right-pads a string to width.
func Pad(s string, width int) string {
if len(s) >= width {
return s
}
return s + strings.Repeat(" ", width-len(s))
}
// FormatAge formats a time as human-readable age (e.g., "2h ago", "3d ago").
func FormatAge(t time.Time) string {
d := time.Since(t)
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
return fmt.Sprintf("%dm ago", int(d.Minutes()))
case d < 24*time.Hour:
return fmt.Sprintf("%dh ago", int(d.Hours()))
case d < 7*24*time.Hour:
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
case d < 30*24*time.Hour:
return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7)))
default:
return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30)))
}
}
// Table renders tabular data with aligned columns.
// HLCRF is for layout; Table is for tabular data - they serve different purposes.
type Table struct {
Headers []string
Rows [][]string
Style TableStyle
}
// TableStyle configures the appearance of table output.
type TableStyle struct {
HeaderStyle *AnsiStyle
CellStyle *AnsiStyle
Separator string
}
// DefaultTableStyle returns sensible defaults.
func DefaultTableStyle() TableStyle {
return TableStyle{
HeaderStyle: HeaderStyle,
CellStyle: nil,
Separator: " ",
}
}
// NewTable creates a table with headers.
func NewTable(headers ...string) *Table {
return &Table{
Headers: headers,
Style: DefaultTableStyle(),
}
}
// AddRow adds a row to the table.
func (t *Table) AddRow(cells ...string) *Table {
t.Rows = append(t.Rows, cells)
return t
}
// String renders the table.
func (t *Table) String() string {
if len(t.Headers) == 0 && len(t.Rows) == 0 {
return ""
}
// Calculate column widths
cols := len(t.Headers)
if cols == 0 && len(t.Rows) > 0 {
cols = len(t.Rows[0])
}
widths := make([]int, cols)
for i, h := range t.Headers {
if len(h) > widths[i] {
widths[i] = len(h)
}
}
for _, row := range t.Rows {
for i, cell := range row {
if i < cols && len(cell) > widths[i] {
widths[i] = len(cell)
}
}
}
var sb strings.Builder
sep := t.Style.Separator
// Headers
if len(t.Headers) > 0 {
for i, h := range t.Headers {
if i > 0 {
sb.WriteString(sep)
}
styled := Pad(h, widths[i])
if t.Style.HeaderStyle != nil {
styled = t.Style.HeaderStyle.Render(styled)
}
sb.WriteString(styled)
}
sb.WriteString("\n")
}
// Rows
for _, row := range t.Rows {
for i, cell := range row {
if i > 0 {
sb.WriteString(sep)
}
styled := Pad(cell, widths[i])
if t.Style.CellStyle != nil {
styled = t.Style.CellStyle.Render(styled)
}
sb.WriteString(styled)
}
sb.WriteString("\n")
}
return sb.String()
}
// Render prints the table to stdout.
func (t *Table) Render() {
fmt.Print(t.String())
}