shopify-cli 0.9.2 → 1.0.3

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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -3
  3. data/docs/_config.yml +3 -0
  4. data/docs/_data/nav.yml +9 -0
  5. data/docs/getting-started/index.md +5 -40
  6. data/docs/getting-started/install/index.md +75 -0
  7. data/docs/getting-started/migrate/index.md +96 -0
  8. data/docs/getting-started/uninstall/index.md +37 -0
  9. data/docs/getting-started/upgrade/index.md +37 -0
  10. data/docs/index.md +5 -6
  11. data/lib/project_types/extension/cli.rb +3 -2
  12. data/lib/project_types/extension/commands/build.rb +1 -1
  13. data/lib/project_types/extension/commands/tunnel.rb +1 -1
  14. data/lib/project_types/extension/forms/register.rb +2 -3
  15. data/lib/project_types/extension/graphql/get_app_by_api_key.graphql +9 -0
  16. data/lib/project_types/extension/tasks/converters/app_converter.rb +27 -0
  17. data/lib/project_types/extension/tasks/get_app.rb +22 -0
  18. data/lib/project_types/extension/tasks/get_apps.rb +1 -6
  19. data/lib/project_types/node/forms/create.rb +3 -54
  20. data/lib/project_types/node/messages/messages.rb +3 -14
  21. data/lib/project_types/rails/cli.rb +0 -1
  22. data/lib/project_types/rails/forms/create.rb +3 -52
  23. data/lib/project_types/rails/messages/messages.rb +2 -13
  24. data/lib/project_types/script/cli.rb +5 -5
  25. data/lib/project_types/script/commands/create.rb +5 -4
  26. data/lib/project_types/script/commands/disable.rb +4 -14
  27. data/lib/project_types/script/commands/enable.rb +35 -11
  28. data/lib/project_types/script/commands/push.rb +9 -9
  29. data/lib/project_types/script/config/extension_points.yml +9 -3
  30. data/lib/project_types/script/errors.rb +1 -0
  31. data/lib/project_types/script/forms/create.rb +8 -4
  32. data/lib/project_types/script/forms/script_form.rb +5 -2
  33. data/lib/project_types/script/layers/application/create_script.rb +14 -20
  34. data/lib/project_types/script/layers/application/disable_script.rb +9 -7
  35. data/lib/project_types/script/layers/application/enable_script.rb +11 -9
  36. data/lib/project_types/script/layers/application/project_dependencies.rb +0 -5
  37. data/lib/project_types/script/layers/application/push_script.rb +6 -4
  38. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +106 -0
  39. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +2 -2
  40. data/lib/project_types/script/layers/infrastructure/errors.rb +2 -1
  41. data/lib/project_types/script/layers/infrastructure/project_creator.rb +23 -0
  42. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +1 -1
  43. data/lib/project_types/script/layers/infrastructure/script_repository.rb +1 -34
  44. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  45. data/lib/project_types/script/messages/messages.rb +21 -25
  46. data/lib/project_types/script/script_project.rb +8 -4
  47. data/lib/project_types/script/templates/ts/as-pect.config.js +6 -0
  48. data/lib/project_types/script/templates/ts/as-pect.d.ts +1 -0
  49. data/lib/project_types/script/ui/error_handler.rb +9 -0
  50. data/lib/project_types/script/ui/printing_spinner.rb +75 -0
  51. data/lib/shopify-cli/admin_api.rb +1 -2
  52. data/lib/shopify-cli/admin_api/populate_resource_command.rb +10 -1
  53. data/lib/shopify-cli/admin_api/schema.rb +20 -8
  54. data/lib/shopify-cli/command.rb +2 -5
  55. data/lib/shopify-cli/commands.rb +1 -0
  56. data/lib/shopify-cli/commands/config.rb +44 -0
  57. data/lib/shopify-cli/commands/connect.rb +17 -10
  58. data/lib/shopify-cli/commands/create.rb +1 -1
  59. data/lib/shopify-cli/commands/help.rb +1 -1
  60. data/lib/shopify-cli/commands/system.rb +1 -1
  61. data/lib/shopify-cli/context.rb +10 -1
  62. data/lib/shopify-cli/core.rb +0 -1
  63. data/lib/shopify-cli/core/entry_point.rb +6 -0
  64. data/lib/shopify-cli/core/finalize.rb +13 -0
  65. data/lib/shopify-cli/feature.rb +97 -0
  66. data/lib/shopify-cli/messages/messages.rb +45 -2
  67. data/lib/shopify-cli/partners_api/organizations.rb +7 -7
  68. data/lib/shopify-cli/project_type.rb +2 -5
  69. data/lib/shopify-cli/tasks.rb +1 -0
  70. data/lib/shopify-cli/tasks/ensure_env.rb +0 -1
  71. data/lib/shopify-cli/tasks/select_org_and_shop.rb +77 -0
  72. data/lib/shopify-cli/tasks/update_dashboard_urls.rb +4 -3
  73. data/lib/shopify-cli/tunnel.rb +66 -10
  74. data/lib/shopify-cli/version.rb +1 -1
  75. data/lib/shopify_cli.rb +1 -0
  76. data/vendor/deps/cli-ui/REVISION +1 -1
  77. data/vendor/deps/cli-ui/lib/cli/ui.rb +52 -11
  78. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +11 -7
  79. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +34 -21
  80. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +107 -149
  81. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +99 -0
  82. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +119 -0
  83. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +158 -0
  84. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +112 -0
  85. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +9 -15
  86. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +47 -0
  87. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +9 -7
  88. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +39 -14
  89. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +62 -44
  90. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +7 -2
  91. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +23 -3
  92. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +34 -10
  93. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +12 -7
  94. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +26 -16
  95. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +3 -3
  96. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +75 -0
  97. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  98. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  99. metadata +25 -9
  100. data/lib/project_types/extension/features/tunnel_url.rb +0 -20
  101. data/lib/project_types/script/forms/enable.rb +0 -24
  102. data/lib/project_types/script/forms/push.rb +0 -19
  103. data/lib/project_types/script/layers/infrastructure/assemblyscript_dependency_manager.rb +0 -51
  104. data/lib/project_types/script/layers/infrastructure/dependency_manager.rb +0 -36
  105. data/lib/project_types/script/layers/infrastructure/test_suite_repository.rb +0 -62
  106. data/vendor/deps/cli-ui/lib/cli/ui/box.rb +0 -15
@@ -28,7 +28,7 @@ module ShopifyCli
28
28
  def self.all_visible_type
29
29
  ProjectType
30
30
  .load_all
31
- .select { |type| !type.hidden }
31
+ .select { |type| !type.hidden? }
32
32
  end
33
33
 
34
34
  def self.help
@@ -47,7 +47,7 @@ module ShopifyCli
47
47
 
48
48
  def core_commands
49
49
  resolved_commands
50
- .select { |_name, c| !c.hidden }
50
+ .select { |_name, c| !c.hidden? }
51
51
  .select { |name, _c| Commands.core_command?(name) }
52
52
  end
53
53
 
@@ -4,7 +4,7 @@ require 'rbconfig'
4
4
  module ShopifyCli
5
5
  module Commands
6
6
  class System < ShopifyCli::Command
7
- hidden_command
7
+ hidden_feature(feature_set: :debug)
8
8
 
9
9
  def call(args, _name)
10
10
  show_all_details = false
@@ -146,10 +146,19 @@ module ShopifyCli
146
146
  # #### Parameters
147
147
  # * `path` - the file path to a directory, relative to the context root to remove from the FS
148
148
  #
149
- def exist?(path)
149
+ def dir_exist?(path)
150
150
  Dir.exist?(ctx_path(path))
151
151
  end
152
152
 
153
+ # checks if a file exists, the filepath is relative to the command root unless absolute
154
+ #
155
+ # #### Parameters
156
+ # * `path` - the file path to a file, relative to the context root to remove from the FS
157
+ #
158
+ def file_exist?(path)
159
+ File.exist?(ctx_path(path))
160
+ end
161
+
153
162
  # will recursively copy a directory from the FS, the filepath is relative to the command
154
163
  # root unless absolute
155
164
  #
@@ -4,6 +4,5 @@ module ShopifyCli
4
4
  autoload :Executor, 'shopify-cli/core/executor'
5
5
  autoload :HelpResolver, 'shopify-cli/core/help_resolver'
6
6
  autoload :Monorail, 'shopify-cli/core/monorail'
7
- autoload :Update, 'shopify-cli/core/update'
8
7
  end
9
8
  end
@@ -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
@@ -0,0 +1,97 @@
1
+ module ShopifyCli
2
+ ##
3
+ # ShopifyCli::Feature contains the logic to hide and show features across the CLI
4
+ # These features can be either commands or project types currently.
5
+ #
6
+ # Feature flags will persist between runs so if the flag is enabled or disabled,
7
+ # it will still be in that same state on the next cli invocation.
8
+ class Feature
9
+ SECTION = 'features'
10
+
11
+ ##
12
+ # ShopifyCli::Feature::Set is included on commands and projects to allow you to hide
13
+ # and enable projects and commands based on feature flags.
14
+ module Set
15
+ ##
16
+ # will hide a feature, either a project_type or a command
17
+ #
18
+ # #### Parameters
19
+ #
20
+ # * `feature_set` - either a single, or array of symbols that represent feature sets
21
+ #
22
+ # #### Example
23
+ #
24
+ # module ShopifyCli
25
+ # module Commands
26
+ # class Config < ShopifyCli::Command
27
+ # hidden_feature(feature_set: :basic)
28
+ # ....
29
+ #
30
+ def hidden_feature(feature_set: [])
31
+ @feature_hidden = true
32
+ @hidden_feature_set = Array(feature_set).compact
33
+ end
34
+
35
+ ##
36
+ # will return if the feature has been hidden or not
37
+ #
38
+ # #### Returns
39
+ #
40
+ # * `is_hidden` - returns true if the feature has been hidden and false otherwise
41
+ #
42
+ # #### Example
43
+ #
44
+ # ShopifyCli::Commands::Config.hidden?
45
+ #
46
+ def hidden?
47
+ enabled = (@hidden_feature_set || []).any? do |feature|
48
+ Feature.enabled?(feature)
49
+ end
50
+ @feature_hidden && !enabled
51
+ end
52
+ end
53
+
54
+ class << self
55
+ ##
56
+ # will enable a feature in the CLI.
57
+ #
58
+ # #### Parameters
59
+ #
60
+ # * `feature` - a symbol representing the flag to be enabled
61
+ def enable(feature)
62
+ set(feature, true)
63
+ end
64
+
65
+ ##
66
+ # will disable a feature in the CLI.
67
+ #
68
+ # #### Parameters
69
+ #
70
+ # * `feature` - a symbol representing the flag to be disabled
71
+ def disable(feature)
72
+ set(feature, false)
73
+ end
74
+
75
+ ##
76
+ # will check if the feature has been enabled
77
+ #
78
+ # #### Parameters
79
+ #
80
+ # * `feature` - a symbol representing a flag that the status should be requested
81
+ #
82
+ # #### Returns
83
+ #
84
+ # * `is_enabled` - will be true if the feature has been enabled.
85
+ def enabled?(feature)
86
+ return false if feature.nil?
87
+ ShopifyCli::Config.get_bool(SECTION, feature.to_s)
88
+ end
89
+
90
+ private
91
+
92
+ def set(feature, value)
93
+ ShopifyCli::Config.set(SECTION, feature.to_s, value)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -24,6 +24,15 @@ module ShopifyCli
24
24
  MESSAGE
25
25
  development_store_select: "Which development store would you like to use?",
26
26
  cli_yml_saved: ".shopify-cli.yml saved to project root",
27
+
28
+ no_apps: 'You have no apps to connect to, creating a new app.',
29
+ app_name: "App name",
30
+ app_type: {
31
+ select: "What type of app are you building?",
32
+ select_public: "Public: An app built for a wide merchant audience.",
33
+ select_custom: "Custom: An app custom built for a single client.",
34
+ selected: "App type {{green:%s}}",
35
+ },
27
36
  },
28
37
 
29
38
  context: {
@@ -52,6 +61,19 @@ module ShopifyCli
52
61
  saved: "%s saved to project root",
53
62
  },
54
63
 
64
+ config: {
65
+ help: <<~HELP,
66
+ Change configuration of how the CLI operates
67
+ Usage: {{command:%s config [ feature ] [ feature_name ] }}
68
+ HELP
69
+ feature: {
70
+ enabled: "{{v}} feature {{green:%s}} was enabled",
71
+ disabled: "{{v}} feature {{green:%s}} was disabled",
72
+ is_enabled: "{{v}} feature {{green:%s}} is enabled",
73
+ is_disabled: "{{v}} feature {{green:%s}} is disabled",
74
+ },
75
+ },
76
+
55
77
  git: {
56
78
  error: {
57
79
  directory_exists: "Project directory already exists. Please create a project with a new name.",
@@ -251,6 +273,20 @@ module ShopifyCli
251
273
  "{{x}} error: For authentication issues, run {{command:%s logout}} to clear invalid credentials",
252
274
  update_prompt: "Do you want to update your application url?",
253
275
  },
276
+ select_org_and_shop: {
277
+ authentication_issue: "For authentication issues, run {{command:%s logout}} to clear invalid credentials",
278
+ create_store: "Visit {{underline:https://partners.shopify.com/%s/stores}} to create one",
279
+ development_store: "Using development store {{green:%s}}",
280
+ development_store_select: "Select a development store",
281
+ error: {
282
+ no_development_stores: "{{x}} No Development Stores available.",
283
+ no_organizations: "No partner organizations available.",
284
+ organization_not_found: "Cannot find a partner organization with that ID",
285
+ partners_notice: "Please visit https://partners.shopify.com/ to create a partners account",
286
+ },
287
+ organization: "Partner organization {{green:%s (%s)}}",
288
+ organization_select: "Select partner organization",
289
+ },
254
290
  },
255
291
 
256
292
  tunnel: {
@@ -259,10 +295,17 @@ module ShopifyCli
259
295
  url_fetch_failure: "Unable to fetch external url",
260
296
  },
261
297
 
262
- stopped: "{{green:x}} ngrok tunnel stopped",
263
298
  not_running: "{{green:x}} ngrok tunnel not running",
264
- start_with_account: "{{v}} ngrok tunnel running at {{underline:%s}}, with account %s",
299
+ signup_suggestion: <<~MESSAGE,
300
+ {{*}} To avoid tunnels that timeout, it is recommended to signup for a free ngrok
301
+ account at {{underline:https://ngrok.com/signup}}. After you signup, install your
302
+ personalized authorization token using {{command:%s tunnel auth <token>}}.
303
+ MESSAGE
265
304
  start: "{{v}} ngrok tunnel running at {{underline:%s}}",
305
+ start_with_account: "{{v}} ngrok tunnel running at {{underline:%s}}, with account %s",
306
+ stopped: "{{green:x}} ngrok tunnel stopped",
307
+ timed_out: "{{x}} ngrok tunnel has timed out, restarting ...",
308
+ will_timeout: "{{*}} This tunnel will timeout in {{red:%s}}",
266
309
  },
267
310
 
268
311
  version: {
@@ -4,25 +4,25 @@ module ShopifyCli
4
4
  class << self
5
5
  def fetch_all(ctx)
6
6
  resp = PartnersAPI.query(ctx, 'all_organizations')
7
- resp['data']['organizations']['nodes'].map do |org|
8
- org['stores'] = org['stores']['nodes']
7
+ (resp.dig('data', 'organizations', 'nodes') || []).map do |org|
8
+ org['stores'] = (org.dig('stores', 'nodes') || [])
9
9
  org
10
10
  end
11
11
  end
12
12
 
13
13
  def fetch(ctx, id:)
14
14
  resp = PartnersAPI.query(ctx, 'find_organization', id: id)
15
- org = resp['data']['organizations']['nodes'].first
15
+ org = resp.dig('data', 'organizations', 'nodes').first
16
16
  return nil if org.nil?
17
- org['stores'] = org['stores']['nodes']
17
+ org['stores'] = (org.dig('stores', 'nodes') || [])
18
18
  org
19
19
  end
20
20
 
21
21
  def fetch_with_app(ctx)
22
22
  resp = PartnersAPI.query(ctx, 'all_orgs_with_apps')
23
- resp['data']['organizations']['nodes'].map do |org|
24
- org['stores'] = org['stores']['nodes']
25
- org['apps'] = org['apps']['nodes']
23
+ (resp.dig('data', 'organizations', 'nodes') || []).map do |org|
24
+ org['stores'] = (org.dig('stores', 'nodes') || [])
25
+ org['apps'] = (org.dig('apps', 'nodes') || [])
26
26
  org
27
27
  end
28
28
  end
@@ -1,11 +1,12 @@
1
1
  module ShopifyCli
2
2
  class ProjectType
3
+ extend Feature::Set
4
+
3
5
  class << self
4
6
  attr_accessor :project_type,
5
7
  :project_name,
6
8
  :project_creator_command_class,
7
9
  :project_load_shallow
8
- attr_reader :hidden
9
10
 
10
11
  def repository
11
12
  @repository ||= []
@@ -53,10 +54,6 @@ module ShopifyCli
53
54
  const_get(@project_creator_command_class)
54
55
  end
55
56
 
56
- def hidden_project_type
57
- @hidden = true
58
- end
59
-
60
57
  def register_command(const, cmd)
61
58
  return if project_load_shallow
62
59
  Context.new.abort(
@@ -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
@@ -14,7 +14,6 @@ module ShopifyCli
14
14
  api_key = CLI::UI.ask(@ctx.message('core.tasks.ensure_env.api_key_question'))
15
15
  api_secret = CLI::UI.ask(@ctx.message('core.tasks.ensure_env.api_secret_key_question'))
16
16
  shop = CLI::UI.ask(@ctx.message('core.tasks.ensure_env.development_store_question'))
17
-
18
17
  shop.gsub!(/https?\:\/\//, '')
19
18
 
20
19
  env = Resources::EnvFile.new(
@@ -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
@@ -28,16 +28,17 @@ module ShopifyCli
28
28
  end
29
29
 
30
30
  def construct_redirect_urls(urls, new_url, callback_url)
31
- urls.map do |url|
31
+ new_urls = urls.map do |url|
32
32
  if (match = url.match(NGROK_REGEX))
33
33
  "#{new_url}#{match[2]}"
34
34
  else
35
35
  url
36
36
  end
37
37
  end
38
- if urls.grep(/#{new_url}#{callback_url}/).empty?
39
- urls.push("#{new_url}#{callback_url}")
38
+ if new_urls.grep(/#{new_url}#{callback_url}/).empty?
39
+ new_urls.push("#{new_url}#{callback_url}")
40
40
  end
41
+ new_urls.uniq
41
42
  end
42
43
  end
43
44
  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
@@ -58,14 +62,19 @@ module ShopifyCli
58
62
  #
59
63
  def start(ctx, port: PORT)
60
64
  install(ctx)
61
- process = ShopifyCli::ProcessSupervision.start(:ngrok, ngrok_command(port))
62
- log = fetch_url(ctx, process.log_path)
63
- if log.account
64
- ctx.puts(ctx.message('core.tunnel.start_with_account', log.url, log.account))
65
+ url, account, seconds_remaining = start_ngrok(ctx, port)
66
+ if account
67
+ ctx.puts(ctx.message('core.tunnel.start_with_account', url, account))
65
68
  else
66
- ctx.puts(ctx.message('core.tunnel.start', log.url))
69
+ if seconds_remaining <= 0
70
+ ctx.puts(ctx.message('core.tunnel.timed_out'))
71
+ url, _account, seconds_remaining = restart_ngrok(ctx, port)
72
+ end
73
+ ctx.puts(ctx.message('core.tunnel.start', url))
74
+ ctx.puts(ctx.message('core.tunnel.will_timeout', seconds_to_hm(seconds_remaining)))
75
+ ctx.puts(ctx.message('core.tunnel.signup_suggestion', ShopifyCli::TOOL_NAME))
67
76
  end
68
- log.url
77
+ url
69
78
  end
70
79
 
71
80
  ##
@@ -82,6 +91,34 @@ module ShopifyCli
82
91
  ctx.system(File.join(ShopifyCli::CACHE_DIR, 'ngrok'), 'authtoken', token)
83
92
  end
84
93
 
94
+ ##
95
+ # will return the statistics of the current running tunnels
96
+ #
97
+ # #### Returns
98
+ #
99
+ # * `stats` - the hash of running statistics returning from the ngrok api
100
+ #
101
+ def stats
102
+ response = Net::HTTP.get_response(NGROK_TUNNELS_URI)
103
+ JSON.parse(response.body)
104
+ rescue
105
+ {}
106
+ end
107
+
108
+ ##
109
+ # will return the urls of the current running tunnels
110
+ #
111
+ # #### Returns
112
+ #
113
+ # * `stats` - the array of urls
114
+ #
115
+ def urls
116
+ tunnels = stats.dig(TUNNELS_FIELD)
117
+ tunnels.map { |tunnel| tunnel.dig(PUBLIC_URL_FIELD) }
118
+ rescue
119
+ []
120
+ end
121
+
85
122
  private
86
123
 
87
124
  def install(ctx)
@@ -106,13 +143,30 @@ module ShopifyCli
106
143
  end
107
144
 
108
145
  def ngrok_command(port)
109
- "exec #{File.join(ShopifyCli::CACHE_DIR, 'ngrok')} http -log=stdout -log-level=debug #{port}"
146
+ "exec #{File.join(ShopifyCli::CACHE_DIR, 'ngrok')} http -inspect=false -log=stdout -log-level=debug #{port}"
147
+ end
148
+
149
+ def seconds_to_hm(seconds)
150
+ format("%d hours %d minutes", seconds / 3600, seconds / 60 % 60)
110
151
  end
111
152
 
153
+ def start_ngrok(ctx, port)
154
+ process = ShopifyCli::ProcessSupervision.start(:ngrok, ngrok_command(port))
155
+ log = fetch_url(ctx, process.log_path)
156
+ seconds_remaining = (process.time.to_i + log.timeout) - Time.now.to_i
157
+ [log.url, log.account, seconds_remaining]
158
+ end
159
+
160
+ def restart_ngrok(ctx, port)
161
+ unless ShopifyCli::ProcessSupervision.stop(:ngrok)
162
+ ctx.abort(ctx.message('core.tunnel.error.stop'))
163
+ end
164
+ start_ngrok(ctx, port)
165
+ end
112
166
  class LogParser # :nodoc:
113
167
  TIMEOUT = 10
114
168
 
115
- attr_reader :url, :account
169
+ attr_reader :url, :account, :timeout
116
170
 
117
171
  def initialize(log_path)
118
172
  @log_path = log_path
@@ -141,7 +195,9 @@ module ShopifyCli
141
195
  end
142
196
 
143
197
  def parse_account
144
- @account, _ = @log.match(/AccountName:([\w\s]+) SessionDuration/)&.captures
198
+ account, timeout, _ = @log.match(/AccountName:([\w\s]*) SessionDuration:([\d]+) PlanName/)&.captures
199
+ @account = account&.empty? ? nil : account
200
+ @timeout = timeout&.empty? ? 0 : timeout.to_i
145
201
  end
146
202
 
147
203
  def error