Module: Mysigner::CLI::Concerns::Helpers
- Included in:
- Mysigner::CLI
- Defined in:
- lib/mysigner/cli/concerns/helpers.rb
Class Method Summary collapse
Instance Method Summary collapse
-
#blank_local_only_config ⇒ Object
mysigner-22 — a Config built without touching disk, ENV, or the encryption key.
- #create_client(config) ⇒ Object
-
#emit_local_only_banner ⇒ Object
One-time banner on stderr (TTY only) so users know they’ve opted into local-only mode.
- #error(message) ⇒ Object
-
#exit_unless_local_supported!(command_name) ⇒ Object
Server-only command guard.
- #format_bytes(bytes) ⇒ Object
- #format_duration(seconds) ⇒ Object
- #load_config ⇒ Object
-
#local_only? ⇒ Boolean
Local-only mode is active when any of, in precedence order: 1.
-
#macos? ⇒ Boolean
True on macOS.
-
#maybe_install_node_deps!(project_dir = Dir.pwd) ⇒ Object
For an Expo / React-Native project, the native android build can’t run until JS dependencies are installed.
-
#require_macos!(command_label = 'This command') ⇒ Object
Guard for iOS-only commands.
-
#resolve_local_android_keystore_or_exit ⇒ Object
mysigner-22 Phase 7 — Android keystore counterpart of the ASC/Play resolvers.
-
#resolve_local_asc_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag → env → keychain → ~/.appstoreconnect → prompt), surface a clear error and exit 1 on miss.
-
#resolve_local_play_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — Google Play counterpart of resolve_local_asc_creds_or_exit.
-
#setup_requested? ⇒ Boolean
Whether the user opted into automatic setup (package install / prebuild) via the –setup flag or MYSIGNER_AUTO_SETUP=1.
-
#valid_ios_udid?(udid) ⇒ Boolean
Client-side UDID validity check for iOS devices.
-
#with_timing(_label) ⇒ Object
Helper for timing operations.
Class Method Details
.banner_emitted? ⇒ Boolean
349 350 351 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 349 def @local_only_banner_emitted == true end |
.mark_banner_emitted! ⇒ Object
353 354 355 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 353 def @local_only_banner_emitted = true end |
.reset_banner! ⇒ Object
357 358 359 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 357 def @local_only_banner_emitted = false end |
Instance Method Details
#blank_local_only_config ⇒ Object
mysigner-22 — a Config built without touching disk, ENV, or the encryption key. We deliberately bypass Config#initialize (which auto-loads ~/.mysigner/config.yml when it exists) because the whole point of local-only is to work on a machine where that file might not exist or might be unreadable (e.g. broken Keychain key). Callers only read ‘current_organization_id` / `api_url` / etc., all of which legitimately return nil here.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 150 def blank_local_only_config config = Config.allocate config.instance_variable_set(:@api_url, nil) config.instance_variable_set(:@user_email, nil) config.instance_variable_set(:@current_organization_id, nil) config.instance_variable_set(:@organizations, {}) config.instance_variable_set(:@encryption_enabled, false) config.instance_variable_set(:@from_env, false) # The whole point of this sentinel is to BE a local-only config — # set @local_only = true so `config.local_only?` (and any caller # reading `config.local_only`) agrees. config.instance_variable_set(:@local_only, true) config end |
#create_client(config) ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 129 def create_client(config) # mysigner-22 — in local-only mode every MySigner API touchpoint is # supposed to be bypassed at the call site. Returning nil here makes # an accidental `client.get(...)` fail loud (NoMethodError on nil) # rather than silently re-introducing a server hit. return nil if local_only? Client.new( api_url: config.api_url, api_token: config.api_token, user_email: config.user_email ) end |
#emit_local_only_banner ⇒ Object
One-time banner on stderr (TTY only) so users know they’ve opted into local-only mode. Module-level guard ensures it fires at most once per CLI invocation, even if multiple commands call it. (Module instance var, not @@, to avoid the class-var smell — every instance method sees the same Helpers module object.)
319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 319 def return if Helpers. return unless $stderr.respond_to?(:tty?) && $stderr.tty? Helpers. # Honest scope: credential transport is what local-only currently # guards. Non-credential MySigner endpoints (app/build registry, # keystore download, etc.) are still used. Documented in the # local-only docs (mysigner-45). warn '[mysigner] local-only mode active — signing credentials stay on this machine ' \ '(other MySigner APIs may still be used; see docs).' end |
#error(message) ⇒ Object
165 166 167 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 165 def error() say "✗ Error: #{}", :red end |
#exit_unless_local_supported!(command_name) ⇒ Object
Server-only command guard. SERVER commands (apps, orgs, sync, certificates, etc.) hit MySigner-side resources and have no local equivalent. Print a clean explanation and exit 2 when local-only mode is active, instead of letting load_config bail with the generic “Not logged in” path.
337 338 339 340 341 342 343 344 345 346 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 337 def exit_unless_local_supported!(command_name) return unless local_only? say "✗ `#{command_name}` manages MySigner-side resources and " \ "isn't available in local-only mode.", :red say '' say 'Disable persistently: mysigner config set local-only false', :yellow say "Override for one call: mysigner --no-local-only #{command_name}", :yellow exit 2 end |
#format_bytes(bytes) ⇒ Object
29 30 31 32 33 34 35 36 37 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 29 def format_bytes(bytes) if bytes < 1024 "#{bytes} B" elsif bytes < 1024 * 1024 "#{(bytes / 1024.0).round(1)} KB" else "#{(bytes / (1024.0 * 1024)).round(1)} MB" end end |
#format_duration(seconds) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 15 def format_duration(seconds) if seconds < 60 "#{seconds.round}s" elsif seconds < 3600 minutes = (seconds / 60).floor secs = (seconds % 60).round "#{minutes}m #{secs}s" else hours = (seconds / 3600).floor minutes = ((seconds % 3600) / 60).floor "#{hours}h #{minutes}m" end end |
#load_config ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 71 def load_config # mysigner-22 — local-only mode is allowed to run with ZERO MySigner # auth: no ~/.mysigner/config.yml, no API token, no login. Return a # blank Config sentinel and skip both the load and the # "Not logged in" exit. Callers must treat the returned config as # opaque (no api_url, no api_token, no organization_id) and must # also treat create_client(config) as returning nil. return blank_local_only_config if local_only? # CI/CD mode: prefer environment variables when set return Config.from_env if Config.env_configured? config = Config.new unless config.exists? error "Not logged in. Run 'mysigner login' first." say '' say 'Tip: For CI/CD, set these environment variables instead:', :yellow say ' export MYSIGNER_API_TOKEN=your_token', :yellow say ' export MYSIGNER_ORG_ID=your_org_id', :yellow say ' export MYSIGNER_API_URL=https://mysigner.dev # optional', :yellow say ' export MYSIGNER_EMAIL=you@example.com # optional', :yellow say '' # Discoverability: a first-time user who doesn't want a MySigner # account at all (BYO-credentials, no server orchestration) needs # to know `--local-only` exists. Without this tip the only error # they see is "Not logged in", which strongly implies signup is # the only path forward. say 'Tip: To skip MySigner entirely and ship locally with your own', :yellow say 'Apple/Google credentials, use --local-only:', :yellow say ' mysigner --local-only ship appstore', :yellow say ' (auto-discovers ASC .p8 from ~/.appstoreconnect/private_keys/,', :yellow say ' Google Play SA-JSON from GOOGLE_APPLICATION_CREDENTIALS / eas.json,', :yellow say ' keystore from key.properties / eas.json — or set them via flags / env.)', :yellow say ' Note: build / ship / sign work fully local; account commands', :yellow say ' (orgs / switch / sync) still need a My Signer login.', :yellow say ' See "Local-only mode" section in README.', :yellow exit 1 end config.load # Surface an unreadable stored token as a clean re-login prompt here, # at the auth gate, instead of letting the decrypt error explode later # inside create_client / Config#display. (api_token decrypts lazily.) begin config.api_token rescue Mysigner::ConfigError error 'Your saved login is unreadable (encryption key changed or ' \ 'config copied between machines).' say "Run 'mysigner logout' then 'mysigner login' to re-authenticate.", :yellow say 'Or run with --local-only to skip MySigner entirely.', :yellow exit 1 end config end |
#local_only? ⇒ Boolean
Local-only mode is active when any of, in precedence order:
1. --local-only / --no-local-only flag on this invocation
2. MYSIGNER_LOCAL_ONLY env var
3. `local_only: true` in ~/.mysigner/config.yml
‘Config.local_only?` (class method) walks #2 then #3.
257 258 259 260 261 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 257 def local_only? return [:local_only] unless [:local_only].nil? Mysigner::Config.local_only? end |
#macos? ⇒ Boolean
True on macOS. iOS building/signing (Xcode, xcodebuild, the keychain) only works there; iOS-only commands call require_macos! to fail with a clear message instead of a raw “xcodebuild: not found” backtrace.
172 173 174 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 172 def macos? !(RbConfig::CONFIG['host_os'] =~ /darwin/i).nil? end |
#maybe_install_node_deps!(project_dir = Dir.pwd) ⇒ Object
For an Expo / React-Native project, the native android build can’t run until JS dependencies are installed. Rather than failing deep inside ‘expo prebuild`, detect it up front and: auto-run with –setup / MYSIGNER_AUTO_SETUP, OR offer to run it interactively, OR print the EXACT install command for this project’s package manager and exit. We never silently run a package install (it’s slow and the wrong manager can corrupt the lockfile) — only with consent or an opt-in.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 197 def maybe_install_node_deps!(project_dir = Dir.pwd) require 'json' require 'mysigner/build/detector' pkg_path = File.join(project_dir, 'package.json') return unless File.exist?(pkg_path) return if Dir.exist?(File.join(project_dir, 'node_modules')) pkg = begin JSON.parse(File.read(pkg_path)) rescue StandardError {} end deps = {} deps.merge!(pkg['dependencies']) if pkg['dependencies'].is_a?(Hash) deps.merge!(pkg['devDependencies']) if pkg['devDependencies'].is_a?(Hash) # Only Expo / React-Native projects need node deps to produce a native # Android build; a plain native/Flutter project does not. return unless deps.key?('expo') || deps.key?('react-native') cmd = Mysigner::Build::Detector.install_command( Mysigner::Build::Detector.detect_package_manager(project_dir) ) auto = setup_requested? auto ||= $stdin.tty? && yes_with_default?( "JavaScript dependencies aren't installed (no node_modules). Run `#{cmd}` now?", :cyan ) unless auto error "JavaScript dependencies aren't installed (no node_modules)." say 'Install them first, then re-run:', :yellow say " #{cmd}", :cyan say '(or pass --setup / set MYSIGNER_AUTO_SETUP=1 to let mysigner run it for you)', :yellow exit 1 end say "📦 Installing JavaScript dependencies: #{cmd}", :cyan ok = Dir.chdir(project_dir) { system(*cmd.split) } unless ok error "`#{cmd}` failed — install dependencies manually, then re-run." exit 1 end say '✓ Dependencies installed.', :green say '' end |
#require_macos!(command_label = 'This command') ⇒ Object
Guard for iOS-only commands. On non-macOS, explain plainly and point the user at the Android path that DOES work cross-platform, then exit.
178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 178 def require_macos!(command_label = 'This command') return if macos? error "#{command_label} requires macOS with Xcode." say '' say 'iOS building, signing, and uploading only work on a Mac (they use Xcode).', :yellow say 'On Linux or Windows you can still build and ship Android:', :yellow say ' mysigner ship internal --platform android', :cyan say ' mysigner android build', :cyan exit 1 end |
#resolve_local_android_keystore_or_exit ⇒ Object
mysigner-22 Phase 7 — Android keystore counterpart of the ASC/Play resolvers. Pre-resolves the keystore (path + passwords + alias) via the cascade so ‘ship android –local-only` can skip the MySigner server entirely.
303 304 305 306 307 308 309 310 311 312 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 303 def resolve_local_android_keystore_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_android_keystore(options: .to_h) warn "[mysigner] Android keystore source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e say "✗ No local Android keystore found:\n#{e.}", :red exit 1 end |
#resolve_local_asc_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag →env → keychain → ~/.appstoreconnect → prompt), surface a clear error and exit 1 on miss. Logs the winning source on stderr so CI runs leave an audit trail of where the credential came from. Returns a Mysigner::CredentialResolver::AscCreds struct.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 268 def resolve_local_asc_creds_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_asc(options: .to_h) warn "[mysigner] ASC credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e # Preserve the historical wording the CLI specs were written # against ("No local ASC credentials found") so users and tests # have a stable identifier, while including the resolver's # multi-line cascade trace + override knob list right after it. say "✗ No local ASC credentials found via `mysigner onboard --local-only` or other sources:\n#{e.}", :red exit 1 end |
#resolve_local_play_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — Google Play counterpart of resolve_local_asc_creds_or_exit. Same semantics: pre-resolve so the CLI can fail fast before the build kicks off.
287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 287 def resolve_local_play_creds_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_play(options: .to_h) warn "[mysigner] Google Play credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e say "✗ No local Google Play credentials found via `mysigner onboard --local-only` or other sources:\n#{e.}", :red exit 1 end |
#setup_requested? ⇒ Boolean
Whether the user opted into automatic setup (package install / prebuild) via the –setup flag or MYSIGNER_AUTO_SETUP=1. Commands without a –setup option still honour the env var.
247 248 249 250 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 247 def setup_requested? flag = respond_to?(:options) && .respond_to?(:[]) ? [:setup] : nil flag == true || ENV['MYSIGNER_AUTO_SETUP'] == '1' end |
#valid_ios_udid?(udid) ⇒ Boolean
Client-side UDID validity check for iOS devices. Matches the two formats Apple uses: 25-character alphanumeric (older devices pre- iPhone X) and 40-character hex, optionally with a single dash after the first 8 chars (newer). Also rejects obviously synthetic values (all zeros, single-character repeats) that Apple’s dev-portal sandbox has been known to accept even though they can never match a real device.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 46 def valid_ios_udid?(udid) return false if udid.nil? || udid.strip.empty? normalized = udid.strip.upcase # 25-char legacy UDID: alphanumeric legacy = normalized.match?(/\A[0-9A-F]{25}\z/) # 40-char modern UDID: hex, optional dash after first 8 chars modern_plain = normalized.match?(/\A[0-9A-F]{40}\z/) modern_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{16}\z/) # 8-16 form some tools emit (older spec) modern_full_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{32}\z/) # 8-32 (what xcrun outputs) return false unless legacy || modern_plain || modern_dashed || modern_full_dashed hex_only = normalized.delete('-') # Reject trivially synthetic UDIDs. A real UDID has at least 4 # distinct hex characters among its 25/40 positions; "000…" or # "AAAA…" or "012345…" style sequences flunk that. distinct = hex_only.chars.uniq.size return false if distinct < 4 true end |
#with_timing(_label) ⇒ Object
Helper for timing operations
8 9 10 11 12 13 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 8 def with_timing(_label) start = Time.now result = yield duration = Time.now - start [result, duration] end |