meta_workflows 0.8.24 → 0.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba1784b378c470dc10b2f863925448fa1990867e7c922e3fcdac70cf5a7225a9
4
- data.tar.gz: d79449ffdd2dd4fa309d4cda8b510f49f10a697729e0491c64097aa087af672d
3
+ metadata.gz: 3d50e1943ccd15113eba9afb7c8cbdc01d438c6d3d49a88ceeff0c99585f6a35
4
+ data.tar.gz: 903ed9aabed86d16a846caf25a96497ebd31ea3c6f1872a4c1ce4a55dbdfaeb8
5
5
  SHA512:
6
- metadata.gz: 43105946578bb6897d6c07762cf4af0ec9456c2d9ac839645c39592fb814d2b7ca44f913b60038c0d43e359d8f5acb876dbaa87f581890d9d0b636d0aec51d05
7
- data.tar.gz: 5bc47a299ef97e9a73a3425fcbde25a6c079a026803b92f706b6bbafe7a8e066cc4c29c8034f6c6af4b40990fa2908019a07ef82ac596c6c0dffc525cc70cc5e
6
+ metadata.gz: 5641f236a0e35b67e90a4a557015d6135f2c03c8061ea5bfd2c4adc5c5ce20b9e90e3a357e5764479f71f3db3aaf81acba5093c6aae4102a7d49881584b0bce7
7
+ data.tar.gz: f591fffcd47a4f0ae49746a3d0aaa2d4c495eb39c6a171bedc049906617deba5493bf2e984ce1b1b4c50c6ad73d4811a774eb51a8b1894295e5a0c6278271ff7
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class BaseDebugController < MetaWorkflows::ApplicationController
5
+ include MetaWorkflows::DebugHelper
6
+
7
+ layout 'meta_workflows/debug'
8
+
9
+ # Apply authentication only in production environment
10
+ before_action :authenticate_with_basic_auth, if: :production_environment?
11
+
12
+ # Skip CSRF protection for this debug interface
13
+ skip_before_action :verify_authenticity_token
14
+
15
+ protected
16
+
17
+ def authenticate_with_basic_auth
18
+ return unless production_environment?
19
+ return unless authentication_required?
20
+
21
+ authenticate_or_request_with_http_basic do |username, password|
22
+ username == ENV.fetch('SIDEKIQ_USER', nil) &&
23
+ password == ENV.fetch('SIDEKIQ_PASSWORD', nil)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def production_environment?
30
+ Rails.env.production?
31
+ end
32
+
33
+ def authentication_required?
34
+ ENV['SIDEKIQ_USER'].present? && ENV['SIDEKIQ_PASSWORD'].present?
35
+ end
36
+
37
+ def paginate_collection(collection)
38
+ # Use Kaminari if available, otherwise limit to 25 records
39
+ if defined?(Kaminari) && collection.respond_to?(:page)
40
+ collection.page(params[:page]).per(25)
41
+ else
42
+ collection.limit(25).offset(((params[:page]&.to_i || 1) - 1) * 25)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,17 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MetaWorkflows
4
- class DebugController < MetaWorkflows::ApplicationController
5
- include MetaWorkflows::DebugHelper
6
-
7
- layout 'meta_workflows/debug'
8
-
9
- # Apply authentication only in production environment
10
- before_action :authenticate_with_basic_auth, if: :production_environment?
11
-
12
- # Skip CSRF protection for this debug interface
13
- skip_before_action :verify_authenticity_token
14
-
4
+ class DebugController < MetaWorkflows::BaseDebugController
15
5
  def index
16
6
  redirect_to workflows_path
17
7
  end
@@ -66,36 +56,5 @@ module MetaWorkflows
66
56
  format.html { redirect_to execution_path(@execution), alert: t('meta_workflows.export_only_json') }
67
57
  end
68
58
  end
69
-
70
- protected
71
-
72
- def authenticate_with_basic_auth
73
- return unless production_environment?
74
- return unless authentication_required?
75
-
76
- authenticate_or_request_with_http_basic do |username, password|
77
- username == ENV.fetch('SIDEKIQ_USER', nil) &&
78
- password == ENV.fetch('SIDEKIQ_PASSWORD', nil)
79
- end
80
- end
81
-
82
- private
83
-
84
- def production_environment?
85
- Rails.env.production?
86
- end
87
-
88
- def authentication_required?
89
- ENV['SIDEKIQ_USER'].present? && ENV['SIDEKIQ_PASSWORD'].present?
90
- end
91
-
92
- def paginate_collection(collection)
93
- # Use Kaminari if available, otherwise limit to 25 records
94
- if defined?(Kaminari) && collection.respond_to?(:page)
95
- collection.page(params[:page]).per(25)
96
- else
97
- collection.limit(25).offset(((params[:page]&.to_i || 1) - 1) * 25)
98
- end
99
- end
100
59
  end
101
60
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class WorkflowImportsController < MetaWorkflows::BaseDebugController
5
+ def new
6
+ # Show the import form
7
+ end
8
+
9
+ def create
10
+ result = perform_import
11
+ return unless result
12
+
13
+ handle_import_result(result)
14
+ end
15
+
16
+ def bulk
17
+ import_service = MetaWorkflows::WorkflowImportService.new
18
+ result = import_service.import_from_directory
19
+
20
+ if result.success?
21
+ data = result.data
22
+ flash[:notice] = data[:message]
23
+ else
24
+ flash[:alert] = result.error
25
+ end
26
+
27
+ redirect_to workflows_path
28
+ end
29
+
30
+ private
31
+
32
+ def perform_import
33
+ import_service = MetaWorkflows::WorkflowImportService.new
34
+
35
+ case params[:import_type]
36
+ when 'file'
37
+ handle_file_import(import_service)
38
+ when 'text'
39
+ handle_text_import(import_service)
40
+ when 'directory'
41
+ import_service.import_from_directory
42
+ else
43
+ flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
44
+ render :new
45
+ nil
46
+ end
47
+ end
48
+
49
+ def handle_file_import(import_service)
50
+ if params[:workflow_file].present?
51
+ import_service.import_from_file(params[:workflow_file])
52
+ else
53
+ flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
54
+ render :new
55
+ nil
56
+ end
57
+ end
58
+
59
+ def handle_text_import(import_service)
60
+ if params[:workflow_name].present? && params[:yaml_content].present?
61
+ import_service.import_from_yaml_text(params[:workflow_name], params[:yaml_content])
62
+ else
63
+ flash[:alert] = 'Please provide either a file, YAML text, or select directory import'
64
+ render :new
65
+ nil
66
+ end
67
+ end
68
+
69
+ def handle_import_result(result)
70
+ if result.success?
71
+ import_result = result.data
72
+ flash[:notice] = success_message(import_result)
73
+ redirect_to workflows_path
74
+ else
75
+ flash[:alert] = result.error
76
+ render :new
77
+ end
78
+ end
79
+
80
+ def success_message(import_result)
81
+ action = import_result[:action] == 'created' ? 'imported' : 'updated'
82
+ "Workflow '#{import_result[:workflow_name]}' #{action} successfully!"
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaWorkflows
4
+ class ApplicationService
5
+ def self.call(...)
6
+ new(...).call
7
+ end
8
+
9
+ protected
10
+
11
+ def success(data = nil)
12
+ ServiceResult.new(success: true, data: data)
13
+ end
14
+
15
+ def failure(error = nil)
16
+ ServiceResult.new(success: false, error: error)
17
+ end
18
+ end
19
+
20
+ # Simple result object for service responses
21
+ class ServiceResult
22
+ attr_reader :data, :error
23
+
24
+ def initialize(success:, data: nil, error: nil)
25
+ @success = success
26
+ @data = data
27
+ @error = error
28
+ end
29
+
30
+ def success?
31
+ @success
32
+ end
33
+
34
+ def failure?
35
+ !@success
36
+ end
37
+ end
38
+ end
@@ -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
@@ -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 = 0 # 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
@@ -2,152 +2,39 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'fileutils'
5
- require_relative '../meta_workflows/asset_installer'
6
5
 
7
6
  namespace :meta_workflows do
8
7
  desc 'Import workflows from YAML files in the workflows directory'
9
8
  task import: :environment do
10
- MetaWorkflows::WorkflowImporter.new.import_workflows
11
- end
9
+ import_service = MetaWorkflows::WorkflowImportService.new
10
+ result = import_service.import_from_directory
12
11
 
13
- namespace :install do
14
- desc 'Install meta_workflows assets (JavaScript controllers and stylesheets) into host application'
15
- task assets: :environment do
16
- MetaWorkflows::AssetInstallerService.new.install
12
+ if result.success?
13
+ puts result.data[:message]
14
+ display_import_results(import_service) if import_service.import_results.any?
15
+ else
16
+ puts "Import failed: #{result.error}"
17
+ exit 1
17
18
  end
18
19
  end
19
20
  end
20
21
 
21
- # Service class to handle workflow import logic
22
- module MetaWorkflows
23
- class WorkflowImporter
24
- def initialize
25
- @workflows_path = Rails.root.join('workflows')
26
- end
27
-
28
- def import_workflows
29
- puts "Importing workflows from #{@workflows_path}..."
30
-
31
- Dir.glob(File.join(@workflows_path, '*.yml')).each do |file_path|
32
- import_single_workflow(file_path)
33
- end
34
-
35
- puts 'Workflow import completed!'
36
- end
37
-
38
- private
39
-
40
- def import_single_workflow(file_path)
41
- file_name = File.basename(file_path, '.yml')
42
- workflow_name = file_name.underscore
43
-
44
- begin
45
- yaml_content = YAML.load_file(file_path)
46
- save_workflow(workflow_name, yaml_content)
47
- rescue StandardError => e
48
- puts "Error processing #{file_path}: #{e.message}"
49
- end
50
- end
51
-
52
- def save_workflow(workflow_name, yaml_content)
53
- workflow = MetaWorkflows::Workflow.find_or_initialize_by(name: workflow_name)
54
- workflow.recipe = yaml_content
55
-
56
- if workflow.save
57
- puts "Successfully imported workflow: #{workflow.name}"
58
- else
59
- puts "Failed to import workflow #{workflow_name}: #{workflow.errors.full_messages.join(', ')}"
60
- end
61
- end
22
+ def display_import_results(import_service)
23
+ puts "\nDetailed results:"
24
+ import_service.import_results.each do |import_result|
25
+ display_single_import_result(import_result)
62
26
  end
27
+ end
63
28
 
64
- # Service class to handle asset installation
65
- class AssetInstallerService
66
- def install
67
- puts 'Installing meta_workflows assets...'
68
-
69
- installer = MetaWorkflows::AssetInstaller.new
70
-
71
- # Pre-installation validation
72
- unless installer.valid_environment?
73
- puts 'Installation aborted due to validation errors.'
74
- return
75
- end
76
-
77
- begin
78
- process_asset_installation(installer)
79
- rescue MetaWorkflows::AssetInstaller::InstallationError => e
80
- handle_installation_error(e)
81
- rescue StandardError => e
82
- handle_unexpected_error(e)
83
- end
84
- end
85
-
86
- private
87
-
88
- def process_asset_installation(installer)
89
- assets = discover_and_validate_assets(installer)
90
- return if assets.empty?
91
-
92
- result = installer.install_assets(assets)
93
- display_installation_results(installer, assets, result)
94
- end
95
-
96
- def discover_and_validate_assets(installer)
97
- assets = installer.discover_assets
98
-
99
- if assets.empty?
100
- puts 'No assets found to install.'
101
- return []
102
- end
103
-
104
- puts "Found #{assets.count} asset(s) to install:"
105
- assets.each { |asset| puts " - #{asset[:relative_path]}" }
106
- puts
107
-
108
- assets
109
- end
110
-
111
- def display_installation_results(installer, assets, result)
112
- puts
113
- if result[:success]
114
- display_successful_installation(installer, assets, result)
115
- else
116
- display_partial_installation(installer, assets, result)
117
- end
118
- end
119
-
120
- def display_successful_installation(installer, assets, result)
121
- puts 'Installation completed successfully!'
122
- puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped."
123
-
124
- return unless result[:copied].positive?
125
-
126
- puts
127
- installer.display_integration_instructions(assets, result)
128
- end
129
-
130
- def display_partial_installation(installer, assets, result)
131
- puts 'Installation completed with errors.'
132
- puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped, " \
133
- "#{result[:failed]} file(s) failed."
134
- puts 'Some files may have been partially installed. Please review the output above.'
135
-
136
- return unless result[:copied].positive?
137
-
138
- puts
139
- puts 'For successfully copied files, see integration instructions below:'
140
- installer.display_integration_instructions(assets, result)
141
- end
142
-
143
- def handle_installation_error(error)
144
- puts "Installation failed: #{error.message}"
145
- puts 'Installation aborted.'
146
- end
147
-
148
- def handle_unexpected_error(error)
149
- puts "Unexpected error during asset installation: #{error.message}"
150
- puts 'Installation aborted.'
151
- end
29
+ def display_single_import_result(import_result)
30
+ if import_result[:success]
31
+ action = determine_import_action(import_result[:action])
32
+ puts "✓ Successfully #{action} workflow: #{import_result[:workflow_name]}"
33
+ else
34
+ puts "✗ Failed to import workflow #{import_result[:workflow_name]}: #{import_result[:error]}"
152
35
  end
153
36
  end
37
+
38
+ def determine_import_action(action)
39
+ action == 'created' ? 'imported' : 'updated'
40
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meta_workflows
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.24
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Medovyy
@@ -127,9 +127,11 @@ files:
127
127
  - app/assets/stylesheets/meta_workflows/application.css
128
128
  - app/controllers/concerns/tray_configurable.rb
129
129
  - app/controllers/meta_workflows/application_controller.rb
130
+ - app/controllers/meta_workflows/base_debug_controller.rb
130
131
  - app/controllers/meta_workflows/debug_controller.rb
131
132
  - app/controllers/meta_workflows/humans_controller.rb
132
133
  - app/controllers/meta_workflows/meta_controller.rb
134
+ - app/controllers/meta_workflows/workflow_imports_controller.rb
133
135
  - app/helpers/meta_workflows/application_helper.rb
134
136
  - app/helpers/meta_workflows/debug_helper.rb
135
137
  - app/helpers/meta_workflows/execution_helper.rb
@@ -150,7 +152,9 @@ files:
150
152
  - app/models/meta_workflows/workflow.rb
151
153
  - app/models/meta_workflows/workflow_execution.rb
152
154
  - app/models/meta_workflows/workflow_step.rb
155
+ - app/services/meta_workflows/application_service.rb
153
156
  - app/services/meta_workflows/execution_filter_service.rb
157
+ - app/services/meta_workflows/workflow_import_service.rb
154
158
  - app/sidekiq/meta_workflows/tools/meta_workflow_tool.rb
155
159
  - app/views/layouts/meta_workflows/application.html.erb
156
160
  - app/views/layouts/meta_workflows/debug.html.erb
@@ -164,6 +168,7 @@ files:
164
168
  - app/views/meta_workflows/debug/show_execution.html.erb
165
169
  - app/views/meta_workflows/debug/show_workflow.html.erb
166
170
  - app/views/meta_workflows/debug/workflows.html.erb
171
+ - app/views/meta_workflows/workflow_imports/new.html.erb
167
172
  - config/routes.rb
168
173
  - db/migrate/20250530220618_create_meta_workflows_workflows.rb
169
174
  - db/migrate/20250530220634_create_meta_workflows_workflow_executions.rb
@@ -182,8 +187,10 @@ files:
182
187
  - lib/meta_workflows/export_execution_service.rb
183
188
  - lib/meta_workflows/version.rb
184
189
  - lib/services/meta_workflows/application_service.rb
190
+ - lib/services/meta_workflows/asset_installer_service.rb
185
191
  - lib/services/meta_workflows/meta_workflow_service.rb
186
192
  - lib/services/meta_workflows/updaters/meta_service.rb
193
+ - lib/tasks/asset_installer_tasks.rake
187
194
  - lib/tasks/git_hooks.rake
188
195
  - lib/tasks/meta_workflows_tasks.rake
189
196
  homepage: https://github.com/strongmind/meta-workflows