strata-cli 0.1.0.beta
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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +65 -0
- data/LICENSE +21 -0
- data/README.md +465 -0
- data/Rakefile +10 -0
- data/exe/strata +6 -0
- data/lib/strata/cli/ai/client.rb +63 -0
- data/lib/strata/cli/ai/configuration.rb +48 -0
- data/lib/strata/cli/ai/services/table_generator.rb +282 -0
- data/lib/strata/cli/api/client.rb +170 -0
- data/lib/strata/cli/api/connection_error_handler.rb +54 -0
- data/lib/strata/cli/configuration.rb +135 -0
- data/lib/strata/cli/credentials.rb +83 -0
- data/lib/strata/cli/descriptions/create/migration.txt +25 -0
- data/lib/strata/cli/descriptions/create/relation.txt +14 -0
- data/lib/strata/cli/descriptions/create/table.txt +23 -0
- data/lib/strata/cli/descriptions/datasource/add.txt +15 -0
- data/lib/strata/cli/descriptions/datasource/auth.txt +14 -0
- data/lib/strata/cli/descriptions/datasource/exec.txt +7 -0
- data/lib/strata/cli/descriptions/datasource/meta.txt +11 -0
- data/lib/strata/cli/descriptions/datasource/tables.txt +12 -0
- data/lib/strata/cli/descriptions/datasource/test.txt +8 -0
- data/lib/strata/cli/descriptions/deploy/deploy.txt +24 -0
- data/lib/strata/cli/descriptions/deploy/status.txt +9 -0
- data/lib/strata/cli/descriptions/init.txt +14 -0
- data/lib/strata/cli/generators/datasource.rb +83 -0
- data/lib/strata/cli/generators/group.rb +13 -0
- data/lib/strata/cli/generators/migration.rb +71 -0
- data/lib/strata/cli/generators/project.rb +190 -0
- data/lib/strata/cli/generators/relation.rb +64 -0
- data/lib/strata/cli/generators/table.rb +143 -0
- data/lib/strata/cli/generators/templates/adapters/athena.yml +53 -0
- data/lib/strata/cli/generators/templates/adapters/druid.yml +42 -0
- data/lib/strata/cli/generators/templates/adapters/duckdb.yml +36 -0
- data/lib/strata/cli/generators/templates/adapters/mysql.yml +45 -0
- data/lib/strata/cli/generators/templates/adapters/postgres.yml +48 -0
- data/lib/strata/cli/generators/templates/adapters/snowflake.yml +69 -0
- data/lib/strata/cli/generators/templates/adapters/sqlserver.yml +45 -0
- data/lib/strata/cli/generators/templates/adapters/trino.yml +56 -0
- data/lib/strata/cli/generators/templates/datasources.yml +4 -0
- data/lib/strata/cli/generators/templates/migration.rename.yml +15 -0
- data/lib/strata/cli/generators/templates/migration.swap.yml +13 -0
- data/lib/strata/cli/generators/templates/project.yml +36 -0
- data/lib/strata/cli/generators/templates/rel.domain.yml +43 -0
- data/lib/strata/cli/generators/templates/strata.yml +24 -0
- data/lib/strata/cli/generators/templates/table.table_name.yml +118 -0
- data/lib/strata/cli/generators/templates/test.yml +34 -0
- data/lib/strata/cli/generators/test.rb +48 -0
- data/lib/strata/cli/guard.rb +21 -0
- data/lib/strata/cli/helpers/color_helper.rb +103 -0
- data/lib/strata/cli/helpers/command_context.rb +41 -0
- data/lib/strata/cli/helpers/datasource_helper.rb +62 -0
- data/lib/strata/cli/helpers/description_helper.rb +18 -0
- data/lib/strata/cli/helpers/project_helper.rb +85 -0
- data/lib/strata/cli/helpers/prompts.rb +42 -0
- data/lib/strata/cli/helpers/table_filter.rb +48 -0
- data/lib/strata/cli/main.rb +71 -0
- data/lib/strata/cli/sub_commands/audit.rb +262 -0
- data/lib/strata/cli/sub_commands/create.rb +419 -0
- data/lib/strata/cli/sub_commands/datasource.rb +353 -0
- data/lib/strata/cli/sub_commands/deploy.rb +433 -0
- data/lib/strata/cli/sub_commands/project.rb +38 -0
- data/lib/strata/cli/sub_commands/table.rb +58 -0
- data/lib/strata/cli/terminal.rb +102 -0
- data/lib/strata/cli/ui/autocomplete.rb +93 -0
- data/lib/strata/cli/ui/field_editor.rb +215 -0
- data/lib/strata/cli/utils/archive.rb +137 -0
- data/lib/strata/cli/utils/deployment_monitor.rb +445 -0
- data/lib/strata/cli/utils/git.rb +253 -0
- data/lib/strata/cli/utils/import_manager.rb +190 -0
- data/lib/strata/cli/utils/test_reporter.rb +131 -0
- data/lib/strata/cli/utils/yaml_import_resolver.rb +91 -0
- data/lib/strata/cli/utils.rb +39 -0
- data/lib/strata/cli/version.rb +7 -0
- data/lib/strata/cli.rb +36 -0
- data/sig/strata/cli.rbs +6 -0
- metadata +306 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require_relative "git"
|
|
8
|
+
|
|
9
|
+
module Strata
|
|
10
|
+
module CLI
|
|
11
|
+
module Utils
|
|
12
|
+
# Manages imported files and tracks external imports in strata.lock.
|
|
13
|
+
module ImportManager
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
LOCK_FILE = "strata.lock"
|
|
17
|
+
|
|
18
|
+
def validate_import_path(import_path)
|
|
19
|
+
if Pathname.new(import_path).absolute?
|
|
20
|
+
raise Strata::InvalidImportPathError, "Import paths must be relative, not absolute: #{import_path}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def track_external_import(import_path, resolved_path, project_path)
|
|
25
|
+
return resolved_path unless external_import?(resolved_path, project_path)
|
|
26
|
+
|
|
27
|
+
lock_data = load_lock_file(project_path)
|
|
28
|
+
current_hash = file_hash(resolved_path)
|
|
29
|
+
|
|
30
|
+
# Find existing entry by path (as specified in YAML)
|
|
31
|
+
existing = find_existing_import(import_path, lock_data)
|
|
32
|
+
|
|
33
|
+
if existing && existing["hash"] == current_hash
|
|
34
|
+
# Hash unchanged, no update needed
|
|
35
|
+
return resolved_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Update or add entry
|
|
39
|
+
entry = {
|
|
40
|
+
"path" => import_path,
|
|
41
|
+
"source" => resolved_path,
|
|
42
|
+
"hash" => current_hash,
|
|
43
|
+
"imported_at" => Time.now.utc.iso8601
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
lock_data["imports"] ||= []
|
|
47
|
+
lock_data["imports"].reject! { |e| e["path"] == import_path }
|
|
48
|
+
lock_data["imports"] << entry
|
|
49
|
+
|
|
50
|
+
save_lock_file(project_path, lock_data)
|
|
51
|
+
|
|
52
|
+
resolved_path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_with_fallback(import_path, base_dir, project_path)
|
|
56
|
+
# Validate path is relative
|
|
57
|
+
validate_import_path(import_path)
|
|
58
|
+
|
|
59
|
+
resolved_path = File.expand_path(import_path, base_dir)
|
|
60
|
+
|
|
61
|
+
unless File.exist?(resolved_path)
|
|
62
|
+
raise Strata::MissingImportError, "Import file not found: #{import_path}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Track external imports in lock file
|
|
66
|
+
track_external_import(import_path, resolved_path, project_path)
|
|
67
|
+
|
|
68
|
+
resolved_path
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def check_changed_imports(project_path)
|
|
72
|
+
lock_data = load_lock_file(project_path)
|
|
73
|
+
return [] unless lock_data["imports"]
|
|
74
|
+
|
|
75
|
+
changed = []
|
|
76
|
+
lock_data["imports"].each do |entry|
|
|
77
|
+
next unless entry["source"] && File.exist?(entry["source"])
|
|
78
|
+
|
|
79
|
+
current_hash = file_hash(entry["source"])
|
|
80
|
+
if current_hash != entry["hash"]
|
|
81
|
+
changed << {
|
|
82
|
+
path: entry["path"],
|
|
83
|
+
source: entry["source"],
|
|
84
|
+
old_hash: entry["hash"],
|
|
85
|
+
new_hash: current_hash
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
changed
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def refresh_external_imports(project_path)
|
|
94
|
+
lock_data = load_lock_file(project_path)
|
|
95
|
+
return [] unless lock_data["imports"]
|
|
96
|
+
|
|
97
|
+
refreshed = []
|
|
98
|
+
lock_data["imports"].each do |entry|
|
|
99
|
+
next unless entry["source"] && File.exist?(entry["source"])
|
|
100
|
+
|
|
101
|
+
current_hash = file_hash(entry["source"])
|
|
102
|
+
if current_hash != entry["hash"]
|
|
103
|
+
entry["hash"] = current_hash
|
|
104
|
+
entry["imported_at"] = Time.now.utc.iso8601
|
|
105
|
+
refreshed << {
|
|
106
|
+
path: entry["path"],
|
|
107
|
+
source: entry["source"],
|
|
108
|
+
hash: current_hash
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
save_lock_file(project_path, lock_data) if refreshed.any?
|
|
114
|
+
|
|
115
|
+
refreshed
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def generate_import_commit_hash(project_path)
|
|
119
|
+
lock_data = load_lock_file(project_path)
|
|
120
|
+
return nil unless lock_data["imports"]&.any?
|
|
121
|
+
|
|
122
|
+
# Create a hash from all import hashes combined
|
|
123
|
+
import_hashes = lock_data["imports"].map { |e| "#{e["path"]}:#{e["hash"]}" }.sort.join("|")
|
|
124
|
+
Digest::SHA256.hexdigest(import_hashes)[0..15]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private_class_method def external_import?(resolved_path, project_path)
|
|
128
|
+
project_abs = File.expand_path(project_path)
|
|
129
|
+
import_abs = File.expand_path(resolved_path)
|
|
130
|
+
!import_abs.start_with?(project_abs)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method def load_lock_file(project_path)
|
|
134
|
+
default_lock_data = {"imports" => []}
|
|
135
|
+
lock_path = File.join(project_path, LOCK_FILE)
|
|
136
|
+
return default_lock_data unless File.exist?(lock_path)
|
|
137
|
+
|
|
138
|
+
YAML.safe_load_file(lock_path, permitted_classes: [Date, Time], aliases: true) || default_lock_data
|
|
139
|
+
rescue => e
|
|
140
|
+
warn "Failed to load lock file: #{e.message}" if ENV["DEBUG"]
|
|
141
|
+
default_lock_data
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method def save_lock_file(project_path, lock_data)
|
|
145
|
+
lock_path = File.join(project_path, LOCK_FILE)
|
|
146
|
+
begin
|
|
147
|
+
File.write(lock_path, YAML.dump(lock_data))
|
|
148
|
+
|
|
149
|
+
# Automatically commit the lock file to git if it changed
|
|
150
|
+
git_dir = File.join(project_path, ".git")
|
|
151
|
+
if File.directory?(git_dir)
|
|
152
|
+
relative_lock_path = LOCK_FILE
|
|
153
|
+
commit_message = "[Strata-CLI] Update strata.lock with external import changes"
|
|
154
|
+
Git.commit_file(relative_lock_path, commit_message, project_path)
|
|
155
|
+
end
|
|
156
|
+
rescue Errno::EACCES => e
|
|
157
|
+
raise Strata::CommandError, "Permission denied writing to #{lock_path}: #{e.message}"
|
|
158
|
+
rescue Errno::ENOSPC => e
|
|
159
|
+
raise Strata::CommandError, "Disk full: #{e.message}"
|
|
160
|
+
rescue => e
|
|
161
|
+
raise Strata::CommandError, "Failed to write lock file #{lock_path}: #{e.message}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private_class_method def file_hash(file_path)
|
|
166
|
+
return nil unless File.exist?(file_path)
|
|
167
|
+
|
|
168
|
+
Digest::SHA256.file(file_path).hexdigest
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private_class_method def find_existing_import(import_path, lock_data)
|
|
172
|
+
return nil unless lock_data["imports"]
|
|
173
|
+
|
|
174
|
+
lock_data["imports"].find { |entry| entry["path"] == import_path }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Public accessors for testing/debugging
|
|
178
|
+
def load_lock_file_public(project_path)
|
|
179
|
+
load_lock_file(project_path)
|
|
180
|
+
end
|
|
181
|
+
module_function :load_lock_file_public
|
|
182
|
+
|
|
183
|
+
def file_hash_public(file_path)
|
|
184
|
+
file_hash(file_path)
|
|
185
|
+
end
|
|
186
|
+
module_function :file_hash_public
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../helpers/color_helper"
|
|
4
|
+
require_relative "../terminal"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module Utils
|
|
9
|
+
class TestReporter
|
|
10
|
+
include Terminal
|
|
11
|
+
|
|
12
|
+
def initialize(color_helper: ColorHelper)
|
|
13
|
+
@color_helper = color_helper
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def display(test_results, skip_opening_divider: false)
|
|
17
|
+
return unless test_results.is_a?(Hash) && test_results["tests"].is_a?(Array)
|
|
18
|
+
|
|
19
|
+
display_summary(test_results, skip_opening_divider: skip_opening_divider)
|
|
20
|
+
display_failures(test_results) if has_failures?(test_results)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :color_helper
|
|
26
|
+
|
|
27
|
+
def say(message, color_helper_result = nil)
|
|
28
|
+
if color_helper_result
|
|
29
|
+
if color_helper_result.is_a?(Array)
|
|
30
|
+
colored_message = ColorHelper.pastel.decorate(message, *color_helper_result)
|
|
31
|
+
$stdout.puts(colored_message)
|
|
32
|
+
elsif color_helper_result.is_a?(Symbol)
|
|
33
|
+
theme = ColorHelper::THEME[color_helper_result]
|
|
34
|
+
if theme
|
|
35
|
+
styles = Array(theme)
|
|
36
|
+
colored_message = ColorHelper.pastel.decorate(message, *styles)
|
|
37
|
+
$stdout.puts(colored_message)
|
|
38
|
+
else
|
|
39
|
+
begin
|
|
40
|
+
colored_message = ColorHelper.pastel.send(color_helper_result, message)
|
|
41
|
+
$stdout.puts(colored_message)
|
|
42
|
+
rescue NoMethodError
|
|
43
|
+
$stdout.puts(message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
$stdout.puts(message)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
$stdout.puts(message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def display_summary(test_results, skip_opening_divider: false)
|
|
55
|
+
tests = test_results["tests"] || []
|
|
56
|
+
total = tests.length
|
|
57
|
+
passed = tests.count { |t| t["status"] == "passed" }
|
|
58
|
+
failed = tests.count { |t| t["status"] == "failed" }
|
|
59
|
+
errors = tests.count { |t| t["status"] == "error" }
|
|
60
|
+
|
|
61
|
+
say "\n" + "=" * 60, :border unless skip_opening_divider
|
|
62
|
+
say "\n Tests ran: #{total} total, #{passed} passed", :info
|
|
63
|
+
say " #{failed} failed, #{errors} error#{"s" if errors != 1}" if failed > 0 || errors > 0
|
|
64
|
+
say "\n" + "=" * 60, :border
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def display_failures(test_results)
|
|
68
|
+
tests = test_results["tests"] || []
|
|
69
|
+
failed_tests = tests.select { |t| t["status"] == "failed" || t["status"] == "error" }
|
|
70
|
+
|
|
71
|
+
return if failed_tests.empty?
|
|
72
|
+
|
|
73
|
+
say "\n Failed Tests:\n", :error
|
|
74
|
+
|
|
75
|
+
failed_tests.each do |test|
|
|
76
|
+
display_test_failure(test)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def display_test_failure(test)
|
|
81
|
+
name = test["name"] || "Unknown test"
|
|
82
|
+
status = test["status"]
|
|
83
|
+
|
|
84
|
+
say "\n ✖ #{name}", :error
|
|
85
|
+
|
|
86
|
+
if status == "error"
|
|
87
|
+
say " Error: #{test["error"]}", :error
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Display assertion failure details
|
|
92
|
+
if test["assertion_type"]
|
|
93
|
+
say " Assertion failed: #{test["assertion_type"]}", :error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if test["message"]
|
|
97
|
+
say " #{test["message"]}", :error
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Show expected vs generated SQL for debugging
|
|
101
|
+
if test["expected_sql"]
|
|
102
|
+
say " Expected: #{truncate(test["expected_sql"], 100)}", :error
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if test["pattern"]
|
|
106
|
+
say " Pattern: #{test["pattern"]}", :error
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if test["generated_sql"]
|
|
110
|
+
say " Generated: #{truncate(test["generated_sql"], 200)}", :error
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if test["failing_values"] && test["failing_values"].is_a?(Array) && test["failing_values"].any?
|
|
114
|
+
sample_values = test["failing_values"].first(5)
|
|
115
|
+
say " Sample failing values: #{sample_values.inspect}", :error
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def truncate(str, max_length)
|
|
120
|
+
return str if str.nil? || str.length <= max_length
|
|
121
|
+
str[0, max_length] + "..."
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def has_failures?(test_results)
|
|
125
|
+
tests = test_results["tests"] || []
|
|
126
|
+
tests.any? { |t| t["status"] == "failed" || t["status"] == "error" }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require_relative "import_manager"
|
|
6
|
+
|
|
7
|
+
module Strata
|
|
8
|
+
module CLI
|
|
9
|
+
module Utils
|
|
10
|
+
# Resolves YAML imports and merges fields with granular override logic.
|
|
11
|
+
module YamlImportResolver
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def resolve(file_path, project_path, visited = Set.new)
|
|
15
|
+
return {} unless File.exist?(file_path)
|
|
16
|
+
|
|
17
|
+
absolute_path = File.expand_path(file_path)
|
|
18
|
+
raise Strata::CircularImportError, "Circular import detected: #{format_cycle(visited, absolute_path)}" if visited.include?(absolute_path)
|
|
19
|
+
|
|
20
|
+
visited.add(absolute_path)
|
|
21
|
+
content = YAML.safe_load_file(file_path, permitted_classes: [Date, Time], aliases: true)
|
|
22
|
+
return {} unless content.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
resolved_content = content.dup
|
|
25
|
+
resolved_content.delete("imports")
|
|
26
|
+
|
|
27
|
+
if content["imports"]&.is_a?(Array)
|
|
28
|
+
merged_fields = resolve_imports(content["imports"], file_path, project_path, visited)
|
|
29
|
+
resolved_content["fields"] = merge_fields(merged_fields, content["fields"] || [])
|
|
30
|
+
else
|
|
31
|
+
resolved_content["fields"] = content["fields"] || []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
resolved_content
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method def resolve_imports(imports, base_file_path, project_path, visited)
|
|
38
|
+
base_dir = File.dirname(base_file_path)
|
|
39
|
+
all_imported_fields = []
|
|
40
|
+
|
|
41
|
+
imports.each do |import_path|
|
|
42
|
+
ImportManager.validate_import_path(import_path)
|
|
43
|
+
|
|
44
|
+
resolved_path = ImportManager.resolve_with_fallback(import_path, base_dir, project_path)
|
|
45
|
+
imported_content = resolve(resolved_path, project_path, visited.dup)
|
|
46
|
+
all_imported_fields.concat(imported_content["fields"] || []) if imported_content.is_a?(Hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
all_imported_fields
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_class_method def merge_fields(imported_fields, local_fields)
|
|
53
|
+
field_map = {}
|
|
54
|
+
imported_fields.each do |field|
|
|
55
|
+
next unless field.is_a?(Hash) && field["name"]
|
|
56
|
+
|
|
57
|
+
field_map[field["name"]] = field.dup
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local_fields.each do |field|
|
|
61
|
+
next unless field.is_a?(Hash) && field["name"]
|
|
62
|
+
|
|
63
|
+
field_name = field["name"]
|
|
64
|
+
|
|
65
|
+
if field_map[field_name]
|
|
66
|
+
merged_field = field.dup
|
|
67
|
+
field_map[field_name].each do |key, value|
|
|
68
|
+
next if key == "name" # name is always from local
|
|
69
|
+
|
|
70
|
+
if !merged_field.key?(key) || (merged_field[key].nil? || merged_field[key] == "")
|
|
71
|
+
merged_field[key] = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
field_map[field_name] = merged_field
|
|
76
|
+
else
|
|
77
|
+
field_map[field_name] = field.dup
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
field_map.values.sort_by { |f| f["name"] || "" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private_class_method def format_cycle(visited, current)
|
|
85
|
+
cycle = visited.to_a + [current]
|
|
86
|
+
cycle.map { |path| File.basename(path) }.join(" -> ")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Strata
|
|
2
|
+
module CLI
|
|
3
|
+
module Utils
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
# Convert a given string to a Strata compliant
|
|
7
|
+
# slug or url friendly ID.
|
|
8
|
+
#
|
|
9
|
+
# @param str [String]
|
|
10
|
+
def url_safe_str(str)
|
|
11
|
+
str.downcase.strip.gsub(/\s+/, "-").gsub(/[^\w-]/, "")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def exit_error_if_not_strata!
|
|
15
|
+
return if CLI.config.strata_project?
|
|
16
|
+
|
|
17
|
+
Thor::Shell::Color.new.say_error "ERROR: This is not a valid strata project", :red
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Match a YAML block given a key.
|
|
22
|
+
# It should match a block like so:
|
|
23
|
+
#
|
|
24
|
+
# key:
|
|
25
|
+
# attribute: value
|
|
26
|
+
# attr_2: val
|
|
27
|
+
#
|
|
28
|
+
# another_key: another_val
|
|
29
|
+
#
|
|
30
|
+
# This should match just the 'key' block
|
|
31
|
+
# above.
|
|
32
|
+
#
|
|
33
|
+
# @param key [String]
|
|
34
|
+
def yaml_block_matcher(key)
|
|
35
|
+
/^#{key}:.*?(?=^\S|\z)/m
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/strata/cli.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "dwh"
|
|
3
|
+
require_relative "cli/version"
|
|
4
|
+
require_relative "cli/configuration"
|
|
5
|
+
require_relative "cli/main"
|
|
6
|
+
require_relative "cli/utils"
|
|
7
|
+
|
|
8
|
+
module Strata
|
|
9
|
+
class StrataError < StandardError; end
|
|
10
|
+
|
|
11
|
+
class ConfigError < StrataError; end
|
|
12
|
+
|
|
13
|
+
class CommandError < StrataError; end
|
|
14
|
+
|
|
15
|
+
class ImportError < CommandError; end
|
|
16
|
+
|
|
17
|
+
class CircularImportError < ImportError; end
|
|
18
|
+
|
|
19
|
+
class MissingImportError < ImportError; end
|
|
20
|
+
|
|
21
|
+
class InvalidImportPathError < ImportError; end
|
|
22
|
+
|
|
23
|
+
module CLI
|
|
24
|
+
@configuration = Configuration.new
|
|
25
|
+
def self.config
|
|
26
|
+
@configuration
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.start(args)
|
|
30
|
+
Main.start(args)
|
|
31
|
+
rescue TTY::Reader::InputInterrupt
|
|
32
|
+
warn "\nCancelled."
|
|
33
|
+
exit(1)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|