ruby_native 0.19.0 → 0.19.1

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: 491f0a1e2130f57fed894e6135f335799520c8050790ab19065f5719124bfdf9
4
- data.tar.gz: ba626083a16d9f075905a9739245dc626f9b21c8dd6302727beafb53263c176a
3
+ metadata.gz: 781df25f5a04d4f1d797f8004248c71e5e9f6b73d18965b4036e51bd8990f021
4
+ data.tar.gz: b232c5cdfd1f2cda62214936b27ae08e840f1666c0feecdd4e92bd7fd7b70c1c
5
5
  SHA512:
6
- metadata.gz: f2c5079d15d1ab0497534dd09df22def4fef55816620518ed6405cc6b7ce2329d0cd4a2e6470fa280c4c785c77076ee3cf0402433f9c3fe9f7d581899d49a84d
7
- data.tar.gz: 62580eabcc487edb2ac827a19071733e565fdab8ca41764127e0c5854b0257b136305324758865c141841fbe26e6f84485c0970f88761cd6eacb77318b1526a3
6
+ metadata.gz: 5afe36bca245ee30724d854b2d11b8684090ae7fd4d441f1d5d8f6267113c3abbc18533fedfe0654ee3800d8e78509b0322b41675d282483ae3dc4a6de247695
7
+ data.tar.gz: d2334ac64c9a9283ddacbd4230fb6e965ddf9fec9f909bb91b6c0675e73c0e15ebcb064d77e93c8aa15c31733cec9ae47adc7531cefdeb8d9386880cfe4d1715
@@ -10,7 +10,26 @@ module RubyNative
10
10
  class Link < Command
11
11
  def run
12
12
  ensure_authenticated!
13
- finish(self.class.link(positional.first || flag("app")))
13
+ return finish(self.class.link(positional.first || flag("app"))) unless @argv.include?("--new")
14
+
15
+ name, url = flag("name"), flag("url")
16
+ if name.to_s.strip.empty? || url.to_s.strip.empty?
17
+ puts "Usage: ruby_native link --new --name=\"App name\" --url=https://example.com"
18
+ exit 1
19
+ end
20
+ finish(self.class.create(name: name, url: url))
21
+ end
22
+
23
+ # Creates the app on the account and links it, so an agent never sends
24
+ # the person to Create your app.
25
+ def self.create(name:, url:)
26
+ result = Mcp::Platform.post("/api/v1/apps", { name: name, url: url })
27
+ return failure(result) unless result.ok?
28
+
29
+ app = result.value
30
+ Mcp::Platform.write_app_id(app["public_id"])
31
+ success("Created #{app["name"]} (#{app["public_id"]}) for #{app["url"]} and linked it. Wrote ruby_native.app_id to #{Mcp::Platform::CONFIG_PATH}.",
32
+ { "app_id" => app["public_id"], "name" => app["name"], "url" => app["url"] })
14
33
  end
15
34
 
16
35
  def self.link(app_id = nil)
@@ -18,7 +37,7 @@ module RubyNative
18
37
  return failure(result) unless result.ok?
19
38
 
20
39
  apps = Array(result.value)
21
- return Outcome.new(ok: false, text: "No apps on this account yet. Create one at #{Mcp::Platform::HOST}/apps/new, then run this again.", data: { "apps" => [] }) if apps.empty?
40
+ return Outcome.new(ok: false, text: "No apps on this account yet. Create one with `ruby_native link --new --name=\"App name\" --url=https://example.com`.", data: { "apps" => [] }) if apps.empty?
22
41
 
23
42
  app = if app_id.to_s.strip.empty?
24
43
  apps.one? ? apps.first : nil
@@ -81,22 +81,27 @@ module RubyNative
81
81
  pass.
82
82
 
83
83
  When you are done, run `validate_config` and `check_views` and fix what they report. Then
84
- tell the person to run `bundle exec ruby_native preview` and scan the code it prints with
85
- the Ruby Native app on their phone. Do not run preview yourself: it opens a tunnel and
86
- waits for a phone. List what you changed and what they should look at on the device,
84
+ get the preview ready so the person only has to scan: call `preview_check`. If cloudflared
85
+ is missing, run the install command it gives you, or hand over the download link when
86
+ there is no Homebrew. If the Rails server is not running, ask the person to start it in
87
+ its own terminal; if it answers with an error, fix what the check reports. Call
88
+ `preview_check` again until it says ready.
89
+
90
+ Then tell the person to run `bundle exec ruby_native preview`, long-press the Ruby Native
91
+ app on their phone, tap Switch website, and scan the code it prints. No Ruby Native app
92
+ yet? It is at https://rubynative.com/try. Do not run preview yourself: it opens a tunnel
93
+ and waits for a phone. List what you changed and what they should look at on the device,
87
94
  since you cannot see the app running in the native shell.
88
95
 
89
96
  ## 3. The account and the app
90
97
 
91
- If `status` says there is no login or no linked app, the person does two things, and you
92
- wait after each one:
98
+ If `status` says there is no login, run `bundle exec ruby_native login`. It opens the
99
+ browser, where the person signs in, or creates an account and enters the code we email
100
+ them, then clicks Approve. Wait for them. No password goes through the terminal.
93
101
 
94
- 1. Create the app at #{Platform::HOST}/apps/new: a name and the site's URL, and an account
95
- if they do not have one.
96
- 2. Run `bundle exec ruby_native login` on this machine. It opens the browser and they click
97
- Approve. No password goes through the terminal.
98
-
99
- Then call `link_app`.
102
+ Then, with no linked app, call `link_app`. If the account has no app for this project yet,
103
+ call `app_create` with the app's name and its production URL. Take both from the codebase
104
+ when it says, and ask the person when it does not; never use localhost or a staging host.
100
105
 
101
106
  ## 4. The stores
102
107
 
@@ -9,6 +9,7 @@ require "ruby_native/cli/demo_account"
9
9
  require "ruby_native/cli/icon"
10
10
  require "ruby_native/cli/listing"
11
11
  require "ruby_native/cli/permissions"
12
+ require "ruby_native/cli/preview"
12
13
  require "ruby_native/cli/screenshots"
13
14
  require "ruby_native/cli/link"
14
15
  require "ruby_native/cli/status"
@@ -114,6 +115,22 @@ module RubyNative
114
115
  },
115
116
  annotations: { title: "Validate config", readOnlyHint: true, openWorldHint: false }
116
117
  },
118
+ {
119
+ name: "preview_check",
120
+ description: "Check what `bundle exec ruby_native preview` needs before handing it to the person: whether " \
121
+ "cloudflared is installed, with the command that installs it, and whether the Rails server " \
122
+ "answers on its port with a Ruby Native config. Opens no tunnel and reaches only the local " \
123
+ "server. Run it, fix what it reports, and run it again until it says ready.",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ port: { type: "integer", description: "The Rails server's port. Defaults to PORT, then 3000, like preview." },
128
+ url: { type: "string", description: "The Rails server's URL, for a server not on localhost. Overrides port." }
129
+ },
130
+ additionalProperties: false
131
+ },
132
+ annotations: { title: "Check preview", readOnlyHint: true, openWorldHint: false }
133
+ },
117
134
  {
118
135
  name: "config_errors",
119
136
  description: "Report what the app your users are running is failing on: the config errors real devices " \
@@ -184,6 +201,23 @@ module RubyNative
184
201
  },
185
202
  annotations: { title: "Link app", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
186
203
  },
204
+ {
205
+ name: "app_create",
206
+ description: "Create the app on the person's rubynative.com account and link this project to it, in place of " \
207
+ "sending them to Create your app. Use the app's name and its production URL, never localhost " \
208
+ "or a staging host; ask the person when the codebase doesn't say. Call `link_app` instead when " \
209
+ "the account already has this app. Reaches rubynative.com and needs `ruby_native login`.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ name: { type: "string", description: "What the person's customers see under the icon on their Home Screen." },
214
+ url: { type: "string", description: "The production Rails root URL, like https://example.com." }
215
+ },
216
+ required: [ "name", "url" ],
217
+ additionalProperties: false
218
+ },
219
+ annotations: { title: "Create app", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
220
+ },
187
221
  {
188
222
  name: "status",
189
223
  description: "Report where the app is on the way to the App Store and Google Play and what is in the way: " \
@@ -532,8 +566,8 @@ module RubyNative
532
566
  (2) In the layout <head>, add `<%= stylesheet_link_tag :ruby_native %>` and put
533
567
  `viewport-fit=cover` in the viewport meta tag. (3) In the layout <body>, add
534
568
  `<%= native_tabs_tag %>` and `<%= native_identity_tag(current_user&.id) %>`.
535
- (4) Run `validate_config` and `check_views`, then `bundle exec ruby_native preview` to see it
536
- on a phone. Full docs: https://rubynative.com/llms.txt (index) and llms-full.txt (everything).
569
+ (4) Run `validate_config` and `check_views`, then `preview_check` until it says ready, and
570
+ hand the person `bundle exec ruby_native preview` to see it on a phone. Full docs: https://rubynative.com/llms.txt (index) and llms-full.txt (everything).
537
571
 
538
572
  Signals fail silently: an attribute the app does not know is ignored with no error anywhere.
539
573
  Call `lookup_signals` before writing one and `check_views` after editing any view.
@@ -564,10 +598,14 @@ module RubyNative
564
598
  when "check_views" then check_views(arguments)
565
599
  when "lookup_signals" then lookup_signals(arguments)
566
600
  when "validate_config" then validate_config(arguments)
601
+ when "preview_check" then preview_check(arguments)
567
602
  when "config_errors" then config_errors(arguments)
568
603
  when "deployed_builds" then deployed_builds(arguments)
569
604
  when "build_status" then build_status(arguments)
570
605
  when "status" then status(arguments)
606
+ when "app_create"
607
+ outcome = Link.create(name: string!(arguments, "name"), url: string!(arguments, "url"))
608
+ tool_result(outcome.text, outcome.data, is_error: !outcome.ok)
571
609
  when "link_app"
572
610
  outcome = Link.link(arguments["app_id"].to_s.strip.empty? ? nil : arguments["app_id"].to_s.strip)
573
611
  tool_result(outcome.text, outcome.data, is_error: !outcome.ok)
@@ -677,6 +715,27 @@ module RubyNative
677
715
  })
678
716
  end
679
717
 
718
+ def preview_check(arguments)
719
+ argv = []
720
+ argv += [ "--port", arguments["port"].to_s ] if arguments["port"]
721
+ argv += [ "--url", arguments["url"].to_s ] if arguments["url"].is_a?(String) && !arguments["url"].strip.empty?
722
+ ready = Preview.new(argv).readiness
723
+
724
+ lines = []
725
+ if ready[:cloudflared]
726
+ lines << "cloudflared is installed."
727
+ else
728
+ lines << "cloudflared is not installed. Install it with: #{ready[:cloudflared_install]}"
729
+ end
730
+ lines << (ready[:server_ready] ? "The Rails server answers at #{ready[:upstream]} with a Ruby Native config." : ready[:server_problem])
731
+ if ready[:cloudflared] && ready[:server_ready]
732
+ lines << "Ready. Tell the person to run `bundle exec ruby_native preview`, long-press the Ruby Native app " \
733
+ "on their phone, tap Switch website, and scan the code it prints."
734
+ end
735
+
736
+ tool_result(lines.join("\n\n"), ready.merge(ready: ready[:cloudflared] && ready[:server_ready]))
737
+ end
738
+
680
739
  # --- Tools that read the API ---
681
740
 
682
741
  def config_errors(arguments)
@@ -2,6 +2,7 @@ require "open3"
2
2
  require "net/http"
3
3
  require "uri"
4
4
  require "resolv"
5
+ require "stringio"
5
6
  require "ruby_native/cli/credentials"
6
7
 
7
8
  module RubyNative
@@ -17,6 +18,8 @@ module RubyNative
17
18
  OUTPUT_TAIL_LINES = 5
18
19
  DEPLOY_URL = "https://rubynative.com/deploy"
19
20
  QR_QUIET_ZONE = 4
21
+ BREW_INSTALL = "brew install cloudflare/cloudflare/cloudflared".freeze
22
+ CLOUDFLARED_DOWNLOADS = "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/".freeze
20
23
 
21
24
  def initialize(argv)
22
25
  @url = parse_option(argv, "--url")
@@ -30,6 +33,19 @@ module RubyNative
30
33
  start_tunnel
31
34
  end
32
35
 
36
+ # What `run` would stop on, without opening a tunnel. The MCP's
37
+ # preview_check reads this so an agent can fix it before handing over.
38
+ def readiness
39
+ problem = upstream_problem
40
+ {
41
+ cloudflared: cloudflared_installed?,
42
+ cloudflared_install: homebrew_installed? ? BREW_INSTALL : CLOUDFLARED_DOWNLOADS,
43
+ upstream: @upstream,
44
+ server_ready: problem.nil?,
45
+ server_problem: problem
46
+ }
47
+ end
48
+
33
49
  private
34
50
 
35
51
  def check_upstream!
@@ -189,17 +205,58 @@ module RubyNative
189
205
  end
190
206
 
191
207
  def check_cloudflared!
192
- unless system("which cloudflared > /dev/null 2>&1")
193
- puts "cloudflared is not installed."
208
+ return if cloudflared_installed?
209
+
210
+ puts "cloudflared is not installed."
211
+ puts ""
212
+ return if offer_to_install_cloudflared
213
+
214
+ puts "Install it with Homebrew:"
215
+ puts " #{BREW_INSTALL}"
216
+ puts ""
217
+ puts "Or see: #{CLOUDFLARED_DOWNLOADS}"
218
+ exit 1
219
+ end
220
+
221
+ # Only someone at a terminal can say yes; an agent or CI gets the command instead.
222
+ def offer_to_install_cloudflared
223
+ return false unless homebrew_installed? && $stdin.tty?
224
+
225
+ print "Install it now with Homebrew? [Y/n] "
226
+ answer = $stdin.gets.to_s.strip.downcase
227
+ return false unless answer.empty? || answer.start_with?("y")
228
+
229
+ if system(BREW_INSTALL) && cloudflared_installed?
230
+ puts ""
231
+ true
232
+ else
194
233
  puts ""
195
- puts "Install it with Homebrew:"
196
- puts " brew install cloudflare/cloudflare/cloudflared"
234
+ puts "Homebrew could not install cloudflared."
197
235
  puts ""
198
- puts "Or see: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
199
- exit 1
236
+ false
200
237
  end
201
238
  end
202
239
 
240
+ def cloudflared_installed?
241
+ system("which cloudflared > /dev/null 2>&1")
242
+ end
243
+
244
+ def homebrew_installed?
245
+ system("which brew > /dev/null 2>&1")
246
+ end
247
+
248
+ # check_upstream! prints its advice and exits; this keeps the advice as text.
249
+ def upstream_problem
250
+ original = $stdout
251
+ $stdout = StringIO.new
252
+ check_upstream!
253
+ nil
254
+ rescue SystemExit
255
+ $stdout.string.strip
256
+ ensure
257
+ $stdout = original
258
+ end
259
+
203
260
  def start_tunnel
204
261
  puts "Starting tunnel to #{@upstream}..."
205
262
  if @url
@@ -93,8 +93,8 @@ module RubyNative
93
93
  "the first native pass, the seven changes in the `ship` prompt (`ruby_native ship` prints it). " \
94
94
  "Then `bundle exec ruby_native preview` to see it on a phone."
95
95
  elsif !local["logged_in"]
96
- "create the app at #{Mcp::Platform::HOST}/apps/new (a name, the site's URL, and an account if you have none), " \
97
- "then `ruby_native login` and `ruby_native link`."
96
+ "`ruby_native login` (it opens the browser to sign in, or to create an account and confirm the email), " \
97
+ "then `ruby_native link --new --name=NAME --url=URL` to create the app, or `ruby_native link` if it already exists."
98
98
  else
99
99
  "run `ruby_native link` to pick the app on your account."
100
100
  end
@@ -83,6 +83,7 @@ module RubyNative
83
83
  puts "Commands:"
84
84
  puts " Every command that talks to rubynative.com reads the app from config/ruby_native.yml, or takes --app=ID."
85
85
  puts " link [APP_ID] Link this project to an app on your account (writes ruby_native.app_id)"
86
+ puts " --new --name=NAME --url=URL Create the app on your account first, then link it"
86
87
  puts " apple key PATH --key-id=KEY --issuer-id=ISSUER --team-id=TEAM"
87
88
  puts " Hand over the App Store Connect key (.p8) and wait for Apple to accept it"
88
89
  puts " check Validate your views against the signal vocabulary"
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.19.0"
2
+ VERSION = "0.19.1"
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.19.0
4
+ version: 0.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Masilotti