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 +4 -4
- data/lib/everywhere/cli.rb +4 -0
- data/lib/everywhere/commands/build.rb +4 -3
- data/lib/everywhere/commands/clean.rb +40 -0
- data/lib/everywhere/commands/doctor.rb +2 -21
- data/lib/everywhere/commands/install.rb +28 -11
- data/lib/everywhere/commands/platform/build.rb +3 -0
- data/lib/everywhere/commands/platform/runner.rb +22 -9
- data/lib/everywhere/commands/release.rb +13 -13
- data/lib/everywhere/commands/shell_dir.rb +18 -0
- data/lib/everywhere/log_filter.rb +148 -0
- data/lib/everywhere/paths.rb +6 -5
- data/lib/everywhere/shellout.rb +36 -5
- data/lib/everywhere/ui.rb +70 -11
- data/lib/everywhere/version.rb +1 -1
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e9efcbd721dfa8ec5eb10adcbd9d96970feb860b9bfa20d3d6b761012bb8d1b
|
|
4
|
+
data.tar.gz: 66a196953da3501c430aa91318d02cc51fc42ff4ebfe294d33a58c44cf7d5333
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0644e199416ba7568a78812a83ba97fda7b97a829bca821540ba03f355b5798b2e69765db1aa5bd498e0f537edff73d8cad34ba0ce855261338a1522421b60ef
|
|
7
|
+
data.tar.gz: 9fb95b8c424cbcc6c82148f4bab93fd07d243e5c2051a0b7b5b056a3ded8fc2a8f43f5e6730b3791edd53d249dde25e001ff2d5d729e4c6a216d44831448cb6b
|
data/lib/everywhere/cli.rb
CHANGED
|
@@ -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}
|
|
93
|
-
UI.
|
|
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
|
|
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
|
|
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 :
|
|
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: ".",
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
#
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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.
|
|
190
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
129
|
-
UI.
|
|
130
|
-
UI.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
UI.
|
|
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
|
|
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
|
data/lib/everywhere/paths.rb
CHANGED
|
@@ -12,21 +12,22 @@ module Everywhere
|
|
|
12
12
|
File.expand_path("../..", __dir__)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# The generic Tauri shell
|
|
16
|
-
# src-tauri app and the splash/ dir it serves. This is the
|
|
17
|
-
#
|
|
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
|
|
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
|
|
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")
|
data/lib/everywhere/shellout.rb
CHANGED
|
@@ -31,14 +31,45 @@ module Everywhere
|
|
|
31
31
|
["", nil]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Run a command
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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)
|
|
21
|
-
def dim(t)
|
|
22
|
-
def red(t)
|
|
23
|
-
def green(t)
|
|
24
|
-
def yellow(t)
|
|
25
|
-
def
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
data/lib/everywhere/version.rb
CHANGED
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
|
+
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
|