shopify-cli 1.1.2 → 1.4.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.
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