mysigner 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -144,6 +144,23 @@ module Mysigner
144
144
  udid = args[1]
145
145
  platform = options[:platform].upcase
146
146
 
147
+ # Client-side UDID sanity check — catches obvious typos and
148
+ # copy-paste errors before they hit Apple's API (which, for IOS
149
+ # at least, has historically accepted synthetic/all-zeros UDIDs
150
+ # in sandbox environments). Skipped for non-IOS platforms.
151
+ if (platform == 'IOS') && !valid_ios_udid?(udid)
152
+ error "Invalid iOS UDID: #{udid}"
153
+ say ''
154
+ say 'iOS UDIDs must:', :yellow
155
+ say ' • Be either 25 chars (older devices) or 40 hex chars (newer)', :cyan
156
+ say ' • Be hexadecimal (0-9, A-F)', :cyan
157
+ say ' • Not be trivially synthetic (e.g. all zeros, single repeated char)', :cyan
158
+ say ''
159
+ say '💡 Get a real UDID:', :cyan
160
+ say ' mysigner device detect', :yellow
161
+ exit 1
162
+ end
163
+
147
164
  say '📱 Registering device...', :cyan
148
165
  say ''
149
166
 
@@ -296,7 +313,11 @@ module Mysigner
296
313
 
297
314
  # Also try xcrun xctrace (Xcode command line tools)
298
315
  if devices.empty? && system('which xcrun > /dev/null 2>&1')
299
- output = `xcrun xctrace list devices 2>/dev/null`
316
+ # xctrace output can contain non-ASCII bytes (emoji in device
317
+ # names, accented characters). Force UTF-8 + scrub invalid
318
+ # sequences so `.strip` / `.each_line` don't raise
319
+ # Encoding::CompatibilityError under the default US-ASCII locale.
320
+ output = `xcrun xctrace list devices 2>/dev/null`.force_encoding('UTF-8').scrub
300
321
  in_devices_section = false
301
322
 
302
323
  output.each_line do |line|
@@ -591,14 +612,18 @@ module Mysigner
591
612
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
592
613
  profile = response[:data]
593
614
 
594
- # Determine output path
615
+ # Determine output path. Default to ~/Downloads/ (created if
616
+ # missing) instead of the current working directory — users
617
+ # were ending up with .mobileprovision files sprinkled inside
618
+ # whatever project they ran the command from.
595
619
  output_path = if options[:output]
596
620
  options[:output]
597
621
  else
598
- # Use profile name, sanitize it for filename
599
622
  name = profile['name'] || "profile_#{profile['id']}"
600
623
  filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
601
- "#{filename}.mobileprovision"
624
+ downloads_dir = File.expand_path('~/Downloads')
625
+ FileUtils.mkdir_p(downloads_dir)
626
+ File.join(downloads_dir, "#{filename}.mobileprovision")
602
627
  end
603
628
 
604
629
  # Download the profile content using the client's connection with auth
@@ -924,14 +949,17 @@ module Mysigner
924
949
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}")
925
950
  certificate = response[:data]
926
951
 
927
- # Determine output path
952
+ # Determine output path. Default to ~/Downloads/ (mirrors
953
+ # profile download behavior) rather than the CWD to avoid
954
+ # dropping .cer files inside the user's project tree.
928
955
  output_path = if options[:output]
929
956
  options[:output]
930
957
  else
931
- # Use certificate name, sanitize it for filename
932
958
  name = certificate['name'] || "certificate_#{certificate['id']}"
933
959
  filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
934
- "#{filename}.cer"
960
+ downloads_dir = File.expand_path('~/Downloads')
961
+ FileUtils.mkdir_p(downloads_dir)
962
+ File.join(downloads_dir, "#{filename}.cer")
935
963
  end
936
964
 
937
965
  # Download the certificate content (binary response)
@@ -1127,14 +1155,33 @@ module Mysigner
1127
1155
  say '🔐 Uploading keystore...', :cyan
1128
1156
  say ''
1129
1157
 
1130
- # Get keystore details
1158
+ # Phase 0: support non-TTY automation. `ask(echo: false)` raises
1159
+ # Errno::ENOTTY on piped stdin, which made `keystore upload`
1160
+ # unusable from CI. When stdin isn't a TTY, require passwords
1161
+ # to come from env vars (MYSIGNER_KEYSTORE_PASSWORD /
1162
+ # MYSIGNER_KEY_PASSWORD) so operators can script uploads
1163
+ # without also having to wrap every invocation in `expect`.
1131
1164
  name = options[:name] || ask("Keystore name (e.g., 'Release Key'):")
1132
1165
  key_alias = options[:alias] || ask('Key alias:')
1133
- password = ask('Keystore password:', echo: false)
1134
- say ''
1135
- key_password = ask('Key password (press Enter if same as keystore):', echo: false)
1136
- say ''
1137
- key_password = password if key_password.empty?
1166
+
1167
+ if $stdin.tty?
1168
+ password = ask('Keystore password:', echo: false)
1169
+ say ''
1170
+ key_password = ask('Key password (press Enter if same as keystore):', echo: false)
1171
+ say ''
1172
+ key_password = password if key_password.empty?
1173
+ else
1174
+ password = ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
1175
+ key_password = ENV['MYSIGNER_KEY_PASSWORD'] || password
1176
+ if password.nil? || password.empty?
1177
+ error 'Non-interactive upload requires MYSIGNER_KEYSTORE_PASSWORD (and optionally MYSIGNER_KEY_PASSWORD) in the environment.'
1178
+ say ''
1179
+ say 'Example:', :cyan
1180
+ say ' MYSIGNER_KEYSTORE_PASSWORD=... MYSIGNER_KEY_PASSWORD=... \\', :yellow
1181
+ say ' mysigner keystore upload ./release.jks --name "Release" --alias myalias', :yellow
1182
+ exit 1
1183
+ end
1184
+ end
1138
1185
 
1139
1186
  begin
1140
1187
  result = manager.upload(
@@ -1742,35 +1789,64 @@ module Mysigner
1742
1789
  exit 1
1743
1790
  end
1744
1791
 
1745
- Dir.chdir(project_dir) do
1746
- args = ['flutter', 'build', 'appbundle', '--release']
1792
+ # Phase 0: do NOT write plaintext key.properties into the user's
1793
+ # project tree. Use a two-step build so we can inject signing via
1794
+ # a Gradle init script on the Gradle step only. Env vars are set
1795
+ # on the child process; passwords never appear in argv or on disk.
1796
+ require_relative '../signing/gradle_signing_injector'
1747
1797
 
1748
- # Add version code override
1749
- args += ['--build-number', version_code_override.to_s] if version_code_override
1798
+ injector = nil
1799
+ env = {}
1800
+ init_path = nil
1750
1801
 
1751
- # Add signing if keystore provided (Flutter reads key.properties from android/)
1752
- if keystore_info
1753
- # Create key.properties for Flutter
1754
- key_props = File.join(project_dir, 'android/key.properties')
1755
- File.write(key_props, <<~PROPS)
1756
- storePassword=#{keystore_info[:password]}
1757
- keyPassword=#{keystore_info[:key_password]}
1758
- keyAlias=#{keystore_info[:key_alias]}
1759
- storeFile=#{keystore_info[:path]}
1760
- PROPS
1761
- end
1802
+ if keystore_info
1803
+ injector = Mysigner::Signing::GradleSigningInjector.new
1804
+ init_path = injector.write_init_script!
1805
+ env = keystore_info[:signing_env_vars] || injector.env_vars(
1806
+ keystore_path: keystore_info[:path],
1807
+ store_password: keystore_info[:password],
1808
+ key_password: keystore_info[:key_password],
1809
+ key_alias: keystore_info[:key_alias]
1810
+ )
1811
+ end
1762
1812
 
1763
- success = system(*args)
1764
- unless success
1765
- error 'Flutter build failed'
1766
- exit 1
1813
+ begin
1814
+ Dir.chdir(project_dir) do
1815
+ if keystore_info
1816
+ # Step 1: Flutter prepares the Gradle project (no signing needed).
1817
+ prepare_args = ['flutter', 'build', 'appbundle', '--release', '--config-only']
1818
+ prepare_args += ['--build-number', version_code_override.to_s] if version_code_override
1819
+ unless system(env, *prepare_args)
1820
+ error 'Flutter prebuild (--config-only) failed'
1821
+ exit 1
1822
+ end
1823
+
1824
+ # Step 2: invoke Gradle directly so we can pass --init-script.
1825
+ Dir.chdir(File.join(project_dir, 'android')) do
1826
+ gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all', '--init-script', init_path]
1827
+ gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
1828
+ unless system(env, *gradle_args)
1829
+ error 'Gradle bundleRelease failed'
1830
+ exit 1
1831
+ end
1832
+ end
1833
+ else
1834
+ # No keystore: plain Flutter build (debug signing).
1835
+ args = ['flutter', 'build', 'appbundle', '--release']
1836
+ args += ['--build-number', version_code_override.to_s] if version_code_override
1837
+ unless system(*args)
1838
+ error 'Flutter build failed'
1839
+ exit 1
1840
+ end
1841
+ end
1767
1842
  end
1843
+ ensure
1844
+ injector&.cleanup!
1768
1845
  end
1769
1846
 
1770
1847
  # Flutter outputs to build/app/outputs/bundle/release/
1771
1848
  aab_path = File.join(project_dir, 'build/app/outputs/bundle/release/app-release.aab')
1772
1849
  unless File.exist?(aab_path)
1773
- # Try alternate paths
1774
1850
  alt_paths = Dir.glob(File.join(project_dir, 'build/app/outputs/bundle/*/*.aab'))
1775
1851
  aab_path = alt_paths.first if alt_paths.any?
1776
1852
  end
@@ -1858,47 +1934,45 @@ module Mysigner
1858
1934
  return nil unless config.exists?
1859
1935
 
1860
1936
  config.load
1861
- return nil unless config.api_token && config.organization_id
1937
+ return nil unless config.api_token && config.current_organization_id
1862
1938
 
1863
1939
  client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
1864
1940
  user_email: config.user_email)
1865
- keystore_manager = Signing::KeystoreManager.new(client, config.organization_id)
1941
+ keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
1866
1942
 
1867
1943
  # Find app by package name to get its keystore
1868
- response = client.get("/api/v1/organizations/#{config.organization_id}/android_apps")
1944
+ response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
1869
1945
  apps = response[:data]['android_apps'] || []
1870
1946
  app = apps.find { |a| a['package_name'] == package_name }
1871
1947
 
1872
- if app
1873
- # Get active keystore for this app with secrets
1874
- keystore = keystore_manager.active_keystore(android_app_id: app['id'], include_secrets: true)
1875
- if keystore
1876
- # Download the keystore file
1877
- downloaded = keystore_manager.get_or_download(keystore['id'])
1878
- return {
1879
- path: downloaded[:path],
1880
- name: keystore['name'],
1881
- password: keystore['keystore_password'],
1882
- key_alias: keystore['key_alias'],
1883
- key_password: keystore['key_password'] || keystore['keystore_password']
1884
- }
1885
- end
1886
- end
1887
-
1888
- # Try to get any active keystore
1889
- keystore = keystore_manager.active_keystore(include_secrets: true)
1890
- if keystore
1891
- downloaded = keystore_manager.get_or_download(keystore['id'])
1892
- return {
1893
- path: downloaded[:path],
1894
- name: keystore['name'],
1895
- password: keystore['keystore_password'],
1896
- key_alias: keystore['key_alias'],
1897
- key_password: keystore['key_password'] || keystore['keystore_password']
1948
+ keystore = nil
1949
+ keystore = keystore_manager.active_keystore(android_app_id: app['id']) if app
1950
+ keystore ||= keystore_manager.active_keystore
1951
+ return nil unless keystore
1952
+
1953
+ # Phase 0: fetch passwords via narrow audit-logged /secrets endpoint
1954
+ # instead of the deprecated ?include_secrets=true param on the list.
1955
+ secrets = keystore_manager.fetch_secrets(keystore['id'])
1956
+ downloaded = keystore_manager.get_or_download(keystore['id'])
1957
+ password = secrets['keystore_password']
1958
+ key_password = secrets['key_password'] || password
1959
+ key_alias = secrets['key_alias'] || keystore['key_alias']
1960
+ {
1961
+ path: downloaded[:path],
1962
+ name: keystore['name'],
1963
+ password: password,
1964
+ key_alias: key_alias,
1965
+ key_password: key_password,
1966
+ id: keystore['id'],
1967
+ # Ready-to-spawn env vars consumed by GradleSigningInjector in
1968
+ # build_gradle_aab / build_flutter_aab / Build::AndroidExecutor.
1969
+ signing_env_vars: {
1970
+ 'MYSIGNER_STORE_FILE' => downloaded[:path],
1971
+ 'MYSIGNER_STORE_PASSWORD' => password,
1972
+ 'MYSIGNER_KEY_PASSWORD' => key_password,
1973
+ 'MYSIGNER_KEY_ALIAS' => key_alias
1898
1974
  }
1899
- end
1900
-
1901
- nil
1975
+ }
1902
1976
  rescue StandardError
1903
1977
  # Silently fail - we'll use debug signing
1904
1978
  nil
@@ -1912,45 +1986,84 @@ module Mysigner
1912
1986
  exit 1
1913
1987
  end
1914
1988
 
1989
+ require 'tmpdir'
1990
+ pw_tmpdir = nil
1991
+ store_pw_path = nil
1992
+ key_pw_path = nil
1993
+
1915
1994
  Dir.chdir(project_dir) do
1916
1995
  base_args = []
1917
1996
 
1918
- # Add signing args if keystore provided
1997
+ # Phase 0: For AAB output, MSBuild only supports `file:<path>`
1998
+ # for AndroidSigning*Pass (env: is explicitly unsupported for AAB).
1999
+ # Write passwords to 0600 tempfiles whose *paths* go into argv
2000
+ # instead of the passwords themselves.
1919
2001
  if keystore_info
2002
+ pw_tmpdir = Dir.mktmpdir('mysigner-maui-pw-')
2003
+ store_pw_path = File.join(pw_tmpdir, 'store_pw.txt')
2004
+ File.write(store_pw_path, keystore_info[:password].to_s)
2005
+ File.chmod(0o600, store_pw_path)
2006
+
2007
+ # Reuse the same file if store/key passwords match (common)
2008
+ if keystore_info[:key_password] == keystore_info[:password]
2009
+ key_pw_path = store_pw_path
2010
+ else
2011
+ key_pw_path = File.join(pw_tmpdir, 'key_pw.txt')
2012
+ File.write(key_pw_path, keystore_info[:key_password].to_s)
2013
+ File.chmod(0o600, key_pw_path)
2014
+ end
2015
+
1920
2016
  base_args += [
1921
2017
  '-p:AndroidKeyStore=true',
1922
2018
  "-p:AndroidSigningKeyStore=#{keystore_info[:path]}",
1923
2019
  "-p:AndroidSigningKeyAlias=#{keystore_info[:key_alias]}",
1924
- "-p:AndroidSigningKeyPass=#{keystore_info[:key_password]}",
1925
- "-p:AndroidSigningStorePass=#{keystore_info[:password]}"
2020
+ "-p:AndroidSigningKeyPass=file:#{key_pw_path}",
2021
+ "-p:AndroidSigningStorePass=file:#{store_pw_path}"
1926
2022
  ]
1927
2023
  end
1928
2024
 
1929
2025
  # Add version code override
1930
2026
  base_args << "-p:ApplicationVersion=#{version_code_override}" if version_code_override
1931
2027
 
1932
- # MAUI uses dotnet publish with Android target
1933
- success = if framework == :maui
1934
- system(
1935
- 'dotnet', 'publish',
1936
- '-f', 'net8.0-android',
1937
- '-c', 'Release',
1938
- '-p:AndroidPackageFormat=aab',
1939
- *base_args
1940
- )
1941
- else
1942
- # Xamarin uses msbuild
1943
- system(
1944
- 'dotnet', 'build',
1945
- '-c', 'Release',
1946
- '-p:AndroidPackageFormat=aab',
1947
- *base_args
1948
- )
1949
- end
1950
-
1951
- unless success
1952
- error '.NET build failed'
1953
- exit 1
2028
+ begin
2029
+ # MAUI uses dotnet publish with Android target
2030
+ success = if framework == :maui
2031
+ system(
2032
+ 'dotnet', 'publish',
2033
+ '-f', 'net8.0-android',
2034
+ '-c', 'Release',
2035
+ '-p:AndroidPackageFormat=aab',
2036
+ *base_args
2037
+ )
2038
+ else
2039
+ # Xamarin uses msbuild
2040
+ system(
2041
+ 'dotnet', 'build',
2042
+ '-c', 'Release',
2043
+ '-p:AndroidPackageFormat=aab',
2044
+ *base_args
2045
+ )
2046
+ end
2047
+
2048
+ unless success
2049
+ error '.NET build failed'
2050
+ exit 1
2051
+ end
2052
+ ensure
2053
+ # Clean up password files. On Windows NTFS the child may still
2054
+ # hold the file open; fall back to at_exit deferred delete.
2055
+ [store_pw_path, key_pw_path].compact.uniq.each do |p|
2056
+ File.delete(p) if File.exist?(p)
2057
+ rescue Errno::EACCES
2058
+ at_exit do
2059
+ File.delete(p)
2060
+ rescue StandardError
2061
+ nil
2062
+ end
2063
+ rescue StandardError
2064
+ # Best effort
2065
+ end
2066
+ FileUtils.rm_rf(pw_tmpdir) if pw_tmpdir && Dir.exist?(pw_tmpdir)
1954
2067
  end
1955
2068
  end
1956
2069
 
@@ -1976,28 +2089,38 @@ module Mysigner
1976
2089
  exit 1
1977
2090
  end
1978
2091
 
1979
- # Build gradle command with signing via command-line properties
2092
+ # Phase 0: inject signing via Gradle init-script + env vars so
2093
+ # passwords never appear in `ps aux` (ORG_GRADLE_PROJECT_<name>
2094
+ # was the old workaround but doesn't support dotted names).
2095
+ require_relative '../signing/gradle_signing_injector'
2096
+
1980
2097
  gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all']
2098
+ gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
1981
2099
 
2100
+ injector = nil
2101
+ env = {}
1982
2102
  if keystore_info
1983
- # Pass signing config via command-line properties (no file modification needed)
1984
- gradle_args += [
1985
- "-Pandroid.injected.signing.store.file=#{keystore_info[:path]}",
1986
- "-Pandroid.injected.signing.store.password=#{keystore_info[:password]}",
1987
- "-Pandroid.injected.signing.key.alias=#{keystore_info[:key_alias]}",
1988
- "-Pandroid.injected.signing.key.password=#{keystore_info[:key_password]}"
1989
- ]
2103
+ injector = Mysigner::Signing::GradleSigningInjector.new
2104
+ init_path = injector.write_init_script!
2105
+ env = keystore_info[:signing_env_vars] || injector.env_vars(
2106
+ keystore_path: keystore_info[:path],
2107
+ store_password: keystore_info[:password],
2108
+ key_password: keystore_info[:key_password],
2109
+ key_alias: keystore_info[:key_alias]
2110
+ )
2111
+ gradle_args.insert(1, '--init-script', init_path)
1990
2112
  end
1991
2113
 
1992
- # Pass version code override if provided (no file modification needed)
1993
- gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
1994
-
1995
- Dir.chdir(android_dir) do
1996
- success = system(*gradle_args)
1997
- unless success
1998
- error 'Gradle build failed'
1999
- exit 1
2114
+ begin
2115
+ Dir.chdir(android_dir) do
2116
+ success = system(env, *gradle_args)
2117
+ unless success
2118
+ error 'Gradle build failed'
2119
+ exit 1
2120
+ end
2000
2121
  end
2122
+ ensure
2123
+ injector&.cleanup!
2001
2124
  end
2002
2125
 
2003
2126
  # Find the AAB
@@ -2099,19 +2222,19 @@ module Mysigner
2099
2222
 
2100
2223
  case action
2101
2224
  when 'register'
2102
- if args.empty?
2225
+ if args.empty? || args[0].nil? || args[0].to_s.empty?
2103
2226
  error 'Usage: mysigner bundleid register IDENTIFIER [NAME]'
2104
2227
  say ''
2105
2228
  say 'Example: mysigner bundleid register com.company.myapp', :yellow
2106
2229
  say 'Example: mysigner bundleid register com.company.myapp.widget "My Widget"', :yellow
2107
2230
  exit 1
2231
+ return
2108
2232
  end
2109
2233
 
2110
2234
  identifier = args[0]
2111
- # Default name is the last component of the identifier
2112
- name = args[1] || identifier.split('.').last.capitalize
2113
2235
 
2114
- # Validate bundle ID format
2236
+ # Validate bundle ID format BEFORE dereferencing (guards against
2237
+ # `.split` on a degenerate identifier like "123.com.app").
2115
2238
  unless identifier =~ /^[a-zA-Z][a-zA-Z0-9.-]*\.[a-zA-Z][a-zA-Z0-9.-]*$/
2116
2239
  error "Invalid Bundle ID format: #{identifier}"
2117
2240
  say ''
@@ -2120,8 +2243,12 @@ module Mysigner
2120
2243
  say ' • Use reverse domain notation (e.g., com.company.app)', :cyan
2121
2244
  say ' • Contain only letters, numbers, hyphens, and periods', :cyan
2122
2245
  exit 1
2246
+ return
2123
2247
  end
2124
2248
 
2249
+ # Default name is the last component of the identifier
2250
+ name = args[1] || identifier.split('.').last.capitalize
2251
+
2125
2252
  say '🔗 Registering Bundle ID...', :cyan
2126
2253
  say ''
2127
2254
  say " Identifier: #{identifier}", :white
@@ -2205,12 +2332,67 @@ module Mysigner
2205
2332
  exit 1
2206
2333
  end
2207
2334
 
2335
+ when 'delete'
2336
+ if args.empty? || args[0].nil? || args[0].to_s.empty?
2337
+ error 'Usage: mysigner bundleid delete IDENTIFIER'
2338
+ say ''
2339
+ say 'Example: mysigner bundleid delete com.company.myapp', :yellow
2340
+ exit 1
2341
+ return
2342
+ end
2343
+
2344
+ identifier = args[0]
2345
+
2346
+ say '🗑 Removing Bundle ID...', :cyan
2347
+ say " Identifier: #{identifier}", :white
2348
+ say ''
2349
+
2350
+ begin
2351
+ # Resolve the identifier → numeric id (the backend DELETE is keyed on id).
2352
+ list_response = client.get(
2353
+ "/api/v1/organizations/#{config.current_organization_id}/bundle_ids"
2354
+ )
2355
+ bundle_ids = list_response[:data]['bundle_ids'] || list_response[:data] || []
2356
+ bid = bundle_ids.find { |b| (b['identifier'] || b['bundle_id']) == identifier }
2357
+
2358
+ if bid.nil?
2359
+ error "Bundle ID not found: #{identifier}"
2360
+ say ''
2361
+ say ' → List existing: mysigner bundleid list', :yellow
2362
+ exit 1
2363
+ return
2364
+ end
2365
+
2366
+ client.delete(
2367
+ "/api/v1/organizations/#{config.current_organization_id}/bundle_ids/#{bid['id']}"
2368
+ )
2369
+
2370
+ say '✓ Bundle ID deleted from App Store Connect and local cache', :green
2371
+ say ''
2372
+ say 'Run `mysigner sync ios` to refresh your local cache if needed.', :cyan
2373
+ rescue Mysigner::ValidationError => e
2374
+ # Backend maps 409 Conflict → ValidationError. Apple refused
2375
+ # because of dependent resources (apps/profiles/capabilities).
2376
+ error e.message
2377
+ say ''
2378
+ say '💡 To delete this Bundle ID:', :cyan
2379
+ say ' 1. Remove any provisioning profiles that use it', :yellow
2380
+ say ' 2. Remove any apps tied to it in App Store Connect', :yellow
2381
+ say ' 3. Disable capabilities attached to it', :yellow
2382
+ say ' 4. Re-run `mysigner bundleid delete`', :yellow
2383
+ exit 1
2384
+ rescue Mysigner::ClientError => e
2385
+ error "Failed to delete Bundle ID: #{e.message}"
2386
+ exit 1
2387
+ end
2388
+
2208
2389
  else
2209
2390
  error "Unknown action: #{action}"
2210
2391
  say ''
2211
2392
  say 'Available actions:', :yellow
2212
2393
  say ' mysigner bundleid register IDENTIFIER [NAME]', :cyan
2213
2394
  say ' mysigner bundleid list', :cyan
2395
+ say ' mysigner bundleid delete IDENTIFIER', :cyan
2214
2396
  exit 1
2215
2397
  end
2216
2398
  end
@@ -2407,11 +2589,12 @@ module Mysigner
2407
2589
 
2408
2590
  case action
2409
2591
  when 'create'
2410
- if identifier.nil?
2592
+ if identifier.nil? || identifier.to_s.empty?
2411
2593
  error 'Usage: mysigner merchant-id create IDENTIFIER [--name NAME]'
2412
2594
  say ''
2413
2595
  say 'Example: mysigner merchant-id create merchant.com.company.app', :yellow
2414
2596
  exit 1
2597
+ return
2415
2598
  end
2416
2599
 
2417
2600
  unless identifier.start_with?('merchant.')
@@ -2419,6 +2602,7 @@ module Mysigner
2419
2602
  say ''
2420
2603
  say 'Example: merchant.com.company.app', :cyan
2421
2604
  exit 1
2605
+ return
2422
2606
  end
2423
2607
 
2424
2608
  say '💳 Creating Merchant ID...', :cyan
@@ -2448,9 +2632,10 @@ module Mysigner
2448
2632
  end
2449
2633
 
2450
2634
  when 'delete'
2451
- if identifier.nil?
2635
+ if identifier.nil? || identifier.to_s.empty?
2452
2636
  error 'Usage: mysigner merchant-id delete IDENTIFIER'
2453
2637
  exit 1
2638
+ return
2454
2639
  end
2455
2640
 
2456
2641
  say '💳 Deleting Merchant ID...', :cyan
@@ -2468,6 +2653,7 @@ module Mysigner
2468
2653
  if m.nil?
2469
2654
  error "Merchant ID not found: #{identifier}"
2470
2655
  exit 1
2656
+ return
2471
2657
  end
2472
2658
 
2473
2659
  client.delete(
@@ -2739,13 +2925,14 @@ module Mysigner
2739
2925
 
2740
2926
  case action
2741
2927
  when 'register'
2742
- if identifier.nil?
2928
+ if identifier.nil? || identifier.to_s.empty?
2743
2929
  error 'Usage: mysigner app-group register IDENTIFIER [--name NAME]'
2744
2930
  say ''
2745
2931
  say 'Example: mysigner app-group register group.com.company.shared', :yellow
2746
2932
  say ''
2747
2933
  say 'Note: Create the App Group in Apple Developer Portal first!', :cyan
2748
2934
  exit 1
2935
+ return
2749
2936
  end
2750
2937
 
2751
2938
  unless identifier.start_with?('group.')
@@ -2753,6 +2940,7 @@ module Mysigner
2753
2940
  say ''
2754
2941
  say 'Example: group.com.company.shared', :cyan
2755
2942
  exit 1
2943
+ return
2756
2944
  end
2757
2945
 
2758
2946
  say '📦 Registering App Group...', :cyan
@@ -2785,9 +2973,10 @@ module Mysigner
2785
2973
  end
2786
2974
 
2787
2975
  when 'delete'
2788
- if identifier.nil?
2976
+ if identifier.nil? || identifier.to_s.empty?
2789
2977
  error 'Usage: mysigner app-group delete IDENTIFIER'
2790
2978
  exit 1
2979
+ return
2791
2980
  end
2792
2981
 
2793
2982
  say '📦 Removing App Group...', :cyan
@@ -2805,6 +2994,7 @@ module Mysigner
2805
2994
  if g.nil?
2806
2995
  error "App Group not found: #{identifier}"
2807
2996
  exit 1
2997
+ return
2808
2998
  end
2809
2999
 
2810
3000
  client.delete(
data/lib/mysigner/cli.rb CHANGED
@@ -14,6 +14,14 @@ require_relative 'cli/diagnostic_commands'
14
14
  require_relative 'cli/build_commands'
15
15
  require_relative 'cli/resource_commands'
16
16
  require_relative 'cli/validate_commands'
17
+ require_relative 'cleanup/private_keys_purger'
18
+
19
+ # Phase 0: one-time cleanup of legacy plaintext .p8 files that older CLI
20
+ # versions wrote to ~/.private_keys/ and ~/.appstoreconnect/private_keys/.
21
+ # Idempotent — a marker file at ~/.mysigner/.private_keys_purged prevents
22
+ # re-running. Skipped when MYSIGNER_USE_LEGACY_ASC=1 so users who opted
23
+ # back into the legacy altool path keep their existing keys.
24
+ Mysigner::Cleanup::PrivateKeysPurger.new.call
17
25
 
18
26
  module Mysigner
19
27
  class CLI < Thor