ruby_native 0.2.9 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02e351525d22c03236baded5c69e7a52e0abe24142f67bf1b54712a9197a6b12
4
- data.tar.gz: 98e40a8af831d540900aae80a738699d756aac03d198a49f92ce1c366555cddb
3
+ metadata.gz: d439edf69ea1ee01ec61d732c6d60590e2cfe8ad82af5fbb3c914d08b3bb678e
4
+ data.tar.gz: 7c8a4001bddbefb569eda81a455e14643aa08e76a3ec1d15edcbfa9acdb5b1e7
5
5
  SHA512:
6
- metadata.gz: fd31d17baba29cf99034f7ec3250ee6df962eaa955fd48412014456eed614791d21ac6ebbaeec65f39a5cbac126d00804d3aaca050810d2175454d8deb6bb1c8
7
- data.tar.gz: b68d553bd40441f9bf16491be624c8af5907e11c78e34b3d0a20ac8c633c1a41fd5fafc9365b1b168408aeec94d201cbda0fc1334275376e3a8f1d2f8a7e3272
6
+ metadata.gz: 5668a8b5c5e9e1e3a10effd882711867ba004f7015a1dcca39a3a41efdbf306acb09809486f058d6f2fcc775df6c40b5a91c5bf2e0cff45a4c79ac8a65795892
7
+ data.tar.gz: 03b6f326068805f701d208c065f493c2d48a556e7e5e14adc6520fd98f3e2084254b139a7074e58e89344e7bb01b236c635ad02ef9d60240354ccd9838941c1f
@@ -20,6 +20,15 @@ module RubyNative
20
20
  say " Added .trycloudflare.com to allowed hosts in development.rb", :green
21
21
  end
22
22
 
23
+ def add_gitignore
24
+ gitignore = File.join(destination_root, ".gitignore")
25
+ return unless File.exist?(gitignore)
26
+ return if File.read(gitignore).include?(".ruby_native")
27
+
28
+ append_to_file ".gitignore", "\n# Ruby Native (Playwright, screenshots, session data)\n.ruby_native/\n"
29
+ say " Added .ruby_native/ to .gitignore", :green
30
+ end
31
+
23
32
  def copy_claude_instructions
24
33
  return unless File.directory?(File.join(destination_root, ".claude"))
25
34
  copy_file "CLAUDE.md", ".claude/ruby_native.md"
@@ -0,0 +1,28 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module RubyNative
5
+ class CLI
6
+ class Credentials
7
+ PATH = File.join(Dir.home, ".ruby_native", "credentials")
8
+
9
+ def self.token
10
+ return unless File.exist?(PATH)
11
+ JSON.parse(File.read(PATH))["token"]
12
+ rescue JSON::ParserError
13
+ nil
14
+ end
15
+
16
+ def self.save(token)
17
+ dir = File.dirname(PATH)
18
+ FileUtils.mkdir_p(dir)
19
+ File.write(PATH, JSON.generate(token: token))
20
+ File.chmod(0600, PATH)
21
+ end
22
+
23
+ def self.clear
24
+ File.delete(PATH) if File.exist?(PATH)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,69 @@
1
+ require "securerandom"
2
+ require "net/http"
3
+ require "uri"
4
+ require "json"
5
+ require "ruby_native/cli/credentials"
6
+
7
+ module RubyNative
8
+ class CLI
9
+ class Login
10
+ HOST = ENV.fetch("RUBY_NATIVE_HOST", "https://rubynative.com")
11
+
12
+ def initialize(argv = [])
13
+ end
14
+
15
+ def run
16
+ code = SecureRandom.hex(20)
17
+ url = "#{HOST}/cli/session/new?code=#{code}"
18
+
19
+ puts "Opening browser to authorize..."
20
+ open_browser(url)
21
+ puts "Waiting for authorization..."
22
+
23
+ token = poll_for_token(code)
24
+
25
+ if token
26
+ Credentials.save(token)
27
+ puts "Logged in to Ruby Native."
28
+ else
29
+ puts "Authorization timed out. Please try again."
30
+ exit 1
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def open_browser(url)
37
+ case RUBY_PLATFORM
38
+ when /darwin/
39
+ system("open", url)
40
+ when /linux/
41
+ system("xdg-open", url)
42
+ when /mingw|mswin/
43
+ system("start", url)
44
+ end
45
+ end
46
+
47
+ def poll_for_token(code)
48
+ uri = URI("#{HOST}/cli/session/poll?code=#{code}")
49
+ attempts = 0
50
+ max_attempts = 60
51
+
52
+ loop do
53
+ attempts += 1
54
+ return nil if attempts > max_attempts
55
+
56
+ sleep 2
57
+
58
+ response = Net::HTTP.get_response(uri)
59
+ if response.is_a?(Net::HTTPSuccess)
60
+ data = JSON.parse(response.body)
61
+ return data["token"]
62
+ end
63
+ end
64
+ rescue Interrupt
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -109,7 +109,7 @@ module RubyNative
109
109
  puts ""
110
110
  puts url
111
111
  puts ""
112
- puts "Scan with the Ruby Native preview app."
112
+ puts "Scan the QR code or paste the URL into the Ruby Native Preview app."
113
113
  puts "Keep this running and your Rails server on port #{@port} in another terminal."
114
114
  puts "Press Ctrl+C to stop."
115
115
  end
@@ -0,0 +1,591 @@
1
+ require "json"
2
+ require "open3"
3
+ require "fileutils"
4
+ require "tempfile"
5
+ require "net/http"
6
+ require "uri"
7
+ require "ruby_native/cli/credentials"
8
+
9
+ module RubyNative
10
+ class CLI
11
+ class Screenshots
12
+ STORAGE_DIR = ".ruby_native"
13
+ STORAGE_FILE = "screenshots_storage.json"
14
+ OUTPUT_DIR = ".ruby_native/screenshots"
15
+ CONFIG_PATH = "config/ruby_native.yml"
16
+ SCALE = 3
17
+ WIDTH_PX = 1320
18
+ HEIGHT_PX = 2868
19
+ WIDTH_PT = WIDTH_PX / SCALE # 440
20
+ HEIGHT_PT = HEIGHT_PX / SCALE # 956
21
+
22
+ HOST = ENV.fetch("RUBY_NATIVE_HOST", "https://rubynative.com")
23
+
24
+ def initialize(argv)
25
+ @url = parse_option(argv, "--url", nil)
26
+ @port = parse_option(argv, "--port", nil)
27
+ @output = parse_option(argv, "--output", OUTPUT_DIR)
28
+ @login = argv.delete("--login")
29
+
30
+ if @url
31
+ unless @url.match?(%r{\Ahttps?://})
32
+ host = @url.split(":", 2).first
33
+ scheme = (host == "localhost" || host.match?(/\A\d+\.\d+\.\d+\.\d+\z/)) ? "http" : "https"
34
+ @url = "#{scheme}://#{@url}"
35
+ end
36
+ @url = @url.chomp("/")
37
+ @port = URI(@url).port if @port.nil?
38
+ else
39
+ @port = (@port || 3000).to_i
40
+ end
41
+ end
42
+
43
+ def run
44
+ load_config!
45
+ check_node!
46
+ check_playwright!
47
+
48
+ if @login
49
+ run_login
50
+ else
51
+ run_setup unless screenshots_configured?
52
+ check_server!
53
+ capture_screenshots
54
+ upload_screenshots if credentials_available?
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def parse_option(argv, flag, default)
61
+ index = argv.index(flag)
62
+ if index
63
+ argv.delete_at(index)
64
+ argv.delete_at(index) || default
65
+ else
66
+ default
67
+ end
68
+ end
69
+
70
+ # --- Config ---
71
+
72
+ def load_config!
73
+ unless File.exist?(CONFIG_PATH)
74
+ puts "config/ruby_native.yml not found. Run `rails generate ruby_native:install` first."
75
+ exit 1
76
+ end
77
+
78
+ require "yaml"
79
+ @config = YAML.load_file(CONFIG_PATH, symbolize_names: true) || {}
80
+ end
81
+
82
+ def screenshots_configured?
83
+ paths = @config.dig(:screenshots, :paths)
84
+ paths.is_a?(Array) && !paths.empty?
85
+ end
86
+
87
+ def screenshot_paths
88
+ @config.dig(:screenshots, :paths) || []
89
+ end
90
+
91
+ def tab_paths
92
+ tabs = @config[:tabs] || []
93
+ tabs.map { |tab| tab[:path] }.compact
94
+ end
95
+
96
+ # --- First-run setup ---
97
+
98
+ def run_setup
99
+ puts "Let's set up screenshots! (one-time)"
100
+ puts ""
101
+
102
+ paths = prompt_for_paths
103
+ write_paths_to_config(paths)
104
+ prompt_for_login
105
+
106
+ puts ""
107
+ end
108
+
109
+ def prompt_for_paths
110
+ tabs = tab_paths
111
+
112
+ if tabs.any?
113
+ puts "Which paths do you want to capture?"
114
+ puts "Your tabs: #{tabs.join(", ")}"
115
+ puts "Enter paths (comma-separated) or press Enter to use tab paths:"
116
+ else
117
+ puts "Which paths do you want to capture?"
118
+ puts "Enter paths (comma-separated):"
119
+ end
120
+
121
+ print "> "
122
+ input = $stdin.gets&.strip || ""
123
+
124
+ if input.empty?
125
+ if tabs.any?
126
+ tabs
127
+ else
128
+ puts "No paths entered and no tabs configured."
129
+ exit 1
130
+ end
131
+ else
132
+ input.split(",").map(&:strip).reject(&:empty?).map { |p| p.start_with?("/") ? p : "/#{p}" }
133
+ end
134
+ end
135
+
136
+ def write_paths_to_config(paths)
137
+ raw = File.read(CONFIG_PATH)
138
+
139
+ yaml_paths = paths.map { |p| " - #{p}" }.join("\n")
140
+ screenshot_block = "\nscreenshots:\n paths:\n#{yaml_paths}\n"
141
+
142
+ File.write(CONFIG_PATH, raw.rstrip + "\n" + screenshot_block)
143
+
144
+ # Reload config
145
+ require "yaml"
146
+ @config = YAML.load_file(CONFIG_PATH, symbolize_names: true) || {}
147
+
148
+ puts ""
149
+ puts "Added to #{CONFIG_PATH}:"
150
+ puts ""
151
+ puts " screenshots:"
152
+ puts " paths:"
153
+ paths.each { |p| puts " - #{p}" }
154
+ end
155
+
156
+ def prompt_for_login
157
+ puts ""
158
+ puts "Does your app require sign-in? (y/n)"
159
+ print "> "
160
+ input = $stdin.gets&.strip&.downcase || ""
161
+
162
+ if input == "y" || input == "yes"
163
+ puts ""
164
+ run_login
165
+ end
166
+ end
167
+
168
+ # --- Checks ---
169
+
170
+ def check_node!
171
+ _, _, status = Open3.capture3("node", "--version")
172
+ unless status.success?
173
+ puts "Node.js is required for screenshots. Install it from https://nodejs.org"
174
+ exit 1
175
+ end
176
+ end
177
+
178
+ def check_server!
179
+ uri = URI("#{base_url}/")
180
+ http = Net::HTTP.new(uri.host, uri.port)
181
+ http.use_ssl = uri.scheme == "https"
182
+ http.open_timeout = 5
183
+ http.request(Net::HTTP::Head.new(uri))
184
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError, Net::OpenTimeout
185
+ if @url
186
+ puts "Could not reach #{@url}."
187
+ else
188
+ puts "No server running on port #{@port}."
189
+ puts ""
190
+ puts "Start your Rails server first:"
191
+ puts " bin/rails server#{" -p #{@port}" if @port != 3000}"
192
+ end
193
+ puts ""
194
+ puts "Then run this command again."
195
+ exit 1
196
+ end
197
+
198
+ def check_playwright!
199
+ FileUtils.mkdir_p(STORAGE_DIR)
200
+ add_to_gitignore
201
+
202
+ playwright_dir = File.join(STORAGE_DIR, "node_modules", "playwright")
203
+ unless File.exist?(playwright_dir)
204
+ puts "Playwright is required for screenshots."
205
+ puts "Install it now? (y/n)"
206
+ print "> "
207
+ input = $stdin.gets&.strip&.downcase || ""
208
+ exit 0 unless input == "y" || input == "yes"
209
+
210
+ puts ""
211
+ puts "Installing Playwright..."
212
+ system("npm", "install", "--prefix", STORAGE_DIR, "playwright", out: File::NULL, err: File::NULL)
213
+
214
+ puts "Installing WebKit browser..."
215
+ system("npx", "--prefix", STORAGE_DIR, "playwright", "install", "webkit", out: File::NULL)
216
+
217
+ unless File.exist?(playwright_dir)
218
+ puts ""
219
+ puts "Failed to install Playwright. Install it manually:"
220
+ puts " npm install playwright"
221
+ puts " npx playwright install webkit"
222
+ exit 1
223
+ end
224
+
225
+ puts ""
226
+ end
227
+ end
228
+
229
+ def base_url
230
+ @url || "http://localhost:#{@port}"
231
+ end
232
+
233
+ # --- Login ---
234
+
235
+ def run_login
236
+ puts "Opening browser to #{base_url}..."
237
+ puts "Sign in to your app, then close the browser window."
238
+ puts ""
239
+
240
+ script = login_script
241
+ run_playwright(script)
242
+
243
+ if File.exist?(storage_path)
244
+ puts ""
245
+ puts "Session saved."
246
+ else
247
+ puts ""
248
+ puts "No session saved. Try again with `ruby_native screenshots --login`."
249
+ exit 1
250
+ end
251
+ end
252
+
253
+ # --- Capture ---
254
+
255
+ def capture_screenshots
256
+ paths = screenshot_paths
257
+
258
+ if paths.empty?
259
+ puts "No screenshot paths configured in #{CONFIG_PATH}."
260
+ puts "Run `ruby_native screenshots` to set them up, or add manually:"
261
+ puts ""
262
+ puts " screenshots:"
263
+ puts " paths:"
264
+ puts " - /inbox"
265
+ puts " - /profile"
266
+ exit 1
267
+ end
268
+
269
+ FileUtils.mkdir_p(@output)
270
+
271
+ puts "Capturing #{paths.length} screenshot#{"s" if paths.length > 1} at #{WIDTH_PX}x#{HEIGHT_PX} (#{WIDTH_PT}x#{HEIGHT_PT}pt @#{SCALE}x)..."
272
+ puts ""
273
+
274
+ script = capture_script(paths)
275
+ run_playwright(script)
276
+
277
+ puts ""
278
+ puts "Screenshots saved to #{@output}/."
279
+ end
280
+
281
+ # --- Upload ---
282
+
283
+ def credentials_available?
284
+ if Credentials.token
285
+ true
286
+ else
287
+ puts ""
288
+ puts "Run `ruby_native login` to upload screenshots to Ruby Native."
289
+ false
290
+ end
291
+ end
292
+
293
+ def upload_screenshots
294
+ begin
295
+ app_id = @config.dig(:ruby_native, :app_id)
296
+ app_id = link_app unless app_id
297
+ return unless app_id
298
+
299
+ files = Dir.glob(File.join(@output, "*.png")).sort
300
+ if files.empty?
301
+ puts "No screenshots to upload."
302
+ return
303
+ end
304
+
305
+ puts ""
306
+ puts "Uploading #{files.length} screenshot#{"s" if files.length > 1}..."
307
+
308
+ api_delete("/api/v1/apps/#{app_id}/web_screenshots")
309
+
310
+ files.each_with_index do |file, index|
311
+ path = screenshot_paths[index] || File.basename(file, ".png")
312
+ config = compositor_config_for(path)
313
+ api_upload("/api/v1/apps/#{app_id}/web_screenshots", file, screenshot_path: path, position: index, total: files.length, compositor_config: config)
314
+ puts " #{File.basename(file)}"
315
+ end
316
+
317
+ puts ""
318
+ puts "Uploaded to Ruby Native."
319
+ rescue TokenExpiredError
320
+ puts "Token expired. Run `ruby_native login` again."
321
+ rescue => e
322
+ puts "Upload failed: #{e.message}"
323
+ puts "Screenshots are saved locally in #{@output}/."
324
+ end
325
+ end
326
+
327
+ def link_app
328
+ apps = fetch_apps
329
+ return unless apps
330
+
331
+ if apps.empty?
332
+ puts "No apps found on your account."
333
+ return
334
+ end
335
+
336
+ app = if apps.length == 1
337
+ puts "Using app: #{apps[0]["name"]}"
338
+ apps[0]
339
+ else
340
+ puts "Which app?"
341
+ apps.each_with_index do |a, i|
342
+ puts " #{i + 1}. #{a["name"]}"
343
+ end
344
+ print "> "
345
+ choice = ($stdin.gets&.strip || "").to_i
346
+ unless choice.between?(1, apps.length)
347
+ puts "Invalid choice."
348
+ return
349
+ end
350
+ apps[choice - 1]
351
+ end
352
+
353
+ app_id = app["public_id"]
354
+ write_app_id_to_config(app_id)
355
+ app_id
356
+ end
357
+
358
+ def write_app_id_to_config(app_id)
359
+ raw = File.read(CONFIG_PATH)
360
+
361
+ if raw.match?(/^ruby_native:/)
362
+ # Append under existing ruby_native key
363
+ raw = raw.gsub(/^(ruby_native:\s*\n)/, "\\1 app_id: #{app_id}\n")
364
+ else
365
+ raw = raw.rstrip + "\n\nruby_native:\n app_id: #{app_id}\n"
366
+ end
367
+
368
+ File.write(CONFIG_PATH, raw)
369
+
370
+ require "yaml"
371
+ @config = YAML.load_file(CONFIG_PATH, symbolize_names: true) || {}
372
+ end
373
+
374
+ def fetch_apps
375
+ uri = URI("#{HOST}/api/v1/apps")
376
+ req = Net::HTTP::Get.new(uri)
377
+ req["Authorization"] = "Token #{Credentials.token}"
378
+
379
+ response = make_request(uri, req)
380
+
381
+ case response
382
+ when Net::HTTPUnauthorized
383
+ raise TokenExpiredError
384
+ when Net::HTTPSuccess
385
+ JSON.parse(response.body)
386
+ else
387
+ puts "Failed to fetch apps: #{response.code}"
388
+ nil
389
+ end
390
+ end
391
+
392
+ TokenExpiredError = Class.new(StandardError)
393
+
394
+ def api_delete(path)
395
+ uri = URI("#{HOST}#{path}")
396
+ req = Net::HTTP::Delete.new(uri)
397
+ req["Authorization"] = "Token #{Credentials.token}"
398
+
399
+ response = make_request(uri, req)
400
+ raise TokenExpiredError if response.is_a?(Net::HTTPUnauthorized)
401
+ response
402
+ end
403
+
404
+ def compositor_config_for(path)
405
+ tabs = (@config[:tabs] || []).map { |tab|
406
+ {title: tab[:title], icon: tab[:icon], selected: tab[:path] == path}
407
+ }
408
+
409
+ appearance = @config[:appearance] || {}
410
+ {
411
+ tabs: tabs,
412
+ tint_color: resolve_color(appearance[:tint_color]),
413
+ background_color: resolve_color(appearance[:background_color]),
414
+ status_bar_color: resolve_color(appearance[:status_bar_color])
415
+ }.compact
416
+ end
417
+
418
+ def resolve_color(value)
419
+ case value
420
+ when Hash then value[:light] || value["light"]
421
+ when String then value
422
+ end
423
+ end
424
+
425
+ def api_upload(endpoint, file_path, screenshot_path:, position:, total:, compositor_config: nil)
426
+ uri = URI("#{HOST}#{endpoint}")
427
+ boundary = "RubyNative#{SecureRandom.hex(16)}"
428
+
429
+ body = build_multipart_body(boundary, file_path, screenshot_path, position, total, compositor_config)
430
+
431
+ req = Net::HTTP::Post.new(uri)
432
+ req["Authorization"] = "Token #{Credentials.token}"
433
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
434
+ req.body = body
435
+
436
+ response = make_request(uri, req)
437
+ raise TokenExpiredError if response.is_a?(Net::HTTPUnauthorized)
438
+
439
+ if response.is_a?(Net::HTTPNotFound)
440
+ puts "App not found. Clearing app_id from config."
441
+ clear_app_id_from_config
442
+ return
443
+ end
444
+
445
+ response
446
+ end
447
+
448
+ def build_multipart_body(boundary, file_path, path, position, total, compositor_config)
449
+ parts = []
450
+
451
+ parts << "--#{boundary}\r\n"
452
+ parts << "Content-Disposition: form-data; name=\"path\"\r\n\r\n"
453
+ parts << "#{path}\r\n"
454
+
455
+ parts << "--#{boundary}\r\n"
456
+ parts << "Content-Disposition: form-data; name=\"position\"\r\n\r\n"
457
+ parts << "#{position}\r\n"
458
+
459
+ parts << "--#{boundary}\r\n"
460
+ parts << "Content-Disposition: form-data; name=\"total\"\r\n\r\n"
461
+ parts << "#{total}\r\n"
462
+
463
+ if compositor_config
464
+ parts << "--#{boundary}\r\n"
465
+ parts << "Content-Disposition: form-data; name=\"compositor_config\"\r\n\r\n"
466
+ parts << "#{JSON.generate(compositor_config)}\r\n"
467
+ end
468
+
469
+ parts << "--#{boundary}\r\n"
470
+ parts << "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(file_path)}\"\r\n"
471
+ parts << "Content-Type: image/png\r\n\r\n"
472
+ parts << File.binread(file_path)
473
+ parts << "\r\n"
474
+
475
+ parts << "--#{boundary}--\r\n"
476
+ parts.join
477
+ end
478
+
479
+ def make_request(uri, req)
480
+ http = Net::HTTP.new(uri.host, uri.port)
481
+ http.use_ssl = uri.scheme == "https"
482
+ http.open_timeout = 10
483
+ http.read_timeout = 30
484
+ http.request(req)
485
+ end
486
+
487
+ def clear_app_id_from_config
488
+ raw = File.read(CONFIG_PATH)
489
+ raw = raw.gsub(/^ app_id: .+\n/, "")
490
+ File.write(CONFIG_PATH, raw)
491
+
492
+ require "yaml"
493
+ @config = YAML.load_file(CONFIG_PATH, symbolize_names: true) || {}
494
+ end
495
+
496
+ # --- Playwright ---
497
+
498
+ def add_to_gitignore
499
+ gitignore = ".gitignore"
500
+ return unless File.exist?(gitignore)
501
+ return if File.read(gitignore).include?(".ruby_native")
502
+
503
+ File.open(gitignore, "a") { |f| f.puts "\n# Ruby Native\n.ruby_native/" }
504
+ end
505
+
506
+ def storage_path
507
+ File.join(STORAGE_DIR, STORAGE_FILE)
508
+ end
509
+
510
+ def run_playwright(script)
511
+ FileUtils.mkdir_p(STORAGE_DIR)
512
+ script_path = File.join(STORAGE_DIR, "capture.js")
513
+ File.write(script_path, script)
514
+
515
+ node_path = File.expand_path(File.join(STORAGE_DIR, "node_modules"))
516
+ env = {"NODE_PATH" => node_path}
517
+
518
+ stdout, stderr, status = Open3.capture3(env, "node", script_path)
519
+ puts stdout unless stdout.empty?
520
+
521
+ unless status.success?
522
+ puts stderr unless stderr.empty?
523
+ end
524
+ ensure
525
+ File.delete(script_path) if script_path && File.exist?(script_path)
526
+ end
527
+
528
+ def login_script
529
+ <<~JS
530
+ const { webkit } = require('playwright');
531
+
532
+ (async () => {
533
+ const browser = await webkit.launch({ headless: false });
534
+ const context = await browser.newContext();
535
+ const page = await context.newPage();
536
+
537
+ await page.goto('#{base_url}/');
538
+ console.log('Sign in to your app, then close the browser window.');
539
+
540
+ await page.waitForEvent('close', { timeout: 0 }).catch(() => {});
541
+ await context.storageState({ path: '#{storage_path}' });
542
+ await browser.close();
543
+ })();
544
+ JS
545
+ end
546
+
547
+ def capture_script(paths)
548
+ storage_opt = if File.exist?(storage_path)
549
+ "storageState: '#{storage_path}',"
550
+ else
551
+ ""
552
+ end
553
+
554
+ screenshots_js = paths.map.with_index { |path, i|
555
+ safe_name = path.gsub(/[^a-z0-9]/i, "_").gsub(/^_+|_+$/, "")
556
+ safe_name = "root" if safe_name.empty?
557
+ output_path = File.join(@output, "#{"%02d" % (i + 1)}_#{safe_name}.png")
558
+
559
+ <<~CAPTURE
560
+ const response_#{i} = await page.goto('#{base_url}#{path}', { waitUntil: 'networkidle' });
561
+ if (response_#{i} && response_#{i}.status() >= 400) {
562
+ console.log(' #{path} -> ERROR ' + response_#{i}.status() + ' (skipped)');
563
+ } else {
564
+ await page.screenshot({ path: '#{output_path}' });
565
+ console.log(' #{path} -> #{output_path}');
566
+ }
567
+ CAPTURE
568
+ }.join("\n")
569
+
570
+ <<~JS
571
+ const { webkit } = require('playwright');
572
+
573
+ (async () => {
574
+ const browser = await webkit.launch();
575
+ const context = await browser.newContext({
576
+ #{storage_opt}
577
+ viewport: { width: #{WIDTH_PT}, height: #{HEIGHT_PT} },
578
+ deviceScaleFactor: #{SCALE},
579
+ userAgent: 'Ruby Native iOS/1.0 Screenshot',
580
+ });
581
+ const page = await context.newPage();
582
+
583
+ #{screenshots_js}
584
+
585
+ await browser.close();
586
+ })();
587
+ JS
588
+ end
589
+ end
590
+ end
591
+ end
@@ -1,4 +1,7 @@
1
+ require "ruby_native/cli/credentials"
2
+ require "ruby_native/cli/login"
1
3
  require "ruby_native/cli/preview"
4
+ require "ruby_native/cli/screenshots"
2
5
 
3
6
  module RubyNative
4
7
  class CLI
@@ -7,11 +10,21 @@ module RubyNative
7
10
  case command
8
11
  when "preview"
9
12
  RubyNative::CLI::Preview.new(argv).run
13
+ when "screenshots"
14
+ RubyNative::CLI::Screenshots.new(argv).run
15
+ when "login"
16
+ RubyNative::CLI::Login.new(argv).run
17
+ when "logout"
18
+ RubyNative::CLI::Credentials.clear
19
+ puts "Logged out of Ruby Native."
10
20
  else
11
21
  puts "Usage: ruby_native <command>"
12
22
  puts ""
13
23
  puts "Commands:"
14
- puts " preview Start a tunnel and display a QR code"
24
+ puts " login Authenticate with Ruby Native"
25
+ puts " logout Remove stored credentials"
26
+ puts " preview Start a tunnel and display a QR code"
27
+ puts " screenshots Capture web screenshots for App Store images"
15
28
  end
16
29
  end
17
30
  end
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.2.9"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Masilotti
@@ -73,7 +73,10 @@ files:
73
73
  - lib/generators/ruby_native/templates/ruby_native.yml
74
74
  - lib/ruby_native.rb
75
75
  - lib/ruby_native/cli.rb
76
+ - lib/ruby_native/cli/credentials.rb
77
+ - lib/ruby_native/cli/login.rb
76
78
  - lib/ruby_native/cli/preview.rb
79
+ - lib/ruby_native/cli/screenshots.rb
77
80
  - lib/ruby_native/engine.rb
78
81
  - lib/ruby_native/helper.rb
79
82
  - lib/ruby_native/inertia_support.rb