shopify-cli 2.8.0 → 2.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/CHANGELOG.md +34 -0
  4. data/Gemfile.lock +1 -1
  5. data/RELEASING.md +4 -3
  6. data/ext/javy/javy.rb +1 -1
  7. data/lib/project_types/extension/commands/push.rb +2 -2
  8. data/lib/project_types/extension/messages/messages.rb +1 -1
  9. data/lib/project_types/extension/models/development_server.rb +2 -4
  10. data/lib/project_types/rails/gem.rb +1 -2
  11. data/lib/project_types/script/cli.rb +10 -0
  12. data/lib/project_types/script/commands/connect.rb +1 -1
  13. data/lib/project_types/script/commands/create.rb +8 -2
  14. data/lib/project_types/script/commands/push.rb +35 -12
  15. data/lib/project_types/script/config/extension_points.yml +12 -0
  16. data/lib/project_types/script/graphql/app_script_set.graphql +2 -0
  17. data/lib/project_types/script/graphql/module_upload_url_generate.graphql +5 -1
  18. data/lib/project_types/script/layers/application/build_script.rb +6 -3
  19. data/lib/project_types/script/layers/application/connect_app.rb +11 -5
  20. data/lib/project_types/script/layers/application/extension_points.rb +50 -26
  21. data/lib/project_types/script/layers/application/project_dependencies.rb +1 -1
  22. data/lib/project_types/script/layers/application/push_script.rb +41 -30
  23. data/lib/project_types/script/layers/domain/errors.rb +10 -3
  24. data/lib/project_types/script/layers/domain/extension_point.rb +16 -2
  25. data/lib/project_types/script/layers/domain/push_package.rb +0 -3
  26. data/lib/project_types/script/layers/domain/script_config.rb +6 -4
  27. data/lib/project_types/script/layers/domain/script_project.rb +1 -0
  28. data/lib/project_types/script/layers/infrastructure/errors.rb +47 -23
  29. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +2 -12
  30. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +1 -0
  31. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +1 -0
  32. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +2 -12
  33. data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +15 -0
  34. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +36 -0
  35. data/lib/project_types/script/layers/infrastructure/metadata_repository.rb +18 -0
  36. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +7 -8
  37. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -54
  38. data/lib/project_types/script/layers/infrastructure/script_service.rb +35 -12
  39. data/lib/project_types/script/layers/infrastructure/script_uploader.rb +22 -9
  40. data/lib/project_types/script/loaders/project.rb +44 -0
  41. data/lib/project_types/script/loaders/specification_handler.rb +22 -0
  42. data/lib/project_types/script/messages/messages.rb +38 -19
  43. data/lib/project_types/script/ui/error_handler.rb +52 -30
  44. data/lib/project_types/theme/commands/pull.rb +45 -17
  45. data/lib/project_types/theme/commands/push.rb +62 -27
  46. data/lib/project_types/theme/commands/serve.rb +5 -0
  47. data/lib/project_types/theme/messages/messages.rb +33 -18
  48. data/lib/shopify_cli/constants.rb +7 -2
  49. data/lib/shopify_cli/context.rb +66 -12
  50. data/lib/shopify_cli/core/executor.rb +4 -4
  51. data/lib/shopify_cli/environment.rb +50 -20
  52. data/lib/shopify_cli/identity_auth.rb +4 -3
  53. data/lib/shopify_cli/messages/messages.rb +2 -0
  54. data/lib/shopify_cli/method_object.rb +21 -9
  55. data/lib/shopify_cli/resources/env_file.rb +5 -1
  56. data/lib/shopify_cli/result.rb +61 -59
  57. data/lib/shopify_cli/task.rb +5 -3
  58. data/lib/shopify_cli/theme/dev_server/hot-reload.js +19 -1
  59. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +18 -2
  60. data/lib/shopify_cli/theme/dev_server/proxy.rb +1 -0
  61. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +34 -0
  62. data/lib/shopify_cli/theme/dev_server.rb +6 -21
  63. data/lib/shopify_cli/theme/file.rb +2 -2
  64. data/lib/shopify_cli/theme/filter/path_matcher.rb +38 -0
  65. data/lib/shopify_cli/theme/ignore_filter.rb +14 -18
  66. data/lib/shopify_cli/theme/include_filter.rb +43 -0
  67. data/lib/shopify_cli/theme/syncer.rb +17 -2
  68. data/lib/shopify_cli/theme/theme.rb +26 -4
  69. data/lib/shopify_cli/version.rb +1 -1
  70. data/lib/shopify_cli.rb +6 -1
  71. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +10 -5
  72. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +6 -4
  73. data/vendor/deps/ruby2_keywords/LICENSE +22 -0
  74. data/vendor/deps/ruby2_keywords/README.md +67 -0
  75. data/vendor/deps/ruby2_keywords/Rakefile +54 -0
  76. data/vendor/deps/ruby2_keywords/lib/ruby2_keywords.rb +57 -0
  77. data/vendor/deps/ruby2_keywords/ruby2_keywords.gemspec +18 -0
  78. data/vendor/deps/ruby2_keywords/test/test_keyword.rb +41 -0
  79. metadata +16 -2
@@ -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,30 +28,8 @@ 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
35
  question = @ctx.message("theme.push.live")
@@ -56,10 +37,13 @@ module Theme
56
37
  return unless CLI::UI::Prompt.confirm(question)
57
38
  end
58
39
 
40
+ include_filter = ShopifyCLI::Theme::IncludeFilter.new(options.flags[:includes])
59
41
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
60
42
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
61
43
 
62
- 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)
63
47
  begin
64
48
  syncer.start_threads
65
49
  if options.flags[:json]
@@ -78,16 +62,67 @@ module Theme
78
62
  end
79
63
  end
80
64
  raise ShopifyCLI::AbortSilent if syncer.has_any_error?
81
- rescue ShopifyCLI::API::APIRequestNotFoundError
82
- @ctx.abort(@ctx.message("theme.push.theme_not_found", theme.id))
83
65
  ensure
84
66
  syncer.shutdown
85
67
  end
68
+ rescue ShopifyCLI::API::APIRequestNotFoundError
69
+ @ctx.abort(@ctx.message("theme.push.theme_not_found", "##{theme.id}"))
86
70
  end
87
71
 
88
72
  def self.help
89
73
  ShopifyCLI::Context.message("theme.push.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
90
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
91
126
  end
92
127
  end
93
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
@@ -75,7 +77,10 @@ module Theme
75
77
  select: "Select theme to push to",
76
78
  live: "Are you sure you want to push to your live theme?",
77
79
  theme: "\n Theme: {{blue:%s #%s}} {{green:[live]}}",
78
- theme_not_found: "Theme #%s doesn't exist",
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",
79
84
  done: <<~DONE,
80
85
  {{green:Your theme was pushed successfully}}
81
86
 
@@ -94,10 +99,16 @@ module Theme
94
99
  Usage: {{command:%s theme serve}}
95
100
 
96
101
  Options:
97
- {{command:--port=PORT}} Local port to serve theme preview from
98
- {{command:--poll}} Force polling to detect file changes
99
- {{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
100
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.",
101
112
  viewing_theme: "Viewing theme…",
102
113
  syncing_theme: "Syncing theme #%s on %s",
103
114
  open_fail: "Couldn't open the theme",
@@ -131,9 +142,7 @@ module Theme
131
142
  You are not authorized to edit themes on %s.
132
143
  Make sure you are a user of that store, and allowed to edit themes.
133
144
  ENSURE_USER
134
- already_in_use_error: "Error",
135
145
  address_already_in_use: "The address \"%s\" is already in use.",
136
- try_this: "Try this",
137
146
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
138
147
  },
139
148
  check: {
@@ -189,16 +198,22 @@ module Theme
189
198
  Usage: {{command:%s theme pull [ ROOT ]}}
190
199
 
191
200
  Options:
192
- {{command:-i, --themeid=THEMEID}} The Theme ID. Must be an existing theme on your store.
193
- {{command:-l, --live}} Pull theme files from your remote live theme.
194
- {{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.
195
207
 
196
208
  Run without options to select theme from a list.
197
209
  HELP
198
210
  select: "Select a theme to pull from",
199
211
  pulling: "Pulling theme files from %s (#%s) on %s",
200
212
  done: "Theme pulled successfully",
201
- 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",
202
217
  },
203
218
  },
204
219
  }.freeze
@@ -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"
@@ -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
- !testing? && $stdin.tty?
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)
@@ -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
@@ -790,6 +790,8 @@ module ShopifyCLI
790
790
  logged_in_partner_only: "Logged into partner organization {{green:%s}}",
791
791
  logged_in_partner_and_shop: "Logged into store {{green:%s}} in partner organization {{green:%s}}",
792
792
  },
793
+ error: "Error",
794
+ try_this: "Try this",
793
795
  },
794
796
  }.freeze
795
797
  end
@@ -66,15 +66,27 @@ module ShopifyCLI
66
66
  # initializer or to `call`. If the keyword argument matches the name of
67
67
  # property, it is forwarded to the initializer, otherwise to call.
68
68
  #
69
- def call(*args, **kwargs, &block)
70
- properties.keys.yield_self do |properties|
71
- instance = new(**kwargs.slice(*properties))
72
- kwargs = kwargs.slice(*(kwargs.keys - properties))
73
- if kwargs.any?
74
- instance.call(*args, **kwargs, &block)
75
- else
76
- instance.call(*args, &block)
77
- end
69
+ ruby2_keywords def call(*args, &block)
70
+ # This is an extremely complicated case of delegation. The method wants
71
+ # to delegate arguments, but to have control over which keyword
72
+ # arguments are delegated. I'm not sure the forward and backward
73
+ # compatibility of this unusual form of delegation has really been
74
+ # explored or there's any good way to support it. So I have done
75
+ # done something hacky here and I'm looking at the last argument and
76
+ # modifying the package of arguments to be delegated in-place.
77
+ if args.last.is_a?(Hash)
78
+ kwargs = args.last
79
+
80
+ initializer_kwargs = kwargs.slice(*properties.keys)
81
+ instance = new(**initializer_kwargs)
82
+
83
+ kwargs.reject! { |key| initializer_kwargs.key?(key) }
84
+ args.pop if kwargs.empty?
85
+ instance.call(*args, &block)
86
+ else
87
+ # Since the former is so complicated - let's have a fast path that
88
+ # is much simpler.
89
+ new.call(*args, &block)
78
90
  end
79
91
  end
80
92
 
@@ -14,6 +14,10 @@ module ShopifyCLI
14
14
  }
15
15
 
16
16
  class << self
17
+ def path(directory)
18
+ File.join(directory, FILENAME)
19
+ end
20
+
17
21
  def read(_directory = Dir.pwd, overrides: {})
18
22
  input = parse_external_env(overrides: overrides)
19
23
  new(input)
@@ -24,7 +28,7 @@ module ShopifyCLI
24
28
  end
25
29
 
26
30
  def parse(directory)
27
- File.read(File.join(directory, FILENAME))
31
+ File.read(path(directory))
28
32
  .gsub("\r\n", "\n").split("\n").each_with_object({}) do |line, output|
29
33
  match = /\A([A-Za-z_0-9]+)\s*=\s*(.*)\z/.match(line)
30
34
  if match