shopify-cli 2.7.3 → 2.10.0

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +44 -0
  4. data/Gemfile.lock +1 -1
  5. data/RELEASING.md +4 -3
  6. data/dev.yml +2 -2
  7. data/ext/javy/javy.rb +8 -9
  8. data/lib/graphql/get_extension_registrations.graphql +27 -0
  9. data/lib/project_types/extension/cli.rb +27 -2
  10. data/lib/project_types/extension/commands/build.rb +10 -10
  11. data/lib/project_types/extension/commands/create.rb +2 -3
  12. data/lib/project_types/extension/commands/push.rb +36 -8
  13. data/lib/project_types/extension/extension_project.rb +1 -1
  14. data/lib/project_types/extension/features/argo_serve.rb +6 -5
  15. data/lib/project_types/extension/forms/questions/ask_registration.rb +6 -2
  16. data/lib/project_types/extension/loaders/project.rb +29 -0
  17. data/lib/project_types/extension/loaders/specification_handler.rb +22 -0
  18. data/lib/project_types/extension/messages/messages.rb +4 -0
  19. data/lib/project_types/extension/models/app.rb +1 -1
  20. data/lib/project_types/extension/models/development_server.rb +2 -4
  21. data/lib/project_types/extension/models/specification_handlers/default.rb +4 -0
  22. data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
  23. data/lib/project_types/extension/tasks/execute_commands/base.rb +13 -0
  24. data/lib/project_types/extension/tasks/execute_commands/build.rb +29 -0
  25. data/lib/project_types/extension/tasks/execute_commands/create.rb +33 -0
  26. data/lib/project_types/extension/tasks/execute_commands/serve.rb +35 -0
  27. data/lib/project_types/extension/tasks/merge_server_config.rb +33 -22
  28. data/lib/project_types/rails/gem.rb +1 -2
  29. data/lib/project_types/script/cli.rb +7 -0
  30. data/lib/project_types/script/commands/connect.rb +19 -0
  31. data/lib/project_types/script/commands/create.rb +8 -2
  32. data/lib/project_types/script/commands/push.rb +35 -12
  33. data/lib/project_types/script/layers/application/connect_app.rb +15 -3
  34. data/lib/project_types/script/layers/application/create_script.rb +16 -16
  35. data/lib/project_types/script/layers/application/extension_points.rb +50 -26
  36. data/lib/project_types/script/layers/application/push_script.rb +5 -2
  37. data/lib/project_types/script/layers/domain/errors.rb +3 -2
  38. data/lib/project_types/script/layers/domain/extension_point.rb +14 -0
  39. data/lib/project_types/script/layers/domain/script_config.rb +6 -4
  40. data/lib/project_types/script/layers/infrastructure/errors.rb +38 -23
  41. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +49 -28
  42. data/lib/project_types/script/layers/infrastructure/script_service.rb +22 -5
  43. data/lib/project_types/script/loaders/project.rb +44 -0
  44. data/lib/project_types/script/loaders/specification_handler.rb +22 -0
  45. data/lib/project_types/script/messages/messages.rb +39 -16
  46. data/lib/project_types/script/ui/error_handler.rb +46 -29
  47. data/lib/project_types/theme/commands/pull.rb +45 -17
  48. data/lib/project_types/theme/commands/push.rb +65 -28
  49. data/lib/project_types/theme/commands/serve.rb +5 -0
  50. data/lib/project_types/theme/messages/messages.rb +34 -18
  51. data/lib/shopify_cli/command.rb +6 -0
  52. data/lib/shopify_cli/commands/login.rb +1 -1
  53. data/lib/shopify_cli/commands/switch.rb +1 -1
  54. data/lib/shopify_cli/constants.rb +11 -2
  55. data/lib/shopify_cli/context.rb +66 -12
  56. data/lib/shopify_cli/core/executor.rb +4 -4
  57. data/lib/shopify_cli/environment.rb +50 -20
  58. data/lib/shopify_cli/form.rb +2 -0
  59. data/lib/shopify_cli/identity_auth.rb +4 -3
  60. data/lib/shopify_cli/messages/messages.rb +9 -1
  61. data/lib/shopify_cli/method_object.rb +21 -9
  62. data/lib/shopify_cli/partners_api/app_extensions/job.rb +36 -0
  63. data/lib/shopify_cli/partners_api/app_extensions.rb +46 -0
  64. data/lib/shopify_cli/partners_api/organizations.rb +2 -5
  65. data/lib/shopify_cli/partners_api.rb +1 -0
  66. data/lib/shopify_cli/project.rb +8 -7
  67. data/lib/shopify_cli/resources/env_file.rb +18 -6
  68. data/lib/shopify_cli/result.rb +61 -59
  69. data/lib/shopify_cli/task.rb +5 -3
  70. data/lib/shopify_cli/theme/dev_server/cdn/cdn_helper.rb +49 -0
  71. data/lib/shopify_cli/theme/dev_server/cdn_assets.rb +69 -0
  72. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +8 -28
  73. data/lib/shopify_cli/theme/dev_server/hot-reload.js +34 -3
  74. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +18 -2
  75. data/lib/shopify_cli/theme/dev_server/local_assets.rb +4 -0
  76. data/lib/shopify_cli/theme/dev_server/proxy/template_param_builder.rb +84 -0
  77. data/lib/shopify_cli/theme/dev_server/proxy.rb +10 -15
  78. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +34 -0
  79. data/lib/shopify_cli/theme/dev_server.rb +8 -21
  80. data/lib/shopify_cli/theme/file.rb +2 -2
  81. data/lib/shopify_cli/theme/filter/path_matcher.rb +38 -0
  82. data/lib/shopify_cli/theme/ignore_filter.rb +14 -18
  83. data/lib/shopify_cli/theme/include_filter.rb +43 -0
  84. data/lib/shopify_cli/theme/syncer.rb +17 -2
  85. data/lib/shopify_cli/theme/theme.rb +26 -4
  86. data/lib/shopify_cli/thread_pool/job.rb +27 -0
  87. data/lib/shopify_cli/thread_pool.rb +37 -0
  88. data/lib/shopify_cli/version.rb +1 -1
  89. data/lib/shopify_cli.rb +6 -1
  90. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +3 -1
  91. data/vendor/deps/ruby2_keywords/LICENSE +22 -0
  92. data/vendor/deps/ruby2_keywords/README.md +67 -0
  93. data/vendor/deps/ruby2_keywords/Rakefile +54 -0
  94. data/vendor/deps/ruby2_keywords/lib/ruby2_keywords.rb +57 -0
  95. data/vendor/deps/ruby2_keywords/ruby2_keywords.gemspec +18 -0
  96. data/vendor/deps/ruby2_keywords/test/test_keyword.rb +41 -0
  97. metadata +28 -4
  98. data/lib/graphql/all_orgs_with_extensions.graphql +0 -37
  99. data/lib/project_types/extension/tasks/run_extension_command.rb +0 -82
@@ -2,6 +2,7 @@
2
2
  require "shopify_cli/theme/theme"
3
3
  require "shopify_cli/theme/development_theme"
4
4
  require "shopify_cli/theme/ignore_filter"
5
+ require "shopify_cli/theme/include_filter"
5
6
  require "shopify_cli/theme/syncer"
6
7
 
7
8
  module Theme
@@ -10,12 +11,14 @@ module Theme
10
11
  options do |parser, flags|
11
12
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
12
13
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
14
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
13
15
  parser.on("-l", "--live") { flags[:live] = true }
14
16
  parser.on("-d", "--development") { flags[:development] = true }
15
17
  parser.on("-u", "--unpublished") { flags[:unpublished] = true }
16
18
  parser.on("-j", "--json") { flags[:json] = true }
17
19
  parser.on("-a", "--allow-live") { flags[:allow_live] = true }
18
20
  parser.on("-p", "--publish") { flags[:publish] = true }
21
+ parser.on("-o", "--only=PATTERN") { |pattern| flags[:includes] = pattern }
19
22
  parser.on("-x", "--ignore=PATTERN") do |pattern|
20
23
  flags[:ignores] ||= []
21
24
  flags[:ignores] << pattern
@@ -25,39 +28,22 @@ module Theme
25
28
  def call(args, _name)
26
29
  root = args.first || "."
27
30
  delete = !options.flags[:nodelete]
28
-
29
- theme = if (theme_id = options.flags[:theme_id])
30
- ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
31
- elsif options.flags[:live]
32
- ShopifyCLI::Theme::Theme.live(@ctx, root: root)
33
- elsif options.flags[:development]
34
- theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
35
- theme.ensure_exists!
36
- theme
37
- elsif options.flags[:unpublished]
38
- name = CLI::UI::Prompt.ask(@ctx.message("theme.push.name"), allow_empty: false)
39
- theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
40
- theme.create
41
- theme
42
- else
43
- form = Forms::Select.ask(
44
- @ctx,
45
- [],
46
- title: @ctx.message("theme.push.select"),
47
- root: root,
48
- )
49
- return unless form
50
- form.theme
51
- end
31
+ theme = find_theme(root, **options.flags)
32
+ return if theme.nil?
52
33
 
53
34
  if theme.live? && !options.flags[:allow_live]
54
- return unless CLI::UI::Prompt.confirm(@ctx.message("theme.push.live"))
35
+ question = @ctx.message("theme.push.live")
36
+ question += @ctx.message("theme.push.theme", theme.name, theme.id) if options.flags[:live]
37
+ return unless CLI::UI::Prompt.confirm(question)
55
38
  end
56
39
 
40
+ include_filter = ShopifyCLI::Theme::IncludeFilter.new(options.flags[:includes])
57
41
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
58
42
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
59
43
 
60
- syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme, ignore_filter: ignore_filter)
44
+ syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme,
45
+ include_filter: include_filter,
46
+ ignore_filter: ignore_filter)
61
47
  begin
62
48
  syncer.start_threads
63
49
  if options.flags[:json]
@@ -76,16 +62,67 @@ module Theme
76
62
  end
77
63
  end
78
64
  raise ShopifyCLI::AbortSilent if syncer.has_any_error?
79
- rescue ShopifyCLI::API::APIRequestNotFoundError
80
- @ctx.abort(@ctx.message("theme.push.theme_not_found", theme.id))
81
65
  ensure
82
66
  syncer.shutdown
83
67
  end
68
+ rescue ShopifyCLI::API::APIRequestNotFoundError
69
+ @ctx.abort(@ctx.message("theme.push.theme_not_found", "##{theme.id}"))
84
70
  end
85
71
 
86
72
  def self.help
87
73
  ShopifyCLI::Context.message("theme.push.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
88
74
  end
75
+
76
+ private
77
+
78
+ def find_theme(root, theme_id: nil, theme: nil, live: nil, development: nil, unpublished: nil, **_args)
79
+ if theme_id
80
+ @ctx.warn(@ctx.message("theme.push.deprecated_themeid"))
81
+ return ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
82
+ end
83
+
84
+ if live
85
+ return ShopifyCLI::Theme::Theme.live(@ctx, root: root)
86
+ end
87
+
88
+ if development
89
+ new_theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
90
+ new_theme.ensure_exists!
91
+ return new_theme
92
+ end
93
+
94
+ if unpublished
95
+ name = theme || ask_theme_name
96
+ new_theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
97
+ new_theme.create
98
+ return new_theme
99
+ end
100
+
101
+ if theme
102
+ selected_theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: theme)
103
+ return selected_theme || @ctx.abort(@ctx.message("theme.push.theme_not_found", theme))
104
+ end
105
+
106
+ select_theme(root)
107
+ end
108
+
109
+ def ask_theme_name
110
+ CLI::UI::Prompt.ask(@ctx.message("theme.push.name"), allow_empty: false)
111
+ end
112
+
113
+ def select_theme(root)
114
+ form = Forms::Select.ask(
115
+ @ctx,
116
+ [],
117
+ title: @ctx.message("theme.push.select"),
118
+ root: root,
119
+ )
120
+ form&.theme
121
+ end
122
+
123
+ def themes(root)
124
+ ShopifyCLI::Theme::Theme.all(@ctx, root: root)
125
+ end
89
126
  end
90
127
  end
91
128
  end
@@ -10,6 +10,7 @@ module Theme
10
10
  parser.on("--host=HOST") { |host| flags[:host] = host.to_s }
11
11
  parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
12
12
  parser.on("--poll") { flags[:poll] = true }
13
+ parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
13
14
  end
14
15
 
15
16
  def call(*)
@@ -23,6 +24,10 @@ module Theme
23
24
  ShopifyCLI::Context.message("theme.serve.error.address_binding_error", ShopifyCLI::TOOL_NAME)
24
25
  end
25
26
 
27
+ def self.as_reload_mode(mode)
28
+ ShopifyCLI::Theme::DevServer::ReloadMode.get!(mode)
29
+ end
30
+
26
31
  def self.help
27
32
  ShopifyCLI::Context.message("theme.serve.help", ShopifyCLI::TOOL_NAME)
28
33
  end
@@ -57,14 +57,16 @@ module Theme
57
57
  Usage: {{command:%s theme push [ ROOT ]}}
58
58
 
59
59
  Options:
60
- {{command:-i, --themeid=THEMEID}} Theme ID. Must be an existing theme on your store.
61
- {{command:-l, --live}} Push to your remote live theme, and update your live store.
62
- {{command:-d, --development}} Push to your remote development theme, and create it if needed.
63
- {{command:-u, --unpublished}} Create a new unpublished theme and push to it.
64
- {{command:-n, --nodelete}} Runs the push command without deleting remote files from Shopify.
65
- {{command:-j, --json}} Output JSON instead of a UI.
66
- {{command:-a, --allow-live}} Allow push to a live theme.
67
- {{command:-p, --publish}} Publish as the live theme after uploading.
60
+ {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
61
+ {{command:-l, --live}} Push to your remote live theme, and update your live store.
62
+ {{command:-d, --development}} Push to your remote development theme, and create it if needed.
63
+ {{command:-u, --unpublished}} Create a new unpublished theme and push to it.
64
+ {{command:-n, --nodelete}} Runs the push command without deleting remote files from Shopify.
65
+ {{command:-j, --json}} Output JSON instead of a UI.
66
+ {{command:-a, --allow-live}} Allow push to a live theme.
67
+ {{command:-p, --publish}} Publish as the live theme after uploading.
68
+ {{command:-o, --only}} Upload only the specified files.
69
+ {{command:-x, --ignore}} Skip uploading the specified files.
68
70
 
69
71
  Run without options to select theme from a list.
70
72
  HELP
@@ -74,7 +76,11 @@ module Theme
74
76
  push: "Pushing theme files to Shopify",
75
77
  select: "Select theme to push to",
76
78
  live: "Are you sure you want to push to your live theme?",
77
- theme_not_found: "Theme #%s doesn't exist",
79
+ theme: "\n Theme: {{blue:%s #%s}} {{green:[live]}}",
80
+ deprecated_themeid: <<~WARN,
81
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
82
+ WARN
83
+ theme_not_found: "Theme \"%s\" doesn't exist",
78
84
  done: <<~DONE,
79
85
  {{green:Your theme was pushed successfully}}
80
86
 
@@ -93,10 +99,16 @@ module Theme
93
99
  Usage: {{command:%s theme serve}}
94
100
 
95
101
  Options:
96
- {{command:--port=PORT}} Local port to serve theme preview from
97
- {{command:--poll}} Force polling to detect file changes
98
- {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
102
+ {{command:--port=PORT}} Local port to serve theme preview from.
103
+ {{command:--poll}} Force polling to detect file changes.
104
+ {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
105
+ {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
106
+ - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
107
+ - {{command:full-page}} Always refreshes the entire page
108
+ - {{command:off}} Deactivate live reload
99
109
  HELP
110
+ reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
111
+ try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
100
112
  viewing_theme: "Viewing theme…",
101
113
  syncing_theme: "Syncing theme #%s on %s",
102
114
  open_fail: "Couldn't open the theme",
@@ -130,9 +142,7 @@ module Theme
130
142
  You are not authorized to edit themes on %s.
131
143
  Make sure you are a user of that store, and allowed to edit themes.
132
144
  ENSURE_USER
133
- already_in_use_error: "Error",
134
145
  address_already_in_use: "The address \"%s\" is already in use.",
135
- try_this: "Try this",
136
146
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
137
147
  },
138
148
  check: {
@@ -188,16 +198,22 @@ module Theme
188
198
  Usage: {{command:%s theme pull [ ROOT ]}}
189
199
 
190
200
  Options:
191
- {{command:-i, --themeid=THEMEID}} The Theme ID. Must be an existing theme on your store.
192
- {{command:-l, --live}} Pull theme files from your remote live theme.
193
- {{command:-n, --nodelete}} Runs the pull command without deleting local files.
201
+ {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
202
+ {{command:-l, --live}} Pull theme files from your remote live theme.
203
+ {{command:-d, --development}} Pull theme files from your remote development theme.
204
+ {{command:-n, --nodelete}} Runs the pull command without deleting local files.
205
+ {{command:-o, --only}} Download only the specified files.
206
+ {{command:-x, --ignore}} Skip downloading the specified files.
194
207
 
195
208
  Run without options to select theme from a list.
196
209
  HELP
197
210
  select: "Select a theme to pull from",
198
211
  pulling: "Pulling theme files from %s (#%s) on %s",
199
212
  done: "Theme pulled successfully",
200
- not_found: "{{x}} Theme #%s doesn't exist",
213
+ deprecated_themeid: <<~WARN,
214
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
215
+ WARN
216
+ theme_not_found: "Theme \"%s\" doesn't exist",
201
217
  },
202
218
  },
203
219
  }.freeze
@@ -29,6 +29,12 @@ module ShopifyCLI
29
29
  run_prerequisites
30
30
  cmd.call(args, command_name)
31
31
  end
32
+ rescue OptionParser::InvalidOption => error
33
+ arg = error.args.first
34
+ raise ShopifyCLI::Abort, @ctx.message("core.errors.option_parser.invalid_option", arg)
35
+ rescue OptionParser::MissingArgument => error
36
+ arg = error.args.first
37
+ raise ShopifyCLI::Abort, @ctx.message("core.errors.option_parser.missing_argument", arg)
32
38
  end
33
39
 
34
40
  def options(&block)
@@ -7,7 +7,7 @@ module ShopifyCLI
7
7
  PERMANENT_DOMAIN_SUFFIX = /\.myshopify\.(com|io)$/
8
8
 
9
9
  options do |parser, flags|
10
- parser.on("--store=STORE") { |url| flags[:shop] = url }
10
+ parser.on("-s", "--store=STORE") { |url| flags[:shop] = url }
11
11
  # backwards compatibility allow 'shop' for now
12
12
  parser.on("--shop=SHOP") { |url| flags[:shop] = url }
13
13
  parser.on("--password=PASSWORD") { |password| flags[:password] = password }
@@ -4,7 +4,7 @@ module ShopifyCLI
4
4
  module Commands
5
5
  class Switch < ShopifyCLI::Command
6
6
  options do |parser, flags|
7
- parser.on("--store=STORE") { |url| flags[:shop] = url }
7
+ parser.on("-s", "--store=STORE") { |url| flags[:shop] = url }
8
8
  # backwards compatibility allow 'shop' for now
9
9
  parser.on("--shop=SHOP") { |url| flags[:shop] = url }
10
10
  end
@@ -30,17 +30,22 @@ module ShopifyCLI
30
30
 
31
31
  module EnvironmentVariables
32
32
  STACKTRACE = "SHOPIFY_CLI_STACKTRACE"
33
+ TTY = "SHOPIFY_CLI_TTY"
33
34
 
34
35
  # When true the CLI points to a local instance of
35
36
  # the partners dashboard and identity.
36
37
  LOCAL_PARTNERS = "SHOPIFY_APP_CLI_LOCAL_PARTNERS"
37
38
 
38
- # When true the CLI points to a spin instance of spin
39
- SPIN_PARTNERS = "SHOPIFY_APP_CLI_SPIN_PARTNERS"
39
+ # When true the CLI points to spin instances of services
40
+ SPIN = "SPIN"
41
+ INFER_SPIN = "INFER_SPIN"
40
42
  SPIN_WORKSPACE = "SPIN_WORKSPACE"
41
43
  SPIN_NAMESPACE = "SPIN_NAMESPACE"
42
44
  SPIN_HOST = "SPIN_HOST"
43
45
 
46
+ # Deprecated, equivalent to using SPIN=1
47
+ SPIN_PARTNERS = "SHOPIFY_APP_CLI_SPIN_PARTNERS"
48
+
44
49
  # Environments
45
50
  TEST = "SHOPIFY_CLI_TEST"
46
51
  ACCEPTANCE_TEST = "SHOPIFY_CLI_ACCEPTANCE_TEST"
@@ -61,5 +66,9 @@ module ShopifyCLI
61
66
  module Links
62
67
  NEW_ISSUE = "https://github.com/Shopify/shopify-cli/issues/new"
63
68
  end
69
+
70
+ module Extension
71
+ DEFAULT_PORT = 39351
72
+ end
64
73
  end
65
74
  end
@@ -44,6 +44,56 @@ module ShopifyCLI
44
44
  str = Context.messages.dig(*key_parts)
45
45
  str ? str % params : key
46
46
  end
47
+
48
+ # a wrapper around Kernel.puts to allow for easy formatting
49
+ #
50
+ # #### Parameters
51
+ # * `text` - a string message to output
52
+ def puts(*args)
53
+ Kernel.puts(CLI::UI.fmt(*args))
54
+ end
55
+
56
+ # aborts the current running command and outputs an error message:
57
+ # - when the `help_message` is not provided, the error message appears in
58
+ # a red frame, prefixed by an ✗ icon
59
+ # - when the `help_message` is provided, the error message appears in a
60
+ # red frame, and the help message appears in a green frame
61
+ #
62
+ # #### Parameters
63
+ # * `error_message` - an error message to output
64
+ # * `help_message` - an optional help message
65
+ #
66
+ # #### Example
67
+ #
68
+ # ShopifyCLI::Context.abort("Execution error")
69
+ # # Output:
70
+ # # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
+ # # ┃ ✗ Execution error
72
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+ #
74
+ # ShopifyCLI::Context.abort("Execution error", "export EXECUTION=1")
75
+ # # Output:
76
+ # # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
+ # # ┃ Execution error
78
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
+ # # ┏━━ Try this ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80
+ # # ┃ export EXECUTION=1
81
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+ #
83
+ def abort(error_message, help_message = nil)
84
+ raise ShopifyCLI::Abort, "{{x}} #{error_message}" if help_message.nil?
85
+
86
+ frame(message("core.error"), color: :red) { self.puts(error_message) }
87
+ frame(message("core.try_this"), color: :green) { self.puts(help_message) }
88
+
89
+ raise ShopifyCLI::AbortSilent
90
+ end
91
+
92
+ private
93
+
94
+ def frame(title, color:, &block)
95
+ CLI::UI::Frame.open(title, color: CLI::UI.resolve_color(color), timing: false, &block)
96
+ end
47
97
  end
48
98
 
49
99
  # is the directory root that the current command is running in. If you want to
@@ -61,14 +111,19 @@ module ShopifyCLI
61
111
  # will return which operating system that the cli is running on [:mac, :linux]
62
112
  def os
63
113
  host = uname
64
- return :mac_m1 if /arm64-apple-darwin/i.match(host)
114
+ return :mac_m1 if /arm64.*darwin/i.match(host)
65
115
  return :mac if /darwin/i.match(host)
66
116
  return :windows if /mswin|mingw|cygwin/i.match(host)
67
117
  return :linux if /linux|bsd/i.match(host)
68
118
  :unknown
69
119
  end
70
120
 
71
- # will return true if the cli is running on an apple computer.
121
+ # will return true if the cli is running on an ARM Apple computer.
122
+ def mac_m1?
123
+ os == :mac_m1
124
+ end
125
+
126
+ # will return true if the cli is running on a Intel x86 Apple computer.
72
127
  def mac?
73
128
  os == :mac
74
129
  end
@@ -90,7 +145,7 @@ module ShopifyCLI
90
145
 
91
146
  # will return true if being launched from a tty
92
147
  def tty?
93
- $stdin.tty? && !testing?
148
+ $stdin.tty?
94
149
  end
95
150
 
96
151
  # will return true if the cli is being run from an installation, and not a
@@ -329,7 +384,7 @@ module ShopifyCLI
329
384
  system("xdg-open", uri.to_s)
330
385
  elsif windows?
331
386
  system("start \"\" \"#{uri}\"")
332
- elsif mac?
387
+ elsif mac? || mac_m1?
333
388
  system("open", uri.to_s)
334
389
  else
335
390
  open_url!(uri)
@@ -349,13 +404,13 @@ module ShopifyCLI
349
404
  puts "{{yellow:*}} #{text}"
350
405
  end
351
406
 
352
- # a wrapper around Kernel.puts to allow for easy formatting
407
+ # proxy call to Context.puts.
353
408
  #
354
409
  # #### Parameters
355
410
  # * `text` - a string message to output
356
411
  #
357
412
  def puts(*args)
358
- Kernel.puts(CLI::UI.fmt(*args))
413
+ Context.puts(*args)
359
414
  end
360
415
 
361
416
  # a wrapper around $stderr.puts to allow for easy formatting
@@ -385,14 +440,13 @@ module ShopifyCLI
385
440
  puts("{{v}} #{text}")
386
441
  end
387
442
 
388
- # aborts the current running command and outputs an error message, prefixed
389
- # by a red x
443
+ # proxy call to Context.abort.
390
444
  #
391
445
  # #### Parameters
392
- # * `text` - a string message to output
393
- #
394
- def abort(text)
395
- raise ShopifyCLI::Abort, "{{x}} #{text}"
446
+ # * `error_message` - an error message to output
447
+ # * `help_message` - an optional help message
448
+ def abort(error_message, help_message = nil)
449
+ Context.abort(error_message, help_message)
396
450
  end
397
451
 
398
452
  # outputs a message, prefixed by a red `DEBUG` tag. This will only output to
@@ -3,10 +3,10 @@ require "shopify_cli"
3
3
  module ShopifyCLI
4
4
  module Core
5
5
  class Executor < CLI::Kit::Executor
6
- def initialize(ctx, task_registry, *args, **kwargs)
7
- @ctx = ctx || ShopifyCLI::Context.new
8
- @task_registry = task_registry || ShopifyCLI::Tasks::TaskRegistry.new
9
- super(*args, **kwargs)
6
+ ruby2_keywords def initialize(ctx, task_registry, *args)
7
+ @ctx = ctx || ShopifyCli::Context.new
8
+ @task_registry = task_registry || ShopifyCli::Tasks::TaskRegistry.new
9
+ super(*args)
10
10
  end
11
11
 
12
12
  def call(command, command_name, args)
@@ -4,6 +4,21 @@ module ShopifyCLI
4
4
  module Environment
5
5
  TRUTHY_ENV_VARIABLE_VALUES = ["1", "true", "TRUE", "yes", "YES"]
6
6
 
7
+ def self.interactive=(interactive)
8
+ @interactive = interactive
9
+ end
10
+
11
+ def self.interactive?(env_variables: ENV)
12
+ if env_variables.key?(Constants::EnvironmentVariables::TTY)
13
+ env_variable_truthy?(
14
+ Constants::EnvironmentVariables::TTY,
15
+ env_variables: env_variables
16
+ )
17
+ else
18
+ @interactive ||= STDIN.tty?
19
+ end
20
+ end
21
+
7
22
  def self.development?(env_variables: ENV)
8
23
  env_variable_truthy?(
9
24
  Constants::EnvironmentVariables::DEVELOPMENT,
@@ -11,10 +26,6 @@ module ShopifyCLI
11
26
  )
12
27
  end
13
28
 
14
- def self.interactive?
15
- ShopifyCLI::Context.new.tty?
16
- end
17
-
18
29
  def self.use_local_partners_instance?(env_variables: ENV)
19
30
  env_variable_truthy?(
20
31
  Constants::EnvironmentVariables::LOCAL_PARTNERS,
@@ -50,17 +61,10 @@ module ShopifyCLI
50
61
  )
51
62
  end
52
63
 
53
- def self.use_spin_partners_instance?(env_variables: ENV)
54
- env_variable_truthy?(
55
- Constants::EnvironmentVariables::SPIN_PARTNERS,
56
- env_variables: env_variables
57
- )
58
- end
59
-
60
64
  def self.partners_domain(env_variables: ENV)
61
65
  if use_local_partners_instance?(env_variables: env_variables)
62
66
  "partners.myshopify.io"
63
- elsif use_spin_partners_instance?(env_variables: env_variables)
67
+ elsif use_spin?(env_variables: env_variables)
64
68
  "partners.#{spin_url(env_variables: env_variables)}"
65
69
  else
66
70
  "partners.shopify.com"
@@ -68,15 +72,31 @@ module ShopifyCLI
68
72
  end
69
73
 
70
74
  def self.use_spin?(env_variables: ENV)
71
- !env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE].nil? &&
72
- !env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE].nil?
75
+ env_variable_truthy?(
76
+ Constants::EnvironmentVariables::SPIN,
77
+ env_variables: env_variables
78
+ ) || env_variable_truthy?(
79
+ Constants::EnvironmentVariables::SPIN_PARTNERS,
80
+ env_variables: env_variables
81
+ )
82
+ end
83
+
84
+ def self.infer_spin?(env_variables: ENV)
85
+ env_variable_truthy?(
86
+ Constants::EnvironmentVariables::INFER_SPIN,
87
+ env_variables: env_variables
88
+ )
73
89
  end
74
90
 
75
91
  def self.spin_url(env_variables: ENV)
76
- spin_workspace = spin_workspace(env_variables: env_variables)
77
- spin_namespace = spin_namespace(env_variables: env_variables)
78
- spin_host = spin_host(env_variables: env_variables)
79
- "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
92
+ if infer_spin?(env_variables: env_variables)
93
+ %x(spin info fqdn 2> /dev/null).strip
94
+ else
95
+ spin_workspace = spin_workspace(env_variables: env_variables)
96
+ spin_namespace = spin_namespace(env_variables: env_variables)
97
+ spin_host = spin_host(env_variables: env_variables)
98
+ "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
99
+ end
80
100
  end
81
101
 
82
102
  def self.send_monorail_events?(env_variables: ENV)
@@ -95,11 +115,21 @@ module ShopifyCLI
95
115
  end
96
116
 
97
117
  def self.spin_workspace(env_variables: ENV)
98
- env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
118
+ env_value = env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
119
+ return env_value unless env_value.nil?
120
+
121
+ if env_value.nil?
122
+ raise "No value set for #{Constants::EnvironmentVariables::SPIN_WORKSPACE}"
123
+ end
99
124
  end
100
125
 
101
126
  def self.spin_namespace(env_variables: ENV)
102
- env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
127
+ env_value = env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
128
+ return env_value unless env_value.nil?
129
+
130
+ if env_value.nil?
131
+ raise "No value set for #{Constants::EnvironmentVariables::SPIN_NAMESPACE}"
132
+ end
103
133
  end
104
134
 
105
135
  def self.spin_host(env_variables: ENV)
@@ -15,6 +15,8 @@ module ShopifyCLI
15
15
  rescue ShopifyCLI::Abort => err
16
16
  ctx.puts(err.message)
17
17
  nil
18
+ rescue ShopifyCLI::AbortSilent
19
+ nil
18
20
  end
19
21
  end
20
22
 
@@ -229,6 +229,7 @@ module ShopifyCLI
229
229
  uri = URI.parse("#{auth_url}#{endpoint}")
230
230
  https = Net::HTTP.new(uri.host, uri.port)
231
231
  https.use_ssl = true
232
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV["SSL_VERIFY_NONE"]
232
233
  request = Net::HTTP::Post.new(uri.path)
233
234
  request["User-Agent"] = "Shopify CLI #{::ShopifyCLI::VERSION}"
234
235
  request.body = URI.encode_www_form(params)
@@ -255,7 +256,7 @@ module ShopifyCLI
255
256
  def auth_url
256
257
  if Environment.use_local_partners_instance?
257
258
  "https://identity.myshopify.io/oauth"
258
- elsif Environment.use_spin_partners_instance?
259
+ elsif Environment.use_spin?
259
260
  "https://identity.#{Environment.spin_url}/oauth"
260
261
  else
261
262
  "https://accounts.shopify.com/oauth"
@@ -263,7 +264,7 @@ module ShopifyCLI
263
264
  end
264
265
 
265
266
  def client_id_for_application(application_name)
266
- client_ids = if Environment.use_local_partners_instance? || Environment.use_spin_partners_instance?
267
+ client_ids = if Environment.use_local_partners_instance? || Environment.use_spin?
267
268
  DEV_APPLICATION_CLIENT_IDS
268
269
  else
269
270
  APPLICATION_CLIENT_IDS
@@ -279,7 +280,7 @@ module ShopifyCLI
279
280
  end
280
281
 
281
282
  def client_id
282
- if Environment.use_local_partners_instance? || Environment.use_spin_partners_instance?
283
+ if Environment.use_local_partners_instance? || Environment.use_spin?
283
284
  Constants::Identity::CLIENT_ID_DEV
284
285
  else
285
286
  # In the future we might want to use Identity's dynamic
@@ -14,6 +14,12 @@ module ShopifyCLI
14
14
  },
15
15
  },
16
16
  core: {
17
+ errors: {
18
+ option_parser: {
19
+ invalid_option: "The option {{command:%s}} is not supported.",
20
+ missing_argument: "The required argument {{command:%s}} is missing.",
21
+ },
22
+ },
17
23
  app: {
18
24
  help: <<~HELP,
19
25
  Suite of commands for developing apps. See {{command:%1$s app <command> --help}} for usage of each command.
@@ -719,7 +725,7 @@ module ShopifyCLI
719
725
  signup_suggestion: <<~MESSAGE,
720
726
  {{*}} To avoid tunnels that timeout, it is recommended to signup for a free ngrok
721
727
  account at {{underline:https://ngrok.com/signup}}. After you signup, install your
722
- personalized authorization token using {{command:%s [ node | rails ] tunnel auth <token>}}.
728
+ personalized authorization token using {{command:%s app tunnel auth <token>}}.
723
729
  MESSAGE
724
730
  start: "{{v}} ngrok tunnel running at {{underline:%s}}",
725
731
  start_with_account: "{{v}} ngrok tunnel running at {{underline:%s}}, with account %s",
@@ -784,6 +790,8 @@ module ShopifyCLI
784
790
  logged_in_partner_only: "Logged into partner organization {{green:%s}}",
785
791
  logged_in_partner_and_shop: "Logged into store {{green:%s}} in partner organization {{green:%s}}",
786
792
  },
793
+ error: "Error",
794
+ try_this: "Try this",
787
795
  },
788
796
  }.freeze
789
797
  end