Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c2340ad
feat: proper rendering of api.Text and Textable in all formatters
moshloop Nov 3, 2025
b1a9880
feat: refactor formatters to use FieldValue.Text and PrettyData.Pretty()
moshloop Nov 4, 2025
57b2bb0
chore: misc
moshloop Nov 4, 2025
b3522ce
chore: update examples
moshloop Nov 4, 2025
173d6df
fix: restore table rendering temporarily, fix ANSI test checks
moshloop Nov 4, 2025
da8ab64
fix: wrap string values in Text for TextList compatibility
moshloop Nov 4, 2025
123cd14
fix: add missing types and methods for compilation
moshloop Nov 4, 2025
37919a6
refactor: remove Formatted(), Plain(), Markdown(), HTML() from FieldV…
moshloop Nov 4, 2025
21f9e44
fix: update test to handle Textable interface properly
moshloop Nov 4, 2025
774ad88
wip
moshloop Nov 4, 2025
50e1326
misc
moshloop Nov 5, 2025
dc3936e
fix: improve HTML tree rendering with embedded assets
moshloop Nov 5, 2025
d563f5f
refactor: extract inline CSS/JS to go:embed files and use Iconify che…
moshloop Nov 5, 2025
a0cf374
fix: align tree chevrons to top
moshloop Nov 5, 2025
6cef77f
fix: use interface assertion in NewTypedValue for nested trees
moshloop Nov 5, 2025
fdfee78
feat: implement nested table rendering in HTML with compact support
moshloop Nov 5, 2025
f205a3a
feat(ai): add LLM agent adapter with structured output support
moshloop Nov 7, 2025
afd2e3c
fix(task): prevent hanging and terminal corruption in task manager
moshloop Nov 14, 2025
12ddad5
fix(task): resolve race conditions in batch.go
moshloop Nov 16, 2025
e686a55
fix(task): resolve data races in Task and Manager
moshloop Nov 16, 2025
094b57b
fix: formatting improvements
moshloop Nov 17, 2025
94c8cd2
fix: args handling in clicky.AddCommand
moshloop Nov 17, 2025
5a78548
chore: ai refactorings
moshloop Nov 17, 2025
f54969b
fix: batch race conditions
moshloop Nov 17, 2025
33f6988
fix: parse max-w unit suffixes (ch, px, rem, em)
moshloop Nov 17, 2025
dd79195
feat: default to truncate-suffix when constraints specified
moshloop Nov 17, 2025
10436ec
fix: prevent truncate class from overwriting explicit max-w
moshloop Nov 17, 2025
b229f73
feat: migrate terminal tables to lipgloss (hybrid approach)
moshloop Nov 17, 2025
1653eff
feat: migrate tree rendering to lipgloss
moshloop Nov 17, 2025
c587b42
chore: switch tree/table printing back to lipgloss
moshloop Nov 17, 2025
13947dc
chore: fix nested html table output
moshloop Nov 18, 2025
0266e5d
chore: fix nested html tree output
moshloop Nov 18, 2025
2e45834
chore: html fixes
moshloop Nov 19, 2025
ddf7c70
chore: uber demo
moshloop Nov 19, 2025
7cd3b7e
chore: fix lint errors
moshloop Nov 19, 2025
a25d6e3
chore: review fixes
moshloop Nov 19, 2025
56006ce
chore: fix test errors
moshloop Nov 20, 2025
ac5681c
feat(formatters): make TextTable self-contained with column schema
moshloop Nov 20, 2025
86c09a4
fix(html): add fallback to data.Tree for tree fields
moshloop Nov 21, 2025
f0ade9f
refactor(html): use Value() + type switch instead of GetValue()
moshloop Nov 21, 2025
7d6de30
chore: refactor formatters package
moshloop Nov 21, 2025
0d9dd0e
chore: refactor formatters package
moshloop Nov 21, 2025
7efdaf9
refactor(table): make lipgloss table Textable-aware
moshloop Nov 23, 2025
40058f0
chore: html table fixes
moshloop Nov 26, 2025
5aeb357
feat: batch timeouts
moshloop Nov 26, 2025
7cafb26
fix: check PrettyRow interface before extracting struct fields
moshloop Nov 27, 2025
c619bc2
fix: duplicate rendering of logs with --no-progress
moshloop Dec 9, 2025
dfdf7df
fix: formatting tests
moshloop Dec 9, 2025
970bbb4
fix: reset capture output when reusing exec wrappers
moshloop Dec 9, 2025
8bf74ec
chore: fix tests
moshloop Dec 9, 2025
32d5261
chore: fix formatting tests
moshloop Dec 14, 2025
1506311
fix: batch timeouts
moshloop Dec 14, 2025
8d62c85
chore: test / logging improvements
moshloop Dec 14, 2025
9aec737
Merge branch 'main' of https://github.com/flanksource/clicky into dev
moshloop Dec 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions ai/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,13 @@ func (am *AgentManager) Close() error {
}

var defaultConfig AgentConfig = AgentConfig{
Type: AgentTypeClaude,
Model: "claude-haiku-4-5",
MaxTokens: 10000,
MaxConcurrent: 4,
Debug: false,
Verbose: false,
// Temperature: 0.2,
Type: AgentTypeClaude,
Model: "claude-haiku-4-5",
MaxTokens: 10000,
MaxConcurrent: 4,
Debug: false,
Verbose: false,
Temperature: 0.2,
StrictMCPConfig: true,
CacheTTL: 24 * time.Hour, // Default 24 hour TTL
NoCache: false,
Expand Down
26 changes: 12 additions & 14 deletions api/human.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,30 @@ func Human(content any, styles ...string) Text {
if content == nil {
return Text{}
}

style := uniqueStyles("", styles...)
switch t := content.(type) {

case Text:
return t
case Textable:
return Text{}.Add(t)
return Text{}.Append(t, styles...)
case time.Time:
if t.IsZero() {
return Text{}
}
if t.Truncate(time.Hour * 24).Equal(t) {
return Text{
Content: t.Format("2006-01-02"),
Style: strings.Join(append(styles, "date"), " "),
}
}.Styles(style, "date")
}
// Only omit timezone if it's UTC
if t.Location() == time.UTC {
return Text{
Content: t.Format("2006-01-02 15:04:05"),
Style: strings.Join(append(styles, "date"), " "),
}
Content: t.Format("2006-01-02T15:04:05Z")}.Styles(style, "date")
}
return Text{
Content: t.Format(time.RFC3339),
Style: strings.Join(append(styles, "date"), " "),
}
Content: t.Format(time.RFC3339)}.Styles(style, "date")
case *time.Time:
if t == nil {
return Text{}
Expand All @@ -92,8 +92,7 @@ func Human(content any, styles ...string) Text {
}
return Text{
Content: v,
Style: strings.Join(append(styles, "duration"), " "),
}
}.Styles(style, "duration")
case *time.Duration:
if t == nil {
return Text{}
Expand All @@ -108,8 +107,7 @@ func Human(content any, styles ...string) Text {
case float32, float64:
return Text{
Content: fmt.Sprintf("%.2f", t),
Style: strings.Join(append(styles, "number"), " "),
}
}.Styles(style, "number")

case bool:
if t {
Expand All @@ -119,7 +117,7 @@ func Human(content any, styles ...string) Text {
}
}

return Text{Content: fmt.Sprintf("%v", content), Style: strings.Join(styles, " ")}
return Text{Content: fmt.Sprintf("%v", content), Style: style}
}

func HumanNumber(value int64, styles ...string) Text {
Expand Down
2 changes: 1 addition & 1 deletion api/human_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestHuman(t *testing.T) {
{input: 123345633, expected: "123M"},
{input: 67.89, expected: "67.89"},
{input: fmt.Sprintf("(%v in, %v out)", Human(5403200), Human(9003200)), expected: "(5.4M in, 9M out)"},
{input: time.Date(2023, 10, 5, 14, 30, 0, 0, time.UTC), expected: "2023-10-05 14:30:00"},
{input: time.Date(2023, 10, 5, 14, 30, 0, 0, time.UTC), expected: "2023-10-05T14:30:00Z"},
{input: time.Date(2023, 10, 5, 0, 0, 0, 0, time.UTC), expected: "2023-10-05"},

{input: Text{Content: "Preformatted Text"}, expected: "Preformatted Text"},
Expand Down
54 changes: 40 additions & 14 deletions api/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,17 +392,20 @@ func (tv TypedValue) Visit(visitor VisitorFunc) bool {
return true
}

func TryTypedValue(o any) *TypedValue {
type TypeOptions struct {
SkipTable bool
SkipTree bool
}

func TryTypedValue(o any, opts ...TypeOptions) *TypedValue {

switch v := o.(type) {
case *PrettyData:
return &TypedValue{Textable: v}
// TextTable and TextTree must come before Textable since they implement Textable
case TextTable:
return &TypedValue{Table: &v}
case TextTree:
return &TypedValue{Tree: &v}
case Textable:
return &TypedValue{Textable: v}
case TextList:
return &TypedValue{Slice: &v}
case TextMap:
Expand All @@ -411,18 +414,41 @@ func TryTypedValue(o any) *TypedValue {
return &TypedValue{TypedMap: &v}
case TypedList:
return &TypedValue{TypedList: &v}
case TreeNode:
return &TypedValue{Tree: lo.ToPtr(NewTree(v))}
case TreeMixin:
return &TypedValue{Tree: lo.ToPtr(NewTree(v.Tree()))}
}

skipTable := false
skipTree := false
for _, opt := range opts {
skipTable = skipTable || opt.SkipTable
skipTree = skipTree || opt.SkipTree
}

if !skipTable {
switch v := o.(type) {
case []TableMixin:
return &TypedValue{Table: lo.ToPtr(NewTable(v))}
case []TableRowMixin2:
return &TypedValue{Table: lo.ToPtr(NewTableFromMixin(v))}
case []PrettyDataRow:
return &TypedValue{Table: lo.ToPtr(NewTableFromRows(v))}
}
}

if !skipTree {
switch v := o.(type) {
case TreeNode:
return &TypedValue{Tree: lo.ToPtr(NewTree(v))}
case TreeMixin:
return &TypedValue{Tree: lo.ToPtr(NewTree(v.Tree()))}
}
}

switch v := o.(type) {
case Pretty:
return &TypedValue{Textable: v.Pretty()}
case []TableMixin:
return &TypedValue{Table: lo.ToPtr(NewTable(v))}
case []TableRowMixin2:
return &TypedValue{Table: lo.ToPtr(NewTableFromMixin(v))}
case []PrettyDataRow:
return &TypedValue{Table: lo.ToPtr(NewTableFromRows(v))}
case Textable:
return &TypedValue{Textable: v}

}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion api/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func ResolveStyles(styles ...string) Class {
resolved.Font.Underline = false
}

if class == "line-through" || class == "strikethrough" {
if class == "line-through" || class == "strikethrough" || class == "text-strikethrough" {
resolved.Font.Strikethrough = true
}

Expand Down
33 changes: 27 additions & 6 deletions api/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,34 @@ func (t Text) AddIcon(icon Textable, styles ...string) Text {
return t.Add(icon)
}

func (t Text) Styles(classes ...string) Text {
if t.Style != "" {
// Append new classes to existing style
t.Style = t.Style + " " + strings.Join(classes, " ")
} else {
t.Style = strings.Join(classes, " ")
func uniqueStyles(existing string, styles ...string) string {
styleSet := make(map[string]struct{})
if existing != "" {
for _, s := range strings.Split(existing, " ") {
styleSet[s] = struct{}{}
}
}
for _, style := range styles {
for _, s := range strings.Split(style, " ") {
if s != "" {
styleSet[s] = struct{}{}
}
}
}
uniq := ""
for s := range styleSet {
if uniq == "" {
uniq = s
} else {
uniq += " " + s
}
}

return uniq
}

func (t Text) Styles(classes ...string) Text {
t.Style = uniqueStyles(t.Style, classes...)
return t
}

Expand Down
1 change: 1 addition & 0 deletions cobra_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
// Call the function
result, err := fn(optsValue.Interface().(T))
if err != nil {
logger.GetSlogLogger().WithSkipReportLevel(1).Errorf("Command %s failed: %v", name, err)

Check failure on line 175 in cobra_command.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: logger (typecheck)

Check failure on line 175 in cobra_command.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Go 1.25)

undefined: logger

Check failure on line 175 in cobra_command.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: logger (typecheck)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for logger package definition and usage

echo "=== Searching for logger package definition ==="
fd --type f -e go | head -20 | xargs grep -l "package logger" 2>/dev/null || echo "No logger package found"

echo -e "\n=== Searching for GetSlogLogger function ==="
rg "func.*GetSlogLogger" --type go

echo -e "\n=== Searching for GetSlogLogger calls ==="
rg "GetSlogLogger" --type go -B2 -A2

echo -e "\n=== Checking cobra_command.go imports ==="
rg "^import" cobra_command.go -A20

echo -e "\n=== Checking for logger imports elsewhere ==="
rg "import.*logger" --type go -C1

Repository: flanksource/clicky

Length of output: 2794


🏁 Script executed:

#!/bin/bash
# Get more context around line 175 in cobra_command.go

echo "=== Full context around line 175 (lines 160-190) ==="
sed -n '160,190p' cobra_command.go

echo -e "\n=== Check entire function containing line 175 ==="
rg "func.*\{" cobra_command.go -B5 -A150 | head -200

echo -e "\n=== Look for any logger variable or context setup in cobra_command.go ==="
rg "logger|Logger" cobra_command.go

Repository: flanksource/clicky

Length of output: 4872


🏁 Script executed:

#!/bin/bash
# Check if there's a way to get a logger in the cobra context or if this is simply a mistake

echo "=== Check if cobra.Command has any logger context ==="
rg "WithContext|Context" cobra_command.go -B2 -A2

echo -e "\n=== Look for similar error handling patterns in the codebase ==="
rg "\.Errorf\(" --type go -B2 -A1 | head -40

echo -e "\n=== Check how errors are handled elsewhere in cobra commands ==="
rg "cmd\.RunE.*error" --type go -A10 | head -60

Repository: flanksource/clicky

Length of output: 2572


🏁 Script executed:

#!/bin/bash
# Final check: is there a global logger variable or package-level logger in cobra_command.go?

echo "=== Check entire cobra_command.go file structure ==="
head -50 cobra_command.go

echo -e "\n=== Check if logger might be accessible from opts T ==="
rg "interface.*Logger|Logger.*interface" --type go -B2 -A5

echo -e "\n=== Look at the actual package name and any global vars in cobra_command.go ==="
rg "^package|^var|^const" cobra_command.go

Repository: flanksource/clicky

Length of output: 11910


Critical: Undefined logger identifier breaks compilation at line 175.

The code references logger.GetSlogLogger().WithSkipReportLevel(1).Errorf(...) but logger is not defined anywhere in scope—not imported, not passed as a parameter, and not available in the RunE callback context. This is a compilation error that blocks the build.

Recommended fixes:

  1. Remove the logging line (simplest):

    if err != nil {
        return err
    }
  2. Use the options parameter if it implements logger.Logger:

    if err != nil {
        if lg, ok := optsValue.Interface().(interface{ GetSlogLogger() *slog.Logger }); ok {
            lg.GetSlogLogger().WithSkipReportLevel(1).Errorf("Command %s failed: %v", name, err)
        }
        return err
    }
  3. Accept a logger in a callback parameter (requires API change).

🧰 Tools
🪛 GitHub Actions: Lint

[error] 175-175: golangci-lint: undefined: logger (typecheck)

🪛 GitHub Actions: PR #44

[error] 175-175: build failed: undefined: logger

🪛 GitHub Actions: Test

[error] 175-175: golangci-lint: undefined: logger (typecheck)

🪛 GitHub Check: golangci-lint

[failure] 175-175:
undefined: logger (typecheck)

🪛 GitHub Check: Lint

[failure] 175-175:
undefined: logger (typecheck)

🪛 GitHub Check: Test (ubuntu-latest, Go 1.25)

[failure] 175-175:
undefined: logger

🤖 Prompt for AI Agents
In cobra_command.go around line 175 the identifier `logger` is undefined causing
a compile error; remove the invalid logging call and simply return the error, or
if you want to preserve logging use the existing `optsValue` by type-asserting
it to an interface that exposes GetSlogLogger(), call
GetSlogLogger().WithSkipReportLevel(1).Errorf(...) with the command name and
err, then return the error; choose one of these two fixes and apply it where the
current undefined logger call occurs.

return err
}

Expand Down
2 changes: 1 addition & 1 deletion exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ func (p *Process) Run() *Process {
cmd.Stderr = p.captureOutput.GetStderrWriter()
cmd.Stdout = p.captureOutput.GetStdoutWriter()

p.log.Debugf(api.Text{}.Append("run", "text-muted").Append(icons.MinimalArrow, "text-muted").Space().Add(p.Short()).ANSI())
p.log.Tracef(api.Text{}.Append("run", "text-muted").Append(icons.MinimalArrow, "text-muted").Space().Add(p.Short()).ANSI())

now := time.Now()
p.Started = &now
Expand Down
13 changes: 11 additions & 2 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package clicky
import (
"github.com/flanksource/commons/collections"
"github.com/flanksource/commons/logger"
"github.com/samber/lo"
"github.com/spf13/pflag"
)

Expand Down Expand Up @@ -65,8 +66,16 @@ func BindAllFlags(flags *pflag.FlagSet, filters ...string) *AllFlags {
flags.BoolVar(&Flags.PDF, "pdf", false, "Output in PDF format")

// Display structure flags (additive with format)
flags.BoolVar(&Flags.Tree, "tree", false, "Display in tree structure (additive with format)")
flags.BoolVar(&Flags.Table, "table", false, "Display in table structure (additive with format)")
// Display structure flags (additive with format)
flags.BoolFunc("tree", "Display in tree structure (additive with format)", func(s string) error {
Flags.Tree = lo.ToPtr(s == "true")
return nil
})
flags.BoolFunc("table", "Display in table structure (additive with format), or false to disable tables",
func(b string) error {
Flags.Table = lo.ToPtr(b == "true")
return nil
})
}

return Flags
Expand Down
4 changes: 2 additions & 2 deletions formatters/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ func (f FormatManager) FormatWithOptions(options FormatOptions, data ...any) (st
// Handle display structure overrides (additive flags)
// Tree flag: For text formats, use tree visual; for structured formats, pass tree data through
// Table flag: Convert to table structure before applying format
if options.Tree {
if options.Tree != nil && *options.Tree {
return f.treeFormatter.Format(data...)
} else if options.Table {
} else if options.Table != nil && *options.Table {
logger.V(4).Infof("Applying table structure transformation before %s formatting", format)
// Convert data to table structure first, then apply the format
// For text-based formats, apply table formatting directly
Expand Down
43 changes: 33 additions & 10 deletions formatters/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package formatters
import (
"flag"

"github.com/samber/lo"
"github.com/spf13/pflag"

"github.com/flanksource/clicky/api"
Expand Down Expand Up @@ -33,8 +34,8 @@ type FormatOptions struct {
PDF bool `json:"pdf,omitempty"`

// Display structure flags (additive with format flags)
Tree bool `json:"tree,omitempty"` // Display in tree structure
Table bool `json:"table,omitempty"` // Display in table structure
Tree *bool `json:"tree,omitempty"` // Display in tree structure
Table *bool `json:"table,omitempty"` // Display in table structure

// Paging options
Page int `json:"page,omitempty"` // Current page (1-indexed)
Expand All @@ -44,6 +45,14 @@ type FormatOptions struct {
depth int // Hidden field for tracking nesting depth in recursive formatting
}

func (o FormatOptions) SkipTable() bool {
return (o.Table != nil && !*o.Table) || (o.Tree != nil && *o.Tree)
}

func (o FormatOptions) SkipTree() bool {
return (o.Tree != nil && !*o.Tree) || (o.Table != nil && *o.Table)
}

func MergeOptions(opts ...FormatOptions) FormatOptions {
merged := FormatOptions{}
for _, opt := range opts {
Expand All @@ -68,11 +77,11 @@ func MergeOptions(opts ...FormatOptions) FormatOptions {
if opt.Filter != "" {
merged.Filter = opt.Filter
}
if opt.Tree {
merged.Tree = true
if opt.Tree != nil {
merged.Tree = opt.Tree
}
if opt.Table {
merged.Table = true
if opt.Table != nil {
merged.Table = opt.Table
}
if opt.Page > 0 {
merged.Page = opt.Page
Expand Down Expand Up @@ -133,8 +142,15 @@ func BindFlags(flags *flag.FlagSet, options *FormatOptions) {
flags.BoolVar(&options.PDF, "pdf", false, "Output in PDF format")

// Display structure flags (additive with format)
flags.BoolVar(&options.Tree, "tree", false, "Display in tree structure (additive with format)")
flags.BoolVar(&options.Table, "table", false, "Display in table structure (additive with format)")
flags.BoolFunc("tree", "Display in tree structure (additive with format)", func(s string) error {
options.Tree = lo.ToPtr(s == "true")
return nil
})
flags.BoolFunc("table", "Display in table structure (additive with format), or false to disable tables",
func(b string) error {
options.Table = lo.ToPtr(b == "true")
return nil
})
}

// BindPFlags adds formatting flags to the provided pflag set (for cobra)
Expand All @@ -155,8 +171,15 @@ func BindPFlags(flags *pflag.FlagSet, options *FormatOptions) {
flags.BoolVar(&options.PDF, "pdf", false, "Output in PDF format")

// Display structure flags (additive with format)
flags.BoolVar(&options.Tree, "tree", false, "Display in tree structure (additive with format)")
flags.BoolVar(&options.Table, "table", false, "Display in table structure (additive with format)")
flags.BoolFunc("tree", "Display in tree structure (additive with format)", func(s string) error {
options.Tree = lo.ToPtr(s == "true")
return nil
})
flags.BoolFunc("table", "Display in table structure (additive with format), or false to disable tables",
func(b string) error {
options.Table = lo.ToPtr(b == "true")
return nil
})
}

// ResolveFormat resolves the output format from format-specific flags
Expand Down
Loading
Loading