mysigner 0.3.1 → 0.3.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/CHANGELOG.md +24 -0
- data/Gemfile.lock +2 -2
- data/README.md +16 -0
- data/exe/mysigner +36 -1
- data/lib/mysigner/build/android_executor.rb +101 -29
- data/lib/mysigner/build/android_parser.rb +2 -2
- data/lib/mysigner/build/detector.rb +122 -53
- data/lib/mysigner/cli/auth_commands.rb +132 -9
- data/lib/mysigner/cli/build_commands.rb +24 -9
- data/lib/mysigner/cli/concerns/error_handlers.rb +9 -12
- data/lib/mysigner/cli/concerns/helpers.rb +37 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +67 -29
- data/lib/mysigner/cli/resource_commands.rb +72 -27
- data/lib/mysigner/cli/validate_commands.rb +16 -1
- data/lib/mysigner/config.rb +14 -2
- data/lib/mysigner/signing/gradle_signing_injector.rb +14 -0
- data/lib/mysigner/version.rb +1 -1
- data/mysigner.gemspec +13 -8
- metadata +11 -10
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'stringio' # discover_local_*_silently use StringIO as a non-tty stderr proxy
|
|
4
|
+
|
|
3
5
|
module Mysigner
|
|
4
6
|
class CLI < Thor
|
|
5
7
|
module AuthCommands
|
|
@@ -28,6 +30,10 @@ module Mysigner
|
|
|
28
30
|
|
|
29
31
|
New user? Run 'mysigner onboard' for step-by-step guidance.
|
|
30
32
|
|
|
33
|
+
No My Signer account? You don't need one to sign and ship — run
|
|
34
|
+
'mysigner --local-only onboard' to use your own Apple/Google
|
|
35
|
+
credentials, kept on this machine.
|
|
36
|
+
|
|
31
37
|
Your credentials will be stored securely in ~/.mysigner/config.yml
|
|
32
38
|
|
|
33
39
|
Note: API tokens are organization-specific. This token will only
|
|
@@ -232,6 +238,26 @@ module Mysigner
|
|
|
232
238
|
|
|
233
239
|
# Check if already configured
|
|
234
240
|
config = Config.new
|
|
241
|
+
|
|
242
|
+
# Fresh user, interactive: offer the two modes up front so a newcomer
|
|
243
|
+
# doesn't assume a My Signer website account is mandatory.
|
|
244
|
+
if $stdin.tty? && !(config.exists? && config.current_organization_id)
|
|
245
|
+
say 'How do you want to use My Signer?', :cyan
|
|
246
|
+
say ''
|
|
247
|
+
say ' 1. With a free My Signer account', :white
|
|
248
|
+
say ' (keys stored on the server; sync across machines & team)'
|
|
249
|
+
say ' 2. Local-only — no account', :white
|
|
250
|
+
say ' (your Apple/Google keys stay on THIS machine; nothing sent to a server)'
|
|
251
|
+
say ''
|
|
252
|
+
say 'Not sure? Choose 2 (local-only).', :yellow
|
|
253
|
+
say ''
|
|
254
|
+
if ask('Select (1-2):', limited_to: %w[1 2]) == '2'
|
|
255
|
+
emit_local_only_banner
|
|
256
|
+
return onboard_local_only
|
|
257
|
+
end
|
|
258
|
+
say ''
|
|
259
|
+
end
|
|
260
|
+
|
|
235
261
|
if config.exists? && config.api_token && config.current_organization_id
|
|
236
262
|
say "✓ You're already logged in!", :green
|
|
237
263
|
say ''
|
|
@@ -540,6 +566,19 @@ module Mysigner
|
|
|
540
566
|
end
|
|
541
567
|
end
|
|
542
568
|
|
|
569
|
+
# Android: offer Google Play setup too. Previously onboard only
|
|
570
|
+
# ever configured App Store Connect, leaving Android users without
|
|
571
|
+
# a guided path even though setup_google_play_credentials existed.
|
|
572
|
+
# Only prompt interactively — in CI/non-tty contexts onboarding is
|
|
573
|
+
# driven by env vars/flags, not the wizard.
|
|
574
|
+
gp_configured = false
|
|
575
|
+
if $stdin.tty? && yes_with_default?('Set up Google Play (Android) credentials now?', :cyan)
|
|
576
|
+
say ''
|
|
577
|
+
gp_configured = setup_google_play_credentials(client, config, org_id)
|
|
578
|
+
elsif $stdin.tty?
|
|
579
|
+
say '⏭️ Skipped Google Play setup (run `mysigner onboard` again anytime)', :yellow
|
|
580
|
+
end
|
|
581
|
+
|
|
543
582
|
say ''
|
|
544
583
|
say '=' * 80, :green
|
|
545
584
|
say '🎉 Setup Complete!', :green
|
|
@@ -564,6 +603,13 @@ module Mysigner
|
|
|
564
603
|
say " Run 'mysigner doctor' to set it up", :yellow
|
|
565
604
|
end
|
|
566
605
|
|
|
606
|
+
if gp_configured
|
|
607
|
+
say '✓ Google Play: Configured', :green
|
|
608
|
+
else
|
|
609
|
+
say '⚠️ Google Play: Not configured', :yellow
|
|
610
|
+
say " Run 'mysigner onboard' to set it up", :yellow
|
|
611
|
+
end
|
|
612
|
+
|
|
567
613
|
say ''
|
|
568
614
|
say '🔒 Security Note:', :yellow
|
|
569
615
|
say " Your token is organization-specific. Use 'mysigner switch'", :yellow
|
|
@@ -663,10 +709,30 @@ module Mysigner
|
|
|
663
709
|
config.load
|
|
664
710
|
purge_server_credentials(config)
|
|
665
711
|
purge_local_credentials
|
|
666
|
-
rescue Mysigner::
|
|
667
|
-
|
|
712
|
+
rescue Mysigner::ConfigError => e
|
|
713
|
+
# The stored token is UNREADABLE (can't be decrypted), so it can
|
|
714
|
+
# never be revoked on the server from here and retrying --purge
|
|
715
|
+
# would fail identically. Clearing local is the only way out, so
|
|
716
|
+
# we do that (the token is useless anyway) and fall through to
|
|
717
|
+
# config.clear below — the user ends up logged out, and we tell
|
|
718
|
+
# them to revoke the old token in the dashboard if they care.
|
|
719
|
+
reason = e.message.sub(/\AFailed to decrypt token:\s*/, '').strip
|
|
720
|
+
detail = reason.empty? ? '' : " (#{reason})"
|
|
721
|
+
say "⚠️ Your stored token is unreadable#{detail}, " \
|
|
722
|
+
'so it could not be revoked on the server.', :yellow
|
|
723
|
+
say ' Logging you out locally anyway. To revoke the old token, do it in', :yellow
|
|
724
|
+
say ' the My Signer dashboard (it cannot be read from this machine).', :yellow
|
|
668
725
|
say ''
|
|
669
|
-
|
|
726
|
+
begin
|
|
727
|
+
purge_local_credentials
|
|
728
|
+
rescue StandardError
|
|
729
|
+
# best-effort; a corrupt Keychain entry must not block the clear
|
|
730
|
+
end
|
|
731
|
+
# fall through to config.clear
|
|
732
|
+
rescue Mysigner::ClientError => e
|
|
733
|
+
error "Couldn't reach the server to revoke your credentials: #{e.message}"
|
|
734
|
+
say ''
|
|
735
|
+
say 'Your local config was NOT cleared so you can retry. Options:', :yellow
|
|
670
736
|
say " • Re-run 'mysigner logout --purge' once the server is reachable", :yellow
|
|
671
737
|
say " • Run 'mysigner logout --no-purge' to log out locally only", :yellow
|
|
672
738
|
exit 1
|
|
@@ -1027,9 +1093,12 @@ module Mysigner
|
|
|
1027
1093
|
def config(action = nil, *args)
|
|
1028
1094
|
return config_set(*args) if action == 'set'
|
|
1029
1095
|
|
|
1030
|
-
|
|
1096
|
+
# `show` is the documented alias for the bare display (README +
|
|
1097
|
+
# `config [ACTION]` help). Treat nil/`show` as display; anything
|
|
1098
|
+
# else is a genuine typo.
|
|
1099
|
+
if action && !%w[show].include?(action)
|
|
1031
1100
|
error "Unknown config action: #{action}"
|
|
1032
|
-
say 'Did you mean: `mysigner config set <key> <value>`?', :yellow
|
|
1101
|
+
say 'Did you mean: `mysigner config show` or `mysigner config set <key> <value>`?', :yellow
|
|
1033
1102
|
exit 1
|
|
1034
1103
|
end
|
|
1035
1104
|
|
|
@@ -1679,23 +1748,35 @@ module Mysigner
|
|
|
1679
1748
|
say ''
|
|
1680
1749
|
stored_play = collect_local_google_play_credential
|
|
1681
1750
|
end
|
|
1751
|
+
say ''
|
|
1752
|
+
|
|
1753
|
+
# Android signing keystore (the actual signing key). Stored locally
|
|
1754
|
+
# in the exact envelope CredentialResolver#resolve_android_keystore
|
|
1755
|
+
# expects, so `ship --platform android --local-only` can sign offline.
|
|
1756
|
+
stored_keystore = []
|
|
1757
|
+
if $stdin.tty? && yes_with_default?('Set up an Android signing keystore now?', :cyan)
|
|
1758
|
+
say ''
|
|
1759
|
+
stored_keystore = collect_local_keystore_credential
|
|
1760
|
+
end
|
|
1682
1761
|
|
|
1683
1762
|
say ''
|
|
1684
1763
|
say '=' * 80, :green
|
|
1685
1764
|
say '✓ Local-only setup complete.', :green
|
|
1686
1765
|
say '=' * 80, :green
|
|
1687
1766
|
say ''
|
|
1688
|
-
if stored_asc.empty? && stored_play.empty?
|
|
1767
|
+
if stored_asc.empty? && stored_play.empty? && stored_keystore.empty?
|
|
1689
1768
|
say 'No credentials were stored.', :yellow
|
|
1690
1769
|
say "Re-run 'mysigner --local-only onboard' when you're ready.", :yellow
|
|
1691
1770
|
else
|
|
1692
1771
|
say 'Stored credentials:', :cyan
|
|
1693
|
-
stored_asc.each
|
|
1694
|
-
stored_play.each
|
|
1772
|
+
stored_asc.each { |id| say " • ASC key: #{id}" }
|
|
1773
|
+
stored_play.each { |id| say " • Google Play SA: #{id}" }
|
|
1774
|
+
stored_keystore.each { |id| say " • Android keystore: #{id}" }
|
|
1695
1775
|
say ''
|
|
1696
1776
|
say 'To ship:', :bold
|
|
1697
1777
|
say ' mysigner --local-only ship appstore' unless stored_asc.empty?
|
|
1698
1778
|
say ' mysigner --local-only ship play' unless stored_play.empty?
|
|
1779
|
+
say ' mysigner --local-only ship internal --platform android' unless stored_keystore.empty?
|
|
1699
1780
|
end
|
|
1700
1781
|
say ''
|
|
1701
1782
|
end
|
|
@@ -1721,8 +1802,14 @@ module Mysigner
|
|
|
1721
1802
|
key_id = ask('Enter your Key ID (e.g., ABC12345):').to_s.strip if key_id.nil? || key_id.empty?
|
|
1722
1803
|
raise_local_onboard_error!('Key ID cannot be empty') if key_id.empty?
|
|
1723
1804
|
|
|
1805
|
+
say 'Find your Issuer ID at https://appstoreconnect.apple.com/access/api', :cyan
|
|
1806
|
+
say '(the UUID shown at the top of the Keys page).', :cyan
|
|
1724
1807
|
issuer_id = ask('Enter your Issuer ID (UUID):').to_s.strip
|
|
1725
|
-
|
|
1808
|
+
if issuer_id.empty?
|
|
1809
|
+
raise_local_onboard_error!(
|
|
1810
|
+
"Issuer ID cannot be empty (it's the UUID at appstoreconnect.apple.com/access/api)"
|
|
1811
|
+
)
|
|
1812
|
+
end
|
|
1726
1813
|
|
|
1727
1814
|
# Storage shape matches mysigner-42's Option A: id == key_id,
|
|
1728
1815
|
# secret is a JSON envelope so AscJwtMinter can reconstruct
|
|
@@ -1753,6 +1840,42 @@ module Mysigner
|
|
|
1753
1840
|
[client_email]
|
|
1754
1841
|
end
|
|
1755
1842
|
|
|
1843
|
+
# Returns Array<String> of ids actually stored (empty on skip).
|
|
1844
|
+
# Persists the keystore bytes (base64) + passwords + alias under
|
|
1845
|
+
# LocalCredentials(kind: :android_keystore) in the exact envelope
|
|
1846
|
+
# CredentialResolver#resolve_android_keystore reads back.
|
|
1847
|
+
def collect_local_keystore_credential
|
|
1848
|
+
require 'base64'
|
|
1849
|
+
say '🔑 Android keystore (local-only)', :cyan
|
|
1850
|
+
say ''
|
|
1851
|
+
|
|
1852
|
+
ks_path = ask('Path to your keystore (.jks/.keystore):').to_s.strip.gsub(/^['"]|['"]$/, '')
|
|
1853
|
+
ks_path = File.expand_path(ks_path)
|
|
1854
|
+
raise_local_onboard_error!("Keystore file not found: #{ks_path}") unless File.exist?(ks_path)
|
|
1855
|
+
|
|
1856
|
+
# echo: false — keep secrets out of terminal scrollback, matching
|
|
1857
|
+
# every other secret prompt in this file.
|
|
1858
|
+
store_password = ask('Keystore password:', echo: false).to_s
|
|
1859
|
+
raise_local_onboard_error!('Keystore password cannot be empty') if store_password.empty?
|
|
1860
|
+
|
|
1861
|
+
key_alias = ask('Key alias:').to_s.strip
|
|
1862
|
+
raise_local_onboard_error!('Key alias cannot be empty') if key_alias.empty?
|
|
1863
|
+
|
|
1864
|
+
key_password = ask('Key password (press Enter to reuse the keystore password):', echo: false).to_s
|
|
1865
|
+
key_password = store_password if key_password.empty?
|
|
1866
|
+
|
|
1867
|
+
envelope = JSON.generate(
|
|
1868
|
+
'keystore_b64' => Base64.strict_encode64(File.binread(ks_path)),
|
|
1869
|
+
'keystore_password' => store_password,
|
|
1870
|
+
'key_alias' => key_alias,
|
|
1871
|
+
'key_password' => key_password
|
|
1872
|
+
)
|
|
1873
|
+
Mysigner::LocalCredentials.store(kind: :android_keystore, id: key_alias, secret: envelope)
|
|
1874
|
+
|
|
1875
|
+
say "✓ Android keystore stored locally (alias: #{key_alias}).", :green
|
|
1876
|
+
[key_alias]
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1756
1879
|
# Verifies the file looks like an EC private key in the form
|
|
1757
1880
|
# AscJwtMinter requires. Fails loud — any malformed input raises
|
|
1758
1881
|
# before we touch the Keychain.
|
|
@@ -20,15 +20,18 @@ module Mysigner
|
|
|
20
20
|
long_desc <<~DESC
|
|
21
21
|
Build your project, sign it, and upload in one go.
|
|
22
22
|
|
|
23
|
-
iOS TARGETS
|
|
24
|
-
testflight :
|
|
25
|
-
appstore :
|
|
23
|
+
iOS TARGETS (requires macOS + Xcode)
|
|
24
|
+
testflight : Beta testing via TestFlight
|
|
25
|
+
appstore : Submit for public App Store release
|
|
26
26
|
|
|
27
|
-
ANDROID TARGETS
|
|
28
|
-
internal :
|
|
29
|
-
alpha :
|
|
30
|
-
beta :
|
|
31
|
-
production :
|
|
27
|
+
ANDROID TARGETS (works on macOS, Linux, and Windows)
|
|
28
|
+
internal : Fastest — up to 100 testers you list. NOT public.
|
|
29
|
+
alpha : Closed testing — an invite-only tester group.
|
|
30
|
+
beta : Open testing — anyone with your opt-in link.
|
|
31
|
+
production : PUBLIC — goes LIVE to everyone on the Google Play Store.
|
|
32
|
+
|
|
33
|
+
An AAB (Android App Bundle, .aab) is what Google Play requires — the
|
|
34
|
+
CLI builds and signs it for you (you don't upload an APK).
|
|
32
35
|
|
|
33
36
|
PLATFORM OPTIONS
|
|
34
37
|
--platform ios Force iOS build (auto-detected by default)
|
|
@@ -43,6 +46,14 @@ module Mysigner
|
|
|
43
46
|
--release-notes TEXT Release notes for Play Store
|
|
44
47
|
--package-name PKG Override the detected package name
|
|
45
48
|
|
|
49
|
+
LOCAL-ONLY (no My Signer account — use your OWN credentials)
|
|
50
|
+
Run `mysigner --local-only onboard` once for a guided setup, or pass:
|
|
51
|
+
iOS: --asc-key-path (the AuthKey_XXXX.p8 from
|
|
52
|
+
appstoreconnect.apple.com/access/api), --asc-key-id (the
|
|
53
|
+
XXXX in that filename), --asc-issuer-id (the UUID on that page)
|
|
54
|
+
Android: --keystore-path / --keystore-password / --key-alias and
|
|
55
|
+
--play-credentials (a Google Play service-account .json)
|
|
56
|
+
|
|
46
57
|
WORKFLOW
|
|
47
58
|
For iOS TestFlight:
|
|
48
59
|
mysigner ship testflight # Build → Upload → Done!
|
|
@@ -140,7 +151,8 @@ module Mysigner
|
|
|
140
151
|
return
|
|
141
152
|
end
|
|
142
153
|
|
|
143
|
-
# iOS flow continues below
|
|
154
|
+
# iOS flow continues below — requires macOS + Xcode.
|
|
155
|
+
require_macos!("ship #{target}")
|
|
144
156
|
|
|
145
157
|
is_appstore = (target == 'appstore')
|
|
146
158
|
|
|
@@ -1817,6 +1829,7 @@ module Mysigner
|
|
|
1817
1829
|
method_option :skip_extensions, type: :boolean, default: false,
|
|
1818
1830
|
desc: 'Skip extension targets (useful when extensions are not configured)'
|
|
1819
1831
|
def build
|
|
1832
|
+
require_macos!('build')
|
|
1820
1833
|
config = load_config
|
|
1821
1834
|
client = create_client(config)
|
|
1822
1835
|
|
|
@@ -2031,6 +2044,7 @@ module Mysigner
|
|
|
2031
2044
|
desc: 'Export method (appstore, adhoc, enterprise, development)'
|
|
2032
2045
|
method_option :output, type: :string, desc: 'Output directory for .ipa file'
|
|
2033
2046
|
def export(archive_path)
|
|
2047
|
+
require_macos!('export')
|
|
2034
2048
|
load_config
|
|
2035
2049
|
|
|
2036
2050
|
begin
|
|
@@ -2085,6 +2099,7 @@ module Mysigner
|
|
|
2085
2099
|
"Upload existing .ipa to TestFlight (advanced - most users should use 'ship')"
|
|
2086
2100
|
method_option :wait, type: :boolean, default: false, desc: 'Wait for processing to complete'
|
|
2087
2101
|
def upload(target, ipa_path)
|
|
2102
|
+
require_macos!('upload testflight')
|
|
2088
2103
|
unless target == 'testflight'
|
|
2089
2104
|
error "Only 'testflight' target is supported currently"
|
|
2090
2105
|
say 'Usage: mysigner upload testflight IPA_PATH', :yellow
|
|
@@ -84,19 +84,16 @@ module Mysigner
|
|
|
84
84
|
say ''
|
|
85
85
|
say 'To fix this:', :cyan
|
|
86
86
|
say ''
|
|
87
|
-
if api_url.include?('localhost')
|
|
88
|
-
say ' For
|
|
89
|
-
say ' 1.
|
|
90
|
-
say '
|
|
91
|
-
say ' bin/rails server'
|
|
92
|
-
say ''
|
|
93
|
-
say " 2. Verify it's accessible:"
|
|
94
|
-
say " curl #{api_url}/up"
|
|
87
|
+
if api_url.include?('localhost') && ENV['MYSIGNER_DEV']
|
|
88
|
+
say ' For My Signer backend development:', :bold
|
|
89
|
+
say ' 1. Start the Rails server: cd path/to/my-signer && bin/rails server'
|
|
90
|
+
say " 2. Verify it's accessible: curl #{api_url}/up"
|
|
95
91
|
else
|
|
96
|
-
say '
|
|
97
|
-
say ' 1. Check
|
|
98
|
-
say ' 2. Verify the
|
|
99
|
-
say
|
|
92
|
+
say ' To fix:', :bold
|
|
93
|
+
say ' 1. Check your internet connection'
|
|
94
|
+
say ' 2. Verify the API URL is correct: mysigner config show'
|
|
95
|
+
say " 3. Don't need a My Signer account? Run any command with --local-only"
|
|
96
|
+
say ' to sign with your own Apple/Google credentials.'
|
|
100
97
|
end
|
|
101
98
|
say ''
|
|
102
99
|
say ' Or set a custom API URL:', :bold
|
|
@@ -102,11 +102,27 @@ module Mysigner
|
|
|
102
102
|
say ' (auto-discovers ASC .p8 from ~/.appstoreconnect/private_keys/,', :yellow
|
|
103
103
|
say ' Google Play SA-JSON from GOOGLE_APPLICATION_CREDENTIALS / eas.json,', :yellow
|
|
104
104
|
say ' keystore from key.properties / eas.json — or set them via flags / env.)', :yellow
|
|
105
|
+
say ' Note: build / ship / sign work fully local; account commands', :yellow
|
|
106
|
+
say ' (orgs / switch / sync) still need a My Signer login.', :yellow
|
|
105
107
|
say ' See "Local-only mode" section in README.', :yellow
|
|
106
108
|
exit 1
|
|
107
109
|
end
|
|
108
110
|
|
|
109
111
|
config.load
|
|
112
|
+
|
|
113
|
+
# Surface an unreadable stored token as a clean re-login prompt here,
|
|
114
|
+
# at the auth gate, instead of letting the decrypt error explode later
|
|
115
|
+
# inside create_client / Config#display. (api_token decrypts lazily.)
|
|
116
|
+
begin
|
|
117
|
+
config.api_token
|
|
118
|
+
rescue Mysigner::ConfigError
|
|
119
|
+
error 'Your saved login is unreadable (encryption key changed or ' \
|
|
120
|
+
'config copied between machines).'
|
|
121
|
+
say "Run 'mysigner logout' then 'mysigner login' to re-authenticate.", :yellow
|
|
122
|
+
say 'Or run with --local-only to skip MySigner entirely.', :yellow
|
|
123
|
+
exit 1
|
|
124
|
+
end
|
|
125
|
+
|
|
110
126
|
config
|
|
111
127
|
end
|
|
112
128
|
|
|
@@ -150,6 +166,27 @@ module Mysigner
|
|
|
150
166
|
say "✗ Error: #{message}", :red
|
|
151
167
|
end
|
|
152
168
|
|
|
169
|
+
# True on macOS. iOS building/signing (Xcode, xcodebuild, the keychain)
|
|
170
|
+
# only works there; iOS-only commands call require_macos! to fail with a
|
|
171
|
+
# clear message instead of a raw "xcodebuild: not found" backtrace.
|
|
172
|
+
def macos?
|
|
173
|
+
!(RbConfig::CONFIG['host_os'] =~ /darwin/i).nil?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Guard for iOS-only commands. On non-macOS, explain plainly and point
|
|
177
|
+
# the user at the Android path that DOES work cross-platform, then exit.
|
|
178
|
+
def require_macos!(command_label = 'This command')
|
|
179
|
+
return if macos?
|
|
180
|
+
|
|
181
|
+
error "#{command_label} requires macOS with Xcode."
|
|
182
|
+
say ''
|
|
183
|
+
say 'iOS building, signing, and uploading only work on a Mac (they use Xcode).', :yellow
|
|
184
|
+
say 'On Linux or Windows you can still build and ship Android:', :yellow
|
|
185
|
+
say ' mysigner ship internal --platform android', :cyan
|
|
186
|
+
say ' mysigner android build', :cyan
|
|
187
|
+
exit 1
|
|
188
|
+
end
|
|
189
|
+
|
|
153
190
|
# Local-only mode is active when any of, in precedence order:
|
|
154
191
|
# 1. --local-only / --no-local-only flag on this invocation
|
|
155
192
|
# 2. MYSIGNER_LOCAL_ONLY env var
|
|
@@ -18,8 +18,6 @@ module Mysigner
|
|
|
18
18
|
|
|
19
19
|
# Determine which platforms to check
|
|
20
20
|
platform_filter = options[:platform]&.downcase
|
|
21
|
-
check_ios = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'ios'
|
|
22
|
-
check_android = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'android'
|
|
23
21
|
|
|
24
22
|
if platform_filter && !%w[ios android all].include?(platform_filter)
|
|
25
23
|
error "Invalid platform: #{platform_filter}"
|
|
@@ -27,6 +25,18 @@ module Mysigner
|
|
|
27
25
|
exit 1
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
want_ios = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'ios'
|
|
29
|
+
# iOS checks only mean something on macOS. On Linux/Windows, skip them
|
|
30
|
+
# with one info line instead of a wall of red "Xcode not found" issues —
|
|
31
|
+
# unless the user EXPLICITLY asked for --platform ios.
|
|
32
|
+
check_ios = want_ios && (macos? || platform_filter == 'ios')
|
|
33
|
+
check_android = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'android'
|
|
34
|
+
|
|
35
|
+
if want_ios && !check_ios
|
|
36
|
+
say 'ℹ️ iOS checks skipped — iOS building requires macOS + Xcode.', :cyan
|
|
37
|
+
say ''
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Check 1: Xcode (iOS only)
|
|
31
41
|
if check_ios
|
|
32
42
|
say 'Checking Xcode...', :yellow
|
|
@@ -110,6 +120,12 @@ module Mysigner
|
|
|
110
120
|
say ' ✗ Token is invalid or expired', :red
|
|
111
121
|
issues << "Token authentication failed - run 'mysigner onboard' to re-authenticate"
|
|
112
122
|
client = nil
|
|
123
|
+
rescue Mysigner::ConfigError
|
|
124
|
+
# An undecryptable stored token is a LOCAL credential problem,
|
|
125
|
+
# not an API/network failure — say so and point at re-login.
|
|
126
|
+
say ' ✗ Saved credentials unreadable (encryption key changed or config corrupt)', :red
|
|
127
|
+
issues << "Stored login unreadable - run 'mysigner logout' then 'mysigner login'"
|
|
128
|
+
client = nil
|
|
113
129
|
rescue Mysigner::ConnectionError => e
|
|
114
130
|
say " ✗ Cannot connect to API: #{e.message}", :red
|
|
115
131
|
issues << 'API connection failed - check your network or API URL'
|
|
@@ -253,18 +269,24 @@ module Mysigner
|
|
|
253
269
|
end
|
|
254
270
|
say ''
|
|
255
271
|
|
|
256
|
-
# Check 8: Project Detection (
|
|
272
|
+
# Check 8: Project Detection (read-only — must NEVER trigger an
|
|
273
|
+
# expo prebuild from a diagnostic, hence allow_prebuild: false).
|
|
257
274
|
say 'Checking current directory...', :yellow
|
|
258
275
|
project_info = nil
|
|
259
276
|
begin
|
|
260
|
-
project_info = Build::Detector.detect
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
277
|
+
project_info = Build::Detector.detect(allow_prebuild: false)
|
|
278
|
+
if project_info[:needs_prebuild]
|
|
279
|
+
say ' ℹ️ Expo managed project detected (no native ios/ folder yet)', :cyan
|
|
280
|
+
say ' Generate it with: mysigner ship (or `npx expo prebuild`)', :cyan
|
|
281
|
+
else
|
|
282
|
+
framework = case project_info[:framework]
|
|
283
|
+
when :capacitor then 'Capacitor/Ionic'
|
|
284
|
+
when :react_native then 'React Native'
|
|
285
|
+
when :flutter then 'Flutter'
|
|
286
|
+
else 'Native iOS'
|
|
287
|
+
end
|
|
288
|
+
say " ✓ Found #{framework} project: #{File.basename(project_info[:path])}", :green
|
|
289
|
+
end
|
|
268
290
|
rescue StandardError
|
|
269
291
|
say ' ℹ️ No project detected in current directory', :cyan
|
|
270
292
|
end
|
|
@@ -516,6 +538,12 @@ module Mysigner
|
|
|
516
538
|
say ' ⚠️ JAVA_HOME not set', :yellow
|
|
517
539
|
end
|
|
518
540
|
end
|
|
541
|
+
elsif platform_filter == 'android'
|
|
542
|
+
# When the user explicitly asks to check Android, a missing JDK
|
|
543
|
+
# is a hard blocker, not an FYI — otherwise doctor green-lights an
|
|
544
|
+
# environment that cannot build.
|
|
545
|
+
say ' ✗ Java not found (required for Android)', :red
|
|
546
|
+
issues << 'Java (JDK) not found — required to build Android. Install JDK 17+ and set JAVA_HOME.'
|
|
519
547
|
else
|
|
520
548
|
say ' ℹ️ Java not found (required for Android)', :cyan
|
|
521
549
|
end
|
|
@@ -525,6 +553,9 @@ module Mysigner
|
|
|
525
553
|
if android_home && Dir.exist?(android_home)
|
|
526
554
|
say " ✓ Android SDK: #{android_home}", :green
|
|
527
555
|
android_available = true
|
|
556
|
+
elsif platform_filter == 'android'
|
|
557
|
+
say ' ✗ Android SDK not found (set ANDROID_HOME)', :red
|
|
558
|
+
issues << 'Android SDK not found — set ANDROID_HOME to your SDK location.'
|
|
528
559
|
else
|
|
529
560
|
say ' ℹ️ Android SDK not found (set ANDROID_HOME)', :cyan
|
|
530
561
|
end
|
|
@@ -581,25 +612,32 @@ module Mysigner
|
|
|
581
612
|
say ''
|
|
582
613
|
end
|
|
583
614
|
|
|
584
|
-
# Check 15: Android Project Detection
|
|
585
|
-
|
|
615
|
+
# Check 15: Android Project Detection (read-only — must NEVER
|
|
616
|
+
# trigger an expo prebuild from a diagnostic, hence allow_prebuild:
|
|
617
|
+
# false).
|
|
586
618
|
begin
|
|
587
|
-
android_project = Build::Detector.detect_android
|
|
588
|
-
framework = case android_project[:framework]
|
|
589
|
-
when :capacitor then 'Capacitor/Ionic'
|
|
590
|
-
when :react_native then 'React Native'
|
|
591
|
-
when :flutter then 'Flutter'
|
|
592
|
-
else 'Native Android'
|
|
593
|
-
end
|
|
619
|
+
android_project = Build::Detector.detect_android(allow_prebuild: false)
|
|
594
620
|
say 'Checking Android project...', :yellow
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
621
|
+
if android_project[:needs_prebuild]
|
|
622
|
+
say ' ℹ️ Expo managed project detected (no android/ folder yet)', :cyan
|
|
623
|
+
say ' Generate it with: mysigner android build', :cyan
|
|
624
|
+
say ' (needs Node >= 20.19.4 and `npm install` first)', :cyan
|
|
625
|
+
else
|
|
626
|
+
framework = case android_project[:framework]
|
|
627
|
+
when :capacitor then 'Capacitor/Ionic'
|
|
628
|
+
when :react_native then 'React Native'
|
|
629
|
+
when :flutter then 'Flutter'
|
|
630
|
+
else 'Native Android'
|
|
631
|
+
end
|
|
632
|
+
say " ✓ Found #{framework} Android project", :green
|
|
633
|
+
|
|
634
|
+
# Parse project details
|
|
635
|
+
require_relative '../build/android_parser'
|
|
636
|
+
parser = Build::AndroidParser.new(android_project)
|
|
637
|
+
say " Package: #{parser.application_id}", :cyan
|
|
638
|
+
say " Version: #{parser.version_name} (#{parser.version_code})", :cyan
|
|
639
|
+
say " Gradle wrapper: #{parser.gradle_wrapper_exists? ? '✓' : '✗'}", :cyan
|
|
640
|
+
end
|
|
603
641
|
say ''
|
|
604
642
|
rescue Build::Detector::NoProjectError
|
|
605
643
|
# Not an Android project, that's fine
|
|
@@ -617,7 +655,7 @@ module Mysigner
|
|
|
617
655
|
if issues.empty? && warnings.empty?
|
|
618
656
|
say "🎉 All checks passed! You're good to go!", :green
|
|
619
657
|
say ''
|
|
620
|
-
say 'Try: mysigner ship testflight', :cyan
|
|
658
|
+
say(platform_filter == 'android' ? 'Try: mysigner ship internal --platform android' : 'Try: mysigner ship testflight', :cyan)
|
|
621
659
|
elsif issues.empty?
|
|
622
660
|
say "⚠️ #{warnings.length} warning(s), but you're mostly good!", :yellow
|
|
623
661
|
say ''
|