ruby_everywhere 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19d066bb2584933621929b270c69e6205804d07441a6e4a8bfcc052cc6483e55
4
- data.tar.gz: c337dfa569b91412f4cb03027006d3199c9f224ebe63e5c5a066396986729194
3
+ metadata.gz: 0e9efcbd721dfa8ec5eb10adcbd9d96970feb860b9bfa20d3d6b761012bb8d1b
4
+ data.tar.gz: 66a196953da3501c430aa91318d02cc51fc42ff4ebfe294d33a58c44cf7d5333
5
5
  SHA512:
6
- metadata.gz: b0865c4cc65cfdc86706332f2d00c704fcd361ffe72ed6bbc51100297640634d246a3537b2f4ed38318fbacacbbd490ff422e4b6449905d06918ce7663dbc2f5
7
- data.tar.gz: 119672c4cf55ea3ea05e768c3eba161ee2aa6b81b6f5f68046c6327bc99f2dcd9897658d7320e0a0d7a58cbc64d8ac457a2522d6cd71434f1df8ef974367a783
6
+ metadata.gz: 0644e199416ba7568a78812a83ba97fda7b97a829bca821540ba03f355b5798b2e69765db1aa5bd498e0f537edff73d8cad34ba0ce855261338a1522421b60ef
7
+ data.tar.gz: 9fb95b8c424cbcc6c82148f4bab93fd07d243e5c2051a0b7b5b056a3ded8fc2a8f43f5e6730b3791edd53d249dde25e001ff2d5d729e4c6a216d44831448cb6b
@@ -4,11 +4,13 @@ require "dry/cli"
4
4
  require_relative "../everywhere"
5
5
  require_relative "framework"
6
6
  require_relative "commands/build"
7
+ require_relative "commands/clean"
7
8
  require_relative "commands/dev"
8
9
  require_relative "commands/icon"
9
10
  require_relative "commands/doctor"
10
11
  require_relative "commands/install"
11
12
  require_relative "commands/release"
13
+ require_relative "commands/shell_dir"
12
14
  require_relative "commands/platform/login"
13
15
  require_relative "commands/platform/logout"
14
16
  require_relative "commands/platform/auth_status"
@@ -34,8 +36,10 @@ module Everywhere
34
36
  register "install", Commands::Install
35
37
  register "dev", Commands::Dev
36
38
  register "build", Commands::Build
39
+ register "clean", Commands::Clean
37
40
  register "icon", Commands::Icon
38
41
  register "release", Commands::Release
42
+ register "shell-dir", Commands::ShellDir
39
43
 
40
44
  register "platform login", Commands::PlatformLogin
41
45
  register "platform logout", Commands::PlatformLogout
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "tmpdir"
6
6
  require "shellwords"
7
7
  require_relative "../shellout"
8
+ require_relative "../log_filter"
8
9
  require_relative "../png"
9
10
  require_relative "../icon"
10
11
  require_relative "../paths"
@@ -89,11 +90,11 @@ module Everywhere
89
90
  staging = File.join(Dir.mktmpdir("everywhere-press"), File.basename(output_path))
90
91
  log = "#{output_path}.build.log"
91
92
 
92
- UI.step("rbe-tebako press (Ruby #{ruby}) #{UI.dim("— grab a coffee on first run")}")
93
- UI.step(UI.dim("full build log: #{log}"))
93
+ UI.step("rbe-tebako press #{UI.dim("(Ruby #{ruby} — grab a coffee on first run)")}")
94
+ UI.detail("full build log: #{UI.short_path(log)}")
94
95
  Shellout.run_logged!(press_env, ["rbe-tebako", "press",
95
96
  "--root=#{framework.root}", "--entry-point=#{entry}",
96
- "--Ruby=#{ruby}", "--output=#{staging}"], log: log)
97
+ "--Ruby=#{ruby}", "--output=#{staging}"], log: log, filter: LogFilter.for(:tebako))
97
98
 
98
99
  # Tebako's final strip step only warns (and still exits 0) if it can't
99
100
  # write the output, so verify the artifact instead of trusting exit codes.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "fileutils"
5
+
6
+ module Everywhere
7
+ module Commands
8
+ # Wipes the shared cargo build cache under ~/.rubyeverywhere (or
9
+ # $RUBYEVERYWHERE_HOME). Safe to run any time: only compiled shell output
10
+ # lives there, so the next `every dev`/`every build` just recompiles cold.
11
+ class Clean < Dry::CLI::Command
12
+ desc "Remove the cargo shell-build cache (~/.rubyeverywhere/shell-target)"
13
+
14
+ def call(**)
15
+ target = Everywhere::Paths.cargo_target_dir
16
+ return UI.step("nothing to clean #{UI.dim("(#{target} doesn't exist)")}") unless File.exist?(target)
17
+
18
+ size = human_size(dir_bytes(target))
19
+ FileUtils.rm_rf(target)
20
+ UI.success("removed #{target} (#{size} freed) — the next build recompiles the shell cold")
21
+ end
22
+
23
+ private
24
+
25
+ def dir_bytes(dir)
26
+ Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH).sum do |f|
27
+ File.file?(f) ? File.size(f) : 0
28
+ end
29
+ end
30
+
31
+ def human_size(bytes)
32
+ return "#{bytes} B" if bytes < 1024
33
+
34
+ units = %w[KB MB GB TB]
35
+ exp = [(Math.log(bytes) / Math.log(1024)).floor, units.size].min
36
+ format("%.1f %s", bytes.to_f / (1024**exp), units[exp - 1])
37
+ end
38
+ end
39
+ end
40
+ end
@@ -7,7 +7,7 @@ module Everywhere
7
7
  module Commands
8
8
  # Check that this machine can build desktop apps.
9
9
  class Doctor < Dry::CLI::Command
10
- desc "Check the toolchain (tebako, rust, brew deps, Xcode 26 shims)"
10
+ desc "Check the toolchain (rbe-tebako, rust, brew deps)"
11
11
 
12
12
  def call(**)
13
13
  ok = true
@@ -15,23 +15,8 @@ module Everywhere
15
15
  ok &= check("cargo installed") { Shellout.run?("cargo --version > /dev/null 2>&1") }
16
16
  ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
17
17
 
18
- if RUBY_PLATFORM.include?("darwin") && clang_major >= 21
19
- ok &= check("Xcode #{clang_major} CXX shim (~/.tebako-shims/clang++)") do
20
- File.executable?(File.expand_path("~/.tebako-shims/clang++"))
21
- end
22
- ok &= check("tebako force_ruby_platform patch") do
23
- helper = `gem contents rbe-tebako 2>/dev/null`.lines.map(&:strip).find { |l| l.end_with?("deploy_helper.rb") }
24
- helper.nil? || File.read(helper).include?(%(@force_ruby_platform = "true"
25
- @nokogiri_option = "--no-use-system-libraries"))
26
- end
27
- ok &= check("tebako rbconfig warnflags patch") do
28
- stash = Dir[File.expand_path("~/.tebako/deps/stash_*/lib/ruby/*/**/rbconfig.rb")]
29
- stash.empty? || stash.all? { |f| File.read(f).include?("default-const-init") }
30
- end
31
- end
32
-
33
18
  puts
34
- ok ? UI.success("ready to build") : UI.bad("fix the items above (see project docs / memory for recipes)")
19
+ ok ? UI.success("ready to build") : UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
35
20
  exit(1) unless ok
36
21
  end
37
22
 
@@ -45,10 +30,6 @@ module Everywhere
45
30
  UI.bad(label)
46
31
  false
47
32
  end
48
-
49
- def clang_major
50
- @clang_major ||= `clang --version 2>/dev/null`[/version (\d+)/, 1].to_i
51
- end
52
33
  end
53
34
  end
54
35
  end
@@ -12,15 +12,17 @@ module Everywhere
12
12
  desc "Prepare this app for RubyEverywhere (boot stub, config/everywhere.yml, config tweaks)"
13
13
 
14
14
  option :root, default: ".", desc: "App root directory"
15
- option :path, default: nil, desc: "Use a local checkout of ruby_everywhere in the Gemfile (path:)"
15
+ option :local, type: :boolean, default: false, desc: "Configure local mode (compile & ship the app) instead of remote mode"
16
16
 
17
- def call(root: ".", path: nil, **)
17
+ def call(root: ".", local: false, **)
18
+ @mode = local ? "local" : "remote"
18
19
  @framework = Framework.detect(root)
19
20
  @root = @framework.root
20
21
  UI.step("#{UI.bold(@framework.name)} app at #{@root}")
22
+ UI.warn("local mode: only SQLite databases are supported") if local
21
23
 
22
24
  # Common to every framework.
23
- add_gem(path)
25
+ add_gem
24
26
  write_config
25
27
  write_boot_stub
26
28
 
@@ -86,13 +88,12 @@ module Everywhere
86
88
  made ? UI.ok(label) : UI.step("#{label} #{UI.dim("(already done, skipped)")}")
87
89
  end
88
90
 
89
- def add_gem(path)
91
+ def add_gem
90
92
  gemfile = app_file("Gemfile")
91
93
  contents = File.read(gemfile)
92
94
  return change("add ruby_everywhere to Gemfile", false) if contents.match?(/gem ["']ruby_everywhere["']/)
93
95
 
94
- line = path ? "gem \"ruby_everywhere\", path: #{path.inspect}" : "gem \"ruby_everywhere\""
95
- File.write(gemfile, "#{contents.chomp}\n\n# Desktop apps from this #{@framework.name.capitalize} app — https://rubyeverywhere.com\n#{line}\n")
96
+ File.write(gemfile, "#{contents.chomp}\n\n# Desktop apps from this #{@framework.name.capitalize} app https://rubyeverywhere.com\ngem \"ruby_everywhere\"\n")
96
97
  Shellout.run!({}, "bundle", "install", "--quiet", chdir: @root)
97
98
  change("add ruby_everywhere to Gemfile", true)
98
99
  end
@@ -112,15 +113,12 @@ module Everywhere
112
113
  # platform (and the macOS bundle id). Set once, never change it.
113
114
  bundle_id: com.example.#{slug}
114
115
  version: "0.1.0" # marketing version (CFBundleShortVersionString)
115
- mode: local # "local" (compile & ship the app) or "remote" (native shell around a deployed site)
116
+ mode: #{@mode.ljust(6)} # "local" (compile & ship the app) or "remote" (native shell around a deployed site)
116
117
  entry_path: / # initial url on app launch
117
118
  # icon: icon.png # PNG used to generate app icons (defaults to icon.png at the app root if present)
118
119
  # splash: splash.html # custom boot splash (HTML); window.__EVERYWHERE_CONFIG__ carries name/colors
119
120
 
120
- # Remote mode only — the deployed URL the native shell opens.
121
- # Set mode: remote above, then uncomment:
122
- # remote:
123
- # url: https://example.com
121
+ #{remote_section.chomp}
124
122
 
125
123
  appearance:
126
124
  # Colors take a hex string, or split by system theme:
@@ -171,6 +169,25 @@ module Everywhere
171
169
  change("create config/everywhere.yml", true)
172
170
  end
173
171
 
172
+ # Remote mode (the default) gets a live remote: block ready to point at the
173
+ # deployed site; local mode keeps it commented out for later.
174
+ def remote_section
175
+ if @mode == "remote"
176
+ <<~YAML
177
+ # Remote mode — the deployed URL the native shell opens.
178
+ remote:
179
+ url: "https://example.com"
180
+ YAML
181
+ else
182
+ <<~YAML
183
+ # Remote mode only — the deployed URL the native shell opens.
184
+ # Set mode: remote above, then uncomment:
185
+ # remote:
186
+ # url: "https://example.com"
187
+ YAML
188
+ end
189
+ end
190
+
174
191
  def write_boot_stub
175
192
  stub = app_file("native_boot.rb")
176
193
  return change("create native_boot.rb", false) if File.exist?(stub)
@@ -50,6 +50,9 @@ module Everywhere
50
50
 
51
51
  build = submit(client, targets, channel, signed_id, manifest)
52
52
  UI.success("submitted #{UI.bold(build["id"])} for #{targets.join(", ")}")
53
+ if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
54
+ UI.substep("watch it live → #{UI.cyan(build_url)}")
55
+ end
53
56
 
54
57
  return unless wait
55
58
 
@@ -31,13 +31,18 @@ module Everywhere
31
31
  option :shell_dir, default: nil, desc: "Path to the generic Tauri shell (src-tauri)"
32
32
 
33
33
  def call(artifact:, token:, url: nil, shell_dir: nil, **)
34
+ # We run headless on a build runner (a pipe, not a TTY) and stream into
35
+ # the Platform web log, which renders ANSI. Force color on so the hosted
36
+ # log reads like a local run.
37
+ ENV["CLICOLOR_FORCE"] ||= "1" unless ENV["NO_COLOR"]
38
+
34
39
  base = Everywhere::Platform::Client.base_url(url)
35
40
  @client = Everywhere::Platform::Client.new(base, token: token)
36
41
  @artifact = artifact
37
42
 
38
43
  job = get!("/runner/artifacts/#{artifact}")
39
44
  patch!("/runner/artifacts/#{artifact}", status: "running")
40
- remote_log("runner: picked up #{job.dig("target", "os")}-#{job.dig("target", "arch")} for #{job.dig("app", "name")}")
45
+ remote_log("picked up #{UI.cyan("#{job.dig("target", "os")}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
41
46
 
42
47
  Dir.mktmpdir("every-runner") do |work|
43
48
  src = fetch_snapshot(work, job)
@@ -63,7 +68,7 @@ module Everywhere
63
68
  # ---- pipeline steps ------------------------------------------------------
64
69
 
65
70
  def fetch_snapshot(work, job)
66
- remote_log("runner: downloading source snapshot")
71
+ remote_log("downloading source snapshot")
67
72
  tarball = File.join(work, "snapshot.tar.gz")
68
73
  @client.download(job["snapshot_url"], tarball)
69
74
  src = File.join(work, "src")
@@ -73,7 +78,7 @@ module Everywhere
73
78
  end
74
79
 
75
80
  def pull_credentials(work)
76
- remote_log("runner: pulling signing credentials")
81
+ remote_log("pulling signing credentials")
77
82
  creds = get!("/runner/artifacts/#{@artifact}/credentials")["credentials"] || []
78
83
  env = {}
79
84
  creds.each do |cred|
@@ -113,19 +118,23 @@ module Everywhere
113
118
  listing = `security find-identity -p codesigning #{@keychain.shellescape} 2>/dev/null`
114
119
  sha = listing[/\b([0-9A-F]{40})\b/, 1]
115
120
  UI.die!("no codesigning identity in the delivered .p12") unless sha
116
- remote_log("runner: signing identity #{listing[/"(.+)"/, 1]} (#{sha[0, 10]}…)")
121
+ remote_log("signing identity #{listing[/"(.+)"/, 1]} #{UI.dim("(#{sha[0, 10]}…)")}")
117
122
  sha
118
123
  end
119
124
 
120
125
  def run_release(src, job, signing_env, shell_dir)
121
- remote_log("runner: every release (press → sign → notarize → staple)")
126
+ remote_log("every release press → sign → notarize → staple")
122
127
  cmd = [RbConfig.ruby, "-I", lib_dir, every_exe, "release",
123
128
  "--root", src,
124
129
  "--target", "#{job.dig("target", "os")}-#{job.dig("target", "arch")}",
125
130
  "--channel", job.dig("target", "distribution_channel").to_s]
126
131
  cmd += ["--shell-dir", shell_dir] if shell_dir
127
132
 
128
- success = stream(signing_env, cmd)
133
+ # The web UI renders ANSI, but `every release` runs here as a pipe (not a
134
+ # TTY) so it would otherwise strip all color. Force it on — for our own
135
+ # output and for cargo — so the hosted log looks like the local one.
136
+ forced = { "CLICOLOR_FORCE" => "1", "CARGO_TERM_COLOR" => "always" }
137
+ success = stream(forced.merge(signing_env), cmd)
129
138
 
130
139
  dist = File.join(src, "dist")
131
140
  release_json = JSON.parse(File.read(File.join(dist, "release.json"))) rescue nil
@@ -134,7 +143,7 @@ module Everywhere
134
143
  end
135
144
 
136
145
  def complete_success(zip, release_json)
137
- remote_log("runner: uploading artifact (#{(File.size(zip) / 1024.0 / 1024.0).round(1)}MB)")
146
+ remote_log("uploading artifact #{UI.dim("(#{(File.size(zip) / 1024.0 / 1024.0).round(1)}MB)")}")
138
147
  signed_id = upload_artifact(zip)
139
148
  post!("/runner/artifacts/#{@artifact}/complete",
140
149
  status: "succeeded", file_signed_id: signed_id, release_json: release_json)
@@ -185,9 +194,13 @@ module Everywhere
185
194
  nil # never let a log hiccup kill the build
186
195
  end
187
196
 
197
+ # A runner-orchestration line: distinct from the `every release` steps it
198
+ # brackets (tagged "runner"), rendered once and forwarded verbatim — color
199
+ # and all — to the web log.
188
200
  def remote_log(message)
189
- UI.step(message)
190
- flush_log("#{message}\n")
201
+ line = "#{UI.gray("runner")} #{UI.cyan("→")} #{message}"
202
+ puts line
203
+ flush_log("#{line}\n")
191
204
  end
192
205
 
193
206
  # ---- helpers -------------------------------------------------------------
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "dry/cli"
4
4
  require_relative "../shellout"
5
+ require_relative "../log_filter"
5
6
  require_relative "../receipt"
6
7
  require_relative "../paths"
7
8
  require_relative "build"
@@ -48,7 +49,7 @@ module Everywhere
48
49
  # 1. Produce (or reuse) the signed-for-dev .app via the build path. The
49
50
  # notarize step below re-signs it inside-out with the Developer ID.
50
51
  unless skip_build
51
- UI.step("building #{UI.bold(app_name)} for #{UI.cyan(target)}")
52
+ UI.phase("Building #{app_name} for #{target}")
52
53
  Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
53
54
  end
54
55
 
@@ -58,10 +59,11 @@ module Everywhere
58
59
  # 2. Sign (Developer ID + hardened runtime) → notarize → staple → verify.
59
60
  env_file ||= discover_env_file(root_path)
60
61
  env = env_file ? { "ENV_FILE" => env_file } : {}
61
- UI.step("signing + notarizing #{File.basename(app_dir)}#{env_file ? " #{UI.dim("(#{rel(env_file, root_path)})")}" : ""}")
62
+ UI.phase("Signing & notarizing #{File.basename(app_dir)}")
63
+ UI.step("preparing #{File.basename(app_dir)}#{env_file ? " #{UI.dim("(env: #{rel(env_file, root_path)})")}" : ""}")
62
64
  UI.warn("no signing env found — set APPLE_* or pass --env-file (notarize.sh will fail without it)") unless env_file || ENV["APPLE_SIGNING_IDENTITY"]
63
65
  log = "#{app_dir}.notarize.log"
64
- Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log)
66
+ Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log, filter: LogFilter.for(:notarize))
65
67
 
66
68
  # 3. Gather signing facts from the bundle itself, then emit the receipt.
67
69
  artifact = "#{app_dir.sub(/\.app\z/, "")}.zip" # notarize.sh re-zips the stapled app
@@ -125,18 +127,16 @@ module Everywhere
125
127
  end
126
128
 
127
129
  def summarize(out, artifact, signing, root_path)
128
- UI.success("release ready")
129
- UI.step("artifact: #{rel(artifact, root_path)} (#{(File.size(artifact) / 1024.0 / 1024.0).round}MB)")
130
- UI.step("signed by #{signing["signed_by"] || "?"} · " \
131
- "notarized #{yn(signing.dig("platform", "notarized"))} · " \
132
- "stapled #{yn(signing.dig("platform", "stapled"))} · " \
133
- "gatekeeper #{yn(signing["verified"])}")
134
- UI.step("receipt: #{rel(out, root_path)}")
130
+ UI.phase("Release ready 🎉")
131
+ UI.success("#{rel(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB — send this to your friends)")}")
132
+ UI.substep("signed by #{UI.bold(signing["signed_by"] || "?")}")
133
+ UI.substep("notarized #{UI.yn(signing.dig("platform", "notarized"))} · " \
134
+ "stapled #{UI.yn(signing.dig("platform", "stapled"))} · " \
135
+ "gatekeeper #{UI.yn(signing["verified"])}")
136
+ UI.substep("receipt #{UI.dim(rel(out, root_path))}")
135
137
  end
136
138
 
137
- def yn(bool) = bool ? UI.green("yes") : UI.red("no")
138
-
139
- def rel(path, root_path) = path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", "")
139
+ def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
140
140
  end
141
141
  end
142
142
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ module Everywhere
6
+ module Commands
7
+ # Print the absolute path of the gem-bundled Tauri shell (src-tauri).
8
+ # Output is the bare path so external tooling can capture it — e.g. the
9
+ # build-runner workflows resolve the shell with `$(every shell-dir)`.
10
+ class ShellDir < Dry::CLI::Command
11
+ desc "Print the path of the bundled Tauri shell (src-tauri)"
12
+
13
+ def call(**)
14
+ puts Everywhere::Paths.shell_dir!
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ui"
4
+
5
+ module Everywhere
6
+ # Turns the raw, chatty output of the external build tools (tebako, the
7
+ # notarize script and the Apple tools it drives) into clean, indented, colored
8
+ # lines for the console and the Platform web log.
9
+ #
10
+ # A filter is a small state machine fed one raw line at a time. It returns:
11
+ # * a String -> print this (already colored/indented) instead of the raw line
12
+ # * :drop -> swallow the line (pure noise: repeated status, temp paths…)
13
+ # * nil -> not recognized; caller shows the raw line dimmed as a detail
14
+ #
15
+ # The FULL raw stream is always still written to the on-disk log, so nothing is
16
+ # lost — this only governs what a human sees scroll by.
17
+ class LogFilter
18
+ def self.for(profile) = new(profile)
19
+
20
+ def initialize(profile)
21
+ @profile = profile
22
+ @seen = {}
23
+ end
24
+
25
+ def call(raw)
26
+ line = raw.to_s.rstrip
27
+ return :drop if line.empty?
28
+
29
+ case @profile
30
+ when :notarize then notarize(line)
31
+ when :tebako then tebako(line)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # --- `every release` signing pipeline (notarize.sh + Apple tools) ---------
38
+
39
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
40
+ def notarize(line)
41
+ case line
42
+ # notarize.sh's own "==> phase" markers become substeps.
43
+ when /\A==> signing with:\s*(.+)/
44
+ UI.substep_line("signing bundle #{UI.dim("(#{short_identity(Regexp.last_match(1))})")}")
45
+ when /\A==> verifying signature/
46
+ UI.substep_line("verifying signature")
47
+ when /\A==> zipping for notarization:\s*(.+)/
48
+ UI.substep_line("zipping for notarization #{UI.dim("→ #{File.basename(Regexp.last_match(1))}")}")
49
+ when /\A==> submitting to Apple notary service/
50
+ UI.substep_line("submitting to Apple notary service #{UI.dim("(this can take a few minutes)")}")
51
+ when /\A==> stapling ticket/
52
+ UI.substep_line("stapling notarization ticket")
53
+ when /\A==> Gatekeeper assessment/
54
+ UI.substep_line("Gatekeeper assessment")
55
+ when /\A==> re-zipping/
56
+ UI.substep_line("re-zipping stapled app for distribution")
57
+
58
+ # Per-binary signing chatter -> one dim line each (basename only).
59
+ when /\A\s+sign (.+)/
60
+ UI.note_line("signed #{File.basename(Regexp.last_match(1))}")
61
+ when /: replacing existing signature/
62
+ :drop
63
+
64
+ # codesign --verify / stapler / spctl outcomes.
65
+ when /: valid on disk/
66
+ UI.note_line("valid on disk", marker: "✓", color: :green)
67
+ when /: satisfies its Designated Requirement/
68
+ :drop
69
+ when /The staple and validate action worked!/
70
+ UI.note_line("stapled and validated", marker: "✓", color: :green)
71
+ when /The validate action worked!/
72
+ :drop
73
+ when /\A(.+): accepted/
74
+ UI.note_line("Gatekeeper accepted", marker: "✓", color: :green)
75
+ when /\Asource=/, /\Aorigin=/
76
+ UI.note_line(line)
77
+
78
+ # notarytool --wait progress. Collapse the repeated dotted "In Progress"
79
+ # lines into a single heartbeat, and celebrate the terminal states.
80
+ when /Current status:\s*Accepted/
81
+ UI.note_line("notarization accepted", marker: "✓", color: :green)
82
+ when /Current status:\s*(Invalid|Rejected)/
83
+ UI.note_line("notarization #{Regexp.last_match(1).downcase}", marker: "✗", color: :red)
84
+ when /Current status:\s*In Progress/
85
+ once(:progress) { UI.note_line("waiting on Apple…", marker: "⋯") }
86
+ when /\A\s*id:\s*([0-9a-fA-F-]{36})/
87
+ once(:submission) { UI.note_line("submission #{Regexp.last_match(1)[0, 8]}…") }
88
+
89
+ # Pure noise from notarytool's own prose / the final banner (release.rb
90
+ # prints its own summary, so drop notarize.sh's).
91
+ when /\AConducting pre-submission checks/,
92
+ /\ASubmission ID received/,
93
+ /\ASuccessfully uploaded file/,
94
+ /\A\s*path:/,
95
+ /\A\s*status:/,
96
+ /\AWaiting for processing/,
97
+ /\AProcessing:/,
98
+ /Processing complete/,
99
+ /\A✅ Done\./,
100
+ /Send this to your friend/,
101
+ /Stapled, so it works even offline/,
102
+ /zipping preserves the signature/
103
+ :drop
104
+
105
+ else
106
+ nil # unrecognized -> caller shows it dimmed
107
+ end
108
+ end
109
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
110
+
111
+ # --- tebako press ---------------------------------------------------------
112
+ #
113
+ # Thousands of lines of C++/Ruby build output. We can't restructure it, but
114
+ # we can keep errors/warnings legible and dim the rest into the background so
115
+ # the important lines (and our own steps around it) stand out.
116
+ def tebako(line)
117
+ short = UI.short_path(line)
118
+ case line
119
+ when /\b(error|fatal|undefined reference|cannot find)\b/i
120
+ " #{UI.red(short)}"
121
+ when /\b(warning|deprecated)\b/i
122
+ " #{UI.yellow(short)}"
123
+ when /\A-- /, /\bBuilding\b/, /\bConfiguring\b/, /\bInstalling\b/
124
+ UI.note_line(short.sub(/\A-- /, ""))
125
+ else
126
+ " #{UI.gray(short)}"
127
+ end
128
+ end
129
+
130
+ # Show a line only the first time its key is seen; :drop the repeats. Keeps a
131
+ # long notarization from scrolling a wall of identical "waiting" lines (and
132
+ # notarytool's thrice-echoed submission id from showing three times).
133
+ def once(key)
134
+ return :drop if @seen[key]
135
+
136
+ @seen[key] = true
137
+ yield
138
+ end
139
+
140
+ # "Developer ID Application: Andrea Fomera (TU4485Z3KR)" or a bare SHA-1.
141
+ def short_identity(id)
142
+ id = id.strip
143
+ return "#{id[0, 10]}…" if id.match?(/\A[0-9A-F]{40}\z/i)
144
+
145
+ id[/\(([^)]+)\)\s*\z/, 1] || id
146
+ end
147
+ end
148
+ end
@@ -12,21 +12,22 @@ module Everywhere
12
12
  File.expand_path("../..", __dir__)
13
13
  end
14
14
 
15
- # The generic Tauri shell vendored inside the gem: support/shell/ holds the
16
- # src-tauri app and the splash/ dir it serves. This is the shell the CLI
17
- # uses unless the caller overrides it with --shell-dir.
15
+ # The generic Tauri shell bundled inside the gem: support/shell/ holds the
16
+ # src-tauri app and the splash/ dir it serves. This is the CANONICAL copy of
17
+ # the shell — external consumers (e.g. the build-runner repo) resolve it via
18
+ # `every shell-dir`. The CLI uses it unless the caller passes --shell-dir.
18
19
  def bundled_shell_dir
19
20
  File.join(gem_root, "support", "shell", "src-tauri")
20
21
  end
21
22
 
22
- # Absolute path to the shell's src-tauri directory, or nil if the vendored
23
+ # Absolute path to the shell's src-tauri directory, or nil if the bundled
23
24
  # copy is missing (a corrupt/partial install).
24
25
  def shell_dir
25
26
  bundled_shell_dir if File.exist?(File.join(bundled_shell_dir, "tauri.conf.json"))
26
27
  end
27
28
 
28
29
  # Like #shell_dir but aborts with a helpful message when the shell is
29
- # missing. The --shell-dir hint covers monorepo dev pointed at build-runner.
30
+ # missing. The --shell-dir hint covers dev on a modified shell checkout.
30
31
  def shell_dir!
31
32
  shell_dir or UI.die!("couldn't find the bundled shell at #{bundled_shell_dir}; " \
32
33
  "reinstall ruby_everywhere or pass --shell-dir")
@@ -31,14 +31,45 @@ module Everywhere
31
31
  ["", nil]
32
32
  end
33
33
 
34
- # Run a command streaming to the console while also capturing to a log file.
35
- def run_logged!(env, cmd, log:)
36
- require "shellwords"
37
- line = "#{cmd.shelljoin} 2>&1 | tee #{log.shellescape}"
38
- success = unbundled { system(env, "/bin/bash", "-o", "pipefail", "-c", line) }
34
+ # Run a command, capturing its full combined output to a log file while
35
+ # showing a (optionally filtered) view on the console.
36
+ #
37
+ # Pass filter: a LogFilter (or any #call(line) -> String | :drop | nil) to
38
+ # prettify what a human sees; the on-disk log always gets the raw stream, so
39
+ # nothing is lost. Unrecognized lines (filter returns nil) are shown dimmed
40
+ # and indented so they read as background detail under the current step.
41
+ def run_logged!(env, cmd, log:, filter: nil)
42
+ require "open3"
43
+ success = false
44
+ unbundled do
45
+ File.open(log, "w") do |logf|
46
+ Open3.popen2e(env, *cmd) do |stdin, out, wait|
47
+ stdin.close
48
+ out.each_line do |raw|
49
+ logf.write(raw)
50
+ emit(raw, filter)
51
+ end
52
+ success = wait.value.success?
53
+ end
54
+ end
55
+ end
39
56
  UI.die!("command failed: #{cmd.join(" ")} (full log: #{log})") unless success
40
57
  end
41
58
 
59
+ # Route one raw line to the console: verbatim when there's no filter, else
60
+ # the filter's rendition (:drop swallows it, nil falls back to dim detail).
61
+ def emit(raw, filter)
62
+ return $stdout.print(raw) unless filter
63
+
64
+ shown = filter.call(raw)
65
+ case shown
66
+ when :drop, false then nil
67
+ when nil then $stdout.puts(" #{UI.gray(UI.short_path(raw.rstrip))}")
68
+ else $stdout.puts(shown)
69
+ end
70
+ end
71
+ private_class_method :emit
72
+
42
73
  def spawn(env, cmd, chdir:)
43
74
  unbundled { Process.spawn(env, cmd, chdir: chdir) }
44
75
  end
data/lib/everywhere/ui.rb CHANGED
@@ -3,6 +3,10 @@
3
3
  module Everywhere
4
4
  # Terminal output helpers. Colors follow the informal standard:
5
5
  # on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
6
+ #
7
+ # The hosted runner streams `every release` through a pipe (not a TTY) into the
8
+ # Platform web log, so it exports CLICOLOR_FORCE=1 to keep our color — the web
9
+ # UI renders ANSI. That's why we force rather than sniff the TTY there.
6
10
  module UI
7
11
  module_function
8
12
 
@@ -17,21 +21,76 @@ module Everywhere
17
21
  color? ? "\e[#{codes.join(";")}m#{text}\e[0m" : text.to_s
18
22
  end
19
23
 
20
- def bold(t) = paint(t, 1)
21
- def dim(t) = paint(t, 2)
22
- def red(t) = paint(t, 31)
23
- def green(t) = paint(t, 32)
24
- def yellow(t) = paint(t, 33)
25
- def cyan(t) = paint(t, 36)
24
+ def bold(t) = paint(t, 1)
25
+ def dim(t) = paint(t, 2)
26
+ def red(t) = paint(t, 31)
27
+ def green(t) = paint(t, 32)
28
+ def yellow(t) = paint(t, 33)
29
+ def blue(t) = paint(t, 34)
30
+ def magenta(t) = paint(t, 35)
31
+ def cyan(t) = paint(t, 36)
32
+ def gray(t) = paint(t, 90) # bright black — dimmer than dim() on most themes
26
33
 
27
- def step(msg) = puts("#{cyan("→")} #{msg}")
28
- def ok(msg) = puts("#{green("✓")} #{msg}")
29
- def bad(msg) = puts("#{red("✗")} #{msg}")
30
- def warn(msg) = puts("#{yellow("!")} #{msg}")
31
- def success(msg) = puts("#{green("✓")} #{bold(msg)}")
34
+ # --- output levels --------------------------------------------------------
35
+ #
36
+ # phase ● a major phase boundary (build / sign / notarize)
37
+ # step → a top-level action inside a phase
38
+ # substep ▸ a sub-action under a step (indented)
39
+ # detail · raw-ish detail under a substep (indented + dim)
40
+ #
41
+ # Every level has a *_line string builder (used by LogFilter, which returns
42
+ # lines for someone else to print) and a printer that puts it. ok / success /
43
+ # warn / bad / die! carry outcomes at any level.
44
+
45
+ def phase_line(msg) = "\n#{bold(cyan("●"))} #{bold(msg)}"
46
+ def step_line(msg) = "#{cyan("→")} #{msg}"
47
+ def substep_line(msg) = " #{cyan("▸")} #{msg}"
48
+ def detail_line(msg) = " #{gray("·")} #{gray(msg)}"
49
+ def ok_line(msg) = "#{green("✓")} #{msg}"
50
+ def bad_line(msg) = "#{red("✗")} #{msg}"
51
+ def warn_line(msg) = "#{yellow("!")} #{msg}"
52
+ def success_line(msg) = "#{green("✓")} #{bold(msg)}"
53
+
54
+ def phase(msg) = puts(phase_line(msg))
55
+ def step(msg) = puts(step_line(msg))
56
+ def substep(msg) = puts(substep_line(msg))
57
+ def detail(msg) = puts(detail_line(msg))
58
+ def ok(msg) = puts(ok_line(msg))
59
+ def bad(msg) = puts(bad_line(msg))
60
+ def warn(msg) = puts(warn_line(msg))
61
+ def success(msg) = puts(success_line(msg))
62
+
63
+ # An indented, dim outcome under a substep — e.g. "accepted", "valid on disk".
64
+ def note_line(msg, marker: "·", color: :gray)
65
+ tint = respond_to?(color) ? method(color) : method(:gray)
66
+ " #{tint.call(marker)} #{tint.call(msg)}"
67
+ end
68
+
69
+ def note(msg, marker: "·", color: :gray) = puts(note_line(msg, marker: marker, color: color))
32
70
 
33
71
  def die!(msg)
34
72
  abort "#{red("✗")} #{red(msg)}"
35
73
  end
74
+
75
+ # A yes/no fact, colored by truthiness (for signing receipts).
76
+ def yn(bool) = bool ? green("yes") : red("no")
77
+
78
+ # --- path shortening ------------------------------------------------------
79
+
80
+ # Collapse the noisy machine paths that external tools echo (codesign,
81
+ # notarytool, stapler) down to something a human can scan. In particular the
82
+ # runner works out of a deep `/var/folders/…/every-runner…/src/` sandbox — we
83
+ # strip that prefix so only the meaningful `dist/App.app` tail remains. Then
84
+ # $HOME → ~ and the cwd → a leading (dropped) prefix.
85
+ def short_path(str)
86
+ s = str.to_s.dup
87
+ # Runner/local temp sandboxes: keep only the part after the extracted src/.
88
+ s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/every-[a-z]+[0-9A-Za-z_-]+/src/}, "")
89
+ # Any other everywhere-* scratch dir: keep the basename tail.
90
+ s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/everywhere-[a-z]+[0-9A-Za-z_.-]*/}, "")
91
+ home = ENV["HOME"]
92
+ s = s.gsub("#{home}/", "~/") if home && !home.empty?
93
+ s
94
+ end
36
95
  end
37
96
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Everywhere
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
 
6
6
  # Version of the vendored @rubyeverywhere/bridge JS this gem ships. Tracks
7
7
  # bridge/package.json — bump it whenever lib/everywhere/javascript/bridge.js is
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_everywhere
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrea Fomera
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: dry-cli
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +67,7 @@ files:
53
67
  - lib/everywhere/boot.rb
54
68
  - lib/everywhere/cli.rb
55
69
  - lib/everywhere/commands/build.rb
70
+ - lib/everywhere/commands/clean.rb
56
71
  - lib/everywhere/commands/dev.rb
57
72
  - lib/everywhere/commands/doctor.rb
58
73
  - lib/everywhere/commands/icon.rb
@@ -63,12 +78,14 @@ files:
63
78
  - lib/everywhere/commands/platform/logout.rb
64
79
  - lib/everywhere/commands/platform/runner.rb
65
80
  - lib/everywhere/commands/release.rb
81
+ - lib/everywhere/commands/shell_dir.rb
66
82
  - lib/everywhere/config.rb
67
83
  - lib/everywhere/database.rb
68
84
  - lib/everywhere/framework.rb
69
85
  - lib/everywhere/icon.rb
70
86
  - lib/everywhere/ignore.rb
71
87
  - lib/everywhere/javascript/bridge.js
88
+ - lib/everywhere/log_filter.rb
72
89
  - lib/everywhere/paths.rb
73
90
  - lib/everywhere/platform/client.rb
74
91
  - lib/everywhere/platform/credentials.rb