language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -0,0 +1,538 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+ require 'tty-prompt'
5
+
6
+ module LanguageOperator
7
+ module CLI
8
+ module Helpers
9
+ # Provides unified access to TTY UI components across all CLI commands,
10
+ # formatters, wizards, and error handlers.
11
+ #
12
+ # This module consolidates TTY initialization that was previously duplicated
13
+ # across multiple files. It provides memoized instances to avoid unnecessary
14
+ # object allocation.
15
+ #
16
+ # Available helpers:
17
+ # - +pastel+ - Terminal colors and styles
18
+ # - +prompt+ - Interactive user input
19
+ # - +spinner+ - Loading/progress spinners
20
+ # - +table+ - Formatted table display
21
+ # - +box+ - Framed messages
22
+ #
23
+ # @example Using in a command
24
+ # class MyCommand < Thor
25
+ # include Helpers::UxHelper
26
+ #
27
+ # def execute
28
+ # puts pastel.green("Success!")
29
+ # answer = prompt.ask("What's your name?")
30
+ #
31
+ # spin = spinner("Loading...")
32
+ # spin.auto_spin
33
+ # # do work
34
+ # spin.success("Done!")
35
+ # end
36
+ # end
37
+ #
38
+ # @example Using in a formatter
39
+ # class MyFormatter
40
+ # include Helpers::UxHelper
41
+ #
42
+ # def format(data)
43
+ # tbl = table(['Name', 'Status'], data)
44
+ # tbl.render(:unicode)
45
+ # end
46
+ # end
47
+ module UxHelper
48
+ # Returns a memoized Pastel instance for colorizing terminal output
49
+ #
50
+ # @return [Pastel] Colorization utility
51
+ # @example
52
+ # puts pastel.green("Success")
53
+ # puts pastel.red.bold("Error!")
54
+ def pastel
55
+ @pastel ||= Pastel.new
56
+ end
57
+
58
+ # Returns a memoized TTY::Prompt instance for interactive input
59
+ #
60
+ # @return [TTY::Prompt] Interactive prompt utility
61
+ # @example
62
+ # name = prompt.ask("Name?")
63
+ # confirmed = prompt.yes?("Continue?")
64
+ # choice = prompt.select("Pick:", %w[a b c])
65
+ def prompt
66
+ @prompt ||= TTY::Prompt.new
67
+ end
68
+
69
+ # Creates a new spinner for long-running operations
70
+ #
71
+ # @param message [String] The message to display next to the spinner
72
+ # @param format [Symbol] Spinner format (:dots, :dots2, :line, :pipe, etc.)
73
+ # @return [TTY::Spinner] Spinner instance
74
+ # @example Basic usage
75
+ # spin = spinner("Loading...")
76
+ # spin.auto_spin
77
+ # # do work
78
+ # spin.success("Done!")
79
+ # @example With custom format
80
+ # spin = spinner("Processing...", format: :dots2)
81
+ # spin.auto_spin
82
+ def spinner(message, format: :dots)
83
+ require 'tty-spinner'
84
+ TTY::Spinner.new(
85
+ "[:spinner] #{message}",
86
+ format: format,
87
+ success_mark: pastel.green('✓'),
88
+ error_mark: pastel.red('✗')
89
+ )
90
+ end
91
+
92
+ # Creates a formatted table for structured data display
93
+ #
94
+ # @param header [Array<String>] Column headers
95
+ # @param rows [Array<Array>] Table rows
96
+ # @param style [Symbol] Rendering style (:unicode, :ascii, :basic, etc.)
97
+ # @return [TTY::Table] Table instance ready to render
98
+ # @example Basic table
99
+ # tbl = table(['Name', 'Status'], [['agent1', 'running'], ['agent2', 'stopped']])
100
+ # puts tbl.render(:unicode)
101
+ # @example With padding
102
+ # tbl = table(['ID', 'Value'], data)
103
+ # puts tbl.render(:unicode, padding: [0, 1])
104
+ def table(header, rows, style: :unicode)
105
+ require 'tty-table'
106
+ tbl = TTY::Table.new(header, rows)
107
+ tbl.render(style, padding: [0, 1])
108
+ end
109
+
110
+ # Creates a framed box around a message
111
+ #
112
+ # @param message [String] The message to frame
113
+ # @param title [String, nil] Optional title for the box
114
+ # @param style [Hash, Symbol] Box style or preset (:classic, :thick, :light)
115
+ # @param padding [Integer, Array] Padding inside the box
116
+ # @return [String] The framed message ready to print
117
+ # @example Simple box
118
+ # puts box("Important message!")
119
+ # @example With title and custom style
120
+ # puts box("Warning!", title: "Alert", border: :thick)
121
+ # @example With custom styling
122
+ # puts box("Info", style: { border: { fg: :cyan } }, padding: 1)
123
+ def box(message, title: nil, border: :light, padding: 1)
124
+ require 'tty-box'
125
+
126
+ options = {
127
+ padding: padding,
128
+ border: border
129
+ }
130
+ options[:title] = { top_left: " #{title} " } if title
131
+
132
+ TTY::Box.frame(message, **options)
133
+ end
134
+
135
+ # Highlights Ruby code with syntax highlighting for terminal display
136
+ #
137
+ # @param code_content [String] The Ruby code to highlight
138
+ # @return [String] Syntax-highlighted code ready for terminal output
139
+ # @example
140
+ # puts highlight_ruby_code("puts 'Hello, world!'")
141
+ def highlight_ruby_code(code_content)
142
+ highlighted = rouge_formatter.format(rouge_lexer.lex(code_content))
143
+ highlighted
144
+ end
145
+
146
+ def logo(title: nil, sparkle: false)
147
+ puts
148
+
149
+ if sparkle
150
+ animate_sparkle_logo
151
+ else
152
+ puts "#{pastel.bold.green('LANGUAGE OPERATOR')} v#{pastel.bold(LanguageOperator::VERSION)}"
153
+ end
154
+
155
+ puts pastel.dim("#{pastel.bold('↪')} #{title}") if title
156
+ puts
157
+ end
158
+
159
+ private
160
+
161
+ # Returns a memoized Rouge formatter for syntax highlighting
162
+ #
163
+ # @return [Rouge::Formatters::Terminal256] Terminal formatter instance
164
+ def rouge_formatter
165
+ @rouge_formatter ||= begin
166
+ require 'rouge'
167
+ Rouge::Formatters::Terminal256.new
168
+ end
169
+ end
170
+
171
+ # Returns a memoized Rouge lexer for Ruby code
172
+ #
173
+ # @return [Rouge::Lexers::Ruby] Ruby lexer instance
174
+ def rouge_lexer
175
+ @rouge_lexer ||= begin
176
+ require 'rouge'
177
+ Rouge::Lexers::Ruby.new
178
+ end
179
+ end
180
+
181
+ def animate_sparkle_logo
182
+ text = 'LANGUAGE OPERATOR'
183
+ frames = 8
184
+ duration = 0.05 # seconds per frame
185
+
186
+ # Move cursor up to overwrite the same line
187
+ print "\e[?25l" # Hide cursor
188
+
189
+ frames.times do |frame|
190
+ # Build the colored string
191
+ colored_text = text.chars.map.with_index do |char, idx|
192
+ # Calculate distance from the wave position
193
+ wave_position = (text.length.to_f / frames) * frame
194
+ distance = (idx - wave_position).abs
195
+
196
+ # Create a gradient effect based on distance
197
+ if distance < 2
198
+ pastel.bold.bright_green(char) # Bright sparkle
199
+ elsif distance < 4
200
+ pastel.bold.green(char) # Medium green
201
+ else
202
+ pastel.green(char) # Base green
203
+ end
204
+ end.join
205
+
206
+ # Print the frame
207
+ print "\r#{colored_text} v#{pastel.bold(LanguageOperator::VERSION)}"
208
+ $stdout.flush
209
+ sleep duration
210
+ end
211
+
212
+ # Final state - full bright
213
+ print "\r#{pastel.bold.bright_green(text)} v#{pastel.bold(LanguageOperator::VERSION)}"
214
+ puts
215
+ print "\e[?25h" # Show cursor
216
+ end
217
+
218
+ # Creates a highlighted box with a colored title bar and content rows
219
+ #
220
+ # @param title [String] The title for the box
221
+ # @param rows [Hash] Content rows where key is the label and value is the content
222
+ # @param title_char [String] Character to use for the title bar (default: '❚')
223
+ # @param color [Symbol] Color for the title and character (default: :yellow)
224
+ # @return [String] The formatted box output
225
+ # @example Simple usage
226
+ # highlighted_box(
227
+ # title: 'Model Details',
228
+ # rows: {
229
+ # 'Name' => 'gpt-4',
230
+ # 'Provider' => 'OpenAI',
231
+ # 'Status' => 'active'
232
+ # }
233
+ # )
234
+ # # Output:
235
+ # # ❚ Model Details:
236
+ # # ❚ Name: gpt-4
237
+ # # ❚ Provider: OpenAI
238
+ # # ❚ Status: active
239
+ #
240
+ # @example With custom color
241
+ # highlighted_box(
242
+ # title: 'Error Details',
243
+ # rows: { 'Code' => '500', 'Message' => 'Server error' },
244
+ # color: :red
245
+ # )
246
+ def highlighted_box(title:, rows:, title_char: '❚', color: :yellow)
247
+ output = []
248
+ output << pastel.bold.public_send(color, "#{title_char} #{title}")
249
+
250
+ # Find max label width for alignment
251
+ max_label_width = rows.keys.map(&:length).max || 0
252
+
253
+ rows.each do |label, value|
254
+ next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
255
+
256
+ padded_label = label.ljust(max_label_width)
257
+ output << "#{pastel.dim.public_send(color, title_char)} #{padded_label}: #{value}"
258
+ end
259
+
260
+ puts output.join("\n")
261
+ end
262
+
263
+ # Displays a formatted list with various styles
264
+ #
265
+ # @param title [String] The title/header for the list
266
+ # @param items [Array, Hash] The items to display
267
+ # @param empty_message [String] Message to show when list is empty (default: '(none)')
268
+ # @param style [Symbol] Display style (:simple, :detailed, :conditions, :key_value)
269
+ # @param bullet [String] Bullet character for list items (default: '-')
270
+ # @return [void]
271
+ # @example Simple list
272
+ # list_box(
273
+ # title: 'Models',
274
+ # items: ['gpt-4', 'claude-3']
275
+ # )
276
+ # # Output:
277
+ # # Models (2):
278
+ # # - gpt-4
279
+ # # - claude-3
280
+ #
281
+ # @example Simple list with custom bullet
282
+ # list_box(
283
+ # title: 'Models',
284
+ # items: ['gpt-4', 'claude-3'],
285
+ # bullet: '•'
286
+ # )
287
+ # # Output:
288
+ # # Models (2):
289
+ # # • gpt-4
290
+ # # • claude-3
291
+ #
292
+ # @example Detailed list with metadata
293
+ # list_box(
294
+ # title: 'Agents',
295
+ # items: [
296
+ # { name: 'bash', status: 'Running' },
297
+ # { name: 'web', status: 'Stopped' }
298
+ # ],
299
+ # style: :detailed
300
+ # )
301
+ # # Output:
302
+ # # Agents (2):
303
+ # # - bash (Running)
304
+ # # - web (Stopped)
305
+ #
306
+ # @example Conditions list
307
+ # list_box(
308
+ # title: 'Conditions',
309
+ # items: [
310
+ # { type: 'Ready', status: 'True', message: 'Agent is ready' },
311
+ # { type: 'Validated', status: 'False', message: 'Validation failed' }
312
+ # ],
313
+ # style: :conditions
314
+ # )
315
+ # # Output:
316
+ # # Conditions (2):
317
+ # # ✓ Ready: Agent is ready
318
+ # # ✗ Validated: Validation failed
319
+ #
320
+ # @example Key-value pairs
321
+ # list_box(
322
+ # title: 'Labels',
323
+ # items: { 'app' => 'web', 'env' => 'prod' },
324
+ # style: :key_value
325
+ # )
326
+ # # Output:
327
+ # # Labels:
328
+ # # app: web
329
+ # # env: prod
330
+ def list_box(title:, items:, empty_message: 'none', style: :simple, bullet: '•')
331
+ # Convert K8s::Resource or hash-like objects to plain Hash/Array
332
+ # Only call to_h if it's not already an Array (Arrays respond to to_h but it behaves differently)
333
+ items_normalized = if items.is_a?(Array)
334
+ items
335
+ else
336
+ (items.respond_to?(:to_h) ? items.to_h : items)
337
+ end
338
+
339
+ # Convert items to array if it's a hash (for key_value style)
340
+ items_array = items_normalized.is_a?(Hash) ? items_normalized.to_a : items_normalized
341
+ count = items_array.length
342
+
343
+ # Print title with count
344
+ puts "#{pastel.white.bold(title)} #{pastel.dim("(#{count})")}"
345
+
346
+ # Handle empty lists
347
+ if items_array.empty?
348
+ puts pastel.dim(empty_message)
349
+ return
350
+ end
351
+
352
+ # Render based on style
353
+ case style
354
+ when :simple
355
+ items_array.each do |item|
356
+ puts "#{bullet} #{item}"
357
+ end
358
+ when :detailed
359
+ items_array.each do |item|
360
+ name = item[:name] || item['name']
361
+ meta = item[:meta] || item['meta'] || item[:status] || item['status']
362
+ if meta
363
+ puts "#{bullet} #{name} (#{meta})"
364
+ else
365
+ puts "#{bullet} #{name}"
366
+ end
367
+ end
368
+ when :conditions
369
+ items_array.each do |condition|
370
+ status = condition[:status] || condition['status']
371
+ type = condition[:type] || condition['type']
372
+ message = condition[:message] || condition['message'] || condition[:reason] || condition['reason']
373
+ icon = status == 'True' ? pastel.green('✓') : pastel.red('✗')
374
+ puts "#{icon} #{type}: #{message}"
375
+ end
376
+ when :key_value
377
+ items_array.each do |key, value|
378
+ puts "#{key}: #{value}"
379
+ end
380
+ end
381
+ end
382
+
383
+ # Confirm deletion with user in a clean, simple format
384
+ #
385
+ # @param resource_type [String] Type of resource being deleted (e.g., 'agent', 'model')
386
+ # @param name [String] Resource name
387
+ # @param cluster [String] Cluster name
388
+ # @return [Boolean] True if user confirms, false otherwise
389
+ # @example
390
+ # confirm_deletion('agent', 'bash', 'production')
391
+ # # Output: Are you sure you want to delete agent bash from cluster production? (y/N)
392
+ def confirm_deletion(resource_type, name, cluster)
393
+ message = if resource_type == 'cluster'
394
+ "Are you sure you want to delete #{resource_type} #{pastel.red.bold(name)}?"
395
+ else
396
+ "Are you sure you want to delete #{resource_type} #{pastel.red.bold(name)} " \
397
+ "from cluster #{pastel.red.bold(cluster)}?"
398
+ end
399
+ prompt.yes?(message)
400
+ end
401
+
402
+ # Generic resource detail formatter that eliminates duplication
403
+ #
404
+ # @param type [String] Resource type (e.g., 'Cluster', 'Agent', 'Model', 'Tool')
405
+ # @param name [String] Resource name
406
+ # @param common_fields [Hash] Fields that appear in all resources
407
+ # @param optional_fields [Hash] Fields that may be nil and should be filtered
408
+ # @return [void] Displays formatted resource information
409
+ def format_resource_details(type:, name:, common_fields: {}, optional_fields: {})
410
+ rows = { 'Name' => pastel.white.bold(name) }
411
+ rows.merge!(common_fields)
412
+
413
+ optional_fields.each do |key, value|
414
+ case key
415
+ when 'Domain'
416
+ rows[key] = value if value && !value.empty?
417
+ else
418
+ rows[key] = value if value
419
+ end
420
+ end
421
+
422
+ highlighted_box(
423
+ title: "Language#{type}",
424
+ rows: rows.compact
425
+ )
426
+ end
427
+
428
+ # Formats cluster details for consistent display in creation and inspection
429
+ #
430
+ # @param name [String] Cluster name
431
+ # @param namespace [String] Kubernetes namespace
432
+ # @param context [String] Kubernetes context
433
+ # @param status [String, nil] Cluster status (optional)
434
+ # @param created [String, nil] Creation timestamp (optional)
435
+ # @param domain [String, nil] Cluster domain (optional)
436
+ # @return [void] Displays formatted cluster information
437
+ def format_cluster_details(name:, namespace:, context:, status: nil, created: nil, domain: nil)
438
+ format_resource_details(
439
+ type: 'Cluster',
440
+ name: name,
441
+ common_fields: {
442
+ 'Namespace' => namespace,
443
+ 'Context' => context
444
+ },
445
+ optional_fields: {
446
+ 'Domain' => domain,
447
+ 'Status' => status,
448
+ 'Created' => created
449
+ }
450
+ )
451
+ end
452
+
453
+ # Formats agent details for consistent display in creation and inspection
454
+ #
455
+ # @param name [String] Agent name
456
+ # @param namespace [String] Kubernetes namespace
457
+ # @param cluster [String] Cluster name
458
+ # @param status [String, nil] Agent status (optional)
459
+ # @param mode [String, nil] Agent mode (optional)
460
+ # @param schedule [String, nil] Agent schedule (optional)
461
+ # @param persona [String, nil] Agent persona (optional)
462
+ # @param created [String, nil] Creation timestamp (optional)
463
+ # @return [void] Displays formatted agent information
464
+ def format_agent_details(name:, namespace:, cluster:, status: nil, mode: nil, schedule: nil, persona: nil, created: nil)
465
+ format_resource_details(
466
+ type: 'Agent',
467
+ name: name,
468
+ common_fields: {
469
+ 'Namespace' => namespace,
470
+ 'Cluster' => cluster
471
+ },
472
+ optional_fields: {
473
+ 'Status' => status,
474
+ 'Mode' => mode,
475
+ 'Schedule' => schedule,
476
+ 'Persona' => persona,
477
+ 'Created' => created
478
+ }
479
+ )
480
+ end
481
+
482
+ # Formats model details for consistent display in creation and inspection
483
+ #
484
+ # @param name [String] Model name
485
+ # @param namespace [String] Kubernetes namespace
486
+ # @param cluster [String] Cluster name
487
+ # @param status [String, nil] Model status (optional)
488
+ # @param provider [String, nil] Model provider (optional)
489
+ # @param model [String, nil] Model identifier (optional)
490
+ # @param endpoint [String, nil] Model endpoint (optional)
491
+ # @param created [String, nil] Creation timestamp (optional)
492
+ # @return [void] Displays formatted model information
493
+ def format_model_details(name:, namespace:, cluster:, status: nil, provider: nil, model: nil, endpoint: nil, created: nil)
494
+ format_resource_details(
495
+ type: 'Model',
496
+ name: name,
497
+ common_fields: {
498
+ 'Namespace' => namespace,
499
+ 'Cluster' => cluster
500
+ },
501
+ optional_fields: {
502
+ 'Status' => status,
503
+ 'Provider' => provider,
504
+ 'Model' => model,
505
+ 'Endpoint' => endpoint,
506
+ 'Created' => created
507
+ }
508
+ )
509
+ end
510
+
511
+ # Formats tool details for consistent display in creation and inspection
512
+ #
513
+ # @param name [String] Tool name
514
+ # @param namespace [String] Kubernetes namespace
515
+ # @param cluster [String] Cluster name
516
+ # @param status [String, nil] Tool status (optional)
517
+ # @param image [String, nil] Tool container image (optional)
518
+ # @param created [String, nil] Creation timestamp (optional)
519
+ # @return [void] Displays formatted tool information
520
+ def format_tool_details(name:, namespace:, cluster:, status: nil, image: nil, created: nil)
521
+ format_resource_details(
522
+ type: 'Tool',
523
+ name: name,
524
+ common_fields: {
525
+ 'Namespace' => namespace,
526
+ 'Cluster' => cluster
527
+ },
528
+ optional_fields: {
529
+ 'Status' => status,
530
+ 'Image' => image,
531
+ 'Created' => created
532
+ }
533
+ )
534
+ end
535
+ end
536
+ end
537
+ end
538
+ end
@@ -1,23 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module LanguageOperator
4
- module Ux
5
- module Concerns
6
- # Mixin for common input validation and prompting patterns
6
+ module CLI
7
+ module Helpers
8
+ # Common input validation patterns for CLI wizards
7
9
  #
8
- # Provides helpers for common validation scenarios like URLs, emails,
9
- # Kubernetes resource names, and other frequently validated inputs.
10
+ # Provides validation helpers for URLs, Kubernetes names, emails, ports, etc.
11
+ # Uses UxHelper for prompt access.
10
12
  #
11
13
  # @example
12
- # class MyFlow < Base
13
- # include Concerns::InputValidation
14
+ # class MyWizard
15
+ # include Helpers::UxHelper
16
+ # include Helpers::ValidationHelper
14
17
  #
15
- # def execute
16
- # url = ask_url('Enter endpoint URL:')
18
+ # def run
19
+ # url = ask_url('Enter endpoint:')
17
20
  # name = ask_k8s_name('Resource name:')
18
21
  # end
19
22
  # end
20
- module InputValidation
23
+ module ValidationHelper
21
24
  # Ask for a URL with validation
22
25
  #
23
26
  # @param question [String] The prompt question
@@ -49,21 +52,6 @@ module LanguageOperator
49
52
  nil
50
53
  end
51
54
 
52
- # Ask for an email address with validation
53
- #
54
- # @param question [String] The prompt question
55
- # @param default [String, nil] Default value
56
- # @return [String, nil] The validated email or nil if cancelled
57
- def ask_email(question, default: nil)
58
- prompt.ask(question, default: default) do |q|
59
- q.required true
60
- q.validate(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
61
- q.messages[:valid?] = 'Please enter a valid email address'
62
- end
63
- rescue TTY::Reader::InputInterrupt
64
- nil
65
- end
66
-
67
55
  # Ask for a masked input (like API keys or passwords)
68
56
  #
69
57
  # @param question [String] The prompt question
@@ -77,21 +65,6 @@ module LanguageOperator
77
65
  nil
78
66
  end
79
67
 
80
- # Ask for a port number with validation
81
- #
82
- # @param question [String] The prompt question
83
- # @param default [Integer, nil] Default value
84
- # @return [Integer, nil] The validated port or nil if cancelled
85
- def ask_port(question, default: nil)
86
- prompt.ask(question, default: default, convert: :int) do |q|
87
- q.required true
88
- q.validate(->(v) { v.to_i.between?(1, 65_535) })
89
- q.messages[:valid?] = 'Must be a valid port number (1-65535)'
90
- end
91
- rescue TTY::Reader::InputInterrupt
92
- nil
93
- end
94
-
95
68
  # Ask a yes/no question
96
69
  #
97
70
  # @param question [String] The prompt question
@@ -114,32 +87,6 @@ module LanguageOperator
114
87
  rescue TTY::Reader::InputInterrupt
115
88
  nil
116
89
  end
117
-
118
- # Validate and coerce a Kubernetes resource name
119
- #
120
- # @param name [String] The name to validate
121
- # @return [String] The validated and normalized name
122
- # @raise [ArgumentError] If name is invalid
123
- def validate_k8s_name(name)
124
- normalized = name.to_s.downcase.strip
125
- raise ArgumentError, "Invalid Kubernetes name: #{name}" unless normalized.match?(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/)
126
-
127
- normalized
128
- end
129
-
130
- # Validate a URL
131
- #
132
- # @param url [String] The URL to validate
133
- # @return [String] The validated URL
134
- # @raise [ArgumentError] If URL is invalid
135
- def validate_url(url)
136
- uri = URI.parse(url)
137
- raise ArgumentError, "Invalid URL: #{url}" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
138
-
139
- url
140
- rescue URI::InvalidURIError => e
141
- raise ArgumentError, "Invalid URL: #{e.message}"
142
- end
143
90
  end
144
91
  end
145
92
  end