language-operator 0.1.31 → 0.1.35
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/.rubocop.yml +7 -8
- data/CHANGELOG.md +14 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +22 -6
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +339 -407
- data/lib/language_operator/cli/commands/cluster.rb +274 -290
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +218 -284
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +31 -35
- data/lib/language_operator/cli/commands/system.rb +221 -233
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +84 -43
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +0 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
- data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +37 -10
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- data/test_agent_dsl.rb +0 -108
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
# Type coercion system for task inputs and outputs
|
|
5
|
+
#
|
|
6
|
+
# Provides smart type coercion with automatic conversion for common cases
|
|
7
|
+
# and clear error messages when coercion is not possible. This enables
|
|
8
|
+
# flexible type handling while maintaining type safety.
|
|
9
|
+
#
|
|
10
|
+
# Supported types:
|
|
11
|
+
# - integer: Coerces String, Integer, Float to Integer
|
|
12
|
+
# - number: Coerces String, Integer, Float to Float
|
|
13
|
+
# - string: Coerces any value to String via to_s
|
|
14
|
+
# - boolean: Coerces String, Boolean to Boolean (explicit values only)
|
|
15
|
+
# - array: Strict validation (no coercion)
|
|
16
|
+
# - hash: Strict validation (no coercion)
|
|
17
|
+
# - any: No coercion, passes through any value
|
|
18
|
+
#
|
|
19
|
+
# @example Integer coercion
|
|
20
|
+
# TypeCoercion.coerce("123", "integer") # => 123
|
|
21
|
+
# TypeCoercion.coerce(123, "integer") # => 123
|
|
22
|
+
# TypeCoercion.coerce("abc", "integer") # raises ArgumentError
|
|
23
|
+
#
|
|
24
|
+
# @example Boolean coercion
|
|
25
|
+
# TypeCoercion.coerce("true", "boolean") # => true
|
|
26
|
+
# TypeCoercion.coerce("1", "boolean") # => true
|
|
27
|
+
# TypeCoercion.coerce("false", "boolean") # => false
|
|
28
|
+
# TypeCoercion.coerce("maybe", "boolean") # raises ArgumentError
|
|
29
|
+
#
|
|
30
|
+
# @example String coercion (never fails)
|
|
31
|
+
# TypeCoercion.coerce(:symbol, "string") # => "symbol"
|
|
32
|
+
# TypeCoercion.coerce(123, "string") # => "123"
|
|
33
|
+
#
|
|
34
|
+
# @example Strict validation
|
|
35
|
+
# TypeCoercion.coerce([1, 2], "array") # => [1, 2]
|
|
36
|
+
# TypeCoercion.coerce({a: 1}, "array") # raises ArgumentError
|
|
37
|
+
module TypeCoercion
|
|
38
|
+
# Coerce a value to the specified type
|
|
39
|
+
#
|
|
40
|
+
# @param value [Object] Value to coerce
|
|
41
|
+
# @param type [String] Target type (see COERCION_RULES for valid types)
|
|
42
|
+
# @return [Object] Coerced value
|
|
43
|
+
# @raise [ArgumentError] If coercion fails or type is unknown
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# TypeCoercion.coerce("345", "integer") # => 345
|
|
47
|
+
# TypeCoercion.coerce("true", "boolean") # => true
|
|
48
|
+
def self.coerce(value, type)
|
|
49
|
+
case type
|
|
50
|
+
when 'integer'
|
|
51
|
+
coerce_integer(value)
|
|
52
|
+
when 'number'
|
|
53
|
+
coerce_number(value)
|
|
54
|
+
when 'string'
|
|
55
|
+
coerce_string(value)
|
|
56
|
+
when 'boolean'
|
|
57
|
+
coerce_boolean(value)
|
|
58
|
+
when 'array'
|
|
59
|
+
validate_array(value)
|
|
60
|
+
when 'hash'
|
|
61
|
+
validate_hash(value)
|
|
62
|
+
when 'any'
|
|
63
|
+
value
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "Unknown type: #{type}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Coerce value to Integer
|
|
70
|
+
#
|
|
71
|
+
# Accepts: String, Integer, Float
|
|
72
|
+
# Coercion: Uses Ruby's Integer() method
|
|
73
|
+
# Errors: Cannot parse as integer
|
|
74
|
+
#
|
|
75
|
+
# @param value [Object] Value to coerce
|
|
76
|
+
# @return [Integer] Coerced integer
|
|
77
|
+
# @raise [ArgumentError] If coercion fails
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# coerce_integer("123") # => 123
|
|
81
|
+
# coerce_integer(123) # => 123
|
|
82
|
+
# coerce_integer(123.0) # => 123
|
|
83
|
+
# coerce_integer("abc") # raises ArgumentError
|
|
84
|
+
def self.coerce_integer(value)
|
|
85
|
+
return value if value.is_a?(Integer)
|
|
86
|
+
|
|
87
|
+
Integer(value)
|
|
88
|
+
rescue ArgumentError, TypeError => e
|
|
89
|
+
raise ArgumentError, "Cannot coerce #{value.inspect} to integer: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Coerce value to Float (number)
|
|
93
|
+
#
|
|
94
|
+
# Accepts: String, Integer, Float
|
|
95
|
+
# Coercion: Uses Ruby's Float() method
|
|
96
|
+
# Errors: Cannot parse as number
|
|
97
|
+
#
|
|
98
|
+
# @param value [Object] Value to coerce
|
|
99
|
+
# @return [Float] Coerced number
|
|
100
|
+
# @raise [ArgumentError] If coercion fails
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# coerce_number("3.14") # => 3.14
|
|
104
|
+
# coerce_number(3) # => 3.0
|
|
105
|
+
# coerce_number(3.14) # => 3.14
|
|
106
|
+
# coerce_number("not a num") # raises ArgumentError
|
|
107
|
+
def self.coerce_number(value)
|
|
108
|
+
return value if value.is_a?(Numeric)
|
|
109
|
+
|
|
110
|
+
Float(value)
|
|
111
|
+
rescue ArgumentError, TypeError => e
|
|
112
|
+
raise ArgumentError, "Cannot coerce #{value.inspect} to number: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Coerce value to String
|
|
116
|
+
#
|
|
117
|
+
# Accepts: Any object with to_s method (all Ruby objects)
|
|
118
|
+
# Coercion: Uses to_s method
|
|
119
|
+
# Errors: Never (everything has to_s)
|
|
120
|
+
#
|
|
121
|
+
# @param value [Object] Value to coerce
|
|
122
|
+
# @return [String] Coerced string
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# coerce_string(:symbol) # => "symbol"
|
|
126
|
+
# coerce_string(123) # => "123"
|
|
127
|
+
# coerce_string(nil) # => ""
|
|
128
|
+
def self.coerce_string(value)
|
|
129
|
+
value.to_s
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Coerce value to Boolean
|
|
133
|
+
#
|
|
134
|
+
# Accepts: Boolean, String (explicit values only)
|
|
135
|
+
# Coercion: Case-insensitive string matching
|
|
136
|
+
# Truthy: "true", "1", "yes", "t", "y"
|
|
137
|
+
# Falsy: "false", "0", "no", "f", "n"
|
|
138
|
+
# Errors: Ambiguous values (e.g., "maybe", "unknown")
|
|
139
|
+
#
|
|
140
|
+
# @param value [Object] Value to coerce
|
|
141
|
+
# @return [Boolean] Coerced boolean
|
|
142
|
+
# @raise [ArgumentError] If coercion is ambiguous
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# coerce_boolean(true) # => true
|
|
146
|
+
# coerce_boolean("true") # => true
|
|
147
|
+
# coerce_boolean("1") # => true
|
|
148
|
+
# coerce_boolean("yes") # => true
|
|
149
|
+
# coerce_boolean(false) # => false
|
|
150
|
+
# coerce_boolean("false") # => false
|
|
151
|
+
# coerce_boolean("0") # => false
|
|
152
|
+
# coerce_boolean("no") # => false
|
|
153
|
+
# coerce_boolean("maybe") # raises ArgumentError
|
|
154
|
+
def self.coerce_boolean(value)
|
|
155
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
156
|
+
|
|
157
|
+
# Only allow string values for coercion (not integers or other types)
|
|
158
|
+
raise ArgumentError, "Cannot coerce #{value.inspect} to boolean" unless value.is_a?(String)
|
|
159
|
+
|
|
160
|
+
str = value.strip.downcase
|
|
161
|
+
return true if %w[true 1 yes t y].include?(str)
|
|
162
|
+
return false if %w[false 0 no f n].include?(str)
|
|
163
|
+
|
|
164
|
+
raise ArgumentError, "Cannot coerce #{value.inspect} to boolean"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate value is an Array (no coercion)
|
|
168
|
+
#
|
|
169
|
+
# Accepts: Array
|
|
170
|
+
# Coercion: None (strict validation)
|
|
171
|
+
# Errors: Not an array
|
|
172
|
+
#
|
|
173
|
+
# @param value [Object] Value to validate
|
|
174
|
+
# @return [Array] Validated array
|
|
175
|
+
# @raise [ArgumentError] If not an array
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# validate_array([1, 2, 3]) # => [1, 2, 3]
|
|
179
|
+
# validate_array({a: 1}) # raises ArgumentError
|
|
180
|
+
def self.validate_array(value)
|
|
181
|
+
raise ArgumentError, "Expected array, got #{value.class}" unless value.is_a?(Array)
|
|
182
|
+
|
|
183
|
+
value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Validate value is a Hash (no coercion)
|
|
187
|
+
#
|
|
188
|
+
# Accepts: Hash
|
|
189
|
+
# Coercion: None (strict validation)
|
|
190
|
+
# Errors: Not a hash
|
|
191
|
+
#
|
|
192
|
+
# @param value [Object] Value to validate
|
|
193
|
+
# @return [Hash] Validated hash
|
|
194
|
+
# @raise [ArgumentError] If not a hash
|
|
195
|
+
#
|
|
196
|
+
# @example
|
|
197
|
+
# validate_hash({a: 1, b: 2}) # => {a: 1, b: 2}
|
|
198
|
+
# validate_hash([1, 2]) # raises ArgumentError
|
|
199
|
+
def self.validate_hash(value)
|
|
200
|
+
raise ArgumentError, "Expected hash, got #{value.class}" unless value.is_a?(Hash)
|
|
201
|
+
|
|
202
|
+
value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Coercion rules table
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash] Mapping of types to their coercion behavior
|
|
208
|
+
# rubocop:disable Metrics/MethodLength
|
|
209
|
+
def self.coercion_rules
|
|
210
|
+
{
|
|
211
|
+
'integer' => {
|
|
212
|
+
accepts: 'String, Integer, Float',
|
|
213
|
+
method: 'Integer(value)',
|
|
214
|
+
errors: 'Cannot parse as integer'
|
|
215
|
+
},
|
|
216
|
+
'number' => {
|
|
217
|
+
accepts: 'String, Integer, Float',
|
|
218
|
+
method: 'Float(value)',
|
|
219
|
+
errors: 'Cannot parse as number'
|
|
220
|
+
},
|
|
221
|
+
'string' => {
|
|
222
|
+
accepts: 'Any object',
|
|
223
|
+
method: 'value.to_s',
|
|
224
|
+
errors: 'Never (everything has to_s)'
|
|
225
|
+
},
|
|
226
|
+
'boolean' => {
|
|
227
|
+
accepts: 'Boolean, String (explicit values)',
|
|
228
|
+
method: 'Pattern matching (true/1/yes/t/y or false/0/no/f/n)',
|
|
229
|
+
errors: 'Ambiguous values'
|
|
230
|
+
},
|
|
231
|
+
'array' => {
|
|
232
|
+
accepts: 'Array only',
|
|
233
|
+
method: 'No coercion (strict)',
|
|
234
|
+
errors: 'Not an array'
|
|
235
|
+
},
|
|
236
|
+
'hash' => {
|
|
237
|
+
accepts: 'Hash only',
|
|
238
|
+
method: 'No coercion (strict)',
|
|
239
|
+
errors: 'Not a hash'
|
|
240
|
+
},
|
|
241
|
+
'any' => {
|
|
242
|
+
accepts: 'Any value',
|
|
243
|
+
method: 'No coercion (pass-through)',
|
|
244
|
+
errors: 'Never'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
# rubocop:enable Metrics/MethodLength
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-prompt'
|
|
4
|
+
require 'pastel'
|
|
5
|
+
require_relative '../cli/formatters/progress_formatter'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module Ux
|
|
9
|
+
# Base class for interactive user experience flows
|
|
10
|
+
#
|
|
11
|
+
# Provides common infrastructure for TTY-based interactive wizards
|
|
12
|
+
# including cluster context validation and standard UI helpers.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a new UX flow
|
|
15
|
+
# class CreateModel < Base
|
|
16
|
+
# def execute
|
|
17
|
+
# show_welcome
|
|
18
|
+
# # ... flow logic
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Using a UX flow
|
|
23
|
+
# Ux::CreateModel.execute(ctx)
|
|
24
|
+
#
|
|
25
|
+
class Base
|
|
26
|
+
attr_reader :prompt, :pastel, :ctx
|
|
27
|
+
|
|
28
|
+
# Initialize the UX flow
|
|
29
|
+
#
|
|
30
|
+
# @param ctx [Object, nil] Cluster context (required unless overridden)
|
|
31
|
+
def initialize(ctx = nil)
|
|
32
|
+
@prompt = TTY::Prompt.new
|
|
33
|
+
@pastel = Pastel.new
|
|
34
|
+
@ctx = ctx
|
|
35
|
+
|
|
36
|
+
validate_cluster_context! if requires_cluster?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Execute the UX flow
|
|
40
|
+
#
|
|
41
|
+
# Subclasses must override this method to implement their flow logic.
|
|
42
|
+
#
|
|
43
|
+
# @raise [NotImplementedError] if not overridden by subclass
|
|
44
|
+
def execute
|
|
45
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Execute the UX flow as a class method
|
|
49
|
+
#
|
|
50
|
+
# @param ctx [Object, nil] Cluster context
|
|
51
|
+
# @return [Object] Result of execute method
|
|
52
|
+
def self.execute(ctx = nil)
|
|
53
|
+
new(ctx).execute
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Whether this flow requires a cluster context
|
|
59
|
+
#
|
|
60
|
+
# Subclasses can override to disable cluster requirement (e.g., Quickstart).
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] true if cluster is required
|
|
63
|
+
def requires_cluster?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate that a cluster context is available
|
|
68
|
+
#
|
|
69
|
+
# Exits with error if cluster is required but not provided.
|
|
70
|
+
def validate_cluster_context!
|
|
71
|
+
return unless requires_cluster?
|
|
72
|
+
return if ctx
|
|
73
|
+
|
|
74
|
+
CLI::Formatters::ProgressFormatter.error(
|
|
75
|
+
'No cluster selected. Run "aictl cluster add" first.'
|
|
76
|
+
)
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Ux::Concerns
|
|
2
|
+
|
|
3
|
+
Reusable mixins for interactive UX flows.
|
|
4
|
+
|
|
5
|
+
## Available Concerns
|
|
6
|
+
|
|
7
|
+
### Headings
|
|
8
|
+
|
|
9
|
+
Provides consistent formatting for headings, step indicators, and banners.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class MyFlow < Ux::Base
|
|
13
|
+
include Concerns::Headings
|
|
14
|
+
|
|
15
|
+
def execute
|
|
16
|
+
heading('Welcome!', emoji: '🎉')
|
|
17
|
+
step_heading(1, 3, 'First Step')
|
|
18
|
+
subheading('Configuration')
|
|
19
|
+
separator
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Methods:**
|
|
25
|
+
- `heading(text, emoji: nil, width: 50)` - Display prominent heading with border
|
|
26
|
+
- `step_heading(current, total, title, width: 50)` - Display step indicator
|
|
27
|
+
- `subheading(text)` - Display simple subheading
|
|
28
|
+
- `separator(width: 50, char: '─')` - Display separator line
|
|
29
|
+
- `section(title, description: nil)` - Display section header with description
|
|
30
|
+
|
|
31
|
+
### ProviderHelpers
|
|
32
|
+
|
|
33
|
+
Common operations for LLM provider integration.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
class MyFlow < Ux::Base
|
|
37
|
+
include Concerns::ProviderHelpers
|
|
38
|
+
|
|
39
|
+
def execute
|
|
40
|
+
result = test_provider_connection(:anthropic, api_key: 'sk-...')
|
|
41
|
+
models = fetch_provider_models(:openai, api_key: 'sk-...')
|
|
42
|
+
info = provider_info(:anthropic)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Methods:**
|
|
48
|
+
- `test_provider_connection(provider, api_key:, endpoint:)` - Test connection to provider
|
|
49
|
+
- `fetch_provider_models(provider, api_key:, endpoint:)` - Fetch available models
|
|
50
|
+
- `provider_info(provider)` - Get provider display info and documentation URLs
|
|
51
|
+
|
|
52
|
+
**Supported Providers:**
|
|
53
|
+
- `:anthropic` - Anthropic (Claude)
|
|
54
|
+
- `:openai` - OpenAI (GPT)
|
|
55
|
+
- `:openai_compatible` - OpenAI-compatible endpoints (Ollama, vLLM, LM Studio, etc)
|
|
56
|
+
|
|
57
|
+
### InputValidation
|
|
58
|
+
|
|
59
|
+
Common input validation and prompting helpers.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class MyFlow < Ux::Base
|
|
63
|
+
include Concerns::InputValidation
|
|
64
|
+
|
|
65
|
+
def execute
|
|
66
|
+
url = ask_url('Enter endpoint URL:')
|
|
67
|
+
name = ask_k8s_name('Resource name:', default: 'my-resource')
|
|
68
|
+
email = ask_email('Your email:')
|
|
69
|
+
api_key = ask_secret('API key:')
|
|
70
|
+
port = ask_port('Port:', default: 8080)
|
|
71
|
+
confirmed = ask_yes_no('Continue?', default: true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Methods:**
|
|
77
|
+
- `ask_url(question, default:, required:)` - Prompt for URL with validation
|
|
78
|
+
- `ask_k8s_name(question, default:)` - Prompt for Kubernetes resource name
|
|
79
|
+
- `ask_email(question, default:)` - Prompt for email address
|
|
80
|
+
- `ask_secret(question, required:)` - Prompt for masked input (API keys, passwords)
|
|
81
|
+
- `ask_port(question, default:)` - Prompt for port number (1-65535)
|
|
82
|
+
- `ask_yes_no(question, default:)` - Prompt for yes/no confirmation
|
|
83
|
+
- `ask_select(question, choices, per_page:)` - Prompt for selection from list
|
|
84
|
+
- `validate_k8s_name(name)` - Validate and normalize Kubernetes name
|
|
85
|
+
- `validate_url(url)` - Validate URL format
|
|
86
|
+
|
|
87
|
+
## Usage Guidelines
|
|
88
|
+
|
|
89
|
+
### When to Use Concerns
|
|
90
|
+
|
|
91
|
+
✅ **DO use concerns for:**
|
|
92
|
+
- Common formatting patterns used across multiple flows
|
|
93
|
+
- Repeated validation logic
|
|
94
|
+
- Shared provider/API operations
|
|
95
|
+
- Reusable UI components
|
|
96
|
+
|
|
97
|
+
❌ **DON'T use concerns for:**
|
|
98
|
+
- Flow-specific business logic
|
|
99
|
+
- One-off operations
|
|
100
|
+
- Complex state management
|
|
101
|
+
|
|
102
|
+
### Naming Convention
|
|
103
|
+
|
|
104
|
+
Concerns should be:
|
|
105
|
+
- Named as adjectives or capabilities (e.g., `Headings`, `ProviderHelpers`)
|
|
106
|
+
- Focused on a single responsibility
|
|
107
|
+
- Well-documented with examples
|
|
108
|
+
|
|
109
|
+
### Testing
|
|
110
|
+
|
|
111
|
+
Each concern should have corresponding specs in `spec/language_operator/ux/concerns/`.
|
|
112
|
+
|
|
113
|
+
## Creating New Concerns
|
|
114
|
+
|
|
115
|
+
1. Create file in `lib/language_operator/ux/concerns/my_concern.rb`
|
|
116
|
+
2. Define module under `LanguageOperator::Ux::Concerns`
|
|
117
|
+
3. Add YARD documentation with examples
|
|
118
|
+
4. Include in flows via `include Concerns::MyConcern`
|
|
119
|
+
5. Add tests in `spec/language_operator/ux/concerns/my_concern_spec.rb`
|
|
120
|
+
6. Update this README
|
|
121
|
+
|
|
122
|
+
## Example: Creating a New Concern
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# lib/language_operator/ux/concerns/kubernetes_helpers.rb
|
|
126
|
+
module LanguageOperator
|
|
127
|
+
module Ux
|
|
128
|
+
module Concerns
|
|
129
|
+
# Mixin for Kubernetes resource operations
|
|
130
|
+
module KubernetesHelpers
|
|
131
|
+
def resource_exists?(type, name)
|
|
132
|
+
ctx.client.get_resource(type, name, ctx.namespace)
|
|
133
|
+
true
|
|
134
|
+
rescue K8s::Error::NotFound
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then use it:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class CreateAgent < Base
|
|
147
|
+
include Concerns::KubernetesHelpers
|
|
148
|
+
|
|
149
|
+
def execute
|
|
150
|
+
if resource_exists?('LanguageAgent', 'my-agent')
|
|
151
|
+
puts "Agent already exists!"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module Ux
|
|
5
|
+
module Concerns
|
|
6
|
+
# Mixin for consistent heading and banner formatting in UX flows
|
|
7
|
+
#
|
|
8
|
+
# Provides helpers for creating section headers, step indicators,
|
|
9
|
+
# welcome banners, and separator lines.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class MyFlow < Base
|
|
13
|
+
# include Concerns::Headings
|
|
14
|
+
#
|
|
15
|
+
# def execute
|
|
16
|
+
# heading('Welcome to My Flow', emoji: '🎉')
|
|
17
|
+
# step_heading(1, 5, 'First Step')
|
|
18
|
+
# # ... flow logic
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
module Headings
|
|
22
|
+
def title(text)
|
|
23
|
+
puts
|
|
24
|
+
puts pastel.bold.green("LANGUAGE OPERATOR v#{LanguageOperator::VERSION}")
|
|
25
|
+
puts pastel.dim("↪ #{text}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Display a prominent heading with optional emoji
|
|
29
|
+
#
|
|
30
|
+
# @param text [String] The heading text
|
|
31
|
+
# @param emoji [String, nil] Optional emoji to display
|
|
32
|
+
# @param width [Integer] Width of the banner (default: 50)
|
|
33
|
+
def heading(text, emoji: nil, width: 50)
|
|
34
|
+
border = "╭#{'─' * (width - 2)}╮"
|
|
35
|
+
bottom = "╰#{'─' * (width - 2)}╯"
|
|
36
|
+
|
|
37
|
+
display_text = emoji ? "#{text} #{emoji}" : text
|
|
38
|
+
padding = width - display_text.length - 4
|
|
39
|
+
|
|
40
|
+
puts
|
|
41
|
+
puts pastel.cyan(border)
|
|
42
|
+
puts "#{pastel.cyan('│')} #{display_text}#{' ' * padding}#{pastel.cyan('│')}"
|
|
43
|
+
puts pastel.cyan(bottom)
|
|
44
|
+
puts
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Display a step heading with step number
|
|
48
|
+
#
|
|
49
|
+
# @param current [Integer] Current step number
|
|
50
|
+
# @param total [Integer] Total number of steps
|
|
51
|
+
# @param title [String] Step title
|
|
52
|
+
# @param width [Integer] Width of the separator line (default: 50)
|
|
53
|
+
def step_heading(current, total, title, width: 50)
|
|
54
|
+
puts
|
|
55
|
+
puts '─' * width
|
|
56
|
+
puts pastel.cyan("Step #{current}/#{total}: #{title}")
|
|
57
|
+
puts '─' * width
|
|
58
|
+
puts
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Display a simple subheading
|
|
62
|
+
#
|
|
63
|
+
# @param text [String] The subheading text
|
|
64
|
+
def subheading(text)
|
|
65
|
+
puts
|
|
66
|
+
puts pastel.bold(text)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Display a separator line
|
|
70
|
+
#
|
|
71
|
+
# @param width [Integer] Width of the line (default: 50)
|
|
72
|
+
# @param char [String] Character to use for the line (default: '─')
|
|
73
|
+
def separator(width: 50, char: '─')
|
|
74
|
+
puts char * width
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Display a section header with description
|
|
78
|
+
#
|
|
79
|
+
# @param title [String] Section title
|
|
80
|
+
# @param description [String, nil] Optional description
|
|
81
|
+
def section(title, description: nil)
|
|
82
|
+
puts
|
|
83
|
+
puts pastel.cyan.bold(title)
|
|
84
|
+
puts pastel.dim(description) if description
|
|
85
|
+
puts
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|