ruby_everywhere 0.1.15 → 0.2.0

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/bridge/README.md +57 -2
  3. data/bridge/everywhere/bridge.js +803 -9
  4. data/bridge/package.json +1 -1
  5. data/lib/everywhere/builders/ios.rb +364 -0
  6. data/lib/everywhere/cli.rb +2 -0
  7. data/lib/everywhere/commands/build.rb +17 -1
  8. data/lib/everywhere/commands/clean.rb +19 -10
  9. data/lib/everywhere/commands/dev.rb +182 -19
  10. data/lib/everywhere/commands/doctor.rb +45 -3
  11. data/lib/everywhere/commands/icon.rb +14 -0
  12. data/lib/everywhere/commands/install.rb +37 -6
  13. data/lib/everywhere/commands/logs.rb +56 -0
  14. data/lib/everywhere/commands/platform/build.rb +3 -3
  15. data/lib/everywhere/commands/platform/runner.rb +1 -1
  16. data/lib/everywhere/commands/release.rb +1 -1
  17. data/lib/everywhere/commands/shell_dir.rb +11 -4
  18. data/lib/everywhere/config.rb +366 -1
  19. data/lib/everywhere/engine.rb +33 -1
  20. data/lib/everywhere/icon.rb +50 -0
  21. data/lib/everywhere/log_filter.rb +28 -2
  22. data/lib/everywhere/mobile_config_endpoint.rb +75 -0
  23. data/lib/everywhere/mobile_configs_controller.rb +46 -0
  24. data/lib/everywhere/native_helper.rb +156 -0
  25. data/lib/everywhere/paths.rb +41 -0
  26. data/lib/everywhere/raster.rb +17 -0
  27. data/lib/everywhere/shellout.rb +3 -1
  28. data/lib/everywhere/simulator.rb +74 -0
  29. data/lib/everywhere/ui.rb +18 -0
  30. data/lib/everywhere/version.rb +1 -1
  31. data/support/mobile/ios/App/App.xcconfig +6 -0
  32. data/support/mobile/ios/App/AppDelegate.swift +163 -0
  33. data/support/mobile/ios/App/Assets.xcassets/AccentColor.colorset/Contents.json +20 -0
  34. data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  35. data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  36. data/support/mobile/ios/App/Assets.xcassets/Contents.json +6 -0
  37. data/support/mobile/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  38. data/support/mobile/ios/App/Base.lproj/LaunchScreen.storyboard +32 -0
  39. data/support/mobile/ios/App/Bridge/BiometricsComponent.swift +276 -0
  40. data/support/mobile/ios/App/Bridge/HapticsComponent.swift +47 -0
  41. data/support/mobile/ios/App/Bridge/NotificationComponent.swift +56 -0
  42. data/support/mobile/ios/App/Bridge/PermissionsComponent.swift +142 -0
  43. data/support/mobile/ios/App/Bridge/StorageComponent.swift +63 -0
  44. data/support/mobile/ios/App/ErrorViewController.swift +64 -0
  45. data/support/mobile/ios/App/EverywhereConfig.swift +268 -0
  46. data/support/mobile/ios/App/EverywhereHost.swift +34 -0
  47. data/support/mobile/ios/App/Extensions/EverywhereExtensions.swift +32 -0
  48. data/support/mobile/ios/App/Info.plist +28 -0
  49. data/support/mobile/ios/App/Resources/everywhere.json +8 -0
  50. data/support/mobile/ios/App/Resources/path-configuration.json +19 -0
  51. data/support/mobile/ios/App/SceneDelegate.swift +443 -0
  52. data/support/mobile/ios/App.xcodeproj/project.pbxproj +454 -0
  53. data/support/mobile/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  54. data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +14 -0
  55. data/support/mobile/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +77 -0
  56. data/support/mobile/ios/NativeExtensions/Package.swift +26 -0
  57. data/support/mobile/ios/NativeExtensions/Sources/NativeExtensions/Exports.swift +5 -0
  58. data/support/mobile/ios/README.md +66 -0
  59. metadata +35 -1
@@ -2,48 +2,211 @@
2
2
 
3
3
  require "dry/cli"
4
4
  require "socket"
5
+ require "shellwords"
5
6
  require_relative "../shellout"
6
7
  require_relative "../paths"
8
+ require_relative "../builders/ios"
9
+ require_relative "../simulator"
10
+ require_relative "logs"
7
11
 
8
12
  module Everywhere
9
13
  module Commands
10
- # Run the app's normal dev server and open the desktop shell pointed at it.
11
- # No packaging: the shell skips the sidecar when NATIVE_DEV_URL is set,
12
- # so the edit-refresh loop is untouched Rails/Hanami.
14
+ # Run the app's normal dev server and open native shells pointed at it.
15
+ # No packaging: the desktop shell skips the sidecar when NATIVE_DEV_URL is
16
+ # set, and the iOS shell reads EVERYWHERE_DEV_URL at launch — so the
17
+ # edit-refresh loop is untouched Rails/Hanami either way.
18
+ #
19
+ # Target flags are additive (--desktop --ios --browser opens all three);
20
+ # with no flags the server boots and a key menu launches shells on demand.
13
21
  class Dev < Dry::CLI::Command
14
- desc "Run the dev server inside the desktop shell (live reload, no packaging)"
22
+ desc "Run the dev server with native shells on demand (live reload, no packaging)"
15
23
 
16
24
  option :port, default: "3000", desc: "Dev server port"
17
25
  option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
26
+ option :target, default: nil, desc: "Single shell to open: macos (desktop) or ios (Simulator)"
27
+ option :desktop, type: :boolean, default: false, desc: "Open the desktop shell"
28
+ option :ios, type: :boolean, default: false, desc: "Open the iOS Simulator shell"
29
+ option :browser, type: :boolean, default: false, desc: "Open the app in the default browser"
18
30
 
19
- def call(port: "3000", shell_dir: nil, **)
20
- framework = Framework.detect
21
- config = Everywhere::Config.load(framework.root)
22
- shell_dir ||= Everywhere::Paths.shell_dir!
31
+ def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, browser: false, **)
32
+ @shell_dir = shell_dir
33
+ targets = requested_targets(target, desktop: desktop, ios: ios, browser: browser)
34
+ framework, @config = detect_app
35
+
36
+ start_server(framework, port)
37
+ @dev_url = if framework
38
+ "http://127.0.0.1:#{port}#{@config.entry_path}"
39
+ else
40
+ "#{@config.remote_url}#{@config.entry_path}"
41
+ end
42
+
43
+ # Headless (non-tty) runs can't take keystrokes, so keep the old
44
+ # default of the desktop shell.
45
+ targets << "desktop" if targets.empty? && !$stdin.tty?
46
+ targets.each { |t| launch(t) }
47
+
48
+ $stdin.tty? ? interact : wait_for_children
49
+ rescue Interrupt
50
+ nil
51
+ ensure
52
+ teardown
53
+ end
54
+
55
+ private
56
+
57
+ def requested_targets(target, desktop:, ios:, browser:)
58
+ targets = []
59
+ targets << "desktop" if desktop || (target && Everywhere::Config.os_of(target) != "ios")
60
+ targets << "ios" if ios || (target && Everywhere::Config.os_of(target) == "ios")
61
+ targets << "browser" if browser
62
+ targets
63
+ end
64
+
65
+ # The framework (nil for remote-only roots) and config. A root with
66
+ # neither a framework nor remote.url is a genuine error.
67
+ def detect_app
68
+ framework = begin
69
+ Framework.detect
70
+ rescue Everywhere::Error
71
+ nil
72
+ end
73
+ config = Everywhere::Config.load(framework ? framework.root : Dir.pwd)
74
+ unless framework || config.remote?
75
+ UI.die!("no supported framework here (Rails, Hanami, Sinatra) and no remote.url in " \
76
+ "config/everywhere.yml — nothing to run")
77
+ end
78
+ [framework, config]
79
+ end
80
+
81
+ def start_server(framework, port)
82
+ unless framework
83
+ # Remote-mode app with no local code (just everywhere.yml): nothing
84
+ # to serve, point the shells straight at the deployed app.
85
+ UI.step("#{UI.bold("remote")} mode → #{UI.cyan(@config.remote_url)} #{UI.dim("(no local server)")}")
86
+ return
87
+ end
23
88
 
24
- server_pid = nil
25
89
  if port_open?(port)
26
90
  UI.warn "something is ALREADY RUNNING on port #{port} — attaching to it instead of starting"
27
91
  UI.warn "#{UI.dim("your Procfile.dev/watchers are NOT running; stop that server or use --port if this isn't yours")}"
28
92
  else
29
93
  UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
30
- server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
94
+ @server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
95
+ @server_thread = Process.detach(@server_pid)
31
96
  wait_for_port(port)
32
97
  end
98
+ end
99
+
100
+ def launch(target)
101
+ case target
102
+ when "ios" then launch_ios
103
+ when "browser" then launch_browser
104
+ else launch_desktop
105
+ end
106
+ end
107
+
108
+ def launch_desktop
109
+ if @desktop_pid && alive?(@desktop_pid)
110
+ UI.warn "desktop shell already running"
111
+ return
112
+ end
113
+ dir = @shell_dir || Everywhere::Paths.shell_dir!
114
+ UI.step("opening desktop shell → #{UI.cyan(@dev_url)}")
115
+ @desktop_pid = Shellout.spawn({ "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
116
+ "CARGO_TARGET_DIR" => Everywhere::Paths.cargo_target_dir },
117
+ "cargo run", chdir: dir)
118
+ @desktop_thread = Process.detach(@desktop_pid)
119
+ end
120
+
121
+ # Build the shell (Debug, warm after the first run), put it on a booted
122
+ # Simulator and hand it the dev URL. The Simulator shares the Mac's
123
+ # loopback, so 127.0.0.1 works as-is; the template's ATS exception
124
+ # covers plain http for local networking. Pressing i again rebuilds and
125
+ # relaunches, so it doubles as a native-shell reload.
126
+ def launch_ios
127
+ app = Builders::Ios.new(config: @config, root: @config.root, target: "ios-arm64")
128
+ .build!(dist_dir: File.expand_path("dist", @config.root), dev: true)
33
129
 
34
- dev_url = "http://127.0.0.1:#{port}#{config.entry_path}"
35
- UI.step("opening desktop shell → #{UI.cyan(dev_url)}")
36
- Shellout.run!({ "NATIVE_DEV_URL" => dev_url, "NATIVE_CONFIG" => config.to_shell_json,
37
- "CARGO_TARGET_DIR" => Everywhere::Paths.cargo_target_dir },
38
- "cargo", "run", chdir: shell_dir)
130
+ udid = Simulator.boot_default!
131
+ UI.step("installing #{UI.bold(File.basename(app))}")
132
+ Simulator.install(udid, app)
133
+ UI.step("launching #{UI.cyan(@dev_url)}")
134
+ Simulator.launch(udid, @config.bundle_id(target: "ios-arm64"), env: { "EVERYWHERE_DEV_URL" => @dev_url })
135
+ end
136
+
137
+ def launch_browser
138
+ UI.step("opening browser → #{UI.cyan(@dev_url)}")
139
+ opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
140
+ Shellout.run?(opener, @dev_url, quiet: true)
141
+ end
142
+
143
+ # Toggle the `every logs --ios` stream, interleaved with the server
144
+ # output on this same terminal.
145
+ def toggle_logs
146
+ if @logs_pid && alive?(@logs_pid)
147
+ stop(@logs_pid)
148
+ @logs_pid = nil
149
+ UI.step("stopped iOS Simulator logs")
150
+ return
151
+ end
152
+
153
+ udid = Simulator.booted_udid
154
+ unless udid
155
+ UI.warn "no booted iOS Simulator — press #{UI.bold("i")} first"
156
+ return
157
+ end
158
+ UI.step("watching iOS Simulator logs #{UI.dim("(press l again to stop)")}")
159
+ @logs_pid = Shellout.spawn({}, Logs.new.argv(udid).shelljoin, chdir: Dir.pwd)
160
+ end
161
+
162
+ # Key menu. io/console's getch holds full raw mode while blocked, which
163
+ # clears OPOST and stair-steps every log line the server prints; stty
164
+ # -icanon -echo disables only line buffering and echo, leaving output
165
+ # processing (and Ctrl-C as SIGINT) intact.
166
+ def interact
167
+ UI.note("press #{UI.bold("d")} desktop · #{UI.bold("i")} iOS Simulator · #{UI.bold("b")} browser · " \
168
+ "#{UI.bold("l")} iOS logs · #{UI.bold("q")} quit", marker: "⌨", color: :cyan)
169
+ saved_tty = `stty -g`.chomp
170
+ system("stty", "-icanon", "-echo")
171
+ loop do
172
+ case $stdin.getc
173
+ when "d" then launch("desktop")
174
+ when "i" then launch("ios")
175
+ when "b" then launch("browser")
176
+ when "l" then toggle_logs
177
+ when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
178
+ end
179
+ end
39
180
  ensure
40
- if server_pid
41
- Process.kill("TERM", -Process.getpgid(server_pid)) rescue Process.kill("TERM", server_pid) rescue nil
42
- Process.wait(server_pid) rescue nil
181
+ system("stty", saved_tty) unless saved_tty.to_s.empty?
182
+ end
183
+
184
+ def wait_for_children
185
+ (@server_thread || @desktop_thread)&.join
186
+ end
187
+
188
+ def teardown
189
+ [@server_pid, @desktop_pid, @logs_pid].compact.each { |pid| stop(pid) }
190
+ end
191
+
192
+ def stop(pid)
193
+ return unless alive?(pid)
194
+
195
+ begin
196
+ Process.kill("TERM", -Process.getpgid(pid))
197
+ rescue StandardError
198
+ Process.kill("TERM", pid) rescue nil
43
199
  end
44
200
  end
45
201
 
46
- private
202
+ def alive?(pid)
203
+ Process.kill(0, pid)
204
+ true
205
+ rescue Errno::ESRCH
206
+ false
207
+ rescue Errno::EPERM
208
+ true
209
+ end
47
210
 
48
211
  def port_open?(port)
49
212
  TCPSocket.new("127.0.0.1", Integer(port)).close
@@ -2,19 +2,27 @@
2
2
 
3
3
  require "dry/cli"
4
4
  require_relative "../shellout"
5
+ require_relative "../paths"
5
6
 
6
7
  module Everywhere
7
8
  module Commands
8
- # Check that this machine can build desktop apps.
9
+ # Check that this machine can build native apps. Desktop checks always
10
+ # run; the iOS section joins in when the app's build.targets include an
11
+ # ios target, or explicitly via --target ios-*.
9
12
  class Doctor < Dry::CLI::Command
10
- desc "Check the toolchain (rbe-tebako, rust, brew deps)"
13
+ desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS targets)"
11
14
 
12
- def call(**)
15
+ option :root, default: ".", desc: "App root (its build.targets pick the sections)"
16
+ option :target, default: nil, desc: "Also check a specific target's toolchain (e.g. ios-arm64)"
17
+
18
+ def call(root: ".", target: nil, **)
13
19
  ok = true
14
20
  ok &= check("rbe-tebako installed") { Shellout.run?("rbe-tebako help > /dev/null 2>&1") }
15
21
  ok &= check("cargo installed") { Shellout.run?("cargo --version > /dev/null 2>&1") }
16
22
  ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
17
23
 
24
+ ok &= ios_checks if ios_wanted?(root, target)
25
+
18
26
  puts
19
27
  ok ? UI.success("ready to build") : UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
20
28
  exit(1) unless ok
@@ -22,6 +30,40 @@ module Everywhere
22
30
 
23
31
  private
24
32
 
33
+ def ios_wanted?(root, target)
34
+ return true if target && Everywhere::Config.os_of(target) == "ios"
35
+
36
+ Everywhere::Config.load(File.expand_path(root)).targets
37
+ .any? { |t| Everywhere::Config.os_of(t) == "ios" }
38
+ rescue StandardError
39
+ false
40
+ end
41
+
42
+ def ios_checks
43
+ puts
44
+ UI.step("iOS target")
45
+ ok = check("full Xcode selected") { Shellout.run?("xcodebuild -version > /dev/null 2>&1") }
46
+ ok &= check("iOS Simulator SDK") do
47
+ Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
48
+ end
49
+ # A runtime matching the SDK's iOS version specifically: a half-installed
50
+ # platform (download interrupted mid-registration) leaves older runtimes
51
+ # in place and fails builds with "No simulator runtime version … available"
52
+ # / "Platform Not Installed". `xcodebuild -downloadPlatform iOS` fixes it.
53
+ ok &= check("Simulator runtime matches the iOS SDK (xcodebuild -downloadPlatform iOS)") do
54
+ sdk, = Shellout.capture("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version")
55
+ version = sdk.strip[/\A\d+\.\d+/]
56
+ # `runtime list` shows the disk images; `list runtimes` can lag on
57
+ # lazily-mounted ones, so accept a Ready image OR a registered runtime.
58
+ images, = Shellout.capture("xcrun", "simctl", "runtime", "list")
59
+ runtimes, = Shellout.capture("xcrun", "simctl", "list", "runtimes")
60
+ !version.nil? && (images.match?(/iOS #{Regexp.escape(version)}.*\(Ready\)/) ||
61
+ runtimes.include?("iOS #{version}"))
62
+ end
63
+ ok &= check("bundled iOS shell template") { !Everywhere::Paths.ios_dir.nil? }
64
+ ok
65
+ end
66
+
25
67
  def check(label)
26
68
  passed = yield
27
69
  passed ? UI.ok(label) : UI.bad(label)
@@ -36,6 +36,11 @@ module Everywhere
36
36
  Everywhere::Icon.write_icns(master, File.join(out, "AppIcon.icns"))
37
37
  UI.ok("macOS icon AppIcon.icns")
38
38
 
39
+ background = ios_background(root_path)
40
+ Everywhere::Icon.write_ios_appiconset(source, File.join(out, "ios", "AppIcon.appiconset"),
41
+ background: background)
42
+ UI.ok("iOS icon ios/AppIcon.appiconset #{UI.dim("(full-bleed 1024px, alpha flattened)")}")
43
+
39
44
  Everywhere::Icon.write_ico(source, File.join(out, "icon.ico"))
40
45
  UI.ok("Windows icon icon.ico")
41
46
 
@@ -57,6 +62,15 @@ module Everywhere
57
62
  default = File.join(root_path, "icon.png")
58
63
  File.exist?(default) ? default : nil
59
64
  end
65
+
66
+ # The iOS icon's flatten backdrop: the app's light background color when
67
+ # set, else Icon's white default.
68
+ def ios_background(root_path)
69
+ color = Everywhere::Config.load(root_path).background_color
70
+ Everywhere::Icon.hex_rgb(color && color["light"])
71
+ rescue StandardError
72
+ nil
73
+ end
60
74
  end
61
75
  end
62
76
  end
@@ -66,13 +66,15 @@ module Everywhere
66
66
  # Rack frameworks vary too much to auto-edit their boot files safely, so we
67
67
  # vendor the bridge and print the remaining wiring rather than guess.
68
68
  def vendor_bridge_to_public
69
- source = File.expand_path("../../../bridge/everywhere/bridge.js", __dir__)
70
69
  target_dir = app_file("public")
71
70
  FileUtils.mkdir_p(target_dir)
72
- target = File.join(target_dir, "bridge.js")
73
- fresh = !File.exist?(target) || File.read(target) != File.read(source)
74
- FileUtils.cp(source, target) if fresh
75
- change("vendor @rubyeverywhere/bridge to public/bridge.js", fresh)
71
+ %w[bridge.js native.css].each do |name|
72
+ source = File.expand_path("../../../bridge/everywhere/#{name}", __dir__)
73
+ target = File.join(target_dir, name)
74
+ fresh = !File.exist?(target) || File.read(target) != File.read(source)
75
+ FileUtils.cp(source, target) if fresh
76
+ change("vendor @rubyeverywhere/bridge to public/#{name}", fresh)
77
+ end
76
78
  end
77
79
 
78
80
  def guide_rack_manual_steps(db:, boot:, script:)
@@ -130,6 +132,13 @@ module Everywhere
130
132
  light: "#FFFFFF"
131
133
  dark: "#1C1B1A"
132
134
 
135
+ # Native permissions the app may request — only declared ones can prompt.
136
+ # camera/location values are the sentence iOS shows when asking (mandatory).
137
+ # permissions:
138
+ # notifications: true
139
+ # camera: "Scan QR codes to pair devices."
140
+ # location: "Find things near you."
141
+
133
142
  # Native menu-bar items — each one visits a route in your app (no native code).
134
143
  # menu:
135
144
  # - label: Settings…
@@ -149,12 +158,34 @@ module Everywhere
149
158
  # - label: Quit
150
159
  # action: quit
151
160
 
161
+ # Mobile tab bar (iOS today, Android soon). Served live from
162
+ # /everywhere/ios_v1.json too, so tab changes deploy with the app —
163
+ # no app-store release. Icons: ios = SF Symbol name.
164
+ # tabs:
165
+ # - name: Home
166
+ # path: /
167
+ # icons:
168
+ # ios: house
169
+ # - name: Settings
170
+ # path: /settings
171
+ # icons:
172
+ # ios: gear
173
+
174
+ # iOS-specific knobs (and native Swift you compile into the shell —
175
+ # see /docs/diy/native-extensions).
176
+ # native:
177
+ # ios:
178
+ # lazy_load_tabs: true # defer each tab's first load until it's tapped
179
+ # # (false = load every tab up front; omit = default)
180
+
152
181
  # Durable build knobs (CLI flags still override per-run).
153
182
  # build:
154
183
  # ruby: "4.0.6"
155
184
  # targets: # the platforms you ship — the CI matrix, expressed once
156
185
  # - macos-arm64
157
- # # - ios-arm64 # future mobile is remote mode only
186
+ # # - ios-arm64 # Hotwire Native shell; remote mode only. Local builds
187
+ # # # (`every build` / `every dev --ios`) work today;
188
+ # # # hosted platform builds for iOS aren't live yet.
158
189
  # # - android-arm64 # future — mobile is remote mode only
159
190
  # permissions: [notifications, filesystem:read, clipboard]
160
191
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require_relative "../simulator"
5
+ require_relative "../ui"
6
+
7
+ module Everywhere
8
+ module Commands
9
+ # Tail the native shell's logs. iOS only so far: the Simulator's unified
10
+ # log, scoped to the shell process. Watches (log stream) by default;
11
+ # --last replays recent history (log show) and exits.
12
+ class Logs < Dry::CLI::Command
13
+ desc "Stream native shell logs (iOS Simulator; watches by default)"
14
+
15
+ option :ios, type: :boolean, default: false, desc: "iOS Simulator logs (the only target so far)"
16
+ option :last, default: nil, desc: 'Show recent history instead of watching (e.g. "5m", "1h")'
17
+ option :udid, default: nil, desc: "Simulator UDID (default: the booted one)"
18
+ option :verbose, type: :boolean, default: false, desc: "Everything the shell process logs, debug level included"
19
+
20
+ # The template contract fixes the product name (App.app), so the shell's
21
+ # process on the Simulator is always "App" — one predicate covers every
22
+ # Everywhere app without knowing which one is installed. Scoping to the
23
+ # process alone still yields ~100k lines/half hour of Apple-framework
24
+ # chatter (WebKit resource loads, activities); the default predicate
25
+ # keeps what a shell developer logs — NSLog (Foundation sender, no
26
+ # subsystem), Hotwire Native's subsystem, UIKit warnings — and drops the
27
+ # rest. --verbose falls back to the whole process.
28
+ PREDICATE = 'process == "App" AND eventType == logEvent AND NOT subsystem BEGINSWITH "com.apple"'
29
+ VERBOSE_PREDICATE = 'process == "App"'
30
+
31
+ def call(ios: false, last: nil, udid: nil, verbose: false, **)
32
+ UI.die!("only the iOS Simulator is supported so far — run: every logs --ios") unless ios
33
+
34
+ udid ||= Everywhere::Simulator.booted_udid
35
+ UI.die!("no booted iOS Simulator — start one with `every dev --ios`") unless udid
36
+
37
+ UI.step(last ? "iOS Simulator logs from the last #{UI.bold(last)}"
38
+ : "watching iOS Simulator logs #{UI.dim("(Ctrl-C to stop)")}")
39
+ exec(*argv(udid, last: last, verbose: verbose))
40
+ end
41
+
42
+ # simctl spawn runs the `log` tool inside the Simulator runtime — the
43
+ # supported way to read a Simulator's unified log from the host. `log show`
44
+ # spells verbosity --info/--debug where `log stream` takes --level.
45
+ def argv(udid, last: nil, verbose: false)
46
+ argv = ["xcrun", "simctl", "spawn", udid, "log"]
47
+ argv += if last
48
+ ["show", "--last", last, *(verbose ? ["--info", "--debug"] : [])]
49
+ else
50
+ ["stream", *(verbose ? ["--level", "debug"] : [])]
51
+ end
52
+ argv + ["--style", "compact", "--predicate", verbose ? VERBOSE_PREDICATE : PREDICATE]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -49,7 +49,7 @@ module Everywhere
49
49
  ).manifest
50
50
 
51
51
  build = submit(client, targets, channel, signed_id, manifest)
52
- UI.success("submitted #{UI.bold(build["id"])} for #{targets.join(", ")}")
52
+ UI.success("submitted #{UI.bold(build["id"])} for #{targets.map { |t| UI.target_label(t) }.join(", ")}")
53
53
  if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
54
54
  UI.substep("watch it live → #{UI.cyan(build_url)}")
55
55
  end
@@ -98,7 +98,7 @@ module Everywhere
98
98
  UI.bad("can't build yet — missing signing credentials:")
99
99
  Array(res.body["targets"]).each do |t|
100
100
  tgt = t["target"] || {}
101
- UI.step("#{tgt["os"]}-#{tgt["arch"]}/#{tgt["distribution_channel"]}: add #{Array(t["missing"]).join(", ")}")
101
+ UI.step("#{UI.os_label(tgt["os"])}-#{tgt["arch"]}/#{tgt["distribution_channel"]}: add #{Array(t["missing"]).join(", ")}")
102
102
  end
103
103
  UI.step("set them up → #{UI.cyan(res.body["manage_url"])}")
104
104
  exit 1
@@ -232,7 +232,7 @@ module Everywhere
232
232
  end
233
233
 
234
234
  def label(target)
235
- target ? "#{target["os"]}-#{target["arch"]}" : "target"
235
+ target ? "#{UI.os_label(target["os"])}-#{target["arch"]}" : "target"
236
236
  end
237
237
 
238
238
  def terminal?(status) = %w[succeeded failed].include?(status)
@@ -43,7 +43,7 @@ module Everywhere
43
43
 
44
44
  job = get!("/runner/artifacts/#{artifact}")
45
45
  patch!("/runner/artifacts/#{artifact}", status: "running")
46
- remote_log("picked up #{UI.cyan("#{job.dig("target", "os")}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
46
+ remote_log("picked up #{UI.cyan("#{UI.os_label(job.dig("target", "os"))}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
47
47
 
48
48
  Dir.mktmpdir("every-runner") do |work|
49
49
  src = fetch_snapshot(work, job)
@@ -49,7 +49,7 @@ module Everywhere
49
49
  # 1. Produce (or reuse) the signed-for-dev .app via the build path. The
50
50
  # notarize step below re-signs it inside-out with the Developer ID.
51
51
  unless skip_build
52
- UI.phase("Building #{app_name} for #{target}")
52
+ UI.phase("Building #{app_name} for #{UI.target_label(target)}")
53
53
  Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
54
54
  end
55
55
 
@@ -4,14 +4,21 @@ require "dry/cli"
4
4
 
5
5
  module Everywhere
6
6
  module Commands
7
- # Print the absolute path of the gem-bundled Tauri shell (src-tauri).
7
+ # Print the absolute path of the gem-bundled shell for a target's os.
8
8
  # Output is the bare path so external tooling can capture it — e.g. the
9
9
  # build-runner workflows resolve the shell with `$(every shell-dir)`.
10
+ # Default stays the desktop Tauri shell (src-tauri) so existing callers
11
+ # keep working; `--target ios` prints the Hotwire Native template instead.
10
12
  class ShellDir < Dry::CLI::Command
11
- desc "Print the path of the bundled Tauri shell (src-tauri)"
13
+ desc "Print the path of the bundled shell (Tauri src-tauri, or the iOS template)"
12
14
 
13
- def call(**)
14
- puts Everywhere::Paths.shell_dir!
15
+ option :target, default: "macos-arm64", desc: "Build target (os or os-arch) selecting which shell"
16
+
17
+ def call(target: "macos-arm64", **)
18
+ case Everywhere::Config.os_of(target)
19
+ when "ios" then puts Everywhere::Paths.ios_dir!
20
+ else puts Everywhere::Paths.shell_dir!
21
+ end
15
22
  end
16
23
  end
17
24
  end