strata-cli 0.1.8 → 0.1.10
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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +26 -2
- data/lib/strata/cli/agent_mode.rb +26 -0
- data/lib/strata/cli/agent_output.rb +21 -0
- data/lib/strata/cli/ai/services/table_generator.rb +35 -20
- data/lib/strata/cli/api/client.rb +23 -63
- data/lib/strata/cli/api/response_error_handler.rb +115 -0
- data/lib/strata/cli/error_reporter.rb +4 -1
- data/lib/strata/cli/generators/datasource.rb +4 -3
- data/lib/strata/cli/generators/group.rb +37 -0
- data/lib/strata/cli/generators/migration.rb +2 -1
- data/lib/strata/cli/generators/project.rb +18 -11
- data/lib/strata/cli/generators/relation.rb +2 -1
- data/lib/strata/cli/generators/table.rb +5 -8
- data/lib/strata/cli/generators/templates/AGENTS.md +457 -88
- data/lib/strata/cli/generators/templates/table.table_name.yml +8 -3
- data/lib/strata/cli/generators/test.rb +2 -1
- data/lib/strata/cli/guard.rb +4 -1
- data/lib/strata/cli/helpers/command_context.rb +8 -9
- data/lib/strata/cli/helpers/datasource_helper.rb +27 -1
- data/lib/strata/cli/helpers/description_helper.rb +2 -1
- data/lib/strata/cli/main.rb +12 -3
- data/lib/strata/cli/output.rb +103 -0
- data/lib/strata/cli/sub_commands/audit.rb +136 -16
- data/lib/strata/cli/sub_commands/branch.rb +165 -0
- data/lib/strata/cli/sub_commands/create.rb +13 -2
- data/lib/strata/cli/sub_commands/datasource.rb +21 -3
- data/lib/strata/cli/sub_commands/deploy.rb +16 -13
- data/lib/strata/cli/sub_commands/project.rb +6 -3
- data/lib/strata/cli/sub_commands/table.rb +11 -8
- data/lib/strata/cli/terminal.rb +7 -4
- data/lib/strata/cli/ui/field_editor.rb +21 -27
- data/lib/strata/cli/utils/deployment_monitor.rb +15 -34
- data/lib/strata/cli/utils/git.rb +78 -0
- data/lib/strata/cli/utils/import_manager.rb +4 -1
- data/lib/strata/cli/utils/test_reporter.rb +4 -32
- data/lib/strata/cli/utils/version_checker.rb +4 -8
- data/lib/strata/cli/utils.rb +3 -1
- data/lib/strata/cli/version.rb +1 -1
- data/lib/strata/cli.rb +4 -3
- metadata +6 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4995c9ab5a77b99effe743f6518204ffe8c2417e06f2f3acac57efe5f82cd041
|
|
4
|
+
data.tar.gz: 89c9a32ddf48ac1e4e68a92aa0f6f8acf6f37fbc9267a0134fddc3a34a859ed0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca85facb30bebc4b5ec05ca79b15e3e94434fa10122400baadab6b923700fa4117b39b5cc06fddb1ccbf791ee1d772d755e1618159a5957d761ac1851ead083f
|
|
7
|
+
data.tar.gz: 07e20501e723d426b03044996bd2a20748e4bd322fa71e43f6c193be8d0516a49672e0ed7acd94f81a451cc99c0c8b48b087b02af8f334b10e50157518df520e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.10] - 2026-05-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Agent mode (`--agent`)**: Structured JSON output for automation and agent workflows on read-only commands (`datasource tables`, `datasource meta`, `version`, and others). Interactive `create` flows return machine-readable errors with stable codes when agent mode is requested.
|
|
8
|
+
- **Audit field validation**: Model audit checks allowed field keys, expression shape (string shortcut or mapping with `sql`), and `format` types (string shortcuts and hash forms).
|
|
9
|
+
- **Audit import resolution**: Model checks resolve YAML imports via `YamlImportResolver` before validating table and relationship definitions.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **AGENTS.md template**: Updated project init template with semantic-model authoring guidance and `--agent` command usage.
|
|
14
|
+
|
|
15
|
+
## [0.1.9] - 2026-05-13
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Terminal output**: Centralized printing through `Output`, reduced verbose logs from deployment monitoring and test reporting; removed `ColorHelper` in favor of shared output helpers.
|
|
24
|
+
- **API client errors**: Added `ResponseErrorHandler` to centralize HTTP error handling and user-facing messages across the client and generators.
|
|
25
|
+
- **AI table generation**: Updated `SYSTEM_PROMPT` for YAML model / field generation.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`list` command**: Correctly includes nested models in listings.
|
|
30
|
+
- **Field editor**: Dimension lookup flags apply only to dimensions.
|
|
31
|
+
|
|
3
32
|
## [0.1.8] - 2026-04-28
|
|
4
33
|
|
|
5
34
|
### Changed
|
data/README.md
CHANGED
|
@@ -25,6 +25,30 @@ gem install strata-cli
|
|
|
25
25
|
|
|
26
26
|
> 💡 **Tip:** Run `strata COMMAND --help` for detailed help, options, and examples on any command. The CLI help system provides comprehensive documentation for each command.
|
|
27
27
|
|
|
28
|
+
## Agent mode (`--agent`)
|
|
29
|
+
|
|
30
|
+
Pass `--agent` on any command for structured JSON output and no interactive prompts (where the command supports it).
|
|
31
|
+
|
|
32
|
+
**Supported examples:**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
strata datasource list --agent
|
|
36
|
+
strata datasource tables my_db --agent
|
|
37
|
+
strata datasource meta my_db orders --agent
|
|
38
|
+
strata table list --agent
|
|
39
|
+
strata audit all --agent
|
|
40
|
+
strata deploy --yes --agent
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Not supported** — these commands are interactive generators; write YAML directly instead:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
strata create table [TABLE_PATH] # → models/tbl.*.yml
|
|
47
|
+
strata create relation RELATION_PATH # → models/rel.*.yml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `strata datasource meta DS_KEY TABLE_NAME --agent` for column metadata when authoring table models.
|
|
51
|
+
|
|
28
52
|
<details>
|
|
29
53
|
<summary><strong>Project Management</strong></summary>
|
|
30
54
|
|
|
@@ -127,7 +151,7 @@ strata datasource exec my_db -f queries/analysis.sql
|
|
|
127
151
|
<summary><strong>Model Creation</strong></summary>
|
|
128
152
|
|
|
129
153
|
### `strata create table [TABLE_PATH]`
|
|
130
|
-
Create a semantic table model from a datasource table.
|
|
154
|
+
Create a semantic table model from a datasource table. **Not available with `--agent`** — write `models/tbl.*.yml` directly or use `strata datasource meta` for schema metadata.
|
|
131
155
|
|
|
132
156
|
**Options:** `-d, --datasource DS_KEY`
|
|
133
157
|
|
|
@@ -142,7 +166,7 @@ strata create table contact/dse.call_center_d # With schema
|
|
|
142
166
|
**Process:** Checks table existence → Fetches metadata → AI suggests fields → Interactive editor → Generates model file
|
|
143
167
|
|
|
144
168
|
### `strata create relation RELATION_PATH`
|
|
145
|
-
Create a relation (join) definition file.
|
|
169
|
+
Create a relation (join) definition file. **Not available with `--agent`** — write `models/rel.*.yml` directly.
|
|
146
170
|
|
|
147
171
|
**Options:** `-d, --datasource DS_KEY`
|
|
148
172
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agent_output"
|
|
4
|
+
|
|
5
|
+
module Strata
|
|
6
|
+
module CLI
|
|
7
|
+
module AgentMode
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.class_option :agent, type: :boolean, default: false,
|
|
10
|
+
desc: "Agent mode: structured output, no interactive prompts"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def agent_mode?
|
|
14
|
+
value = options[:agent]
|
|
15
|
+
value = options["agent"] if value.nil?
|
|
16
|
+
value == true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reject_agent_mode!(message, code: "agent_mode_unsupported")
|
|
20
|
+
return unless agent_mode?
|
|
21
|
+
|
|
22
|
+
AgentOutput.emit_error(message, code: code)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Strata
|
|
6
|
+
module CLI
|
|
7
|
+
module AgentOutput
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def emit_json(data)
|
|
11
|
+
$stdout.puts(JSON.generate(data))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def emit_error(message, code: "agent_mode_unsupported", **metadata)
|
|
15
|
+
payload = {error: message, code: code}.merge(metadata)
|
|
16
|
+
warn(JSON.generate(payload))
|
|
17
|
+
exit(1)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -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.
|
|
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.
|
|
20
|
-
potential dimension or measure is the same as an existing one. You can
|
|
21
|
-
column name,
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
|
|
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
|
|
122
|
-
#
|
|
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
|
|
165
|
-
"#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
Output.print_status(:created, output_path, type: :success, context: self)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
private
|