strata-cli 0.1.7 → 0.1.9

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +24 -1
  4. data/lib/strata/cli/ai/services/table_generator.rb +35 -20
  5. data/lib/strata/cli/api/client.rb +23 -63
  6. data/lib/strata/cli/api/response_error_handler.rb +115 -0
  7. data/lib/strata/cli/credentials.rb +5 -9
  8. data/lib/strata/cli/error_reporter.rb +4 -1
  9. data/lib/strata/cli/generators/datasource.rb +4 -3
  10. data/lib/strata/cli/generators/group.rb +37 -0
  11. data/lib/strata/cli/generators/migration.rb +2 -1
  12. data/lib/strata/cli/generators/project.rb +18 -11
  13. data/lib/strata/cli/generators/relation.rb +2 -1
  14. data/lib/strata/cli/generators/table.rb +5 -8
  15. data/lib/strata/cli/generators/templates/adapters/databricks.yml +4 -3
  16. data/lib/strata/cli/generators/templates/adapters/snowflake.yml +1 -21
  17. data/lib/strata/cli/generators/templates/table.table_name.yml +1 -1
  18. data/lib/strata/cli/generators/test.rb +2 -1
  19. data/lib/strata/cli/guard.rb +4 -1
  20. data/lib/strata/cli/helpers/command_context.rb +8 -9
  21. data/lib/strata/cli/helpers/datasource_helper.rb +1 -1
  22. data/lib/strata/cli/helpers/description_helper.rb +2 -1
  23. data/lib/strata/cli/main.rb +15 -3
  24. data/lib/strata/cli/output.rb +103 -0
  25. data/lib/strata/cli/sub_commands/audit.rb +4 -3
  26. data/lib/strata/cli/sub_commands/branch.rb +163 -0
  27. data/lib/strata/cli/sub_commands/create.rb +1 -2
  28. data/lib/strata/cli/sub_commands/datasource.rb +13 -4
  29. data/lib/strata/cli/sub_commands/deploy.rb +14 -13
  30. data/lib/strata/cli/sub_commands/project.rb +4 -3
  31. data/lib/strata/cli/sub_commands/table.rb +9 -8
  32. data/lib/strata/cli/terminal.rb +7 -4
  33. data/lib/strata/cli/ui/field_editor.rb +21 -27
  34. data/lib/strata/cli/utils/deployment_monitor.rb +15 -34
  35. data/lib/strata/cli/utils/git.rb +78 -0
  36. data/lib/strata/cli/utils/import_manager.rb +4 -1
  37. data/lib/strata/cli/utils/test_reporter.rb +4 -32
  38. data/lib/strata/cli/utils/version_checker.rb +4 -8
  39. data/lib/strata/cli/utils.rb +3 -1
  40. data/lib/strata/cli/version.rb +1 -1
  41. data/lib/strata/cli.rb +4 -3
  42. metadata +4 -2
  43. data/lib/strata/cli/helpers/color_helper.rb +0 -103
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17bd3f24bcacdbae3b7dce1d9af614e28d94d31ced0a3f9d491dccc6a0a0cc9f
4
- data.tar.gz: 4ee0c2de9c22a06e6ac0c91e74f04a6a0dd9e76edcdbd2ea5f07c0b028f7fed2
3
+ metadata.gz: 31a0327b1b8df2f7a982ece6c882cf019322cdf6e649b8cafdf035132424a705
4
+ data.tar.gz: 16dbe764a144c7098574900d54fdad599727a65dbfc82fef531b6f63a59e39a7
5
5
  SHA512:
6
- metadata.gz: 5932b6185ba4683a5a610866dd8bccc22216cf8974a6d03dee14bd0df843c0bb4a909fc679f9e152273059e8745e94b689da2a0688c13be44bd041953110a1cb
7
- data.tar.gz: d8d3440b5b02d90bce6c1be4d0f8dd7f18129c45f2635a4119b8c55f23c1393457764479a27c89c2c06e9c377b1d5c251527ddeb6d96cd48434519baf0698022
6
+ metadata.gz: 2fa33f77c3082a06a9a7e3e52cbeebd5b12ed4195b621011380ed2c39d124f3ebcd28b1962f8cf1565a999e101c8df7db15ea864dafed4d778211272a5c3f250
7
+ data.tar.gz: b1b1250691596a599af3b3193220242fe36f50eb4c7cab83eaac18d3a59795979b4b861c11799a65588e6a077594b2ac396e45a54c4aa216421aaf6846896146
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.9] - 2026-05-13
4
+
5
+ ### Added
6
+
7
+ - **Branch subcommands**: `strata branch` with `list` (local and server branches), `create`, `checkout`, and `delete` (remove a branch on the Strata server). Git helpers extended to support these flows.
8
+
9
+ ### Changed
10
+
11
+ - **Terminal output**: Centralized printing through `Output`, reduced verbose logs from deployment monitoring and test reporting; removed `ColorHelper` in favor of shared output helpers.
12
+ - **API client errors**: Added `ResponseErrorHandler` to centralize HTTP error handling and user-facing messages across the client and generators.
13
+ - **AI table generation**: Updated `SYSTEM_PROMPT` for YAML model / field generation.
14
+
15
+ ### Fixed
16
+
17
+ - **`list` command**: Correctly includes nested models in listings.
18
+ - **Field editor**: Dimension lookup flags apply only to dimensions.
19
+
20
+ ## [0.1.8] - 2026-04-28
21
+
22
+ ### Changed
23
+
24
+ - **Snowflake authentication**: Removed OAuth flow from CLI prompts and templates; Snowflake now supports `pat` and `kp` only.
25
+ - **Databricks authentication**: Standardized on OAuth M2M (service principal) only; removed U2M flow and auth-mode selection prompt.
26
+ - **CLI credential UX**: Added explicit guidance during Databricks credential collection that service principal `oauth_client_id` and `oauth_client_secret` are expected.
27
+
3
28
  ## [0.1.7] - 2026-04-23
4
29
 
5
30
  ### Added
data/README.md CHANGED
@@ -342,14 +342,37 @@ my_snowflake:
342
342
  schema: PUBLIC
343
343
  role: ACCOUNTADMIN
344
344
  auth_mode: pat
345
+
346
+ my_databricks:
347
+ adapter: databricks
348
+ name: Databricks Warehouse
349
+ host: workspace.cloud.databricks.com
350
+ warehouse: warehouse_id
351
+ catalog: main
352
+ schema: default
353
+ auth_mode: oauth_m2m
354
+ ```
355
+
356
+ ### Databricks authentication
357
+
358
+ Databricks now requires explicit `auth_mode`.
359
+
360
+ If you already have Databricks datasources, update `datasources.yml`:
361
+
362
+ ```yaml
363
+ my_databricks:
364
+ adapter: databricks
365
+ auth_mode: oauth_m2m
345
366
  ```
346
367
 
368
+ Use service principal credentials and run `strata ds auth my_databricks` to save `oauth_client_id` and `oauth_client_secret` in `.strata`.
369
+
347
370
  ## Supported Data Warehouse Adapters
348
371
 
349
372
  - **PostgreSQL** - Full support
350
373
  - **MySQL** - Full support
351
374
  - **SQL Server** - Full support (including Azure)
352
- - **Snowflake** - Full support (PAT, Key Pair, OAuth)
375
+ - **Snowflake** - Full support (PAT, Key Pair)
353
376
  - **Athena** - AWS Athena support
354
377
  - **Trino** - Trino/Presto support
355
378
  - **DuckDB** - Embedded analytics database
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../client"
4
+ require_relative "../../output"
4
5
  require "yaml"
5
6
 
6
7
  module Strata
@@ -14,17 +15,29 @@ module Strata
14
15
  You are a semantic modeling expert for the Strata Business Intelligence platform.
15
16
  Analyze database columns and generate semantic field definitions.
16
17
 
17
- You must understand that Names are extremely important in the Strata application. When the same named dimension
18
+ You must understand that Names are extremely important in the Strata application. When the same named dimension
18
19
  is mapped to multiple tables, Strata treats them as the same entity but with multiple potential tables. Strata
19
- will at query generation time choose the best table. Therefore it is extremely important to consider whether a
20
- potential dimension or measure is the same as an existing one. You can try and infer that based on the table name,
21
- column name, and the existing fields. In general, you should use existing names for the same schema type if it makes sense.
20
+ will at query generation time choose the best table. Therefore it is extremely important to consider whether a
21
+ potential dimension or measure is the same as an existing one. You can infer that based on the table name,
22
+ column name, existing fields, and business context. Reuse an existing dimension name only when it represents
23
+ the same business concept across tables. If the same column label describes the current table's own entity,
24
+ lifecycle, or attributes, create a table-specific name instead of reusing a generic existing name.
25
+ Example:
26
+ existing dimension name: Record Start Date
27
+ table name: web_page
28
+ column name: record_start_date
29
+ dimension name: Web Page Record Start Date
30
+ Example:
31
+ existing dimension name: Country
32
+ table name: customer
33
+ column name: country
34
+ dimension name: Country
22
35
  The edge case is when you have a Dimension table like Customer with column first_name and another Dimension
23
36
  table called Billed Customer with first_name, then they are likely not the same. A dimension created from
24
37
  Billed Customer should be given an appropriately prefixed name: Billed Customer First Name.
25
38
 
26
- Next, if the column is something highly ambigous and the current table is likely a dimension table, we should prefix
27
- then dimension name with approprite prefix based on the name of the dimension table.
39
+ Next, if the column is something highly ambiguous and the current table is likely a dimension table, prefix
40
+ the dimension name with an appropriate prefix based on the name of the dimension table.
28
41
  Example:
29
42
  table name: item_dim
30
43
  column name: color
@@ -48,8 +61,8 @@ module Strata
48
61
  Example: "State" in a geography table → ["province"]
49
62
  Example: "Customer Return Date" → [] -- name has high specificity already so no synonyms needed
50
63
 
51
- In cases where a dimension already exists (i.e. a dimension with same name exists), omit everything
52
- except the following fields: name, schema_type, data_type, expression.
64
+ In cases where you intentionally reuse an existing dimension name because it is the same business concept,
65
+ omit everything except the following fields: name, schema_type, data_type, expression.
53
66
 
54
67
  Output ONLY valid JSON array, no explanations.
55
68
  PROMPT
@@ -68,7 +81,7 @@ module Strata
68
81
  # @return [Hash, nil] Generated model data or nil if AI disabled
69
82
  def call(table_name:, columns:, datasource:, user_context: nil)
70
83
  unless ai_available?
71
- warn "AI not available - using rule-based fallback"
84
+ Output.print_warning("AI not available - using rule-based fallback")
72
85
  return fallback_fields(columns)
73
86
  end
74
87
 
@@ -85,7 +98,7 @@ module Strata
85
98
  parse_response(response, columns)
86
99
  rescue => e
87
100
  # Fallback to basic field generation on AI error
88
- warn "AI failed to generate fields: #{e.message} - performing basic field generation"
101
+ Output.print_warning("AI failed to generate fields: #{e.message} - performing basic field generation")
89
102
  fallback_fields(columns)
90
103
  end
91
104
  end
@@ -99,8 +112,7 @@ module Strata
99
112
  # @return [Array<Hash>] Regenerated fields
100
113
  def call_with_prompt(table_name:, columns:, datasource:, user_prompt:, current_fields:)
101
114
  unless ai_available?
102
- warn "AI not available - could not use prompt mode"
103
- return nil
115
+ raise AIError, "AI is not available - configure ai_api_key in .strata"
104
116
  end
105
117
 
106
118
  prompt = build_prompt_with_user_input(
@@ -114,8 +126,7 @@ module Strata
114
126
  response = @client.complete(prompt, system_prompt: SYSTEM_PROMPT)
115
127
  parse_response(response, columns)
116
128
  rescue => e
117
- warn "AI failed to generate fields: #{e.message}"
118
- nil
129
+ raise AIError, "AI failed to generate fields: #{e.message}"
119
130
  end
120
131
  end
121
132
 
@@ -134,7 +145,7 @@ module Strata
134
145
  end.join("\n")
135
146
 
136
147
  context_section = if existing_models_context && !existing_models_context.empty?
137
- "\n\nExisting model patterns for consistency:\n#{existing_models_context}"
148
+ "\n\nExisting semantic model fields for naming comparison:\n#{existing_models_context}"
138
149
  else
139
150
  ""
140
151
  end
@@ -168,7 +179,7 @@ module Strata
168
179
 
169
180
  existing_models = load_existing_models_context
170
181
  context_section = if existing_models && !existing_models.empty?
171
- "\n\nExisting model patterns for consistency:\n#{existing_models}"
182
+ "\n\nExisting semantic model fields for naming comparison:\n#{existing_models}"
172
183
  else
173
184
  ""
174
185
  end
@@ -190,17 +201,21 @@ module Strata
190
201
  def load_existing_models_context
191
202
  return nil unless Dir.exist?("models")
192
203
 
193
- model_files = Dir.glob("models/**/tbl.*.yml").first(100) # Limit to 5 for performance
204
+ model_files = Dir.glob("models/**/tbl[._]*.yml").first(100) # Limit to 5 for performance
194
205
  return nil if model_files.empty?
195
206
 
196
207
  contexts = model_files.filter_map do |file|
197
208
  model = YAML.safe_load_file(file, permitted_classes: [Date, Time], aliases: true) || {}
198
209
  fields_summary = (model["fields"] || []).map do |f|
199
- "#{f["name"]} (#{f["type"]}) data type: #{f["data_type"]}"
210
+ expression = f["expression"]
211
+ expression_sql = expression.is_a?(Hash) ? expression["sql"] : expression
212
+ "#{f["name"]} (#{f["type"]}) data type: #{f["data_type"]}, expression: #{expression_sql}"
200
213
  end.join("\n\t")
201
- "Model: #{model["name"]} \n Fields: \n\t#{fields_summary}"
214
+ "Model: #{model["name"]}, physical table: #{model["physical_name"]}\n Fields: \n\t#{fields_summary}"
202
215
  rescue => e
203
- warn "Failed to load model file #{file}: #{e.message}" if ENV["DEBUG"]
216
+ if ENV["DEBUG"]
217
+ Output.print_warning("Failed to load model file #{file}: #{e.message}")
218
+ end
204
219
  nil
205
220
  end
206
221
 
@@ -4,12 +4,14 @@ require "faraday"
4
4
  require "faraday/multipart"
5
5
  require "json"
6
6
  require_relative "connection_error_handler"
7
+ require_relative "response_error_handler"
7
8
 
8
9
  module Strata
9
10
  module CLI
10
11
  module API
11
12
  class Client
12
13
  include ConnectionErrorHandler
14
+ include ResponseErrorHandler
13
15
 
14
16
  def initialize(server_url, api_key)
15
17
  @server_url = server_url.chomp("/")
@@ -65,6 +67,21 @@ module Strata
65
67
  deployments.first
66
68
  end
67
69
 
70
+ def branches(project_id)
71
+ response = with_connection_error_handling(@server_url) { connection.get(branches_url(project_id)) }
72
+ return [] if response.status == 404
73
+
74
+ handle_response(response)
75
+ end
76
+
77
+ def delete_branch(project_id, branch_id)
78
+ response = with_connection_error_handling(@server_url) { connection.delete(branch_url(project_id, branch_id)) }
79
+ return false if response.status == 404
80
+
81
+ handle_response(response)
82
+ true
83
+ end
84
+
68
85
  def create_project(name, uid, description: nil, git: nil, production_branch: "main")
69
86
  url = "#{@server_url}/api/v1/projects"
70
87
  response = with_connection_error_handling(@server_url) do
@@ -96,73 +113,16 @@ module Strata
96
113
  end
97
114
  end
98
115
 
99
- def handle_response(response)
100
- case response.status
101
- when 200..299
102
- response.body
103
- when 401
104
- raise Strata::CommandError, "Authentication failed. Check your API key."
105
- when 403
106
- raise Strata::CommandError, "Access denied. You don't have permission for this action."
107
- when 404
108
- raise Strata::CommandError, "Resource not found."
109
- when 422
110
- errors = extract_errors(response.body)
111
- raise Strata::CommandError, "Validation failed: #{errors}"
112
- when 500..599
113
- raise Strata::CommandError, "Server error: #{response.status}"
114
- else
115
- raise Strata::CommandError, "Unexpected response: #{response.status}"
116
- end
117
- rescue Faraday::Error => e
118
- raise Strata::CommandError, "Network error: #{e.message}"
116
+ def deployment_url(project_id, branch_id)
117
+ "#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}/deploys"
119
118
  end
120
119
 
121
- def extract_errors(body)
122
- # Handle nil or empty body
123
- return "Unknown error" if body.nil? || body.empty?
124
-
125
- # Check if it's an HTML error page (Rails error page)
126
- if body.is_a?(String) && body.include?("<!DOCTYPE html>")
127
- # Try to extract error message from HTML
128
- if body =~ %r{<h1[^>]*>(.*?)</h1>}
129
- return "Server error: #{::Regexp.last_match(1).strip}"
130
- elsif body =~ %r{<title[^>]*>(.*?)</title>}
131
- return "Server error: #{::Regexp.last_match(1).strip}"
132
- else
133
- return "Server returned an HTML error page. Check server logs for details."
134
- end
135
- end
136
-
137
- # Parse JSON string if needed
138
- parsed_body = if body.is_a?(String)
139
- begin
140
- JSON.parse(body)
141
- rescue JSON::ParserError
142
- return body[0..200] # Return first 200 chars if not JSON
143
- end
144
- else
145
- body
146
- end
147
-
148
- # Extract error messages from various formats
149
- if parsed_body.is_a?(Hash)
150
- if parsed_body["errors"].is_a?(Array)
151
- parsed_body["errors"].join(", ")
152
- elsif parsed_body["error"].is_a?(String)
153
- parsed_body["error"]
154
- elsif parsed_body["message"].is_a?(String)
155
- parsed_body["message"]
156
- else
157
- parsed_body.to_json
158
- end
159
- else
160
- parsed_body.to_s[0..200] # Limit length
161
- end
120
+ def branches_url(project_id)
121
+ "#{@server_url}/api/v1/projects/#{project_id}/branches"
162
122
  end
163
123
 
164
- def deployment_url(project_id, branch_id)
165
- "#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}/deploys"
124
+ def branch_url(project_id, branch_id)
125
+ "#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}"
166
126
  end
167
127
  end
168
128
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Strata
7
+ module CLI
8
+ module API
9
+ module ResponseErrorHandler
10
+ DETAIL_LIMIT = 200
11
+
12
+ private
13
+
14
+ def handle_response(response)
15
+ case response.status
16
+ when 200..299
17
+ response.body
18
+ else
19
+ raise Strata::CommandError, response_error_message(response)
20
+ end
21
+ rescue Faraday::Error => e
22
+ raise Strata::CommandError, "Network error: #{e.message}"
23
+ end
24
+
25
+ def response_error_message(response, default_message: nil)
26
+ status = response.status
27
+ return "Server error: #{status}" if (500..599).cover?(status)
28
+
29
+ message = default_message || base_response_error_message(status)
30
+ details = extract_error_details(response.body)
31
+ return message unless details
32
+
33
+ return "#{message}: #{details}" if status == 422
34
+
35
+ "#{message} Details: #{details}"
36
+ end
37
+
38
+ def base_response_error_message(status)
39
+ case status
40
+ when 401
41
+ "Authentication failed. Check your API key."
42
+ when 403
43
+ "Access denied. You don't have permission for this action."
44
+ when 404
45
+ "Resource not found."
46
+ when 422
47
+ "Validation failed"
48
+ else
49
+ "Request failed with status #{status}."
50
+ end
51
+ end
52
+
53
+ def extract_error_details(body)
54
+ return nil if body.nil? || (body.respond_to?(:empty?) && body.empty?)
55
+
56
+ return extract_html_error_details(body) if body.is_a?(String) && body.include?("<!DOCTYPE html>")
57
+
58
+ parsed_body = parse_error_body(body)
59
+ details = details_from_parsed_body(parsed_body)
60
+ truncate_error_detail(details)
61
+ end
62
+
63
+ def extract_html_error_details(body)
64
+ detail = if body =~ %r{<h1[^>]*>(.*?)</h1>}m
65
+ ::Regexp.last_match(1)
66
+ elsif body =~ %r{<title[^>]*>(.*?)</title>}m
67
+ ::Regexp.last_match(1)
68
+ else
69
+ "Server returned an HTML error page."
70
+ end
71
+
72
+ truncate_error_detail(detail)
73
+ end
74
+
75
+ def parse_error_body(body)
76
+ return body unless body.is_a?(String)
77
+
78
+ JSON.parse(body)
79
+ rescue JSON::ParserError
80
+ body
81
+ end
82
+
83
+ def details_from_parsed_body(parsed_body)
84
+ case parsed_body
85
+ when Hash
86
+ parsed_body = parsed_body.transform_keys(&:to_s)
87
+
88
+ if parsed_body["errors"].is_a?(Array)
89
+ parsed_body["errors"].join(", ")
90
+ elsif parsed_body["errors"].is_a?(Hash)
91
+ parsed_body["errors"].flat_map do |field, messages|
92
+ Array(messages).map { |message| "#{field} #{message}" }
93
+ end.join(", ")
94
+ elsif parsed_body["error"].is_a?(String)
95
+ parsed_body["error"]
96
+ elsif parsed_body["message"].is_a?(String)
97
+ parsed_body["message"]
98
+ else
99
+ parsed_body.to_json
100
+ end
101
+ else
102
+ parsed_body.to_s
103
+ end
104
+ end
105
+
106
+ def truncate_error_detail(detail)
107
+ detail = detail.to_s.gsub(/\s+/, " ").strip
108
+ return nil if detail.empty?
109
+
110
+ (detail.length > DETAIL_LIMIT) ? "#{detail[0...DETAIL_LIMIT]}..." : detail
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -16,8 +16,9 @@ module Strata
16
16
 
17
17
  attr_reader :adapter, :credentials
18
18
 
19
- def initialize(adapter)
19
+ def initialize(adapter, datasource_config: nil)
20
20
  @adapter = adapter.downcase.strip
21
+ @datasource_config = datasource_config || {}
21
22
  @prompt = TTY::Prompt.new
22
23
  end
23
24
 
@@ -30,7 +31,7 @@ module Strata
30
31
 
31
32
  case adapter
32
33
  when "snowflake"
33
- auth_mode = @prompt.select("Authentication mode:", %w[pat kp oauth], default: "pat")
34
+ auth_mode = @prompt.select("Authentication mode:", %w[pat kp], default: "pat")
34
35
  credentials["auth_mode"] = auth_mode
35
36
 
36
37
  case auth_mode
@@ -39,18 +40,13 @@ module Strata
39
40
  when "kp"
40
41
  credentials["username"] = @prompt.ask("Enter Username:")
41
42
  credentials["private_key"] = @prompt.ask("Enter Private Key Absolute Path:")
42
- when "oauth"
43
- credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
44
- credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
45
- credentials["oauth_redirect_uri"] = @prompt.ask("OAuth Redirect URI:", default: "https://localhost:3420/callback")
46
- oauth_scope = @prompt.ask("OAuth Scope (optional):")
47
- credentials["oauth_scope"] = oauth_scope unless oauth_scope.empty?
48
43
  end
49
44
  when "athena"
50
45
  credentials["access_key_id"] = @prompt.ask("AWS Access Key ID:")
51
46
  credentials["secret_access_key"] = @prompt.ask("AWS Secret Access Key:")
52
47
  when "databricks"
53
- credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID (service principal):")
48
+ credentials["auth_mode"] = "oauth_m2m"
49
+ credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
54
50
  credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
55
51
  else
56
52
  if required?
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "time"
5
+ require_relative "output"
5
6
 
6
7
  module Strata
7
8
  module CLI
@@ -63,7 +64,9 @@ module Strata
63
64
  f.puts("")
64
65
  end
65
66
  rescue => e
66
- warn "Failed to write CLI log: #{e.message}" if ENV["DEBUG"]
67
+ if ENV["DEBUG"]
68
+ Output.print_warning("Failed to write CLI log: #{e.message}")
69
+ end
67
70
  end
68
71
  end
69
72
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "group"
4
4
  require "open3"
5
+ require_relative "../output"
5
6
 
6
7
  module Strata
7
8
  module CLI
@@ -24,7 +25,7 @@ module Strata
24
25
 
25
26
  def add_datasource_config
26
27
  @ds_key = get_unique_ds_key
27
- say_status :adapter, "adding #{adapter} config to datasources", :yellow
28
+ Output.print_status(:adapter, "adding #{adapter} config to datasources", type: :warning, context: self)
28
29
 
29
30
  # Interactive mode: write config directly from prompts
30
31
  config_yaml = {@ds_key => options[:config]}.to_yaml.sub(/^---\n/, "\n")
@@ -44,12 +45,12 @@ module Strata
44
45
  end
45
46
 
46
47
  def install_duckdb_gem
47
- say_status :gem, "Installing duckdb gem...", :yellow
48
+ Output.print_status(:gem, "Installing duckdb gem...", type: :warning, context: self)
48
49
 
49
50
  begin
50
51
  # Install the duckdb gem
51
52
  run "gem install duckdb", verbose: false, capture: true
52
- say_status :success, "duckdb gem installed successfully", :green
53
+ Output.print_status(:success, "duckdb gem installed successfully", type: :success, context: self)
53
54
  rescue => e
54
55
  raise DWH::ConfigError, "Failed to install duckdb gem: #{e.message}"
55
56
  end
@@ -1,10 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../output"
4
+
3
5
  module Strata
4
6
  module CLI
5
7
  module Generators
6
8
  class Group < Thor::Group
7
9
  include Thor::Actions
10
+ include Output
11
+
12
+ class QuietActionsShell < Thor::Shell::Color
13
+ DEFAULT_ALLOWED_STATUSES = %w[
14
+ created
15
+ setup
16
+ clone
17
+ success
18
+ adapter
19
+ gem
20
+ ].freeze
21
+
22
+ def initialize(verbose: false, allowed_statuses: DEFAULT_ALLOWED_STATUSES)
23
+ super()
24
+ @verbose = verbose
25
+ @allowed_statuses = allowed_statuses
26
+ end
27
+
28
+ def say_status(status, message, log_status = true)
29
+ return super if @verbose
30
+ return if log_status == false
31
+
32
+ s = status.to_s
33
+ return super if @allowed_statuses.include?(s)
34
+
35
+ nil
36
+ end
37
+ end
38
+
39
+ no_commands do
40
+ def initialize(*args)
41
+ super
42
+ @shell = QuietActionsShell.new(verbose: options[:verbose] == true)
43
+ end
44
+ end
8
45
 
9
46
  def self.source_root
10
47
  File.expand_path("templates/", __dir__)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "group"
4
+ require_relative "../output"
4
5
 
5
6
  module Strata
6
7
  module CLI
@@ -28,7 +29,7 @@ module Strata
28
29
  # Write the updated template
29
30
  create_file output_path, updated_content
30
31
 
31
- say_status :created, output_path, :green
32
+ Output.print_status(:created, output_path, type: :success, context: self)
32
33
  end
33
34
 
34
35
  private