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
@@ -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
 
@@ -33,10 +33,11 @@
33
33
  # Optional: default schema within that catalog (often "default")
34
34
  schema: default
35
35
 
36
- # OAuth M2M (service principal) is implicit for this adapter in dwh.
37
- # Do not set auth_mode here.
36
+ # Required: Authentication mode
37
+ # oauth_m2m: service principal (client_credentials)
38
+ auth_mode: oauth_m2m
38
39
  #
39
- # Set credentials securely (oauth_client_id + oauth_client_secret) via:
40
+ # Set credentials securely via:
40
41
  # `strata ds auth <%= @ds_key %>`
41
42
  # or they are collected when you run `strata datasource add databricks`.
42
43
  #
@@ -33,15 +33,9 @@
33
33
  # Optional: Role name
34
34
  role: ACCOUNTADMIN
35
35
 
36
- # Required: Authentication mode - one of: pat, kp, oauth
36
+ # Required: Authentication mode - one of: pat, kp
37
37
  # pat: personal_access_token
38
38
  # kp: key pair (requires username)
39
- # oauth: OAuth (requires setup on snowflake to enable)
40
- # Additional required params
41
- # oauth_client_id: <MYCLIENTID>
42
- # oauth_cleint_secret: <MYCLIENTSECRET>
43
- # oauth_redirect_uri: https://localhost:3420/callback
44
- # oauth_scope: <SCOPE> # optional
45
39
  auth_mode: pat
46
40
 
47
41
  # For Personal Access Token (PAT) authentication:
@@ -53,17 +47,3 @@
53
47
  # For Key Pair (KP) authentication, uncomment and set:
54
48
  # username: john_doe
55
49
  # private_key: /path/to/private_key.pem
56
-
57
- # For OAuth authentication, additional setup required.
58
- # https://docs.snowflake.com/en/user-guide/oauth-custom
59
- #
60
- # Securely save tokens in .strata by running:
61
- # `strata ds auth <%= @ds_key %>`
62
- #
63
- # This would only work if your snowflake setup allows modification
64
- # of the redirect url. Otherwise, you can get access_token and
65
- # refresh_token by other means. Then add it to .strata like so:
66
- #
67
- # access_token: wrwerjwrljwer
68
- # refresh_token: erwerewrwer
69
- # expires_at: <%= Time.now %>
@@ -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
  #
@@ -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
@@ -23,7 +24,7 @@ module Strata
23
24
  # Write the updated template
24
25
  create_file output_path, updated_content
25
26
 
26
- say_status :created, output_path, :green
27
+ Output.print_status(:created, output_path, type: :success, context: self)
27
28
  end
28
29
 
29
30
  private
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "utils"
4
+ require_relative "output"
5
+
4
6
  module Strata
5
7
  module CLI
6
8
  module Guard
@@ -10,13 +12,14 @@ module Strata
10
12
  adapters
11
13
  version
12
14
  deploy
15
+ branch
13
16
  ].freeze
14
17
 
15
18
  def invoke_command(command, *args)
16
19
  Utils.exit_error_if_not_strata! unless ALLOWED_COMMANDS.include?(command.name)
17
20
  super
18
21
  rescue Strata::CommandError => e
19
- shell.say_error "ERROR: #{e.message}", :red
22
+ Output.print_error("ERROR: #{e.message}", context: self)
20
23
  exit 1
21
24
  end
22
25
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require_relative "datasource_helper"
5
- require_relative "color_helper"
5
+ require_relative "../output"
6
6
  require_relative "prompts"
7
7
  require_relative "../error_reporter"
8
8
 
@@ -10,6 +10,7 @@ module Strata
10
10
  module CLI
11
11
  module Helpers
12
12
  module CommandContext
13
+ include Output
13
14
  include DatasourceHelper
14
15
 
15
16
  def adapter
@@ -28,8 +29,7 @@ module Strata
28
29
  @table_fetch_result ||= begin
29
30
  tables = with_spinner("Fetching tables from #{datasource_key}...") { adapter.tables }
30
31
  if tables.empty?
31
- # Assuming Prompts and ColorHelper are available in the class or via module
32
- say Prompts::MSG_NO_TABLES_FOUND % datasource_key, ColorHelper.warning
32
+ say(Output.format(:warning, Prompts::MSG_NO_TABLES_FOUND % datasource_key), nil) if respond_to?(:say)
33
33
  {tables: [], failed: false}
34
34
  else
35
35
  {tables: tables, failed: false}
@@ -38,15 +38,14 @@ module Strata
38
38
  ErrorReporter.log_error(e, context: "create table: failed fetching tables for '#{datasource_key}'")
39
39
 
40
40
  if ErrorReporter.connection_error?(e)
41
- say "Could not connect to datasource '#{datasource_key}'.", ColorHelper.warning
41
+ say(Output.format(:warning, "Could not connect to datasource '#{datasource_key}'."), nil) if respond_to?(:say)
42
42
  else
43
- say "Could not fetch tables from datasource '#{datasource_key}'.", ColorHelper.warning
44
- say "Reason: #{ErrorReporter.user_message_for(e)}", ColorHelper.warning
43
+ say(Output.format(:warning, "Could not fetch tables from datasource '#{datasource_key}'."), nil) if respond_to?(:say)
44
+ say(Output.format(:warning, "Reason: #{ErrorReporter.user_message_for(e)}"), nil) if respond_to?(:say)
45
45
  end
46
46
 
47
- say "Hint: verify credentials/settings and run 'strata datasource test #{datasource_key}'.",
48
- ColorHelper.info
49
- say "Details logged to '#{ErrorReporter.log_relative_path}'.", ColorHelper.info
47
+ say(Output.format(:dim, "Hint: verify credentials/settings and run 'strata datasource test #{datasource_key}'."), nil) if respond_to?(:say)
48
+ say(Output.format(:info, "Details logged to '#{ErrorReporter.log_relative_path}'."), nil) if respond_to?(:say)
50
49
  {tables: [], failed: true}
51
50
  end
52
51
  end
@@ -8,7 +8,7 @@ module Strata
8
8
  redshift: %w[pg],
9
9
  mysql: %w[mysql2],
10
10
  sqlserver: %w[tiny_tds],
11
- athena: %w[aws-sdk-athena aws-sdk-s3],
11
+ athena: %w[aws-sdk-athena aws-sdk-s3 rexml],
12
12
  trino: %w[trino-client],
13
13
  sqlite: %w[sqlite3],
14
14
  duckdb: %w[duckdb]
@@ -11,7 +11,8 @@ module Strata
11
11
  if File.exist?(file_path)
12
12
  long_desc File.read(file_path)
13
13
  else
14
- warn "Warning: Description file not found at #{file_path}"
14
+ require_relative "../output"
15
+ Output.print_warning("Warning: Description file not found at #{file_path}")
15
16
  end
16
17
  end
17
18
  end
@@ -3,16 +3,19 @@
3
3
  require_relative "generators/project"
4
4
  require_relative "sub_commands/datasource"
5
5
  require_relative "sub_commands/deploy"
6
+ require_relative "sub_commands/branch"
6
7
  require_relative "sub_commands/project"
7
8
  require_relative "sub_commands/table"
8
9
  require_relative "sub_commands/create"
9
10
  require_relative "sub_commands/audit"
10
11
  require_relative "helpers/description_helper"
12
+ require_relative "output"
11
13
 
12
14
  module Strata
13
15
  module CLI
14
16
  class Main < Thor
15
17
  include Guard
18
+ include Output
16
19
  extend Helpers::DescriptionHelper
17
20
 
18
21
  def self.exit_on_failure?
@@ -30,21 +33,22 @@ module Strata
30
33
  repeatable: true
31
34
  option :source, aliases: ["s"], type: :string, desc: "URL of existing project"
32
35
  option :api_key, aliases: ["a"], type: :string, desc: "Api Key. Required if initializing existing project."
36
+ option :verbose, aliases: ["v"], type: :boolean, default: false,
37
+ desc: "Show detailed init output (file-by-file actions)."
33
38
 
34
39
  def init(project_name = nil)
35
40
  unless project_name || options[:source]
36
41
  raise Strata::CommandError, "PROJECT_NAME is required when not using --source option."
37
42
  end
38
43
 
39
- say_status :started, "Creating #{project_name || "project from source"} - #{options[:datasource]}",
40
- ColorHelper.info
44
+ print_status(:created, (project_name || "project from source").to_s, type: :info)
41
45
  invoke Generators::Project, [project_name], options
42
46
  end
43
47
 
44
48
  desc "adapters", "Lists supported data warehouse adapters"
45
49
  def adapters
46
50
  out = " SUPPORTED ADAPTERS: \n\t#{DWH.adapters.keys.join("\n\t")}"
47
- say out, ColorHelper.info
51
+ print_info(out)
48
52
  end
49
53
 
50
54
  desc "datasource", "Manage project datasources"
@@ -56,12 +60,20 @@ module Strata
56
60
  desc "table", "Manage semantic tables"
57
61
  subcommand "table", SubCommands::Table
58
62
 
63
+ desc "tables", "List all semantic models in the project"
64
+ def tables
65
+ SubCommands::Table.start(["list"])
66
+ end
67
+
59
68
  desc "audit", "Audit project configuration and models"
60
69
  subcommand "audit", SubCommands::Audit
61
70
 
62
71
  desc "deploy", "Deploy project to Strata server"
63
72
  subcommand "deploy", SubCommands::Deploy
64
73
 
74
+ desc "branch", "Manage Strata server branches"
75
+ subcommand "branch", SubCommands::Branch
76
+
65
77
  desc "project", "Manage project configuration"
66
78
  subcommand "project", SubCommands::Project
67
79
 
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "pastel"
5
+
6
+ module Strata
7
+ module CLI
8
+ module Output
9
+ THEME = {
10
+ success: :green,
11
+ error: %i[red bold],
12
+ warning: :yellow,
13
+ info: :cyan,
14
+ title: %i[cyan bold],
15
+ highlight: %i[cyan bold],
16
+ dim: :bright_black,
17
+ primary: :blue,
18
+ secondary: :magenta,
19
+ border: %i[cyan dim],
20
+ selected: %i[green bold],
21
+ disabled: :bright_black
22
+ }.freeze
23
+
24
+ class << self
25
+ def pastel
26
+ @pastel ||= Pastel.new(enabled: $stdout.tty?)
27
+ end
28
+
29
+ def shell_for(context = nil)
30
+ return context.shell if context&.respond_to?(:shell)
31
+ return context if context.is_a?(Thor::Shell)
32
+
33
+ Thor::Shell::Color.new
34
+ end
35
+
36
+ def print_info(message, context: nil)
37
+ shell_for(context).say(format(:info, message))
38
+ end
39
+
40
+ def print_success(message, context: nil)
41
+ shell_for(context).say(format(:success, message))
42
+ end
43
+
44
+ def print_warning(message, context: nil)
45
+ shell_for(context).say(format(:warning, message))
46
+ end
47
+
48
+ def print_error(message, context: nil)
49
+ shell_for(context).say_error(format(:error, message))
50
+ end
51
+
52
+ def print_hint(message, context: nil, stderr: false)
53
+ shell = shell_for(context)
54
+ formatted = format(:dim, message)
55
+ stderr ? shell.say_error(formatted) : shell.say(formatted)
56
+ end
57
+
58
+ # Standard status output with Thor's prefix formatting.
59
+ # We keep the label color via Thor, but decorate the message via Output theme.
60
+ def print_status(label, message, type: :info, context: nil)
61
+ shell = shell_for(context)
62
+ shell.say_status(label, format(type, message), thor_color(type))
63
+ end
64
+
65
+ def format(type, message)
66
+ return "" if message.nil?
67
+
68
+ styles = THEME[type]
69
+ return message.to_s unless styles
70
+
71
+ if styles.is_a?(Array)
72
+ pastel.decorate(message.to_s, *styles)
73
+ else
74
+ pastel.send(styles, message.to_s)
75
+ end
76
+ end
77
+
78
+ # Thor only understands a limited palette; keep it simple/stable.
79
+ def thor_color(type)
80
+ case type
81
+ when :success then :green
82
+ when :error then :red
83
+ when :warning then :yellow
84
+ when :info, :title, :border then :cyan
85
+ when :primary then :blue
86
+ when :secondary then :magenta
87
+ when :dim then :white
88
+ else
89
+ :white
90
+ end
91
+ end
92
+ end
93
+
94
+ # Convenience instance methods for Thor classes.
95
+ def print_info(message) = Output.print_info(message, context: self)
96
+ def print_success(message) = Output.print_success(message, context: self)
97
+ def print_warning(message) = Output.print_warning(message, context: self)
98
+ def print_error(message) = Output.print_error(message, context: self)
99
+ def print_hint(message, stderr: false) = Output.print_hint(message, context: self, stderr: stderr)
100
+ def print_status(label, message, type: :info) = Output.print_status(label, message, type: type, context: self)
101
+ end
102
+ end
103
+ end
@@ -3,7 +3,7 @@
3
3
  require_relative "../guard"
4
4
  require_relative "../terminal"
5
5
  require_relative "../credentials"
6
- require_relative "../helpers/color_helper"
6
+ require_relative "../output"
7
7
  require_relative "../helpers/datasource_helper"
8
8
  require_relative "../utils/yaml_import_resolver"
9
9
  require_relative "../utils/import_manager"
@@ -17,6 +17,7 @@ module Strata
17
17
  include Thor::Actions
18
18
  include Guard
19
19
  include Terminal
20
+ include Output
20
21
  include DatasourceHelper
21
22
 
22
23
  REQUIRED_KEYS_FOR_TABLE_MODEL = %w[name physical_name fields datasource].freeze
@@ -61,7 +62,7 @@ module Strata
61
62
  if results.values.all?
62
63
  # All checks passed - no need for additional message, spinners already show success
63
64
  else
64
- say "\n Some checks failed.", ColorHelper.error
65
+ print_error("\n Some checks failed.")
65
66
  exit(1)
66
67
  end
67
68
  end
@@ -76,7 +77,7 @@ module Strata
76
77
  rescue
77
78
  failures.each do |f|
78
79
  msg = f.is_a?(Hash) ? "#{f[:file]}: #{f[:message]}" : f.to_s
79
- say " ✖ #{msg}", ColorHelper.error
80
+ print_error(" ✖ #{msg}")
80
81
  end
81
82
  false
82
83
  end