Skip to content

Commit 9ed1585

Browse files
committed
Use CLI::Kit instead of Thor
1 parent 71acfa7 commit 9ed1585

21 files changed

+934
-451
lines changed

Gemfile.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ PATH
1111
raix (~> 1.0.2)
1212
ruby-graphviz (~> 1.2)
1313
sqlite3 (~> 2.6)
14-
thor (~> 1.3)
1514
zeitwerk (~> 2.6)
1615

1716
GEM

exe/roast

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ unshift_path.call("lib")
1212

1313
require "bundler/setup"
1414
require "roast"
15+
require "roast/entry_point"
1516

16-
puts "🔥🔥🔥 Everyone loves a good roast 🔥🔥🔥\n\n"
17-
Roast::CLI.start(ARGV)
17+
Roast::EntryPoint.call(ARGV)

lib/roast.rb

Lines changed: 0 additions & 320 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
require "raix/chat_completion"
3636
require "raix/function_dispatch"
3737
require "ruby-graphviz"
38-
require "thor"
3938
require "timeout"
4039

4140
# Autoloading setup
@@ -48,323 +47,4 @@
4847

4948
module Roast
5049
ROOT = File.expand_path("../..", __FILE__)
51-
52-
class CLI < Thor
53-
desc "execute [WORKFLOW_CONFIGURATION_FILE] [FILES...]", "Run a configured workflow"
54-
option :concise, type: :boolean, aliases: "-c", desc: "Optional flag for use in output templates"
55-
option :output, type: :string, aliases: "-o", desc: "Save results to a file"
56-
option :verbose, type: :boolean, aliases: "-v", desc: "Show output from all steps as they are executed"
57-
option :target, type: :string, aliases: "-t", desc: "Override target files. Can be file path, glob pattern, or $(shell command)"
58-
option :replay, type: :string, aliases: "-r", desc: "Resume workflow from a specific step. Format: step_name or session_timestamp:step_name"
59-
option :pause, type: :string, aliases: "-p", desc: "Pause workflow after a specific step. Format: step_name"
60-
option :file_storage, type: :boolean, aliases: "-f", desc: "Use filesystem storage for sessions instead of SQLite"
61-
option :executor, type: :string, default: "default", desc: "Set workflow executor - experimental syntax"
62-
63-
def execute(*paths)
64-
raise Thor::Error, "Workflow configuration file is required" if paths.empty?
65-
66-
workflow_path, *files = paths
67-
68-
if options[:executor] == "dsl"
69-
puts "⚠️ WARNING: This is an experimental syntax and may break at any time. Don't depend on it."
70-
Roast::DSL::Executor.from_file(workflow_path)
71-
else
72-
expanded_workflow_path = if workflow_path.include?("workflow.yml")
73-
File.expand_path(workflow_path)
74-
else
75-
File.expand_path("roast/#{workflow_path}/workflow.yml")
76-
end
77-
78-
raise Thor::Error, "Expected a Roast workflow configuration file, got directory: #{expanded_workflow_path}" if File.directory?(expanded_workflow_path)
79-
80-
Roast::Workflow::WorkflowRunner.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!
81-
end
82-
rescue => e
83-
if options[:verbose]
84-
raise e
85-
else
86-
$stderr.puts e.message
87-
end
88-
end
89-
90-
desc "resume WORKFLOW_FILE", "Resume a paused workflow with an event"
91-
option :event, type: :string, aliases: "-e", required: true, desc: "Event name to trigger"
92-
option :session_id, type: :string, aliases: "-s", desc: "Specific session ID to resume (defaults to most recent)"
93-
option :event_data, type: :string, desc: "JSON data to pass with the event"
94-
def resume(workflow_path)
95-
expanded_workflow_path = if workflow_path.include?("workflow.yml")
96-
File.expand_path(workflow_path)
97-
else
98-
File.expand_path("roast/#{workflow_path}/workflow.yml")
99-
end
100-
101-
unless File.exist?(expanded_workflow_path)
102-
raise Thor::Error, "Workflow file not found: #{expanded_workflow_path}"
103-
end
104-
105-
# Store the event in the session
106-
repository = Workflow::StateRepositoryFactory.create
107-
108-
unless repository.respond_to?(:add_event)
109-
raise Thor::Error, "Event resumption requires SQLite storage. Set ROAST_STATE_STORAGE=sqlite"
110-
end
111-
112-
# Parse event data if provided
113-
event_data = options[:event_data] ? JSON.parse(options[:event_data]) : nil
114-
115-
# Add the event to the session
116-
session_id = options[:session_id]
117-
repository.add_event(expanded_workflow_path, session_id, options[:event], event_data)
118-
119-
# Resume workflow execution from the wait state
120-
resume_options = options.transform_keys(&:to_sym).merge(
121-
resume_from_event: options[:event],
122-
session_id: session_id,
123-
)
124-
125-
Roast::Workflow::WorkflowRunner.new(expanded_workflow_path, [], resume_options).begin!
126-
end
127-
128-
desc "version", "Display the current version of Roast"
129-
def version
130-
puts "Roast version #{Roast::VERSION}"
131-
end
132-
133-
desc "init", "Initialize a new Roast workflow from an example"
134-
option :example, type: :string, aliases: "-e", desc: "Name of the example to use directly (skips picker)"
135-
def init
136-
if options[:example]
137-
copy_example(options[:example])
138-
else
139-
show_example_picker
140-
end
141-
end
142-
143-
desc "list", "List workflows visible to Roast and their source"
144-
def list
145-
roast_dir = File.join(Dir.pwd, "roast")
146-
147-
unless File.directory?(roast_dir)
148-
raise Thor::Error, "No roast/ directory found in current path"
149-
end
150-
151-
workflow_files = Dir.glob(File.join(roast_dir, "**/workflow.yml")).sort
152-
153-
if workflow_files.empty?
154-
raise Thor::Error, "No workflow.yml files found in roast/ directory"
155-
end
156-
157-
puts "Available workflows:"
158-
puts
159-
160-
workflow_files.each do |file|
161-
workflow_name = File.dirname(file.sub("#{roast_dir}/", ""))
162-
puts " #{workflow_name} (from project)"
163-
end
164-
165-
puts
166-
puts "Run a workflow with: roast execute <workflow_name>"
167-
end
168-
169-
desc "validate [WORKFLOW_CONFIGURATION_FILE]", "Validate a workflow configuration"
170-
option :strict, type: :boolean, aliases: "-s", desc: "Treat warnings as errors"
171-
def validate(workflow_path = nil)
172-
validation_command = Roast::Workflow::ValidationCommand.new(options)
173-
validation_command.execute(workflow_path)
174-
end
175-
176-
desc "sessions", "List stored workflow sessions"
177-
option :status, type: :string, aliases: "-s", desc: "Filter by status (running, waiting, completed, failed)"
178-
option :workflow, type: :string, aliases: "-w", desc: "Filter by workflow name"
179-
option :older_than, type: :string, desc: "Show sessions older than specified time (e.g., '7d', '1h')"
180-
option :cleanup, type: :boolean, desc: "Clean up old sessions"
181-
def sessions
182-
repository = Workflow::StateRepositoryFactory.create
183-
184-
unless repository.respond_to?(:list_sessions)
185-
raise Thor::Error, "Session listing is only available with SQLite storage. Set ROAST_STATE_STORAGE=sqlite"
186-
end
187-
188-
if options[:cleanup] && options[:older_than]
189-
count = repository.cleanup_old_sessions(options[:older_than])
190-
puts "Cleaned up #{count} old sessions"
191-
return
192-
end
193-
194-
sessions = repository.list_sessions(
195-
status: options[:status],
196-
workflow_name: options[:workflow],
197-
older_than: options[:older_than],
198-
)
199-
200-
if sessions.empty?
201-
puts "No sessions found"
202-
return
203-
end
204-
205-
puts "Found #{sessions.length} session(s):"
206-
puts
207-
208-
sessions.each do |session|
209-
id, workflow_name, _, status, current_step, created_at, updated_at = session
210-
211-
puts "Session: #{id}"
212-
puts " Workflow: #{workflow_name}"
213-
puts " Status: #{status}"
214-
puts " Current step: #{current_step || "N/A"}"
215-
puts " Created: #{created_at}"
216-
puts " Updated: #{updated_at}"
217-
puts
218-
end
219-
end
220-
221-
desc "session SESSION_ID", "Show details for a specific session"
222-
def session(session_id)
223-
repository = Workflow::StateRepositoryFactory.create
224-
225-
unless repository.respond_to?(:get_session_details)
226-
raise Thor::Error, "Session details are only available with SQLite storage. Set ROAST_STATE_STORAGE=sqlite"
227-
end
228-
229-
details = repository.get_session_details(session_id)
230-
231-
unless details
232-
raise Thor::Error, "Session not found: #{session_id}"
233-
end
234-
235-
session = details[:session]
236-
states = details[:states]
237-
events = details[:events]
238-
239-
puts "Session: #{session[0]}"
240-
puts "Workflow: #{session[1]}"
241-
puts "Path: #{session[2]}"
242-
puts "Status: #{session[3]}"
243-
puts "Created: #{session[6]}"
244-
puts "Updated: #{session[7]}"
245-
246-
if session[5]
247-
puts
248-
puts "Final output:"
249-
puts session[5]
250-
end
251-
252-
if states && !states.empty?
253-
puts
254-
puts "Steps executed:"
255-
states.each do |step_index, step_name, created_at|
256-
puts " #{step_index}: #{step_name} (#{created_at})"
257-
end
258-
end
259-
260-
if events && !events.empty?
261-
puts
262-
puts "Events:"
263-
events.each do |event_name, event_data, received_at|
264-
puts " #{event_name} at #{received_at}"
265-
puts " Data: #{event_data}" if event_data
266-
end
267-
end
268-
end
269-
270-
desc "diagram WORKFLOW_FILE", "Generate a visual diagram of a workflow"
271-
option :output, type: :string, aliases: "-o", desc: "Output file path (defaults to workflow_name_diagram.png)"
272-
def diagram(workflow_file)
273-
unless File.exist?(workflow_file)
274-
raise Thor::Error, "Workflow file not found: #{workflow_file}"
275-
end
276-
277-
workflow = Workflow::Configuration.new(workflow_file)
278-
generator = WorkflowDiagramGenerator.new(workflow, workflow_file)
279-
output_path = generator.generate(options[:output])
280-
281-
puts ::CLI::UI.fmt("{{success:✓}} Diagram generated: #{output_path}")
282-
rescue StandardError => e
283-
raise Thor::Error, "Error generating diagram: #{e.message}"
284-
end
285-
286-
private
287-
288-
def show_example_picker
289-
examples = available_examples
290-
291-
if examples.empty?
292-
puts "No examples found!"
293-
return
294-
end
295-
296-
puts "Select an option:"
297-
choices = ["Pick from examples", "New from prompt (beta)"]
298-
299-
selected = run_picker(choices, "Select initialization method:")
300-
301-
case selected
302-
when "Pick from examples"
303-
example_choice = run_picker(examples, "Select an example:")
304-
copy_example(example_choice) if example_choice
305-
when "New from prompt (beta)"
306-
create_from_prompt
307-
end
308-
end
309-
310-
def available_examples
311-
examples_dir = File.join(Roast::ROOT, "examples")
312-
return [] unless File.directory?(examples_dir)
313-
314-
Dir.entries(examples_dir)
315-
.select { |entry| File.directory?(File.join(examples_dir, entry)) && entry != "." && entry != ".." }
316-
.sort
317-
end
318-
319-
def run_picker(options, prompt)
320-
return if options.empty?
321-
322-
::CLI::UI::Prompt.ask(prompt) do |handler|
323-
options.each { |option| handler.option(option) { |selection| selection } }
324-
end
325-
end
326-
327-
def copy_example(example_name)
328-
examples_dir = File.join(Roast::ROOT, "examples")
329-
source_path = File.join(examples_dir, example_name)
330-
target_path = File.join(Dir.pwd, example_name)
331-
332-
unless File.directory?(source_path)
333-
puts "Example '#{example_name}' not found!"
334-
return
335-
end
336-
337-
if File.exist?(target_path)
338-
puts "Directory '#{example_name}' already exists in current directory!"
339-
return
340-
end
341-
342-
FileUtils.cp_r(source_path, target_path)
343-
puts "Successfully copied example '#{example_name}' to current directory."
344-
end
345-
346-
def create_from_prompt
347-
puts("Create a new workflow from a description")
348-
puts
349-
350-
# Execute the workflow generator
351-
generator_path = File.join(Roast::ROOT, "examples", "workflow_generator", "workflow.yml")
352-
353-
begin
354-
# Execute the workflow generator (it will handle user input)
355-
Roast::Workflow::WorkflowRunner.new(generator_path, [], {}).begin!
356-
357-
puts
358-
puts("Workflow generation complete!")
359-
rescue => e
360-
puts("Error generating workflow: #{e.message}")
361-
end
362-
end
363-
364-
class << self
365-
def exit_on_failure?
366-
true
367-
end
368-
end
369-
end
37050
end

0 commit comments

Comments
 (0)