luciq-cli 0.1.0 → 0.2.0
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 +17 -0
- data/README.md +212 -4
- data/lib/luciq/api/client.rb +159 -10
- data/lib/luciq/cli.rb +46 -17
- data/lib/luciq/commands/query.rb +228 -0
- data/lib/luciq/commands/query_arguments.rb +166 -0
- data/lib/luciq/commands/upload.rb +167 -12
- data/lib/luciq/config.rb +2 -1
- data/lib/luciq/query_cli.rb +579 -0
- data/lib/luciq/upload_cli.rb +188 -0
- data/lib/luciq/version.rb +1 -1
- metadata +8 -4
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'luciq/api/client'
|
|
5
|
+
require 'luciq/config'
|
|
6
|
+
require 'luciq/commands/query_arguments'
|
|
7
|
+
|
|
8
|
+
module Luciq
|
|
9
|
+
module Commands
|
|
10
|
+
# Read/query commands. Each public method maps one-to-one to an MCP tool:
|
|
11
|
+
# it builds that tool's arguments from typed CLI flags (see QueryArguments)
|
|
12
|
+
# and proxies to the MCP server's CLI endpoint.
|
|
13
|
+
class Query
|
|
14
|
+
include QueryArguments
|
|
15
|
+
|
|
16
|
+
def initialize(options = {})
|
|
17
|
+
@options = options
|
|
18
|
+
@client = API::Client.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# --- Crashes -----------------------------------------------------------
|
|
22
|
+
def crashes_list
|
|
23
|
+
execute('list_crashes') { base_args.merge(page_args, sort_args, filters_arg(crash_filters)) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def crash_show(number)
|
|
27
|
+
execute('crash_details') { base_args.merge(number: number) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def crash_patterns(number)
|
|
31
|
+
pattern = { pattern_key: @options[:pattern_key], sort_by: @options[:sort_by],
|
|
32
|
+
direction: @options[:direction] }.compact
|
|
33
|
+
execute('crash_patterns') do
|
|
34
|
+
base_args.merge(number: number).merge(pattern).merge(filters_arg(raw_filters))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def crash_diagnostics(number)
|
|
39
|
+
execute('crash_diagnostics') { base_args.merge(number: number) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def app_hangs
|
|
43
|
+
execute('list_app_hangs') { base_args.merge(page_args, sort_args, filters_arg(hang_filters)) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def occurrence_tokens(number)
|
|
47
|
+
cursor = { current_token: @options[:current_token], direction: @options[:direction] }.compact
|
|
48
|
+
execute('list_occurrences_tokens') do
|
|
49
|
+
base_args.merge(number: number).merge(cursor).merge(filters_arg(raw_filters))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def occurrence_details(number, ulid)
|
|
54
|
+
execute('get_occurrence_details') { base_args.merge(number: number, ulid: ulid) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --- Bugs --------------------------------------------------------------
|
|
58
|
+
def bugs_list
|
|
59
|
+
execute('list_bugs') { base_args.merge(page_args, sort_args, filters_arg(bug_filters)) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def bug_show(number)
|
|
63
|
+
execute('bug_details') { base_args.merge(number: number) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def bug_update(number)
|
|
67
|
+
execute('update_bug') { base_args.merge(number: number).merge(bug_update_changes) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# --- APM ---------------------------------------------------------------
|
|
71
|
+
def apm_groups
|
|
72
|
+
execute('apm_list_groups') do
|
|
73
|
+
base_args.merge({ metric: @options[:metric] }.compact, page_args)
|
|
74
|
+
.merge(apm_sort_arg, filters_arg(raw_filters))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apm_group
|
|
79
|
+
group = { metric: @options[:metric], group_uuid: @options[:group_uuid],
|
|
80
|
+
group_url: @options[:group_url], method: @options[:method] }.compact
|
|
81
|
+
views = @options[:views] ? json_arg(:views, @options[:views]) : { views: ['summary'] }
|
|
82
|
+
execute('apm_group_view') do
|
|
83
|
+
base_args.merge(group).merge(views, filters_arg(raw_filters))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apm_occurrence
|
|
88
|
+
selector = { metric: @options[:metric], selector: @options[:selector],
|
|
89
|
+
group_uuid: @options[:group_uuid], group_url: @options[:group_url],
|
|
90
|
+
method: @options[:method], token: @options[:token],
|
|
91
|
+
current_token: @options[:current_token],
|
|
92
|
+
direction: @options[:direction], limit: @options[:limit] }.compact
|
|
93
|
+
execute('apm_occurrence') { base_args.merge(selector).merge(filters_arg(raw_filters)) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def apm_funnel_events
|
|
97
|
+
params = { event_type: @options[:event_type], q: @options[:q], limit: @options[:limit] }.compact
|
|
98
|
+
execute('apm_funnel_events') { base_args.merge(params) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def apm_funnel_create
|
|
102
|
+
execute('apm_funnel_write') do
|
|
103
|
+
base_args.merge(operation: 'create', name: @options[:name]).merge(json_arg(:events, @options[:events]))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apm_funnel_update
|
|
108
|
+
changes = { ulid: @options[:ulid], name: @options[:name] }.compact
|
|
109
|
+
execute('apm_funnel_write') do
|
|
110
|
+
base_args.merge(operation: 'update').merge(changes, json_arg(:events, @options[:events]))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def apm_funnel_delete
|
|
115
|
+
execute('apm_funnel_write') { base_args.merge(operation: 'delete', ulid: @options[:ulid]) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# --- Reviews -----------------------------------------------------------
|
|
119
|
+
def reviews_list
|
|
120
|
+
execute('list_reviews') do
|
|
121
|
+
base_args.merge(page_args, sort_dir_args).merge(filters_arg(review_filters))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --- Surveys -----------------------------------------------------------
|
|
126
|
+
def surveys_list
|
|
127
|
+
execute('list_surveys') { base_args.merge(page_args, filters_arg(survey_filters)) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def survey_show(id)
|
|
131
|
+
page = { page: @options[:page] }.compact
|
|
132
|
+
execute('survey_details') { base_args.merge(id: id).merge(page, filters_arg(raw_filters)) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# --- Applications & health --------------------------------------------
|
|
136
|
+
def apps_list
|
|
137
|
+
execute('list_applications') { page_args.merge({ platform: @options[:platform] }.compact) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def insights
|
|
141
|
+
execute('app_insights') { base_args.merge(filters_arg(raw_filters)) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- Issues ------------------------------------------------------------
|
|
145
|
+
def issues_list
|
|
146
|
+
top = { top_issues: @options[:top_issues] }.compact
|
|
147
|
+
execute('list_issues') do
|
|
148
|
+
base_args.merge({ limit: @options[:limit] }.compact, sort_dir_args, top)
|
|
149
|
+
.merge(json_arg(:pagination, @options[:pagination]), filters_arg(raw_filters))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# --- Opportunities -----------------------------------------------------
|
|
154
|
+
def opportunities_list
|
|
155
|
+
execute('list_opportunities') { base_args.merge(page_args, filters_arg(opportunity_filters)) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def opportunity_show(id)
|
|
159
|
+
execute('opportunity_details') { base_args.merge(id: id) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- Alerts (rules) ----------------------------------------------------
|
|
163
|
+
def alerts_list
|
|
164
|
+
execute('read_alerts') { base_args.merge(action: 'list').merge(sort_dir_args) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def alert_show(ulid)
|
|
168
|
+
execute('read_alerts') { base_args.merge(action: 'details', ulid: ulid) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def alerts_init
|
|
172
|
+
execute('read_alerts') { base_args.merge(action: 'init') }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def alert_create
|
|
176
|
+
execute('write_alerts') { alert_payload.merge(base_args).merge(action: 'create') }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def alert_update(ulid)
|
|
180
|
+
execute('write_alerts') { alert_payload.merge(base_args).merge(action: 'update', ulid: ulid) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def alert_delete(ulid)
|
|
184
|
+
execute('write_alerts') { base_args.merge(action: 'delete', ulid: ulid) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# --- Incidents (triggered alerts) --------------------------------------
|
|
188
|
+
def incidents_list
|
|
189
|
+
execute('read_incidents') do
|
|
190
|
+
base_args.merge(action: 'list').merge(page_args, sort_dir_args, filters_arg(incident_filters))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def incident_show(ulid)
|
|
195
|
+
execute('read_incidents') { base_args.merge(action: 'details', ulid: ulid) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def incident_resolve(ulid)
|
|
199
|
+
execute('write_incidents') { base_args.merge(action: 'resolve', ulid: ulid) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def incident_reopen(ulid)
|
|
203
|
+
execute('write_incidents') { base_args.merge(action: 'reopen', ulid: ulid) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def execute(tool_name)
|
|
209
|
+
ensure_authenticated!
|
|
210
|
+
print_result(@client.invoke_tool(tool_name, yield))
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
puts "✗ #{e.message}"
|
|
213
|
+
exit 1
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def ensure_authenticated!
|
|
217
|
+
return if Config.load_token
|
|
218
|
+
|
|
219
|
+
puts '✗ Not authenticated. Run: luciq login'
|
|
220
|
+
exit 1
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def print_result(response)
|
|
224
|
+
puts(response.is_a?(String) ? response : JSON.pretty_generate(response))
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Luciq
|
|
6
|
+
module Commands
|
|
7
|
+
# Translates CLI flags (@options) into the argument/filter hashes the MCP
|
|
8
|
+
# tools expect. Mixed into Commands::Query; kept separate so the command
|
|
9
|
+
# dispatch stays thin and the flag→argument mapping is easy to test.
|
|
10
|
+
module QueryArguments
|
|
11
|
+
CRASH_STATUS_IDS = { 'open' => 1, 'closed' => 2, 'in_progress' => 3 }.freeze
|
|
12
|
+
BUG_STATUS_IDS = { 'new' => 1, 'closed' => 2, 'in_progress' => 3 }.freeze
|
|
13
|
+
BUG_PRIORITY_IDS = { 'na' => -1, 'trivial' => 1, 'minor' => 2, 'major' => 3, 'blocker' => 4 }.freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def base_args
|
|
18
|
+
{ slug: @options[:slug], mode: @options[:mode] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def page_args
|
|
22
|
+
{ offset: @options[:offset], limit: @options[:limit] }.compact
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sort_args
|
|
26
|
+
{ sort_by: @options[:sort_by], direction: @options[:direction] }.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sort_dir_args
|
|
30
|
+
{ sort_by: @options[:sort_by], sort_direction: @options[:sort_direction] }.compact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def apm_sort_arg
|
|
34
|
+
return {} unless @options[:sort]
|
|
35
|
+
|
|
36
|
+
{ sort: [parse_json(@options[:sort], '--sort')] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def crash_filters
|
|
40
|
+
f = raw_filters
|
|
41
|
+
ids = map_ids(CRASH_STATUS_IDS, @options[:status])
|
|
42
|
+
f['status_id'] = ids unless ids.empty?
|
|
43
|
+
f['platform'] = Array(@options[:platform]) if @options[:platform]
|
|
44
|
+
f['type'] = Array(@options[:type]) if @options[:type]
|
|
45
|
+
f['app_versions'] = Array(@options[:app_version]) if @options[:app_version]
|
|
46
|
+
f
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def hang_filters
|
|
50
|
+
f = raw_filters
|
|
51
|
+
ids = map_ids(CRASH_STATUS_IDS, @options[:status])
|
|
52
|
+
f['status_id'] = ids unless ids.empty?
|
|
53
|
+
f['platform'] = Array(@options[:platform]) if @options[:platform]
|
|
54
|
+
f['app_versions'] = Array(@options[:app_version]) if @options[:app_version]
|
|
55
|
+
f
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def bug_filters
|
|
59
|
+
f = raw_filters
|
|
60
|
+
ids = map_ids(BUG_STATUS_IDS, @options[:status])
|
|
61
|
+
f['status_id'] = ids unless ids.empty?
|
|
62
|
+
pri = map_ids(BUG_PRIORITY_IDS, @options[:priority])
|
|
63
|
+
f['priority_id'] = pri unless pri.empty?
|
|
64
|
+
f['app_version'] = Array(@options[:app_version]) if @options[:app_version]
|
|
65
|
+
f
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def review_filters
|
|
69
|
+
f = raw_filters
|
|
70
|
+
f['rating'] = Array(@options[:rating]).map(&:to_i) if @options[:rating]
|
|
71
|
+
f['country'] = Array(@options[:country]) if @options[:country]
|
|
72
|
+
f['os'] = Array(@options[:os]) if @options[:os]
|
|
73
|
+
f
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def survey_filters
|
|
77
|
+
f = raw_filters
|
|
78
|
+
f['type'] = Array(@options[:type]).map(&:to_i) if @options[:type]
|
|
79
|
+
f['status'] = Array(@options[:status]).map(&:to_i) if @options[:status]
|
|
80
|
+
f
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def opportunity_filters
|
|
84
|
+
f = raw_filters
|
|
85
|
+
f['status'] = Array(@options[:status]) if @options[:status]
|
|
86
|
+
f['priority'] = Array(@options[:priority]) if @options[:priority]
|
|
87
|
+
f['team_id'] = @options[:team_id] if @options[:team_id]
|
|
88
|
+
f
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def incident_filters
|
|
92
|
+
f = raw_filters
|
|
93
|
+
f['status'] = Array(@options[:status]) if @options[:status]
|
|
94
|
+
f['type'] = Array(@options[:type]) if @options[:type]
|
|
95
|
+
f
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Top-level (non-filter) changes for `bugs update`.
|
|
99
|
+
def bug_update_changes
|
|
100
|
+
action = bug_duplicate_action
|
|
101
|
+
if action && (@options[:status] || @options[:priority])
|
|
102
|
+
raise 'Duplicate marking cannot be combined with --status/--priority'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
changes = {
|
|
106
|
+
status_id: map_one(BUG_STATUS_IDS, @options[:status]),
|
|
107
|
+
priority_id: map_one(BUG_PRIORITY_IDS, @options[:priority]),
|
|
108
|
+
tags: (@options[:clear_tags] ? [] : @options[:tags]),
|
|
109
|
+
action: action,
|
|
110
|
+
original_bug_number: @options[:duplicate_of]
|
|
111
|
+
}.compact
|
|
112
|
+
return changes unless changes.empty?
|
|
113
|
+
|
|
114
|
+
raise 'Provide at least one change (--status/--priority/--tags/--clear-tags/--duplicate-of/--action)'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def bug_duplicate_action
|
|
118
|
+
return @options[:action] if @options[:action]
|
|
119
|
+
|
|
120
|
+
'mark_as_duplicate' if @options[:duplicate_of]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def filters_arg(filters)
|
|
124
|
+
filters.empty? ? {} : { filters: filters }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def json_arg(key, raw)
|
|
128
|
+
return {} if raw.nil?
|
|
129
|
+
|
|
130
|
+
{ key => parse_json(raw, "--#{key}") }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def raw_filters
|
|
134
|
+
return {} unless @options[:filters]
|
|
135
|
+
|
|
136
|
+
parsed = parse_json(@options[:filters], '--filters')
|
|
137
|
+
raise '--filters must be a JSON object' unless parsed.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
parsed
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def alert_payload
|
|
143
|
+
return {} unless @options[:payload]
|
|
144
|
+
|
|
145
|
+
parsed = parse_json(@options[:payload], '--payload')
|
|
146
|
+
raise '--payload must be a JSON object' unless parsed.is_a?(Hash)
|
|
147
|
+
|
|
148
|
+
parsed.transform_keys(&:to_sym)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def parse_json(str, label)
|
|
152
|
+
JSON.parse(str)
|
|
153
|
+
rescue JSON::ParserError => e
|
|
154
|
+
raise "Invalid #{label} JSON: #{e.message}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def map_ids(mapping, values)
|
|
158
|
+
Array(values).filter_map { |v| mapping[v] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def map_one(mapping, value)
|
|
162
|
+
value && mapping[value]
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -5,33 +5,159 @@ require 'luciq/api/client'
|
|
|
5
5
|
module Luciq
|
|
6
6
|
module Commands
|
|
7
7
|
class Upload
|
|
8
|
+
ALLOWED_ARCHITECTURES = %w[armeabi-v7a arm64-v8a x86 x86_64].freeze
|
|
9
|
+
|
|
8
10
|
def initialize(options = {})
|
|
9
11
|
@options = options
|
|
10
12
|
@client = API::Client.new
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
def ios_dsym(file_path)
|
|
16
|
+
run_upload(file_path, 'iOS dSYM file', format: :zip) do
|
|
17
|
+
@client.upload_ios_dsym(file_path: file_path, app_token: @options[:app_token])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def android_mapping(file_path)
|
|
22
|
+
run_upload(file_path, 'Android mapping file') do
|
|
23
|
+
@client.upload_android_mapping(
|
|
24
|
+
file_path: file_path,
|
|
25
|
+
app_token: @options[:app_token],
|
|
26
|
+
version_code: @options[:version_code],
|
|
27
|
+
version_name: @options[:version_name]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def android_ndk(file_path)
|
|
33
|
+
run_upload(file_path, 'Android NDK .so files', format: :zip) do
|
|
34
|
+
validate_arch!
|
|
35
|
+
@client.upload_android_ndk(
|
|
36
|
+
file_path: file_path,
|
|
37
|
+
app_token: @options[:app_token],
|
|
38
|
+
app_version: @options[:version_name],
|
|
39
|
+
arch: @options[:arch]
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def react_native_ios_dsym(file_path)
|
|
45
|
+
run_upload(file_path, 'React Native iOS dSYM', format: :zip) do
|
|
46
|
+
@client.upload_react_native_ios_dsym(file_path: file_path, app_token: @options[:app_token])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def react_native_ios_sourcemap(file_path)
|
|
51
|
+
run_upload(file_path, 'React Native iOS source map', format: :sourcemap) do
|
|
52
|
+
@client.upload_react_native_ios_sourcemap(
|
|
53
|
+
file_path: file_path,
|
|
54
|
+
app_token: @options[:app_token],
|
|
55
|
+
app_version: build_app_version
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def react_native_android_mapping(file_path)
|
|
61
|
+
run_upload(file_path, 'React Native Android mapping file') do
|
|
62
|
+
@client.upload_react_native_android_mapping(
|
|
63
|
+
file_path: file_path,
|
|
64
|
+
app_token: @options[:app_token],
|
|
65
|
+
version_code: @options[:version_code],
|
|
66
|
+
version_name: @options[:version_name]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def react_native_android_sourcemap(file_path)
|
|
72
|
+
run_upload(file_path, 'React Native Android source map', format: :sourcemap) do
|
|
73
|
+
@client.upload_react_native_android_sourcemap(
|
|
74
|
+
file_path: file_path,
|
|
75
|
+
app_token: @options[:app_token],
|
|
76
|
+
app_version: build_app_version
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def react_native_ndk(file_path)
|
|
82
|
+
run_upload(file_path, 'React Native NDK .so files', format: :zip) do
|
|
83
|
+
validate_arch!
|
|
84
|
+
@client.upload_react_native_ndk(
|
|
85
|
+
file_path: file_path,
|
|
86
|
+
app_token: @options[:app_token],
|
|
87
|
+
app_version: @options[:version_name],
|
|
88
|
+
arch: @options[:arch]
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def flutter_ios_dsym(file_path)
|
|
94
|
+
run_upload(file_path, 'Flutter iOS dSYM', format: :zip) do
|
|
95
|
+
@client.upload_flutter_ios_dsym(file_path: file_path, app_token: @options[:app_token])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def flutter_ios_sourcemap(file_path)
|
|
100
|
+
run_upload(file_path, 'Flutter iOS Dart sourcemap', format: :zip) do
|
|
101
|
+
@client.upload_flutter_ios_sourcemap(
|
|
102
|
+
file_path: file_path,
|
|
103
|
+
app_token: @options[:app_token],
|
|
104
|
+
version_name: @options[:version_name],
|
|
105
|
+
version_code: @options[:version_code]
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def flutter_android_mapping(file_path)
|
|
111
|
+
run_upload(file_path, 'Flutter Android mapping file') do
|
|
112
|
+
@client.upload_flutter_android_mapping(
|
|
113
|
+
file_path: file_path,
|
|
114
|
+
app_token: @options[:app_token],
|
|
115
|
+
version_code: @options[:version_code],
|
|
116
|
+
version_name: @options[:version_name]
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def flutter_android_sourcemap(file_path)
|
|
122
|
+
run_upload(file_path, 'Flutter Android Dart sourcemap', format: :zip) do
|
|
123
|
+
@client.upload_flutter_android_sourcemap(
|
|
124
|
+
file_path: file_path,
|
|
125
|
+
app_token: @options[:app_token],
|
|
126
|
+
version_name: @options[:version_name],
|
|
127
|
+
version_code: @options[:version_code]
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def flutter_ndk(file_path)
|
|
133
|
+
run_upload(file_path, 'Flutter NDK .so files', format: :zip) do
|
|
134
|
+
validate_arch!
|
|
135
|
+
@client.upload_flutter_ndk(
|
|
136
|
+
file_path: file_path,
|
|
137
|
+
app_token: @options[:app_token],
|
|
138
|
+
app_version: @options[:version_name],
|
|
139
|
+
arch: @options[:arch]
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def run_upload(file_path, label, format: nil)
|
|
14
147
|
validate_file!(file_path)
|
|
15
|
-
|
|
148
|
+
validate_format!(file_path, format)
|
|
16
149
|
|
|
17
|
-
puts "Uploading
|
|
150
|
+
puts "Uploading #{label}: #{File.basename(file_path)}"
|
|
18
151
|
puts
|
|
19
152
|
|
|
20
|
-
|
|
21
|
-
file_path: file_path,
|
|
22
|
-
app_token: @options[:app_token],
|
|
23
|
-
version_code: @options[:version_code],
|
|
24
|
-
version_name: @options[:version_name]
|
|
25
|
-
)
|
|
153
|
+
yield
|
|
26
154
|
|
|
27
|
-
puts
|
|
155
|
+
puts "✓ #{label} uploaded successfully!"
|
|
28
156
|
rescue StandardError => e
|
|
29
157
|
puts "✗ Upload failed: #{e.message}"
|
|
30
158
|
exit 1
|
|
31
159
|
end
|
|
32
160
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
161
|
def validate_file!(file_path)
|
|
36
162
|
unless File.exist?(file_path)
|
|
37
163
|
puts "✗ File not found: #{file_path}"
|
|
@@ -44,13 +170,42 @@ module Luciq
|
|
|
44
170
|
exit 1
|
|
45
171
|
end
|
|
46
172
|
|
|
173
|
+
def validate_format!(file_path, format)
|
|
174
|
+
case format
|
|
175
|
+
when :zip then validate_zip_extension!(file_path)
|
|
176
|
+
when :sourcemap then validate_sourcemap_extension!(file_path)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
47
180
|
def validate_zip_extension!(file_path)
|
|
48
181
|
return if file_path.downcase.end_with?('.zip')
|
|
49
182
|
|
|
50
183
|
puts "✗ File must be a .zip archive: #{file_path}"
|
|
51
|
-
puts ' The ZIP should contain a single text file.'
|
|
52
184
|
exit 1
|
|
53
185
|
end
|
|
186
|
+
|
|
187
|
+
def validate_sourcemap_extension!(file_path)
|
|
188
|
+
return if file_path.downcase.end_with?('.json', '.txt')
|
|
189
|
+
|
|
190
|
+
puts "✗ Source map must be a .json or .txt file: #{file_path}"
|
|
191
|
+
exit 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def validate_arch!
|
|
195
|
+
return if ALLOWED_ARCHITECTURES.include?(@options[:arch])
|
|
196
|
+
|
|
197
|
+
puts "✗ Invalid architecture: #{@options[:arch]}"
|
|
198
|
+
puts " Allowed values: #{ALLOWED_ARCHITECTURES.join(', ')}"
|
|
199
|
+
exit 1
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def build_app_version
|
|
203
|
+
{
|
|
204
|
+
code: @options[:version_code],
|
|
205
|
+
name: @options[:version_name],
|
|
206
|
+
codepush: @options[:codepush]
|
|
207
|
+
}.compact
|
|
208
|
+
end
|
|
54
209
|
end
|
|
55
210
|
end
|
|
56
211
|
end
|