shopify-cli 1.1.2 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +8 -0
  3. data/.github/workflows/release.yml +62 -0
  4. data/.gitignore +0 -1
  5. data/.rubocop.yml +41 -2
  6. data/.rubocop_todo.yml +24 -0
  7. data/.travis.yml +1 -0
  8. data/CHANGELOG.md +24 -0
  9. data/Gemfile +2 -2
  10. data/Gemfile.lock +35 -36
  11. data/RELEASING.md +24 -26
  12. data/docs/core/index.md +16 -0
  13. data/lib/project_types/extension/cli.rb +6 -1
  14. data/lib/project_types/extension/commands/register.rb +1 -1
  15. data/lib/project_types/extension/features/argo/admin.rb +20 -0
  16. data/lib/project_types/extension/features/argo/base.rb +129 -0
  17. data/lib/project_types/extension/features/argo/checkout.rb +20 -0
  18. data/lib/project_types/extension/features/argo_config.rb +60 -0
  19. data/lib/project_types/extension/messages/messages.rb +11 -2
  20. data/lib/project_types/extension/models/type.rb +4 -0
  21. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +6 -3
  22. data/lib/project_types/extension/models/types/product_subscription.rb +24 -0
  23. data/lib/project_types/node/commands/create.rb +6 -1
  24. data/lib/project_types/rails/commands/create.rb +6 -1
  25. data/lib/project_types/script/config/extension_points.yml +8 -8
  26. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +2 -2
  27. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +36 -1
  28. data/lib/project_types/script/layers/infrastructure/errors.rb +7 -0
  29. data/lib/project_types/script/messages/messages.rb +4 -3
  30. data/lib/project_types/script/ui/error_handler.rb +13 -5
  31. data/lib/shopify-cli/api.rb +5 -9
  32. data/lib/shopify-cli/commands/config.rb +33 -1
  33. data/lib/shopify-cli/commands/system.rb +9 -0
  34. data/lib/shopify-cli/core/entry_point.rb +1 -1
  35. data/lib/shopify-cli/core/monorail.rb +4 -3
  36. data/lib/shopify-cli/http_request.rb +15 -0
  37. data/lib/shopify-cli/js_system.rb +22 -5
  38. data/lib/shopify-cli/messages/messages.rb +30 -10
  39. data/lib/shopify-cli/project.rb +3 -3
  40. data/lib/shopify-cli/shopifolk.rb +67 -0
  41. data/lib/shopify-cli/tasks/create_api_client.rb +4 -2
  42. data/lib/shopify-cli/tasks/select_org_and_shop.rb +1 -0
  43. data/lib/shopify-cli/tunnel.rb +1 -1
  44. data/lib/shopify-cli/version.rb +1 -1
  45. data/lib/shopify_cli.rb +1 -0
  46. metadata +11 -4
  47. data/lib/project_types/extension/features/argo.rb +0 -48
  48. data/lib/project_types/extension/models/types/subscription_management.rb +0 -20
@@ -34,6 +34,13 @@ module Script
34
34
  class ShopScriptConflictError < ScriptProjectError; end
35
35
  class ShopScriptUndefinedError < ScriptProjectError; end
36
36
  class TaskRunnerNotFoundError < ScriptProjectError; end
37
+ class PackagesOutdatedError < ScriptProjectError
38
+ attr_reader :outdated_packages
39
+ def initialize(outdated_packages)
40
+ super("EP packages are outdated and need to be updated: #{outdated_packages.join(', ')}")
41
+ @outdated_packages = outdated_packages
42
+ end
43
+ end
37
44
  end
38
45
  end
39
46
  end
@@ -45,9 +45,7 @@ module Script
45
45
 
46
46
  app_not_installed_cause: "App not installed on store.",
47
47
 
48
- app_script_not_pushed_help: "Push the script and then try this command again.",
49
-
50
- app_script_undefined_help: "Push script to app.",
48
+ app_script_not_pushed_help: "Script isn't on the app. Run {{command:shopify push}}, and then try again.",
51
49
 
52
50
  build_error_cause: "Something went wrong while building the script.",
53
51
  build_error_help: "Correct the errors and try again.",
@@ -71,6 +69,9 @@ module Script
71
69
  shop_script_conflict_help: "Disable that script or uninstall that app and try again.",
72
70
 
73
71
  shop_script_undefined_cause: "Script is already turned off in store.",
72
+
73
+ packages_outdated_cause: "The following npm packages are out of date: %s.",
74
+ packages_outdated_help: "Update them by running {{cyan:npm install --save-dev %s}}.",
74
75
  },
75
76
 
76
77
  create: {
@@ -96,14 +96,11 @@ module Script
96
96
  {
97
97
  cause_of_error: ShopifyCli::Context.message('script.error.app_not_installed_cause'),
98
98
  }
99
- when Layers::Infrastructure::Errors::AppScriptNotPushedError
99
+ when Layers::Infrastructure::Errors::AppScriptNotPushedError,
100
+ Layers::Infrastructure::Errors::AppScriptUndefinedError
100
101
  {
101
102
  cause_of_error: ShopifyCli::Context.message('script.error.app_script_not_pushed_help'),
102
103
  }
103
- when Layers::Infrastructure::Errors::AppScriptUndefinedError
104
- {
105
- help_suggestion: ShopifyCli::Context.message('script.error.app_script_undefined_help'),
106
- }
107
104
  when Layers::Infrastructure::Errors::BuildError
108
105
  {
109
106
  cause_of_error: ShopifyCli::Context.message('script.error.build_error_cause'),
@@ -142,6 +139,17 @@ module Script
142
139
  {
143
140
  cause_of_error: ShopifyCli::Context.message('script.error.shop_script_undefined_cause'),
144
141
  }
142
+ when Layers::Infrastructure::Errors::PackagesOutdatedError
143
+ {
144
+ cause_of_error: ShopifyCli::Context.message(
145
+ 'script.error.packages_outdated_cause',
146
+ e.outdated_packages.join(', ')
147
+ ),
148
+ help_suggestion: ShopifyCli::Context.message(
149
+ 'script.error.packages_outdated_help',
150
+ e.outdated_packages.collect { |package| "#{package}@latest" }.join(' ')
151
+ ),
152
+ }
145
153
  end
146
154
  end
147
155
  end
@@ -1,5 +1,4 @@
1
1
  require 'shopify_cli'
2
- require 'net/http'
3
2
 
4
3
  module ShopifyCli
5
4
  class API
@@ -32,8 +31,9 @@ module ShopifyCli
32
31
  )
33
32
  ctx.debug(resp)
34
33
  resp
35
- rescue API::APIRequestServerError, API::APIRequestUnexpectedError
34
+ rescue API::APIRequestServerError, API::APIRequestUnexpectedError => e
36
35
  ctx.puts(ctx.message('core.api.error.internal_server_error'))
36
+ ctx.debug(ctx.message('core.api.error.internal_server_error_debug', e.message))
37
37
  end
38
38
 
39
39
  protected
@@ -58,14 +58,10 @@ module ShopifyCli
58
58
  unless uri.is_a?(URI::HTTP)
59
59
  ctx.abort("Invalid URL: #{graphql_url}")
60
60
  end
61
- http = ::Net::HTTP.new(uri.host, uri.port)
62
- http.use_ssl = true
63
61
 
64
- req = ::Net::HTTP::Post.new(uri.request_uri)
65
- req.body = JSON.dump(query: body.tr("\n", ""), variables: variables)
66
- req['Content-Type'] = 'application/json'
67
- headers.each { |header, value| req[header] = value }
68
- response = http.request(req)
62
+ # we delay this require so as to avoid a performance hit on starting the CLI
63
+ require 'shopify-cli/http_request'
64
+ response = HttpRequest.call(uri, body, variables, headers)
69
65
 
70
66
  case response.code.to_i
71
67
  when 200..399
@@ -6,6 +6,7 @@ module ShopifyCli
6
6
  hidden_feature(feature_set: :debug)
7
7
 
8
8
  subcommand :Feature, 'feature'
9
+ subcommand :Analytics, 'analytics'
9
10
 
10
11
  def call(*)
11
12
  @ctx.puts(self.class.help)
@@ -16,6 +17,10 @@ module ShopifyCli
16
17
  end
17
18
 
18
19
  class Feature < ShopifyCli::SubCommand
20
+ def self.help
21
+ ShopifyCli::Context.message('core.config.feature.help', ShopifyCli::TOOL_NAME)
22
+ end
23
+
19
24
  options do |parser, flags|
20
25
  parser.on('--enable') { flags[:action] = 'enable' }
21
26
  parser.on('--disable') { flags[:action] = 'disable' }
@@ -28,7 +33,7 @@ module ShopifyCli
28
33
  is_enabled = ShopifyCli::Feature.enabled?(feature_name)
29
34
  if options.flags[:action] == 'disable' && is_enabled
30
35
  ShopifyCli::Feature.disable(feature_name)
31
- @ctx.puts(@ctx.message('core.config.feature.disabled', is_enabled))
36
+ @ctx.puts(@ctx.message('core.config.feature.disabled', feature_name))
32
37
  elsif options.flags[:action] == 'enable' && !is_enabled
33
38
  ShopifyCli::Feature.enable(feature_name)
34
39
  @ctx.puts(@ctx.message('core.config.feature.enabled', feature_name))
@@ -39,6 +44,33 @@ module ShopifyCli
39
44
  end
40
45
  end
41
46
  end
47
+
48
+ class Analytics < ShopifyCli::SubCommand
49
+ def self.help
50
+ ShopifyCli::Context.message('core.config.analytics.help', ShopifyCli::TOOL_NAME)
51
+ end
52
+
53
+ options do |parser, flags|
54
+ parser.on('--enable') { flags[:action] = 'enable' }
55
+ parser.on('--disable') { flags[:action] = 'disable' }
56
+ parser.on('--status') { flags[:action] = 'status' }
57
+ end
58
+
59
+ def call(_args, _name)
60
+ is_enabled = ShopifyCli::Config.get_bool('analytics', 'enabled')
61
+ if options.flags[:action] == 'disable' && is_enabled
62
+ ShopifyCli::Config.set('analytics', 'enabled', false)
63
+ @ctx.puts(@ctx.message('core.config.analytics.disabled'))
64
+ elsif options.flags[:action] == 'enable' && !is_enabled
65
+ ShopifyCli::Config.set('analytics', 'enabled', true)
66
+ @ctx.puts(@ctx.message('core.config.analytics.enabled'))
67
+ elsif is_enabled
68
+ @ctx.puts(@ctx.message('core.config.analytics.is_enabled'))
69
+ else
70
+ @ctx.puts(@ctx.message('core.config.analytics.is_disabled'))
71
+ end
72
+ end
73
+ end
42
74
  end
43
75
  end
44
76
  end
@@ -23,6 +23,7 @@ module ShopifyCli
23
23
  display_cli_ruby(show_all_details)
24
24
  display_utility_commands(show_all_details)
25
25
  display_project_commands(show_all_details)
26
+ display_shopify_staff_identity if show_all_details
26
27
  end
27
28
 
28
29
  def self.help
@@ -139,6 +140,14 @@ module ShopifyCli
139
140
  end
140
141
  @ctx.puts("")
141
142
  end
143
+
144
+ def display_shopify_staff_identity
145
+ is_shopifolk = ShopifyCli::Shopifolk.check
146
+ if is_shopifolk
147
+ @ctx.puts("\n" + @ctx.message('core.system.identity_header'))
148
+ @ctx.puts(" " + @ctx.message('core.system.identity_is_shopifolk'))
149
+ end
150
+ end
142
151
  end
143
152
  end
144
153
  end
@@ -28,7 +28,7 @@ module ShopifyCli
28
28
  ctx.puts(
29
29
  ctx.message('core.warning.development_version', File.join(ShopifyCli::ROOT, 'bin', ShopifyCli::TOOL_NAME))
30
30
  )
31
- else
31
+ elsif !ctx.testing?
32
32
  new_version = ctx.new_version
33
33
  ctx.puts(ctx.message('core.warning.new_version', ShopifyCli::VERSION, new_version)) unless new_version.nil?
34
34
  end
@@ -102,13 +102,14 @@ module ShopifyCli
102
102
  cli_version: ShopifyCli::VERSION,
103
103
  ruby_version: RUBY_VERSION,
104
104
  }.tap do |payload|
105
- payload[:metadata] = JSON.dump(metadata) unless metadata.empty?
106
-
105
+ payload[:api_key] = metadata.delete(:api_key)
106
+ payload[:partner_id] = metadata.delete(:organization_id)
107
107
  if Project.has_current?
108
- project = Project.current
108
+ project = Project.current(force_reload: true)
109
109
  payload[:api_key] = project.env&.api_key
110
110
  payload[:partner_id] = project.config['organization_id']
111
111
  end
112
+ payload[:metadata] = JSON.dump(metadata) unless metadata.empty?
112
113
  end,
113
114
  }
114
115
  end
@@ -0,0 +1,15 @@
1
+ require 'net/http'
2
+
3
+ module ShopifyCli
4
+ class HttpRequest
5
+ def self.call(uri, body, variables, headers)
6
+ http = ::Net::HTTP.new(uri.host, uri.port)
7
+ http.use_ssl = true
8
+ req = ::Net::HTTP::Post.new(uri.request_uri)
9
+ req.body = JSON.dump(query: body.tr("\n", ""), variables: variables)
10
+ req['Content-Type'] = 'application/json'
11
+ headers.each { |header, value| req[header] = value }
12
+ http.request(req)
13
+ end
14
+ end
15
+ end
@@ -76,23 +76,40 @@ module ShopifyCli
76
76
  # - `ctx`: running context from your command
77
77
  # - `yarn`: The proc, array, or string command to run if yarn is available
78
78
  # - `npm`: The proc, array, or string command to run if npm is available
79
+ # - `capture_response`: The boolean flag to capture the output of the running command if it is set to true
79
80
  #
80
81
  # #### Example
81
82
  #
82
- # ShopifyCli::JsSystem.new(ctx: ctx).call(yarn: ['install', '--silent'], npm: ['install', '--no-audit'])
83
+ # ShopifyCli::JsSystem.new(ctx: ctx).call(
84
+ # yarn: ['install', '--silent'],
85
+ # npm: ['install', '--no-audit'],
86
+ # capture_response: false
87
+ # )
83
88
  #
84
- def call(yarn:, npm:)
85
- yarn? ? call_command(yarn, YARN_CORE_COMMAND) : call_command(npm, NPM_CORE_COMMAND)
89
+ def call(yarn:, npm:, capture_response: false)
90
+ if yarn?
91
+ call_command(yarn, YARN_CORE_COMMAND, capture_response)
92
+ else
93
+ call_command(npm, NPM_CORE_COMMAND, capture_response)
94
+ end
86
95
  end
87
96
 
88
97
  private
89
98
 
90
- def call_command(command, core_command)
99
+ def call_command(command, core_command, capture_response)
91
100
  if command.is_a?(String) || command.is_a?(Array)
92
- CLI::Kit::System.system(core_command, *command, chdir: ctx.root).success?
101
+ capture_response ? call_with_capture(command, core_command) : call_without_capture(command, core_command)
93
102
  else
94
103
  command.call
95
104
  end
96
105
  end
106
+
107
+ def call_with_capture(command, core_command)
108
+ CLI::Kit::System.capture3(core_command, *command, chdir: ctx.root)
109
+ end
110
+
111
+ def call_without_capture(command, core_command)
112
+ CLI::Kit::System.system(core_command, *command, chdir: ctx.root).success?
113
+ end
97
114
  end
98
115
  end
@@ -48,13 +48,27 @@ module ShopifyCli
48
48
  config: {
49
49
  help: <<~HELP,
50
50
  Change configuration of how the CLI operates
51
- Usage: {{command:%s config [ feature ] [ feature_name ] }}
51
+ Usage: {{command:%s config [ feature | analytics ] }}
52
52
  HELP
53
53
  feature: {
54
- enabled: "{{v}} feature {{green:%s}} was enabled",
55
- disabled: "{{v}} feature {{green:%s}} was disabled",
56
- is_enabled: "{{v}} feature {{green:%s}} is enabled",
57
- is_disabled: "{{v}} feature {{green:%s}} is disabled",
54
+ help: <<~HELP,
55
+ Change configuration of various features
56
+ Usage: {{command:%s config [ feature ] [ feature_name ] }}
57
+ HELP
58
+ enabled: "{{v}} feature {{green:%s}} has been enabled",
59
+ disabled: "{{v}} feature {{green:%s}} has been disabled",
60
+ is_enabled: "{{v}} feature {{green:%s}} is currently enabled",
61
+ is_disabled: "{{v}} feature {{green:%s}} is currently disabled",
62
+ },
63
+ analytics: {
64
+ help: <<~HELP,
65
+ Opt in/out of anonymous usage reporting
66
+ Usage: {{command:%s config [ analytics ] }}
67
+ HELP
68
+ enabled: "{{v}} analytics have been enabled",
69
+ disabled: "{{v}} analytics have been disabled",
70
+ is_enabled: "{{v}} analytics are currently enabled",
71
+ is_disabled: "{{v}} analytics are currently disabled",
58
72
  },
59
73
  },
60
74
 
@@ -163,6 +177,7 @@ module ShopifyCli
163
177
  api: {
164
178
  error: {
165
179
  internal_server_error: '{{red:{{x}} An unexpected error occurred on Shopify.}}',
180
+ internal_server_error_debug: "\n{{red:Response details:}}\n%s\n\n",
166
181
  },
167
182
  },
168
183
 
@@ -184,11 +199,14 @@ module ShopifyCli
184
199
  {{x}} You are not in a Shopify app project
185
200
  {{yellow:{{*}}}}{{reset: Run}}{{cyan: shopify create}}{{reset: to create your app}}
186
201
  MESSAGE
187
- cli_yaml: {
188
- not_hash: "{{x}} .shopify-cli.yml was not a proper YAML file. Expecting a hash.",
189
- invalid: "{{x}} %s contains invalid YAML: %s",
190
- not_found: "{{x}} %s not found",
191
- },
202
+ },
203
+ },
204
+
205
+ yaml: {
206
+ error: {
207
+ not_hash: "{{x}} %s was not a proper YAML file. Expecting a hash.",
208
+ invalid: "{{x}} %s contains invalid YAML: %s",
209
+ not_found: "{{x}} %s not found",
192
210
  },
193
211
  },
194
212
 
@@ -234,6 +252,8 @@ module ShopifyCli
234
252
  },
235
253
  environment_header: "{{bold:Environment}}",
236
254
  env: "%-17s = %s",
255
+ identity_header: "{{bold:Identity}}",
256
+ identity_is_shopifolk: "{{v}} Checked user settings: you’re Shopify staff!",
237
257
  },
238
258
 
239
259
  tasks: {
@@ -166,7 +166,7 @@ module ShopifyCli
166
166
  @config ||= begin
167
167
  config = load_yaml_file('.shopify-cli.yml')
168
168
  unless config.is_a?(Hash)
169
- raise ShopifyCli::Abort, Context.message('core.project.error.cli_yaml.not_hash')
169
+ raise ShopifyCli::Abort, Context.message('core.yaml.error.not_hash', '.shopify-cli.yml')
170
170
  end
171
171
 
172
172
  # The app_type key was deprecated in favour of project_type, so replace it
@@ -187,12 +187,12 @@ module ShopifyCli
187
187
  begin
188
188
  YAML.load_file(f)
189
189
  rescue Psych::SyntaxError => e
190
- raise(ShopifyCli::Abort, Context.message('core.project.error.cli_yaml.invalid', relative_path, e.message))
190
+ raise(ShopifyCli::Abort, Context.message('core.yaml.error.invalid', relative_path, e.message))
191
191
  # rescue Errno::EACCES => e
192
192
  # TODO
193
193
  # Dev::Helpers::EaccesHandler.diagnose_and_raise(f, e, mode: :read)
194
194
  rescue Errno::ENOENT
195
- raise ShopifyCli::Abort, Context.message('core.project.error.cli_yaml.not_found', f)
195
+ raise ShopifyCli::Abort, Context.message('core.yaml.error.not_found', f)
196
196
  end
197
197
  end
198
198
  end
@@ -0,0 +1,67 @@
1
+ module ShopifyCli
2
+ ##
3
+ # ShopifyCli::Shopifolk contains the logic to determine if the user appears to be a Shopify staff
4
+ #
5
+ # The Shopifolk Feature flag will persist between runs so if the flag is enabled or disabled,
6
+ # it will still be in that same state until the next class invocation.
7
+ class Shopifolk
8
+ GCLOUD_CONFIG_FILE = File.expand_path('~/.config/gcloud/configurations/config_default')
9
+ DEV_PATH = '/opt/dev'
10
+ SECTION = 'core'
11
+ FEATURE_NAME = 'shopifolk'
12
+
13
+ ##
14
+ # will return if the user appears to be a Shopify employee, based on several heuristics
15
+ #
16
+ # #### Returns
17
+ #
18
+ # * `is_shopifolk` - returns true if the user is a Shopify Employee
19
+ #
20
+ # #### Example
21
+ #
22
+ # ShopifyCli::Shopifolk.check
23
+ #
24
+ def self.check
25
+ ShopifyCli::Shopifolk.new.shopifolk?
26
+ end
27
+
28
+ ##
29
+ # will return if the user is a Shopify employee
30
+ #
31
+ # #### Returns
32
+ #
33
+ # * `is_shopifolk` - returns true if the user has `dev` installed and
34
+ # a valid google cloud config file with email ending in "@shopify.com"
35
+ #
36
+ def shopifolk?
37
+ return true if Feature.enabled?(FEATURE_NAME)
38
+ if shopifolk_by_gcloud? && shopifolk_by_dev?
39
+ ShopifyCli::Feature.enable(FEATURE_NAME)
40
+ true
41
+ else
42
+ ShopifyCli::Feature.disable(FEATURE_NAME)
43
+ false
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def shopifolk_by_gcloud?
50
+ ini&.dig("[#{SECTION}]", 'account')&.match?(/@shopify.com\z/)
51
+ end
52
+
53
+ def shopifolk_by_dev?
54
+ File.exist?("#{DEV_PATH}/bin/dev") && File.exist?("#{DEV_PATH}/.shopify-build")
55
+ end
56
+
57
+ def ini
58
+ @ini ||= begin
59
+ if File.exist?(GCLOUD_CONFIG_FILE)
60
+ CLI::Kit::Ini
61
+ .new(GCLOUD_CONFIG_FILE, default_section: "[#{SECTION}]", convert_types: false)
62
+ .tap(&:parse).ini
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -17,12 +17,14 @@ module ShopifyCli
17
17
  redir: [OAuth::REDIRECT_HOST]
18
18
  )
19
19
 
20
- user_errors = resp["data"]["appCreate"]["userErrors"]
20
+ user_errors = resp.dig("data", "appCreate", "userErrors")
21
21
  if !user_errors.nil? && user_errors.any?
22
22
  ctx.abort(user_errors.map { |err| "#{err['field']} #{err['message']}" }.join(", "))
23
23
  end
24
24
 
25
- resp["data"]["appCreate"]["app"]
25
+ ShopifyCli::Core::Monorail.metadata[:api_key] = resp.dig("data", "appCreate", "app", "apiKey")
26
+
27
+ resp.dig("data", "appCreate", "app")
26
28
  end
27
29
  end
28
30
  end