shopify-cli 0.9.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +3 -0
  3. data/CHANGELOG.md +26 -0
  4. data/{LICENSE.md → LICENSE} +0 -0
  5. data/RELEASING.md +51 -0
  6. data/Rakefile +1 -6
  7. data/docs/_config.yml +3 -0
  8. data/docs/_data/nav.yml +9 -0
  9. data/docs/getting-started/index.md +5 -40
  10. data/docs/getting-started/install/index.md +39 -0
  11. data/docs/getting-started/migrate/index.md +63 -0
  12. data/docs/getting-started/uninstall/index.md +37 -0
  13. data/docs/getting-started/upgrade/index.md +37 -0
  14. data/docs/index.md +5 -6
  15. data/lib/project_types/extension/cli.rb +2 -1
  16. data/lib/project_types/extension/commands/tunnel.rb +1 -1
  17. data/lib/project_types/extension/forms/register.rb +2 -3
  18. data/lib/project_types/extension/graphql/get_app_by_api_key.graphql +9 -0
  19. data/lib/project_types/extension/tasks/converters/app_converter.rb +27 -0
  20. data/lib/project_types/extension/tasks/get_app.rb +22 -0
  21. data/lib/project_types/extension/tasks/get_apps.rb +1 -6
  22. data/lib/project_types/node/forms/create.rb +3 -54
  23. data/lib/project_types/node/messages/messages.rb +1 -12
  24. data/lib/project_types/rails/forms/create.rb +3 -52
  25. data/lib/project_types/rails/messages/messages.rb +0 -11
  26. data/lib/project_types/script/cli.rb +5 -5
  27. data/lib/project_types/script/commands/create.rb +5 -4
  28. data/lib/project_types/script/commands/push.rb +1 -0
  29. data/lib/project_types/script/errors.rb +1 -0
  30. data/lib/project_types/script/forms/create.rb +8 -4
  31. data/lib/project_types/script/layers/application/build_script.rb +7 -10
  32. data/lib/project_types/script/layers/application/create_script.rb +16 -14
  33. data/lib/project_types/script/layers/application/project_dependencies.rb +3 -9
  34. data/lib/project_types/script/layers/application/push_script.rb +13 -11
  35. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +106 -0
  36. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +64 -0
  37. data/lib/project_types/script/layers/infrastructure/errors.rb +2 -2
  38. data/lib/project_types/script/layers/infrastructure/project_creator.rb +23 -0
  39. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +5 -2
  40. data/lib/project_types/script/layers/infrastructure/script_repository.rb +7 -37
  41. data/lib/project_types/script/layers/infrastructure/task_runner.rb +18 -0
  42. data/lib/project_types/script/messages/messages.rb +5 -6
  43. data/lib/project_types/script/script_project.rb +22 -9
  44. data/lib/project_types/script/templates/ts/as-pect.d.ts +1 -0
  45. data/lib/project_types/script/ui/error_handler.rb +5 -0
  46. data/lib/shopify-cli/admin_api.rb +1 -2
  47. data/lib/shopify-cli/admin_api/populate_resource_command.rb +10 -1
  48. data/lib/shopify-cli/api.rb +2 -0
  49. data/lib/shopify-cli/context.rb +51 -0
  50. data/lib/shopify-cli/core/entry_point.rb +6 -0
  51. data/lib/shopify-cli/core/finalize.rb +13 -0
  52. data/lib/shopify-cli/git.rb +14 -10
  53. data/lib/shopify-cli/messages/messages.rb +22 -2
  54. data/lib/shopify-cli/packager.rb +17 -25
  55. data/lib/shopify-cli/tasks.rb +1 -0
  56. data/lib/shopify-cli/tasks/select_org_and_shop.rb +77 -0
  57. data/lib/shopify-cli/tunnel.rb +33 -1
  58. data/lib/shopify-cli/version.rb +1 -1
  59. data/shopify-cli.gemspec +3 -5
  60. data/vendor/deps/cli-ui/REVISION +1 -1
  61. data/vendor/deps/cli-ui/lib/cli/ui.rb +52 -11
  62. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +11 -7
  63. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +34 -21
  64. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +107 -149
  65. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +99 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +119 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +158 -0
  68. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +112 -0
  69. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +9 -15
  70. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +47 -0
  71. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +9 -7
  72. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +39 -14
  73. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +62 -44
  74. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +7 -2
  75. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +23 -3
  76. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +34 -10
  77. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +12 -7
  78. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +26 -16
  79. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +3 -3
  80. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +75 -0
  81. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  82. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  83. metadata +30 -13
  84. data/lib/project_types/extension/features/tunnel_url.rb +0 -20
  85. data/lib/project_types/script/layers/infrastructure/assemblyscript_dependency_manager.rb +0 -73
  86. data/lib/project_types/script/layers/infrastructure/assemblyscript_wasm_builder.rb +0 -39
  87. data/lib/project_types/script/layers/infrastructure/dependency_manager.rb +0 -36
  88. data/lib/project_types/script/layers/infrastructure/script_builder.rb +0 -34
  89. data/lib/project_types/script/layers/infrastructure/test_suite_repository.rb +0 -59
  90. data/vendor/deps/cli-ui/lib/cli/ui/box.rb +0 -15
@@ -0,0 +1 @@
1
+ /// <reference types="@as-pect/assembly/types/as-pect" />
@@ -44,6 +44,11 @@ module Script
44
44
  cause_of_error: ShopifyCli::Context.message('script.error.invalid_context_cause'),
45
45
  help_suggestion: ShopifyCli::Context.message('script.error.invalid_context_help'),
46
46
  }
47
+ when Errors::InvalidScriptNameError
48
+ {
49
+ cause_of_error: ShopifyCli::Context.message('script.error.invalid_script_name_cause'),
50
+ help_suggestion: ShopifyCli::Context.message('script.error.invalid_script_name_help'),
51
+ }
47
52
  when Errors::NoExistingAppsError
48
53
  {
49
54
  cause_of_error: ShopifyCli::Context.message('script.error.no_existing_apps_cause'),
@@ -37,8 +37,7 @@ module ShopifyCli
37
37
  #
38
38
  # ShopifyCli::AdminAPI.query(@ctx, 'all_organizations')
39
39
  #
40
- def query(ctx, query_name, api_version: nil, shop: nil, **variables)
41
- shop ||= Project.current.env.shop
40
+ def query(ctx, query_name, shop:, api_version: nil, **variables)
42
41
  authenticated_req(ctx, shop) do
43
42
  api_client(ctx, api_version, shop).query(query_name, variables: variables)
44
43
  end
@@ -41,6 +41,8 @@ module ShopifyCli
41
41
  return @ctx.puts(output)
42
42
  end
43
43
 
44
+ @shop ||= Project.current.env.shop || get_shop(@ctx)
45
+
44
46
  if @silent
45
47
  spin_group = CLI::UI::SpinGroup.new
46
48
  spin_group.add(@ctx.message('core.populate.populating', @count, camel_case_resource_type)) do |spinner|
@@ -114,7 +116,7 @@ module ShopifyCli
114
116
 
115
117
  def run_mutation(data)
116
118
  kwargs = { input: data }
117
- kwargs[:shop] = @shop if @shop
119
+ kwargs[:shop] = @shop
118
120
  resp = AdminAPI.query(
119
121
  @ctx, "create_#{snake_case_resource_type}", kwargs
120
122
  )
@@ -145,6 +147,13 @@ module ShopifyCli
145
147
 
146
148
  private
147
149
 
150
+ def get_shop(ctx)
151
+ res = ShopifyCli::Tasks::SelectOrgAndShop.call(ctx)
152
+ domain = res[:shop_domain]
153
+ Project.current.env.update(ctx, :shop, domain)
154
+ domain
155
+ end
156
+
148
157
  def camel_case_resource_type
149
158
  @camel_case_resource_type ||= self.class.to_s.split('::').last
150
159
  end
@@ -32,6 +32,8 @@ module ShopifyCli
32
32
  )
33
33
  ctx.debug(resp)
34
34
  resp
35
+ rescue API::APIRequestServerError, API::APIRequestUnexpectedError
36
+ ctx.puts(ctx.message('core.api.error.internal_server_error'))
35
37
  end
36
38
 
37
39
  protected
@@ -131,6 +131,47 @@ module ShopifyCli
131
131
  File.write(ctx_path(fname), content)
132
132
  end
133
133
 
134
+ # will change directories and update the root, the filepath is relative to the command root unless absolute
135
+ #
136
+ # #### Parameters
137
+ # * `path` - the file path to a directory, relative to the context root to remove from the FS
138
+ #
139
+ def chdir(path)
140
+ Dir.chdir(ctx_path(path))
141
+ self.root = ctx_path(path)
142
+ end
143
+
144
+ # checks if a directory exists, the filepath is relative to the command root unless absolute
145
+ #
146
+ # #### Parameters
147
+ # * `path` - the file path to a directory, relative to the context root to remove from the FS
148
+ #
149
+ def exist?(path)
150
+ Dir.exist?(ctx_path(path))
151
+ end
152
+
153
+ # will recursively copy a directory from the FS, the filepath is relative to the command
154
+ # root unless absolute
155
+ #
156
+ # #### Parameters
157
+ # * `from` - the path of the original file
158
+ # * `to` - the destination path
159
+ #
160
+ def cp_r(from, to)
161
+ FileUtils.cp_r(ctx_path(from), ctx_path(to))
162
+ end
163
+
164
+ # will copy a directory from the FS, the filepath is relative to the command
165
+ # root unless absolute
166
+ #
167
+ # #### Parameters
168
+ # * `from` - the path of the original file
169
+ # * `to` - the destination path
170
+ #
171
+ def cp(from, to)
172
+ FileUtils.cp(ctx_path(from), ctx_path(to))
173
+ end
174
+
134
175
  # will rename a file from one place to another, relative to the command root
135
176
  # unless the path is absolute.
136
177
  #
@@ -162,6 +203,16 @@ module ShopifyCli
162
203
  FileUtils.rm_r(ctx_path(fname))
163
204
  end
164
205
 
206
+ # will force remove a directory from the FS, the filepath is relative to the command
207
+ # root unless absolute
208
+ #
209
+ # #### Parameters
210
+ # * `fname` - the file path to a directory, relative to the context root to remove from the FS
211
+ #
212
+ def rm_rf(fname)
213
+ FileUtils.rm_rf(ctx_path(fname))
214
+ end
215
+
165
216
  # will create a directory, recursively if it does not exist. So if you create
166
217
  # a directory `foo/bar/dun`, this will also create the directories `foo` and
167
218
  # `foo/bar` if they do not exist. The path will be made relative to the command
@@ -11,6 +11,12 @@ module ShopifyCli
11
11
  IO.open(9) { is_shell_shim = true }
12
12
  rescue Errno::EBADF
13
13
  # This is expected if the descriptor doesn't exist
14
+ rescue ArgumentError => e
15
+ # This can happen on RVM, because it can use fd 9 itself and block access to it. That only happens if the fd
16
+ # did not exist beforehand, so that means there was no fd 9 before Ruby started.
17
+ unless e.message == 'The given fd is not accessible because RubyVM reserves it'
18
+ raise e
19
+ end
14
20
  end
15
21
 
16
22
  if !ctx.testing? && is_shell_shim
@@ -0,0 +1,13 @@
1
+ module ShopifyCli
2
+ module Core
3
+ # This class is just a dummy to make sure that we don't trigger warnings on the first time the updated code runs.
4
+ # The old code would try to call the Finalizer after it is done updating, which would then trigger an autoload of
5
+ # this class and fail.
6
+ module Finalize
7
+ class << self
8
+ def deliver!
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -46,10 +46,10 @@ module ShopifyCli
46
46
  if Dir.exist?(dest)
47
47
  ctx.abort(ctx.message('core.git.error.directory_exists'))
48
48
  else
49
- CLI::UI::Frame.open(ctx.message('core.git.cloning', dest)) do
49
+ success_message = ctx.message('core.git.cloned', dest)
50
+ CLI::UI::Frame.open(ctx.message('core.git.cloning', repository, dest), success_text: success_message) do
50
51
  clone_progress('clone', '--single-branch', repository, dest, ctx: ctx)
51
52
  end
52
- ctx.done(ctx.message('core.git.cloned', dest))
53
53
  end
54
54
  end
55
55
 
@@ -121,19 +121,23 @@ module ShopifyCli
121
121
  def clone_progress(*git_command, ctx:)
122
122
  CLI::UI::Progress.progress do |bar|
123
123
  msg = []
124
- success = ctx.system('git', *git_command, '--progress') do |_out, err|
125
- if err.strip.start_with?('Receiving objects:')
126
- percent = (err.match(/Receiving objects:\s+(\d+)/)[1].to_f / 100).round(2)
124
+ require 'open3'
125
+
126
+ success = Open3.popen3('git', *git_command, '--progress') do |_stdin, _stdout, stderr, thread|
127
+ while (line = stderr.gets)
128
+ next unless line.strip.start_with?('Receiving objects:')
129
+ percent = (line.match(/Receiving objects:\s+(\d+)/)[1].to_f / 100).round(2)
127
130
  bar.tick(set_percent: percent)
128
131
  next
129
132
  end
130
- msg << err
133
+
134
+ msg << stderr
135
+ thread.value
131
136
  end.success?
132
- unless success
133
- ctx.abort(msg.join("\n"))
134
- end
137
+
138
+ ctx.abort(msg.join("\n")) unless success
135
139
  bar.tick(set_percent: 1.0)
136
- true
140
+ success
137
141
  end
138
142
  end
139
143
  end
@@ -60,8 +60,8 @@ module ShopifyCli
60
60
  no_commits_made: "No git commits have been made. Please make at least one commit.",
61
61
  },
62
62
 
63
- cloning: "Cloning into %s...",
64
- cloned: "Cloned into %s",
63
+ cloning: "Cloning %s into %s...",
64
+ cloned: "{{v}} Cloned into %s",
65
65
  },
66
66
 
67
67
  help: {
@@ -154,6 +154,12 @@ module ShopifyCli
154
154
  },
155
155
  },
156
156
 
157
+ api: {
158
+ error: {
159
+ internal_server_error: '{{red:{{x}} An unexpected error occurred on Shopify.}}',
160
+ },
161
+ },
162
+
157
163
  populate: {
158
164
  options: {
159
165
  header: "{{bold:{{cyan:%s}} options:}}",
@@ -245,6 +251,20 @@ module ShopifyCli
245
251
  "{{x}} error: For authentication issues, run {{command:%s logout}} to clear invalid credentials",
246
252
  update_prompt: "Do you want to update your application url?",
247
253
  },
254
+ select_org_and_shop: {
255
+ authentication_issue: "For authentication issues, run {{command:%s logout}} to clear invalid credentials",
256
+ create_store: "Visit {{underline:https://partners.shopify.com/%s/stores}} to create one",
257
+ development_store: "Using development store {{green:%s}}",
258
+ development_store_select: "Select a development store",
259
+ error: {
260
+ no_development_stores: "{{x}} No Development Stores available.",
261
+ no_organizations: "No partner organizations available.",
262
+ organization_not_found: "Cannot find a partner organization with that ID",
263
+ partners_notice: "Please visit https://partners.shopify.com/ to create a partners account",
264
+ },
265
+ organization: "Partner organization {{green:%s (%s)}}",
266
+ organization_select: "Select partner organization",
267
+ },
248
268
  },
249
269
 
250
270
  tunnel: {
@@ -7,22 +7,8 @@ module ShopifyCli
7
7
  FileUtils.mkdir_p(BUILDS_DIR)
8
8
  end
9
9
 
10
- def build_gem
11
- build_path = gem_path
12
- puts "\nBuilding gem"
13
-
14
- puts "Outputting gem to:\n #{build_path}\n\n"
15
- raise "Failed to build gem" unless system(
16
- 'gem',
17
- 'build',
18
- '-o',
19
- build_path,
20
- File.join(ShopifyCli::ROOT, 'shopify-cli.gemspec')
21
- )
22
- end
23
-
24
10
  def build_debian
25
- ensure_program_installed('dpkg-deb')
11
+ ensure_program_installed('dpkg-deb', 'brew install dpkg')
26
12
 
27
13
  root_dir = File.join(PACKAGING_DIR, 'debian')
28
14
  debian_dir = File.join(root_dir, 'shopify-cli', 'DEBIAN')
@@ -54,7 +40,7 @@ module ShopifyCli
54
40
  end
55
41
 
56
42
  def build_rpm
57
- ensure_program_installed('rpmbuild')
43
+ ensure_program_installed('rpmbuild', 'brew install rpm')
58
44
 
59
45
  root_dir = File.join(PACKAGING_DIR, 'rpm')
60
46
  rpm_build_dir = File.join(root_dir, 'build')
@@ -92,10 +78,14 @@ module ShopifyCli
92
78
  spec_contents = File.read(File.join(root_dir, 'shopify-cli.base.rb'))
93
79
  spec_contents = spec_contents.gsub('SHOPIFY_CLI_VERSION', ShopifyCli::VERSION)
94
80
 
95
- checksum = %x`shasum -a 256 #{gem_path}`
96
- gem_checksum = checksum.split(' ')[0]
81
+ puts "Grabbing sha256 checksum from Rubygems.org"
82
+ require 'digest/sha2'
83
+ require 'open-uri'
84
+ gem_checksum = open("https://rubygems.org/downloads/shopify-cli-#{ShopifyCli::VERSION}.gem") do |io|
85
+ Digest::SHA256.new.hexdigest(io.read)
86
+ end
97
87
 
98
- puts "Got sha256 sum for gem: #{gem_checksum}"
88
+ puts "Got sha256 checksum for gem: #{gem_checksum}"
99
89
  spec_contents = spec_contents.gsub('SHOPIFY_CLI_GEM_CHECKSUM', gem_checksum)
100
90
 
101
91
  puts "Writing generated formula\n To: #{build_path}\n\n"
@@ -104,13 +94,15 @@ module ShopifyCli
104
94
 
105
95
  private
106
96
 
107
- def ensure_program_installed(program)
108
- raise "Could not find program #{program} which is required to build the package" unless
109
- system(program, '--version', out: File::NULL, err: File::NULL)
110
- end
97
+ def ensure_program_installed(program, installation_cmd)
98
+ unless system(program, '--version', out: File::NULL, err: File::NULL)
99
+ raise <<~MESSAGE
100
+
101
+ Could not find program #{program} which is required to build the package.
102
+ You can install it by running `#{installation_cmd}`.
111
103
 
112
- def gem_path
113
- File.join(BUILDS_DIR, "shopify-cli.gem")
104
+ MESSAGE
105
+ end
114
106
  end
115
107
  end
116
108
  end
@@ -27,6 +27,7 @@ module ShopifyCli
27
27
  register :EnsureEnv, :ensure_env, 'shopify-cli/tasks/ensure_env'
28
28
  register :EnsureLoopbackURL, :ensure_loopback_url, 'shopify-cli/tasks/ensure_loopback_url'
29
29
  register :EnsureDevStore, :ensure_dev_store, 'shopify-cli/tasks/ensure_dev_store'
30
+ register :SelectOrgAndShop, :select_org_and_shop, 'shopify-cli/tasks/select_org_and_shop'
30
31
  register :UpdateDashboardURLS, :update_dashboard_urls, 'shopify-cli/tasks/update_dashboard_urls'
31
32
  end
32
33
  end
@@ -0,0 +1,77 @@
1
+ require 'shopify_cli'
2
+
3
+ module ShopifyCli
4
+ module Tasks
5
+ class SelectOrgAndShop < ShopifyCli::Task
6
+ attr_reader :ctx
7
+
8
+ def call(ctx, organization_id: nil, shop_domain: nil)
9
+ @ctx = ctx
10
+ return response(organization_id.to_i, shop_domain) unless organization_id.nil? || shop_domain.nil?
11
+ org = get_organization(organization_id)
12
+ shop_domain ||= get_shop_domain(org)
13
+ response(org["id"].to_i, shop_domain)
14
+ end
15
+
16
+ private
17
+
18
+ def response(organization_id, shop_domain)
19
+ {
20
+ organization_id: organization_id,
21
+ shop_domain: shop_domain,
22
+ }
23
+ end
24
+
25
+ def organizations
26
+ @organizations ||= ShopifyCli::PartnersAPI::Organizations.fetch_all(ctx)
27
+ end
28
+
29
+ def get_organization(organization_id)
30
+ @organization ||= if !organization_id.nil?
31
+ org = ShopifyCli::PartnersAPI::Organizations.fetch(ctx, id: organization_id)
32
+ if org.nil?
33
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.error.authentication_issue', ShopifyCli::TOOL_NAME))
34
+ ctx.abort(ctx.message('core.tasks.select_org_and_shop.error.organization_not_found'))
35
+ end
36
+ org
37
+ elsif organizations.count == 0
38
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.error.partners_notice'))
39
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.authentication_issue', ShopifyCli::TOOL_NAME))
40
+ ctx.abort(ctx.message('core.tasks.select_org_and_shop.error.no_organizations'))
41
+ elsif organizations.count == 1
42
+ org = organizations.first
43
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.organization', org['businessName'], org['id']))
44
+ org
45
+ else
46
+ org_id = CLI::UI::Prompt.ask(ctx.message('core.tasks.select_org_and_shop.organization_select')) do |handler|
47
+ organizations.each do |o|
48
+ handler.option(ctx.message('core.partners_api.org_name_and_id', o['businessName'], o['id'])) { o['id'] }
49
+ end
50
+ end
51
+ organizations.find { |o| o['id'] == org_id }
52
+ end
53
+ end
54
+
55
+ def get_shop_domain(organization)
56
+ valid_stores = organization['stores'].select do |store|
57
+ store['transferDisabled'] == true || store['convertableToPartnerTest'] == true
58
+ end
59
+
60
+ if valid_stores.count == 0
61
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.error.no_development_stores'))
62
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.create_store', organization['id']))
63
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.authentication_issue', ShopifyCli::TOOL_NAME))
64
+ elsif valid_stores.count == 1
65
+ domain = valid_stores.first['shopDomain']
66
+ ctx.puts(ctx.message('core.tasks.select_org_and_shop.development_store', domain))
67
+ domain
68
+ else
69
+ CLI::UI::Prompt.ask(
70
+ ctx.message('core.tasks.select_org_and_shop.development_store_select'),
71
+ options: valid_stores.map { |s| s['shopDomain'] }
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -11,7 +11,7 @@ module ShopifyCli
11
11
  class Tunnel
12
12
  extend SingleForwardable
13
13
 
14
- def_delegators :new, :start, :stop, :auth
14
+ def_delegators :new, :start, :stop, :auth, :stats, :urls
15
15
 
16
16
  class FetchUrlError < RuntimeError; end
17
17
  class NgrokError < RuntimeError; end
@@ -23,6 +23,10 @@ module ShopifyCli
23
23
  linux: 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip',
24
24
  }
25
25
 
26
+ NGROK_TUNNELS_URI = URI.parse('http://localhost:4040/api/tunnels')
27
+ TUNNELS_FIELD = 'tunnels'
28
+ PUBLIC_URL_FIELD = 'public_url'
29
+
26
30
  ##
27
31
  # will find and stop a running tunnel process. It will also output if the
28
32
  # operation was successful or not
@@ -82,6 +86,34 @@ module ShopifyCli
82
86
  ctx.system(File.join(ShopifyCli::CACHE_DIR, 'ngrok'), 'authtoken', token)
83
87
  end
84
88
 
89
+ ##
90
+ # will return the statistics of the current running tunnels
91
+ #
92
+ # #### Returns
93
+ #
94
+ # * `stats` - the hash of running statistics returning from the ngrok api
95
+ #
96
+ def stats
97
+ response = Net::HTTP.get_response(NGROK_TUNNELS_URI)
98
+ JSON.parse(response.body)
99
+ rescue
100
+ {}
101
+ end
102
+
103
+ ##
104
+ # will return the urls of the current running tunnels
105
+ #
106
+ # #### Returns
107
+ #
108
+ # * `stats` - the array of urls
109
+ #
110
+ def urls
111
+ tunnels = stats.dig(TUNNELS_FIELD)
112
+ tunnels.map { |tunnel| tunnel.dig(PUBLIC_URL_FIELD) }
113
+ rescue
114
+ []
115
+ end
116
+
85
117
  private
86
118
 
87
119
  def install(ctx)