@caretive/caret-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.npmrc.tmp +2 -0
  2. package/README.md +72 -0
  3. package/cmd/cline/main.go +348 -0
  4. package/cmd/cline-host/main.go +71 -0
  5. package/e2e/default_update_test.go +154 -0
  6. package/e2e/helpers_test.go +378 -0
  7. package/e2e/main_test.go +47 -0
  8. package/e2e/mixed_stress_test.go +120 -0
  9. package/e2e/sqlite_helper.go +161 -0
  10. package/e2e/start_list_test.go +178 -0
  11. package/go.mod +64 -0
  12. package/go.sum +162 -0
  13. package/man/cline.1 +331 -0
  14. package/man/cline.1.md +332 -0
  15. package/package.json +54 -0
  16. package/pkg/cli/auth/auth_cline_provider.go +285 -0
  17. package/pkg/cli/auth/auth_menu.go +323 -0
  18. package/pkg/cli/auth/auth_subscription.go +130 -0
  19. package/pkg/cli/auth/byo_quick_setup.go +247 -0
  20. package/pkg/cli/auth/models_cline.go +141 -0
  21. package/pkg/cli/auth/models_list_fetch.go +156 -0
  22. package/pkg/cli/auth/models_list_static.go +69 -0
  23. package/pkg/cli/auth/providers_byo.go +184 -0
  24. package/pkg/cli/auth/providers_list.go +517 -0
  25. package/pkg/cli/auth/update_api_configurations.go +647 -0
  26. package/pkg/cli/auth/wizard_byo.go +764 -0
  27. package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
  28. package/pkg/cli/auth/wizard_byo_oca.go +366 -0
  29. package/pkg/cli/auth.go +43 -0
  30. package/pkg/cli/clerror/cline_error.go +187 -0
  31. package/pkg/cli/config/manager.go +208 -0
  32. package/pkg/cli/config/settings_renderer.go +198 -0
  33. package/pkg/cli/config.go +152 -0
  34. package/pkg/cli/display/ansi.go +27 -0
  35. package/pkg/cli/display/banner.go +211 -0
  36. package/pkg/cli/display/deduplicator.go +95 -0
  37. package/pkg/cli/display/markdown_renderer.go +139 -0
  38. package/pkg/cli/display/renderer.go +304 -0
  39. package/pkg/cli/display/segment_streamer.go +212 -0
  40. package/pkg/cli/display/streaming.go +134 -0
  41. package/pkg/cli/display/system_renderer.go +269 -0
  42. package/pkg/cli/display/tool_renderer.go +455 -0
  43. package/pkg/cli/display/tool_result_parser.go +371 -0
  44. package/pkg/cli/display/typewriter.go +210 -0
  45. package/pkg/cli/doctor.go +65 -0
  46. package/pkg/cli/global/cline-clients.go +501 -0
  47. package/pkg/cli/global/global.go +113 -0
  48. package/pkg/cli/global/registry.go +304 -0
  49. package/pkg/cli/handlers/ask_handlers.go +339 -0
  50. package/pkg/cli/handlers/handler.go +130 -0
  51. package/pkg/cli/handlers/say_handlers.go +521 -0
  52. package/pkg/cli/instances.go +506 -0
  53. package/pkg/cli/logs.go +382 -0
  54. package/pkg/cli/output/coordinator.go +167 -0
  55. package/pkg/cli/output/input_model.go +497 -0
  56. package/pkg/cli/sqlite/locks.go +366 -0
  57. package/pkg/cli/task/history_handler.go +72 -0
  58. package/pkg/cli/task/input_handler.go +577 -0
  59. package/pkg/cli/task/manager.go +1283 -0
  60. package/pkg/cli/task/settings_parser.go +754 -0
  61. package/pkg/cli/task/stream_coordinator.go +60 -0
  62. package/pkg/cli/task.go +675 -0
  63. package/pkg/cli/terminal/keyboard.go +695 -0
  64. package/pkg/cli/tui/HELP_WANTED.md +1 -0
  65. package/pkg/cli/types/history.go +17 -0
  66. package/pkg/cli/types/messages.go +329 -0
  67. package/pkg/cli/types/state.go +59 -0
  68. package/pkg/cli/updater/updater.go +409 -0
  69. package/pkg/cli/version.go +43 -0
  70. package/pkg/common/constants.go +6 -0
  71. package/pkg/common/schema.go +54 -0
  72. package/pkg/common/types.go +54 -0
  73. package/pkg/common/utils.go +185 -0
  74. package/pkg/generated/field_overrides.go +39 -0
  75. package/pkg/generated/providers.go +1584 -0
  76. package/pkg/hostbridge/diff.go +351 -0
  77. package/pkg/hostbridge/disabled/watch.go +39 -0
  78. package/pkg/hostbridge/disabled/window.go +63 -0
  79. package/pkg/hostbridge/disabled/workspace.go +66 -0
  80. package/pkg/hostbridge/env.go +166 -0
  81. package/pkg/hostbridge/grpc_server.go +113 -0
  82. package/pkg/hostbridge/simple.go +43 -0
  83. package/pkg/hostbridge/simple_workspace.go +85 -0
  84. package/pkg/hostbridge/window.go +129 -0
  85. package/scripts/publish-caret-cli.sh +39 -0
@@ -0,0 +1,187 @@
1
+ package clerror
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+ )
8
+
9
+ // ClineErrorType represents the category of error
10
+ type ClineErrorType string
11
+
12
+ const (
13
+ ErrorTypeAuth ClineErrorType = "auth"
14
+ ErrorTypeNetwork ClineErrorType = "network"
15
+ ErrorTypeRateLimit ClineErrorType = "rateLimit"
16
+ ErrorTypeBalance ClineErrorType = "balance"
17
+ ErrorTypeUnknown ClineErrorType = "unknown"
18
+ )
19
+
20
+ // ClineError represents a parsed error from Cline API
21
+ type ClineError struct {
22
+ Message string `json:"message"`
23
+ Status int `json:"status"`
24
+ RequestID string `json:"request_id"`
25
+ Code interface{} `json:"code"` // Can be string or int
26
+ ModelID string `json:"modelId"`
27
+ ProviderID string `json:"providerId"`
28
+ Details map[string]interface{} `json:"details"`
29
+ }
30
+
31
+ // GetCodeString returns the code as a string regardless of its type
32
+ func (e *ClineError) GetCodeString() string {
33
+ if e == nil || e.Code == nil {
34
+ return ""
35
+ }
36
+ switch v := e.Code.(type) {
37
+ case string:
38
+ return v
39
+ case float64:
40
+ return fmt.Sprintf("%.0f", v)
41
+ case int:
42
+ return fmt.Sprintf("%d", v)
43
+ default:
44
+ return fmt.Sprintf("%v", v)
45
+ }
46
+ }
47
+
48
+ // Rate limit patterns from webview
49
+ var rateLimitPatterns = []string{
50
+ "status code 429",
51
+ "rate limit",
52
+ "too many requests",
53
+ "quota exceeded",
54
+ "resource exhausted",
55
+ }
56
+
57
+ // ParseClineError parses a JSON error string into a ClineError
58
+ func ParseClineError(errorJSON string) (*ClineError, error) {
59
+ if errorJSON == "" {
60
+ return nil, nil
61
+ }
62
+
63
+ var err ClineError
64
+ if parseErr := json.Unmarshal([]byte(errorJSON), &err); parseErr != nil {
65
+ // If JSON parsing fails, create a simple error with the message
66
+ return &ClineError{
67
+ Message: errorJSON,
68
+ }, nil
69
+ }
70
+
71
+ return &err, nil
72
+ }
73
+
74
+ // GetErrorType determines the type of error based on code, status, and message
75
+ func (e *ClineError) GetErrorType() ClineErrorType {
76
+ if e == nil {
77
+ return ErrorTypeUnknown
78
+ }
79
+
80
+ // Check balance error first (most specific)
81
+ codeStr := e.GetCodeString()
82
+ if codeStr == "insufficient_credits" {
83
+ return ErrorTypeBalance
84
+ }
85
+
86
+ // Check auth errors
87
+ if codeStr == "ERR_BAD_REQUEST" || e.Status == 401 {
88
+ return ErrorTypeAuth
89
+ }
90
+
91
+ // Check for auth message
92
+ if strings.Contains(e.Message, "Authentication required") ||
93
+ strings.Contains(e.Message, "Invalid API key") ||
94
+ strings.Contains(e.Message, "Unauthorized") {
95
+ return ErrorTypeAuth
96
+ }
97
+
98
+ // Check rate limit patterns
99
+ messageLower := strings.ToLower(e.Message)
100
+ for _, pattern := range rateLimitPatterns {
101
+ if strings.Contains(messageLower, pattern) {
102
+ return ErrorTypeRateLimit
103
+ }
104
+ }
105
+
106
+ return ErrorTypeUnknown
107
+ }
108
+
109
+ // IsBalanceError returns true if this is a balance/credits error
110
+ func (e *ClineError) IsBalanceError() bool {
111
+ return e.GetErrorType() == ErrorTypeBalance
112
+ }
113
+
114
+ // IsAuthError returns true if this is an authentication error
115
+ func (e *ClineError) IsAuthError() bool {
116
+ return e.GetErrorType() == ErrorTypeAuth
117
+ }
118
+
119
+ // IsRateLimitError returns true if this is a rate limit error
120
+ func (e *ClineError) IsRateLimitError() bool {
121
+ return e.GetErrorType() == ErrorTypeRateLimit
122
+ }
123
+
124
+ // GetCurrentBalance returns the current balance if available
125
+ func (e *ClineError) GetCurrentBalance() *float64 {
126
+ if e == nil || e.Details == nil {
127
+ return nil
128
+ }
129
+
130
+ if balance, ok := e.Details["current_balance"].(float64); ok {
131
+ return &balance
132
+ }
133
+
134
+ return nil
135
+ }
136
+
137
+ // GetBuyCreditsURL returns the URL to buy credits if available
138
+ func (e *ClineError) GetBuyCreditsURL() string {
139
+ if e == nil || e.Details == nil {
140
+ return ""
141
+ }
142
+
143
+ if url, ok := e.Details["buy_credits_url"].(string); ok {
144
+ return url
145
+ }
146
+
147
+ return ""
148
+ }
149
+
150
+ // GetTotalSpent returns the total spent amount if available
151
+ func (e *ClineError) GetTotalSpent() *float64 {
152
+ if e == nil || e.Details == nil {
153
+ return nil
154
+ }
155
+
156
+ if spent, ok := e.Details["total_spent"].(float64); ok {
157
+ return &spent
158
+ }
159
+
160
+ return nil
161
+ }
162
+
163
+ // GetTotalPromotions returns the total promotions amount if available
164
+ func (e *ClineError) GetTotalPromotions() *float64 {
165
+ if e == nil || e.Details == nil {
166
+ return nil
167
+ }
168
+
169
+ if promos, ok := e.Details["total_promotions"].(float64); ok {
170
+ return &promos
171
+ }
172
+
173
+ return nil
174
+ }
175
+
176
+ // GetDetailMessage returns the detail message from error.details if available
177
+ func (e *ClineError) GetDetailMessage() string {
178
+ if e == nil || e.Details == nil {
179
+ return ""
180
+ }
181
+
182
+ if msg, ok := e.Details["message"].(string); ok {
183
+ return msg
184
+ }
185
+
186
+ return ""
187
+ }
@@ -0,0 +1,208 @@
1
+ package config
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/cline/cli/pkg/cli/global"
10
+ "github.com/cline/grpc-go/client"
11
+ "github.com/cline/grpc-go/cline"
12
+ )
13
+
14
+ type Manager struct {
15
+ client *client.ClineClient
16
+ clientAddress string
17
+ }
18
+
19
+ func NewManager(ctx context.Context, address string) (*Manager, error) {
20
+ var c *client.ClineClient
21
+ var err error
22
+
23
+ if address != "" {
24
+ c, err = global.GetClientForAddress(ctx, address)
25
+ } else {
26
+ c, err = global.GetDefaultClient(ctx)
27
+ }
28
+
29
+ if err != nil {
30
+ return nil, fmt.Errorf("failed to get client: %w", err)
31
+ }
32
+
33
+ // Get the actual address being used
34
+ clientAddress := address
35
+ if address == "" && global.Clients != nil {
36
+ clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
37
+ }
38
+
39
+ return &Manager{
40
+ client: c,
41
+ clientAddress: clientAddress,
42
+ }, nil
43
+ }
44
+
45
+ // GetCurrentInstance returns the address of the current instance
46
+ func (m *Manager) GetCurrentInstance() string {
47
+ return m.clientAddress
48
+ }
49
+
50
+ func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error {
51
+ request := &cline.UpdateSettingsRequestCli{
52
+ Metadata: &cline.Metadata{},
53
+ Settings: settings,
54
+ Secrets: secrets,
55
+ }
56
+
57
+ // Call the updateSettingsCli RPC
58
+ _, err := m.client.State.UpdateSettingsCli(ctx, request)
59
+ if err != nil {
60
+ return fmt.Errorf("failed to update settings: %w", err)
61
+ }
62
+
63
+ fmt.Println("Settings updated successfully")
64
+ fmt.Printf("Instance: %s\n", m.clientAddress)
65
+ return nil
66
+ }
67
+
68
+ func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) {
69
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
70
+ if err != nil {
71
+ return nil, fmt.Errorf("failed to get state: %w", err)
72
+ }
73
+
74
+ var stateData map[string]interface{}
75
+ if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
76
+ return nil, fmt.Errorf("failed to parse state: %w", err)
77
+ }
78
+
79
+ return stateData, nil
80
+ }
81
+
82
+ func (m *Manager) ListSettings(ctx context.Context) error {
83
+ // Get state
84
+ stateData, err := m.GetState(ctx)
85
+ if err != nil {
86
+ return err
87
+ }
88
+
89
+ // Subset of fields we will print the values for
90
+ settingsFields := []string{
91
+ "apiConfiguration",
92
+ "telemetrySetting",
93
+ "planActSeparateModelsSetting",
94
+ "enableCheckpointsSetting",
95
+ "mcpMarketplaceEnabled",
96
+ "shellIntegrationTimeout",
97
+ "terminalReuseEnabled",
98
+ "mcpResponsesCollapsed",
99
+ "mcpDisplayMode",
100
+ "terminalOutputLineLimit",
101
+ "mode",
102
+ "preferredLanguage",
103
+ "openaiReasoningEffort",
104
+ "strictPlanModeEnabled",
105
+ "focusChainSettings",
106
+ "useAutoCondense",
107
+ "customPrompt",
108
+ "browserSettings",
109
+ "defaultTerminalProfile",
110
+ "yoloModeToggled",
111
+ "dictationSettings",
112
+ "autoCondenseThreshold",
113
+ "autoApprovalSettings",
114
+ }
115
+
116
+ // Render each field using the renderer
117
+ for _, field := range settingsFields {
118
+ if value, ok := stateData[field]; ok {
119
+ if err := RenderField(field, value, true); err != nil {
120
+ fmt.Printf("Error rendering %s: %v\n", field, err)
121
+ }
122
+ fmt.Println()
123
+ }
124
+ }
125
+
126
+ return nil
127
+ }
128
+
129
+ func (m *Manager) GetSetting(ctx context.Context, key string) error {
130
+ // Get state
131
+ stateData, err := m.GetState(ctx)
132
+ if err != nil {
133
+ return err
134
+ }
135
+
136
+ // Convert kebab-case to camelCase path
137
+ parts := kebabToCamelPath(key)
138
+ rootField := parts[0]
139
+
140
+ // Get the value
141
+ value, found := getNestedValue(stateData, parts)
142
+ if !found {
143
+ return fmt.Errorf("setting '%s' not found", key)
144
+ }
145
+
146
+ // Render the value
147
+ if len(parts) == 1 {
148
+ // Top-level field: use RenderField for nice formatting
149
+ return RenderField(rootField, value, false)
150
+ } else {
151
+ // Nested field: simple print
152
+ fmt.Printf("%s: %s\n", key, formatValue(value, rootField, true))
153
+ }
154
+
155
+ return nil
156
+ }
157
+
158
+ // kebabToCamelPath converts a kebab-case path to camelCase
159
+ // e.g., "auto-approval-settings.actions.read-files" -> "autoApprovalSettings.actions.readFiles"
160
+ func kebabToCamelPath(path string) []string {
161
+ parts := strings.Split(path, ".")
162
+ for i, part := range parts {
163
+ parts[i] = kebabToCamel(part)
164
+ }
165
+ return parts
166
+ }
167
+
168
+ // kebabToCamel converts a single kebab-case string to camelCase
169
+ // e.g., "auto-approval-settings" -> "autoApprovalSettings"
170
+ func kebabToCamel(s string) string {
171
+ if s == "" {
172
+ return s
173
+ }
174
+
175
+ parts := strings.Split(s, "-")
176
+ if len(parts) == 1 {
177
+ return s
178
+ }
179
+
180
+ // First part stays lowercase, rest are capitalized
181
+ result := parts[0]
182
+ for i := 1; i < len(parts); i++ {
183
+ if parts[i] != "" {
184
+ result += strings.ToUpper(parts[i][:1]) + parts[i][1:]
185
+ }
186
+ }
187
+ return result
188
+ }
189
+
190
+ // getNestedValue retrieves a value from a nested map using dot notation
191
+ // e.g., "autoApprovalSettings.actions.readFiles"
192
+ func getNestedValue(data map[string]interface{}, parts []string) (interface{}, bool) {
193
+ current := interface{}(data)
194
+
195
+ for _, part := range parts {
196
+ // Try to access as map
197
+ if m, ok := current.(map[string]interface{}); ok {
198
+ if val, exists := m[part]; exists {
199
+ current = val
200
+ continue
201
+ }
202
+ return nil, false
203
+ }
204
+ return nil, false
205
+ }
206
+
207
+ return current, true
208
+ }
@@ -0,0 +1,198 @@
1
+ package config
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ )
7
+
8
+ // sensitiveKeywords defines field name patterns that should be censored
9
+ var sensitiveKeywords = []string{"key", "secret", "password", "cline-account-id"}
10
+
11
+ // camelToKebab converts camelCase to kebab-case
12
+ // e.g., "autoApprovalSettings" -> "auto-approval-settings"
13
+ func camelToKebab(s string) string {
14
+ if s == "" {
15
+ return s
16
+ }
17
+
18
+ var result []rune
19
+ for i, r := range s {
20
+ if i > 0 && r >= 'A' && r <= 'Z' {
21
+ result = append(result, '-')
22
+ }
23
+ result = append(result, r|32) // Convert to lowercase (works for A-Z)
24
+ }
25
+ return string(result)
26
+ }
27
+
28
+ // isSensitiveField checks if a field name contains sensitive keywords
29
+ func isSensitiveField(fieldName string) bool {
30
+ if fieldName == "" {
31
+ return false
32
+ }
33
+
34
+ lowerName := strings.ToLower(fieldName)
35
+ for _, keyword := range sensitiveKeywords {
36
+ if strings.Contains(lowerName, keyword) {
37
+ return true
38
+ }
39
+ }
40
+
41
+ return false
42
+ }
43
+
44
+ // formatValue formats a value for display, handling empty strings and censoring sensitive fields
45
+ func formatValue(val interface{}, fieldName string, censor bool) string {
46
+ // Handle empty strings specifically
47
+ if str, ok := val.(string); ok && str == "" {
48
+ return "''"
49
+ }
50
+
51
+ if censor && isSensitiveField(fieldName) {
52
+ valStr := fmt.Sprintf("%v", val)
53
+ if valStr != "" && valStr != "''" {
54
+ return "********"
55
+ }
56
+ }
57
+
58
+ return fmt.Sprintf("%v", val)
59
+ }
60
+
61
+ // RenderField renders a single config field with proper formatting
62
+ func RenderField(key string, value interface{}, censor bool) error {
63
+ switch key {
64
+ // Nested objects - render with header + nested fields
65
+ case "apiConfiguration":
66
+ return renderApiConfiguration(value, censor)
67
+ case "browserSettings":
68
+ return renderBrowserSettings(value, censor)
69
+ case "focusChainSettings":
70
+ return renderFocusChainSettings(value, censor)
71
+ case "dictationSettings":
72
+ return renderDictationSettings(value, censor)
73
+ case "autoApprovalSettings":
74
+ return renderAutoApprovalSettings(value, censor)
75
+
76
+ // Simple values - just print key: value
77
+ case "mode", "telemetrySetting", "preferredLanguage", "customPrompt",
78
+ "defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort",
79
+ "planActSeparateModelsSetting", "enableCheckpointsSetting",
80
+ "mcpMarketplaceEnabled", "terminalReuseEnabled",
81
+ "mcpResponsesCollapsed", "strictPlanModeEnabled",
82
+ "useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
83
+ "terminalOutputLineLimit", "autoCondenseThreshold":
84
+ fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
85
+ return nil
86
+
87
+ default:
88
+ return fmt.Errorf("unknown config field: %s", key)
89
+ }
90
+ }
91
+
92
+ // renderApiConfiguration renders the API configuration object
93
+ func renderApiConfiguration(value interface{}, censor bool) error {
94
+ fmt.Println("api-configuration:")
95
+
96
+ configMap, ok := value.(map[string]interface{})
97
+ if !ok {
98
+ return fmt.Errorf("invalid api-configuration format")
99
+ }
100
+
101
+ // Print each field directly
102
+ for key, val := range configMap {
103
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
104
+ }
105
+
106
+ return nil
107
+ }
108
+
109
+ // renderBrowserSettings renders browser settings
110
+ func renderBrowserSettings(value interface{}, censor bool) error {
111
+ fmt.Println("browser-settings:")
112
+
113
+ settingsMap, ok := value.(map[string]interface{})
114
+ if !ok {
115
+ return fmt.Errorf("invalid browser-settings format")
116
+ }
117
+
118
+ // Handle nested viewport if present
119
+ if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok {
120
+ fmt.Println(" viewport:")
121
+ for key, val := range viewport {
122
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
123
+ }
124
+ }
125
+
126
+ // Print other fields
127
+ for key, val := range settingsMap {
128
+ if key != "viewport" {
129
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
130
+ }
131
+ }
132
+
133
+ return nil
134
+ }
135
+
136
+ // renderFocusChainSettings renders focus chain settings
137
+ func renderFocusChainSettings(value interface{}, censor bool) error {
138
+ fmt.Println("focus-chain-settings:")
139
+
140
+ settingsMap, ok := value.(map[string]interface{})
141
+ if !ok {
142
+ return fmt.Errorf("invalid focus-chain-settings format")
143
+ }
144
+
145
+ for key, val := range settingsMap {
146
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
147
+ }
148
+
149
+ return nil
150
+ }
151
+
152
+ // renderDictationSettings renders dictation settings
153
+ func renderDictationSettings(value interface{}, censor bool) error {
154
+ fmt.Println("dictation-settings:")
155
+
156
+ settingsMap, ok := value.(map[string]interface{})
157
+ if !ok {
158
+ return fmt.Errorf("invalid dictation-settings format")
159
+ }
160
+
161
+ for key, val := range settingsMap {
162
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
163
+ }
164
+
165
+ return nil
166
+ }
167
+
168
+ // renderAutoApprovalSettings renders auto approval settings
169
+ func renderAutoApprovalSettings(value interface{}, censor bool) error {
170
+ fmt.Println("auto-approval-settings:")
171
+
172
+ settingsMap, ok := value.(map[string]interface{})
173
+ if !ok {
174
+ return fmt.Errorf("invalid auto-approval-settings format")
175
+ }
176
+
177
+ // Print top-level fields (skip version, handle actions specially)
178
+ for key, val := range settingsMap {
179
+ if key == "version" {
180
+ continue // Skip version
181
+ }
182
+
183
+ if key == "actions" {
184
+ // Handle nested actions with double indentation
185
+ fmt.Println(" actions:")
186
+ if actionsMap, ok := val.(map[string]interface{}); ok {
187
+ for actionKey, actionVal := range actionsMap {
188
+ fmt.Printf(" %s: %s\n", camelToKebab(actionKey), formatValue(actionVal, actionKey, censor))
189
+ }
190
+ }
191
+ } else {
192
+ // Print other fields normally (enabled, enableNotifications, favorites)
193
+ fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
194
+ }
195
+ }
196
+
197
+ return nil
198
+ }