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,93 @@
|
|
|
1
|
+
require "tty-prompt"
|
|
2
|
+
require_relative "../helpers/table_filter"
|
|
3
|
+
|
|
4
|
+
module Strata
|
|
5
|
+
module CLI
|
|
6
|
+
# Reusable autocomplete component for fuzzy search selection.
|
|
7
|
+
# Works with any collection of items - tables, models, dimensions, etc.
|
|
8
|
+
class Autocomplete
|
|
9
|
+
attr_reader :prompt
|
|
10
|
+
|
|
11
|
+
def initialize(prompt: TTY::Prompt.new)
|
|
12
|
+
@prompt = prompt
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Select an item with fuzzy search filtering
|
|
16
|
+
# @param message [String] The prompt message
|
|
17
|
+
# @param items [Array<String>] Items to search through
|
|
18
|
+
# @param per_page [Integer] Number of items to show at once
|
|
19
|
+
# @param display_transform [Proc, nil] Optional proc to transform display names
|
|
20
|
+
# @param default_filter [String, nil] Optional initial filter to pre-apply
|
|
21
|
+
# @return [String, nil] Selected item or nil if cancelled
|
|
22
|
+
def select(message, items, per_page: 10, display_transform: nil, default_filter: nil)
|
|
23
|
+
return nil if items.empty?
|
|
24
|
+
|
|
25
|
+
# Create display mapping if transform provided
|
|
26
|
+
display_map = {}
|
|
27
|
+
display_items = if display_transform
|
|
28
|
+
items.map do |item|
|
|
29
|
+
display_name = display_transform.call(item)
|
|
30
|
+
display_map[display_name] = item
|
|
31
|
+
display_name
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
items
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Pre-filter items if default_filter provided
|
|
38
|
+
filtered_items = display_items
|
|
39
|
+
if default_filter && !default_filter.empty?
|
|
40
|
+
pattern = Regexp.new(Regexp.escape(default_filter), Regexp::IGNORECASE)
|
|
41
|
+
filtered_items = display_items.select { |item| item.match?(pattern) }
|
|
42
|
+
|
|
43
|
+
# If filter matches exactly one item, still show all but highlight the match
|
|
44
|
+
# If filter matches some items, show those
|
|
45
|
+
# If no matches, show all items
|
|
46
|
+
filtered_items = display_items if filtered_items.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build the prompt message with filter hint
|
|
50
|
+
full_message = if default_filter && !default_filter.empty? && filtered_items.length < display_items.length
|
|
51
|
+
"#{message} (filtered by '#{default_filter}'. Type in characters to apply different filter)"
|
|
52
|
+
else
|
|
53
|
+
message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
selected_display = prompt.select(
|
|
57
|
+
full_message,
|
|
58
|
+
filtered_items,
|
|
59
|
+
filter: true,
|
|
60
|
+
per_page: per_page,
|
|
61
|
+
show_help: :always,
|
|
62
|
+
help: "(Type to filter, ↑/↓ to navigate)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return nil if selected_display.nil?
|
|
66
|
+
|
|
67
|
+
# Map back to original item if transform was used
|
|
68
|
+
display_transform ? display_map[selected_display] : selected_display
|
|
69
|
+
rescue TTY::Reader::InputInterrupt
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Multi-select with fuzzy search
|
|
74
|
+
# @param message [String] The prompt message
|
|
75
|
+
# @param items [Array<String>] Items to search through
|
|
76
|
+
# @return [Array<String>] Selected items
|
|
77
|
+
def multi_select(message, items, per_page: 10)
|
|
78
|
+
return [] if items.empty?
|
|
79
|
+
|
|
80
|
+
prompt.multi_select(
|
|
81
|
+
message,
|
|
82
|
+
items,
|
|
83
|
+
filter: true,
|
|
84
|
+
per_page: per_page,
|
|
85
|
+
show_help: :always,
|
|
86
|
+
help: "(Type to filter, Space to select, Enter to confirm)"
|
|
87
|
+
)
|
|
88
|
+
rescue TTY::Reader::InputInterrupt
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require "tty-prompt"
|
|
2
|
+
require "tty-table"
|
|
3
|
+
require "pastel"
|
|
4
|
+
require_relative "../helpers/color_helper"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
# Interactive field editor with row-by-row navigation.
|
|
9
|
+
# Displays a table of AI-generated fields and allows inline editing.
|
|
10
|
+
class FieldEditor
|
|
11
|
+
include Terminal
|
|
12
|
+
|
|
13
|
+
attr_reader :fields, :prompt, :pastel, :model_context
|
|
14
|
+
|
|
15
|
+
# @param fields [Array<Hash>] Fields from AI with :name, :description, :expression, :schema_type, :data_type
|
|
16
|
+
# @param table_context [Hash, nil] Optional context with :table_name, :columns, :datasource for prompt mode
|
|
17
|
+
# @param model_context [Hash, nil] Optional model metadata with :description
|
|
18
|
+
def initialize(fields, prompt: TTY::Prompt.new, table_context: nil, model_context: nil)
|
|
19
|
+
@fields = fields.map { |f| f.merge(skipped: false) }
|
|
20
|
+
@prompt = prompt
|
|
21
|
+
@pastel = Pastel.new
|
|
22
|
+
@selected_index = 0
|
|
23
|
+
@model_context = model_context || {description: ""}
|
|
24
|
+
@table_context = table_context
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run the interactive editor
|
|
28
|
+
# @return [Array<Hash>, nil] Confirmed fields or nil if cancelled
|
|
29
|
+
def run
|
|
30
|
+
loop do
|
|
31
|
+
clear_screen
|
|
32
|
+
display_table
|
|
33
|
+
display_help
|
|
34
|
+
show_cursor
|
|
35
|
+
|
|
36
|
+
key = prompt.reader.read_keypress
|
|
37
|
+
|
|
38
|
+
case key
|
|
39
|
+
when "\e[A", "k" # Up arrow or k
|
|
40
|
+
@selected_index = [@selected_index - 1, 0].max
|
|
41
|
+
when "\e[B", "j" # Down arrow or j
|
|
42
|
+
@selected_index = [@selected_index + 1, @fields.length - 1].min
|
|
43
|
+
when "\r", "\n" # Enter
|
|
44
|
+
handle_edit
|
|
45
|
+
when "s", "S"
|
|
46
|
+
toggle_skip
|
|
47
|
+
when "p", "P"
|
|
48
|
+
handle_prompt_mode
|
|
49
|
+
when "c", "C"
|
|
50
|
+
if prompt.yes?("Confirm and generate model file?")
|
|
51
|
+
show_cursor
|
|
52
|
+
return {fields: active_fields, model_context: @model_context}
|
|
53
|
+
end
|
|
54
|
+
when "q", "Q", "\e" # q or Escape
|
|
55
|
+
if prompt.yes?("Quit without saving?")
|
|
56
|
+
show_cursor
|
|
57
|
+
return nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def clear_screen
|
|
66
|
+
# Hide cursor, move to home, clear screen, then show cursor after render
|
|
67
|
+
# This reduces perceived flicker
|
|
68
|
+
print "\e[?25l" # Hide cursor
|
|
69
|
+
print "\e[H" # Move to home (top-left)
|
|
70
|
+
print "\e[2J" # Clear screen
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def show_cursor
|
|
74
|
+
print "\e[?25h" # Show cursor
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def display_table
|
|
78
|
+
colors = ColorHelper
|
|
79
|
+
puts colors.highlight("\n Field Editor - Review & Edit Generated Fields\n")
|
|
80
|
+
|
|
81
|
+
rows = @fields.each_with_index.map do |field, idx|
|
|
82
|
+
indicator = (idx == @selected_index) ? colors.selected("❯") : " "
|
|
83
|
+
status = field[:skipped] ? colors.disabled("[SKIP]") : ""
|
|
84
|
+
|
|
85
|
+
name_cell = field[:skipped] ? colors.disabled(field[:name]) : field[:name]
|
|
86
|
+
desc_cell = if field[:skipped]
|
|
87
|
+
colors.disabled(truncate(field[:description],
|
|
88
|
+
25))
|
|
89
|
+
else
|
|
90
|
+
truncate(field[:description], 25)
|
|
91
|
+
end
|
|
92
|
+
expr_cell = field[:skipped] ? colors.disabled(field[:expression]) : field[:expression]
|
|
93
|
+
|
|
94
|
+
[
|
|
95
|
+
"#{indicator} #{status}",
|
|
96
|
+
name_cell,
|
|
97
|
+
desc_cell,
|
|
98
|
+
expr_cell
|
|
99
|
+
]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
table = TTY::Table.new(
|
|
103
|
+
header: [" ", "Field Name", "Description", "Expression"],
|
|
104
|
+
rows: rows
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
puts table.render(:unicode, padding: [0, 1]) do |renderer|
|
|
108
|
+
renderer.border.style = :bright_cyan
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def display_help
|
|
113
|
+
colors = ColorHelper
|
|
114
|
+
puts ""
|
|
115
|
+
puts " #{colors.info("↑/↓")} Navigate " \
|
|
116
|
+
"#{colors.success("[Enter]")} Edit " \
|
|
117
|
+
"#{colors.warning("[S]")}kip " \
|
|
118
|
+
"#{colors.primary("[P]")}rompt " \
|
|
119
|
+
"#{colors.secondary("[C]")}onfirm " \
|
|
120
|
+
"#{colors.error("[Q/Esc]")}uit"
|
|
121
|
+
puts ""
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_edit
|
|
125
|
+
edit_current_field
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def edit_current_field
|
|
129
|
+
field = @fields[@selected_index]
|
|
130
|
+
colors = ColorHelper
|
|
131
|
+
|
|
132
|
+
puts colors.highlight("\n Editing: #{field[:name]}\n")
|
|
133
|
+
puts colors.dim(" (Press Ctrl+C or type 'back' to go back without saving)\n")
|
|
134
|
+
|
|
135
|
+
begin
|
|
136
|
+
field[:name] = prompt.ask(" Field Name:", default: field[:name])
|
|
137
|
+
return if field[:name] == "back"
|
|
138
|
+
|
|
139
|
+
field[:description] = prompt.ask(" Description:", default: field[:description])
|
|
140
|
+
return if field[:description] == "back"
|
|
141
|
+
|
|
142
|
+
field[:expression] = prompt.ask(" Expression:", default: field[:expression])
|
|
143
|
+
return if field[:expression] == "back"
|
|
144
|
+
|
|
145
|
+
field[:schema_type] = prompt.select(" Type:", %w[dimension measure], default: field[:schema_type])
|
|
146
|
+
rescue TTY::Reader::InputInterrupt
|
|
147
|
+
# User pressed Ctrl+C, go back without saving
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def toggle_skip
|
|
153
|
+
@fields[@selected_index][:skipped] = !@fields[@selected_index][:skipped]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def active_fields
|
|
157
|
+
@fields.reject { |f| f[:skipped] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def truncate(str, length)
|
|
161
|
+
return "" if str.nil?
|
|
162
|
+
|
|
163
|
+
(str.length > length) ? "#{str[0...length - 2]}.." : str
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def handle_prompt_mode
|
|
167
|
+
require_relative "../ai/services/model_generator"
|
|
168
|
+
require_relative "../terminal"
|
|
169
|
+
generator = AI::Services::ModelGenerator.new
|
|
170
|
+
|
|
171
|
+
colors = ColorHelper
|
|
172
|
+
|
|
173
|
+
unless generator.ai_available?
|
|
174
|
+
puts colors.warning("\n AI is not available. Configure AI in .strata file.")
|
|
175
|
+
sleep 1.5
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
unless @table_context
|
|
180
|
+
puts colors.warning("\n Prompt mode requires table context.")
|
|
181
|
+
sleep 1.5
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
print "\n"
|
|
186
|
+
user_prompt = prompt.ask(" Enter your prompt to regenerate/modify the table:")
|
|
187
|
+
return if user_prompt.nil? || user_prompt.strip.empty?
|
|
188
|
+
|
|
189
|
+
begin
|
|
190
|
+
new_fields = with_spinner("Regenerating fields with AI") do
|
|
191
|
+
generator.call_with_prompt(
|
|
192
|
+
table_name: @table_context[:table_name],
|
|
193
|
+
columns: @table_context[:columns],
|
|
194
|
+
datasource: @table_context[:datasource],
|
|
195
|
+
user_prompt: user_prompt,
|
|
196
|
+
current_fields: active_fields
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if new_fields && !new_fields.empty?
|
|
201
|
+
@fields = new_fields.map { |f| f.merge(skipped: false) }
|
|
202
|
+
@selected_index = 0
|
|
203
|
+
# No message needed - table will refresh and show new fields
|
|
204
|
+
else
|
|
205
|
+
puts colors.warning(" No changes - keeping current fields")
|
|
206
|
+
sleep 1
|
|
207
|
+
end
|
|
208
|
+
rescue => e
|
|
209
|
+
puts colors.error("\n Error: #{e.message}")
|
|
210
|
+
sleep 2
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "rubygems/package"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
require "yaml"
|
|
10
|
+
require_relative "yaml_import_resolver"
|
|
11
|
+
require_relative "import_manager"
|
|
12
|
+
|
|
13
|
+
module Strata
|
|
14
|
+
module CLI
|
|
15
|
+
module Utils
|
|
16
|
+
module Archive
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def create(project_path, files_to_include: nil)
|
|
20
|
+
if files_to_include
|
|
21
|
+
# Use provided file list, but ensure they exist and are yml files
|
|
22
|
+
files = files_to_include.select do |file_path|
|
|
23
|
+
File.file?(file_path) && file_path.end_with?(".yml", ".yaml")
|
|
24
|
+
end
|
|
25
|
+
# Ensure required files are included if they exist
|
|
26
|
+
files = ensure_required_files(files, project_path)
|
|
27
|
+
else
|
|
28
|
+
# Collect all yml files if no list provided
|
|
29
|
+
files = collect_yml_files(project_path)
|
|
30
|
+
end
|
|
31
|
+
files = exclude_secrets(files)
|
|
32
|
+
inlined_files = process_imports(files, project_path)
|
|
33
|
+
build_archive(files, project_path, inlined_files)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method def collect_yml_files(project_path)
|
|
37
|
+
Dir.glob(File.join(project_path, "**", "*.yml")).select do |file_path|
|
|
38
|
+
File.file?(file_path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method def ensure_required_files(files, project_path)
|
|
43
|
+
required_files = [
|
|
44
|
+
File.join(project_path, "project.yml"),
|
|
45
|
+
File.join(project_path, "datasources.yml")
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
required_files.each do |required_file|
|
|
49
|
+
if File.exist?(required_file) && !files.include?(required_file)
|
|
50
|
+
files << required_file
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Always include all test files
|
|
55
|
+
test_files = Dir.glob(File.join(project_path, "tests", "*.{yml,yaml}"))
|
|
56
|
+
test_files.each do |test_file|
|
|
57
|
+
files << test_file unless files.include?(test_file)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
files
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method def exclude_secrets(files)
|
|
64
|
+
files.reject do |file_path|
|
|
65
|
+
file_path.include?(".strata") || file_path.include?("/.strata")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private_class_method def process_imports(files, project_path)
|
|
70
|
+
inlined_files = {}
|
|
71
|
+
files.each do |file_path|
|
|
72
|
+
next unless file_path.end_with?(".yml", ".yaml")
|
|
73
|
+
next unless file_path.start_with?(project_path)
|
|
74
|
+
|
|
75
|
+
# Only process table files (tbl.*.yml) through YamlImportResolver
|
|
76
|
+
# Other files (datasources.yml, rel.*.yml, etc.) don't have imports/fields structure
|
|
77
|
+
filename = File.basename(file_path)
|
|
78
|
+
next unless filename.start_with?("tbl.")
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
inlined_content = YamlImportResolver.resolve(file_path, project_path)
|
|
82
|
+
inlined_files[file_path] = inlined_content
|
|
83
|
+
rescue Strata::ImportError
|
|
84
|
+
# If import resolution fails, use original file
|
|
85
|
+
# Error will be caught during archive creation or audit
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
inlined_files
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_archive(files, project_path, inlined_files = {})
|
|
93
|
+
archive_file = archive_path
|
|
94
|
+
|
|
95
|
+
File.open(archive_file, "wb") do |file|
|
|
96
|
+
Zlib::GzipWriter.open(file) do |gz|
|
|
97
|
+
Gem::Package::TarWriter.new(gz) do |tar|
|
|
98
|
+
files.each do |file_path|
|
|
99
|
+
add_file_to_tar(tar, file_path, project_path, inlined_files)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
archive_file
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private_class_method def add_file_to_tar(tar, file_path, project_path, inlined_files = {})
|
|
109
|
+
relative_path = file_path.sub("#{project_path}/", "")
|
|
110
|
+
|
|
111
|
+
if inlined_files[file_path]
|
|
112
|
+
content = YAML.dump(inlined_files[file_path])
|
|
113
|
+
size = content.bytesize
|
|
114
|
+
mode = 0o644
|
|
115
|
+
|
|
116
|
+
tar.add_file_simple(relative_path, mode, size) do |tar_file|
|
|
117
|
+
tar_file.write(content)
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
mode = File.stat(file_path).mode
|
|
121
|
+
size = File.size(file_path)
|
|
122
|
+
|
|
123
|
+
tar.add_file_simple(relative_path, mode, size) do |tar_file|
|
|
124
|
+
File.open(file_path, "rb") { |f| tar_file.write(f.read) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private_class_method def archive_path
|
|
130
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
|
131
|
+
random_suffix = SecureRandom.hex(8)
|
|
132
|
+
File.join(Dir.tmpdir, "deploy-#{timestamp}-#{random_suffix}.tar.gz")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|