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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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
|
data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb}
RENAMED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
3
5
|
module LanguageOperator
|
|
4
|
-
module
|
|
5
|
-
module
|
|
6
|
-
#
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Common input validation patterns for CLI wizards
|
|
7
9
|
#
|
|
8
|
-
# Provides helpers for
|
|
9
|
-
#
|
|
10
|
+
# Provides validation helpers for URLs, Kubernetes names, emails, ports, etc.
|
|
11
|
+
# Uses UxHelper for prompt access.
|
|
10
12
|
#
|
|
11
13
|
# @example
|
|
12
|
-
# class
|
|
13
|
-
# include
|
|
14
|
+
# class MyWizard
|
|
15
|
+
# include Helpers::UxHelper
|
|
16
|
+
# include Helpers::ValidationHelper
|
|
14
17
|
#
|
|
15
|
-
# def
|
|
16
|
-
# url = ask_url('Enter endpoint
|
|
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
|
|
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
|