mysigner 0.1.1 → 0.1.3
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 +1 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +112 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +87 -17
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/exe/mysigner +2 -1
- data/lib/mysigner/build/android_executor.rb +46 -52
- 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/cli/auth_commands.rb +735 -752
- data/lib/mysigner/cli/build_commands.rb +697 -721
- 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 +12 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
- data/lib/mysigner/cli/resource_commands.rb +1266 -822
- data/lib/mysigner/cli/validate_commands.rb +161 -0
- data/lib/mysigner/cli.rb +5 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +93 -56
- data/lib/mysigner/export/exporter.rb +32 -36
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/keystore_manager.rb +34 -39
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +51 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/play_store_uploader.rb +98 -115
- data/lib/mysigner/upload/uploader.rb +101 -109
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/test_manual.rb +37 -36
- metadata +38 -16
|
@@ -1,28 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mysigner
|
|
2
4
|
class CLI < Thor
|
|
3
5
|
module AuthCommands
|
|
4
6
|
def self.included(base)
|
|
5
7
|
base.class_eval do
|
|
6
|
-
desc
|
|
8
|
+
desc 'version', 'Show CLI version and system information'
|
|
7
9
|
def version
|
|
8
10
|
say "My Signer CLI v#{Mysigner::VERSION}", :cyan
|
|
9
|
-
say
|
|
11
|
+
say ''
|
|
10
12
|
say "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})", :white
|
|
11
|
-
say "Install: #{File.expand_path('
|
|
13
|
+
say "Install: #{File.expand_path('../../..', __dir__)}", :white
|
|
12
14
|
say "Config: #{Config::CONFIG_FILE}", :white
|
|
13
|
-
say
|
|
14
|
-
say
|
|
15
|
-
say
|
|
15
|
+
say ''
|
|
16
|
+
say 'Docs: https://mysigner.dev/docs/commands', :white
|
|
17
|
+
say 'Support: https://mysigner.dev/landing#contact', :white
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
desc
|
|
20
|
+
desc 'login', "Log in with existing API token (⭐ first-timers: use 'onboard' instead)"
|
|
19
21
|
long_desc <<~DESC
|
|
20
22
|
Authenticate with My Signer API using an API token.
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
New user? Run 'mysigner onboard' for step-by-step guidance.
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
Your credentials will be stored securely in ~/.mysigner/config.yml
|
|
25
|
-
|
|
27
|
+
|
|
26
28
|
Note: API tokens are organization-specific. This token will only
|
|
27
29
|
grant access to the organization it was created in.
|
|
28
30
|
DESC
|
|
@@ -31,86 +33,86 @@ module Mysigner
|
|
|
31
33
|
config = Config.new
|
|
32
34
|
if config.exists?
|
|
33
35
|
config.load
|
|
34
|
-
say
|
|
35
|
-
say
|
|
36
|
-
say
|
|
36
|
+
say '⚠️ Already logged in', :yellow
|
|
37
|
+
say ''
|
|
38
|
+
say 'Current configuration:', :yellow
|
|
37
39
|
say " User: #{config.user_email || '(unknown)'}"
|
|
38
40
|
say " Organization: #{config.org_name || '(unknown)'} (ID: #{config.current_organization_id})"
|
|
39
41
|
say " API URL: #{config.api_url}"
|
|
40
|
-
say
|
|
41
|
-
|
|
42
|
-
if yes?(
|
|
42
|
+
say ''
|
|
43
|
+
|
|
44
|
+
if yes?('Do you want to logout and login with different user? (y/n)')
|
|
43
45
|
config.clear
|
|
44
|
-
say
|
|
45
|
-
say
|
|
46
|
+
say '✓ Logged out successfully', :green
|
|
47
|
+
say ''
|
|
46
48
|
else
|
|
47
49
|
say "Login cancelled. Use 'mysigner logout' to logout first.", :yellow
|
|
48
|
-
say
|
|
50
|
+
say ''
|
|
49
51
|
say "💡 Tip: Use 'mysigner switch' to switch organizations for the same user", :yellow
|
|
50
52
|
return
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
54
|
-
say
|
|
55
|
-
say
|
|
56
|
-
say
|
|
56
|
+
say '🔐 My Signer Login', :cyan
|
|
57
|
+
say '=' * 80, :cyan
|
|
58
|
+
say ''
|
|
57
59
|
|
|
58
60
|
# Get API URL with smart default
|
|
59
61
|
api_url = prompt_api_url
|
|
60
|
-
say
|
|
61
|
-
|
|
62
|
+
say ''
|
|
63
|
+
|
|
62
64
|
# Get user email
|
|
63
65
|
user_email = prompt_for_email
|
|
64
|
-
say
|
|
65
|
-
|
|
66
|
+
say ''
|
|
67
|
+
|
|
66
68
|
# Show guidance for getting token
|
|
67
69
|
show_token_guidance(api_url)
|
|
68
|
-
|
|
69
|
-
api_token = ask(
|
|
70
|
-
say
|
|
71
|
-
|
|
70
|
+
|
|
71
|
+
api_token = ask('API Token:', echo: false)
|
|
72
|
+
say '' # New line after hidden input
|
|
73
|
+
|
|
72
74
|
if api_token.empty?
|
|
73
|
-
error
|
|
74
|
-
say
|
|
75
|
+
error 'API token cannot be empty'
|
|
76
|
+
say ''
|
|
75
77
|
say "💡 Tip: Run 'mysigner onboard' for detailed guidance", :yellow
|
|
76
78
|
exit 1
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
say
|
|
80
|
-
|
|
81
|
+
say 'Validating token and email...', :yellow
|
|
82
|
+
|
|
81
83
|
begin
|
|
82
84
|
client = Client.new(api_url: api_url, api_token: api_token, user_email: user_email)
|
|
83
85
|
response = client.test_connection
|
|
84
|
-
|
|
86
|
+
|
|
85
87
|
if response[:success]
|
|
86
|
-
say
|
|
88
|
+
say '✓ Token valid', :green
|
|
87
89
|
else
|
|
88
|
-
error
|
|
90
|
+
error 'Connection failed'
|
|
89
91
|
handle_connection_failure(api_url)
|
|
90
92
|
exit 1
|
|
91
93
|
end
|
|
92
|
-
|
|
94
|
+
|
|
93
95
|
# Fetch organization info (token can only access its own org)
|
|
94
|
-
say
|
|
95
|
-
|
|
96
|
+
say 'Detecting organization...', :yellow
|
|
97
|
+
|
|
96
98
|
# Try to fetch organizations - with org-specific tokens, this will return only the token's org
|
|
97
99
|
orgs_response = client.get('/api/v1/organizations')
|
|
98
100
|
organizations = orgs_response[:data]['organizations']
|
|
99
|
-
|
|
101
|
+
|
|
100
102
|
if ENV['DEBUG']
|
|
101
103
|
say "DEBUG: Found #{organizations.length} organizations", :cyan
|
|
102
104
|
organizations.each do |org|
|
|
103
105
|
say "DEBUG: - #{org['name']} (ID: #{org['id']})", :cyan
|
|
104
106
|
end
|
|
105
107
|
end
|
|
106
|
-
|
|
108
|
+
|
|
107
109
|
if organizations.empty?
|
|
108
|
-
error
|
|
109
|
-
say
|
|
110
|
-
say
|
|
110
|
+
error 'No organizations found for this token'
|
|
111
|
+
say ''
|
|
112
|
+
say 'This might mean:', :yellow
|
|
111
113
|
say " • Your token doesn't have access to any organizations", :yellow
|
|
112
|
-
say
|
|
113
|
-
say
|
|
114
|
+
say ' • The token was created but the organization was deleted', :yellow
|
|
115
|
+
say ''
|
|
114
116
|
show_create_org_guidance(api_url)
|
|
115
117
|
exit 1
|
|
116
118
|
end
|
|
@@ -118,19 +120,19 @@ module Mysigner
|
|
|
118
120
|
# With org-specific tokens, there should only be one organization
|
|
119
121
|
selected_org = organizations.first
|
|
120
122
|
org_id = selected_org['id']
|
|
121
|
-
|
|
123
|
+
|
|
122
124
|
say "DEBUG: Fetching details for organization #{org_id}...", :cyan if ENV['DEBUG']
|
|
123
|
-
|
|
125
|
+
|
|
124
126
|
# Get detailed org info to extract user email and token_organization_id
|
|
125
127
|
org_response = client.get("/api/v1/organizations/#{org_id}")
|
|
126
128
|
org_data = org_response[:data]
|
|
127
|
-
|
|
128
|
-
say
|
|
129
|
-
|
|
129
|
+
|
|
130
|
+
say 'DEBUG: Organization data received', :cyan if ENV['DEBUG']
|
|
131
|
+
|
|
130
132
|
say "✓ Organization detected: #{org_data['name']}", :green
|
|
131
133
|
say "✓ Email validated: #{user_email}", :green
|
|
132
|
-
say
|
|
133
|
-
|
|
134
|
+
say ''
|
|
135
|
+
|
|
134
136
|
# Save configuration with multi-token support
|
|
135
137
|
config = Config.new
|
|
136
138
|
config.api_url = api_url
|
|
@@ -139,49 +141,48 @@ module Mysigner
|
|
|
139
141
|
config.save_token_for_org(org_id, org_data['name'], api_token)
|
|
140
142
|
config.save
|
|
141
143
|
|
|
142
|
-
say
|
|
143
|
-
say
|
|
144
|
-
say
|
|
145
|
-
say
|
|
146
|
-
say
|
|
144
|
+
say ''
|
|
145
|
+
say '=' * 80, :green
|
|
146
|
+
say '✓ Successfully logged in!', :green
|
|
147
|
+
say '=' * 80, :green
|
|
148
|
+
say ''
|
|
147
149
|
say "Organization: #{org_data['name']} (ID: #{org_id})", :cyan
|
|
148
150
|
say "Role: #{org_data['role'] || 'viewer'}", :cyan
|
|
149
151
|
say "Config saved to: #{Config::CONFIG_FILE}", :cyan
|
|
150
|
-
say
|
|
151
|
-
say
|
|
152
|
-
say
|
|
152
|
+
say ''
|
|
153
|
+
say '🔒 Security Note:', :yellow
|
|
154
|
+
say ' Your token is organization-specific and can only access', :yellow
|
|
153
155
|
say " #{org_data['name']}. To access other organizations,", :yellow
|
|
154
156
|
say " use 'mysigner switch' to add tokens for those organizations.", :yellow
|
|
155
|
-
say
|
|
156
|
-
say
|
|
157
|
-
say
|
|
158
|
-
say
|
|
159
|
-
say
|
|
160
|
-
say
|
|
161
|
-
say
|
|
162
|
-
say
|
|
163
|
-
say
|
|
164
|
-
say
|
|
165
|
-
|
|
157
|
+
say ''
|
|
158
|
+
say '🚀 Next steps:', :bold
|
|
159
|
+
say ' cd your-ios-project'
|
|
160
|
+
say ' mysigner ship testflight'
|
|
161
|
+
say ''
|
|
162
|
+
say '💡 Helpful commands:', :cyan
|
|
163
|
+
say ' • mysigner doctor - Check your environment'
|
|
164
|
+
say ' • mysigner orgs - List all organizations'
|
|
165
|
+
say ' • mysigner switch - Switch to another organization'
|
|
166
|
+
say ''
|
|
166
167
|
rescue Mysigner::UnauthorizedError => e
|
|
167
|
-
error
|
|
168
|
-
say
|
|
169
|
-
|
|
168
|
+
error 'Authentication failed'
|
|
169
|
+
say ''
|
|
170
|
+
|
|
170
171
|
# Check if it's an email validation error
|
|
171
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
172
|
-
say
|
|
173
|
-
say
|
|
172
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
173
|
+
say '⚠️ Token Email Mismatch', :yellow
|
|
174
|
+
say ''
|
|
174
175
|
say "The token you provided doesn't belong to #{user_email}.", :yellow
|
|
175
|
-
say
|
|
176
|
-
say
|
|
176
|
+
say ''
|
|
177
|
+
say 'This could mean:', :yellow
|
|
177
178
|
say " • You're using a token created by someone else", :yellow
|
|
178
179
|
say " • You're using a token from a different account", :yellow
|
|
179
|
-
say
|
|
180
|
-
say
|
|
181
|
-
say
|
|
180
|
+
say ''
|
|
181
|
+
say '💡 Solutions:', :cyan
|
|
182
|
+
say ' 1. Generate a new token from your own account at:', :cyan
|
|
182
183
|
say " #{api_url}", :cyan
|
|
183
184
|
say " 2. Make sure you're logged in as #{user_email} on the web", :cyan
|
|
184
|
-
say
|
|
185
|
+
say ' 3. Check that you entered the correct email address', :cyan
|
|
185
186
|
else
|
|
186
187
|
handle_unauthorized_error(api_url)
|
|
187
188
|
end
|
|
@@ -189,16 +190,16 @@ module Mysigner
|
|
|
189
190
|
rescue Mysigner::ConnectionError => e
|
|
190
191
|
handle_connection_error(e, api_url)
|
|
191
192
|
exit 1
|
|
192
|
-
rescue => e
|
|
193
|
+
rescue StandardError => e
|
|
193
194
|
handle_unexpected_error(e, api_url)
|
|
194
195
|
exit 1
|
|
195
196
|
end
|
|
196
197
|
end
|
|
197
198
|
|
|
198
|
-
desc
|
|
199
|
+
desc 'onboard', '⭐ START HERE - Complete setup wizard for new users'
|
|
199
200
|
long_desc <<~DESC
|
|
200
201
|
Step-by-step guide to get started with My Signer CLI.
|
|
201
|
-
|
|
202
|
+
|
|
202
203
|
This command will:
|
|
203
204
|
1. Check if you have an account
|
|
204
205
|
2. Guide you through creating an organization
|
|
@@ -206,273 +207,271 @@ module Mysigner
|
|
|
206
207
|
4. Configure your CLI
|
|
207
208
|
DESC
|
|
208
209
|
def onboard
|
|
209
|
-
say
|
|
210
|
-
say
|
|
211
|
-
say
|
|
210
|
+
say '🚀 My Signer Setup Guide', :cyan
|
|
211
|
+
say '=' * 80, :cyan
|
|
212
|
+
say ''
|
|
212
213
|
say "Welcome! Let's get you set up with My Signer.", :bold
|
|
213
|
-
say
|
|
214
|
+
say ''
|
|
214
215
|
|
|
215
216
|
# Check if already configured
|
|
216
217
|
config = Config.new
|
|
217
218
|
if config.exists? && config.api_token && config.current_organization_id
|
|
218
219
|
say "✓ You're already logged in!", :green
|
|
219
|
-
say
|
|
220
|
-
say
|
|
220
|
+
say ''
|
|
221
|
+
say 'Current configuration:', :cyan
|
|
221
222
|
say " Email: #{config.user_email}"
|
|
222
223
|
say " Organization ID: #{config.current_organization_id}"
|
|
223
224
|
say " API URL: #{config.api_url}"
|
|
224
|
-
say
|
|
225
|
-
|
|
225
|
+
say ''
|
|
226
|
+
|
|
226
227
|
# Check App Store Connect status
|
|
227
228
|
begin
|
|
228
229
|
client = Client.new(api_url: config.api_url, api_token: config.api_token, user_email: config.user_email)
|
|
229
230
|
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
230
231
|
org_data = org_response[:data]
|
|
231
|
-
|
|
232
|
+
|
|
232
233
|
asc_configured = org_data['app_store_connect_configured'] || false
|
|
233
|
-
|
|
234
|
-
if
|
|
235
|
-
#
|
|
236
|
-
say
|
|
237
|
-
say ""
|
|
238
|
-
say
|
|
239
|
-
say "
|
|
240
|
-
say
|
|
241
|
-
say
|
|
242
|
-
say
|
|
243
|
-
say
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
say
|
|
247
|
-
|
|
234
|
+
|
|
235
|
+
if asc_configured
|
|
236
|
+
# Already fully configured
|
|
237
|
+
say '✓ App Store Connect: Configured', :green
|
|
238
|
+
say " Team ID: #{org_data['app_store_connect_team_id']}" if org_data['app_store_connect_team_id']
|
|
239
|
+
say ''
|
|
240
|
+
say "You're all set! 🎉", :bold
|
|
241
|
+
say ''
|
|
242
|
+
say 'What would you like to do?', :bold
|
|
243
|
+
say ' 1. Check status'
|
|
244
|
+
say ' 2. Switch to another organization'
|
|
245
|
+
say ' 3. Log out and start fresh'
|
|
246
|
+
say ' 4. Exit'
|
|
247
|
+
say ''
|
|
248
|
+
|
|
249
|
+
choice = ask('Select (1-4):', limited_to: %w[1 2 3 4])
|
|
250
|
+
say ''
|
|
251
|
+
|
|
248
252
|
case choice
|
|
249
253
|
when '1'
|
|
250
|
-
|
|
251
|
-
say "🚀 Setting up App Store Connect credentials...", :cyan
|
|
252
|
-
say ""
|
|
253
|
-
asc_configured = setup_app_store_connect_credentials(client, config, config.current_organization_id)
|
|
254
|
-
|
|
255
|
-
say ""
|
|
256
|
-
say "=" * 80, :green
|
|
257
|
-
if asc_configured
|
|
258
|
-
say "✓ App Store Connect configured successfully!", :green
|
|
259
|
-
else
|
|
260
|
-
say "⚠️ Setup incomplete", :yellow
|
|
261
|
-
say "Run 'mysigner onboard' again or use 'mysigner doctor'", :yellow
|
|
262
|
-
end
|
|
263
|
-
say "=" * 80, :green
|
|
254
|
+
invoke :status
|
|
264
255
|
return
|
|
265
256
|
when '2'
|
|
266
|
-
invoke :
|
|
257
|
+
invoke :switch
|
|
267
258
|
return
|
|
268
259
|
when '3'
|
|
269
|
-
say
|
|
270
|
-
say
|
|
260
|
+
say 'Clearing configuration...', :yellow
|
|
261
|
+
say ''
|
|
271
262
|
# Continue with full onboarding
|
|
272
263
|
when '4'
|
|
273
|
-
say
|
|
264
|
+
say 'No changes made.', :green
|
|
274
265
|
return
|
|
275
266
|
end
|
|
276
267
|
else
|
|
277
|
-
#
|
|
278
|
-
say
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
say ""
|
|
283
|
-
say
|
|
284
|
-
say
|
|
285
|
-
say
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
say
|
|
289
|
-
|
|
290
|
-
say ""
|
|
291
|
-
|
|
292
|
-
choice = ask("Select (1-4):", limited_to: ['1', '2', '3', '4'])
|
|
293
|
-
say ""
|
|
294
|
-
|
|
268
|
+
# Missing ASC credentials - offer to add them
|
|
269
|
+
say '⚠️ App Store Connect: Not configured', :yellow
|
|
270
|
+
say ''
|
|
271
|
+
say 'What would you like to do?', :bold
|
|
272
|
+
say ' 1. Set up App Store Connect credentials now'
|
|
273
|
+
say " 2. Check status with 'mysigner status'"
|
|
274
|
+
say ' 3. Log out and start fresh'
|
|
275
|
+
say ' 4. Exit'
|
|
276
|
+
say ''
|
|
277
|
+
|
|
278
|
+
choice = ask('Select (1-4):', limited_to: %w[1 2 3 4])
|
|
279
|
+
say ''
|
|
280
|
+
|
|
295
281
|
case choice
|
|
296
282
|
when '1'
|
|
297
|
-
|
|
283
|
+
# Go directly to ASC setup
|
|
284
|
+
say '🚀 Setting up App Store Connect credentials...', :cyan
|
|
285
|
+
say ''
|
|
286
|
+
asc_configured = setup_app_store_connect_credentials(client, config, config.current_organization_id)
|
|
287
|
+
|
|
288
|
+
say ''
|
|
289
|
+
say '=' * 80, :green
|
|
290
|
+
if asc_configured
|
|
291
|
+
say '✓ App Store Connect configured successfully!', :green
|
|
292
|
+
else
|
|
293
|
+
say '⚠️ Setup incomplete', :yellow
|
|
294
|
+
say "Run 'mysigner onboard' again or use 'mysigner doctor'", :yellow
|
|
295
|
+
end
|
|
296
|
+
say '=' * 80, :green
|
|
298
297
|
return
|
|
299
298
|
when '2'
|
|
300
|
-
invoke :
|
|
299
|
+
invoke :status
|
|
301
300
|
return
|
|
302
301
|
when '3'
|
|
303
|
-
say
|
|
304
|
-
say
|
|
302
|
+
say 'Clearing configuration...', :yellow
|
|
303
|
+
say ''
|
|
305
304
|
# Continue with full onboarding
|
|
306
305
|
when '4'
|
|
307
|
-
say
|
|
306
|
+
say 'No changes made.', :green
|
|
308
307
|
return
|
|
309
308
|
end
|
|
310
309
|
end
|
|
311
|
-
rescue => e
|
|
310
|
+
rescue StandardError => e
|
|
312
311
|
say "⚠️ Could not check organization status: #{e.message}", :yellow
|
|
313
|
-
say
|
|
314
|
-
|
|
315
|
-
unless yes_with_default?(
|
|
316
|
-
say
|
|
317
|
-
say
|
|
318
|
-
say
|
|
312
|
+
say ''
|
|
313
|
+
|
|
314
|
+
unless yes_with_default?('Do you want to re-configure from scratch?', :yellow)
|
|
315
|
+
say ''
|
|
316
|
+
say 'Keeping existing configuration.', :green
|
|
317
|
+
say ''
|
|
319
318
|
say "💡 Tip: Use 'mysigner status' to check your setup", :cyan
|
|
320
319
|
say "💡 Tip: Use 'mysigner switch' to add another organization", :cyan
|
|
321
320
|
return
|
|
322
321
|
end
|
|
323
|
-
|
|
324
|
-
say
|
|
325
|
-
say
|
|
326
|
-
say
|
|
322
|
+
|
|
323
|
+
say ''
|
|
324
|
+
say 'Clearing existing configuration...', :yellow
|
|
325
|
+
say ''
|
|
327
326
|
end
|
|
328
327
|
end
|
|
329
328
|
|
|
330
329
|
# Get API URL
|
|
331
330
|
api_url = prompt_api_url
|
|
332
|
-
say
|
|
331
|
+
say ''
|
|
333
332
|
|
|
334
333
|
# Step 1: Check if user has account
|
|
335
|
-
say
|
|
336
|
-
say
|
|
337
|
-
say
|
|
338
|
-
say
|
|
339
|
-
say
|
|
340
|
-
say
|
|
341
|
-
say
|
|
342
|
-
|
|
343
|
-
choice = ask(
|
|
344
|
-
say
|
|
345
|
-
|
|
334
|
+
say 'Step 1: Account Setup', :cyan
|
|
335
|
+
say '-' * 80
|
|
336
|
+
say ''
|
|
337
|
+
say 'Do you have a My Signer account?', :bold
|
|
338
|
+
say ' 1. Yes, I have an account'
|
|
339
|
+
say ' 2. No, I need to sign up'
|
|
340
|
+
say ''
|
|
341
|
+
|
|
342
|
+
choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
343
|
+
say ''
|
|
344
|
+
|
|
346
345
|
if choice == '2'
|
|
347
346
|
# Guide to signup
|
|
348
347
|
say "📝 Let's create your account:", :cyan
|
|
349
|
-
say
|
|
350
|
-
say
|
|
348
|
+
say ''
|
|
349
|
+
say '1. Open your browser and go to:', :bold
|
|
351
350
|
say " #{api_url}", :green
|
|
352
|
-
say
|
|
351
|
+
say ''
|
|
353
352
|
say "2. Click 'Sign Up' and create your account", :bold
|
|
354
|
-
say
|
|
355
|
-
say
|
|
356
|
-
say
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
say
|
|
353
|
+
say ''
|
|
354
|
+
say '3. Verify your email (check your inbox)', :bold
|
|
355
|
+
say ''
|
|
356
|
+
|
|
357
|
+
unless yes_with_default?('Have you created your account?', :green)
|
|
358
|
+
say ''
|
|
359
|
+
say "Come back and run 'mysigner onboard' when you're ready!", :yellow
|
|
360
|
+
return
|
|
361
|
+
end
|
|
362
|
+
say ''
|
|
364
363
|
end
|
|
365
364
|
|
|
366
365
|
# Step 2: Organization
|
|
367
|
-
say
|
|
368
|
-
say
|
|
369
|
-
say
|
|
370
|
-
say
|
|
371
|
-
say
|
|
372
|
-
say
|
|
373
|
-
say
|
|
374
|
-
|
|
375
|
-
choice = ask(
|
|
376
|
-
say
|
|
377
|
-
|
|
366
|
+
say 'Step 2: Organization Setup', :cyan
|
|
367
|
+
say '-' * 80
|
|
368
|
+
say ''
|
|
369
|
+
say 'Do you have an organization?', :bold
|
|
370
|
+
say ' 1. Yes, I have an organization'
|
|
371
|
+
say ' 2. No, I need to create one'
|
|
372
|
+
say ''
|
|
373
|
+
|
|
374
|
+
choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
375
|
+
say ''
|
|
376
|
+
|
|
378
377
|
if choice == '2'
|
|
379
378
|
# Guide to create org
|
|
380
379
|
say "🏢 Let's create your organization:", :cyan
|
|
381
|
-
say
|
|
382
|
-
say
|
|
380
|
+
say ''
|
|
381
|
+
say '1. Go to the dashboard:', :bold
|
|
383
382
|
say " #{api_url}", :green
|
|
384
|
-
say
|
|
385
|
-
say
|
|
386
|
-
say
|
|
383
|
+
say ''
|
|
384
|
+
say '2. Sign in with your account', :bold
|
|
385
|
+
say ''
|
|
387
386
|
say "3. Click 'Create Organization'", :bold
|
|
388
|
-
say
|
|
387
|
+
say ''
|
|
389
388
|
say "4. Enter your organization name (e.g., 'My Startup')", :bold
|
|
390
|
-
say
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
say
|
|
389
|
+
say ''
|
|
390
|
+
|
|
391
|
+
unless yes_with_default?('Have you created your organization?', :green)
|
|
392
|
+
say ''
|
|
393
|
+
say "Come back and run 'mysigner onboard' when you're ready!", :yellow
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
say ''
|
|
398
397
|
end
|
|
399
398
|
|
|
400
399
|
# Step 3: API Token
|
|
401
|
-
say
|
|
402
|
-
say
|
|
403
|
-
say
|
|
400
|
+
say 'Step 3: Generate API Token', :cyan
|
|
401
|
+
say '-' * 80
|
|
402
|
+
say ''
|
|
404
403
|
say "Now let's generate your API token:", :bold
|
|
405
|
-
say
|
|
406
|
-
say
|
|
404
|
+
say ''
|
|
405
|
+
say '1. Go to API Tokens:', :bold
|
|
407
406
|
say " #{api_url}/organizations/YOUR_ORG_ID/api_tokens", :green
|
|
408
|
-
say
|
|
409
|
-
say
|
|
410
|
-
say
|
|
407
|
+
say ''
|
|
408
|
+
say ' Or navigate: Dashboard → Your Organization → API Tokens', :cyan
|
|
409
|
+
say ''
|
|
411
410
|
say "2. Click 'Create Token'", :bold
|
|
412
|
-
say
|
|
413
|
-
say
|
|
411
|
+
say ''
|
|
412
|
+
say '3. Fill in the details:', :bold
|
|
414
413
|
say " • Name: 'CLI Access' (or anything you like)"
|
|
415
|
-
say
|
|
414
|
+
say ' • Scopes: ✓ read ✓ write (minimum required)'
|
|
416
415
|
say " • Expiration: Choose 'Never' or '1 year'"
|
|
417
|
-
say
|
|
416
|
+
say ''
|
|
418
417
|
say "4. Click 'Create' and COPY the token", :bold
|
|
419
418
|
say " ⚠️ You'll only see it once!", :yellow
|
|
420
|
-
say
|
|
421
|
-
|
|
422
|
-
unless yes_with_default?(
|
|
423
|
-
say
|
|
419
|
+
say ''
|
|
420
|
+
|
|
421
|
+
unless yes_with_default?('Have you generated and copied your token?', :green)
|
|
422
|
+
say ''
|
|
424
423
|
say "Come back and run 'mysigner onboard' when you have your token!", :yellow
|
|
425
424
|
return
|
|
426
425
|
end
|
|
427
|
-
say
|
|
426
|
+
say ''
|
|
428
427
|
|
|
429
428
|
# Step 4: Login
|
|
430
|
-
say
|
|
431
|
-
say
|
|
432
|
-
say
|
|
429
|
+
say 'Step 4: Login to CLI', :cyan
|
|
430
|
+
say '-' * 80
|
|
431
|
+
say ''
|
|
433
432
|
say "Great! Now let's log you in.", :bold
|
|
434
|
-
say
|
|
435
|
-
|
|
433
|
+
say ''
|
|
434
|
+
|
|
436
435
|
# Get user email
|
|
437
436
|
user_email = prompt_for_email
|
|
438
|
-
say
|
|
439
|
-
|
|
440
|
-
api_token = ask(
|
|
441
|
-
say
|
|
442
|
-
|
|
437
|
+
say ''
|
|
438
|
+
|
|
439
|
+
api_token = ask('Paste your API Token:', echo: false)
|
|
440
|
+
say ''
|
|
441
|
+
|
|
443
442
|
if api_token.empty?
|
|
444
|
-
error
|
|
443
|
+
error 'Token cannot be empty'
|
|
445
444
|
say "Run 'mysigner onboard' again when you have your token", :yellow
|
|
446
445
|
return
|
|
447
446
|
end
|
|
448
447
|
|
|
449
|
-
say
|
|
450
|
-
|
|
448
|
+
say 'Validating token and email...', :yellow
|
|
449
|
+
|
|
451
450
|
begin
|
|
452
451
|
client = Client.new(api_url: api_url, api_token: api_token, user_email: user_email)
|
|
453
452
|
response = client.test_connection
|
|
454
|
-
|
|
453
|
+
|
|
455
454
|
unless response[:success]
|
|
456
|
-
error
|
|
455
|
+
error 'Connection test failed'
|
|
457
456
|
return
|
|
458
457
|
end
|
|
459
|
-
|
|
458
|
+
|
|
460
459
|
response = client.get('/api/v1/organizations')
|
|
461
460
|
organizations = response[:data]['organizations']
|
|
462
|
-
|
|
461
|
+
|
|
463
462
|
if organizations.empty?
|
|
464
|
-
error
|
|
465
|
-
say
|
|
463
|
+
error 'No organizations found'
|
|
464
|
+
say 'Please check that your token is associated with an organization', :yellow
|
|
466
465
|
return
|
|
467
466
|
end
|
|
468
467
|
|
|
469
468
|
selected_org = organizations.first
|
|
470
469
|
org_id = selected_org['id']
|
|
471
|
-
|
|
470
|
+
|
|
472
471
|
# Get detailed org info
|
|
473
472
|
org_response = client.get("/api/v1/organizations/#{org_id}")
|
|
474
473
|
org_data = org_response[:data]
|
|
475
|
-
|
|
474
|
+
|
|
476
475
|
config = Config.new
|
|
477
476
|
config.api_url = api_url
|
|
478
477
|
config.user_email = user_email # Save the verified email
|
|
@@ -481,228 +480,216 @@ module Mysigner
|
|
|
481
480
|
config.save
|
|
482
481
|
|
|
483
482
|
# Step 5: App Store Connect Setup (Optional)
|
|
484
|
-
say
|
|
485
|
-
say
|
|
486
|
-
say
|
|
487
|
-
say
|
|
488
|
-
|
|
483
|
+
say ''
|
|
484
|
+
say 'Step 5: App Store Connect Setup (optional but recommended)', :cyan
|
|
485
|
+
say '-' * 80
|
|
486
|
+
say ''
|
|
487
|
+
|
|
489
488
|
# Check if already configured
|
|
490
489
|
asc_configured = org_data['app_store_connect_configured'] || false
|
|
491
|
-
|
|
490
|
+
|
|
492
491
|
if asc_configured
|
|
493
|
-
say
|
|
494
|
-
say
|
|
495
|
-
say
|
|
496
|
-
if org_data['app_store_connect_team_id']
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
say
|
|
500
|
-
say ""
|
|
501
|
-
say "You can manage credentials in the web dashboard:", :cyan
|
|
492
|
+
say '✓ App Store Connect is already configured!', :green
|
|
493
|
+
say ''
|
|
494
|
+
say 'Current setup:', :cyan
|
|
495
|
+
say " • Team ID: #{org_data['app_store_connect_team_id']}" if org_data['app_store_connect_team_id']
|
|
496
|
+
say ' • Status: Active'
|
|
497
|
+
say ''
|
|
498
|
+
say 'You can manage credentials in the web dashboard:', :cyan
|
|
502
499
|
say " #{api_url}/organizations/#{org_id}", :green
|
|
503
|
-
say
|
|
504
|
-
say
|
|
505
|
-
say
|
|
500
|
+
say ''
|
|
501
|
+
say '💡 Tip: You can add multiple credentials (for different teams)', :cyan
|
|
502
|
+
say ''
|
|
506
503
|
else
|
|
507
|
-
say
|
|
508
|
-
say
|
|
509
|
-
say
|
|
510
|
-
say
|
|
511
|
-
say
|
|
512
|
-
say
|
|
513
|
-
|
|
514
|
-
asc_choice = ask(
|
|
515
|
-
say
|
|
516
|
-
|
|
504
|
+
say 'To upload to TestFlight/App Store, we need your API credentials.', :bold
|
|
505
|
+
say ''
|
|
506
|
+
say 'Do you want to set this up now?', :bold
|
|
507
|
+
say ' 1. Yes, guide me through it (recommended)'
|
|
508
|
+
say ' 2. Skip for now (you can do this later)'
|
|
509
|
+
say ''
|
|
510
|
+
|
|
511
|
+
asc_choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
512
|
+
say ''
|
|
513
|
+
|
|
517
514
|
if asc_choice == '1'
|
|
518
515
|
asc_configured = setup_app_store_connect_credentials(client, config, org_id)
|
|
519
516
|
else
|
|
520
|
-
say
|
|
521
|
-
say
|
|
522
|
-
say
|
|
523
|
-
say
|
|
524
|
-
say
|
|
525
|
-
say
|
|
517
|
+
say '⏭️ Skipped App Store Connect setup', :yellow
|
|
518
|
+
say ''
|
|
519
|
+
say 'You can set this up later by:', :cyan
|
|
520
|
+
say ' • Running: mysigner doctor'
|
|
521
|
+
say ' • Or via the web dashboard'
|
|
522
|
+
say ''
|
|
526
523
|
end
|
|
527
524
|
end
|
|
528
525
|
|
|
529
|
-
say
|
|
530
|
-
say
|
|
531
|
-
say
|
|
532
|
-
say
|
|
533
|
-
say
|
|
526
|
+
say ''
|
|
527
|
+
say '=' * 80, :green
|
|
528
|
+
say '🎉 Setup Complete!', :green
|
|
529
|
+
say '=' * 80, :green
|
|
530
|
+
say ''
|
|
534
531
|
say "You're all set up and ready to go!", :bold
|
|
535
|
-
say
|
|
532
|
+
say ''
|
|
536
533
|
say "User: #{user_email}", :cyan
|
|
537
534
|
say "Organization: #{org_data['name']} (ID: #{org_id})", :cyan
|
|
538
535
|
say "Config saved to: #{Config::CONFIG_FILE}", :cyan
|
|
539
|
-
|
|
536
|
+
|
|
540
537
|
# Show App Store Connect status
|
|
541
|
-
say
|
|
538
|
+
say ''
|
|
542
539
|
if asc_configured
|
|
543
|
-
say
|
|
540
|
+
say '✓ App Store Connect: Configured', :green
|
|
544
541
|
elsif defined?(asc_choice) && asc_choice == '1'
|
|
545
|
-
say
|
|
546
|
-
say
|
|
542
|
+
say '⚠️ App Store Connect:', :yellow
|
|
543
|
+
say ' Setup was attempted but not completed', :yellow
|
|
547
544
|
say " Run 'mysigner doctor' to configure it", :yellow
|
|
548
545
|
else
|
|
549
|
-
say
|
|
546
|
+
say '⚠️ App Store Connect: Not configured', :yellow
|
|
550
547
|
say " Run 'mysigner doctor' to set it up", :yellow
|
|
551
548
|
end
|
|
552
|
-
|
|
553
|
-
say
|
|
554
|
-
say
|
|
549
|
+
|
|
550
|
+
say ''
|
|
551
|
+
say '🔒 Security Note:', :yellow
|
|
555
552
|
say " Your token is organization-specific. Use 'mysigner switch'", :yellow
|
|
556
|
-
say
|
|
557
|
-
say
|
|
558
|
-
say
|
|
559
|
-
say
|
|
560
|
-
say
|
|
561
|
-
say
|
|
562
|
-
say
|
|
563
|
-
say
|
|
553
|
+
say ' to add tokens for other organizations.', :yellow
|
|
554
|
+
say ''
|
|
555
|
+
say '🚀 Try your first ship:', :bold
|
|
556
|
+
say ''
|
|
557
|
+
say ' cd your-ios-project'
|
|
558
|
+
say ' mysigner ship testflight'
|
|
559
|
+
say ''
|
|
560
|
+
say '💡 Tips:', :cyan
|
|
564
561
|
say " • Run 'mysigner doctor' to check your environment"
|
|
565
562
|
say " • Run 'mysigner --help' to see all commands"
|
|
566
563
|
say " • Run 'mysigner status' to verify your setup"
|
|
567
|
-
say
|
|
568
|
-
|
|
564
|
+
say ''
|
|
569
565
|
rescue Mysigner::UnauthorizedError => e
|
|
570
|
-
error
|
|
571
|
-
say
|
|
572
|
-
|
|
566
|
+
error 'Authentication failed'
|
|
567
|
+
say ''
|
|
568
|
+
|
|
573
569
|
# Check if it's an email validation error
|
|
574
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
575
|
-
say
|
|
576
|
-
say
|
|
570
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
571
|
+
say '⚠️ Token Email Mismatch', :yellow
|
|
572
|
+
say ''
|
|
577
573
|
say "The token you provided doesn't belong to #{user_email}.", :yellow
|
|
578
|
-
say
|
|
579
|
-
say
|
|
574
|
+
say ''
|
|
575
|
+
say 'Please make sure you:', :yellow
|
|
580
576
|
say " 1. Are logged in as #{user_email} on the web dashboard", :yellow
|
|
581
577
|
say " 2. Generate the token while logged in as #{user_email}", :yellow
|
|
582
|
-
say
|
|
578
|
+
say ' 3. Enter the correct email address', :yellow
|
|
583
579
|
else
|
|
584
|
-
say
|
|
585
|
-
say
|
|
580
|
+
say 'The token you entered is invalid. Please:', :yellow
|
|
581
|
+
say ' 1. Check you copied the entire token'
|
|
586
582
|
say " 2. Make sure the token hasn't been revoked"
|
|
587
|
-
say
|
|
583
|
+
say ' 3. Generate a new token if needed'
|
|
588
584
|
end
|
|
589
|
-
say
|
|
585
|
+
say ''
|
|
590
586
|
say "Run 'mysigner onboard' to try again", :yellow
|
|
591
|
-
rescue => e
|
|
587
|
+
rescue StandardError => e
|
|
592
588
|
error "Setup failed: #{e.message}"
|
|
593
|
-
say
|
|
589
|
+
say ''
|
|
594
590
|
say "Run 'mysigner onboard' to try again", :yellow
|
|
595
591
|
end
|
|
596
592
|
end
|
|
597
593
|
|
|
598
|
-
desc
|
|
594
|
+
desc 'logout', 'Log out and clear stored credentials'
|
|
599
595
|
def logout
|
|
600
596
|
config = Config.new
|
|
601
|
-
|
|
597
|
+
|
|
602
598
|
unless config.exists?
|
|
603
|
-
say
|
|
599
|
+
say 'No stored credentials found', :yellow
|
|
604
600
|
return
|
|
605
601
|
end
|
|
606
602
|
|
|
607
|
-
if yes?(
|
|
603
|
+
if yes?('Are you sure you want to logout? (y/n)')
|
|
608
604
|
config.clear
|
|
609
|
-
say
|
|
605
|
+
say '✓ Successfully logged out', :green
|
|
610
606
|
say "Config file removed: #{Config::CONFIG_FILE}", :green
|
|
611
607
|
else
|
|
612
|
-
say
|
|
608
|
+
say 'Logout cancelled', :yellow
|
|
613
609
|
end
|
|
614
610
|
end
|
|
615
611
|
|
|
616
|
-
desc
|
|
612
|
+
desc 'status', 'Check connection, credentials, and App Store Connect setup'
|
|
617
613
|
def status
|
|
618
|
-
config =
|
|
619
|
-
|
|
620
|
-
unless config.exists?
|
|
621
|
-
error "Not logged in. Run 'mysigner login' first."
|
|
622
|
-
exit 1
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
config.load
|
|
614
|
+
config = load_config
|
|
626
615
|
|
|
627
|
-
say
|
|
628
|
-
say
|
|
629
|
-
say
|
|
616
|
+
say '📊 My Signer Status', :cyan
|
|
617
|
+
say ''
|
|
618
|
+
say 'Configuration:', :bold
|
|
630
619
|
say " API URL: #{config.api_url}"
|
|
631
620
|
say " User: #{config.user_email || '(unknown)'}"
|
|
632
621
|
say " Encryption: #{config.encrypted_config? ? '✓ Enabled' : '✗ Disabled'}"
|
|
633
|
-
say
|
|
622
|
+
say ''
|
|
634
623
|
|
|
635
624
|
# Show current organization
|
|
636
625
|
if config.current_organization_id
|
|
637
|
-
say
|
|
626
|
+
say 'Current Organization:', :bold
|
|
638
627
|
say " Name: #{config.org_name || '(unknown)'}"
|
|
639
628
|
say " ID: #{config.current_organization_id}"
|
|
640
629
|
say " Token: #{config.display[:current_token]}"
|
|
641
|
-
say
|
|
630
|
+
say ''
|
|
642
631
|
end
|
|
643
632
|
|
|
644
633
|
# Show all saved organizations
|
|
645
634
|
if config.organization_ids.length > 1
|
|
646
635
|
say "Saved Organizations: (#{config.organization_ids.length})", :bold
|
|
647
636
|
config.organization_ids.each do |org_id|
|
|
648
|
-
current_marker = org_id == config.current_organization_id ?
|
|
649
|
-
org_name = config.org_name(org_id) ||
|
|
637
|
+
current_marker = org_id == config.current_organization_id ? ' (current)' : ''
|
|
638
|
+
org_name = config.org_name(org_id) || 'Unknown'
|
|
650
639
|
say " • #{org_name}#{current_marker} (ID: #{org_id})"
|
|
651
640
|
end
|
|
652
|
-
say
|
|
641
|
+
say ''
|
|
653
642
|
end
|
|
654
643
|
|
|
655
644
|
# Test connection
|
|
656
|
-
say
|
|
657
|
-
|
|
645
|
+
say 'Connection:', :bold
|
|
646
|
+
|
|
658
647
|
begin
|
|
659
648
|
client = Client.new(api_url: config.api_url, api_token: config.api_token)
|
|
660
649
|
client.test_connection
|
|
661
|
-
|
|
662
|
-
say
|
|
663
|
-
|
|
650
|
+
|
|
651
|
+
say ' Status: ✓ Connected', :green
|
|
652
|
+
|
|
664
653
|
# Get organization details
|
|
665
654
|
if config.current_organization_id
|
|
666
655
|
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
667
656
|
org = org_response[:data]
|
|
668
|
-
|
|
657
|
+
|
|
669
658
|
say " Role: #{org['role'] || 'viewer'}"
|
|
670
659
|
say " Members: #{org['member_count'] || 0}"
|
|
671
|
-
say
|
|
672
|
-
|
|
660
|
+
say ''
|
|
661
|
+
|
|
673
662
|
# Show App Store Connect status
|
|
674
|
-
say
|
|
663
|
+
say 'App Store Connect:', :bold
|
|
675
664
|
if org['app_store_connect_configured']
|
|
676
|
-
say
|
|
677
|
-
if org['app_store_connect_team_id']
|
|
678
|
-
say " Team ID: #{org['app_store_connect_team_id']}"
|
|
679
|
-
end
|
|
665
|
+
say ' ✓ Configured', :green
|
|
666
|
+
say " Team ID: #{org['app_store_connect_team_id']}" if org['app_store_connect_team_id']
|
|
680
667
|
else
|
|
681
|
-
say
|
|
668
|
+
say ' ✗ Not configured', :yellow
|
|
682
669
|
say " Run 'mysigner doctor' to set it up"
|
|
683
670
|
end
|
|
684
671
|
end
|
|
685
672
|
rescue Mysigner::UnauthorizedError
|
|
686
|
-
say
|
|
673
|
+
say ' Status: ✗ Unauthorized (invalid token)', :red
|
|
687
674
|
exit 1
|
|
688
675
|
rescue Mysigner::ConnectionError => e
|
|
689
|
-
say
|
|
676
|
+
say ' Status: ✗ Connection failed', :red
|
|
690
677
|
say " Error: #{e.message}", :red
|
|
691
678
|
exit 1
|
|
692
|
-
rescue => e
|
|
693
|
-
say
|
|
679
|
+
rescue StandardError => e
|
|
680
|
+
say ' Status: ✗ Error', :red
|
|
694
681
|
say " Error: #{e.message}", :red
|
|
695
682
|
exit 1
|
|
696
683
|
end
|
|
697
684
|
end
|
|
698
685
|
|
|
699
|
-
desc
|
|
686
|
+
desc 'orgs', "List all organizations you're a member of"
|
|
700
687
|
def orgs
|
|
701
688
|
config = load_config
|
|
702
689
|
client = create_client(config)
|
|
703
690
|
|
|
704
|
-
say
|
|
705
|
-
say
|
|
691
|
+
say '📋 Organizations', :cyan
|
|
692
|
+
say ''
|
|
706
693
|
|
|
707
694
|
begin
|
|
708
695
|
# Fetch ALL organizations the user is a member of (not restricted by token's org)
|
|
@@ -713,37 +700,37 @@ module Mysigner
|
|
|
713
700
|
all_org_ids = (config.organization_ids + api_organizations.map { |o| o['id'] }).uniq
|
|
714
701
|
|
|
715
702
|
if all_org_ids.empty?
|
|
716
|
-
say
|
|
703
|
+
say 'No organizations found', :yellow
|
|
717
704
|
return
|
|
718
705
|
end
|
|
719
706
|
|
|
720
707
|
all_org_ids.each do |org_id|
|
|
721
708
|
has_token = config.has_token_for_org?(org_id)
|
|
722
709
|
is_current = org_id == config.current_organization_id
|
|
723
|
-
|
|
710
|
+
|
|
724
711
|
# Get org details
|
|
725
712
|
org_name = config.org_name(org_id)
|
|
726
713
|
api_org = api_organizations.find { |o| o['id'] == org_id }
|
|
727
714
|
org_name = api_org['name'] if api_org && (org_name.nil? || org_name == 'Unknown')
|
|
728
|
-
|
|
729
|
-
current_marker = is_current ?
|
|
730
|
-
token_status = has_token ?
|
|
731
|
-
|
|
715
|
+
|
|
716
|
+
current_marker = is_current ? ' (current)' : ''
|
|
717
|
+
token_status = has_token ? '✓' : '⚠️'
|
|
718
|
+
|
|
732
719
|
say " #{token_status} #{org_name}#{current_marker}", :green
|
|
733
|
-
|
|
720
|
+
|
|
734
721
|
if api_org
|
|
735
722
|
role = api_org['role'] || 'viewer'
|
|
736
723
|
say " ID: #{org_id} | Role: #{role} | Members: #{api_org['member_count'] || 0}"
|
|
737
724
|
else
|
|
738
725
|
say " ID: #{org_id} | #{has_token ? 'Token saved' : 'Need token to access'}"
|
|
739
726
|
end
|
|
740
|
-
say
|
|
727
|
+
say ''
|
|
741
728
|
end
|
|
742
729
|
|
|
743
730
|
say "Total: #{all_org_ids.length} organization(s)", :white
|
|
744
|
-
say
|
|
745
|
-
say
|
|
746
|
-
say
|
|
731
|
+
say ''
|
|
732
|
+
say 'Legend: ✓ = Has token | ⚠️ = Need token', :white
|
|
733
|
+
say ''
|
|
747
734
|
say "💡 Tip: Use 'mysigner switch' to change organizations", :yellow
|
|
748
735
|
rescue Mysigner::ClientError => e
|
|
749
736
|
error "Failed to fetch organizations: #{e.message}"
|
|
@@ -751,10 +738,10 @@ module Mysigner
|
|
|
751
738
|
end
|
|
752
739
|
end
|
|
753
740
|
|
|
754
|
-
desc
|
|
741
|
+
desc 'switch', 'Switch between organizations (for multi-org users)'
|
|
755
742
|
long_desc <<~DESC
|
|
756
743
|
Switch to a different organization.
|
|
757
|
-
|
|
744
|
+
|
|
758
745
|
With organization-specific tokens, you'll need a token for each
|
|
759
746
|
organization you want to access. This command will:
|
|
760
747
|
- Show all organizations you're a member of
|
|
@@ -762,7 +749,7 @@ module Mysigner
|
|
|
762
749
|
- Prompt for a token if switching to an org without one
|
|
763
750
|
- Validate the token belongs to the target organization
|
|
764
751
|
- Update your configuration
|
|
765
|
-
|
|
752
|
+
|
|
766
753
|
Note: You need to be the same user in all organizations. Tokens
|
|
767
754
|
from different user accounts will be rejected.
|
|
768
755
|
DESC
|
|
@@ -770,43 +757,43 @@ module Mysigner
|
|
|
770
757
|
config = load_config
|
|
771
758
|
client = create_client(config)
|
|
772
759
|
|
|
773
|
-
say
|
|
774
|
-
say
|
|
760
|
+
say '🔄 Switch Organization', :cyan
|
|
761
|
+
say ''
|
|
775
762
|
|
|
776
763
|
begin
|
|
777
764
|
# Get current org details
|
|
778
765
|
current_org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
779
766
|
current_org = current_org_response[:data]
|
|
780
767
|
|
|
781
|
-
say
|
|
768
|
+
say 'Current organization:', :yellow
|
|
782
769
|
say " #{current_org['name']} (ID: #{config.current_organization_id})", :green
|
|
783
|
-
say
|
|
770
|
+
say ''
|
|
784
771
|
|
|
785
772
|
# Fetch ALL organizations the user is a member of (not restricted by token's org)
|
|
786
773
|
response = client.get('/api/v1/user/organizations')
|
|
787
774
|
api_organizations = response[:data]['organizations']
|
|
788
|
-
|
|
775
|
+
|
|
789
776
|
# Build comprehensive list: stored orgs + API orgs
|
|
790
777
|
all_org_ids = (config.organization_ids + api_organizations.map { |o| o['id'] }).uniq
|
|
791
|
-
|
|
778
|
+
|
|
792
779
|
if all_org_ids.length < 2
|
|
793
|
-
say
|
|
794
|
-
say
|
|
795
|
-
say
|
|
780
|
+
say 'You only have access to one organization.', :yellow
|
|
781
|
+
say 'Nothing to switch to!', :yellow
|
|
782
|
+
say ''
|
|
796
783
|
say "💡 Tip: If you're a member of other organizations, you'll need", :cyan
|
|
797
|
-
say
|
|
784
|
+
say ' to generate tokens for them first (in the web dashboard).', :cyan
|
|
798
785
|
return
|
|
799
786
|
end
|
|
800
787
|
|
|
801
788
|
# Show available organizations
|
|
802
|
-
say
|
|
803
|
-
say
|
|
789
|
+
say 'Available organizations:', :cyan
|
|
790
|
+
say ''
|
|
804
791
|
organizations_list = []
|
|
805
|
-
|
|
792
|
+
|
|
806
793
|
all_org_ids.each_with_index do |org_id, index|
|
|
807
794
|
has_token = config.has_token_for_org?(org_id)
|
|
808
795
|
is_current = org_id == config.current_organization_id
|
|
809
|
-
|
|
796
|
+
|
|
810
797
|
# Get org name from config or API
|
|
811
798
|
org_name = config.org_name(org_id)
|
|
812
799
|
if org_name.nil? || org_name == 'Unknown'
|
|
@@ -814,63 +801,63 @@ module Mysigner
|
|
|
814
801
|
api_org = api_organizations.find { |o| o['id'] == org_id }
|
|
815
802
|
org_name = api_org['name'] if api_org
|
|
816
803
|
end
|
|
817
|
-
|
|
818
|
-
status = has_token ?
|
|
819
|
-
current_marker = is_current ?
|
|
820
|
-
|
|
804
|
+
|
|
805
|
+
status = has_token ? '✓' : '⚠️ '
|
|
806
|
+
current_marker = is_current ? ' (current)' : ''
|
|
807
|
+
|
|
821
808
|
say " #{index + 1}. #{status} #{org_name}#{current_marker}"
|
|
822
809
|
say " ID: #{org_id} | #{has_token ? 'Token saved' : 'Need token'}", :white
|
|
823
|
-
|
|
810
|
+
|
|
824
811
|
organizations_list << { id: org_id, name: org_name, has_token: has_token }
|
|
825
812
|
end
|
|
826
|
-
|
|
827
|
-
say
|
|
828
|
-
say
|
|
829
|
-
say
|
|
813
|
+
|
|
814
|
+
say ''
|
|
815
|
+
say 'Legend: ✓ = Has token | ⚠️ = Needs token', :white
|
|
816
|
+
say ''
|
|
830
817
|
|
|
831
818
|
# Let user select
|
|
832
819
|
org_index = ask("Select organization (1-#{organizations_list.length}, or 'q' to cancel):")
|
|
833
|
-
|
|
820
|
+
|
|
834
821
|
if org_index.downcase == 'q'
|
|
835
|
-
say
|
|
822
|
+
say 'Cancelled', :yellow
|
|
836
823
|
return
|
|
837
824
|
end
|
|
838
|
-
|
|
825
|
+
|
|
839
826
|
unless org_index.match(/^\d+$/) && org_index.to_i.between?(1, organizations_list.length)
|
|
840
|
-
error
|
|
827
|
+
error 'Invalid selection'
|
|
841
828
|
return
|
|
842
829
|
end
|
|
843
|
-
|
|
830
|
+
|
|
844
831
|
selected_org = organizations_list[org_index.to_i - 1]
|
|
845
832
|
|
|
846
833
|
if selected_org[:id] == config.current_organization_id
|
|
847
|
-
say
|
|
848
|
-
say
|
|
834
|
+
say ''
|
|
835
|
+
say 'Already using this organization!', :yellow
|
|
849
836
|
return
|
|
850
837
|
end
|
|
851
838
|
|
|
852
839
|
# Check if we have a token for this org
|
|
853
840
|
unless selected_org[:has_token]
|
|
854
|
-
say
|
|
841
|
+
say ''
|
|
855
842
|
say "⚠️ You don't have a token for '#{selected_org[:name]}' yet.", :yellow
|
|
856
|
-
say
|
|
857
|
-
say
|
|
843
|
+
say ''
|
|
844
|
+
say 'To switch to this organization:', :cyan
|
|
858
845
|
say " 1. Go to: #{config.api_url}/organizations/#{selected_org[:id]}/api_tokens"
|
|
859
|
-
say
|
|
860
|
-
say
|
|
861
|
-
say
|
|
862
|
-
|
|
846
|
+
say ' 2. Generate a new API token'
|
|
847
|
+
say ' 3. Paste it below'
|
|
848
|
+
say ''
|
|
849
|
+
|
|
863
850
|
new_token = ask("Paste API token for '#{selected_org[:name]}' (or 'q' to cancel):", echo: false)
|
|
864
|
-
say
|
|
865
|
-
|
|
851
|
+
say ''
|
|
852
|
+
|
|
866
853
|
if new_token.downcase == 'q' || new_token.empty?
|
|
867
|
-
say
|
|
854
|
+
say 'Cancelled', :yellow
|
|
868
855
|
return
|
|
869
856
|
end
|
|
870
|
-
|
|
857
|
+
|
|
871
858
|
# Validate the new token (with email validation)
|
|
872
|
-
say
|
|
873
|
-
|
|
859
|
+
say 'Validating token...', :yellow
|
|
860
|
+
|
|
874
861
|
begin
|
|
875
862
|
# Use stored email from config for validation
|
|
876
863
|
temp_client = Client.new(
|
|
@@ -878,42 +865,42 @@ module Mysigner
|
|
|
878
865
|
api_token: new_token,
|
|
879
866
|
user_email: config.user_email
|
|
880
867
|
)
|
|
881
|
-
|
|
868
|
+
|
|
882
869
|
# Try to fetch the target organization with the new token
|
|
883
870
|
validation_response = temp_client.get("/api/v1/organizations/#{selected_org[:id]}")
|
|
884
871
|
token_org_data = validation_response[:data]
|
|
885
|
-
|
|
872
|
+
|
|
886
873
|
# Check if token_organization_id matches (new backend feature)
|
|
887
874
|
if token_org_data['token_organization_id'] && token_org_data['token_organization_id'] != selected_org[:id]
|
|
888
|
-
error
|
|
889
|
-
say
|
|
890
|
-
say "The token you provided is for organization ID #{token_org_data['token_organization_id']},",
|
|
875
|
+
error 'This token belongs to a different organization!'
|
|
876
|
+
say ''
|
|
877
|
+
say "The token you provided is for organization ID #{token_org_data['token_organization_id']},",
|
|
878
|
+
:yellow
|
|
891
879
|
say "but you're trying to access organization ID #{selected_org[:id]}.", :yellow
|
|
892
|
-
say
|
|
893
|
-
say
|
|
880
|
+
say ''
|
|
881
|
+
say 'Please generate a token from the correct organization.', :yellow
|
|
894
882
|
exit 1
|
|
895
883
|
end
|
|
896
|
-
|
|
897
|
-
say
|
|
898
|
-
|
|
884
|
+
|
|
885
|
+
say '✓ Token validated successfully', :green
|
|
886
|
+
|
|
899
887
|
# Save the token
|
|
900
888
|
config.save_token_for_org(selected_org[:id], selected_org[:name], new_token)
|
|
901
|
-
|
|
902
889
|
rescue Mysigner::UnauthorizedError => e
|
|
903
|
-
error
|
|
904
|
-
say
|
|
905
|
-
|
|
890
|
+
error 'Token validation failed'
|
|
891
|
+
say ''
|
|
892
|
+
|
|
906
893
|
# Check if it's an email validation error
|
|
907
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
894
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
908
895
|
say "⚠️ This token doesn't belong to #{config.user_email}!", :yellow
|
|
909
|
-
say
|
|
896
|
+
say ''
|
|
910
897
|
say "You can only use tokens from your own account (#{config.user_email}).", :yellow
|
|
911
898
|
say "Please generate a token while logged in as #{config.user_email} on the web.", :yellow
|
|
912
899
|
else
|
|
913
|
-
say
|
|
900
|
+
say 'The token you provided is not valid.', :yellow
|
|
914
901
|
end
|
|
915
902
|
exit 1
|
|
916
|
-
rescue => e
|
|
903
|
+
rescue StandardError => e
|
|
917
904
|
error "Token validation failed: #{e.message}"
|
|
918
905
|
exit 1
|
|
919
906
|
end
|
|
@@ -923,18 +910,17 @@ module Mysigner
|
|
|
923
910
|
config.current_organization_id = selected_org[:id]
|
|
924
911
|
config.save
|
|
925
912
|
|
|
926
|
-
say
|
|
913
|
+
say ''
|
|
927
914
|
say "✓ Successfully switched to: #{selected_org[:name]}", :green
|
|
928
|
-
say
|
|
915
|
+
say ''
|
|
929
916
|
say "💡 Run 'mysigner status' to verify your new configuration", :cyan
|
|
930
|
-
|
|
931
917
|
rescue Mysigner::ClientError => e
|
|
932
918
|
error "Failed to switch organization: #{e.message}"
|
|
933
919
|
exit 1
|
|
934
920
|
end
|
|
935
921
|
end
|
|
936
922
|
|
|
937
|
-
desc
|
|
923
|
+
desc 'config', 'Show current CLI configuration (API URL, tokens, org)'
|
|
938
924
|
def config
|
|
939
925
|
config = Config.new
|
|
940
926
|
|
|
@@ -945,12 +931,12 @@ module Mysigner
|
|
|
945
931
|
|
|
946
932
|
config.load
|
|
947
933
|
|
|
948
|
-
say
|
|
949
|
-
say
|
|
934
|
+
say '⚙️ Configuration', :cyan
|
|
935
|
+
say ''
|
|
950
936
|
config.display.each do |key, value|
|
|
951
937
|
say " #{key.to_s.ljust(20)}: #{value}"
|
|
952
938
|
end
|
|
953
|
-
say
|
|
939
|
+
say ''
|
|
954
940
|
say "Config file: #{Config::CONFIG_FILE}"
|
|
955
941
|
end
|
|
956
942
|
|
|
@@ -963,273 +949,272 @@ module Mysigner
|
|
|
963
949
|
|
|
964
950
|
# Helper method for App Store Connect credential setup
|
|
965
951
|
# Returns true if successfully configured, false otherwise
|
|
966
|
-
def setup_app_store_connect_credentials(client,
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
952
|
+
def setup_app_store_connect_credentials(client, _config, org_id)
|
|
953
|
+
say '📱 App Store Connect API Key Setup', :cyan
|
|
954
|
+
say ''
|
|
955
|
+
say "Let's set up your App Store Connect credentials.", :bold
|
|
956
|
+
say ''
|
|
957
|
+
say "Step 1: Create an API Key (if you don't have one)", :bold
|
|
958
|
+
say ''
|
|
959
|
+
say '1. Go to:', :cyan
|
|
960
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
961
|
+
say ''
|
|
962
|
+
say "2. Click the '+' button to create a new key", :cyan
|
|
963
|
+
say ''
|
|
964
|
+
say '3. Select access:', :cyan
|
|
965
|
+
say ' • App Manager (for uploading builds)'
|
|
966
|
+
say ' • Or Admin (full access)'
|
|
967
|
+
say ''
|
|
968
|
+
say '4. Download the .p8 file', :cyan
|
|
969
|
+
say ' ⚠️ Save it securely - you can only download it once!', :yellow
|
|
970
|
+
say ''
|
|
971
|
+
|
|
972
|
+
unless yes_with_default?('Have you created and downloaded your API key?', :green)
|
|
973
|
+
say ''
|
|
974
|
+
say '⏭️ You can set this up later with:', :yellow
|
|
975
|
+
say ' • mysigner doctor', :cyan
|
|
976
|
+
say ' • Or via the web dashboard', :cyan
|
|
977
|
+
return false
|
|
978
|
+
end
|
|
979
|
+
say ''
|
|
980
|
+
|
|
981
|
+
# Prompt for .p8 file path with retry
|
|
982
|
+
max_retries = 3
|
|
983
|
+
attempts = 0
|
|
984
|
+
p8_path = nil
|
|
985
|
+
private_key = nil
|
|
986
|
+
|
|
987
|
+
while attempts < max_retries
|
|
988
|
+
say 'Step 2: Locate your .p8 file', :bold
|
|
989
|
+
say ''
|
|
990
|
+
say '💡 Tip: You can drag & drop the file into terminal to get the path', :cyan
|
|
991
|
+
say ''
|
|
992
|
+
p8_path = ask('Enter the path to your .p8 file:').strip.gsub(/^['"]|['"]$/, '') # Remove quotes
|
|
993
|
+
say ''
|
|
994
|
+
|
|
995
|
+
# Expand ~ to home directory
|
|
996
|
+
p8_path = File.expand_path(p8_path)
|
|
997
|
+
|
|
998
|
+
if File.exist?(p8_path)
|
|
999
|
+
# Read private key
|
|
1000
|
+
begin
|
|
1001
|
+
private_key = File.read(p8_path).strip
|
|
1002
|
+
|
|
1003
|
+
# Validate it looks like a private key
|
|
1004
|
+
unless private_key.include?('BEGIN PRIVATE KEY') || private_key.include?('BEGIN EC PRIVATE KEY')
|
|
1005
|
+
error "This doesn't look like a valid .p8 private key file"
|
|
1006
|
+
attempts += 1
|
|
1007
|
+
next
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
break # Success!
|
|
1011
|
+
rescue StandardError => e
|
|
1012
|
+
error "Failed to read file: #{e.message}"
|
|
1020
1013
|
attempts += 1
|
|
1021
1014
|
next
|
|
1022
1015
|
end
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
rescue => e
|
|
1026
|
-
error "Failed to read file: #{e.message}"
|
|
1016
|
+
else
|
|
1017
|
+
error "File not found: #{p8_path}"
|
|
1027
1018
|
attempts += 1
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
if attempts < max_retries
|
|
1035
|
-
say ""
|
|
1036
|
-
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1037
|
-
say ""
|
|
1019
|
+
|
|
1020
|
+
if attempts < max_retries
|
|
1021
|
+
say ''
|
|
1022
|
+
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1023
|
+
say ''
|
|
1024
|
+
end
|
|
1038
1025
|
end
|
|
1039
1026
|
end
|
|
1040
|
-
end
|
|
1041
|
-
|
|
1042
|
-
unless private_key
|
|
1043
|
-
say ""
|
|
1044
|
-
error "Could not read .p8 file after #{max_retries} attempts"
|
|
1045
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1046
|
-
return false
|
|
1047
|
-
end
|
|
1048
1027
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if filename =~ /AuthKey_([A-Z0-9]+)\.p8/i
|
|
1053
|
-
key_id = $1
|
|
1054
|
-
say "✓ Auto-detected Key ID: #{key_id}", :green
|
|
1055
|
-
say ""
|
|
1056
|
-
end
|
|
1057
|
-
|
|
1058
|
-
# Prompt for Key ID if not auto-detected
|
|
1059
|
-
unless key_id
|
|
1060
|
-
say "Could not auto-detect Key ID from filename.", :yellow
|
|
1061
|
-
say ""
|
|
1062
|
-
say "Find your Key ID in App Store Connect:", :cyan
|
|
1063
|
-
say " https://appstoreconnect.apple.com/access/api", :green
|
|
1064
|
-
say ""
|
|
1065
|
-
key_id = ask("Enter your Key ID (e.g., ABC12345):").strip
|
|
1066
|
-
say ""
|
|
1067
|
-
|
|
1068
|
-
if key_id.empty?
|
|
1069
|
-
error "Key ID cannot be empty"
|
|
1028
|
+
unless private_key
|
|
1029
|
+
say ''
|
|
1030
|
+
error "Could not read .p8 file after #{max_retries} attempts"
|
|
1070
1031
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1071
1032
|
return false
|
|
1072
1033
|
end
|
|
1073
|
-
end
|
|
1074
1034
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1035
|
+
# Auto-extract Key ID from filename (e.g., AuthKey_ABC123.p8 → ABC123)
|
|
1036
|
+
filename = File.basename(p8_path)
|
|
1037
|
+
key_id = nil
|
|
1038
|
+
if filename =~ /AuthKey_([A-Z0-9]+)\.p8/i
|
|
1039
|
+
key_id = ::Regexp.last_match(1)
|
|
1040
|
+
say "✓ Auto-detected Key ID: #{key_id}", :green
|
|
1041
|
+
say ''
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
# Prompt for Key ID if not auto-detected
|
|
1045
|
+
unless key_id
|
|
1046
|
+
say 'Could not auto-detect Key ID from filename.', :yellow
|
|
1047
|
+
say ''
|
|
1048
|
+
say 'Find your Key ID in App Store Connect:', :cyan
|
|
1049
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
1050
|
+
say ''
|
|
1051
|
+
key_id = ask('Enter your Key ID (e.g., ABC12345):').strip
|
|
1052
|
+
say ''
|
|
1053
|
+
|
|
1054
|
+
if key_id.empty?
|
|
1055
|
+
error 'Key ID cannot be empty'
|
|
1056
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1057
|
+
return false
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
# Prompt for Issuer ID
|
|
1062
|
+
say 'Step 3: Find your Issuer ID', :bold
|
|
1063
|
+
say ''
|
|
1064
|
+
say 'Find it in App Store Connect (top right of Keys page):', :cyan
|
|
1065
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
1066
|
+
say ''
|
|
1067
|
+
issuer_id = ask('Enter your Issuer ID (UUID format):').strip
|
|
1068
|
+
say ''
|
|
1069
|
+
|
|
1070
|
+
if issuer_id.empty?
|
|
1071
|
+
error 'Issuer ID cannot be empty'
|
|
1096
1072
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1097
1073
|
return false
|
|
1098
1074
|
end
|
|
1099
|
-
say ""
|
|
1100
|
-
end
|
|
1101
1075
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
credential_name = name_input.empty? ? "CLI Setup" : name_input
|
|
1113
|
-
|
|
1114
|
-
if credential_name.empty?
|
|
1115
|
-
error "Name cannot be empty"
|
|
1116
|
-
say ""
|
|
1076
|
+
# Basic UUID format validation
|
|
1077
|
+
unless issuer_id.match?(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
|
1078
|
+
say "⚠️ Warning: Issuer ID doesn't look like a UUID format", :yellow
|
|
1079
|
+
say ' Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', :yellow
|
|
1080
|
+
say ''
|
|
1081
|
+
unless yes_with_default?('Continue anyway?', :yellow)
|
|
1082
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1083
|
+
return false
|
|
1084
|
+
end
|
|
1085
|
+
say ''
|
|
1117
1086
|
end
|
|
1118
|
-
end
|
|
1119
|
-
say ""
|
|
1120
|
-
say "→ Using name: '#{credential_name}'", :cyan
|
|
1121
|
-
say ""
|
|
1122
|
-
|
|
1123
|
-
# Validate and upload
|
|
1124
|
-
say "Step 5: Validating credentials with Apple...", :bold
|
|
1125
|
-
say ""
|
|
1126
|
-
say "This may take a few seconds...", :yellow
|
|
1127
|
-
say ""
|
|
1128
1087
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
issuer_id: issuer_id,
|
|
1136
|
-
private_key: private_key
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
)
|
|
1088
|
+
# Prompt for credential name
|
|
1089
|
+
say 'Step 4: Name this credential', :bold
|
|
1090
|
+
say ''
|
|
1091
|
+
say "Choose a name to identify this API key (e.g., 'Production Key', 'Team A Key')", :cyan
|
|
1092
|
+
say "Default: 'CLI Setup' - just press Enter to use it", :cyan
|
|
1093
|
+
say ''
|
|
1140
1094
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1095
|
+
credential_name = nil
|
|
1096
|
+
while credential_name.nil? || credential_name.empty?
|
|
1097
|
+
name_input = ask('Credential name:').strip
|
|
1098
|
+
credential_name = name_input.empty? ? 'CLI Setup' : name_input
|
|
1099
|
+
|
|
1100
|
+
if credential_name.empty?
|
|
1101
|
+
error 'Name cannot be empty'
|
|
1102
|
+
say ''
|
|
1103
|
+
end
|
|
1104
|
+
end
|
|
1105
|
+
say ''
|
|
1106
|
+
say "→ Using name: '#{credential_name}'", :cyan
|
|
1107
|
+
say ''
|
|
1108
|
+
|
|
1109
|
+
# Validate and upload
|
|
1110
|
+
say 'Step 5: Validating credentials with Apple...', :bold
|
|
1111
|
+
say ''
|
|
1112
|
+
say 'This may take a few seconds...', :yellow
|
|
1113
|
+
say ''
|
|
1114
|
+
|
|
1115
|
+
begin
|
|
1116
|
+
response = client.post("/api/v1/organizations/#{org_id}/app_store_connect_credentials",
|
|
1117
|
+
body: {
|
|
1118
|
+
app_store_connect_credential: {
|
|
1119
|
+
name: credential_name,
|
|
1120
|
+
key_id: key_id,
|
|
1121
|
+
issuer_id: issuer_id,
|
|
1122
|
+
private_key: private_key
|
|
1123
|
+
}
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
if response[:success]
|
|
1127
|
+
data = response[:data]
|
|
1128
|
+
team_id = data['team_id']
|
|
1129
|
+
|
|
1130
|
+
say '✓ Credentials validated successfully!', :green
|
|
1131
|
+
say ''
|
|
1132
|
+
say 'Details:', :cyan
|
|
1133
|
+
say " • Name: #{credential_name}"
|
|
1134
|
+
say " • Key ID: #{key_id}"
|
|
1135
|
+
say " • Issuer ID: #{issuer_id}"
|
|
1136
|
+
if team_id
|
|
1137
|
+
say " • Team ID: #{team_id}"
|
|
1138
|
+
else
|
|
1139
|
+
say ' • Team ID: (will be extracted after first sync)'
|
|
1140
|
+
end
|
|
1141
|
+
say ' • Status: Active ✓'
|
|
1142
|
+
say ''
|
|
1143
|
+
say '🎉 App Store Connect is now configured!', :green
|
|
1144
|
+
say ''
|
|
1145
|
+
true # Success!
|
|
1153
1146
|
else
|
|
1154
|
-
|
|
1147
|
+
error 'Validation failed'
|
|
1148
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1149
|
+
false
|
|
1155
1150
|
end
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1151
|
+
rescue Mysigner::ClientError => e
|
|
1152
|
+
error_msg = e.message
|
|
1153
|
+
|
|
1154
|
+
# Check for duplicate name error
|
|
1155
|
+
if error_msg.include?('Name has already been taken') || error_msg.include?('validation_failed')
|
|
1156
|
+
error "A credential with the name '#{credential_name}' already exists"
|
|
1157
|
+
say ''
|
|
1158
|
+
say 'Please choose a different name and try again.', :yellow
|
|
1159
|
+
say 'Or manage credentials via the web dashboard:', :cyan
|
|
1160
|
+
say " #{client.api_url}/organizations/#{org_id}", :green
|
|
1161
|
+
else
|
|
1162
|
+
error "Failed to configure credentials: #{error_msg}"
|
|
1163
|
+
say ''
|
|
1164
|
+
say 'Common issues:', :yellow
|
|
1165
|
+
say ' • Invalid Key ID or Issuer ID'
|
|
1166
|
+
say ' • Incorrect .p8 file content'
|
|
1167
|
+
say " • API key doesn't have proper permissions"
|
|
1168
|
+
say ' • API key may be revoked or expired'
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
say ''
|
|
1163
1172
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
# Check for duplicate name error
|
|
1170
|
-
if error_msg.include?("Name has already been taken") || error_msg.include?("validation_failed")
|
|
1171
|
-
error "A credential with the name '#{credential_name}' already exists"
|
|
1172
|
-
say ""
|
|
1173
|
-
say "Please choose a different name and try again.", :yellow
|
|
1174
|
-
say "Or manage credentials via the web dashboard:", :cyan
|
|
1175
|
-
say " #{client.api_url}/organizations/#{org_id}", :green
|
|
1176
|
-
else
|
|
1177
|
-
error "Failed to configure credentials: #{error_msg}"
|
|
1178
|
-
say ""
|
|
1179
|
-
say "Common issues:", :yellow
|
|
1180
|
-
say " • Invalid Key ID or Issuer ID"
|
|
1181
|
-
say " • Incorrect .p8 file content"
|
|
1182
|
-
say " • API key doesn't have proper permissions"
|
|
1183
|
-
say " • API key may be revoked or expired"
|
|
1173
|
+
false
|
|
1174
|
+
rescue StandardError => e
|
|
1175
|
+
error "Unexpected error: #{e.message}"
|
|
1176
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1177
|
+
false
|
|
1184
1178
|
end
|
|
1185
|
-
|
|
1186
|
-
say ""
|
|
1187
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1188
|
-
return false
|
|
1189
|
-
rescue => e
|
|
1190
|
-
error "Unexpected error: #{e.message}"
|
|
1191
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1192
|
-
return false
|
|
1193
|
-
end
|
|
1194
1179
|
end
|
|
1195
1180
|
|
|
1196
1181
|
# Setup Google Play credentials
|
|
1197
|
-
def setup_google_play_credentials(client,
|
|
1198
|
-
say
|
|
1199
|
-
say
|
|
1182
|
+
def setup_google_play_credentials(client, _config, org_id)
|
|
1183
|
+
say '🤖 Google Play Service Account Setup', :cyan
|
|
1184
|
+
say ''
|
|
1200
1185
|
say "Let's set up your Google Play credentials.", :bold
|
|
1201
|
-
say
|
|
1186
|
+
say ''
|
|
1202
1187
|
say "Step 1: Create a Service Account (if you don't have one)", :bold
|
|
1203
|
-
say
|
|
1204
|
-
say
|
|
1205
|
-
say
|
|
1206
|
-
say
|
|
1207
|
-
say
|
|
1208
|
-
say
|
|
1188
|
+
say ''
|
|
1189
|
+
say '1. Go to Google Play Console:', :cyan
|
|
1190
|
+
say ' https://play.google.com/console', :green
|
|
1191
|
+
say ''
|
|
1192
|
+
say '2. Navigate to: Settings → API access', :cyan
|
|
1193
|
+
say ''
|
|
1209
1194
|
say "3. Click 'Create new service account' or 'Link existing service account'", :cyan
|
|
1210
|
-
say
|
|
1211
|
-
say
|
|
1195
|
+
say ''
|
|
1196
|
+
say '4. In Google Cloud Console, create a Service Account with:', :cyan
|
|
1212
1197
|
say " • Name: 'My Signer CLI' (or anything)"
|
|
1213
|
-
say
|
|
1214
|
-
say
|
|
1215
|
-
say
|
|
1216
|
-
say
|
|
1217
|
-
say
|
|
1218
|
-
say
|
|
1219
|
-
say
|
|
1198
|
+
say ' • Role: Editor or Admin'
|
|
1199
|
+
say ''
|
|
1200
|
+
say '5. Create a JSON key for the service account', :cyan
|
|
1201
|
+
say ' • Click on the service account → Keys → Add Key → JSON'
|
|
1202
|
+
say ' • Download the JSON file'
|
|
1203
|
+
say ''
|
|
1204
|
+
say '6. Back in Play Console, grant the service account access:', :cyan
|
|
1220
1205
|
say " • Click 'Done' in the modal"
|
|
1221
|
-
say
|
|
1222
|
-
say
|
|
1223
|
-
say
|
|
1224
|
-
|
|
1225
|
-
unless yes_with_default?(
|
|
1226
|
-
say
|
|
1227
|
-
say
|
|
1228
|
-
say
|
|
1229
|
-
say
|
|
1206
|
+
say ' • Click on the service account'
|
|
1207
|
+
say ' • Set permissions: Release apps, Manage production releases'
|
|
1208
|
+
say ''
|
|
1209
|
+
|
|
1210
|
+
unless yes_with_default?('Have you created and downloaded your service account JSON?', :green)
|
|
1211
|
+
say ''
|
|
1212
|
+
say '⏭️ You can set this up later with:', :yellow
|
|
1213
|
+
say ' • mysigner doctor (will prompt for setup)', :cyan
|
|
1214
|
+
say ' • Or via the web dashboard', :cyan
|
|
1230
1215
|
return false
|
|
1231
1216
|
end
|
|
1232
|
-
say
|
|
1217
|
+
say ''
|
|
1233
1218
|
|
|
1234
1219
|
# Prompt for JSON file path with retry
|
|
1235
1220
|
max_retries = 3
|
|
@@ -1238,12 +1223,12 @@ module Mysigner
|
|
|
1238
1223
|
service_account_json = nil
|
|
1239
1224
|
|
|
1240
1225
|
while attempts < max_retries
|
|
1241
|
-
say
|
|
1242
|
-
say
|
|
1243
|
-
say
|
|
1244
|
-
say
|
|
1245
|
-
json_path = ask(
|
|
1246
|
-
say
|
|
1226
|
+
say 'Step 2: Locate your service account JSON file', :bold
|
|
1227
|
+
say ''
|
|
1228
|
+
say '💡 Tip: You can drag & drop the file into terminal to get the path', :cyan
|
|
1229
|
+
say ''
|
|
1230
|
+
json_path = ask('Enter the path to your service account JSON file:').strip.gsub(/^['"]|['"]$/, '')
|
|
1231
|
+
say ''
|
|
1247
1232
|
|
|
1248
1233
|
# Expand ~ to home directory
|
|
1249
1234
|
json_path = File.expand_path(json_path)
|
|
@@ -1251,7 +1236,7 @@ module Mysigner
|
|
|
1251
1236
|
if File.exist?(json_path)
|
|
1252
1237
|
begin
|
|
1253
1238
|
service_account_json = File.read(json_path).strip
|
|
1254
|
-
|
|
1239
|
+
|
|
1255
1240
|
# Validate it looks like a service account JSON
|
|
1256
1241
|
parsed = JSON.parse(service_account_json)
|
|
1257
1242
|
unless parsed['type'] == 'service_account' && parsed['client_email'] && parsed['private_key']
|
|
@@ -1260,16 +1245,16 @@ module Mysigner
|
|
|
1260
1245
|
attempts += 1
|
|
1261
1246
|
next
|
|
1262
1247
|
end
|
|
1263
|
-
|
|
1264
|
-
say
|
|
1248
|
+
|
|
1249
|
+
say '✓ Valid service account JSON detected', :green
|
|
1265
1250
|
say " Email: #{parsed['client_email']}", :cyan
|
|
1266
|
-
say
|
|
1251
|
+
say ''
|
|
1267
1252
|
break # Success!
|
|
1268
1253
|
rescue JSON::ParserError => e
|
|
1269
1254
|
error "Invalid JSON file: #{e.message}"
|
|
1270
1255
|
attempts += 1
|
|
1271
1256
|
next
|
|
1272
|
-
rescue => e
|
|
1257
|
+
rescue StandardError => e
|
|
1273
1258
|
error "Failed to read file: #{e.message}"
|
|
1274
1259
|
attempts += 1
|
|
1275
1260
|
next
|
|
@@ -1277,100 +1262,98 @@ module Mysigner
|
|
|
1277
1262
|
else
|
|
1278
1263
|
error "File not found: #{json_path}"
|
|
1279
1264
|
attempts += 1
|
|
1280
|
-
|
|
1265
|
+
|
|
1281
1266
|
if attempts < max_retries
|
|
1282
|
-
say
|
|
1267
|
+
say ''
|
|
1283
1268
|
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1284
|
-
say
|
|
1269
|
+
say ''
|
|
1285
1270
|
end
|
|
1286
1271
|
end
|
|
1287
1272
|
end
|
|
1288
1273
|
|
|
1289
1274
|
unless service_account_json
|
|
1290
|
-
say
|
|
1275
|
+
say ''
|
|
1291
1276
|
error "Could not read JSON file after #{max_retries} attempts"
|
|
1292
1277
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1293
1278
|
return false
|
|
1294
1279
|
end
|
|
1295
1280
|
|
|
1296
1281
|
# Prompt for credential name
|
|
1297
|
-
say
|
|
1298
|
-
say
|
|
1282
|
+
say 'Step 3: Name this credential', :bold
|
|
1283
|
+
say ''
|
|
1299
1284
|
say "Choose a name to identify this service account (e.g., 'Production', 'CI/CD')", :cyan
|
|
1300
1285
|
say "Default: 'CLI Setup' - just press Enter to use it", :cyan
|
|
1301
|
-
say
|
|
1302
|
-
|
|
1286
|
+
say ''
|
|
1287
|
+
|
|
1303
1288
|
credential_name = nil
|
|
1304
1289
|
while credential_name.nil? || credential_name.empty?
|
|
1305
|
-
name_input = ask(
|
|
1306
|
-
credential_name = name_input.empty? ?
|
|
1307
|
-
|
|
1290
|
+
name_input = ask('Credential name:').strip
|
|
1291
|
+
credential_name = name_input.empty? ? 'CLI Setup' : name_input
|
|
1292
|
+
|
|
1308
1293
|
if credential_name.empty?
|
|
1309
|
-
error
|
|
1310
|
-
say
|
|
1294
|
+
error 'Name cannot be empty'
|
|
1295
|
+
say ''
|
|
1311
1296
|
end
|
|
1312
1297
|
end
|
|
1313
|
-
say
|
|
1298
|
+
say ''
|
|
1314
1299
|
say "→ Using name: '#{credential_name}'", :cyan
|
|
1315
|
-
say
|
|
1316
|
-
|
|
1300
|
+
say ''
|
|
1301
|
+
|
|
1317
1302
|
# Validate and upload
|
|
1318
|
-
say
|
|
1319
|
-
say
|
|
1303
|
+
say 'Step 4: Saving credentials...', :bold
|
|
1304
|
+
say ''
|
|
1320
1305
|
|
|
1321
1306
|
begin
|
|
1322
|
-
response = client.post("/api/v1/organizations/#{org_id}/google_play_credentials",
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
say
|
|
1333
|
-
say
|
|
1334
|
-
say "Details:", :cyan
|
|
1307
|
+
response = client.post("/api/v1/organizations/#{org_id}/google_play_credentials",
|
|
1308
|
+
body: {
|
|
1309
|
+
google_play_credential: {
|
|
1310
|
+
name: credential_name,
|
|
1311
|
+
service_account_json: service_account_json,
|
|
1312
|
+
active: true
|
|
1313
|
+
}
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
say '✓ Google Play credentials saved!', :green
|
|
1317
|
+
say ''
|
|
1318
|
+
say 'Details:', :cyan
|
|
1335
1319
|
say " • Name: #{credential_name}"
|
|
1336
|
-
say
|
|
1337
|
-
say
|
|
1338
|
-
|
|
1320
|
+
say ' • Status: Active ✓'
|
|
1321
|
+
say ''
|
|
1322
|
+
|
|
1339
1323
|
# Test the connection
|
|
1340
|
-
say
|
|
1341
|
-
|
|
1324
|
+
say 'Testing connection to Google Play...', :yellow
|
|
1325
|
+
|
|
1342
1326
|
begin
|
|
1343
1327
|
cred_id = response[:data]['id']
|
|
1344
1328
|
client.post("/api/v1/organizations/#{org_id}/google_play_credentials/#{cred_id}/test")
|
|
1345
|
-
say
|
|
1346
|
-
rescue => e
|
|
1329
|
+
say '✓ Successfully connected to Google Play API!', :green
|
|
1330
|
+
rescue StandardError => e
|
|
1347
1331
|
say "⚠️ Connection test failed: #{e.message}", :yellow
|
|
1348
|
-
say
|
|
1332
|
+
say ' The credentials are saved but may need verification', :yellow
|
|
1349
1333
|
end
|
|
1350
|
-
|
|
1351
|
-
say ""
|
|
1352
|
-
say "🎉 Google Play is now configured!", :green
|
|
1353
|
-
say ""
|
|
1354
|
-
return true
|
|
1355
1334
|
|
|
1335
|
+
say ''
|
|
1336
|
+
say '🎉 Google Play is now configured!', :green
|
|
1337
|
+
say ''
|
|
1338
|
+
true
|
|
1356
1339
|
rescue Mysigner::ClientError => e
|
|
1357
1340
|
error_msg = e.message
|
|
1358
|
-
|
|
1359
|
-
if error_msg.include?(
|
|
1341
|
+
|
|
1342
|
+
if error_msg.include?('already been taken') || error_msg.include?('validation')
|
|
1360
1343
|
error "A credential with the name '#{credential_name}' already exists"
|
|
1361
|
-
say
|
|
1362
|
-
say
|
|
1344
|
+
say ''
|
|
1345
|
+
say 'Please choose a different name and try again.', :yellow
|
|
1363
1346
|
else
|
|
1364
1347
|
error "Failed to configure credentials: #{error_msg}"
|
|
1365
1348
|
end
|
|
1366
|
-
|
|
1367
|
-
say
|
|
1349
|
+
|
|
1350
|
+
say ''
|
|
1368
1351
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1369
|
-
|
|
1370
|
-
rescue => e
|
|
1352
|
+
false
|
|
1353
|
+
rescue StandardError => e
|
|
1371
1354
|
error "Unexpected error: #{e.message}"
|
|
1372
1355
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1373
|
-
|
|
1356
|
+
false
|
|
1374
1357
|
end
|
|
1375
1358
|
end
|
|
1376
1359
|
end
|