meta_workflows 0.8.24 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class WorkflowImportService < ApplicationService
5
+ attr_reader :import_results
6
+
7
+ def initialize
8
+ super
9
+ @import_results = []
10
+ end
11
+
12
+ def import_from_file(file)
13
+ return failure('No file provided') if file.blank?
14
+
15
+ begin
16
+ yaml_content = YAML.safe_load(file.read)
17
+ workflow_name = extract_workflow_name(file.original_filename)
18
+
19
+ result = save_workflow(workflow_name, yaml_content)
20
+ @import_results << result
21
+
22
+ success(result)
23
+ rescue Psych::SyntaxError => e
24
+ failure("Invalid YAML format: #{e.message}")
25
+ rescue StandardError => e
26
+ failure("Error processing file: #{e.message}")
27
+ end
28
+ end
29
+
30
+ def import_from_yaml_text(workflow_name, yaml_text)
31
+ return failure('Workflow name cannot be blank') if workflow_name.blank?
32
+ return failure('YAML content cannot be blank') if yaml_text.blank?
33
+
34
+ begin
35
+ yaml_content = YAML.safe_load(yaml_text)
36
+ result = save_workflow(workflow_name.underscore, yaml_content)
37
+ @import_results << result
38
+
39
+ success(result)
40
+ rescue Psych::SyntaxError => e
41
+ failure("Invalid YAML format: #{e.message}")
42
+ rescue StandardError => e
43
+ failure("Error processing YAML: #{e.message}")
44
+ end
45
+ end
46
+
47
+ def import_from_directory(directory_path = nil)
48
+ directory_path ||= Rails.root.join('workflows')
49
+
50
+ return failure("Directory not found: #{directory_path}") unless Dir.exist?(directory_path)
51
+
52
+ yaml_files = Dir.glob(File.join(directory_path, '*.yml'))
53
+ return failure('No YAML files found in directory') if yaml_files.empty?
54
+
55
+ successful_imports = 0
56
+ failed_imports = 0
57
+
58
+ yaml_files.each do |file_path|
59
+ result = import_single_file(file_path)
60
+ @import_results << result
61
+
62
+ if result[:success]
63
+ successful_imports += 1
64
+ else
65
+ failed_imports += 1
66
+ end
67
+ end
68
+
69
+ message = "Import completed: #{successful_imports} successful, #{failed_imports} failed"
70
+ success({ message: message, successful: successful_imports, failed: failed_imports })
71
+ end
72
+
73
+ private
74
+
75
+ def extract_workflow_name(filename)
76
+ File.basename(filename, '.yml').underscore
77
+ end
78
+
79
+ def import_single_file(file_path)
80
+ file_name = File.basename(file_path, '.yml')
81
+ workflow_name = file_name.underscore
82
+
83
+ begin
84
+ yaml_content = YAML.load_file(file_path)
85
+ save_workflow(workflow_name, yaml_content)
86
+ rescue StandardError => e
87
+ {
88
+ success: false,
89
+ workflow_name: workflow_name,
90
+ error: e.message,
91
+ file_path: file_path
92
+ }
93
+ end
94
+ end
95
+
96
+ def save_workflow(workflow_name, yaml_content)
97
+ workflow = MetaWorkflows::Workflow.find_or_initialize_by(name: workflow_name)
98
+ workflow.recipe = yaml_content
99
+
100
+ if workflow.save
101
+ {
102
+ success: true,
103
+ workflow_name: workflow.name,
104
+ action: workflow.previously_new_record? ? 'created' : 'updated',
105
+ workflow: workflow
106
+ }
107
+ else
108
+ {
109
+ success: false,
110
+ workflow_name: workflow_name,
111
+ error: workflow.errors.full_messages.join(', ')
112
+ }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,16 +1,70 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Meta Workflows</title>
4
+ <title>MetaWorkflows Debug Tool</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
8
- <%= stylesheet_link_tag "meta_workflows/application", media: "all" %>
9
- <%= javascript_include_tag "meta_workflows/application" %>
8
+ <%= yield :head %>
9
+
10
+ <!-- Tailwind CSS -->
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+
14
+ <!-- JSON Viewer Web Component -->
15
+ <script src="https://unpkg.com/@alenaksu/json-viewer@2.1.0/dist/json-viewer.bundle.js"></script>
10
16
  </head>
11
17
 
12
- <body>
13
- <%= render 'layouts/meta_workflows/flash' %>
14
- <%= yield %>
18
+ <body class="bg-gray-50 min-h-screen">
19
+ <!-- Flash Messages -->
20
+ <% flash.each do |type, message| %>
21
+ <% icon, color_classes = case type.to_sym
22
+ when :notice
23
+ ["fa-circle-check", "bg-green-50 border-green-200 text-green-800"]
24
+ when :alert
25
+ ["fa-triangle-exclamation", "bg-red-50 border-red-200 text-red-800"]
26
+ else
27
+ ["fa-info-circle", "bg-blue-50 border-blue-200 text-blue-800"]
28
+ end %>
29
+ <div class="flex items-center gap-3 border-l-4 p-4 mb-4 shadow-sm rounded-md <%= color_classes %>" role="alert">
30
+ <i class="fa-solid <%= icon %> text-xl"></i>
31
+ <div class="flex-1">
32
+ <%= message %>
33
+ </div>
34
+ <button type="button" class="ml-4 text-gray-400 hover:text-gray-600 focus:outline-none" onclick="this.parentElement.style.display='none'" aria-label="Dismiss">
35
+ <i class="fa-solid fa-xmark"></i>
36
+ </button>
37
+ </div>
38
+ <% end %>
39
+
40
+ <!-- Header Navigation -->
41
+ <nav class="bg-white shadow-sm border-b border-gray-200">
42
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
43
+ <div class="flex justify-between items-center h-16">
44
+ <!-- Logo/Title -->
45
+ <div class="flex items-center">
46
+ <h1 class="text-xl font-semibold text-gray-900">
47
+ MetaWorkflows Debug Tool
48
+ </h1>
49
+ </div>
50
+
51
+ <!-- Navigation Links -->
52
+ <div class="flex space-x-8">
53
+ <%= link_to "Workflows", workflows_path,
54
+ class: "#{current_page?(workflows_path) || params[:action] == 'workflows' || params[:action] == 'show_workflow' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'} px-3 py-2 text-sm font-medium" %>
55
+ <%= link_to "Executions", executions_path,
56
+ class: "#{current_page?(executions_path) || params[:action] == 'executions' || params[:action] == 'show_execution' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'} px-3 py-2 text-sm font-medium" %>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </nav>
61
+
62
+ <!-- Main Content -->
63
+ <main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
64
+ <%= yield %>
65
+ </main>
66
+
67
+ <!-- Optional JavaScript -->
68
+ <%= yield :javascripts %>
15
69
  </body>
16
70
  </html>
@@ -1,5 +1,5 @@
1
1
  <%# Lexi Chat Alpha Tray (Center Display) %>
2
- <div class="relative flex flex-col h-[95vh] bg-white px-4">
2
+ <div class="relative flex flex-col lexi-chat-container bg-white px-4">
3
3
  <%# Header with Lexi logo and action icons - spans full width %>
4
4
  <div class="flex-shrink-0 flex items-center justify-between pb-2 border-b border-gray-200">
5
5
  <div class="flex items-center space-x-2">
@@ -19,7 +19,7 @@
19
19
  <div class="flex-1 flex min-h-0">
20
20
  <!-- Lexi auto-width side column -->
21
21
  <div class="flex-shrink-0 flex flex-col justify-end pb-4">
22
- <%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "h-[375px] w-auto") %>
22
+ <%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "lexi-avatar-large w-auto") %>
23
23
  </div>
24
24
 
25
25
  <!-- Main content area (chat + input) -->
@@ -1,5 +1,5 @@
1
1
  <%# Lexi Chat Tray %>
2
- <div class="relative flex flex-col h-[100vh] bg-white px-4">
2
+ <div class="relative flex flex-col lexi-chat-container-full bg-white px-4">
3
3
  <%# Header with Lexi logo and action icons %>
4
4
  <div class="flex-shrink-0 flex items-center justify-between pb-2 border-b border-gray-200">
5
5
  <div class="flex items-center space-x-2">
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
 
23
23
  <%# Lexi avatar and input area pinned to bottom %>
24
- <div class="relative bg-white h-[335px]">
24
+ <div class="relative bg-white lexi-input-container">
25
25
  <!-- Bottom section with recording notice and input area -->
26
26
  <div class="absolute bottom-15 left-0 right-0 flex flex-col space-y-2 z-10">
27
27
  <!-- Recording notice -->
@@ -35,6 +35,6 @@
35
35
  </div>
36
36
 
37
37
  <!-- Lexi image positioned absolutely to the left -->
38
- <%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "absolute bottom-10 left-0 h-[300px] w-auto pointer-events-none") %>
38
+ <%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "absolute bottom-10 left-0 lexi-avatar-medium w-auto pointer-events-none") %>
39
39
  </div>
40
40
  </div>
@@ -5,10 +5,10 @@
5
5
  <fieldset>
6
6
  <div class="flex flex-col gap-1">
7
7
  <!-- Input Container -->
8
- <div class="flex flex-col max-h-[200px] border border-gray-300 rounded-lg bg-white/80 p-2">
8
+ <div class="flex flex-col lexi-input-max-height border border-gray-300 rounded-lg bg-white/80 p-2">
9
9
  <!-- Input area with icons -->
10
10
  <div class="flex-1 relative">
11
- <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "w-full min-h-[35px] border-0 resize-none focus:outline-none focus:ring-0 bg-transparent text-base overflow-y-auto" %>
11
+ <%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "w-full lexi-textarea-min-height border-0 resize-none focus:outline-none focus:ring-0 bg-transparent text-base overflow-y-auto" %>
12
12
  </div>
13
13
 
14
14
  <div class="flex justify-between">
@@ -11,7 +11,7 @@
11
11
  <% if message.role == 'user' %>
12
12
  <!-- User message bubble (right-aligned, amber background) -->
13
13
  <div class="flex justify-end">
14
- <div class="max-w-[80%] bg-amber-100 rounded-xl px-6 py-3">
14
+ <div class="lexi-message-max-width bg-amber-100 rounded-xl px-6 py-3">
15
15
  <div class="text-sm">
16
16
  <%= simple_format(message.content) %>
17
17
  </div>
@@ -22,7 +22,7 @@
22
22
  <!-- Lexi message bubble (left-aligned) -->
23
23
  <div class="flex justify-start">
24
24
  <!-- Message Bubble -->
25
- <div class="max-w-[80%] bg-slate-100 rounded-xl px-6 py-3">
25
+ <div class="lexi-message-max-width bg-slate-100 rounded-xl px-6 py-3">
26
26
  <div class="prose prose-sm max-w-none">
27
27
  <%= markdown(message.content) %>
28
28
  </div>
@@ -35,7 +35,7 @@
35
35
  <% if is_streaming %>
36
36
  <div class="flex justify-start">
37
37
  <!-- Streaming Message Bubble -->
38
- <div class="max-w-[80%] bg-slate-100 rounded-xl px-6 py-3">
38
+ <div class="lexi-message-max-width bg-slate-100 rounded-xl px-6 py-3">
39
39
  <div class="prose prose-sm max-w-none">
40
40
  <%= markdown(full_response) %>
41
41
  </div>
@@ -9,16 +9,29 @@
9
9
  </p>
10
10
  </div>
11
11
 
12
- <!-- Search -->
13
- <div class="w-64">
14
- <%= form_with url: workflows_path, method: :get, local: true, class: "flex" do |form| %>
15
- <%= form.text_field :search,
16
- placeholder: "Search workflows...",
17
- value: params[:search],
18
- class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
19
- <%= form.submit "Search",
20
- class: "ml-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
21
- <% end %>
12
+ <div class="flex items-center space-x-3">
13
+ <!-- Import Actions -->
14
+ <div class="flex items-center space-x-2">
15
+ <%= link_to "Import Workflow", new_workflow_import_path,
16
+ class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
17
+
18
+ <%= button_to "Reimport All", bulk_import_workflow_imports_path,
19
+ method: :post,
20
+ class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
21
+ data: { confirm: "This will reimport all workflows from the workflows directory. Continue?" } %>
22
+ </div>
23
+
24
+ <!-- Search -->
25
+ <div class="w-64">
26
+ <%= form_with url: workflows_path, method: :get, local: true, class: "flex" do |form| %>
27
+ <%= form.text_field :search,
28
+ placeholder: "Search workflows...",
29
+ value: params[:search],
30
+ class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
31
+ <%= form.submit "Search",
32
+ class: "ml-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
33
+ <% end %>
34
+ </div>
22
35
  </div>
23
36
  </div>
24
37
  </div>
@@ -0,0 +1,171 @@
1
+ <div class="bg-white shadow rounded-lg">
2
+ <!-- Header -->
3
+ <div class="px-6 py-4 border-b border-gray-200">
4
+ <div class="flex items-center justify-between">
5
+ <div>
6
+ <h2 class="text-lg font-medium text-gray-900">Import Workflow</h2>
7
+ <p class="mt-1 text-sm text-gray-500">
8
+ Import a new workflow or update an existing one
9
+ </p>
10
+ </div>
11
+
12
+ <div>
13
+ <%= link_to "Back to Workflows", workflows_path,
14
+ class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
15
+ </div>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Import Form -->
20
+ <div class="px-6 py-6">
21
+ <%= form_with url: workflow_imports_path, method: :post, local: true, multipart: true, class: "space-y-6" do |form| %>
22
+
23
+ <!-- Import Type Selection -->
24
+ <div>
25
+ <label class="text-base font-medium text-gray-900">Import Method</label>
26
+ <p class="text-sm leading-5 text-gray-500">Choose how you want to import the workflow</p>
27
+ <fieldset class="mt-4">
28
+ <legend class="sr-only">Import method</legend>
29
+ <div class="space-y-4">
30
+ <div class="flex items-center">
31
+ <%= form.radio_button :import_type, "file", id: "import_type_file",
32
+ class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300",
33
+ checked: true %>
34
+ <label for="import_type_file" class="ml-3 block text-sm font-medium text-gray-700">
35
+ Upload YAML File
36
+ </label>
37
+ </div>
38
+ <div class="flex items-center">
39
+ <%= form.radio_button :import_type, "text", id: "import_type_text",
40
+ class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
41
+ <label for="import_type_text" class="ml-3 block text-sm font-medium text-gray-700">
42
+ Paste YAML Content
43
+ </label>
44
+ </div>
45
+ <div class="flex items-center">
46
+ <%= form.radio_button :import_type, "directory", id: "import_type_directory",
47
+ class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
48
+ <label for="import_type_directory" class="ml-3 block text-sm font-medium text-gray-700">
49
+ Import from workflows directory
50
+ </label>
51
+ </div>
52
+ </div>
53
+ </fieldset>
54
+ </div>
55
+
56
+ <!-- File Upload Section -->
57
+ <div id="file_upload_section" class="space-y-4">
58
+ <div>
59
+ <label for="workflow_file" class="block text-sm font-medium text-gray-700">
60
+ Upload Workflow File
61
+ </label>
62
+ <div class="mt-1">
63
+ <%= form.file_field :workflow_file,
64
+ accept: ".yml,.yaml",
65
+ class: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" %>
66
+ </div>
67
+ <p class="mt-2 text-sm text-gray-500">
68
+ Upload a YAML file containing the workflow definition
69
+ </p>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Text Input Section -->
74
+ <div id="text_input_section" class="space-y-4" style="display: none;">
75
+ <div>
76
+ <label for="workflow_name" class="block text-sm font-medium text-gray-700">
77
+ Workflow Name
78
+ </label>
79
+ <div class="mt-1">
80
+ <%= form.text_field :workflow_name,
81
+ placeholder: "e.g., post_creation",
82
+ class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
83
+ </div>
84
+ <p class="mt-2 text-sm text-gray-500">
85
+ Enter a unique name for this workflow (will be converted to snake_case)
86
+ </p>
87
+ </div>
88
+
89
+ <div>
90
+ <label for="yaml_content" class="block text-sm font-medium text-gray-700">
91
+ YAML Content
92
+ </label>
93
+ <div class="mt-1">
94
+ <%= form.text_area :yaml_content,
95
+ rows: 20,
96
+ placeholder: "steps:\n - name: \"example_step\"\n action: \"human\"\n prompt_id: 'example_prompt'\n ...",
97
+ class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono" %>
98
+ </div>
99
+ <p class="mt-2 text-sm text-gray-500">
100
+ Paste the YAML content for the workflow
101
+ </p>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Directory Import Section -->
106
+ <div id="directory_import_section" class="space-y-4" style="display: none;">
107
+ <div class="rounded-md bg-blue-50 p-4">
108
+ <div class="flex">
109
+ <div class="flex-shrink-0">
110
+ <svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
111
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
112
+ </svg>
113
+ </div>
114
+ <div class="ml-3 flex-1 md:flex md:justify-between">
115
+ <p class="text-sm text-blue-700">
116
+ This will import all YAML files from the <code>workflows</code> directory.
117
+ Existing workflows will be updated with new content.
118
+ </p>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Submit Button -->
125
+ <div class="flex justify-end">
126
+ <%= form.submit "Import Workflow",
127
+ class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
128
+ </div>
129
+ <% end %>
130
+ </div>
131
+ </div>
132
+
133
+ <script>
134
+ // JavaScript to toggle form sections based on selected import type
135
+ document.addEventListener('DOMContentLoaded', function() {
136
+ const importTypeRadios = document.querySelectorAll('input[name="import_type"]');
137
+ const fileSection = document.getElementById('file_upload_section');
138
+ const textSection = document.getElementById('text_input_section');
139
+ const directorySection = document.getElementById('directory_import_section');
140
+
141
+ function toggleSections() {
142
+ const selectedType = document.querySelector('input[name="import_type"]:checked').value;
143
+
144
+ // Hide all sections
145
+ fileSection.style.display = 'none';
146
+ textSection.style.display = 'none';
147
+ directorySection.style.display = 'none';
148
+
149
+ // Show relevant section
150
+ switch (selectedType) {
151
+ case 'file':
152
+ fileSection.style.display = 'block';
153
+ break;
154
+ case 'text':
155
+ textSection.style.display = 'block';
156
+ break;
157
+ case 'directory':
158
+ directorySection.style.display = 'block';
159
+ break;
160
+ }
161
+ }
162
+
163
+ // Add event listeners
164
+ importTypeRadios.forEach(radio => {
165
+ radio.addEventListener('change', toggleSections);
166
+ });
167
+
168
+ // Initialize on page load
169
+ toggleSections();
170
+ });
171
+ </script>
data/config/routes.rb CHANGED
@@ -3,6 +3,13 @@
3
3
  MetaWorkflows::Engine.routes.draw do
4
4
  resources :humans, only: [:update]
5
5
 
6
+ # Workflow imports - RESTful routes
7
+ resources :workflow_imports, only: %i[new create], path: 'workflow_imports' do
8
+ collection do
9
+ post :bulk, as: :bulk_import
10
+ end
11
+ end
12
+
6
13
  # Debug interface routes
7
14
  root 'debug#index'
8
15
  get 'workflows', to: 'debug#workflows'
@@ -2,8 +2,8 @@
2
2
 
3
3
  module MetaWorkflows
4
4
  MAJOR = 0
5
- MINOR = 8
6
- PATCH = 24 # this is automatically incremented by the build process
5
+ MINOR = 9
6
+ PATCH = 1 # this is automatically incremented by the build process
7
7
 
8
8
  VERSION = "#{MetaWorkflows::MAJOR}.#{MetaWorkflows::MINOR}.#{MetaWorkflows::PATCH}".freeze
9
9
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../meta_workflows/asset_installer'
4
+
5
+ module MetaWorkflows
6
+ # Service class to handle asset installation
7
+ class AssetInstallerService
8
+ def install
9
+ puts 'Installing meta_workflows assets...'
10
+
11
+ installer = MetaWorkflows::AssetInstaller.new
12
+
13
+ # Pre-installation validation
14
+ unless installer.valid_environment?
15
+ puts 'Installation aborted due to validation errors.'
16
+ return
17
+ end
18
+
19
+ begin
20
+ process_asset_installation(installer)
21
+ rescue MetaWorkflows::AssetInstaller::InstallationError => e
22
+ handle_installation_error(e)
23
+ rescue StandardError => e
24
+ handle_unexpected_error(e)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def process_asset_installation(installer)
31
+ assets = discover_and_validate_assets(installer)
32
+ return if assets.empty?
33
+
34
+ result = installer.install_assets(assets)
35
+ display_installation_results(installer, assets, result)
36
+ end
37
+
38
+ def discover_and_validate_assets(installer)
39
+ assets = installer.discover_assets
40
+
41
+ if assets.empty?
42
+ puts 'No assets found to install.'
43
+ return []
44
+ end
45
+
46
+ puts "Found #{assets.count} asset(s) to install:"
47
+ assets.each { |asset| puts " - #{asset[:relative_path]}" }
48
+ puts
49
+
50
+ assets
51
+ end
52
+
53
+ def display_installation_results(installer, assets, result)
54
+ puts
55
+ if result[:success]
56
+ display_successful_installation(installer, assets, result)
57
+ else
58
+ display_partial_installation(installer, assets, result)
59
+ end
60
+ end
61
+
62
+ def display_successful_installation(installer, assets, result)
63
+ puts 'Installation completed successfully!'
64
+ puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped."
65
+
66
+ return unless result[:copied].positive?
67
+
68
+ puts
69
+ installer.display_integration_instructions(assets, result)
70
+ end
71
+
72
+ def display_partial_installation(installer, assets, result)
73
+ puts 'Installation completed with errors.'
74
+ puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped, " \
75
+ "#{result[:failed]} file(s) failed."
76
+ puts 'Some files may have been partially installed. Please review the output above.'
77
+
78
+ return unless result[:copied].positive?
79
+
80
+ puts
81
+ puts 'For successfully copied files, see integration instructions below:'
82
+ installer.display_integration_instructions(assets, result)
83
+ end
84
+
85
+ def handle_installation_error(error)
86
+ puts "Installation failed: #{error.message}"
87
+ puts 'Installation aborted.'
88
+ end
89
+
90
+ def handle_unexpected_error(error)
91
+ puts "Unexpected error during asset installation: #{error.message}"
92
+ puts 'Installation aborted.'
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../services/meta_workflows/asset_installer_service'
4
+
5
+ namespace :meta_workflows do
6
+ namespace :install do
7
+ desc 'Install meta_workflows assets (JavaScript controllers and stylesheets) into host application'
8
+ task assets: :environment do
9
+ MetaWorkflows::AssetInstallerService.new.install
10
+ end
11
+ end
12
+ end