fix: lint errors
This commit is contained in:
parent
7cdd86df8b
commit
2f3e2ad9cf
8 changed files with 222 additions and 62 deletions
177
cmd/picoclaw/internal/cliui/cliui_test.go
Normal file
177
cmd/picoclaw/internal/cliui/cliui_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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:")
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -17,9 +23,7 @@ type StatusReport struct {
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue