mysigner 0.1.2 → 0.1.4
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/.githooks/pre-commit +15 -0
- data/.githooks/pre-push +21 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +126 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +14 -16
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/certificate_.cer +0 -0
- data/exe/mysigner +19 -2
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +83 -63
- data/lib/mysigner/build/android_parser.rb +33 -40
- data/lib/mysigner/build/configurator.rb +17 -16
- data/lib/mysigner/build/detector.rb +39 -50
- data/lib/mysigner/build/error_analyzer.rb +70 -68
- data/lib/mysigner/build/executor.rb +30 -37
- data/lib/mysigner/build/parser.rb +18 -18
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +771 -764
- data/lib/mysigner/cli/build_commands.rb +962 -796
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
- data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
- data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
- data/lib/mysigner/cli/concerns/helpers.rb +44 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
- data/lib/mysigner/cli/resource_commands.rb +1153 -985
- data/lib/mysigner/cli/validate_commands.rb +25 -25
- data/lib/mysigner/cli.rb +11 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +161 -60
- data/lib/mysigner/export/exporter.rb +38 -37
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +81 -61
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +96 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +164 -144
- data/lib/mysigner/upload/uploader.rb +136 -115
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/profile_.mobileprovision +0 -0
- data/test_manual.rb +37 -36
- metadata +44 -17
- data/.DS_Store +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mysigner
|
|
2
4
|
class CLI < Thor
|
|
3
5
|
module ResourceCommands
|
|
4
6
|
def self.included(base)
|
|
5
7
|
base.class_eval do
|
|
6
|
-
desc
|
|
8
|
+
desc 'devices', 'List registered test devices (UDIDs)'
|
|
7
9
|
method_option :platform, type: :string, aliases: '-p', desc: 'Filter by platform (IOS, MAC_OS, TV_OS)'
|
|
8
10
|
method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ENABLED, DISABLED)'
|
|
9
11
|
method_option :search, type: :string, aliases: '-q', desc: 'Search by name or UDID'
|
|
@@ -13,8 +15,8 @@ module Mysigner
|
|
|
13
15
|
config = load_config
|
|
14
16
|
client = create_client(config)
|
|
15
17
|
|
|
16
|
-
say
|
|
17
|
-
say
|
|
18
|
+
say '📱 Devices', :cyan
|
|
19
|
+
say ''
|
|
18
20
|
|
|
19
21
|
# Build query params
|
|
20
22
|
params = {
|
|
@@ -31,8 +33,8 @@ module Mysigner
|
|
|
31
33
|
pagination = response[:data]['pagination']
|
|
32
34
|
|
|
33
35
|
if devices.empty?
|
|
34
|
-
say
|
|
35
|
-
say
|
|
36
|
+
say 'No devices found', :yellow
|
|
37
|
+
say ''
|
|
36
38
|
say "Tip: Register a device with 'mysigner device add NAME UDID'", :yellow
|
|
37
39
|
return
|
|
38
40
|
end
|
|
@@ -41,21 +43,19 @@ module Mysigner
|
|
|
41
43
|
devices.each do |device|
|
|
42
44
|
status_icon = device['status'] == 'ENABLED' ? '✓' : '✗'
|
|
43
45
|
status_color = device['status'] == 'ENABLED' ? :green : :red
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
say " #{status_icon} #{device['name']} (ID: #{device['id']})", status_color
|
|
46
48
|
say " UDID: #{device['udid']}"
|
|
47
49
|
say " Platform: #{device['platform']} | Class: #{device['device_class']}"
|
|
48
50
|
say " Status: #{device['status']}"
|
|
49
|
-
say
|
|
51
|
+
say ''
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# Show pagination
|
|
53
55
|
if pagination
|
|
54
56
|
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
|
|
55
57
|
|
|
56
|
-
if pagination['page'] < pagination['total_pages']
|
|
57
|
-
say "Run with --page #{pagination['page'] + 1} to see more", :yellow
|
|
58
|
-
end
|
|
58
|
+
say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
|
|
59
59
|
end
|
|
60
60
|
rescue Mysigner::ClientError => e
|
|
61
61
|
error "Failed to fetch devices: #{e.message}"
|
|
@@ -63,57 +63,57 @@ module Mysigner
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
desc
|
|
66
|
+
desc 'device SUBCOMMAND', 'Manage test devices (detect, add, update)'
|
|
67
67
|
long_desc <<~DESC
|
|
68
68
|
Register and manage test devices for development builds.
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
WHY REGISTER DEVICES?
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
To install development/adhoc builds on physical devices, you must register
|
|
73
73
|
their UDIDs (Unique Device Identifiers) with Apple and include them in your
|
|
74
74
|
provisioning profiles.
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
SUBCOMMANDS:
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
mysigner device detect
|
|
79
79
|
Auto-detect connected iOS devices and show their UDIDs
|
|
80
|
-
|
|
80
|
+
#{' '}
|
|
81
81
|
mysigner device add NAME UDID [--platform IOS]
|
|
82
82
|
Register a new device for testing
|
|
83
|
-
|
|
83
|
+
#{' '}
|
|
84
84
|
mysigner device update ID NEW_NAME
|
|
85
85
|
Rename an existing device
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
HOW TO GET A DEVICE UDID:
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
Method 1 - Auto-detect (Recommended):
|
|
90
90
|
mysigner device detect
|
|
91
|
-
|
|
91
|
+
#{' '}
|
|
92
92
|
This will find all connected iOS devices and let you register them
|
|
93
93
|
interactively. No need to open any other apps.
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
Method 2 - Via Finder:
|
|
96
96
|
1. Connect your iPhone/iPad to your Mac
|
|
97
97
|
2. Open Finder and select your device in the sidebar
|
|
98
98
|
3. Click on the device info to reveal UDID
|
|
99
99
|
4. Right-click → Copy UDID
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
EXAMPLES:
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
# Register your iPhone
|
|
104
104
|
mysigner device add "My iPhone 15" 00008030-001A1B2C3D4E567F
|
|
105
|
-
|
|
105
|
+
#{' '}
|
|
106
106
|
# Register an iPad
|
|
107
107
|
mysigner device add "Test iPad" da83bb40dba39e35d258988d856508798db7afba
|
|
108
|
-
|
|
108
|
+
#{' '}
|
|
109
109
|
# Register a Mac for Mac Catalyst apps
|
|
110
110
|
mysigner device add "MacBook Pro" ABC123... --platform MAC_OS
|
|
111
|
-
|
|
111
|
+
#{' '}
|
|
112
112
|
# Rename a device (use ID from 'mysigner devices' list)
|
|
113
113
|
mysigner device update 42 "John's iPhone"
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
NOTES:
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
• UDIDs are 40 hex characters (0-9, a-f) or 25 characters for newer devices
|
|
118
118
|
• You can register up to 100 devices per year per account
|
|
119
119
|
• After registering, regenerate your provisioning profiles to include the device
|
|
@@ -129,14 +129,14 @@ module Mysigner
|
|
|
129
129
|
detect_connected_devices(config, client)
|
|
130
130
|
when 'add'
|
|
131
131
|
if args.length < 2
|
|
132
|
-
error
|
|
133
|
-
say
|
|
134
|
-
say
|
|
135
|
-
say
|
|
132
|
+
error 'Usage: mysigner device add NAME UDID [--platform IOS]'
|
|
133
|
+
say ''
|
|
134
|
+
say 'Example: mysigner device add "My iPhone" 00008030-001A1B2C3D4E567F', :yellow
|
|
135
|
+
say ''
|
|
136
136
|
say "💡 Don't know your UDID? Run:", :cyan
|
|
137
|
-
say
|
|
138
|
-
say
|
|
139
|
-
say
|
|
137
|
+
say ' mysigner device detect', :cyan
|
|
138
|
+
say ''
|
|
139
|
+
say ' This will auto-detect connected devices and let you register them.', :cyan
|
|
140
140
|
exit 1
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -144,8 +144,25 @@ module Mysigner
|
|
|
144
144
|
udid = args[1]
|
|
145
145
|
platform = options[:platform].upcase
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
# Client-side UDID sanity check — catches obvious typos and
|
|
148
|
+
# copy-paste errors before they hit Apple's API (which, for IOS
|
|
149
|
+
# at least, has historically accepted synthetic/all-zeros UDIDs
|
|
150
|
+
# in sandbox environments). Skipped for non-IOS platforms.
|
|
151
|
+
if (platform == 'IOS') && !valid_ios_udid?(udid)
|
|
152
|
+
error "Invalid iOS UDID: #{udid}"
|
|
153
|
+
say ''
|
|
154
|
+
say 'iOS UDIDs must:', :yellow
|
|
155
|
+
say ' • Be either 25 chars (older devices) or 40 hex chars (newer)', :cyan
|
|
156
|
+
say ' • Be hexadecimal (0-9, A-F)', :cyan
|
|
157
|
+
say ' • Not be trivially synthetic (e.g. all zeros, single repeated char)', :cyan
|
|
158
|
+
say ''
|
|
159
|
+
say '💡 Get a real UDID:', :cyan
|
|
160
|
+
say ' mysigner device detect', :yellow
|
|
161
|
+
exit 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
say '📱 Registering device...', :cyan
|
|
165
|
+
say ''
|
|
149
166
|
|
|
150
167
|
begin
|
|
151
168
|
response = client.post(
|
|
@@ -158,15 +175,15 @@ module Mysigner
|
|
|
158
175
|
)
|
|
159
176
|
|
|
160
177
|
device = response[:data]['device']
|
|
161
|
-
say
|
|
162
|
-
say
|
|
163
|
-
say
|
|
178
|
+
say '✓ Device registered successfully!', :green
|
|
179
|
+
say ''
|
|
180
|
+
say 'Details:', :bold
|
|
164
181
|
say " Name: #{device['name']}"
|
|
165
182
|
say " UDID: #{device['udid']}"
|
|
166
183
|
say " Platform: #{device['platform']}"
|
|
167
184
|
say " Status: #{device['status']}"
|
|
168
185
|
rescue Mysigner::ValidationError => e
|
|
169
|
-
error
|
|
186
|
+
error 'Validation failed:'
|
|
170
187
|
if e.details
|
|
171
188
|
e.details.each do |field, errors|
|
|
172
189
|
say " #{field}: #{errors.join(', ')}", :red
|
|
@@ -177,8 +194,8 @@ module Mysigner
|
|
|
177
194
|
say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
|
|
178
195
|
exit 1
|
|
179
196
|
rescue Mysigner::ClientError => e
|
|
180
|
-
if e.message.include?(
|
|
181
|
-
error
|
|
197
|
+
if e.message.include?('already exists')
|
|
198
|
+
error 'Device with this UDID already exists'
|
|
182
199
|
else
|
|
183
200
|
error "Failed to register device: #{e.message}"
|
|
184
201
|
end
|
|
@@ -186,11 +203,11 @@ module Mysigner
|
|
|
186
203
|
end
|
|
187
204
|
when 'update'
|
|
188
205
|
if args.length < 2
|
|
189
|
-
error
|
|
190
|
-
say
|
|
206
|
+
error 'Usage: mysigner device update ID NEW_NAME'
|
|
207
|
+
say ''
|
|
191
208
|
say "Example: mysigner device update 42 \"John's iPhone\"", :yellow
|
|
192
|
-
say
|
|
193
|
-
say
|
|
209
|
+
say ''
|
|
210
|
+
say '💡 To get device IDs:', :cyan
|
|
194
211
|
say " Run 'mysigner devices' to see all devices with their IDs", :cyan
|
|
195
212
|
exit 1
|
|
196
213
|
end
|
|
@@ -198,8 +215,8 @@ module Mysigner
|
|
|
198
215
|
device_id = args[0]
|
|
199
216
|
new_name = args[1]
|
|
200
217
|
|
|
201
|
-
say
|
|
202
|
-
say
|
|
218
|
+
say '📱 Updating device...', :cyan
|
|
219
|
+
say ''
|
|
203
220
|
|
|
204
221
|
begin
|
|
205
222
|
# Get device details first
|
|
@@ -208,7 +225,7 @@ module Mysigner
|
|
|
208
225
|
|
|
209
226
|
say "Current name: #{device['name']}", :yellow
|
|
210
227
|
say "New name: #{new_name}", :green
|
|
211
|
-
say
|
|
228
|
+
say ''
|
|
212
229
|
|
|
213
230
|
# Update device
|
|
214
231
|
response = client.patch(
|
|
@@ -217,9 +234,9 @@ module Mysigner
|
|
|
217
234
|
)
|
|
218
235
|
|
|
219
236
|
updated_device = response[:data]['device']
|
|
220
|
-
say
|
|
221
|
-
say
|
|
222
|
-
say
|
|
237
|
+
say '✓ Device updated successfully!', :green
|
|
238
|
+
say ''
|
|
239
|
+
say 'Details:', :bold
|
|
223
240
|
say " Name: #{updated_device['name']}"
|
|
224
241
|
say " UDID: #{updated_device['udid']}"
|
|
225
242
|
say " Platform: #{updated_device['platform']}"
|
|
@@ -234,7 +251,7 @@ module Mysigner
|
|
|
234
251
|
invoke :help, ['device']
|
|
235
252
|
else
|
|
236
253
|
error "Unknown action: #{action}"
|
|
237
|
-
say
|
|
254
|
+
say 'Available actions: detect, add, update, help', :yellow
|
|
238
255
|
exit 1
|
|
239
256
|
end
|
|
240
257
|
end
|
|
@@ -242,34 +259,32 @@ module Mysigner
|
|
|
242
259
|
private
|
|
243
260
|
|
|
244
261
|
def detect_connected_devices(config, client)
|
|
245
|
-
say
|
|
246
|
-
say
|
|
262
|
+
say '🔍 Detecting connected iOS devices...', :cyan
|
|
263
|
+
say ''
|
|
247
264
|
|
|
248
265
|
devices = []
|
|
249
266
|
|
|
250
267
|
# Try system_profiler first (built-in macOS)
|
|
251
|
-
if system(
|
|
268
|
+
if system('which system_profiler > /dev/null 2>&1')
|
|
252
269
|
output = `system_profiler SPUSBDataType 2>/dev/null`
|
|
253
|
-
|
|
270
|
+
|
|
254
271
|
# Parse iOS devices from system_profiler output
|
|
255
272
|
current_device = nil
|
|
256
273
|
output.each_line do |line|
|
|
257
274
|
if line =~ /^\s{4}(\S.+):$/
|
|
258
275
|
# New top-level USB device
|
|
259
|
-
current_device = { name:
|
|
276
|
+
current_device = { name: ::Regexp.last_match(1).strip }
|
|
260
277
|
elsif current_device
|
|
261
278
|
if line =~ /Serial Number:\s*([A-Fa-f0-9-]+)/
|
|
262
|
-
serial =
|
|
279
|
+
serial = ::Regexp.last_match(1).gsub('-', '')
|
|
263
280
|
# iOS device serials are typically 24-40 hex chars
|
|
264
|
-
|
|
265
|
-
current_device[:udid] = serial
|
|
266
|
-
end
|
|
281
|
+
current_device[:udid] = serial if serial.length.between?(24, 40)
|
|
267
282
|
elsif line =~ /Product ID:\s*(0x12[aA][0-9a-fA-F])/
|
|
268
283
|
# Apple mobile device product IDs start with 0x12a
|
|
269
284
|
current_device[:is_ios] = true
|
|
270
285
|
end
|
|
271
286
|
end
|
|
272
|
-
|
|
287
|
+
|
|
273
288
|
if current_device && current_device[:udid] && current_device[:is_ios]
|
|
274
289
|
devices << current_device
|
|
275
290
|
current_device = nil
|
|
@@ -278,76 +293,78 @@ module Mysigner
|
|
|
278
293
|
end
|
|
279
294
|
|
|
280
295
|
# Also try idevice_id if available (more reliable)
|
|
281
|
-
if system(
|
|
296
|
+
if system('which idevice_id > /dev/null 2>&1')
|
|
282
297
|
output = `idevice_id -l 2>/dev/null`.strip
|
|
283
298
|
output.each_line do |line|
|
|
284
299
|
udid = line.strip
|
|
285
300
|
next if udid.empty?
|
|
286
|
-
|
|
301
|
+
|
|
287
302
|
# Get device name if ideviceinfo is available
|
|
288
|
-
name =
|
|
289
|
-
if system(
|
|
303
|
+
name = 'iOS Device'
|
|
304
|
+
if system('which ideviceinfo > /dev/null 2>&1')
|
|
290
305
|
device_name = `ideviceinfo -u #{udid} -k DeviceName 2>/dev/null`.strip
|
|
291
306
|
name = device_name unless device_name.empty?
|
|
292
307
|
end
|
|
293
|
-
|
|
308
|
+
|
|
294
309
|
# Avoid duplicates
|
|
295
|
-
unless devices.any? { |d| d[:udid] == udid }
|
|
296
|
-
devices << { name: name, udid: udid }
|
|
297
|
-
end
|
|
310
|
+
devices << { name: name, udid: udid } unless devices.any? { |d| d[:udid] == udid }
|
|
298
311
|
end
|
|
299
312
|
end
|
|
300
313
|
|
|
301
314
|
# Also try xcrun xctrace (Xcode command line tools)
|
|
302
|
-
if devices.empty? && system(
|
|
303
|
-
output
|
|
315
|
+
if devices.empty? && system('which xcrun > /dev/null 2>&1')
|
|
316
|
+
# xctrace output can contain non-ASCII bytes (emoji in device
|
|
317
|
+
# names, accented characters). Force UTF-8 + scrub invalid
|
|
318
|
+
# sequences so `.strip` / `.each_line` don't raise
|
|
319
|
+
# Encoding::CompatibilityError under the default US-ASCII locale.
|
|
320
|
+
output = `xcrun xctrace list devices 2>/dev/null`.force_encoding('UTF-8').scrub
|
|
304
321
|
in_devices_section = false
|
|
305
|
-
|
|
322
|
+
|
|
306
323
|
output.each_line do |line|
|
|
307
324
|
line = line.strip
|
|
308
|
-
|
|
325
|
+
|
|
309
326
|
# Track sections
|
|
310
|
-
if line ==
|
|
327
|
+
if line == '== Devices =='
|
|
311
328
|
in_devices_section = true
|
|
312
329
|
next
|
|
313
|
-
elsif line ==
|
|
330
|
+
elsif line == '== Simulators =='
|
|
314
331
|
in_devices_section = false
|
|
315
332
|
next
|
|
316
333
|
end
|
|
317
|
-
|
|
334
|
+
|
|
318
335
|
next unless in_devices_section
|
|
319
336
|
next if line.empty?
|
|
320
|
-
|
|
337
|
+
|
|
321
338
|
# Format: "Name (Version) (UDID)" - iOS devices have UDIDs starting with 0000
|
|
322
339
|
# Skip Macs which have UUID format
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
next unless line =~ /^(.+?)\s+\([^)]+\)\s+\((0000[A-Fa-f0-9-]+)\)\s*$/
|
|
341
|
+
|
|
342
|
+
name = ::Regexp.last_match(1).strip
|
|
343
|
+
udid = ::Regexp.last_match(2).gsub('-', '')
|
|
344
|
+
devices << { name: name, udid: udid }
|
|
328
345
|
end
|
|
329
346
|
end
|
|
330
347
|
|
|
331
348
|
if devices.empty?
|
|
332
|
-
say
|
|
333
|
-
say
|
|
334
|
-
say
|
|
335
|
-
say
|
|
336
|
-
say
|
|
349
|
+
say 'No iOS devices detected.', :yellow
|
|
350
|
+
say ''
|
|
351
|
+
say 'Make sure:', :cyan
|
|
352
|
+
say ' 1. Your device is connected via USB', :cyan
|
|
353
|
+
say ' 2. The device is unlocked', :cyan
|
|
337
354
|
say " 3. You've trusted this computer on the device", :cyan
|
|
338
|
-
say
|
|
339
|
-
say
|
|
340
|
-
say
|
|
355
|
+
say ''
|
|
356
|
+
say '💡 For better detection, install libimobiledevice:', :yellow
|
|
357
|
+
say ' brew install libimobiledevice', :yellow
|
|
341
358
|
return
|
|
342
359
|
end
|
|
343
360
|
|
|
344
361
|
say "Found #{devices.length} device(s):", :green
|
|
345
|
-
say
|
|
362
|
+
say ''
|
|
346
363
|
|
|
347
364
|
devices.each_with_index do |device, idx|
|
|
348
365
|
say " #{idx + 1}. #{device[:name]}", :green
|
|
349
366
|
say " UDID: #{device[:udid]}", :white
|
|
350
|
-
say
|
|
367
|
+
say ''
|
|
351
368
|
end
|
|
352
369
|
|
|
353
370
|
# Check if running interactively
|
|
@@ -355,20 +372,20 @@ module Mysigner
|
|
|
355
372
|
|
|
356
373
|
# Ask if user wants to register
|
|
357
374
|
say "Would you like to register a device? (Enter number, or 'n' to skip)", :cyan
|
|
358
|
-
choice = ask(
|
|
375
|
+
choice = ask('>')&.strip || ''
|
|
359
376
|
|
|
360
377
|
return if choice.downcase == 'n' || choice.empty?
|
|
361
378
|
|
|
362
379
|
idx = choice.to_i - 1
|
|
363
380
|
if idx >= 0 && idx < devices.length
|
|
364
381
|
device = devices[idx]
|
|
365
|
-
|
|
366
|
-
say
|
|
382
|
+
|
|
383
|
+
say ''
|
|
367
384
|
say "Enter a name for this device (or press Enter to use '#{device[:name]}'):", :cyan
|
|
368
|
-
custom_name = ask(
|
|
385
|
+
custom_name = ask('>')&.strip || ''
|
|
369
386
|
name = custom_name.empty? ? device[:name] : custom_name
|
|
370
387
|
|
|
371
|
-
say
|
|
388
|
+
say ''
|
|
372
389
|
say "📱 Registering '#{name}'...", :cyan
|
|
373
390
|
|
|
374
391
|
begin
|
|
@@ -382,58 +399,59 @@ module Mysigner
|
|
|
382
399
|
)
|
|
383
400
|
|
|
384
401
|
registered = response[:data]['device']
|
|
385
|
-
say
|
|
386
|
-
say
|
|
402
|
+
say ''
|
|
403
|
+
say '✓ Device registered successfully!', :green
|
|
387
404
|
say " Name: #{registered['name']}"
|
|
388
405
|
say " UDID: #{registered['udid']}"
|
|
389
406
|
say " Platform: #{registered['platform']}"
|
|
390
407
|
rescue Mysigner::ClientError => e
|
|
391
|
-
if e.message.include?(
|
|
392
|
-
say
|
|
393
|
-
say
|
|
408
|
+
if e.message.include?('already exists')
|
|
409
|
+
say ''
|
|
410
|
+
say 'ℹ️ Device already registered', :yellow
|
|
394
411
|
else
|
|
395
412
|
error "Failed to register: #{e.message}"
|
|
396
413
|
end
|
|
397
414
|
end
|
|
398
415
|
else
|
|
399
|
-
say
|
|
416
|
+
say 'Invalid selection', :red
|
|
400
417
|
end
|
|
401
418
|
end
|
|
402
419
|
|
|
403
420
|
public
|
|
404
421
|
|
|
405
|
-
desc
|
|
422
|
+
desc 'profiles', 'List provisioning profiles (advanced - only needed for manual signing)'
|
|
406
423
|
long_desc <<~DESC
|
|
407
424
|
List all provisioning profiles in your organization.
|
|
408
|
-
|
|
425
|
+
|
|
409
426
|
WHEN DO YOU NEED THIS?
|
|
410
|
-
|
|
427
|
+
|
|
411
428
|
For Automatic Signing (Most Users):
|
|
412
429
|
❌ You DON'T need this - Xcode handles everything
|
|
413
|
-
|
|
430
|
+
|
|
414
431
|
For Manual Signing (Advanced):
|
|
415
432
|
✅ View available profiles
|
|
416
433
|
✅ Check expiration dates
|
|
417
434
|
✅ Get profile IDs for download/delete
|
|
418
|
-
|
|
435
|
+
|
|
419
436
|
EXAMPLES:
|
|
420
|
-
|
|
437
|
+
|
|
421
438
|
# List all profiles
|
|
422
439
|
mysigner profiles
|
|
423
|
-
|
|
440
|
+
#{' '}
|
|
424
441
|
# Filter by type
|
|
425
442
|
mysigner profiles --type APP_STORE
|
|
426
443
|
mysigner profiles --type DEVELOPMENT
|
|
427
|
-
|
|
444
|
+
#{' '}
|
|
428
445
|
# Filter by status
|
|
429
446
|
mysigner profiles --status EXPIRED
|
|
430
|
-
|
|
447
|
+
#{' '}
|
|
431
448
|
# Search by name
|
|
432
449
|
mysigner profiles --search "MyApp"
|
|
433
|
-
|
|
450
|
+
|
|
434
451
|
NOTE: Most users can skip this and just run 'mysigner build'
|
|
435
452
|
DESC
|
|
436
|
-
method_option :type, type: :string, aliases: '-t',
|
|
453
|
+
method_option :type, type: :string, aliases: '-t',
|
|
454
|
+
desc: 'Filter by type (DEVELOPMENT, AD_HOC, APP_STORE, ENTERPRISE)'
|
|
437
455
|
method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ACTIVE, EXPIRED, INVALID)'
|
|
438
456
|
method_option :search, type: :string, aliases: '-q', desc: 'Search by name or identifier'
|
|
439
457
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
@@ -442,8 +460,8 @@ module Mysigner
|
|
|
442
460
|
config = load_config
|
|
443
461
|
client = create_client(config)
|
|
444
462
|
|
|
445
|
-
say
|
|
446
|
-
say
|
|
463
|
+
say '📄 Provisioning Profiles', :cyan
|
|
464
|
+
say ''
|
|
447
465
|
|
|
448
466
|
# Build query params
|
|
449
467
|
params = {
|
|
@@ -460,9 +478,9 @@ module Mysigner
|
|
|
460
478
|
pagination = response[:data]['pagination']
|
|
461
479
|
|
|
462
480
|
if profiles.empty?
|
|
463
|
-
say
|
|
464
|
-
say
|
|
465
|
-
say
|
|
481
|
+
say 'No profiles found', :yellow
|
|
482
|
+
say ''
|
|
483
|
+
say 'Tip: Profiles are created automatically when you request code signing', :yellow
|
|
466
484
|
return
|
|
467
485
|
end
|
|
468
486
|
|
|
@@ -475,22 +493,20 @@ module Mysigner
|
|
|
475
493
|
say " ID: #{profile['id']} | Type: #{profile['profile_type'] || 'N/A'}"
|
|
476
494
|
say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
|
|
477
495
|
say " Status: #{profile['state'] || 'UNKNOWN'}"
|
|
478
|
-
|
|
496
|
+
|
|
479
497
|
if profile['expires_at']
|
|
480
498
|
expires = Time.parse(profile['expires_at']).strftime('%Y-%m-%d')
|
|
481
499
|
say " Expires: #{expires}"
|
|
482
500
|
end
|
|
483
|
-
|
|
484
|
-
say
|
|
501
|
+
|
|
502
|
+
say ''
|
|
485
503
|
end
|
|
486
504
|
|
|
487
505
|
# Show pagination
|
|
488
506
|
if pagination
|
|
489
507
|
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
|
|
490
508
|
|
|
491
|
-
if pagination['page'] < pagination['total_pages']
|
|
492
|
-
say "Run with --page #{pagination['page'] + 1} to see more", :yellow
|
|
493
|
-
end
|
|
509
|
+
say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
|
|
494
510
|
end
|
|
495
511
|
rescue Mysigner::ClientError => e
|
|
496
512
|
error "Failed to fetch profiles: #{e.message}"
|
|
@@ -498,68 +514,68 @@ module Mysigner
|
|
|
498
514
|
end
|
|
499
515
|
end
|
|
500
516
|
|
|
501
|
-
desc
|
|
517
|
+
desc 'profile SUBCOMMAND', 'Download or delete profiles (advanced - only needed for manual signing)'
|
|
502
518
|
long_desc <<~DESC
|
|
503
519
|
Manage provisioning profiles for code signing.
|
|
504
|
-
|
|
520
|
+
|
|
505
521
|
WHAT ARE PROVISIONING PROFILES?
|
|
506
|
-
|
|
522
|
+
|
|
507
523
|
Provisioning profiles are required for signing iOS apps. They link:
|
|
508
524
|
- Your signing certificate
|
|
509
525
|
- Your App ID (bundle ID)
|
|
510
526
|
- Authorized devices (for development/ad-hoc)
|
|
511
|
-
|
|
527
|
+
|
|
512
528
|
WHEN DO YOU NEED THIS?
|
|
513
|
-
|
|
529
|
+
|
|
514
530
|
For Automatic Signing (Most Users):
|
|
515
531
|
❌ You DON'T need these commands
|
|
516
532
|
✅ Xcode handles profiles automatically
|
|
517
533
|
✅ Just run: mysigner build
|
|
518
|
-
|
|
534
|
+
|
|
519
535
|
For Manual Signing (Advanced):
|
|
520
536
|
✅ Download profiles from My Signer
|
|
521
537
|
✅ Install them to ~/Library/MobileDevice/Provisioning Profiles/
|
|
522
538
|
✅ Delete old/expired profiles
|
|
523
|
-
|
|
539
|
+
|
|
524
540
|
SUBCOMMANDS:
|
|
525
|
-
|
|
541
|
+
|
|
526
542
|
mysigner profile download ID [--output path]
|
|
527
543
|
Download a provisioning profile
|
|
528
|
-
|
|
544
|
+
#{' '}
|
|
529
545
|
mysigner profile delete ID
|
|
530
546
|
Delete a provisioning profile
|
|
531
|
-
|
|
547
|
+
|
|
532
548
|
HOW TO USE:
|
|
533
|
-
|
|
549
|
+
|
|
534
550
|
1. List available profiles:
|
|
535
551
|
mysigner profiles
|
|
536
|
-
|
|
552
|
+
|
|
537
553
|
2. Download a profile:
|
|
538
554
|
mysigner profile download 1
|
|
539
|
-
|
|
555
|
+
|
|
540
556
|
3. Install it (double-click or manual):
|
|
541
557
|
open Profile_Name.mobileprovision
|
|
542
558
|
# Or: cp *.mobileprovision ~/Library/MobileDevice/Provisioning\\ Profiles/
|
|
543
|
-
|
|
559
|
+
|
|
544
560
|
EXAMPLES:
|
|
545
|
-
|
|
561
|
+
|
|
546
562
|
# Download profile ID 1
|
|
547
563
|
mysigner profile download 1
|
|
548
|
-
|
|
564
|
+
#{' '}
|
|
549
565
|
# Download to specific location
|
|
550
566
|
mysigner profile download 1 --output ~/Desktop/MyProfile.mobileprovision
|
|
551
|
-
|
|
567
|
+
#{' '}
|
|
552
568
|
# Delete expired profile
|
|
553
569
|
mysigner profile delete 5
|
|
554
|
-
|
|
570
|
+
#{' '}
|
|
555
571
|
# List all profiles
|
|
556
572
|
mysigner profiles
|
|
557
|
-
|
|
573
|
+
#{' '}
|
|
558
574
|
# Filter by type
|
|
559
575
|
mysigner profiles --type APP_STORE
|
|
560
|
-
|
|
576
|
+
|
|
561
577
|
NOTES:
|
|
562
|
-
|
|
578
|
+
|
|
563
579
|
• Most users with Automatic signing don't need this
|
|
564
580
|
• Manual signing wizard tries to auto-install profiles
|
|
565
581
|
• Profiles expire after 1 year and must be regenerated
|
|
@@ -574,13 +590,13 @@ module Mysigner
|
|
|
574
590
|
case action
|
|
575
591
|
when 'download'
|
|
576
592
|
if args.empty?
|
|
577
|
-
error
|
|
578
|
-
say
|
|
579
|
-
say
|
|
580
|
-
say
|
|
581
|
-
say
|
|
593
|
+
error 'Usage: mysigner profile download ID [--output path.mobileprovision]'
|
|
594
|
+
say ''
|
|
595
|
+
say 'Example: mysigner profile download 1', :yellow
|
|
596
|
+
say ''
|
|
597
|
+
say '💡 To get profile IDs:', :cyan
|
|
582
598
|
say " Run 'mysigner profiles' to see all profiles with their IDs", :cyan
|
|
583
|
-
say
|
|
599
|
+
say ''
|
|
584
600
|
say "Note: Most users with Automatic signing don't need this", :yellow
|
|
585
601
|
say "Run 'mysigner help profile' for more info", :cyan
|
|
586
602
|
exit 1
|
|
@@ -588,48 +604,52 @@ module Mysigner
|
|
|
588
604
|
|
|
589
605
|
profile_id = args[0]
|
|
590
606
|
|
|
591
|
-
say
|
|
592
|
-
say
|
|
607
|
+
say '📄 Downloading profile...', :cyan
|
|
608
|
+
say ''
|
|
593
609
|
|
|
594
610
|
begin
|
|
595
611
|
# Get profile details first
|
|
596
612
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
|
|
597
613
|
profile = response[:data]
|
|
598
614
|
|
|
599
|
-
# Determine output path
|
|
615
|
+
# Determine output path. Default to ~/Downloads/ (created if
|
|
616
|
+
# missing) instead of the current working directory — users
|
|
617
|
+
# were ending up with .mobileprovision files sprinkled inside
|
|
618
|
+
# whatever project they ran the command from.
|
|
600
619
|
output_path = if options[:output]
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
620
|
+
options[:output]
|
|
621
|
+
else
|
|
622
|
+
name = profile['name'] || "profile_#{profile['id']}"
|
|
623
|
+
filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
|
|
624
|
+
downloads_dir = File.expand_path('~/Downloads')
|
|
625
|
+
FileUtils.mkdir_p(downloads_dir)
|
|
626
|
+
File.join(downloads_dir, "#{filename}.mobileprovision")
|
|
627
|
+
end
|
|
608
628
|
|
|
609
629
|
# Download the profile content using the client's connection with auth
|
|
610
630
|
download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}/download"
|
|
611
|
-
|
|
612
|
-
say
|
|
613
|
-
|
|
631
|
+
|
|
632
|
+
say 'Fetching profile content...', :yellow
|
|
633
|
+
|
|
614
634
|
# Use Faraday directly with proper auth for binary download
|
|
615
635
|
conn = Faraday.new(url: config.api_url) do |f|
|
|
616
636
|
f.request :authorization, 'Bearer', config.api_token
|
|
617
637
|
f.headers['X-User-Email'] = config.user_email if config.user_email
|
|
618
638
|
f.adapter Faraday.default_adapter
|
|
619
639
|
end
|
|
620
|
-
|
|
640
|
+
|
|
621
641
|
response = conn.get(download_url) do |req|
|
|
622
|
-
req.options.timeout = 30
|
|
623
|
-
req.options.open_timeout = 10
|
|
642
|
+
req.options.timeout = 30 # 30 second timeout
|
|
643
|
+
req.options.open_timeout = 10 # 10 second connection timeout
|
|
624
644
|
end
|
|
625
|
-
|
|
645
|
+
|
|
626
646
|
unless response.success?
|
|
627
647
|
# Check if it's a JSON error response
|
|
628
648
|
if response.headers['content-type']&.include?('json')
|
|
629
649
|
begin
|
|
630
650
|
error_data = JSON.parse(response.body)
|
|
631
651
|
error "Download failed: #{error_data['message'] || error_data['error']}"
|
|
632
|
-
rescue
|
|
652
|
+
rescue StandardError
|
|
633
653
|
error "Download failed with status #{response.status}"
|
|
634
654
|
end
|
|
635
655
|
else
|
|
@@ -641,62 +661,62 @@ module Mysigner
|
|
|
641
661
|
# Write binary content directly to file
|
|
642
662
|
File.binwrite(output_path, response.body)
|
|
643
663
|
|
|
644
|
-
say
|
|
645
|
-
say
|
|
646
|
-
say
|
|
664
|
+
say '✓ Profile downloaded successfully!', :green
|
|
665
|
+
say ''
|
|
666
|
+
say 'Details:', :bold
|
|
647
667
|
say " Name: #{profile['name']}"
|
|
648
668
|
say " Type: #{profile['profile_type'] || 'N/A'}"
|
|
649
669
|
say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
|
|
650
670
|
say " Status: #{profile['state'] || 'UNKNOWN'}"
|
|
651
671
|
say " File: #{output_path}"
|
|
652
|
-
say
|
|
672
|
+
say ''
|
|
653
673
|
say "File size: #{response.body.bytesize} bytes", :yellow
|
|
654
674
|
rescue Mysigner::NotFoundError
|
|
655
675
|
error "Profile not found with ID: #{profile_id}"
|
|
656
|
-
say
|
|
657
|
-
say
|
|
658
|
-
say
|
|
659
|
-
say
|
|
660
|
-
say
|
|
661
|
-
say
|
|
662
|
-
say
|
|
676
|
+
say ''
|
|
677
|
+
say '💡 Profile Not Found: How to fix', :cyan
|
|
678
|
+
say ''
|
|
679
|
+
say ' → List available profiles: mysigner profiles', :yellow
|
|
680
|
+
say ' → Sync from Apple: mysigner sync ios', :yellow
|
|
681
|
+
say ' → Check ID is correct (IDs are numeric)', :yellow
|
|
682
|
+
say ''
|
|
663
683
|
exit 1
|
|
664
684
|
rescue Mysigner::ClientError => e
|
|
665
685
|
error "Failed to download profile: #{e.message}"
|
|
666
|
-
say
|
|
667
|
-
say
|
|
668
|
-
say
|
|
669
|
-
say
|
|
670
|
-
say
|
|
671
|
-
say
|
|
672
|
-
say
|
|
673
|
-
exit 1
|
|
674
|
-
rescue => e
|
|
686
|
+
say ''
|
|
687
|
+
say '💡 Download Failed: Try these steps', :cyan
|
|
688
|
+
say ''
|
|
689
|
+
say ' → Check your network connection', :yellow
|
|
690
|
+
say ' → Verify API token is valid: mysigner status', :yellow
|
|
691
|
+
say ' → Re-authenticate if needed: mysigner login', :yellow
|
|
692
|
+
say ''
|
|
693
|
+
exit 1
|
|
694
|
+
rescue StandardError => e
|
|
675
695
|
error "Failed to save file: #{e.message}"
|
|
676
|
-
say
|
|
677
|
-
say
|
|
678
|
-
say
|
|
679
|
-
say
|
|
680
|
-
say
|
|
681
|
-
say
|
|
682
|
-
say
|
|
696
|
+
say ''
|
|
697
|
+
say '💡 File Save Failed: Check these', :cyan
|
|
698
|
+
say ''
|
|
699
|
+
say ' → Verify you have write permissions to the directory', :yellow
|
|
700
|
+
say ' → Check disk space is available', :yellow
|
|
701
|
+
say ' → Try specifying a different output path with --output', :yellow
|
|
702
|
+
say ''
|
|
683
703
|
exit 1
|
|
684
704
|
end
|
|
685
705
|
when 'delete'
|
|
686
706
|
if args.empty?
|
|
687
|
-
error
|
|
688
|
-
say
|
|
689
|
-
say
|
|
690
|
-
say
|
|
691
|
-
say
|
|
707
|
+
error 'Usage: mysigner profile delete ID'
|
|
708
|
+
say ''
|
|
709
|
+
say 'Example: mysigner profile delete 5', :yellow
|
|
710
|
+
say ''
|
|
711
|
+
say '💡 To get profile IDs:', :cyan
|
|
692
712
|
say " Run 'mysigner profiles' to see all profiles with their IDs", :cyan
|
|
693
713
|
exit 1
|
|
694
714
|
end
|
|
695
715
|
|
|
696
716
|
profile_id = args[0]
|
|
697
717
|
|
|
698
|
-
say
|
|
699
|
-
say
|
|
718
|
+
say '📄 Deleting profile...', :cyan
|
|
719
|
+
say ''
|
|
700
720
|
|
|
701
721
|
begin
|
|
702
722
|
# Get profile details first
|
|
@@ -704,18 +724,18 @@ module Mysigner
|
|
|
704
724
|
profile = response[:data]
|
|
705
725
|
|
|
706
726
|
# Confirm deletion
|
|
707
|
-
say
|
|
727
|
+
say 'You are about to delete:', :yellow
|
|
708
728
|
say " Name: #{profile['name']}"
|
|
709
729
|
say " Type: #{profile['profile_type']}"
|
|
710
730
|
say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
|
|
711
|
-
say
|
|
731
|
+
say ''
|
|
712
732
|
|
|
713
|
-
if yes?(
|
|
733
|
+
if yes?('Are you sure you want to delete this profile? (y/n)')
|
|
714
734
|
client.delete("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
|
|
715
|
-
say
|
|
716
|
-
say
|
|
735
|
+
say ''
|
|
736
|
+
say '✓ Profile deleted successfully!', :green
|
|
717
737
|
else
|
|
718
|
-
say
|
|
738
|
+
say 'Deletion cancelled', :yellow
|
|
719
739
|
end
|
|
720
740
|
rescue Mysigner::NotFoundError
|
|
721
741
|
error "Profile not found with ID: #{profile_id}"
|
|
@@ -728,12 +748,12 @@ module Mysigner
|
|
|
728
748
|
invoke :help, ['profile']
|
|
729
749
|
else
|
|
730
750
|
error "Unknown action: #{action}"
|
|
731
|
-
say
|
|
751
|
+
say 'Available actions: download, delete, help', :yellow
|
|
732
752
|
exit 1
|
|
733
753
|
end
|
|
734
754
|
end
|
|
735
755
|
|
|
736
|
-
desc
|
|
756
|
+
desc 'certificates', 'List signing certificates from App Store Connect'
|
|
737
757
|
method_option :type, type: :string, aliases: '-p', desc: 'Filter by type (DEVELOPMENT, DISTRIBUTION)'
|
|
738
758
|
method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ACTIVE, EXPIRED, REVOKED)'
|
|
739
759
|
method_option :search, type: :string, aliases: '-q', desc: 'Search by name'
|
|
@@ -743,8 +763,8 @@ module Mysigner
|
|
|
743
763
|
config = load_config
|
|
744
764
|
client = create_client(config)
|
|
745
765
|
|
|
746
|
-
say
|
|
747
|
-
say
|
|
766
|
+
say '🔐 Signing Certificates', :cyan
|
|
767
|
+
say ''
|
|
748
768
|
|
|
749
769
|
# Build query params
|
|
750
770
|
params = {
|
|
@@ -756,14 +776,15 @@ module Mysigner
|
|
|
756
776
|
params[:q] = options[:search] if options[:search]
|
|
757
777
|
|
|
758
778
|
begin
|
|
759
|
-
response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates",
|
|
779
|
+
response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates",
|
|
780
|
+
params: params)
|
|
760
781
|
certificates = response[:data]['certificates']
|
|
761
782
|
pagination = response[:data]['pagination']
|
|
762
783
|
|
|
763
784
|
if certificates.empty?
|
|
764
|
-
say
|
|
765
|
-
say
|
|
766
|
-
say
|
|
785
|
+
say 'No certificates found', :yellow
|
|
786
|
+
say ''
|
|
787
|
+
say 'Tip: Certificates are synced automatically from App Store Connect', :yellow
|
|
767
788
|
return
|
|
768
789
|
end
|
|
769
790
|
|
|
@@ -771,27 +792,25 @@ module Mysigner
|
|
|
771
792
|
certificates.each do |cert|
|
|
772
793
|
status_icon = cert['status'] == 'ACTIVE' ? '✓' : '✗'
|
|
773
794
|
status_color = cert['status'] == 'ACTIVE' ? :green : :red
|
|
774
|
-
|
|
795
|
+
|
|
775
796
|
say " #{status_icon} #{cert['name']}", status_color
|
|
776
797
|
say " ID: #{cert['id']} | Type: #{cert['certificate_type'] || 'N/A'}"
|
|
777
798
|
say " Serial: #{cert['serial_number'] || 'N/A'}"
|
|
778
799
|
say " Status: #{cert['status'] || 'UNKNOWN'}"
|
|
779
|
-
|
|
800
|
+
|
|
780
801
|
if cert['expires_at']
|
|
781
802
|
expires = Time.parse(cert['expires_at']).strftime('%Y-%m-%d')
|
|
782
803
|
say " Expires: #{expires}"
|
|
783
804
|
end
|
|
784
|
-
|
|
785
|
-
say
|
|
805
|
+
|
|
806
|
+
say ''
|
|
786
807
|
end
|
|
787
808
|
|
|
788
809
|
# Show pagination
|
|
789
810
|
if pagination
|
|
790
811
|
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
|
|
791
812
|
|
|
792
|
-
if pagination['page'] < pagination['total_pages']
|
|
793
|
-
say "Run with --page #{pagination['page'] + 1} to see more", :yellow
|
|
794
|
-
end
|
|
813
|
+
say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
|
|
795
814
|
end
|
|
796
815
|
rescue Mysigner::ClientError => e
|
|
797
816
|
error "Failed to fetch certificates: #{e.message}"
|
|
@@ -799,12 +818,12 @@ module Mysigner
|
|
|
799
818
|
end
|
|
800
819
|
end
|
|
801
820
|
|
|
802
|
-
desc
|
|
821
|
+
desc 'certificate ACTION', 'Check local keychain or download certificates (check, download)'
|
|
803
822
|
long_desc <<~DESC
|
|
804
823
|
Actions:
|
|
805
824
|
check - Check certificates installed in your Mac's Keychain (not API)
|
|
806
825
|
download ID - Download a certificate from My Signer API
|
|
807
|
-
|
|
826
|
+
|
|
808
827
|
Note: 'check' scans your LOCAL Keychain, not certificates in My Signer API.
|
|
809
828
|
Use 'mysigner certificates' to see API certificates.
|
|
810
829
|
DESC
|
|
@@ -816,151 +835,157 @@ module Mysigner
|
|
|
816
835
|
case action
|
|
817
836
|
when 'check'
|
|
818
837
|
require_relative '../signing/certificate_checker'
|
|
819
|
-
|
|
820
|
-
say
|
|
821
|
-
say
|
|
822
|
-
|
|
838
|
+
|
|
839
|
+
say '🔍 Checking local certificates...', :cyan
|
|
840
|
+
say ''
|
|
841
|
+
|
|
823
842
|
checker = Signing::CertificateChecker.new
|
|
824
|
-
|
|
843
|
+
|
|
825
844
|
begin
|
|
826
845
|
certificates = checker.check!
|
|
827
|
-
|
|
846
|
+
|
|
828
847
|
if certificates.empty?
|
|
829
|
-
say
|
|
830
|
-
say
|
|
831
|
-
say
|
|
832
|
-
say
|
|
833
|
-
say
|
|
834
|
-
say
|
|
835
|
-
say
|
|
836
|
-
say
|
|
837
|
-
say
|
|
838
|
-
say
|
|
839
|
-
say
|
|
840
|
-
say
|
|
841
|
-
say
|
|
848
|
+
say 'No code signing certificates found in local Keychain', :yellow
|
|
849
|
+
say ''
|
|
850
|
+
say '⚠️ Important:', :yellow
|
|
851
|
+
say ' This command checks certificates INSTALLED ON YOUR MAC.', :white
|
|
852
|
+
say ' Certificates in My Signer API are not automatically installed locally.', :white
|
|
853
|
+
say ''
|
|
854
|
+
say 'To install certificates:', :cyan
|
|
855
|
+
say ' 1. List certificates in My Signer: mysigner certificates', :white
|
|
856
|
+
say ' 2. Download one: mysigner certificate download <ID>', :white
|
|
857
|
+
say ' 3. Double-click the .cer file to install in Keychain', :white
|
|
858
|
+
say ''
|
|
859
|
+
say 'Or download from Apple Developer:', :cyan
|
|
860
|
+
say ' https://developer.apple.com/account/resources/certificates/list', :white
|
|
842
861
|
return
|
|
843
862
|
end
|
|
844
|
-
|
|
863
|
+
|
|
845
864
|
# Group by status
|
|
846
865
|
by_status = checker.by_status
|
|
847
|
-
|
|
866
|
+
|
|
848
867
|
# Show valid certificates
|
|
849
868
|
if by_status[:valid].any?
|
|
850
869
|
say "✓ Valid Certificates (#{by_status[:valid].count})", :green
|
|
851
|
-
say
|
|
870
|
+
say ''
|
|
852
871
|
by_status[:valid].each do |cert|
|
|
853
872
|
say " #{cert[:name]}", :green
|
|
854
873
|
say " Type: #{cert[:type]}"
|
|
855
874
|
say " Team: #{cert[:team_id] || 'Unknown'}"
|
|
856
|
-
say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
|
|
857
|
-
|
|
875
|
+
say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
|
|
876
|
+
:white
|
|
877
|
+
say ''
|
|
858
878
|
end
|
|
859
879
|
end
|
|
860
|
-
|
|
880
|
+
|
|
861
881
|
# Show expiring soon certificates
|
|
862
882
|
if by_status[:expiring_soon].any?
|
|
863
883
|
say "⚠️ Expiring Soon (#{by_status[:expiring_soon].count})", :yellow
|
|
864
|
-
say
|
|
884
|
+
say ''
|
|
865
885
|
by_status[:expiring_soon].each do |cert|
|
|
866
886
|
say " #{cert[:name]}", :yellow
|
|
867
887
|
say " Type: #{cert[:type]}"
|
|
868
888
|
say " Team: #{cert[:team_id] || 'Unknown'}"
|
|
869
|
-
say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
|
|
870
|
-
|
|
889
|
+
say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
|
|
890
|
+
:yellow
|
|
891
|
+
say ''
|
|
871
892
|
end
|
|
872
|
-
say
|
|
873
|
-
say
|
|
893
|
+
say 'Renew these certificates soon to avoid build failures!', :yellow
|
|
894
|
+
say ''
|
|
874
895
|
end
|
|
875
|
-
|
|
896
|
+
|
|
876
897
|
# Show expired certificates
|
|
877
898
|
if by_status[:expired].any?
|
|
878
899
|
say "✗ Expired Certificates (#{by_status[:expired].count})", :red
|
|
879
|
-
say
|
|
900
|
+
say ''
|
|
880
901
|
by_status[:expired].each do |cert|
|
|
881
902
|
say " #{cert[:name]}", :red
|
|
882
903
|
say " Type: #{cert[:type]}"
|
|
883
904
|
say " Team: #{cert[:team_id] || 'Unknown'}"
|
|
884
|
-
say " Expired: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry].abs} days ago)",
|
|
885
|
-
|
|
905
|
+
say " Expired: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry].abs} days ago)",
|
|
906
|
+
:red
|
|
907
|
+
say ''
|
|
886
908
|
end
|
|
887
|
-
say
|
|
888
|
-
say
|
|
889
|
-
say
|
|
909
|
+
say 'These certificates will cause build failures. Renew them at:', :red
|
|
910
|
+
say ' https://developer.apple.com/account/resources/certificates/list', :white
|
|
911
|
+
say ''
|
|
890
912
|
end
|
|
891
|
-
|
|
913
|
+
|
|
892
914
|
# Summary
|
|
893
|
-
say
|
|
894
|
-
say "Total: #{certificates.count} certificate#{
|
|
915
|
+
say '─' * 80, :cyan
|
|
916
|
+
say "Total: #{certificates.count} certificate#{'s' unless certificates.one?} installed locally",
|
|
917
|
+
:cyan
|
|
895
918
|
if checker.has_issues?
|
|
896
|
-
say
|
|
919
|
+
say 'Status: ⚠️ Action required', :yellow
|
|
897
920
|
else
|
|
898
|
-
say
|
|
921
|
+
say 'Status: ✓ All certificates valid', :green
|
|
899
922
|
end
|
|
900
|
-
say
|
|
901
|
-
say
|
|
902
|
-
say
|
|
903
|
-
|
|
923
|
+
say ''
|
|
924
|
+
say '💡 Tip: These are certificates INSTALLED ON YOUR MAC.', :cyan
|
|
925
|
+
say ' To see all certificates in My Signer API, run: mysigner certificates', :white
|
|
904
926
|
rescue Signing::CertificateChecker::CheckError => e
|
|
905
927
|
error "Certificate check failed: #{e.message}"
|
|
906
|
-
say
|
|
907
|
-
say
|
|
908
|
-
say
|
|
909
|
-
say
|
|
910
|
-
say
|
|
928
|
+
say ''
|
|
929
|
+
say 'This usually means:', :yellow
|
|
930
|
+
say ' • Keychain is locked', :white
|
|
931
|
+
say ' • No certificates installed', :white
|
|
932
|
+
say ' • Security command not available', :white
|
|
911
933
|
exit 1
|
|
912
934
|
end
|
|
913
|
-
|
|
935
|
+
|
|
914
936
|
when 'download'
|
|
915
937
|
if args.empty?
|
|
916
|
-
error
|
|
938
|
+
error 'Usage: mysigner certificate download ID [--output path.cer]'
|
|
917
939
|
exit 1
|
|
918
940
|
end
|
|
919
941
|
|
|
920
942
|
certificate_id = args[0]
|
|
921
943
|
|
|
922
|
-
say
|
|
923
|
-
say
|
|
944
|
+
say '🔐 Downloading certificate...', :cyan
|
|
945
|
+
say ''
|
|
924
946
|
|
|
925
947
|
begin
|
|
926
948
|
# Get certificate details first
|
|
927
949
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}")
|
|
928
950
|
certificate = response[:data]
|
|
929
951
|
|
|
930
|
-
# Determine output path
|
|
952
|
+
# Determine output path. Default to ~/Downloads/ (mirrors
|
|
953
|
+
# profile download behavior) rather than the CWD to avoid
|
|
954
|
+
# dropping .cer files inside the user's project tree.
|
|
931
955
|
output_path = if options[:output]
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
956
|
+
options[:output]
|
|
957
|
+
else
|
|
958
|
+
name = certificate['name'] || "certificate_#{certificate['id']}"
|
|
959
|
+
filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
|
|
960
|
+
downloads_dir = File.expand_path('~/Downloads')
|
|
961
|
+
FileUtils.mkdir_p(downloads_dir)
|
|
962
|
+
File.join(downloads_dir, "#{filename}.cer")
|
|
963
|
+
end
|
|
939
964
|
|
|
940
965
|
# Download the certificate content (binary response)
|
|
941
966
|
download_url = "/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}/download"
|
|
942
|
-
|
|
943
|
-
say
|
|
944
|
-
|
|
967
|
+
|
|
968
|
+
say 'Fetching certificate content...', :yellow
|
|
969
|
+
|
|
945
970
|
# Use Faraday directly with proper auth for binary download
|
|
946
971
|
conn = Faraday.new(url: config.api_url) do |f|
|
|
947
972
|
f.request :authorization, 'Bearer', config.api_token
|
|
948
973
|
f.headers['X-User-Email'] = config.user_email if config.user_email
|
|
949
974
|
f.adapter Faraday.default_adapter
|
|
950
975
|
end
|
|
951
|
-
|
|
976
|
+
|
|
952
977
|
response = conn.get(download_url) do |req|
|
|
953
|
-
req.options.timeout = 30
|
|
954
|
-
req.options.open_timeout = 10
|
|
978
|
+
req.options.timeout = 30 # 30 second timeout
|
|
979
|
+
req.options.open_timeout = 10 # 10 second connection timeout
|
|
955
980
|
end
|
|
956
|
-
|
|
981
|
+
|
|
957
982
|
unless response.success?
|
|
958
983
|
# Check if it's a JSON error response
|
|
959
984
|
if response.headers['content-type']&.include?('json')
|
|
960
985
|
begin
|
|
961
986
|
error_data = JSON.parse(response.body)
|
|
962
987
|
error "Download failed: #{error_data['message'] || error_data['error']}"
|
|
963
|
-
rescue
|
|
988
|
+
rescue StandardError
|
|
964
989
|
error "Download failed with status #{response.status}"
|
|
965
990
|
end
|
|
966
991
|
else
|
|
@@ -972,59 +997,59 @@ module Mysigner
|
|
|
972
997
|
# Write binary content directly to file
|
|
973
998
|
File.binwrite(output_path, response.body)
|
|
974
999
|
|
|
975
|
-
say
|
|
976
|
-
say
|
|
977
|
-
say
|
|
1000
|
+
say '✓ Certificate downloaded successfully!', :green
|
|
1001
|
+
say ''
|
|
1002
|
+
say 'Details:', :bold
|
|
978
1003
|
say " Name: #{certificate['name']}"
|
|
979
1004
|
say " Type: #{certificate['certificate_type'] || 'N/A'}"
|
|
980
1005
|
say " Serial: #{certificate['serial_number'] || 'N/A'}"
|
|
981
1006
|
say " Status: #{certificate['status'] || 'UNKNOWN'}"
|
|
982
1007
|
say " File: #{output_path}"
|
|
983
|
-
say
|
|
1008
|
+
say ''
|
|
984
1009
|
say "File size: #{response.body.bytesize} bytes", :yellow
|
|
985
1010
|
rescue Mysigner::NotFoundError
|
|
986
1011
|
error "Certificate not found with ID: #{certificate_id}"
|
|
987
|
-
say
|
|
988
|
-
say
|
|
989
|
-
say
|
|
990
|
-
say
|
|
991
|
-
say
|
|
992
|
-
say
|
|
993
|
-
say
|
|
1012
|
+
say ''
|
|
1013
|
+
say '💡 Certificate Not Found: How to fix', :cyan
|
|
1014
|
+
say ''
|
|
1015
|
+
say ' → List available certificates: mysigner certificates', :yellow
|
|
1016
|
+
say ' → Sync from Apple: mysigner sync ios', :yellow
|
|
1017
|
+
say ' → Check the ID is correct (IDs are numeric)', :yellow
|
|
1018
|
+
say ''
|
|
994
1019
|
exit 1
|
|
995
1020
|
rescue Mysigner::ClientError => e
|
|
996
1021
|
error "Failed to download certificate: #{e.message}"
|
|
997
|
-
say
|
|
998
|
-
say
|
|
999
|
-
say
|
|
1000
|
-
say
|
|
1001
|
-
say
|
|
1002
|
-
say
|
|
1003
|
-
say
|
|
1004
|
-
exit 1
|
|
1005
|
-
rescue => e
|
|
1022
|
+
say ''
|
|
1023
|
+
say '💡 Download Failed: Try these steps', :cyan
|
|
1024
|
+
say ''
|
|
1025
|
+
say ' → Check your network connection', :yellow
|
|
1026
|
+
say ' → Verify API token is valid: mysigner status', :yellow
|
|
1027
|
+
say ' → Re-authenticate if needed: mysigner login', :yellow
|
|
1028
|
+
say ''
|
|
1029
|
+
exit 1
|
|
1030
|
+
rescue StandardError => e
|
|
1006
1031
|
error "Failed to save file: #{e.message}"
|
|
1007
|
-
say
|
|
1008
|
-
say
|
|
1009
|
-
say
|
|
1010
|
-
say
|
|
1011
|
-
say
|
|
1012
|
-
say
|
|
1013
|
-
say
|
|
1032
|
+
say ''
|
|
1033
|
+
say '💡 File Save Failed: Check these', :cyan
|
|
1034
|
+
say ''
|
|
1035
|
+
say ' → Verify you have write permissions to the directory', :yellow
|
|
1036
|
+
say ' → Check disk space is available', :yellow
|
|
1037
|
+
say ' → Try specifying a different output path with --output', :yellow
|
|
1038
|
+
say ''
|
|
1014
1039
|
exit 1
|
|
1015
1040
|
end
|
|
1016
1041
|
when 'help'
|
|
1017
1042
|
invoke :help, ['certificate']
|
|
1018
1043
|
else
|
|
1019
1044
|
error "Unknown action: #{action}"
|
|
1020
|
-
say
|
|
1045
|
+
say 'Available actions: check, download, help', :yellow
|
|
1021
1046
|
exit 1
|
|
1022
1047
|
end
|
|
1023
1048
|
end
|
|
1024
1049
|
|
|
1025
1050
|
# ==================== ANDROID KEYSTORES ====================
|
|
1026
1051
|
|
|
1027
|
-
desc
|
|
1052
|
+
desc 'keystore SUBCOMMAND', 'Manage Android keystores (list, upload, download, delete, activate)'
|
|
1028
1053
|
long_desc <<~DESC
|
|
1029
1054
|
Manage Android keystores for signing your apps.
|
|
1030
1055
|
|
|
@@ -1087,38 +1112,38 @@ module Mysigner
|
|
|
1087
1112
|
|
|
1088
1113
|
case action
|
|
1089
1114
|
when 'list'
|
|
1090
|
-
say
|
|
1091
|
-
say
|
|
1115
|
+
say '🔐 Android Keystores', :cyan
|
|
1116
|
+
say ''
|
|
1092
1117
|
|
|
1093
1118
|
keystores = manager.list(android_app_id: options[:app_id])
|
|
1094
1119
|
|
|
1095
1120
|
if keystores.empty?
|
|
1096
|
-
say
|
|
1097
|
-
say
|
|
1098
|
-
say
|
|
1121
|
+
say 'No keystores found', :yellow
|
|
1122
|
+
say ''
|
|
1123
|
+
say 'Upload a keystore with: mysigner keystore upload PATH', :yellow
|
|
1099
1124
|
return
|
|
1100
1125
|
end
|
|
1101
1126
|
|
|
1102
1127
|
keystores.each do |ks|
|
|
1103
1128
|
active_icon = ks['active'] ? '✓' : '○'
|
|
1104
1129
|
active_color = ks['active'] ? :green : :white
|
|
1105
|
-
|
|
1130
|
+
|
|
1106
1131
|
say " #{active_icon} #{ks['name']} (ID: #{ks['id']})", active_color
|
|
1107
1132
|
say " Key Alias: #{ks['key_alias'] || 'N/A'}"
|
|
1108
1133
|
say " App: #{ks['package_name']}" if ks['package_name']
|
|
1109
1134
|
say " Active: #{ks['active'] ? 'Yes' : 'No'}"
|
|
1110
|
-
say
|
|
1135
|
+
say ''
|
|
1111
1136
|
end
|
|
1112
1137
|
|
|
1113
1138
|
say "Total: #{keystores.count} keystore(s)", :yellow
|
|
1114
1139
|
|
|
1115
1140
|
when 'upload'
|
|
1116
1141
|
keystore_path = args[0]
|
|
1117
|
-
|
|
1142
|
+
|
|
1118
1143
|
unless keystore_path
|
|
1119
|
-
error
|
|
1120
|
-
say
|
|
1121
|
-
say
|
|
1144
|
+
error 'Usage: mysigner keystore upload PATH'
|
|
1145
|
+
say ''
|
|
1146
|
+
say 'Example: mysigner keystore upload ~/keys/release.jks', :yellow
|
|
1122
1147
|
exit 1
|
|
1123
1148
|
end
|
|
1124
1149
|
|
|
@@ -1127,17 +1152,36 @@ module Mysigner
|
|
|
1127
1152
|
exit 1
|
|
1128
1153
|
end
|
|
1129
1154
|
|
|
1130
|
-
say
|
|
1131
|
-
say
|
|
1155
|
+
say '🔐 Uploading keystore...', :cyan
|
|
1156
|
+
say ''
|
|
1132
1157
|
|
|
1133
|
-
#
|
|
1158
|
+
# Phase 0: support non-TTY automation. `ask(echo: false)` raises
|
|
1159
|
+
# Errno::ENOTTY on piped stdin, which made `keystore upload`
|
|
1160
|
+
# unusable from CI. When stdin isn't a TTY, require passwords
|
|
1161
|
+
# to come from env vars (MYSIGNER_KEYSTORE_PASSWORD /
|
|
1162
|
+
# MYSIGNER_KEY_PASSWORD) so operators can script uploads
|
|
1163
|
+
# without also having to wrap every invocation in `expect`.
|
|
1134
1164
|
name = options[:name] || ask("Keystore name (e.g., 'Release Key'):")
|
|
1135
|
-
key_alias = options[:alias] || ask(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1165
|
+
key_alias = options[:alias] || ask('Key alias:')
|
|
1166
|
+
|
|
1167
|
+
if $stdin.tty?
|
|
1168
|
+
password = ask('Keystore password:', echo: false)
|
|
1169
|
+
say ''
|
|
1170
|
+
key_password = ask('Key password (press Enter if same as keystore):', echo: false)
|
|
1171
|
+
say ''
|
|
1172
|
+
key_password = password if key_password.empty?
|
|
1173
|
+
else
|
|
1174
|
+
password = ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
|
|
1175
|
+
key_password = ENV['MYSIGNER_KEY_PASSWORD'] || password
|
|
1176
|
+
if password.nil? || password.empty?
|
|
1177
|
+
error 'Non-interactive upload requires MYSIGNER_KEYSTORE_PASSWORD (and optionally MYSIGNER_KEY_PASSWORD) in the environment.'
|
|
1178
|
+
say ''
|
|
1179
|
+
say 'Example:', :cyan
|
|
1180
|
+
say ' MYSIGNER_KEYSTORE_PASSWORD=... MYSIGNER_KEY_PASSWORD=... \\', :yellow
|
|
1181
|
+
say ' mysigner keystore upload ./release.jks --name "Release" --alias myalias', :yellow
|
|
1182
|
+
exit 1
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1141
1185
|
|
|
1142
1186
|
begin
|
|
1143
1187
|
result = manager.upload(
|
|
@@ -1150,52 +1194,51 @@ module Mysigner
|
|
|
1150
1194
|
active: true
|
|
1151
1195
|
)
|
|
1152
1196
|
|
|
1153
|
-
say
|
|
1154
|
-
say
|
|
1155
|
-
say
|
|
1197
|
+
say '✓ Keystore uploaded successfully!', :green
|
|
1198
|
+
say ''
|
|
1199
|
+
say 'Details:', :bold
|
|
1156
1200
|
say " ID: #{result['id']}"
|
|
1157
1201
|
say " Name: #{result['name']}"
|
|
1158
1202
|
say " Key Alias: #{result['key_alias']}"
|
|
1159
1203
|
say " Active: #{result['active']}"
|
|
1160
|
-
say
|
|
1161
|
-
|
|
1204
|
+
say ''
|
|
1162
1205
|
rescue Signing::KeystoreManager::KeystoreError => e
|
|
1163
1206
|
error "Upload failed: #{e.message}"
|
|
1164
|
-
say
|
|
1165
|
-
say
|
|
1166
|
-
say
|
|
1167
|
-
say
|
|
1168
|
-
say
|
|
1169
|
-
say
|
|
1170
|
-
say
|
|
1171
|
-
say
|
|
1207
|
+
say ''
|
|
1208
|
+
say '💡 Keystore Upload Failed: Common issues', :cyan
|
|
1209
|
+
say ''
|
|
1210
|
+
say ' → Verify the keystore file is valid (.jks or .keystore)', :yellow
|
|
1211
|
+
say ' → Check keystore password is correct', :yellow
|
|
1212
|
+
say ' → Check key alias exists in the keystore', :yellow
|
|
1213
|
+
say ' → Verify key password is correct', :yellow
|
|
1214
|
+
say ''
|
|
1172
1215
|
say " Test with: keytool -list -keystore #{keystore_path}", :green
|
|
1173
|
-
say
|
|
1216
|
+
say ''
|
|
1174
1217
|
exit 1
|
|
1175
1218
|
rescue Mysigner::ClientError => e
|
|
1176
1219
|
error "API error: #{e.message}"
|
|
1177
|
-
say
|
|
1178
|
-
say
|
|
1179
|
-
say
|
|
1180
|
-
say
|
|
1181
|
-
say
|
|
1182
|
-
say
|
|
1183
|
-
say
|
|
1220
|
+
say ''
|
|
1221
|
+
say '💡 API Error: Try these steps', :cyan
|
|
1222
|
+
say ''
|
|
1223
|
+
say ' → Check your network connection', :yellow
|
|
1224
|
+
say ' → Verify API token is valid: mysigner status', :yellow
|
|
1225
|
+
say ' → Re-authenticate if needed: mysigner login', :yellow
|
|
1226
|
+
say ''
|
|
1184
1227
|
exit 1
|
|
1185
1228
|
end
|
|
1186
1229
|
|
|
1187
1230
|
when 'download'
|
|
1188
1231
|
keystore_id = args[0]
|
|
1189
|
-
|
|
1232
|
+
|
|
1190
1233
|
unless keystore_id
|
|
1191
|
-
error
|
|
1192
|
-
say
|
|
1234
|
+
error 'Usage: mysigner keystore download ID'
|
|
1235
|
+
say ''
|
|
1193
1236
|
say "Run 'mysigner keystore list' to see available IDs", :yellow
|
|
1194
1237
|
exit 1
|
|
1195
1238
|
end
|
|
1196
1239
|
|
|
1197
|
-
say
|
|
1198
|
-
say
|
|
1240
|
+
say '🔐 Downloading keystore...', :cyan
|
|
1241
|
+
say ''
|
|
1199
1242
|
|
|
1200
1243
|
begin
|
|
1201
1244
|
result = manager.download(keystore_id)
|
|
@@ -1206,110 +1249,109 @@ module Mysigner
|
|
|
1206
1249
|
result[:path] = options[:output]
|
|
1207
1250
|
end
|
|
1208
1251
|
|
|
1209
|
-
say
|
|
1210
|
-
say
|
|
1211
|
-
say
|
|
1252
|
+
say '✓ Keystore downloaded!', :green
|
|
1253
|
+
say ''
|
|
1254
|
+
say 'Details:', :bold
|
|
1212
1255
|
say " Name: #{result[:name]}"
|
|
1213
1256
|
say " Key Alias: #{result[:key_alias]}"
|
|
1214
1257
|
say " Path: #{result[:path]}"
|
|
1215
|
-
say
|
|
1216
|
-
say
|
|
1217
|
-
|
|
1258
|
+
say ''
|
|
1259
|
+
say '⚠️ Keep this file secure and backed up!', :yellow
|
|
1218
1260
|
rescue Signing::KeystoreManager::KeystoreNotFoundError => e
|
|
1219
1261
|
error "Keystore not found: #{e.message}"
|
|
1220
|
-
say
|
|
1221
|
-
say
|
|
1222
|
-
say
|
|
1223
|
-
say
|
|
1224
|
-
say
|
|
1225
|
-
say
|
|
1226
|
-
say
|
|
1262
|
+
say ''
|
|
1263
|
+
say '💡 Keystore Not Found: How to fix', :cyan
|
|
1264
|
+
say ''
|
|
1265
|
+
say ' → List available keystores: mysigner keystore list', :yellow
|
|
1266
|
+
say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
|
|
1267
|
+
say ' → Check the ID is correct (IDs are numeric)', :yellow
|
|
1268
|
+
say ''
|
|
1227
1269
|
exit 1
|
|
1228
1270
|
rescue Signing::KeystoreManager::DownloadError => e
|
|
1229
1271
|
error "Download failed: #{e.message}"
|
|
1230
|
-
say
|
|
1231
|
-
say
|
|
1232
|
-
say
|
|
1233
|
-
say
|
|
1234
|
-
say
|
|
1235
|
-
say
|
|
1236
|
-
say
|
|
1272
|
+
say ''
|
|
1273
|
+
say '💡 Download Failed: Try these steps', :cyan
|
|
1274
|
+
say ''
|
|
1275
|
+
say ' → Check your network connection', :yellow
|
|
1276
|
+
say ' → Verify API token is valid: mysigner status', :yellow
|
|
1277
|
+
say ' → Re-authenticate if needed: mysigner login', :yellow
|
|
1278
|
+
say ''
|
|
1237
1279
|
exit 1
|
|
1238
1280
|
end
|
|
1239
1281
|
|
|
1240
1282
|
when 'delete'
|
|
1241
1283
|
keystore_id = args[0]
|
|
1242
|
-
|
|
1284
|
+
|
|
1243
1285
|
unless keystore_id
|
|
1244
|
-
error
|
|
1286
|
+
error 'Usage: mysigner keystore delete ID'
|
|
1245
1287
|
exit 1
|
|
1246
1288
|
end
|
|
1247
1289
|
|
|
1248
1290
|
# Get keystore details first
|
|
1249
1291
|
keystores = manager.list
|
|
1250
1292
|
keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
|
|
1251
|
-
|
|
1293
|
+
|
|
1252
1294
|
unless keystore
|
|
1253
1295
|
error "Keystore not found with ID: #{keystore_id}"
|
|
1254
|
-
say
|
|
1255
|
-
say
|
|
1256
|
-
say
|
|
1257
|
-
say
|
|
1258
|
-
say
|
|
1259
|
-
say
|
|
1296
|
+
say ''
|
|
1297
|
+
say '💡 Keystore Not Found: How to fix', :cyan
|
|
1298
|
+
say ''
|
|
1299
|
+
say ' → List available keystores: mysigner keystore list', :yellow
|
|
1300
|
+
say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
|
|
1301
|
+
say ''
|
|
1260
1302
|
exit 1
|
|
1261
1303
|
end
|
|
1262
1304
|
|
|
1263
|
-
say
|
|
1305
|
+
say '⚠️ You are about to delete:', :yellow
|
|
1264
1306
|
say " Name: #{keystore['name']}"
|
|
1265
1307
|
say " Key Alias: #{keystore['key_alias']}"
|
|
1266
|
-
say
|
|
1308
|
+
say ''
|
|
1267
1309
|
|
|
1268
|
-
if yes?(
|
|
1310
|
+
if yes?('Are you sure? This cannot be undone. (y/n)')
|
|
1269
1311
|
begin
|
|
1270
1312
|
manager.delete(keystore_id)
|
|
1271
|
-
say
|
|
1272
|
-
say
|
|
1313
|
+
say ''
|
|
1314
|
+
say '✓ Keystore deleted', :green
|
|
1273
1315
|
rescue Mysigner::ClientError => e
|
|
1274
1316
|
error "Delete failed: #{e.message}"
|
|
1275
1317
|
exit 1
|
|
1276
1318
|
end
|
|
1277
1319
|
else
|
|
1278
|
-
say
|
|
1320
|
+
say 'Deletion cancelled', :yellow
|
|
1279
1321
|
end
|
|
1280
1322
|
|
|
1281
1323
|
when 'activate'
|
|
1282
1324
|
keystore_id = args[0]
|
|
1283
|
-
|
|
1325
|
+
|
|
1284
1326
|
unless keystore_id
|
|
1285
|
-
error
|
|
1327
|
+
error 'Usage: mysigner keystore activate ID'
|
|
1286
1328
|
exit 1
|
|
1287
1329
|
end
|
|
1288
1330
|
|
|
1289
|
-
say
|
|
1331
|
+
say '🔐 Activating keystore...', :cyan
|
|
1290
1332
|
|
|
1291
1333
|
begin
|
|
1292
1334
|
result = manager.activate(keystore_id)
|
|
1293
|
-
say
|
|
1294
|
-
say
|
|
1335
|
+
say '✓ Keystore activated!', :green
|
|
1336
|
+
say ''
|
|
1295
1337
|
say "#{result['name']} is now the default keystore", :cyan
|
|
1296
1338
|
rescue Mysigner::NotFoundError
|
|
1297
1339
|
error "Keystore not found with ID: #{keystore_id}"
|
|
1298
|
-
say
|
|
1299
|
-
say
|
|
1300
|
-
say
|
|
1301
|
-
say
|
|
1302
|
-
say
|
|
1303
|
-
say
|
|
1340
|
+
say ''
|
|
1341
|
+
say '💡 Keystore Not Found: How to fix', :cyan
|
|
1342
|
+
say ''
|
|
1343
|
+
say ' → List available keystores: mysigner keystore list', :yellow
|
|
1344
|
+
say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
|
|
1345
|
+
say ''
|
|
1304
1346
|
exit 1
|
|
1305
1347
|
rescue Mysigner::ClientError => e
|
|
1306
1348
|
error "Activation failed: #{e.message}"
|
|
1307
|
-
say
|
|
1308
|
-
say
|
|
1309
|
-
say
|
|
1310
|
-
say
|
|
1311
|
-
say
|
|
1312
|
-
say
|
|
1349
|
+
say ''
|
|
1350
|
+
say '💡 Activation Failed: Try these steps', :cyan
|
|
1351
|
+
say ''
|
|
1352
|
+
say ' → Verify keystore ID is correct: mysigner keystore list', :yellow
|
|
1353
|
+
say ' → Check API token is valid: mysigner status', :yellow
|
|
1354
|
+
say ''
|
|
1313
1355
|
exit 1
|
|
1314
1356
|
end
|
|
1315
1357
|
|
|
@@ -1317,14 +1359,14 @@ module Mysigner
|
|
|
1317
1359
|
invoke :help, ['keystore']
|
|
1318
1360
|
else
|
|
1319
1361
|
error "Unknown action: #{action}"
|
|
1320
|
-
say
|
|
1362
|
+
say 'Available actions: list, upload, download, delete, activate, help', :yellow
|
|
1321
1363
|
exit 1
|
|
1322
1364
|
end
|
|
1323
1365
|
end
|
|
1324
1366
|
|
|
1325
1367
|
# ==================== ANDROID APP REGISTRATION ====================
|
|
1326
1368
|
|
|
1327
|
-
desc
|
|
1369
|
+
desc 'android SUBCOMMAND', 'Android commands (init, add, build, list)'
|
|
1328
1370
|
long_desc <<~DESC
|
|
1329
1371
|
Register and manage Android apps with My Signer.
|
|
1330
1372
|
|
|
@@ -1375,9 +1417,9 @@ module Mysigner
|
|
|
1375
1417
|
android_init(config, client)
|
|
1376
1418
|
when 'add'
|
|
1377
1419
|
if args.empty?
|
|
1378
|
-
error
|
|
1379
|
-
say
|
|
1380
|
-
say
|
|
1420
|
+
error 'Usage: mysigner android add PACKAGE_NAME [--name NAME]'
|
|
1421
|
+
say ''
|
|
1422
|
+
say 'Example: mysigner android add com.example.myapp --name "My App"', :yellow
|
|
1381
1423
|
exit 1
|
|
1382
1424
|
end
|
|
1383
1425
|
android_add(config, client, args[0], options[:name])
|
|
@@ -1390,7 +1432,7 @@ module Mysigner
|
|
|
1390
1432
|
invoke :help, ['android']
|
|
1391
1433
|
else
|
|
1392
1434
|
error "Unknown action: #{action}"
|
|
1393
|
-
say
|
|
1435
|
+
say 'Available actions: init, add, build, list, help', :yellow
|
|
1394
1436
|
exit 1
|
|
1395
1437
|
end
|
|
1396
1438
|
end
|
|
@@ -1400,8 +1442,8 @@ module Mysigner
|
|
|
1400
1442
|
def android_init(config, client)
|
|
1401
1443
|
require_relative '../build/android_parser'
|
|
1402
1444
|
|
|
1403
|
-
say
|
|
1404
|
-
say
|
|
1445
|
+
say '🔍 Detecting project...', :cyan
|
|
1446
|
+
say ''
|
|
1405
1447
|
|
|
1406
1448
|
package_name = nil
|
|
1407
1449
|
app_name = nil
|
|
@@ -1422,26 +1464,26 @@ module Mysigner
|
|
|
1422
1464
|
app_name = expo_config[:name]
|
|
1423
1465
|
project_type = 'Expo managed'
|
|
1424
1466
|
else
|
|
1425
|
-
error
|
|
1426
|
-
say
|
|
1467
|
+
error 'No Android project or Expo config found'
|
|
1468
|
+
say ''
|
|
1427
1469
|
say "For Expo managed projects, add 'android.package' to app.json:", :yellow
|
|
1428
|
-
say
|
|
1429
|
-
say
|
|
1430
|
-
say
|
|
1431
|
-
say
|
|
1432
|
-
say
|
|
1433
|
-
say
|
|
1434
|
-
say
|
|
1435
|
-
say
|
|
1470
|
+
say ''
|
|
1471
|
+
say ' {', :white
|
|
1472
|
+
say ' "expo": {', :white
|
|
1473
|
+
say ' "android": { "package": "com.yourcompany.app" }', :white
|
|
1474
|
+
say ' }', :white
|
|
1475
|
+
say ' }', :white
|
|
1476
|
+
say ''
|
|
1477
|
+
say 'Or run from a directory with an android/ folder (native/RN/Capacitor).', :yellow
|
|
1436
1478
|
exit 1
|
|
1437
1479
|
end
|
|
1438
1480
|
end
|
|
1439
1481
|
|
|
1440
1482
|
say "✓ Found: #{project_type} project", :green
|
|
1441
|
-
say
|
|
1483
|
+
say ''
|
|
1442
1484
|
say "📦 Package: #{package_name}", :cyan
|
|
1443
1485
|
say "📱 Name: #{app_name || '(not set)'}", :cyan
|
|
1444
|
-
say
|
|
1486
|
+
say ''
|
|
1445
1487
|
|
|
1446
1488
|
# Check if app already exists
|
|
1447
1489
|
begin
|
|
@@ -1453,21 +1495,21 @@ module Mysigner
|
|
|
1453
1495
|
existing = existing_apps.find { |a| a['package_name'] == package_name }
|
|
1454
1496
|
|
|
1455
1497
|
if existing
|
|
1456
|
-
say
|
|
1457
|
-
say
|
|
1458
|
-
say
|
|
1498
|
+
say 'ℹ️ App already registered!', :yellow
|
|
1499
|
+
say ''
|
|
1500
|
+
say 'Details:', :bold
|
|
1459
1501
|
say " ID: #{existing['id']}"
|
|
1460
1502
|
say " Name: #{existing['name'] || '(not set)'}"
|
|
1461
1503
|
say " Package: #{existing['package_name']}"
|
|
1462
1504
|
say " Builds: #{existing['builds_count'] || 0}"
|
|
1463
|
-
say
|
|
1464
|
-
say
|
|
1465
|
-
if (existing['builds_count'] || 0)
|
|
1466
|
-
say
|
|
1505
|
+
say ''
|
|
1506
|
+
say 'Next steps:', :cyan
|
|
1507
|
+
if (existing['builds_count'] || 0).positive?
|
|
1508
|
+
say ' • Ship to Play Store: mysigner ship internal --platform android', :white
|
|
1467
1509
|
else
|
|
1468
|
-
say
|
|
1469
|
-
say
|
|
1470
|
-
say
|
|
1510
|
+
say ' 1. Build AAB: mysigner android build', :white
|
|
1511
|
+
say ' 2. Upload first build manually in Play Console (one-time requirement)', :white
|
|
1512
|
+
say ' 3. After that: mysigner ship internal --platform android', :white
|
|
1471
1513
|
end
|
|
1472
1514
|
return
|
|
1473
1515
|
end
|
|
@@ -1476,7 +1518,7 @@ module Mysigner
|
|
|
1476
1518
|
end
|
|
1477
1519
|
|
|
1478
1520
|
# Register the app
|
|
1479
|
-
say
|
|
1521
|
+
say '🔗 Registering with My Signer...', :cyan
|
|
1480
1522
|
|
|
1481
1523
|
begin
|
|
1482
1524
|
response = client.post(
|
|
@@ -1488,21 +1530,20 @@ module Mysigner
|
|
|
1488
1530
|
)
|
|
1489
1531
|
|
|
1490
1532
|
app = response[:data]['android_app'] || response[:data]
|
|
1491
|
-
say
|
|
1492
|
-
say
|
|
1493
|
-
say
|
|
1533
|
+
say '✓ App registered successfully!', :green
|
|
1534
|
+
say ''
|
|
1535
|
+
say 'Details:', :bold
|
|
1494
1536
|
say " ID: #{app['id']}"
|
|
1495
1537
|
say " Name: #{app['name'] || '(not set)'}"
|
|
1496
1538
|
say " Package: #{app['package_name']}"
|
|
1497
|
-
say
|
|
1498
|
-
say
|
|
1499
|
-
say
|
|
1500
|
-
say
|
|
1501
|
-
say
|
|
1502
|
-
say
|
|
1503
|
-
|
|
1539
|
+
say ''
|
|
1540
|
+
say 'Next steps:', :cyan
|
|
1541
|
+
say ' 1. Create app in Google Play Console', :white
|
|
1542
|
+
say ' 2. Build AAB: mysigner android build', :white
|
|
1543
|
+
say ' 3. Upload first build manually in Play Console (one-time requirement)', :white
|
|
1544
|
+
say ' 4. After that: mysigner ship internal --platform android', :white
|
|
1504
1545
|
rescue Mysigner::ValidationError => e
|
|
1505
|
-
error
|
|
1546
|
+
error 'Validation failed:'
|
|
1506
1547
|
if e.details
|
|
1507
1548
|
e.details.each do |field, errors|
|
|
1508
1549
|
say " #{field}: #{errors.join(', ')}", :red
|
|
@@ -1519,11 +1560,11 @@ module Mysigner
|
|
|
1519
1560
|
end
|
|
1520
1561
|
|
|
1521
1562
|
def android_add(config, client, package_name, name = nil)
|
|
1522
|
-
say
|
|
1523
|
-
say
|
|
1563
|
+
say '📦 Registering Android app...', :cyan
|
|
1564
|
+
say ''
|
|
1524
1565
|
say " Package: #{package_name}", :white
|
|
1525
1566
|
say " Name: #{name || '(will be synced from Play Store)'}", :white
|
|
1526
|
-
say
|
|
1567
|
+
say ''
|
|
1527
1568
|
|
|
1528
1569
|
begin
|
|
1529
1570
|
response = client.post(
|
|
@@ -1535,25 +1576,24 @@ module Mysigner
|
|
|
1535
1576
|
)
|
|
1536
1577
|
|
|
1537
1578
|
app = response[:data]['android_app'] || response[:data]
|
|
1538
|
-
say
|
|
1539
|
-
say
|
|
1540
|
-
say
|
|
1579
|
+
say '✓ App registered successfully!', :green
|
|
1580
|
+
say ''
|
|
1581
|
+
say 'Details:', :bold
|
|
1541
1582
|
say " ID: #{app['id']}"
|
|
1542
1583
|
say " Name: #{app['name'] || '(not set)'}"
|
|
1543
1584
|
say " Package: #{app['package_name']}"
|
|
1544
|
-
say
|
|
1545
|
-
say
|
|
1546
|
-
if (app['builds_count'] || 0)
|
|
1547
|
-
say
|
|
1585
|
+
say ''
|
|
1586
|
+
say 'Next steps:', :cyan
|
|
1587
|
+
if (app['builds_count'] || 0).positive?
|
|
1588
|
+
say ' • Ship to Play Store: mysigner ship internal --platform android', :white
|
|
1548
1589
|
else
|
|
1549
|
-
say
|
|
1550
|
-
say
|
|
1551
|
-
say
|
|
1552
|
-
say
|
|
1590
|
+
say ' 1. Create app in Google Play Console (if not done)', :white
|
|
1591
|
+
say ' 2. Build AAB: mysigner android build', :white
|
|
1592
|
+
say ' 3. Upload first build manually in Play Console', :white
|
|
1593
|
+
say ' 4. Then use: mysigner ship internal --platform android', :white
|
|
1553
1594
|
end
|
|
1554
|
-
|
|
1555
1595
|
rescue Mysigner::ValidationError => e
|
|
1556
|
-
error
|
|
1596
|
+
error 'Validation failed:'
|
|
1557
1597
|
if e.details
|
|
1558
1598
|
e.details.each do |field, errors|
|
|
1559
1599
|
say " #{field}: #{errors.join(', ')}", :red
|
|
@@ -1564,10 +1604,10 @@ module Mysigner
|
|
|
1564
1604
|
say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
|
|
1565
1605
|
exit 1
|
|
1566
1606
|
rescue Mysigner::ClientError => e
|
|
1567
|
-
if e.message.include?(
|
|
1568
|
-
error
|
|
1569
|
-
say
|
|
1570
|
-
say
|
|
1607
|
+
if e.message.include?('already exists') || e.message.include?('taken')
|
|
1608
|
+
error 'An app with this package name already exists'
|
|
1609
|
+
say ''
|
|
1610
|
+
say 'List your apps with: mysigner android list', :yellow
|
|
1571
1611
|
else
|
|
1572
1612
|
error "Failed to register app: #{e.message}"
|
|
1573
1613
|
end
|
|
@@ -1576,32 +1616,32 @@ module Mysigner
|
|
|
1576
1616
|
end
|
|
1577
1617
|
|
|
1578
1618
|
def android_build
|
|
1579
|
-
say
|
|
1580
|
-
say
|
|
1619
|
+
say '🔨 Building Android App Bundle (AAB)...', :cyan
|
|
1620
|
+
say ''
|
|
1581
1621
|
|
|
1582
1622
|
begin
|
|
1583
1623
|
require_relative '../build/android_parser'
|
|
1584
1624
|
require_relative '../signing/keystore_manager'
|
|
1585
|
-
|
|
1625
|
+
|
|
1586
1626
|
project_dir = Dir.pwd
|
|
1587
1627
|
is_expo = expo_project?(project_dir)
|
|
1588
|
-
|
|
1628
|
+
|
|
1589
1629
|
# For Expo, we may need to regenerate android folder with correct versionCode
|
|
1590
1630
|
# Get package name from app.json first if Expo
|
|
1591
1631
|
if is_expo
|
|
1592
1632
|
expo_config = parse_expo_config(project_dir)
|
|
1593
1633
|
package_name = expo_config[:package_name]
|
|
1594
1634
|
local_version_code = expo_config[:version_code] || 1
|
|
1595
|
-
version_name = expo_config[:version] ||
|
|
1596
|
-
|
|
1635
|
+
version_name = expo_config[:version] || '1.0.0'
|
|
1636
|
+
|
|
1597
1637
|
# Check highest version code from API
|
|
1598
1638
|
highest_version_code = fetch_highest_version_code(package_name)
|
|
1599
1639
|
version_code = local_version_code
|
|
1600
1640
|
needs_increment = highest_version_code && local_version_code <= highest_version_code
|
|
1601
|
-
|
|
1641
|
+
|
|
1602
1642
|
if needs_increment
|
|
1603
1643
|
version_code = highest_version_code + 1
|
|
1604
|
-
|
|
1644
|
+
|
|
1605
1645
|
# Check if android folder already has the correct versionCode
|
|
1606
1646
|
android_dir = File.join(project_dir, 'android')
|
|
1607
1647
|
current_android_version = nil
|
|
@@ -1609,49 +1649,54 @@ module Mysigner
|
|
|
1609
1649
|
build_gradle = File.join(android_dir, 'app', 'build.gradle')
|
|
1610
1650
|
if File.exist?(build_gradle)
|
|
1611
1651
|
content = File.read(build_gradle)
|
|
1612
|
-
if content =~ /versionCode\s+(\d+)/
|
|
1613
|
-
current_android_version = $1.to_i
|
|
1614
|
-
end
|
|
1652
|
+
current_android_version = ::Regexp.last_match(1).to_i if content =~ /versionCode\s+(\d+)/
|
|
1615
1653
|
end
|
|
1616
1654
|
end
|
|
1617
|
-
|
|
1618
|
-
say
|
|
1655
|
+
|
|
1656
|
+
say '🔧 Framework: Expo (React Native)', :white
|
|
1619
1657
|
say "📦 Package: #{package_name}", :white
|
|
1620
|
-
|
|
1658
|
+
|
|
1621
1659
|
if current_android_version == version_code
|
|
1622
1660
|
# Android folder already has correct version, no need to regenerate
|
|
1623
1661
|
say "🔢 Version: #{version_name} (#{version_code})", :white
|
|
1624
|
-
say
|
|
1662
|
+
say ' ↳ Already at correct version code', :green
|
|
1625
1663
|
else
|
|
1626
1664
|
say "🔢 Version: #{version_name} (#{local_version_code} → #{version_code})", :white
|
|
1627
1665
|
say " ↳ Auto-incremented (#{highest_version_code} already on Play Store)", :yellow
|
|
1628
|
-
say
|
|
1629
|
-
|
|
1666
|
+
say ''
|
|
1667
|
+
|
|
1630
1668
|
# Regenerate android folder with new versionCode
|
|
1631
1669
|
say "🔄 Regenerating android folder with version code #{version_code}...", :yellow
|
|
1632
1670
|
regenerate_expo_android(project_dir, version_code)
|
|
1633
1671
|
end
|
|
1634
1672
|
end
|
|
1635
1673
|
end
|
|
1636
|
-
|
|
1674
|
+
|
|
1637
1675
|
# Now detect project (android folder should exist)
|
|
1638
1676
|
project_info = Build::Detector.detect_android(project_dir)
|
|
1639
1677
|
framework = project_info[:framework]
|
|
1640
1678
|
parser = Build::AndroidParser.new(project_info)
|
|
1641
|
-
|
|
1679
|
+
|
|
1642
1680
|
package_name ||= parser.application_id
|
|
1643
1681
|
version_name ||= parser.version_name
|
|
1644
1682
|
local_version_code ||= parser.version_code.to_i
|
|
1645
1683
|
android_dir = project_info[:android_directory] || File.join(project_info[:directory], 'android')
|
|
1646
1684
|
|
|
1647
1685
|
# For non-Expo, check version code now
|
|
1648
|
-
|
|
1686
|
+
if is_expo
|
|
1687
|
+
# Expo - already printed above, just show if no increment was needed
|
|
1688
|
+
unless needs_increment
|
|
1689
|
+
say '🔧 Framework: Expo (React Native)', :white
|
|
1690
|
+
say "📦 Package: #{package_name}", :white
|
|
1691
|
+
say "🔢 Version: #{version_name} (#{version_code})", :white
|
|
1692
|
+
end
|
|
1693
|
+
else
|
|
1649
1694
|
say "🔧 Framework: #{framework.to_s.gsub('_', ' ').capitalize}", :white
|
|
1650
1695
|
say "📦 Package: #{package_name}", :white
|
|
1651
|
-
|
|
1696
|
+
|
|
1652
1697
|
highest_version_code = fetch_highest_version_code(package_name)
|
|
1653
1698
|
version_code = local_version_code
|
|
1654
|
-
|
|
1699
|
+
|
|
1655
1700
|
if highest_version_code && local_version_code <= highest_version_code
|
|
1656
1701
|
version_code = highest_version_code + 1
|
|
1657
1702
|
say "🔢 Version: #{version_name} (#{local_version_code} → #{version_code})", :white
|
|
@@ -1659,84 +1704,78 @@ module Mysigner
|
|
|
1659
1704
|
else
|
|
1660
1705
|
say "🔢 Version: #{version_name} (#{version_code})", :white
|
|
1661
1706
|
end
|
|
1662
|
-
else
|
|
1663
|
-
# Expo - already printed above, just show if no increment was needed
|
|
1664
|
-
unless needs_increment
|
|
1665
|
-
say "🔧 Framework: Expo (React Native)", :white
|
|
1666
|
-
say "📦 Package: #{package_name}", :white
|
|
1667
|
-
say "🔢 Version: #{version_name} (#{version_code})", :white
|
|
1668
|
-
end
|
|
1669
1707
|
end
|
|
1670
|
-
say
|
|
1708
|
+
say ''
|
|
1671
1709
|
|
|
1672
1710
|
# Try to get keystore from MySigner
|
|
1673
1711
|
keystore_info = fetch_keystore_for_build(package_name)
|
|
1674
1712
|
if keystore_info
|
|
1675
1713
|
say "🔐 Keystore: #{keystore_info[:name]}", :green
|
|
1676
1714
|
else
|
|
1677
|
-
say
|
|
1715
|
+
say '⚠️ No keystore configured - will use debug signing', :yellow
|
|
1678
1716
|
say " Run 'mysigner android init' to set up release signing", :yellow
|
|
1679
1717
|
end
|
|
1680
|
-
say
|
|
1681
|
-
say
|
|
1682
|
-
say
|
|
1718
|
+
say ''
|
|
1719
|
+
say '⏱️ This may take a few minutes...', :yellow
|
|
1720
|
+
say ''
|
|
1683
1721
|
|
|
1684
1722
|
# Build based on framework (pass version_code override if incremented)
|
|
1685
1723
|
# For Expo, we already regenerated with correct version, so no override needed
|
|
1686
1724
|
version_code_override = nil
|
|
1687
1725
|
unless is_expo
|
|
1688
|
-
version_code_override =
|
|
1726
|
+
version_code_override = version_code == local_version_code ? nil : version_code
|
|
1689
1727
|
end
|
|
1690
|
-
|
|
1728
|
+
|
|
1691
1729
|
aab_path = case framework
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1730
|
+
when :flutter
|
|
1731
|
+
build_flutter_aab(project_dir, keystore_info, version_code_override)
|
|
1732
|
+
when :maui, :xamarin, :xamarin_forms
|
|
1733
|
+
build_dotnet_aab(project_dir, project_info[:csproj_path], framework, keystore_info,
|
|
1734
|
+
version_code_override)
|
|
1735
|
+
when :react_native, :capacitor, :native
|
|
1736
|
+
build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
|
|
1737
|
+
else
|
|
1738
|
+
build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
|
|
1739
|
+
end
|
|
1701
1740
|
|
|
1702
1741
|
unless aab_path && File.exist?(aab_path)
|
|
1703
|
-
error
|
|
1704
|
-
say
|
|
1742
|
+
error 'AAB file not found after build'
|
|
1743
|
+
say 'Check build output for errors.', :yellow
|
|
1705
1744
|
exit 1
|
|
1706
1745
|
end
|
|
1707
1746
|
|
|
1708
|
-
say
|
|
1709
|
-
say
|
|
1710
|
-
say
|
|
1711
|
-
say
|
|
1712
|
-
say
|
|
1747
|
+
say ''
|
|
1748
|
+
say '=' * 80, :green
|
|
1749
|
+
say '✓ Build complete!', :green
|
|
1750
|
+
say '=' * 80, :green
|
|
1751
|
+
say ''
|
|
1713
1752
|
say "📦 AAB: #{aab_path}", :cyan
|
|
1714
1753
|
say "📊 Size: #{format_bytes(File.size(aab_path))}", :cyan
|
|
1715
|
-
say
|
|
1716
|
-
say
|
|
1717
|
-
say
|
|
1718
|
-
say
|
|
1719
|
-
say
|
|
1720
|
-
say
|
|
1721
|
-
say
|
|
1722
|
-
|
|
1754
|
+
say ''
|
|
1755
|
+
say 'Next step:', :bold
|
|
1756
|
+
say ' Upload this AAB to Google Play Console → Internal testing → Create release', :white
|
|
1757
|
+
say ''
|
|
1758
|
+
say 'After uploading, you can use:', :cyan
|
|
1759
|
+
say ' mysigner ship internal --platform android', :green
|
|
1760
|
+
say ''
|
|
1761
|
+
|
|
1723
1762
|
# Open the folder containing the AAB
|
|
1724
1763
|
aab_dir = File.dirname(aab_path)
|
|
1725
|
-
say
|
|
1726
|
-
|
|
1764
|
+
say '📂 Opening folder...', :yellow
|
|
1765
|
+
case RUBY_PLATFORM
|
|
1766
|
+
when /darwin/
|
|
1727
1767
|
system('open', aab_dir)
|
|
1728
|
-
|
|
1768
|
+
when /linux/
|
|
1729
1769
|
system('xdg-open', aab_dir)
|
|
1730
|
-
|
|
1770
|
+
when /mingw|mswin/
|
|
1731
1771
|
system('explorer', aab_dir.gsub('/', '\\'))
|
|
1732
1772
|
end
|
|
1733
|
-
|
|
1734
1773
|
rescue Build::Detector::NoProjectError => e
|
|
1735
1774
|
error e.message
|
|
1736
|
-
say
|
|
1737
|
-
say
|
|
1775
|
+
say ''
|
|
1776
|
+
say 'Run this command from an Android project directory.', :yellow
|
|
1738
1777
|
exit 1
|
|
1739
|
-
rescue => e
|
|
1778
|
+
rescue StandardError => e
|
|
1740
1779
|
error "Build failed: #{e.message}"
|
|
1741
1780
|
exit 1
|
|
1742
1781
|
end
|
|
@@ -1745,42 +1784,69 @@ module Mysigner
|
|
|
1745
1784
|
def build_flutter_aab(project_dir, keystore_info = nil, version_code_override = nil)
|
|
1746
1785
|
# Check for flutter
|
|
1747
1786
|
unless system('which flutter > /dev/null 2>&1')
|
|
1748
|
-
error
|
|
1749
|
-
say
|
|
1787
|
+
error 'Flutter not found in PATH'
|
|
1788
|
+
say 'Install Flutter: https://flutter.dev/docs/get-started/install', :yellow
|
|
1750
1789
|
exit 1
|
|
1751
1790
|
end
|
|
1752
1791
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1792
|
+
# Phase 0: do NOT write plaintext key.properties into the user's
|
|
1793
|
+
# project tree. Use a two-step build so we can inject signing via
|
|
1794
|
+
# a Gradle init script on the Gradle step only. Env vars are set
|
|
1795
|
+
# on the child process; passwords never appear in argv or on disk.
|
|
1796
|
+
require_relative '../signing/gradle_signing_injector'
|
|
1797
|
+
|
|
1798
|
+
injector = nil
|
|
1799
|
+
env = {}
|
|
1800
|
+
init_path = nil
|
|
1801
|
+
|
|
1802
|
+
if keystore_info
|
|
1803
|
+
injector = Mysigner::Signing::GradleSigningInjector.new
|
|
1804
|
+
init_path = injector.write_init_script!
|
|
1805
|
+
env = keystore_info[:signing_env_vars] || injector.env_vars(
|
|
1806
|
+
keystore_path: keystore_info[:path],
|
|
1807
|
+
store_password: keystore_info[:password],
|
|
1808
|
+
key_password: keystore_info[:key_password],
|
|
1809
|
+
key_alias: keystore_info[:key_alias]
|
|
1810
|
+
)
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
begin
|
|
1814
|
+
Dir.chdir(project_dir) do
|
|
1815
|
+
if keystore_info
|
|
1816
|
+
# Step 1: Flutter prepares the Gradle project (no signing needed).
|
|
1817
|
+
prepare_args = ['flutter', 'build', 'appbundle', '--release', '--config-only']
|
|
1818
|
+
prepare_args += ['--build-number', version_code_override.to_s] if version_code_override
|
|
1819
|
+
unless system(env, *prepare_args)
|
|
1820
|
+
error 'Flutter prebuild (--config-only) failed'
|
|
1821
|
+
exit 1
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# Step 2: invoke Gradle directly so we can pass --init-script.
|
|
1825
|
+
Dir.chdir(File.join(project_dir, 'android')) do
|
|
1826
|
+
gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all', '--init-script', init_path]
|
|
1827
|
+
gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
|
|
1828
|
+
unless system(env, *gradle_args)
|
|
1829
|
+
error 'Gradle bundleRelease failed'
|
|
1830
|
+
exit 1
|
|
1831
|
+
end
|
|
1832
|
+
end
|
|
1833
|
+
else
|
|
1834
|
+
# No keystore: plain Flutter build (debug signing).
|
|
1835
|
+
args = ['flutter', 'build', 'appbundle', '--release']
|
|
1836
|
+
args += ['--build-number', version_code_override.to_s] if version_code_override
|
|
1837
|
+
unless system(*args)
|
|
1838
|
+
error 'Flutter build failed'
|
|
1839
|
+
exit 1
|
|
1840
|
+
end
|
|
1841
|
+
end
|
|
1777
1842
|
end
|
|
1843
|
+
ensure
|
|
1844
|
+
injector&.cleanup!
|
|
1778
1845
|
end
|
|
1779
1846
|
|
|
1780
1847
|
# Flutter outputs to build/app/outputs/bundle/release/
|
|
1781
1848
|
aab_path = File.join(project_dir, 'build/app/outputs/bundle/release/app-release.aab')
|
|
1782
1849
|
unless File.exist?(aab_path)
|
|
1783
|
-
# Try alternate paths
|
|
1784
1850
|
alt_paths = Dir.glob(File.join(project_dir, 'build/app/outputs/bundle/*/*.aab'))
|
|
1785
1851
|
aab_path = alt_paths.first if alt_paths.any?
|
|
1786
1852
|
end
|
|
@@ -1798,46 +1864,41 @@ module Mysigner
|
|
|
1798
1864
|
app_json_path = File.join(project_dir, 'app.json')
|
|
1799
1865
|
android_dir = File.join(project_dir, 'android')
|
|
1800
1866
|
local_props_path = File.join(android_dir, 'local.properties')
|
|
1801
|
-
|
|
1867
|
+
|
|
1802
1868
|
# Preserve local.properties if it exists
|
|
1803
1869
|
local_props_content = File.read(local_props_path) if File.exist?(local_props_path)
|
|
1804
|
-
|
|
1870
|
+
|
|
1805
1871
|
# Read original app.json
|
|
1806
1872
|
original_content = File.read(app_json_path)
|
|
1807
1873
|
config = JSON.parse(original_content)
|
|
1808
|
-
|
|
1874
|
+
|
|
1809
1875
|
# Set the new versionCode
|
|
1810
1876
|
config['expo'] ||= {}
|
|
1811
1877
|
config['expo']['android'] ||= {}
|
|
1812
1878
|
config['expo']['android']['versionCode'] = new_version_code
|
|
1813
|
-
|
|
1879
|
+
|
|
1814
1880
|
# Write modified app.json
|
|
1815
1881
|
File.write(app_json_path, JSON.pretty_generate(config))
|
|
1816
|
-
|
|
1882
|
+
|
|
1817
1883
|
begin
|
|
1818
1884
|
# Delete existing android folder
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1885
|
+
|
|
1886
|
+
FileUtils.rm_rf(android_dir)
|
|
1887
|
+
|
|
1823
1888
|
# Run expo prebuild
|
|
1824
1889
|
Dir.chdir(project_dir) do
|
|
1825
1890
|
success = system('npx', 'expo', 'prebuild', '--platform', 'android', '--clean')
|
|
1826
|
-
unless success
|
|
1827
|
-
raise "expo prebuild failed"
|
|
1828
|
-
end
|
|
1891
|
+
raise 'expo prebuild failed' unless success
|
|
1829
1892
|
end
|
|
1830
|
-
|
|
1893
|
+
|
|
1831
1894
|
# Restore local.properties if we had one, or create default
|
|
1832
1895
|
if local_props_content
|
|
1833
1896
|
File.write(local_props_path, local_props_content)
|
|
1834
1897
|
else
|
|
1835
1898
|
# Try to detect Android SDK and create local.properties
|
|
1836
|
-
sdk_path = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT'] ||
|
|
1899
|
+
sdk_path = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT'] ||
|
|
1837
1900
|
File.expand_path('~/Library/Android/sdk')
|
|
1838
|
-
if Dir.exist?(sdk_path)
|
|
1839
|
-
File.write(local_props_path, "sdk.dir=#{sdk_path}\n")
|
|
1840
|
-
end
|
|
1901
|
+
File.write(local_props_path, "sdk.dir=#{sdk_path}\n") if Dir.exist?(sdk_path)
|
|
1841
1902
|
end
|
|
1842
1903
|
ensure
|
|
1843
1904
|
# Restore original app.json
|
|
@@ -1848,19 +1909,22 @@ module Mysigner
|
|
|
1848
1909
|
def fetch_highest_version_code(package_name)
|
|
1849
1910
|
config = Mysigner::Config.new
|
|
1850
1911
|
return nil unless config.exists?
|
|
1912
|
+
|
|
1851
1913
|
config.load
|
|
1852
1914
|
return nil unless config.api_token && config.organization_id
|
|
1853
1915
|
|
|
1854
|
-
client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
1916
|
+
client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
1917
|
+
user_email: config.user_email)
|
|
1855
1918
|
|
|
1856
1919
|
# Find app by package name
|
|
1857
1920
|
response = client.get("/api/v1/organizations/#{config.organization_id}/android_apps")
|
|
1858
1921
|
apps = response[:data]['android_apps'] || []
|
|
1859
1922
|
app = apps.find { |a| a['package_name'] == package_name }
|
|
1860
|
-
|
|
1923
|
+
|
|
1861
1924
|
return app['highest_version_code'].to_i if app && app['highest_version_code']
|
|
1925
|
+
|
|
1862
1926
|
nil
|
|
1863
|
-
rescue
|
|
1927
|
+
rescue StandardError
|
|
1864
1928
|
# Silently fail - we'll use local version
|
|
1865
1929
|
nil
|
|
1866
1930
|
end
|
|
@@ -1868,101 +1932,138 @@ module Mysigner
|
|
|
1868
1932
|
def fetch_keystore_for_build(package_name)
|
|
1869
1933
|
config = Mysigner::Config.new
|
|
1870
1934
|
return nil unless config.exists?
|
|
1935
|
+
|
|
1871
1936
|
config.load
|
|
1872
|
-
return nil unless config.api_token && config.
|
|
1937
|
+
return nil unless config.api_token && config.current_organization_id
|
|
1873
1938
|
|
|
1874
|
-
client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
1875
|
-
|
|
1939
|
+
client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
1940
|
+
user_email: config.user_email)
|
|
1941
|
+
keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
|
|
1876
1942
|
|
|
1877
1943
|
# Find app by package name to get its keystore
|
|
1878
|
-
response = client.get("/api/v1/organizations/#{config.
|
|
1944
|
+
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
|
|
1879
1945
|
apps = response[:data]['android_apps'] || []
|
|
1880
1946
|
app = apps.find { |a| a['package_name'] == package_name }
|
|
1881
1947
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1948
|
+
keystore = nil
|
|
1949
|
+
keystore = keystore_manager.active_keystore(android_app_id: app['id']) if app
|
|
1950
|
+
keystore ||= keystore_manager.active_keystore
|
|
1951
|
+
return nil unless keystore
|
|
1952
|
+
|
|
1953
|
+
# Phase 0: fetch passwords via narrow audit-logged /secrets endpoint
|
|
1954
|
+
# instead of the deprecated ?include_secrets=true param on the list.
|
|
1955
|
+
secrets = keystore_manager.fetch_secrets(keystore['id'])
|
|
1956
|
+
downloaded = keystore_manager.get_or_download(keystore['id'])
|
|
1957
|
+
password = secrets['keystore_password']
|
|
1958
|
+
key_password = secrets['key_password'] || password
|
|
1959
|
+
key_alias = secrets['key_alias'] || keystore['key_alias']
|
|
1960
|
+
{
|
|
1961
|
+
path: downloaded[:path],
|
|
1962
|
+
name: keystore['name'],
|
|
1963
|
+
password: password,
|
|
1964
|
+
key_alias: key_alias,
|
|
1965
|
+
key_password: key_password,
|
|
1966
|
+
id: keystore['id'],
|
|
1967
|
+
# Ready-to-spawn env vars consumed by GradleSigningInjector in
|
|
1968
|
+
# build_gradle_aab / build_flutter_aab / Build::AndroidExecutor.
|
|
1969
|
+
signing_env_vars: {
|
|
1970
|
+
'MYSIGNER_STORE_FILE' => downloaded[:path],
|
|
1971
|
+
'MYSIGNER_STORE_PASSWORD' => password,
|
|
1972
|
+
'MYSIGNER_KEY_PASSWORD' => key_password,
|
|
1973
|
+
'MYSIGNER_KEY_ALIAS' => key_alias
|
|
1908
1974
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
nil
|
|
1912
|
-
rescue => e
|
|
1975
|
+
}
|
|
1976
|
+
rescue StandardError
|
|
1913
1977
|
# Silently fail - we'll use debug signing
|
|
1914
1978
|
nil
|
|
1915
1979
|
end
|
|
1916
1980
|
|
|
1917
|
-
def build_dotnet_aab(project_dir,
|
|
1981
|
+
def build_dotnet_aab(project_dir, _csproj_path, framework, keystore_info = nil, version_code_override = nil)
|
|
1918
1982
|
# Check for dotnet
|
|
1919
1983
|
unless system('which dotnet > /dev/null 2>&1')
|
|
1920
|
-
error
|
|
1921
|
-
say
|
|
1984
|
+
error '.NET SDK not found in PATH'
|
|
1985
|
+
say 'Install .NET: https://dotnet.microsoft.com/download', :yellow
|
|
1922
1986
|
exit 1
|
|
1923
1987
|
end
|
|
1924
1988
|
|
|
1989
|
+
require 'tmpdir'
|
|
1990
|
+
pw_tmpdir = nil
|
|
1991
|
+
store_pw_path = nil
|
|
1992
|
+
key_pw_path = nil
|
|
1993
|
+
|
|
1925
1994
|
Dir.chdir(project_dir) do
|
|
1926
1995
|
base_args = []
|
|
1927
|
-
|
|
1928
|
-
#
|
|
1996
|
+
|
|
1997
|
+
# Phase 0: For AAB output, MSBuild only supports `file:<path>`
|
|
1998
|
+
# for AndroidSigning*Pass (env: is explicitly unsupported for AAB).
|
|
1999
|
+
# Write passwords to 0600 tempfiles whose *paths* go into argv
|
|
2000
|
+
# instead of the passwords themselves.
|
|
1929
2001
|
if keystore_info
|
|
2002
|
+
pw_tmpdir = Dir.mktmpdir('mysigner-maui-pw-')
|
|
2003
|
+
store_pw_path = File.join(pw_tmpdir, 'store_pw.txt')
|
|
2004
|
+
File.write(store_pw_path, keystore_info[:password].to_s)
|
|
2005
|
+
File.chmod(0o600, store_pw_path)
|
|
2006
|
+
|
|
2007
|
+
# Reuse the same file if store/key passwords match (common)
|
|
2008
|
+
if keystore_info[:key_password] == keystore_info[:password]
|
|
2009
|
+
key_pw_path = store_pw_path
|
|
2010
|
+
else
|
|
2011
|
+
key_pw_path = File.join(pw_tmpdir, 'key_pw.txt')
|
|
2012
|
+
File.write(key_pw_path, keystore_info[:key_password].to_s)
|
|
2013
|
+
File.chmod(0o600, key_pw_path)
|
|
2014
|
+
end
|
|
2015
|
+
|
|
1930
2016
|
base_args += [
|
|
1931
|
-
|
|
2017
|
+
'-p:AndroidKeyStore=true',
|
|
1932
2018
|
"-p:AndroidSigningKeyStore=#{keystore_info[:path]}",
|
|
1933
2019
|
"-p:AndroidSigningKeyAlias=#{keystore_info[:key_alias]}",
|
|
1934
|
-
"-p:AndroidSigningKeyPass
|
|
1935
|
-
"-p:AndroidSigningStorePass
|
|
2020
|
+
"-p:AndroidSigningKeyPass=file:#{key_pw_path}",
|
|
2021
|
+
"-p:AndroidSigningStorePass=file:#{store_pw_path}"
|
|
1936
2022
|
]
|
|
1937
2023
|
end
|
|
1938
|
-
|
|
2024
|
+
|
|
1939
2025
|
# Add version code override
|
|
1940
|
-
if version_code_override
|
|
1941
|
-
base_args << "-p:ApplicationVersion=#{version_code_override}"
|
|
1942
|
-
end
|
|
1943
|
-
|
|
1944
|
-
# MAUI uses dotnet publish with Android target
|
|
1945
|
-
if framework == :maui
|
|
1946
|
-
success = system(
|
|
1947
|
-
'dotnet', 'publish',
|
|
1948
|
-
'-f', 'net8.0-android',
|
|
1949
|
-
'-c', 'Release',
|
|
1950
|
-
'-p:AndroidPackageFormat=aab',
|
|
1951
|
-
*base_args
|
|
1952
|
-
)
|
|
1953
|
-
else
|
|
1954
|
-
# Xamarin uses msbuild
|
|
1955
|
-
success = system(
|
|
1956
|
-
'dotnet', 'build',
|
|
1957
|
-
'-c', 'Release',
|
|
1958
|
-
'-p:AndroidPackageFormat=aab',
|
|
1959
|
-
*base_args
|
|
1960
|
-
)
|
|
1961
|
-
end
|
|
2026
|
+
base_args << "-p:ApplicationVersion=#{version_code_override}" if version_code_override
|
|
1962
2027
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2028
|
+
begin
|
|
2029
|
+
# MAUI uses dotnet publish with Android target
|
|
2030
|
+
success = if framework == :maui
|
|
2031
|
+
system(
|
|
2032
|
+
'dotnet', 'publish',
|
|
2033
|
+
'-f', 'net8.0-android',
|
|
2034
|
+
'-c', 'Release',
|
|
2035
|
+
'-p:AndroidPackageFormat=aab',
|
|
2036
|
+
*base_args
|
|
2037
|
+
)
|
|
2038
|
+
else
|
|
2039
|
+
# Xamarin uses msbuild
|
|
2040
|
+
system(
|
|
2041
|
+
'dotnet', 'build',
|
|
2042
|
+
'-c', 'Release',
|
|
2043
|
+
'-p:AndroidPackageFormat=aab',
|
|
2044
|
+
*base_args
|
|
2045
|
+
)
|
|
2046
|
+
end
|
|
2047
|
+
|
|
2048
|
+
unless success
|
|
2049
|
+
error '.NET build failed'
|
|
2050
|
+
exit 1
|
|
2051
|
+
end
|
|
2052
|
+
ensure
|
|
2053
|
+
# Clean up password files. On Windows NTFS the child may still
|
|
2054
|
+
# hold the file open; fall back to at_exit deferred delete.
|
|
2055
|
+
[store_pw_path, key_pw_path].compact.uniq.each do |p|
|
|
2056
|
+
File.delete(p) if File.exist?(p)
|
|
2057
|
+
rescue Errno::EACCES
|
|
2058
|
+
at_exit do
|
|
2059
|
+
File.delete(p)
|
|
2060
|
+
rescue StandardError
|
|
2061
|
+
nil
|
|
2062
|
+
end
|
|
2063
|
+
rescue StandardError
|
|
2064
|
+
# Best effort
|
|
2065
|
+
end
|
|
2066
|
+
FileUtils.rm_rf(pw_tmpdir) if pw_tmpdir && Dir.exist?(pw_tmpdir)
|
|
1966
2067
|
end
|
|
1967
2068
|
end
|
|
1968
2069
|
|
|
@@ -1983,35 +2084,43 @@ module Mysigner
|
|
|
1983
2084
|
when :capacitor
|
|
1984
2085
|
say "Run 'npx cap sync android' first.", :yellow
|
|
1985
2086
|
else
|
|
1986
|
-
say
|
|
2087
|
+
say 'Ensure the android/ folder has gradlew.', :yellow
|
|
1987
2088
|
end
|
|
1988
2089
|
exit 1
|
|
1989
2090
|
end
|
|
1990
2091
|
|
|
1991
|
-
#
|
|
2092
|
+
# Phase 0: inject signing via Gradle init-script + env vars so
|
|
2093
|
+
# passwords never appear in `ps aux` (ORG_GRADLE_PROJECT_<name>
|
|
2094
|
+
# was the old workaround but doesn't support dotted names).
|
|
2095
|
+
require_relative '../signing/gradle_signing_injector'
|
|
2096
|
+
|
|
1992
2097
|
gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all']
|
|
1993
|
-
|
|
2098
|
+
gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
|
|
2099
|
+
|
|
2100
|
+
injector = nil
|
|
2101
|
+
env = {}
|
|
1994
2102
|
if keystore_info
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
# Pass version code override if provided (no file modification needed)
|
|
2005
|
-
if version_code_override
|
|
2006
|
-
gradle_args << "-PversionCode=#{version_code_override}"
|
|
2103
|
+
injector = Mysigner::Signing::GradleSigningInjector.new
|
|
2104
|
+
init_path = injector.write_init_script!
|
|
2105
|
+
env = keystore_info[:signing_env_vars] || injector.env_vars(
|
|
2106
|
+
keystore_path: keystore_info[:path],
|
|
2107
|
+
store_password: keystore_info[:password],
|
|
2108
|
+
key_password: keystore_info[:key_password],
|
|
2109
|
+
key_alias: keystore_info[:key_alias]
|
|
2110
|
+
)
|
|
2111
|
+
gradle_args.insert(1, '--init-script', init_path)
|
|
2007
2112
|
end
|
|
2008
2113
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2114
|
+
begin
|
|
2115
|
+
Dir.chdir(android_dir) do
|
|
2116
|
+
success = system(env, *gradle_args)
|
|
2117
|
+
unless success
|
|
2118
|
+
error 'Gradle build failed'
|
|
2119
|
+
exit 1
|
|
2120
|
+
end
|
|
2014
2121
|
end
|
|
2122
|
+
ensure
|
|
2123
|
+
injector&.cleanup!
|
|
2015
2124
|
end
|
|
2016
2125
|
|
|
2017
2126
|
# Find the AAB
|
|
@@ -2049,11 +2158,9 @@ module Mysigner
|
|
|
2049
2158
|
content = File.read(app_config_path)
|
|
2050
2159
|
# Basic regex extraction for package name
|
|
2051
2160
|
if content =~ /android\s*:\s*\{[^}]*package\s*:\s*["']([^"']+)["']/m
|
|
2052
|
-
package_name =
|
|
2161
|
+
package_name = ::Regexp.last_match(1)
|
|
2053
2162
|
name = nil
|
|
2054
|
-
if content =~ /name\s*:\s*["']([^"']+)["']/
|
|
2055
|
-
name = $1
|
|
2056
|
-
end
|
|
2163
|
+
name = ::Regexp.last_match(1) if content =~ /name\s*:\s*["']([^"']+)["']/
|
|
2057
2164
|
return {
|
|
2058
2165
|
package_name: package_name,
|
|
2059
2166
|
bundle_id: nil,
|
|
@@ -2069,7 +2176,7 @@ module Mysigner
|
|
|
2069
2176
|
|
|
2070
2177
|
# ==================== BUNDLE IDS ====================
|
|
2071
2178
|
|
|
2072
|
-
desc
|
|
2179
|
+
desc 'bundleid SUBCOMMAND', 'Register and manage iOS Bundle IDs'
|
|
2073
2180
|
long_desc <<~DESC
|
|
2074
2181
|
Register and manage iOS Bundle IDs in App Store Connect.
|
|
2075
2182
|
|
|
@@ -2115,34 +2222,38 @@ module Mysigner
|
|
|
2115
2222
|
|
|
2116
2223
|
case action
|
|
2117
2224
|
when 'register'
|
|
2118
|
-
if args.empty?
|
|
2119
|
-
error
|
|
2120
|
-
say
|
|
2121
|
-
say
|
|
2122
|
-
say
|
|
2225
|
+
if args.empty? || args[0].nil? || args[0].to_s.empty?
|
|
2226
|
+
error 'Usage: mysigner bundleid register IDENTIFIER [NAME]'
|
|
2227
|
+
say ''
|
|
2228
|
+
say 'Example: mysigner bundleid register com.company.myapp', :yellow
|
|
2229
|
+
say 'Example: mysigner bundleid register com.company.myapp.widget "My Widget"', :yellow
|
|
2123
2230
|
exit 1
|
|
2231
|
+
return
|
|
2124
2232
|
end
|
|
2125
2233
|
|
|
2126
2234
|
identifier = args[0]
|
|
2127
|
-
# Default name is the last component of the identifier
|
|
2128
|
-
name = args[1] || identifier.split('.').last.capitalize
|
|
2129
2235
|
|
|
2130
|
-
# Validate bundle ID format
|
|
2236
|
+
# Validate bundle ID format BEFORE dereferencing (guards against
|
|
2237
|
+
# `.split` on a degenerate identifier like "123.com.app").
|
|
2131
2238
|
unless identifier =~ /^[a-zA-Z][a-zA-Z0-9.-]*\.[a-zA-Z][a-zA-Z0-9.-]*$/
|
|
2132
2239
|
error "Invalid Bundle ID format: #{identifier}"
|
|
2133
|
-
say
|
|
2134
|
-
say
|
|
2135
|
-
say
|
|
2136
|
-
say
|
|
2137
|
-
say
|
|
2240
|
+
say ''
|
|
2241
|
+
say 'Bundle IDs must:', :yellow
|
|
2242
|
+
say ' • Start with a letter', :cyan
|
|
2243
|
+
say ' • Use reverse domain notation (e.g., com.company.app)', :cyan
|
|
2244
|
+
say ' • Contain only letters, numbers, hyphens, and periods', :cyan
|
|
2138
2245
|
exit 1
|
|
2246
|
+
return
|
|
2139
2247
|
end
|
|
2140
2248
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2249
|
+
# Default name is the last component of the identifier
|
|
2250
|
+
name = args[1] || identifier.split('.').last.capitalize
|
|
2251
|
+
|
|
2252
|
+
say '🔗 Registering Bundle ID...', :cyan
|
|
2253
|
+
say ''
|
|
2143
2254
|
say " Identifier: #{identifier}", :white
|
|
2144
2255
|
say " Name: #{name}", :white
|
|
2145
|
-
say
|
|
2256
|
+
say ''
|
|
2146
2257
|
|
|
2147
2258
|
begin
|
|
2148
2259
|
response = client.post(
|
|
@@ -2155,18 +2266,18 @@ module Mysigner
|
|
|
2155
2266
|
)
|
|
2156
2267
|
|
|
2157
2268
|
bundle_id_data = response[:data]['bundle_id'] || response[:data]
|
|
2158
|
-
say
|
|
2159
|
-
say
|
|
2160
|
-
say
|
|
2269
|
+
say '✓ Bundle ID registered successfully!', :green
|
|
2270
|
+
say ''
|
|
2271
|
+
say 'Details:', :bold
|
|
2161
2272
|
say " Identifier: #{bundle_id_data['identifier'] || identifier}"
|
|
2162
2273
|
say " Name: #{bundle_id_data['name'] || name}"
|
|
2163
|
-
say
|
|
2164
|
-
say
|
|
2165
|
-
say
|
|
2166
|
-
say
|
|
2167
|
-
say
|
|
2274
|
+
say ''
|
|
2275
|
+
say 'Next steps:', :cyan
|
|
2276
|
+
say ' 1. Sync to update local cache: mysigner sync ios', :white
|
|
2277
|
+
say ' 2. Create a provisioning profile: mysigner doctor (will auto-create)', :white
|
|
2278
|
+
say ' 3. Or run: mysigner signing configure', :white
|
|
2168
2279
|
rescue Mysigner::ValidationError => e
|
|
2169
|
-
error
|
|
2280
|
+
error 'Validation failed:'
|
|
2170
2281
|
if e.details
|
|
2171
2282
|
e.details.each do |field, errors|
|
|
2172
2283
|
say " #{field}: #{errors.join(', ')}", :red
|
|
@@ -2177,25 +2288,25 @@ module Mysigner
|
|
|
2177
2288
|
say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
|
|
2178
2289
|
exit 1
|
|
2179
2290
|
rescue Mysigner::ClientError => e
|
|
2180
|
-
if e.message.include?(
|
|
2291
|
+
if e.message.include?('already exists') || e.message.include?('ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE')
|
|
2181
2292
|
say "ℹ️ Bundle ID already registered: #{identifier}", :yellow
|
|
2182
|
-
say
|
|
2183
|
-
say
|
|
2293
|
+
say ''
|
|
2294
|
+
say 'This Bundle ID already exists in App Store Connect.', :white
|
|
2184
2295
|
say "Run 'mysigner sync ios' to update your local cache.", :cyan
|
|
2185
2296
|
else
|
|
2186
2297
|
error "Failed to register Bundle ID: #{e.message}"
|
|
2187
|
-
say
|
|
2188
|
-
say
|
|
2189
|
-
say
|
|
2190
|
-
say
|
|
2191
|
-
say
|
|
2298
|
+
say ''
|
|
2299
|
+
say 'Common issues:', :yellow
|
|
2300
|
+
say ' • Bundle ID already exists (check App Store Connect)', :cyan
|
|
2301
|
+
say ' • Invalid format (must be like com.company.app)', :cyan
|
|
2302
|
+
say ' • API credentials may not have permission', :cyan
|
|
2192
2303
|
end
|
|
2193
2304
|
exit 1
|
|
2194
2305
|
end
|
|
2195
2306
|
|
|
2196
2307
|
when 'list'
|
|
2197
|
-
say
|
|
2198
|
-
say
|
|
2308
|
+
say '📦 Registered Bundle IDs', :cyan
|
|
2309
|
+
say ''
|
|
2199
2310
|
|
|
2200
2311
|
begin
|
|
2201
2312
|
response = client.get(
|
|
@@ -2204,16 +2315,16 @@ module Mysigner
|
|
|
2204
2315
|
bundle_ids = response[:data]['bundle_ids'] || response[:data] || []
|
|
2205
2316
|
|
|
2206
2317
|
if bundle_ids.empty?
|
|
2207
|
-
say
|
|
2208
|
-
say
|
|
2209
|
-
say
|
|
2318
|
+
say ' No Bundle IDs found', :yellow
|
|
2319
|
+
say ''
|
|
2320
|
+
say ' Register one with: mysigner bundleid register com.company.app', :cyan
|
|
2210
2321
|
else
|
|
2211
2322
|
bundle_ids.each do |bid|
|
|
2212
2323
|
identifier = bid['identifier'] || bid['bundle_id']
|
|
2213
2324
|
name = bid['name'] || 'N/A'
|
|
2214
2325
|
say " • #{name}", :green
|
|
2215
2326
|
say " Identifier: #{identifier}"
|
|
2216
|
-
say
|
|
2327
|
+
say ''
|
|
2217
2328
|
end
|
|
2218
2329
|
end
|
|
2219
2330
|
rescue Mysigner::ClientError => e
|
|
@@ -2221,19 +2332,74 @@ module Mysigner
|
|
|
2221
2332
|
exit 1
|
|
2222
2333
|
end
|
|
2223
2334
|
|
|
2335
|
+
when 'delete'
|
|
2336
|
+
if args.empty? || args[0].nil? || args[0].to_s.empty?
|
|
2337
|
+
error 'Usage: mysigner bundleid delete IDENTIFIER'
|
|
2338
|
+
say ''
|
|
2339
|
+
say 'Example: mysigner bundleid delete com.company.myapp', :yellow
|
|
2340
|
+
exit 1
|
|
2341
|
+
return
|
|
2342
|
+
end
|
|
2343
|
+
|
|
2344
|
+
identifier = args[0]
|
|
2345
|
+
|
|
2346
|
+
say '🗑 Removing Bundle ID...', :cyan
|
|
2347
|
+
say " Identifier: #{identifier}", :white
|
|
2348
|
+
say ''
|
|
2349
|
+
|
|
2350
|
+
begin
|
|
2351
|
+
# Resolve the identifier → numeric id (the backend DELETE is keyed on id).
|
|
2352
|
+
list_response = client.get(
|
|
2353
|
+
"/api/v1/organizations/#{config.current_organization_id}/bundle_ids"
|
|
2354
|
+
)
|
|
2355
|
+
bundle_ids = list_response[:data]['bundle_ids'] || list_response[:data] || []
|
|
2356
|
+
bid = bundle_ids.find { |b| (b['identifier'] || b['bundle_id']) == identifier }
|
|
2357
|
+
|
|
2358
|
+
if bid.nil?
|
|
2359
|
+
error "Bundle ID not found: #{identifier}"
|
|
2360
|
+
say ''
|
|
2361
|
+
say ' → List existing: mysigner bundleid list', :yellow
|
|
2362
|
+
exit 1
|
|
2363
|
+
return
|
|
2364
|
+
end
|
|
2365
|
+
|
|
2366
|
+
client.delete(
|
|
2367
|
+
"/api/v1/organizations/#{config.current_organization_id}/bundle_ids/#{bid['id']}"
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
say '✓ Bundle ID deleted from App Store Connect and local cache', :green
|
|
2371
|
+
say ''
|
|
2372
|
+
say 'Run `mysigner sync ios` to refresh your local cache if needed.', :cyan
|
|
2373
|
+
rescue Mysigner::ValidationError => e
|
|
2374
|
+
# Backend maps 409 Conflict → ValidationError. Apple refused
|
|
2375
|
+
# because of dependent resources (apps/profiles/capabilities).
|
|
2376
|
+
error e.message
|
|
2377
|
+
say ''
|
|
2378
|
+
say '💡 To delete this Bundle ID:', :cyan
|
|
2379
|
+
say ' 1. Remove any provisioning profiles that use it', :yellow
|
|
2380
|
+
say ' 2. Remove any apps tied to it in App Store Connect', :yellow
|
|
2381
|
+
say ' 3. Disable capabilities attached to it', :yellow
|
|
2382
|
+
say ' 4. Re-run `mysigner bundleid delete`', :yellow
|
|
2383
|
+
exit 1
|
|
2384
|
+
rescue Mysigner::ClientError => e
|
|
2385
|
+
error "Failed to delete Bundle ID: #{e.message}"
|
|
2386
|
+
exit 1
|
|
2387
|
+
end
|
|
2388
|
+
|
|
2224
2389
|
else
|
|
2225
2390
|
error "Unknown action: #{action}"
|
|
2226
|
-
say
|
|
2227
|
-
say
|
|
2228
|
-
say
|
|
2229
|
-
say
|
|
2391
|
+
say ''
|
|
2392
|
+
say 'Available actions:', :yellow
|
|
2393
|
+
say ' mysigner bundleid register IDENTIFIER [NAME]', :cyan
|
|
2394
|
+
say ' mysigner bundleid list', :cyan
|
|
2395
|
+
say ' mysigner bundleid delete IDENTIFIER', :cyan
|
|
2230
2396
|
exit 1
|
|
2231
2397
|
end
|
|
2232
2398
|
end
|
|
2233
2399
|
|
|
2234
2400
|
# ==================== APPS (iOS + Android) ====================
|
|
2235
2401
|
|
|
2236
|
-
desc
|
|
2402
|
+
desc 'apps', 'List apps from App Store Connect and/or Google Play'
|
|
2237
2403
|
long_desc <<~DESC
|
|
2238
2404
|
List apps synced from app stores.
|
|
2239
2405
|
|
|
@@ -2268,8 +2434,8 @@ module Mysigner
|
|
|
2268
2434
|
|
|
2269
2435
|
# iOS Apps
|
|
2270
2436
|
if show_ios
|
|
2271
|
-
say
|
|
2272
|
-
say
|
|
2437
|
+
say '📱 iOS Apps', :cyan
|
|
2438
|
+
say ''
|
|
2273
2439
|
|
|
2274
2440
|
begin
|
|
2275
2441
|
response = client.get(
|
|
@@ -2279,78 +2445,78 @@ module Mysigner
|
|
|
2279
2445
|
ios_apps = response[:data]['data']&.fetch('apps', nil) || []
|
|
2280
2446
|
|
|
2281
2447
|
if ios_apps.empty?
|
|
2282
|
-
say
|
|
2283
|
-
say
|
|
2448
|
+
say ' No iOS apps found', :yellow
|
|
2449
|
+
say ''
|
|
2284
2450
|
say " Why don't my iOS apps appear?", :cyan
|
|
2285
|
-
say
|
|
2286
|
-
say
|
|
2287
|
-
say
|
|
2288
|
-
say
|
|
2289
|
-
say
|
|
2290
|
-
say
|
|
2291
|
-
say
|
|
2292
|
-
say
|
|
2293
|
-
say
|
|
2294
|
-
say
|
|
2295
|
-
say
|
|
2451
|
+
say ' ─────────────────────────────', :cyan
|
|
2452
|
+
say ''
|
|
2453
|
+
say ' Common reasons:', :yellow
|
|
2454
|
+
say ' • No apps registered in App Store Connect yet'
|
|
2455
|
+
say ' • Team ID not set on your credential'
|
|
2456
|
+
say ' • Bundle IDs exist but apps not created in App Store Connect'
|
|
2457
|
+
say ''
|
|
2458
|
+
say ' How to register an iOS app:', :cyan
|
|
2459
|
+
say ''
|
|
2460
|
+
say ' 1. Register a Bundle ID'
|
|
2461
|
+
say ' https://developer.apple.com/account/resources/identifiers/list'
|
|
2296
2462
|
say " Click '+' → App IDs → Enter your Bundle ID (e.g., com.company.appname)"
|
|
2297
|
-
say
|
|
2298
|
-
say
|
|
2299
|
-
say
|
|
2463
|
+
say ''
|
|
2464
|
+
say ' 2. Create the app in App Store Connect'
|
|
2465
|
+
say ' https://appstoreconnect.apple.com/apps'
|
|
2300
2466
|
say " My Apps → '+' → New App → Select your Bundle ID"
|
|
2301
|
-
say
|
|
2302
|
-
say
|
|
2303
|
-
say
|
|
2304
|
-
say
|
|
2305
|
-
say
|
|
2306
|
-
say
|
|
2467
|
+
say ''
|
|
2468
|
+
say ' 3. Sync your organization'
|
|
2469
|
+
say ' Run: ', :white
|
|
2470
|
+
say 'mysigner sync ios', :green
|
|
2471
|
+
say ''
|
|
2472
|
+
say ' 💡 Team ID tip:', :yellow
|
|
2307
2473
|
say " Apple's API doesn't expose Team ID. Set it manually in the web dashboard."
|
|
2308
|
-
say
|
|
2309
|
-
say
|
|
2474
|
+
say ' Find yours at: https://developer.apple.com/account/#!/membership/'
|
|
2475
|
+
say ''
|
|
2310
2476
|
else
|
|
2311
2477
|
ios_apps.each do |app|
|
|
2312
2478
|
say " • #{app['name'] || app['bundle_id']}", :green
|
|
2313
2479
|
say " Bundle ID: #{app['bundle_id']}"
|
|
2314
|
-
say
|
|
2480
|
+
say ''
|
|
2315
2481
|
end
|
|
2316
2482
|
end
|
|
2317
2483
|
rescue Mysigner::ClientError => e
|
|
2318
2484
|
say " Could not fetch iOS apps: #{e.message}", :yellow
|
|
2319
2485
|
end
|
|
2320
|
-
say
|
|
2486
|
+
say ''
|
|
2321
2487
|
end
|
|
2322
2488
|
|
|
2323
2489
|
# Android Apps
|
|
2324
|
-
|
|
2325
|
-
say "🤖 Android Apps", :cyan
|
|
2326
|
-
say ""
|
|
2490
|
+
return unless show_android
|
|
2327
2491
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
"/api/v1/organizations/#{config.current_organization_id}/android_apps",
|
|
2331
|
-
params: params
|
|
2332
|
-
)
|
|
2333
|
-
android_apps = response[:data]['android_apps'] || []
|
|
2492
|
+
say '🤖 Android Apps', :cyan
|
|
2493
|
+
say ''
|
|
2334
2494
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2495
|
+
begin
|
|
2496
|
+
response = client.get(
|
|
2497
|
+
"/api/v1/organizations/#{config.current_organization_id}/android_apps",
|
|
2498
|
+
params: params
|
|
2499
|
+
)
|
|
2500
|
+
android_apps = response[:data]['android_apps'] || []
|
|
2501
|
+
|
|
2502
|
+
if android_apps.empty?
|
|
2503
|
+
say ' No Android apps found', :yellow
|
|
2504
|
+
say ' Sync with: mysigner sync android', :yellow
|
|
2505
|
+
else
|
|
2506
|
+
android_apps.each do |app|
|
|
2507
|
+
say " • #{app['name'] || app['package_name']}", :green
|
|
2508
|
+
say " Package: #{app['package_name']}"
|
|
2509
|
+
say ''
|
|
2344
2510
|
end
|
|
2345
|
-
rescue Mysigner::ClientError => e
|
|
2346
|
-
say " Could not fetch Android apps: #{e.message}", :yellow
|
|
2347
2511
|
end
|
|
2512
|
+
rescue Mysigner::ClientError => e
|
|
2513
|
+
say " Could not fetch Android apps: #{e.message}", :yellow
|
|
2348
2514
|
end
|
|
2349
2515
|
end
|
|
2350
2516
|
|
|
2351
2517
|
# ==================== MERCHANT IDS (Apple Pay) ====================
|
|
2352
2518
|
|
|
2353
|
-
desc
|
|
2519
|
+
desc 'merchant-ids', 'List Apple Pay Merchant IDs'
|
|
2354
2520
|
method_option :search, type: :string, aliases: '-q', desc: 'Search by identifier or name'
|
|
2355
2521
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
2356
2522
|
method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
|
|
@@ -2358,8 +2524,8 @@ module Mysigner
|
|
|
2358
2524
|
config = load_config
|
|
2359
2525
|
client = create_client(config)
|
|
2360
2526
|
|
|
2361
|
-
say
|
|
2362
|
-
say
|
|
2527
|
+
say '💳 Merchant IDs', :cyan
|
|
2528
|
+
say ''
|
|
2363
2529
|
|
|
2364
2530
|
params = {
|
|
2365
2531
|
page: options[:page],
|
|
@@ -2376,19 +2542,20 @@ module Mysigner
|
|
|
2376
2542
|
pagination = response[:data]['pagination']
|
|
2377
2543
|
|
|
2378
2544
|
if merchant_ids.empty?
|
|
2379
|
-
say
|
|
2380
|
-
say
|
|
2381
|
-
say
|
|
2545
|
+
say ' No Merchant IDs found', :yellow
|
|
2546
|
+
say ''
|
|
2547
|
+
say ' Create one with: mysigner merchant-id create IDENTIFIER', :cyan
|
|
2382
2548
|
else
|
|
2383
2549
|
merchant_ids.each do |m|
|
|
2384
2550
|
say " • #{m['identifier']}", :green
|
|
2385
2551
|
say " Name: #{m['name']}" if m['name'] && m['name'] != m['identifier']
|
|
2386
2552
|
say " Team: #{m['team_id']}" if m['team_id']
|
|
2387
|
-
say
|
|
2553
|
+
say ''
|
|
2388
2554
|
end
|
|
2389
2555
|
|
|
2390
2556
|
if pagination
|
|
2391
|
-
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
|
|
2557
|
+
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
|
|
2558
|
+
:yellow
|
|
2392
2559
|
end
|
|
2393
2560
|
end
|
|
2394
2561
|
rescue Mysigner::ClientError => e
|
|
@@ -2397,7 +2564,7 @@ module Mysigner
|
|
|
2397
2564
|
end
|
|
2398
2565
|
end
|
|
2399
2566
|
|
|
2400
|
-
desc
|
|
2567
|
+
desc 'merchant-id SUBCOMMAND', 'Manage Apple Pay Merchant IDs'
|
|
2401
2568
|
long_desc <<~DESC
|
|
2402
2569
|
Create and delete Apple Pay Merchant IDs.
|
|
2403
2570
|
|
|
@@ -2422,22 +2589,24 @@ module Mysigner
|
|
|
2422
2589
|
|
|
2423
2590
|
case action
|
|
2424
2591
|
when 'create'
|
|
2425
|
-
if identifier.nil?
|
|
2426
|
-
error
|
|
2427
|
-
say
|
|
2428
|
-
say
|
|
2592
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2593
|
+
error 'Usage: mysigner merchant-id create IDENTIFIER [--name NAME]'
|
|
2594
|
+
say ''
|
|
2595
|
+
say 'Example: mysigner merchant-id create merchant.com.company.app', :yellow
|
|
2429
2596
|
exit 1
|
|
2597
|
+
return
|
|
2430
2598
|
end
|
|
2431
2599
|
|
|
2432
2600
|
unless identifier.start_with?('merchant.')
|
|
2433
2601
|
error "Merchant ID must start with 'merchant.'"
|
|
2434
|
-
say
|
|
2435
|
-
say
|
|
2602
|
+
say ''
|
|
2603
|
+
say 'Example: merchant.com.company.app', :cyan
|
|
2436
2604
|
exit 1
|
|
2605
|
+
return
|
|
2437
2606
|
end
|
|
2438
2607
|
|
|
2439
|
-
say
|
|
2440
|
-
say
|
|
2608
|
+
say '💳 Creating Merchant ID...', :cyan
|
|
2609
|
+
say ''
|
|
2441
2610
|
|
|
2442
2611
|
begin
|
|
2443
2612
|
response = client.post(
|
|
@@ -2449,12 +2618,12 @@ module Mysigner
|
|
|
2449
2618
|
)
|
|
2450
2619
|
|
|
2451
2620
|
m = response[:data]['merchant_id'] || response[:data]
|
|
2452
|
-
say
|
|
2453
|
-
say
|
|
2621
|
+
say '✓ Merchant ID created successfully!', :green
|
|
2622
|
+
say ''
|
|
2454
2623
|
say " Identifier: #{m['identifier'] || identifier}", :white
|
|
2455
2624
|
say " Name: #{m['name']}", :white if m['name']
|
|
2456
2625
|
rescue Mysigner::ClientError => e
|
|
2457
|
-
if e.message.include?(
|
|
2626
|
+
if e.message.include?('already exists')
|
|
2458
2627
|
say "ℹ️ Merchant ID already exists: #{identifier}", :yellow
|
|
2459
2628
|
else
|
|
2460
2629
|
error "Failed to create Merchant ID: #{e.message}"
|
|
@@ -2463,13 +2632,14 @@ module Mysigner
|
|
|
2463
2632
|
end
|
|
2464
2633
|
|
|
2465
2634
|
when 'delete'
|
|
2466
|
-
if identifier.nil?
|
|
2467
|
-
error
|
|
2635
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2636
|
+
error 'Usage: mysigner merchant-id delete IDENTIFIER'
|
|
2468
2637
|
exit 1
|
|
2638
|
+
return
|
|
2469
2639
|
end
|
|
2470
2640
|
|
|
2471
|
-
say
|
|
2472
|
-
say
|
|
2641
|
+
say '💳 Deleting Merchant ID...', :cyan
|
|
2642
|
+
say ''
|
|
2473
2643
|
|
|
2474
2644
|
begin
|
|
2475
2645
|
# First find the merchant ID by identifier
|
|
@@ -2483,6 +2653,7 @@ module Mysigner
|
|
|
2483
2653
|
if m.nil?
|
|
2484
2654
|
error "Merchant ID not found: #{identifier}"
|
|
2485
2655
|
exit 1
|
|
2656
|
+
return
|
|
2486
2657
|
end
|
|
2487
2658
|
|
|
2488
2659
|
client.delete(
|
|
@@ -2497,43 +2668,43 @@ module Mysigner
|
|
|
2497
2668
|
|
|
2498
2669
|
else
|
|
2499
2670
|
error "Unknown action: #{action}"
|
|
2500
|
-
say
|
|
2501
|
-
say
|
|
2502
|
-
say
|
|
2503
|
-
say
|
|
2671
|
+
say ''
|
|
2672
|
+
say 'Available actions:', :yellow
|
|
2673
|
+
say ' mysigner merchant-id create IDENTIFIER [--name NAME]', :cyan
|
|
2674
|
+
say ' mysigner merchant-id delete IDENTIFIER', :cyan
|
|
2504
2675
|
exit 1
|
|
2505
2676
|
end
|
|
2506
2677
|
end
|
|
2507
2678
|
|
|
2508
2679
|
# ==================== ANDROID TRACKS ====================
|
|
2509
2680
|
|
|
2510
|
-
desc
|
|
2681
|
+
desc 'tracks PACKAGE_NAME', 'List Google Play tracks for an Android app'
|
|
2511
2682
|
method_option :sort, type: :boolean, desc: 'Sort by track name'
|
|
2512
2683
|
def tracks(package_name = nil)
|
|
2513
2684
|
config = load_config
|
|
2514
2685
|
client = create_client(config)
|
|
2515
2686
|
|
|
2516
2687
|
if package_name.nil?
|
|
2517
|
-
error
|
|
2518
|
-
say
|
|
2519
|
-
say
|
|
2520
|
-
say
|
|
2521
|
-
say
|
|
2522
|
-
say
|
|
2688
|
+
error 'Usage: mysigner tracks PACKAGE_NAME'
|
|
2689
|
+
say ''
|
|
2690
|
+
say 'Example: mysigner tracks com.example.myapp', :yellow
|
|
2691
|
+
say ''
|
|
2692
|
+
say '💡 To see your registered Android apps:', :cyan
|
|
2693
|
+
say ' mysigner apps --platform android', :cyan
|
|
2523
2694
|
exit 1
|
|
2524
2695
|
end
|
|
2525
2696
|
|
|
2526
2697
|
say "🎯 Google Play Tracks for #{package_name}", :cyan
|
|
2527
|
-
say
|
|
2698
|
+
say ''
|
|
2528
2699
|
|
|
2529
2700
|
begin
|
|
2530
2701
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps/package/#{package_name}/tracks")
|
|
2531
2702
|
tracks = response[:data]['tracks'] || []
|
|
2532
2703
|
|
|
2533
2704
|
if tracks.empty?
|
|
2534
|
-
say
|
|
2535
|
-
say
|
|
2536
|
-
say
|
|
2705
|
+
say 'No tracks found', :yellow
|
|
2706
|
+
say ''
|
|
2707
|
+
say 'Tracks appear after you upload your app to Google Play Console', :cyan
|
|
2537
2708
|
say "and sync with: mysigner sync android --package #{package_name}", :cyan
|
|
2538
2709
|
return
|
|
2539
2710
|
end
|
|
@@ -2559,17 +2730,17 @@ module Mysigner
|
|
|
2559
2730
|
updated = Time.parse(track['updated_at']).strftime('%Y-%m-%d %H:%M')
|
|
2560
2731
|
say " Updated: #{updated}"
|
|
2561
2732
|
end
|
|
2562
|
-
say
|
|
2733
|
+
say ''
|
|
2563
2734
|
end
|
|
2564
2735
|
|
|
2565
2736
|
say "Total: #{tracks.count} track(s)", :yellow
|
|
2566
2737
|
rescue Mysigner::NotFoundError => e
|
|
2567
|
-
if e.message.include?(
|
|
2738
|
+
if e.message.include?('Android app')
|
|
2568
2739
|
error "Android app not found: #{package_name}"
|
|
2569
|
-
say
|
|
2570
|
-
say
|
|
2571
|
-
say
|
|
2572
|
-
say
|
|
2740
|
+
say ''
|
|
2741
|
+
say '💡 App not found:', :cyan
|
|
2742
|
+
say ' → Check the package name is correct', :yellow
|
|
2743
|
+
say ' → List your apps: mysigner apps --platform android', :yellow
|
|
2573
2744
|
say " → Register the app: mysigner android add #{package_name}", :yellow
|
|
2574
2745
|
else
|
|
2575
2746
|
error "Not found: #{e.message}"
|
|
@@ -2581,33 +2752,33 @@ module Mysigner
|
|
|
2581
2752
|
end
|
|
2582
2753
|
end
|
|
2583
2754
|
|
|
2584
|
-
desc
|
|
2755
|
+
desc 'track PACKAGE_NAME TRACK_NAME', 'Show details for a specific Google Play track'
|
|
2585
2756
|
def track(package_name = nil, track_name = nil)
|
|
2586
2757
|
config = load_config
|
|
2587
2758
|
client = create_client(config)
|
|
2588
2759
|
|
|
2589
2760
|
if package_name.nil? || track_name.nil?
|
|
2590
|
-
error
|
|
2591
|
-
say
|
|
2592
|
-
say
|
|
2593
|
-
say
|
|
2594
|
-
say
|
|
2595
|
-
say
|
|
2596
|
-
say
|
|
2597
|
-
say
|
|
2598
|
-
say
|
|
2761
|
+
error 'Usage: mysigner track PACKAGE_NAME TRACK_NAME'
|
|
2762
|
+
say ''
|
|
2763
|
+
say 'Example: mysigner track com.example.myapp production', :yellow
|
|
2764
|
+
say ' mysigner track com.example.myapp beta', :yellow
|
|
2765
|
+
say ''
|
|
2766
|
+
say 'Common track names: production, beta, alpha, internal', :cyan
|
|
2767
|
+
say ''
|
|
2768
|
+
say '💡 To see available tracks:', :cyan
|
|
2769
|
+
say ' mysigner tracks com.example.myapp', :cyan
|
|
2599
2770
|
exit 1
|
|
2600
2771
|
end
|
|
2601
2772
|
|
|
2602
2773
|
say "🎯 Track: #{track_name}", :cyan
|
|
2603
2774
|
say " Package: #{package_name}", :white
|
|
2604
|
-
say
|
|
2775
|
+
say ''
|
|
2605
2776
|
|
|
2606
2777
|
begin
|
|
2607
2778
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps/package/#{package_name}/tracks/#{track_name}")
|
|
2608
2779
|
track = response[:data]
|
|
2609
2780
|
|
|
2610
|
-
say
|
|
2781
|
+
say 'Details:', :bold
|
|
2611
2782
|
say " Track Name: #{track['track_name']}"
|
|
2612
2783
|
say " Status: #{track['status'] || 'unknown'}"
|
|
2613
2784
|
|
|
@@ -2617,9 +2788,9 @@ module Mysigner
|
|
|
2617
2788
|
end
|
|
2618
2789
|
|
|
2619
2790
|
# Show releases info
|
|
2791
|
+
say ''
|
|
2620
2792
|
if track['releases'].is_a?(Array) && track['releases'].any?
|
|
2621
|
-
say
|
|
2622
|
-
say "Releases:", :bold
|
|
2793
|
+
say 'Releases:', :bold
|
|
2623
2794
|
track['releases'].each_with_index do |release, idx|
|
|
2624
2795
|
say " Release #{idx + 1}:", :white
|
|
2625
2796
|
say " Status: #{release['status']}" if release['status']
|
|
@@ -2627,14 +2798,12 @@ module Mysigner
|
|
|
2627
2798
|
version_codes = release['versionCodes'] || release['version_codes'] || []
|
|
2628
2799
|
say " Version Codes: #{version_codes.join(', ')}" if version_codes.any?
|
|
2629
2800
|
|
|
2630
|
-
if release['name']
|
|
2631
|
-
say " Name: #{release['name']}"
|
|
2632
|
-
end
|
|
2801
|
+
say " Name: #{release['name']}" if release['name']
|
|
2633
2802
|
|
|
2634
2803
|
if release['releaseNotes'] || release['release_notes']
|
|
2635
2804
|
notes = release['releaseNotes'] || release['release_notes']
|
|
2636
2805
|
if notes.is_a?(Array) && notes.any?
|
|
2637
|
-
say
|
|
2806
|
+
say ' Release Notes:'
|
|
2638
2807
|
notes.each do |note|
|
|
2639
2808
|
lang = note['language'] || 'en-US'
|
|
2640
2809
|
text = note['text'] || ''
|
|
@@ -2649,24 +2818,22 @@ module Mysigner
|
|
|
2649
2818
|
end
|
|
2650
2819
|
end
|
|
2651
2820
|
else
|
|
2652
|
-
say
|
|
2653
|
-
say "No releases found in this track", :yellow
|
|
2821
|
+
say 'No releases found in this track', :yellow
|
|
2654
2822
|
end
|
|
2655
|
-
|
|
2656
2823
|
rescue Mysigner::NotFoundError => e
|
|
2657
|
-
if e.message.include?(
|
|
2824
|
+
if e.message.include?('Android app')
|
|
2658
2825
|
error "Android app not found: #{package_name}"
|
|
2659
|
-
say
|
|
2660
|
-
say
|
|
2661
|
-
say
|
|
2662
|
-
say
|
|
2663
|
-
elsif e.message.include?(
|
|
2826
|
+
say ''
|
|
2827
|
+
say '💡 App not found:', :cyan
|
|
2828
|
+
say ' → Check the package name is correct', :yellow
|
|
2829
|
+
say ' → List your apps: mysigner apps --platform android', :yellow
|
|
2830
|
+
elsif e.message.include?('Track')
|
|
2664
2831
|
error "Track not found: #{track_name}"
|
|
2665
|
-
say
|
|
2666
|
-
say
|
|
2667
|
-
say
|
|
2832
|
+
say ''
|
|
2833
|
+
say '💡 Track not found:', :cyan
|
|
2834
|
+
say ' → Check the track name is correct', :yellow
|
|
2668
2835
|
say " → List available tracks: mysigner tracks #{package_name}", :yellow
|
|
2669
|
-
say
|
|
2836
|
+
say ' → Common tracks: production, beta, alpha, internal', :yellow
|
|
2670
2837
|
else
|
|
2671
2838
|
error "Not found: #{e.message}"
|
|
2672
2839
|
end
|
|
@@ -2679,7 +2846,7 @@ module Mysigner
|
|
|
2679
2846
|
|
|
2680
2847
|
# ==================== APP GROUPS ====================
|
|
2681
2848
|
|
|
2682
|
-
desc
|
|
2849
|
+
desc 'app-groups', 'List App Groups'
|
|
2683
2850
|
method_option :search, type: :string, aliases: '-q', desc: 'Search by identifier or name'
|
|
2684
2851
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
2685
2852
|
method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
|
|
@@ -2687,8 +2854,8 @@ module Mysigner
|
|
|
2687
2854
|
config = load_config
|
|
2688
2855
|
client = create_client(config)
|
|
2689
2856
|
|
|
2690
|
-
say
|
|
2691
|
-
say
|
|
2857
|
+
say '📦 App Groups', :cyan
|
|
2858
|
+
say ''
|
|
2692
2859
|
|
|
2693
2860
|
params = {
|
|
2694
2861
|
page: options[:page],
|
|
@@ -2705,21 +2872,22 @@ module Mysigner
|
|
|
2705
2872
|
pagination = response[:data]['pagination']
|
|
2706
2873
|
|
|
2707
2874
|
if app_groups.empty?
|
|
2708
|
-
say
|
|
2709
|
-
say
|
|
2710
|
-
say
|
|
2711
|
-
say
|
|
2712
|
-
say
|
|
2875
|
+
say ' No App Groups found', :yellow
|
|
2876
|
+
say ''
|
|
2877
|
+
say ' Register one with: mysigner app-group register IDENTIFIER', :cyan
|
|
2878
|
+
say ''
|
|
2879
|
+
say ' Note: App Groups must first be created in Apple Developer Portal', :yellow
|
|
2713
2880
|
else
|
|
2714
2881
|
app_groups.each do |g|
|
|
2715
2882
|
say " • #{g['identifier']}", :green
|
|
2716
2883
|
say " Name: #{g['name']}" if g['name'] && g['name'] != g['identifier']
|
|
2717
2884
|
say " Team: #{g['team_id']}" if g['team_id']
|
|
2718
|
-
say
|
|
2885
|
+
say ''
|
|
2719
2886
|
end
|
|
2720
2887
|
|
|
2721
2888
|
if pagination
|
|
2722
|
-
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
|
|
2889
|
+
say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
|
|
2890
|
+
:yellow
|
|
2723
2891
|
end
|
|
2724
2892
|
end
|
|
2725
2893
|
rescue Mysigner::ClientError => e
|
|
@@ -2728,7 +2896,7 @@ module Mysigner
|
|
|
2728
2896
|
end
|
|
2729
2897
|
end
|
|
2730
2898
|
|
|
2731
|
-
desc
|
|
2899
|
+
desc 'app-group SUBCOMMAND', 'Manage App Groups'
|
|
2732
2900
|
long_desc <<~DESC
|
|
2733
2901
|
Register and delete App Groups.
|
|
2734
2902
|
|
|
@@ -2757,24 +2925,26 @@ module Mysigner
|
|
|
2757
2925
|
|
|
2758
2926
|
case action
|
|
2759
2927
|
when 'register'
|
|
2760
|
-
if identifier.nil?
|
|
2761
|
-
error
|
|
2762
|
-
say
|
|
2763
|
-
say
|
|
2764
|
-
say
|
|
2765
|
-
say
|
|
2928
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2929
|
+
error 'Usage: mysigner app-group register IDENTIFIER [--name NAME]'
|
|
2930
|
+
say ''
|
|
2931
|
+
say 'Example: mysigner app-group register group.com.company.shared', :yellow
|
|
2932
|
+
say ''
|
|
2933
|
+
say 'Note: Create the App Group in Apple Developer Portal first!', :cyan
|
|
2766
2934
|
exit 1
|
|
2935
|
+
return
|
|
2767
2936
|
end
|
|
2768
2937
|
|
|
2769
2938
|
unless identifier.start_with?('group.')
|
|
2770
2939
|
error "App Group identifier must start with 'group.'"
|
|
2771
|
-
say
|
|
2772
|
-
say
|
|
2940
|
+
say ''
|
|
2941
|
+
say 'Example: group.com.company.shared', :cyan
|
|
2773
2942
|
exit 1
|
|
2943
|
+
return
|
|
2774
2944
|
end
|
|
2775
2945
|
|
|
2776
|
-
say
|
|
2777
|
-
say
|
|
2946
|
+
say '📦 Registering App Group...', :cyan
|
|
2947
|
+
say ''
|
|
2778
2948
|
|
|
2779
2949
|
begin
|
|
2780
2950
|
response = client.post(
|
|
@@ -2786,15 +2956,15 @@ module Mysigner
|
|
|
2786
2956
|
)
|
|
2787
2957
|
|
|
2788
2958
|
g = response[:data]['app_group'] || response[:data]
|
|
2789
|
-
say
|
|
2790
|
-
say
|
|
2959
|
+
say '✓ App Group registered!', :green
|
|
2960
|
+
say ''
|
|
2791
2961
|
say " Identifier: #{g['identifier'] || identifier}", :white
|
|
2792
2962
|
say " Name: #{g['name']}", :white if g['name']
|
|
2793
|
-
say
|
|
2794
|
-
say
|
|
2795
|
-
say
|
|
2963
|
+
say ''
|
|
2964
|
+
say ' Remember: This only registers the App Group in My Signer.', :yellow
|
|
2965
|
+
say ' Make sure it exists in Apple Developer Portal.', :yellow
|
|
2796
2966
|
rescue Mysigner::ClientError => e
|
|
2797
|
-
if e.message.include?(
|
|
2967
|
+
if e.message.include?('already exists')
|
|
2798
2968
|
say "ℹ️ App Group already registered: #{identifier}", :yellow
|
|
2799
2969
|
else
|
|
2800
2970
|
error "Failed to register App Group: #{e.message}"
|
|
@@ -2803,13 +2973,14 @@ module Mysigner
|
|
|
2803
2973
|
end
|
|
2804
2974
|
|
|
2805
2975
|
when 'delete'
|
|
2806
|
-
if identifier.nil?
|
|
2807
|
-
error
|
|
2976
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2977
|
+
error 'Usage: mysigner app-group delete IDENTIFIER'
|
|
2808
2978
|
exit 1
|
|
2979
|
+
return
|
|
2809
2980
|
end
|
|
2810
2981
|
|
|
2811
|
-
say
|
|
2812
|
-
say
|
|
2982
|
+
say '📦 Removing App Group...', :cyan
|
|
2983
|
+
say ''
|
|
2813
2984
|
|
|
2814
2985
|
begin
|
|
2815
2986
|
# First find the app group by identifier
|
|
@@ -2823,6 +2994,7 @@ module Mysigner
|
|
|
2823
2994
|
if g.nil?
|
|
2824
2995
|
error "App Group not found: #{identifier}"
|
|
2825
2996
|
exit 1
|
|
2997
|
+
return
|
|
2826
2998
|
end
|
|
2827
2999
|
|
|
2828
3000
|
client.delete(
|
|
@@ -2830,9 +3002,9 @@ module Mysigner
|
|
|
2830
3002
|
)
|
|
2831
3003
|
|
|
2832
3004
|
say "✓ App Group removed from My Signer: #{identifier}", :green
|
|
2833
|
-
say
|
|
2834
|
-
say
|
|
2835
|
-
say
|
|
3005
|
+
say ''
|
|
3006
|
+
say ' Note: The App Group still exists in Apple Developer Portal.', :yellow
|
|
3007
|
+
say ' Delete it manually if needed.', :yellow
|
|
2836
3008
|
rescue Mysigner::ClientError => e
|
|
2837
3009
|
error "Failed to remove App Group: #{e.message}"
|
|
2838
3010
|
exit 1
|
|
@@ -2840,17 +3012,17 @@ module Mysigner
|
|
|
2840
3012
|
|
|
2841
3013
|
else
|
|
2842
3014
|
error "Unknown action: #{action}"
|
|
2843
|
-
say
|
|
2844
|
-
say
|
|
2845
|
-
say
|
|
2846
|
-
say
|
|
3015
|
+
say ''
|
|
3016
|
+
say 'Available actions:', :yellow
|
|
3017
|
+
say ' mysigner app-group register IDENTIFIER [--name NAME]', :cyan
|
|
3018
|
+
say ' mysigner app-group delete IDENTIFIER', :cyan
|
|
2847
3019
|
exit 1
|
|
2848
3020
|
end
|
|
2849
3021
|
end
|
|
2850
3022
|
|
|
2851
3023
|
# ==================== GOOGLE PLAY CREDENTIALS ====================
|
|
2852
3024
|
|
|
2853
|
-
desc
|
|
3025
|
+
desc 'gp-credential SUBCOMMAND', 'Manage Google Play credentials (list, delete, activate, test)'
|
|
2854
3026
|
long_desc <<~DESC
|
|
2855
3027
|
Manage Google Play API credentials for Android app distribution.
|
|
2856
3028
|
|
|
@@ -2888,17 +3060,17 @@ module Mysigner
|
|
|
2888
3060
|
|
|
2889
3061
|
case action
|
|
2890
3062
|
when 'list'
|
|
2891
|
-
say
|
|
2892
|
-
say
|
|
3063
|
+
say '🔑 Google Play Credentials', :cyan
|
|
3064
|
+
say ''
|
|
2893
3065
|
|
|
2894
3066
|
begin
|
|
2895
3067
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials")
|
|
2896
3068
|
credentials = response[:data]['google_play_credentials'] || []
|
|
2897
3069
|
|
|
2898
3070
|
if credentials.empty?
|
|
2899
|
-
say
|
|
2900
|
-
say
|
|
2901
|
-
say
|
|
3071
|
+
say 'No Google Play credentials found', :yellow
|
|
3072
|
+
say ''
|
|
3073
|
+
say 'Set up credentials with: mysigner onboard', :yellow
|
|
2902
3074
|
return
|
|
2903
3075
|
end
|
|
2904
3076
|
|
|
@@ -2917,7 +3089,7 @@ module Mysigner
|
|
|
2917
3089
|
sync_color = cred['last_sync_status'] == 'success' ? :green : :red
|
|
2918
3090
|
say " Sync Status: #{cred['last_sync_status']}", sync_color
|
|
2919
3091
|
end
|
|
2920
|
-
say
|
|
3092
|
+
say ''
|
|
2921
3093
|
end
|
|
2922
3094
|
|
|
2923
3095
|
say "Total: #{credentials.count} credential(s)", :yellow
|
|
@@ -2930,23 +3102,23 @@ module Mysigner
|
|
|
2930
3102
|
credential_id = args[0]
|
|
2931
3103
|
|
|
2932
3104
|
unless credential_id
|
|
2933
|
-
error
|
|
2934
|
-
say
|
|
3105
|
+
error 'Usage: mysigner gp-credential delete ID'
|
|
3106
|
+
say ''
|
|
2935
3107
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
2936
3108
|
exit 1
|
|
2937
3109
|
end
|
|
2938
3110
|
|
|
2939
3111
|
say "⚠️ You are about to delete Google Play credential ID: #{credential_id}", :yellow
|
|
2940
|
-
say
|
|
3112
|
+
say ''
|
|
2941
3113
|
|
|
2942
|
-
if yes?(
|
|
3114
|
+
if yes?('Are you sure? This cannot be undone. (y/n)')
|
|
2943
3115
|
begin
|
|
2944
3116
|
client.delete("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}")
|
|
2945
|
-
say
|
|
2946
|
-
say
|
|
3117
|
+
say ''
|
|
3118
|
+
say '✓ Google Play credential deleted', :green
|
|
2947
3119
|
rescue Mysigner::NotFoundError
|
|
2948
3120
|
error "Credential not found with ID: #{credential_id}"
|
|
2949
|
-
say
|
|
3121
|
+
say ''
|
|
2950
3122
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
2951
3123
|
exit 1
|
|
2952
3124
|
rescue Mysigner::ClientError => e
|
|
@@ -2954,30 +3126,30 @@ module Mysigner
|
|
|
2954
3126
|
exit 1
|
|
2955
3127
|
end
|
|
2956
3128
|
else
|
|
2957
|
-
say
|
|
3129
|
+
say 'Deletion cancelled', :yellow
|
|
2958
3130
|
end
|
|
2959
3131
|
|
|
2960
3132
|
when 'activate'
|
|
2961
3133
|
credential_id = args[0]
|
|
2962
3134
|
|
|
2963
3135
|
unless credential_id
|
|
2964
|
-
error
|
|
2965
|
-
say
|
|
3136
|
+
error 'Usage: mysigner gp-credential activate ID'
|
|
3137
|
+
say ''
|
|
2966
3138
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
2967
3139
|
exit 1
|
|
2968
3140
|
end
|
|
2969
3141
|
|
|
2970
|
-
say
|
|
3142
|
+
say '🔑 Activating credential...', :cyan
|
|
2971
3143
|
|
|
2972
3144
|
begin
|
|
2973
3145
|
response = client.post("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}/activate")
|
|
2974
3146
|
credential = response[:data]['google_play_credential'] || response[:data]
|
|
2975
|
-
say
|
|
2976
|
-
say
|
|
3147
|
+
say '✓ Credential activated!', :green
|
|
3148
|
+
say ''
|
|
2977
3149
|
say "#{credential['name']} is now the active Google Play credential", :cyan
|
|
2978
3150
|
rescue Mysigner::NotFoundError
|
|
2979
3151
|
error "Credential not found with ID: #{credential_id}"
|
|
2980
|
-
say
|
|
3152
|
+
say ''
|
|
2981
3153
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
2982
3154
|
exit 1
|
|
2983
3155
|
rescue Mysigner::ClientError => e
|
|
@@ -2989,37 +3161,37 @@ module Mysigner
|
|
|
2989
3161
|
credential_id = args[0]
|
|
2990
3162
|
|
|
2991
3163
|
unless credential_id
|
|
2992
|
-
error
|
|
2993
|
-
say
|
|
3164
|
+
error 'Usage: mysigner gp-credential test ID'
|
|
3165
|
+
say ''
|
|
2994
3166
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
2995
3167
|
exit 1
|
|
2996
3168
|
end
|
|
2997
3169
|
|
|
2998
|
-
say
|
|
2999
|
-
say
|
|
3170
|
+
say '🔑 Testing credential connection...', :cyan
|
|
3171
|
+
say ''
|
|
3000
3172
|
|
|
3001
3173
|
begin
|
|
3002
3174
|
response = client.post("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}/test")
|
|
3003
3175
|
result = response[:data]
|
|
3004
3176
|
|
|
3005
3177
|
if result['success']
|
|
3006
|
-
say
|
|
3007
|
-
say
|
|
3008
|
-
say
|
|
3178
|
+
say '✓ Connection successful!', :green
|
|
3179
|
+
say ''
|
|
3180
|
+
say ' Google Play API is reachable with this credential', :cyan
|
|
3009
3181
|
else
|
|
3010
|
-
say
|
|
3011
|
-
say
|
|
3182
|
+
say '✗ Connection failed', :red
|
|
3183
|
+
say ''
|
|
3012
3184
|
say " Error: #{result['error']}", :red if result['error']
|
|
3013
|
-
say
|
|
3014
|
-
say
|
|
3015
|
-
say
|
|
3016
|
-
say
|
|
3017
|
-
say
|
|
3185
|
+
say ''
|
|
3186
|
+
say '💡 Check that:', :cyan
|
|
3187
|
+
say ' → The service account JSON key is valid', :yellow
|
|
3188
|
+
say ' → The service account has Google Play Console access', :yellow
|
|
3189
|
+
say ' → API access is enabled in Google Play Console', :yellow
|
|
3018
3190
|
exit 1
|
|
3019
3191
|
end
|
|
3020
3192
|
rescue Mysigner::NotFoundError
|
|
3021
3193
|
error "Credential not found with ID: #{credential_id}"
|
|
3022
|
-
say
|
|
3194
|
+
say ''
|
|
3023
3195
|
say "Run 'mysigner gp-credential list' to see available IDs", :yellow
|
|
3024
3196
|
exit 1
|
|
3025
3197
|
rescue Mysigner::ClientError => e
|
|
@@ -3031,14 +3203,14 @@ module Mysigner
|
|
|
3031
3203
|
invoke :help, ['gp-credential']
|
|
3032
3204
|
else
|
|
3033
3205
|
error "Unknown action: #{action}"
|
|
3034
|
-
say
|
|
3206
|
+
say 'Available actions: list, delete, activate, test, help', :yellow
|
|
3035
3207
|
exit 1
|
|
3036
3208
|
end
|
|
3037
3209
|
end
|
|
3038
3210
|
|
|
3039
3211
|
# ==================== APP STORE RELEASES ====================
|
|
3040
3212
|
|
|
3041
|
-
desc
|
|
3213
|
+
desc 'release SUBCOMMAND', 'Manage App Store release configurations (list, show, create, update)'
|
|
3042
3214
|
long_desc <<~DESC
|
|
3043
3215
|
Manage App Store release configurations for iOS app distribution.
|
|
3044
3216
|
|
|
@@ -3107,8 +3279,8 @@ module Mysigner
|
|
|
3107
3279
|
|
|
3108
3280
|
case action
|
|
3109
3281
|
when 'list'
|
|
3110
|
-
say
|
|
3111
|
-
say
|
|
3282
|
+
say '🚀 App Store Releases', :cyan
|
|
3283
|
+
say ''
|
|
3112
3284
|
|
|
3113
3285
|
params = {}
|
|
3114
3286
|
params[:bundle_id] = options[:bundle_id] if options[:bundle_id]
|
|
@@ -3121,9 +3293,9 @@ module Mysigner
|
|
|
3121
3293
|
releases = response[:data]['app_store_releases'] || []
|
|
3122
3294
|
|
|
3123
3295
|
if releases.empty?
|
|
3124
|
-
say
|
|
3125
|
-
say
|
|
3126
|
-
say
|
|
3296
|
+
say 'No release configurations found', :yellow
|
|
3297
|
+
say ''
|
|
3298
|
+
say 'Create one with: mysigner release create --bundle-id-id ID', :yellow
|
|
3127
3299
|
return
|
|
3128
3300
|
end
|
|
3129
3301
|
|
|
@@ -3134,7 +3306,7 @@ module Mysigner
|
|
|
3134
3306
|
say " Auto Submit: #{rel['auto_submit'] ? 'Yes' : 'No'}"
|
|
3135
3307
|
say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
|
|
3136
3308
|
say " Version: #{rel['version_string']}" if rel['version_string']
|
|
3137
|
-
say
|
|
3309
|
+
say ''
|
|
3138
3310
|
end
|
|
3139
3311
|
|
|
3140
3312
|
say "Total: #{releases.count} release(s)", :yellow
|
|
@@ -3147,8 +3319,8 @@ module Mysigner
|
|
|
3147
3319
|
release_id = args[0]
|
|
3148
3320
|
|
|
3149
3321
|
unless release_id
|
|
3150
|
-
error
|
|
3151
|
-
say
|
|
3322
|
+
error 'Usage: mysigner release show ID'
|
|
3323
|
+
say ''
|
|
3152
3324
|
say "Run 'mysigner release list' to see available IDs", :yellow
|
|
3153
3325
|
exit 1
|
|
3154
3326
|
end
|
|
@@ -3158,31 +3330,31 @@ module Mysigner
|
|
|
3158
3330
|
rel = response[:data]['app_store_release'] || response[:data]
|
|
3159
3331
|
|
|
3160
3332
|
say "🚀 Release Configuration (ID: #{rel['id']})", :cyan
|
|
3161
|
-
say
|
|
3162
|
-
say
|
|
3333
|
+
say ''
|
|
3334
|
+
say 'Details:', :bold
|
|
3163
3335
|
say " App Name: #{rel['app_name'] || 'N/A'}"
|
|
3164
3336
|
say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
|
|
3165
3337
|
say " Version: #{rel['version_string'] || 'N/A'}"
|
|
3166
3338
|
say " Release Type: #{rel['release_type'] || 'N/A'}"
|
|
3167
3339
|
say " Auto Submit: #{rel['auto_submit'] ? 'Yes' : 'No'}"
|
|
3168
3340
|
say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
|
|
3169
|
-
say
|
|
3341
|
+
say ''
|
|
3170
3342
|
if rel['whats_new'] && !rel['whats_new'].empty?
|
|
3171
3343
|
say "What's New:", :bold
|
|
3172
3344
|
say " #{rel['whats_new']}"
|
|
3173
|
-
say
|
|
3345
|
+
say ''
|
|
3174
3346
|
end
|
|
3175
|
-
say
|
|
3347
|
+
say 'URLs:', :bold
|
|
3176
3348
|
say " Support: #{rel['support_url'] || 'N/A'}"
|
|
3177
3349
|
say " Marketing: #{rel['marketing_url'] || 'N/A'}"
|
|
3178
3350
|
say " Privacy: #{rel['privacy_url'] || 'N/A'}"
|
|
3179
3351
|
if rel['scheduled_date']
|
|
3180
|
-
say
|
|
3352
|
+
say ''
|
|
3181
3353
|
say "Scheduled Date: #{rel['scheduled_date']}"
|
|
3182
3354
|
end
|
|
3183
3355
|
rescue Mysigner::NotFoundError
|
|
3184
3356
|
error "Release not found with ID: #{release_id}"
|
|
3185
|
-
say
|
|
3357
|
+
say ''
|
|
3186
3358
|
say "Run 'mysigner release list' to see available IDs", :yellow
|
|
3187
3359
|
exit 1
|
|
3188
3360
|
rescue Mysigner::ClientError => e
|
|
@@ -3191,8 +3363,8 @@ module Mysigner
|
|
|
3191
3363
|
end
|
|
3192
3364
|
|
|
3193
3365
|
when 'create'
|
|
3194
|
-
say
|
|
3195
|
-
say
|
|
3366
|
+
say '🚀 Creating release configuration...', :cyan
|
|
3367
|
+
say ''
|
|
3196
3368
|
|
|
3197
3369
|
body = {}
|
|
3198
3370
|
body[:bundle_id_id] = options[:bundle_id_id] if options[:bundle_id_id]
|
|
@@ -3212,9 +3384,9 @@ module Mysigner
|
|
|
3212
3384
|
)
|
|
3213
3385
|
rel = response[:data]['app_store_release'] || response[:data]
|
|
3214
3386
|
|
|
3215
|
-
say
|
|
3216
|
-
say
|
|
3217
|
-
say
|
|
3387
|
+
say '✓ Release configuration created!', :green
|
|
3388
|
+
say ''
|
|
3389
|
+
say 'Details:', :bold
|
|
3218
3390
|
say " ID: #{rel['id']}"
|
|
3219
3391
|
say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
|
|
3220
3392
|
say " Release Type: #{rel['release_type'] || 'N/A'}"
|
|
@@ -3222,24 +3394,22 @@ module Mysigner
|
|
|
3222
3394
|
say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
|
|
3223
3395
|
rescue Mysigner::ValidationError => e
|
|
3224
3396
|
if e.message.include?('already exists') || (e.error_code && e.error_code.to_s == '409')
|
|
3225
|
-
error
|
|
3226
|
-
say
|
|
3397
|
+
error 'Release configuration already exists for this bundle ID'
|
|
3398
|
+
say ''
|
|
3227
3399
|
say "💡 Use 'mysigner release update ID' to modify it", :cyan
|
|
3228
3400
|
say " Run 'mysigner release list' to find the ID", :yellow
|
|
3229
3401
|
else
|
|
3230
3402
|
error "Validation failed: #{e.message}"
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
say " #{field}: #{errors_text}", :red
|
|
3235
|
-
end
|
|
3403
|
+
e.details&.each do |field, errors|
|
|
3404
|
+
errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
|
|
3405
|
+
say " #{field}: #{errors_text}", :red
|
|
3236
3406
|
end
|
|
3237
3407
|
end
|
|
3238
3408
|
exit 1
|
|
3239
3409
|
rescue Mysigner::ClientError => e
|
|
3240
3410
|
if e.message.include?('409') || e.message.include?('already exists')
|
|
3241
|
-
error
|
|
3242
|
-
say
|
|
3411
|
+
error 'Release configuration already exists for this bundle ID'
|
|
3412
|
+
say ''
|
|
3243
3413
|
say "💡 Use 'mysigner release update ID' to modify it", :cyan
|
|
3244
3414
|
say " Run 'mysigner release list' to find the ID", :yellow
|
|
3245
3415
|
else
|
|
@@ -3252,8 +3422,8 @@ module Mysigner
|
|
|
3252
3422
|
release_id = args[0]
|
|
3253
3423
|
|
|
3254
3424
|
unless release_id
|
|
3255
|
-
error
|
|
3256
|
-
say
|
|
3425
|
+
error 'Usage: mysigner release update ID [OPTIONS]'
|
|
3426
|
+
say ''
|
|
3257
3427
|
say "Run 'mysigner release list' to see available IDs", :yellow
|
|
3258
3428
|
exit 1
|
|
3259
3429
|
end
|
|
@@ -3268,8 +3438,8 @@ module Mysigner
|
|
|
3268
3438
|
body[:release_type] = options[:release_type] if options[:release_type]
|
|
3269
3439
|
body[:scheduled_date] = options[:scheduled_date] if options[:scheduled_date]
|
|
3270
3440
|
|
|
3271
|
-
say
|
|
3272
|
-
say
|
|
3441
|
+
say '🚀 Updating release configuration...', :cyan
|
|
3442
|
+
say ''
|
|
3273
3443
|
|
|
3274
3444
|
begin
|
|
3275
3445
|
response = client.patch(
|
|
@@ -3278,9 +3448,9 @@ module Mysigner
|
|
|
3278
3448
|
)
|
|
3279
3449
|
rel = response[:data]['app_store_release'] || response[:data]
|
|
3280
3450
|
|
|
3281
|
-
say
|
|
3282
|
-
say
|
|
3283
|
-
say
|
|
3451
|
+
say '✓ Release configuration updated!', :green
|
|
3452
|
+
say ''
|
|
3453
|
+
say 'Details:', :bold
|
|
3284
3454
|
say " ID: #{rel['id']}"
|
|
3285
3455
|
say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
|
|
3286
3456
|
say " Release Type: #{rel['release_type'] || 'N/A'}"
|
|
@@ -3288,16 +3458,14 @@ module Mysigner
|
|
|
3288
3458
|
say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
|
|
3289
3459
|
rescue Mysigner::NotFoundError
|
|
3290
3460
|
error "Release not found with ID: #{release_id}"
|
|
3291
|
-
say
|
|
3461
|
+
say ''
|
|
3292
3462
|
say "Run 'mysigner release list' to see available IDs", :yellow
|
|
3293
3463
|
exit 1
|
|
3294
3464
|
rescue Mysigner::ValidationError => e
|
|
3295
3465
|
error "Validation failed: #{e.message}"
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
say " #{field}: #{errors_text}", :red
|
|
3300
|
-
end
|
|
3466
|
+
e.details&.each do |field, errors|
|
|
3467
|
+
errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
|
|
3468
|
+
say " #{field}: #{errors_text}", :red
|
|
3301
3469
|
end
|
|
3302
3470
|
exit 1
|
|
3303
3471
|
rescue Mysigner::ClientError => e
|
|
@@ -3309,7 +3477,7 @@ module Mysigner
|
|
|
3309
3477
|
invoke :help, ['release']
|
|
3310
3478
|
else
|
|
3311
3479
|
error "Unknown action: #{action}"
|
|
3312
|
-
say
|
|
3480
|
+
say 'Available actions: list, show, create, update, help', :yellow
|
|
3313
3481
|
exit 1
|
|
3314
3482
|
end
|
|
3315
3483
|
end
|