strata-cli 0.1.8 → 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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/strata/cli/ai/services/table_generator.rb +35 -20
  4. data/lib/strata/cli/api/client.rb +23 -63
  5. data/lib/strata/cli/api/response_error_handler.rb +115 -0
  6. data/lib/strata/cli/error_reporter.rb +4 -1
  7. data/lib/strata/cli/generators/datasource.rb +4 -3
  8. data/lib/strata/cli/generators/group.rb +37 -0
  9. data/lib/strata/cli/generators/migration.rb +2 -1
  10. data/lib/strata/cli/generators/project.rb +18 -11
  11. data/lib/strata/cli/generators/relation.rb +2 -1
  12. data/lib/strata/cli/generators/table.rb +5 -8
  13. data/lib/strata/cli/generators/templates/table.table_name.yml +1 -1
  14. data/lib/strata/cli/generators/test.rb +2 -1
  15. data/lib/strata/cli/guard.rb +4 -1
  16. data/lib/strata/cli/helpers/command_context.rb +8 -9
  17. data/lib/strata/cli/helpers/datasource_helper.rb +1 -1
  18. data/lib/strata/cli/helpers/description_helper.rb +2 -1
  19. data/lib/strata/cli/main.rb +15 -3
  20. data/lib/strata/cli/output.rb +103 -0
  21. data/lib/strata/cli/sub_commands/audit.rb +4 -3
  22. data/lib/strata/cli/sub_commands/branch.rb +163 -0
  23. data/lib/strata/cli/sub_commands/create.rb +1 -2
  24. data/lib/strata/cli/sub_commands/datasource.rb +2 -0
  25. data/lib/strata/cli/sub_commands/deploy.rb +14 -13
  26. data/lib/strata/cli/sub_commands/project.rb +4 -3
  27. data/lib/strata/cli/sub_commands/table.rb +9 -8
  28. data/lib/strata/cli/terminal.rb +7 -4
  29. data/lib/strata/cli/ui/field_editor.rb +21 -27
  30. data/lib/strata/cli/utils/deployment_monitor.rb +15 -34
  31. data/lib/strata/cli/utils/git.rb +78 -0
  32. data/lib/strata/cli/utils/import_manager.rb +4 -1
  33. data/lib/strata/cli/utils/test_reporter.rb +4 -32
  34. data/lib/strata/cli/utils/version_checker.rb +4 -8
  35. data/lib/strata/cli/utils.rb +3 -1
  36. data/lib/strata/cli/version.rb +1 -1
  37. data/lib/strata/cli.rb +4 -3
  38. metadata +4 -2
  39. 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: e87ebe6383ea4dea3c2b0a57d47bc8a70cdbad507bcb9233614fb33161f5e71a
4
- data.tar.gz: c343df7299ff6959a73e31e68ca3cc959c5e9596dceb671be4d25868c9c04a5e
3
+ metadata.gz: 31a0327b1b8df2f7a982ece6c882cf019322cdf6e649b8cafdf035132424a705
4
+ data.tar.gz: 16dbe764a144c7098574900d54fdad599727a65dbfc82fef531b6f63a59e39a7
5
5
  SHA512:
6
- metadata.gz: 1629834279f085c6ef45b74f057f732e722fea8567a98d3ac48e2ae46ce51e0aff46219f41e392940198b5507a8c22d17b20851724c61136ba29d3837f8647b1
7
- data.tar.gz: 7b37420ff55bdb3910e702e1cb349741d4aa629a67e14b8bb3651189d2d840916a92ac0dbf7e54473ebcc82c71191d3af702a4eb958252a03f97c3919510fd70
6
+ metadata.gz: 2fa33f77c3082a06a9a7e3e52cbeebd5b12ed4195b621011380ed2c39d124f3ebcd28b1962f8cf1565a999e101c8df7db15ea864dafed4d778211272a5c3f250
7
+ data.tar.gz: b1b1250691596a599af3b3193220242fe36f50eb4c7cab83eaac18d3a59795979b4b861c11799a65588e6a077594b2ac396e45a54c4aa216421aaf6846896146
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## [0.1.8] - 2026-04-28
4
21
 
5
22
  ### Changed
@@ -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
@@ -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
@@ -4,6 +4,8 @@ require_relative "group"
4
4
  require_relative "datasource"
5
5
  require_relative "../helpers/project_helper"
6
6
  require_relative "../api/connection_error_handler"
7
+ require_relative "../api/response_error_handler"
8
+ require_relative "../output"
7
9
  require "faraday"
8
10
  require "json"
9
11
  require "uri"
@@ -13,6 +15,7 @@ module Strata
13
15
  module Generators
14
16
  class Project < Group
15
17
  include API::ConnectionErrorHandler
18
+ include API::ResponseErrorHandler
16
19
 
17
20
  BASE_SERVER_URL = "http://localhost:3000"
18
21
 
@@ -20,6 +23,7 @@ module Strata
20
23
  class_option :datasource, type: :string, repeatable: true
21
24
  class_option :source, type: :string
22
25
  class_option :api_key, type: :string
26
+ class_option :verbose, type: :boolean, default: false, desc: "Show detailed init output (file-by-file actions)."
23
27
 
24
28
  desc "Generates a new Strata project."
25
29
 
@@ -111,8 +115,8 @@ module Strata
111
115
  return if cloned_from_git?
112
116
  return if options.key?(:datasource) # Already specified via CLI option
113
117
 
114
- say "\n", :white
115
- say_status :setup, "Let's configure your first datasource", :cyan
118
+ print_status(:setup, "Let's configure your first datasource", type: :info)
119
+ print_info("\n")
116
120
 
117
121
  # Change into the project directory and run the existing add command
118
122
  inside(uid) do
@@ -122,12 +126,12 @@ module Strata
122
126
  end
123
127
 
124
128
  def completion_message
125
- say "\n✔ Strata project '#{uid}' is ready!", :green
126
- say "\nNext steps:", :yellow
127
- say " 1. cd #{uid}", :cyan
128
- say " 2. strata datasource add # To add more datasources", :cyan
129
- say " 3. strata create table # Start adding tables", :cyan
130
- say "\n"
129
+ Output.print_success("\n✔ Strata project '#{uid}' is ready!", context: self)
130
+ Output.print_warning("\nNext steps:", context: self)
131
+ Output.print_info(" 1. cd #{uid}", context: self)
132
+ Output.print_info(" 2. strata datasource add # To add more datasources", context: self)
133
+ Output.print_info(" 3. strata create table # Start adding tables", context: self)
134
+ Output.print_info("\n", context: self)
131
135
  end
132
136
 
133
137
  private
@@ -148,19 +152,22 @@ module Strata
148
152
 
149
153
  unless response.success?
150
154
  raise Strata::CommandError,
151
- "Failed to fetch project info from #{options[:source]}. Status: #{response.status}"
155
+ response_error_message(
156
+ response,
157
+ default_message: "Failed to fetch project info from #{options[:source]}. Status: #{response.status}."
158
+ )
152
159
  end
153
160
 
154
161
  response.body
155
162
  end
156
163
 
157
164
  def clone_project(git_url)
158
- say_status :clone, "Cloning project from #{git_url}", :green
165
+ Output.print_status(:clone, "Cloning project from #{git_url}", type: :success, context: self)
159
166
  run "git clone #{git_url} #{uid}", verbose: false, capture: true
160
167
 
161
168
  raise Strata::CommandError, "Failed to clone repository from #{git_url}" unless File.directory?(uid)
162
169
 
163
- say_status :success, "Project cloned successfully", :green
170
+ Output.print_status(:success, "Project cloned successfully", type: :success, context: self)
164
171
  end
165
172
 
166
173
  def persist_project_id_to_project_yml
@@ -3,6 +3,7 @@
3
3
  require_relative "group"
4
4
  require "yaml"
5
5
  require "fileutils"
6
+ require_relative "../output"
6
7
 
7
8
  module Strata
8
9
  module CLI
@@ -26,7 +27,7 @@ module Strata
26
27
  # Write the updated template
27
28
  create_file output_path, updated_content
28
29
 
29
- say_status :created, output_path, :green
30
+ Output.print_status(:created, output_path, type: :success, context: self)
30
31
  end
31
32
 
32
33
  private
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "group"
4
4
  require "yaml"
5
+ require_relative "../output"
5
6
 
6
7
  module Strata
7
8
  module CLI
@@ -43,7 +44,7 @@ module Strata
43
44
  # Write the updated template
44
45
  create_file output_path, updated_content
45
46
 
46
- say_status :created, output_path, :green
47
+ Output.print_status(:created, output_path, type: :success, context: self)
47
48
  end
48
49
 
49
50
  private
@@ -101,8 +102,9 @@ module Strata
101
102
  # Normalize hash keys to symbols for consistent access
102
103
  normalized = normalize_field_hash(field)
103
104
 
105
+ field_type = normalized[:schema_type] || "dimension"
104
106
  field_hash = {
105
- "type" => normalized[:schema_type] || "dimension",
107
+ "type" => field_type,
106
108
  "name" => normalized[:name],
107
109
  "description" => normalized[:description],
108
110
  "data_type" => normalized[:data_type]
@@ -112,12 +114,7 @@ module Strata
112
114
  field_hash["synonyms"] = synonyms if synonyms.is_a?(Array) && !synonyms.empty?
113
115
 
114
116
  # Build expression with proper nested format
115
- expr = normalized[:expression]
116
- field_hash["expression"] = {
117
- "lookup" => true,
118
- "sql" => expr
119
- }
120
-
117
+ field_hash["expression"] = {"sql" => normalized[:expression], "lookup" => field_type.to_s.casecmp("dimension").zero?}
121
118
  field_hash
122
119
  end
123
120
 
@@ -92,7 +92,7 @@ fields:
92
92
  # # Required: Defines how this field will query this table
93
93
  # expresssion:
94
94
  # primary_key: true|false (optional)
95
- # lookup: true|false (optional)
95
+ # lookup: true|false (optional, dimensions only)
96
96
  # array: true|false (optional)
97
97
  # sql: my_field_column (Required)
98
98
  #