-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add --output markdown format #678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
03a2936
adding agent flag for agent telemtry
nieblara ae23ad3
(feat) adding TTY detection for auto-JSON output
nieblara b07eac9
merging in main
nieblara 7a8c18c
improvements
nieblara 82177a3
addressing pr feedback
nieblara b0d06c4
feat(REL-12754): Agent friendly error handling (#661)
nieblara 29c1faf
Merge branch 'main' into REL-12752-tty-1
nieblara e2d388c
fix: update stale test assertions for key-value plaintext format
nieblara 437db19
refactor: move fields into CmdOutputOpts, warn on plaintext --fields
nieblara 5b7bfe3
docs: document error shape and table format breaking changes in CHANG…
nieblara ebe67c5
feat: add --dry-run flag to toggle-on, toggle-off, and archive commands
nieblara 932a093
fix: use toggle-off in TestToggleOff dry-run tests
nieblara a934dc8
feat: register markdown as a valid output kind
nieblara a7ed7fb
feat: add markdown rendering for resource output
nieblara eb6384b
test: add markdown output tests for flag commands
nieblara 5eb2a6f
merging in main
nieblara 454b585
Merge branch 'feat/dry-run-flag' into feat/markdown-output
nieblara 36e0576
fix: resolve merge conflicts with origin/main
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| package output | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
| ) | ||
|
|
||
| // MarkdownTableOutput formats a slice of resources as a GitHub-flavored markdown table. | ||
| func MarkdownTableOutput(items []resource, cols []ColumnDef) string { | ||
| headers := make([]string, len(cols)) | ||
| separators := make([]string, len(cols)) | ||
| for i, col := range cols { | ||
| headers[i] = col.Header | ||
| separators[i] = "---" | ||
| } | ||
|
|
||
| var sb strings.Builder | ||
| sb.WriteString("| ") | ||
| sb.WriteString(strings.Join(headers, " | ")) | ||
| sb.WriteString(" |\n| ") | ||
| sb.WriteString(strings.Join(separators, " | ")) | ||
| sb.WriteString(" |") | ||
|
|
||
| for _, item := range items { | ||
| vals := make([]string, len(cols)) | ||
| for i, col := range cols { | ||
| vals[i] = escapeMDPipe(colValue(item, col)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need error handling here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a good idea - deferring to a future pr |
||
| } | ||
| sb.WriteString("\n| ") | ||
| sb.WriteString(strings.Join(vals, " | ")) | ||
| sb.WriteString(" |") | ||
| } | ||
|
|
||
| return sb.String() | ||
| } | ||
|
|
||
| // MarkdownKeyValueOutput formats a single resource as a markdown bullet list of key-value pairs. | ||
| func MarkdownKeyValueOutput(r resource, cols []ColumnDef) string { | ||
| lines := make([]string, 0, len(cols)) | ||
| for _, col := range cols { | ||
| val := colValue(r, col) | ||
| lines = append(lines, fmt.Sprintf("- **%s:** %s", col.Header, val)) | ||
| } | ||
| return strings.Join(lines, "\n") | ||
| } | ||
|
|
||
| // MarkdownSingularOutput renders a single resource in markdown with a heading and metadata. | ||
| // For flags it produces a rich view with environment table; for other resources it uses | ||
| // the column registry or a generic fallback. | ||
| func MarkdownSingularOutput(r resource, resourceName string) string { | ||
| if resourceName == "flags" { | ||
| return markdownFlagOutput(r) | ||
| } | ||
|
|
||
| heading := markdownHeading(r) | ||
| if cols := GetSingularColumns(resourceName); cols != nil { | ||
| return heading + "\n\n" + MarkdownKeyValueOutput(r, cols) | ||
| } | ||
| return heading | ||
| } | ||
|
|
||
| // MarkdownMultipleOutput renders a list of resources as a markdown table (if columns are | ||
| // registered) or a bullet list. | ||
| func MarkdownMultipleOutput(items []resource, resourceName string) string { | ||
| if cols := GetListColumns(resourceName); cols != nil { | ||
| return MarkdownTableOutput(items, cols) | ||
| } | ||
|
|
||
| lines := make([]string, 0, len(items)) | ||
| for _, item := range items { | ||
| lines = append(lines, fmt.Sprintf("- %s", SingularPlaintextOutputFn(item))) | ||
| } | ||
| return strings.Join(lines, "\n") | ||
| } | ||
|
|
||
| func markdownFlagOutput(r resource) string { | ||
| var sb strings.Builder | ||
|
|
||
| key := defaultFormat(r["key"]) | ||
| sb.WriteString("## ") | ||
| sb.WriteString(key) | ||
|
|
||
| if desc, ok := r["description"]; ok && desc != nil && fmt.Sprint(desc) != "" { | ||
| sb.WriteString("\n\n") | ||
| sb.WriteString(fmt.Sprint(desc)) | ||
| } | ||
|
|
||
| envTable := markdownEnvTable(r) | ||
| if envTable != "" { | ||
| sb.WriteString("\n\n") | ||
| sb.WriteString(envTable) | ||
| } | ||
|
|
||
| if meta := markdownFlagMetadata(r); meta != "" { | ||
| sb.WriteString("\n\n") | ||
| sb.WriteString(meta) | ||
| } | ||
|
|
||
| return sb.String() | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| func markdownEnvTable(r resource) string { | ||
| envMap, ok := r["environments"].(map[string]interface{}) | ||
| if !ok || len(envMap) == 0 { | ||
| return "" | ||
| } | ||
|
|
||
| variations := extractVariations(r) | ||
|
|
||
| keys := make([]string, 0, len(envMap)) | ||
| for k := range envMap { | ||
| keys = append(keys, k) | ||
| } | ||
| sort.Strings(keys) | ||
|
|
||
| var sb strings.Builder | ||
| sb.WriteString("| Environment | Status | Fallthrough | Rules |\n") | ||
| sb.WriteString("| --- | --- | --- | --- |") | ||
|
|
||
| for _, envKey := range keys { | ||
| envData, ok := envMap[envKey].(map[string]interface{}) | ||
| if !ok { | ||
| continue | ||
| } | ||
|
|
||
| status := "OFF" | ||
| if on, ok := envData["on"].(bool); ok && on { | ||
| status = "ON" | ||
| } | ||
|
|
||
| fallthrough_ := resolveFallthrough(envData, variations) | ||
|
|
||
| rulesCount := 0 | ||
| if rules, ok := envData["rules"].([]interface{}); ok { | ||
| rulesCount = len(rules) | ||
| } | ||
|
|
||
| sb.WriteString(fmt.Sprintf("\n| %s | %s | %s | %d |", | ||
| escapeMDPipe(envKey), status, escapeMDPipe(fallthrough_), rulesCount)) | ||
| } | ||
|
|
||
| return sb.String() | ||
| } | ||
|
|
||
| func markdownFlagMetadata(r resource) string { | ||
| var lines []string | ||
|
|
||
| if kind := r["kind"]; kind != nil { | ||
| lines = append(lines, fmt.Sprintf("- **Kind:** %s", kind)) | ||
| } | ||
| if temp, ok := r["temporary"].(bool); ok { | ||
| lines = append(lines, fmt.Sprintf("- **Temporary:** %s", boolYesNo(temp))) | ||
| } | ||
| if tags, ok := r["tags"].([]interface{}); ok && len(tags) > 0 { | ||
| strs := make([]string, len(tags)) | ||
| for i, t := range tags { | ||
| strs[i] = fmt.Sprint(t) | ||
| } | ||
| lines = append(lines, fmt.Sprintf("- **Tags:** %s", strings.Join(strs, ", "))) | ||
| } | ||
| if maintainer := extractMaintainer(r); maintainer != "" { | ||
| lines = append(lines, fmt.Sprintf("- **Maintainer:** %s", maintainer)) | ||
| } | ||
|
|
||
| return strings.Join(lines, "\n") | ||
| } | ||
|
|
||
| func extractVariations(r resource) []variation { | ||
| raw, ok := r["variations"].([]interface{}) | ||
| if !ok { | ||
| return nil | ||
| } | ||
| vars := make([]variation, 0, len(raw)) | ||
| for _, v := range raw { | ||
| m, ok := v.(map[string]interface{}) | ||
| if !ok { | ||
| continue | ||
| } | ||
| name := "" | ||
| if n, ok := m["name"].(string); ok { | ||
| name = n | ||
| } | ||
| vars = append(vars, variation{ | ||
| Name: name, | ||
| Value: m["value"], | ||
| }) | ||
| } | ||
| return vars | ||
| } | ||
|
|
||
| type variation struct { | ||
| Name string | ||
| Value interface{} | ||
| } | ||
|
|
||
| func resolveFallthrough(envData map[string]interface{}, variations []variation) string { | ||
| ft, ok := envData["fallthrough"].(map[string]interface{}) | ||
| if !ok { | ||
| return "" | ||
| } | ||
| varIdx, ok := ft["variation"].(float64) | ||
| if !ok { | ||
| return "" | ||
| } | ||
| idx := int(varIdx) | ||
| if idx < 0 || idx >= len(variations) { | ||
| return fmt.Sprintf("variation %d", idx) | ||
| } | ||
| v := variations[idx] | ||
| if v.Name != "" { | ||
| return fmt.Sprintf("%s (%v)", v.Name, v.Value) | ||
| } | ||
| return fmt.Sprintf("%v", v.Value) | ||
| } | ||
|
|
||
| func extractMaintainer(r resource) string { | ||
| m, ok := r["_maintainer"].(map[string]interface{}) | ||
| if !ok { | ||
| return "" | ||
| } | ||
| if name, ok := m["name"].(string); ok && name != "" { | ||
| return name | ||
| } | ||
| if email, ok := m["email"].(string); ok && email != "" { | ||
| return email | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func markdownHeading(r resource) string { | ||
| key := r["key"] | ||
| name := r["name"] | ||
| switch { | ||
| case name != nil && key != nil: | ||
| return fmt.Sprintf("## %s (%s)", fmt.Sprint(name), fmt.Sprint(key)) | ||
| case name != nil: | ||
| return fmt.Sprintf("## %s", fmt.Sprint(name)) | ||
| case key != nil: | ||
| return fmt.Sprintf("## %s", fmt.Sprint(key)) | ||
| default: | ||
| return "## (unknown)" | ||
| } | ||
| } | ||
|
|
||
| func escapeMDPipe(s string) string { | ||
| return strings.ReplaceAll(s, "|", "\\|") | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any value to us having a utility to build this out? Just curious, not asking for a change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right now, just dont see the need for a dependency for something im not sure will change often yet - but ill keep an eye out for something suitable!