fix: lint errors

This commit is contained in:
dataCenter430 2026-03-31 17:31:29 +02:00
parent 7cdd86df8b
commit 2f3e2ad9cf
8 changed files with 222 additions and 62 deletions

View file

@ -96,10 +96,10 @@ func InnerStderrWidth() int {
} }
var ( var (
accentBlue = lipgloss.Color("#3E5DB9") accentBlue = lipgloss.Color("#3E5DB9")
accentRed = lipgloss.Color("#D54646") accentRed = lipgloss.Color("#D54646")
colorMuted = lipgloss.Color("#6B6B6B") colorMuted = lipgloss.Color("#6B6B6B")
colorOK = lipgloss.Color("#2E7D32") colorOK = lipgloss.Color("#2E7D32")
) )
func borderStyle() lipgloss.Style { func borderStyle() lipgloss.Style {

View file

@ -0,0 +1,177 @@
package cliui
import (
"testing"
flag "github.com/spf13/pflag"
)
func init() {
// Disable ANSI colors in tests so output is predictable plain text.
Init(true)
}
// ---------------------------------------------------------------------------
// showErrHint
// ---------------------------------------------------------------------------
func TestShowErrHint(t *testing.T) {
cases := []struct {
msg string
want bool
}{
// Cobra flag errors — should show hint
{"unknown flag: --foo", true},
{"unknown shorthand flag: 'f' in -f", true},
{"flag needs an argument: --output", true},
{"required flag(s) \"model\" not set", true},
// Generic invalid-argument errors — should show hint
{"invalid argument \"abc\" for --count", true},
// usage: in message — should show hint
{"bad input\nusage: picoclaw ...", true},
// Should NOT false-positive on unrelated "flag" words
{"connection flagged by remote", false},
{"feature flag not set", false},
{"please flag this issue", false},
// Unrelated messages — no hint
{"something went wrong", false},
{"network timeout", false},
}
for _, tc := range cases {
got := showErrHint(tc.msg)
if got != tc.want {
t.Errorf("showErrHint(%q) = %v, want %v", tc.msg, got, tc.want)
}
}
}
// ---------------------------------------------------------------------------
// styleUsageTokens
// ---------------------------------------------------------------------------
func TestStyleUsageTokensContainsTokens(t *testing.T) {
cases := []struct {
input string
contains []string // substrings that must appear in plain output
}{
{
"picoclaw agent <message>",
[]string{"picoclaw agent", "<message>"},
},
{
"picoclaw [command] [flags]",
[]string{"picoclaw", "[command]", "[flags]"},
},
{
"picoclaw",
[]string{"picoclaw"},
},
{
"cmd <arg1> [--flag]",
[]string{"cmd", "<arg1>", "[--flag]"},
},
}
for _, tc := range cases {
out := styleUsageTokens(tc.input)
for _, sub := range tc.contains {
if !containsStripped(out, sub) {
t.Errorf("styleUsageTokens(%q): output %q does not contain %q", tc.input, out, sub)
}
}
}
}
// containsStripped checks whether plain contains sub after stripping ANSI escapes.
// Since Init(true) sets Ascii profile, lipgloss emits no escape codes in tests,
// so this is just a plain substring check.
func containsStripped(plain, sub string) bool {
return len(plain) >= len(sub) && findSubstring(plain, sub)
}
func findSubstring(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
// collectFlagRows
// ---------------------------------------------------------------------------
func TestCollectFlagRows_Empty(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
rows := collectFlagRows(fs)
if len(rows) != 0 {
t.Fatalf("expected 0 rows for empty FlagSet, got %d", len(rows))
}
}
func TestCollectFlagRows_BasicFlags(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("output", "", "output file path")
fs.Bool("verbose", false, "enable verbose mode")
fs.Int("count", 1, "number of items")
rows := collectFlagRows(fs)
if len(rows) != 3 {
t.Fatalf("expected 3 rows, got %d", len(rows))
}
// Rows must be sorted alphabetically by flag name.
names := []string{}
for _, r := range rows {
names = append(names, r[0])
}
if names[0] > names[1] || names[1] > names[2] {
t.Errorf("rows not sorted: %v", names)
}
}
func TestCollectFlagRows_Shorthand(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.StringP("model", "m", "", "model name")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(rows))
}
left := rows[0][0]
if !findSubstring(left, "-m") || !findSubstring(left, "--model") {
t.Errorf("expected shorthand and long form in %q", left)
}
}
func TestCollectFlagRows_HiddenFlagsExcluded(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("visible", "", "this shows up")
hidden := fs.String("hidden", "", "this should not show up")
_ = hidden
_ = fs.MarkHidden("hidden")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row (hidden excluded), got %d", len(rows))
}
if !findSubstring(rows[0][0], "visible") {
t.Errorf("expected visible flag in rows, got %q", rows[0][0])
}
}
func TestCollectFlagRows_UsageInRightColumn(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.String("format", "json", "output format: json or text")
rows := collectFlagRows(fs)
if len(rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(rows))
}
if rows[0][1] != "output format: json or text" {
t.Errorf("expected usage in right column, got %q", rows[0][1])
}
}

View file

@ -67,7 +67,8 @@ func FormatCLIError(msg string, ctx *cobra.Command) string {
func showErrHint(msg string) bool { func showErrHint(msg string) bool {
m := strings.ToLower(msg) m := strings.ToLower(msg)
return strings.Contains(m, "unknown flag") || return strings.Contains(m, "unknown flag") ||
strings.Contains(m, "flag") || strings.Contains(m, "unknown shorthand flag") ||
strings.Contains(m, "flag needs an argument") ||
strings.Contains(m, "invalid") || strings.Contains(m, "invalid") ||
strings.Contains(m, "required") || strings.Contains(m, "required") ||
strings.Contains(m, "usage:") strings.Contains(m, "usage:")

View file

@ -75,7 +75,7 @@ func printOnboardFancy(logo string, encrypt bool, configPath string) {
// Same order as plain output: numbered steps → recommended → chat line. // Same order as plain output: numbered steps → recommended → chat line.
next := titleBarStyle().Render("Next steps") + "\n\n" + next := titleBarStyle().Render("Next steps") + "\n\n" +
bodyStyle().Width(inner - 4).Render(steps+"\n\n"+rec+"\n\n"+chat) bodyStyle().Width(inner-4).Render(steps+"\n\n"+rec+"\n\n"+chat)
fmt.Println(borderStyle().Width(inner).Render(next)) fmt.Println(borderStyle().Width(inner).Render(next))
} }

View file

@ -7,6 +7,12 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
// ProviderRow holds one provider's display name and status value.
type ProviderRow struct {
Name string
Val string
}
// StatusReport is a structured status view for PrintStatus. // StatusReport is a structured status view for PrintStatus.
type StatusReport struct { type StatusReport struct {
Logo string Logo string
@ -16,10 +22,8 @@ type StatusReport struct {
ConfigOK bool ConfigOK bool
WorkspacePath string WorkspacePath string
WorkspaceOK bool WorkspaceOK bool
Model string Model string
// ProviderNames and ProviderVals same length Providers []ProviderRow
ProviderNames []string
ProviderVals []string
OAuthLines []string // each full line "provider (method): state" OAuthLines []string // each full line "provider (method): state"
} }
@ -45,8 +49,8 @@ func printStatusPlain(r StatusReport) {
if r.ConfigOK { if r.ConfigOK {
fmt.Printf("Model: %s\n", r.Model) fmt.Printf("Model: %s\n", r.Model)
for i := range r.ProviderNames { for _, p := range r.Providers {
fmt.Printf("%s: %s\n", r.ProviderNames[i], r.ProviderVals[i]) fmt.Printf("%s: %s\n", p.Name, p.Val)
} }
if len(r.OAuthLines) > 0 { if len(r.OAuthLines) > 0 {
fmt.Println("\nOAuth/Token Auth:") fmt.Println("\nOAuth/Token Auth:")
@ -80,7 +84,7 @@ func printStatusFancy(r StatusReport) {
fmt.Println(topBox.Render(head.String())) fmt.Println(topBox.Render(head.String()))
fmt.Println() fmt.Println()
if UseColumnLayout() && len(r.ProviderNames) > 0 && r.ConfigOK { if UseColumnLayout() && len(r.Providers) > 0 && r.ConfigOK {
leftW := (inner - 2) / 2 leftW := (inner - 2) / 2
rightW := inner - leftW - 2 rightW := inner - leftW - 2
pathsNarrow := pathStatusPanel(r, leftW) pathsNarrow := pathStatusPanel(r, leftW)
@ -89,7 +93,7 @@ func printStatusFancy(r StatusReport) {
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov)) fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov))
} else { } else {
fmt.Println(pathStatusPanel(r, inner)) fmt.Println(pathStatusPanel(r, inner))
if len(r.ProviderNames) > 0 && r.ConfigOK { if len(r.Providers) > 0 && r.ConfigOK {
fmt.Println(providerTablePanel(r, inner)) fmt.Println(providerTablePanel(r, inner))
} }
} }
@ -130,7 +134,7 @@ func statusMark(ok bool) string {
} }
func providerTablePanel(r StatusReport, colW int) string { func providerTablePanel(r StatusReport, colW int) string {
if len(r.ProviderNames) == 0 { if len(r.Providers) == 0 {
return "" return ""
} }
keyW := min(22, colW/3) keyW := min(22, colW/3)
@ -144,9 +148,9 @@ func providerTablePanel(r StatusReport, colW int) string {
var b strings.Builder var b strings.Builder
b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n") b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n")
for i := range r.ProviderNames { for _, p := range r.Providers {
k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(r.ProviderNames[i]) k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(p.Name)
v := styleProviderVal(r.ProviderVals[i]).Width(valW).Render(r.ProviderVals[i]) v := styleProviderVal(p.Val).Width(valW).Render(p.Val)
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v)) b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v))
b.WriteString("\n") b.WriteString("\n")
} }

View file

@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
//go:generate bash -lc "rm -rf workspace && cp -r ../../../../workspace ./workspace" //go:generate cp -r ../../../../workspace .
//go:embed workspace //go:embed workspace
var embeddedFiles embed.FS var embeddedFiles embed.FS

View file

@ -98,53 +98,30 @@ func statusCmd() {
} }
ollamaBase, hasOllama := findProtocolBase("ollama") ollamaBase, hasOllama := findProtocolBase("ollama")
status := func(enabled bool) string { val := func(enabled bool, extra ...string) string {
if enabled { if enabled {
if len(extra) > 0 && extra[0] != "" {
return "✓ " + extra[0]
}
return "✓" return "✓"
} }
return "not set" return "not set"
} }
report.ProviderNames = []string{ report.Providers = []cliui.ProviderRow{
"OpenRouter API", {"OpenRouter API", val(hasOpenRouter)},
"Anthropic API", {"Anthropic API", val(hasAnthropic)},
"OpenAI API", {"OpenAI API", val(hasOpenAI)},
"Gemini API", {"Gemini API", val(hasGemini)},
"Zhipu API", {"Zhipu API", val(hasZhipu)},
"Qwen API", {"Qwen API", val(hasQwen)},
"Groq API", {"Groq API", val(hasGroq)},
"Moonshot API", {"Moonshot API", val(hasMoonshot)},
"DeepSeek API", {"DeepSeek API", val(hasDeepSeek)},
"VolcEngine API", {"VolcEngine API", val(hasVolcEngine)},
"Nvidia API", {"Nvidia API", val(hasNvidia)},
} {"vLLM / local", val(hasVLLM, vllmBase)},
report.ProviderVals = []string{ {"Ollama", val(hasOllama, ollamaBase)},
status(hasOpenRouter),
status(hasAnthropic),
status(hasOpenAI),
status(hasGemini),
status(hasZhipu),
status(hasQwen),
status(hasGroq),
status(hasMoonshot),
status(hasDeepSeek),
status(hasVolcEngine),
status(hasNvidia),
}
if hasVLLM {
report.ProviderNames = append(report.ProviderNames, "vLLM / local")
report.ProviderVals = append(report.ProviderVals, "✓ "+vllmBase)
} else {
report.ProviderNames = append(report.ProviderNames, "vLLM / local")
report.ProviderVals = append(report.ProviderVals, "not set")
}
if hasOllama {
report.ProviderNames = append(report.ProviderNames, "Ollama")
report.ProviderVals = append(report.ProviderVals, "✓ "+ollamaBase)
} else {
report.ProviderNames = append(report.ProviderNames, "Ollama")
report.ProviderVals = append(report.ProviderVals, "not set")
} }
store, _ := auth.LoadStore() store, _ := auth.LoadStore()

View file

@ -13,9 +13,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate"
@ -40,7 +40,8 @@ func earlyColorDisabled() bool {
return true return true
} }
for i := 1; i < len(os.Args); i++ { for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "--no-color" { arg := os.Args[i]
if arg == "--no-color" || arg == "--no-color=true" || arg == "--no-color=1" {
return true return true
} }
} }