shopify-cli 2.7.2 → 2.9.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +2 -2
  3. data/.github/workflows/shopify.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +52 -0
  7. data/Codespace.dockerfile +2 -2
  8. data/Gemfile.lock +4 -4
  9. data/RELEASING.md +4 -3
  10. data/Tests.dockerfile +2 -2
  11. data/dev.yml +3 -3
  12. data/ext/javy/hashes/javy-arm-macos-v0.1.0.gz.sha256 +1 -0
  13. data/ext/javy/hashes/javy-x86_64-linux-v0.1.0.gz.sha256 +1 -0
  14. data/ext/javy/hashes/javy-x86_64-macos-v0.1.0.gz.sha256 +1 -0
  15. data/ext/javy/hashes/javy-x86_64-windows-v0.1.0.gz.sha256 +1 -0
  16. data/ext/javy/javy.rb +31 -13
  17. data/lib/graphql/get_extension_registrations.graphql +27 -0
  18. data/lib/project_types/extension/cli.rb +27 -2
  19. data/lib/project_types/extension/commands/build.rb +10 -10
  20. data/lib/project_types/extension/commands/create.rb +2 -3
  21. data/lib/project_types/extension/commands/push.rb +36 -8
  22. data/lib/project_types/extension/extension_project.rb +1 -1
  23. data/lib/project_types/extension/features/argo_serve.rb +6 -5
  24. data/lib/project_types/extension/forms/questions/ask_registration.rb +6 -2
  25. data/lib/project_types/extension/loaders/project.rb +29 -0
  26. data/lib/project_types/extension/loaders/specification_handler.rb +22 -0
  27. data/lib/project_types/extension/messages/messages.rb +4 -0
  28. data/lib/project_types/extension/models/app.rb +1 -1
  29. data/lib/project_types/extension/models/development_server.rb +2 -4
  30. data/lib/project_types/extension/models/specification_handlers/default.rb +4 -0
  31. data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
  32. data/lib/project_types/extension/tasks/execute_commands/base.rb +13 -0
  33. data/lib/project_types/extension/tasks/execute_commands/build.rb +29 -0
  34. data/lib/project_types/extension/tasks/execute_commands/create.rb +33 -0
  35. data/lib/project_types/extension/tasks/execute_commands/serve.rb +35 -0
  36. data/lib/project_types/extension/tasks/merge_server_config.rb +33 -22
  37. data/lib/project_types/rails/commands/create.rb +1 -1
  38. data/lib/project_types/rails/gem.rb +1 -2
  39. data/lib/project_types/script/cli.rb +8 -1
  40. data/lib/project_types/script/commands/connect.rb +19 -0
  41. data/lib/project_types/script/commands/create.rb +8 -2
  42. data/lib/project_types/script/commands/push.rb +35 -12
  43. data/lib/project_types/script/config/extension_points.yml +10 -2
  44. data/lib/project_types/script/graphql/app_script_set.graphql +2 -2
  45. data/lib/project_types/script/layers/application/connect_app.rb +15 -3
  46. data/lib/project_types/script/layers/application/create_script.rb +17 -17
  47. data/lib/project_types/script/layers/application/extension_points.rb +50 -26
  48. data/lib/project_types/script/layers/application/push_script.rb +6 -3
  49. data/lib/project_types/script/layers/domain/errors.rb +1 -4
  50. data/lib/project_types/script/layers/domain/extension_point.rb +14 -0
  51. data/lib/project_types/script/layers/domain/push_package.rb +3 -3
  52. data/lib/project_types/script/layers/domain/{script_json.rb → script_config.rb} +2 -2
  53. data/lib/project_types/script/layers/domain/script_project.rb +1 -1
  54. data/lib/project_types/script/layers/infrastructure/errors.rb +30 -5
  55. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -2
  56. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +125 -27
  57. data/lib/project_types/script/layers/infrastructure/script_service.rb +11 -11
  58. data/lib/project_types/script/loaders/project.rb +44 -0
  59. data/lib/project_types/script/loaders/specification_handler.rb +22 -0
  60. data/lib/project_types/script/messages/messages.rb +34 -3
  61. data/lib/project_types/script/ui/error_handler.rb +35 -15
  62. data/lib/project_types/theme/commands/pull.rb +39 -16
  63. data/lib/project_types/theme/commands/push.rb +60 -29
  64. data/lib/project_types/theme/commands/serve.rb +5 -0
  65. data/lib/project_types/theme/messages/messages.rb +30 -18
  66. data/lib/shopify_cli/command.rb +6 -0
  67. data/lib/shopify_cli/commands/login.rb +11 -5
  68. data/lib/shopify_cli/commands/switch.rb +1 -1
  69. data/lib/shopify_cli/constants.rb +5 -0
  70. data/lib/shopify_cli/context.rb +66 -11
  71. data/lib/shopify_cli/environment.rb +15 -4
  72. data/lib/shopify_cli/form.rb +2 -0
  73. data/lib/shopify_cli/git.rb +2 -0
  74. data/lib/shopify_cli/identity_auth.rb +1 -0
  75. data/lib/shopify_cli/messages/messages.rb +10 -2
  76. data/lib/shopify_cli/partners_api/app_extensions/job.rb +36 -0
  77. data/lib/shopify_cli/partners_api/app_extensions.rb +46 -0
  78. data/lib/shopify_cli/partners_api/organizations.rb +2 -5
  79. data/lib/shopify_cli/partners_api.rb +1 -0
  80. data/lib/shopify_cli/project.rb +8 -7
  81. data/lib/shopify_cli/resources/env_file.rb +18 -6
  82. data/lib/shopify_cli/services/app/create/rails_service.rb +1 -1
  83. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +73 -0
  84. data/lib/shopify_cli/theme/dev_server/hot-reload.js +57 -10
  85. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +18 -2
  86. data/lib/shopify_cli/theme/dev_server/proxy/template_param_builder.rb +84 -0
  87. data/lib/shopify_cli/theme/dev_server/proxy.rb +10 -15
  88. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +34 -0
  89. data/lib/shopify_cli/theme/dev_server.rb +8 -21
  90. data/lib/shopify_cli/theme/theme.rb +26 -4
  91. data/lib/shopify_cli/thread_pool/job.rb +27 -0
  92. data/lib/shopify_cli/thread_pool.rb +37 -0
  93. data/lib/shopify_cli/tunnel.rb +1 -0
  94. data/lib/shopify_cli/version.rb +1 -1
  95. data/lib/shopify_cli.rb +4 -0
  96. data/shopify-cli.gemspec +1 -1
  97. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +3 -1
  98. metadata +26 -7
  99. data/lib/graphql/all_orgs_with_extensions.graphql +0 -37
  100. data/lib/project_types/extension/tasks/run_extension_command.rb +0 -82
@@ -103,32 +103,52 @@ module Script
103
103
  cause_of_error: ShopifyCLI::Context.message("script.error.metadata_not_found_cause"),
104
104
  help_suggestion: ShopifyCLI::Context.message("script.error.metadata_not_found_help"),
105
105
  }
106
- when Layers::Domain::Errors::MissingScriptJsonFieldError
106
+ when Layers::Infrastructure::Errors::BuildError
107
107
  {
108
- cause_of_error: ShopifyCLI::Context.message("script.error.missing_script_json_field_cause", e.field),
109
- help_suggestion: ShopifyCLI::Context.message("script.error.missing_script_json_field_help"),
108
+ cause_of_error: ShopifyCLI::Context.message("script.error.build_error_cause"),
109
+ help_suggestion: ShopifyCLI::Context.message("script.error.build_error_help"),
110
110
  }
111
- when Layers::Domain::Errors::InvalidScriptJsonDefinitionError
111
+ when Layers::Infrastructure::Errors::InvalidScriptConfigYmlDefinitionError
112
+ {
113
+ cause_of_error: ShopifyCLI::Context.message("script.error.invalid_script_config_yml_definition_cause"),
114
+ help_suggestion: ShopifyCLI::Context.message("script.error.invalid_script_config_yml_definition_help"),
115
+ }
116
+ when Layers::Infrastructure::Errors::InvalidScriptJsonDefinitionError
112
117
  {
113
118
  cause_of_error: ShopifyCLI::Context.message("script.error.invalid_script_json_definition_cause"),
114
119
  help_suggestion: ShopifyCLI::Context.message("script.error.invalid_script_json_definition_help"),
115
120
  }
116
- when Layers::Domain::Errors::NoScriptJsonFile
121
+ when Layers::Infrastructure::Errors::MissingScriptConfigYmlFieldError
117
122
  {
118
- cause_of_error: ShopifyCLI::Context.message("script.error.no_script_json_file_cause"),
119
- help_suggestion: ShopifyCLI::Context.message("script.error.no_script_json_file_help"),
123
+ cause_of_error: ShopifyCLI::Context.message("script.error.missing_script_config_yml_field_cause", e.field),
124
+ help_suggestion: ShopifyCLI::Context.message("script.error.missing_script_config_yml_field_help"),
120
125
  }
121
- when Layers::Infrastructure::Errors::BuildError
126
+ when Layers::Infrastructure::Errors::MissingScriptConfigYmlFieldError
122
127
  {
123
- cause_of_error: ShopifyCLI::Context.message("script.error.build_error_cause"),
124
- help_suggestion: ShopifyCLI::Context.message("script.error.build_error_help"),
128
+ cause_of_error: ShopifyCLI::Context.message("script.error.missing_script_config_yml_field_cause", e.field),
129
+ help_suggestion: ShopifyCLI::Context.message("script.error.missing_script_config_yml_field_help"),
130
+ }
131
+ when Layers::Infrastructure::Errors::MissingScriptJsonFieldError
132
+ {
133
+ cause_of_error: ShopifyCLI::Context.message("script.error.missing_script_json_field_cause", e.field),
134
+ help_suggestion: ShopifyCLI::Context.message("script.error.missing_script_json_field_help"),
125
135
  }
126
- when Layers::Infrastructure::Errors::ScriptJsonSyntaxError
136
+ when Layers::Infrastructure::Errors::NoScriptConfigYmlFileError
137
+ {
138
+ cause_of_error: ShopifyCLI::Context.message("script.error.no_script_config_yml_file_cause"),
139
+ help_suggestion: ShopifyCLI::Context.message("script.error.no_script_config_yml_file_help"),
140
+ }
141
+ when Layers::Infrastructure::Errors::ScriptConfigSyntaxError
127
142
  {
128
143
  cause_of_error: ShopifyCLI::Context.message("script.error.configuration_syntax_error_cause"),
129
144
  help_suggestion: ShopifyCLI::Context.message("script.error.configuration_syntax_error_help"),
130
145
  }
131
- when Layers::Infrastructure::Errors::ScriptJsonMissingKeysError
146
+ when Layers::Infrastructure::Errors::ScriptEnvAppNotConnectedError
147
+ {
148
+ cause_of_error: ShopifyCLI::Context.message("script.error.app_not_connected_cause"),
149
+ help_suggestion: ShopifyCLI::Context.message("script.error.app_not_connected_help"),
150
+ }
151
+ when Layers::Infrastructure::Errors::ScriptConfigMissingKeysError
132
152
  {
133
153
  cause_of_error: ShopifyCLI::Context.message(
134
154
  "script.error.configuration_missing_keys_error_cause",
@@ -136,7 +156,7 @@ module Script
136
156
  ),
137
157
  help_suggestion: ShopifyCLI::Context.message("script.error.configuration_missing_keys_error_help"),
138
158
  }
139
- when Layers::Infrastructure::Errors::ScriptJsonInvalidValueError
159
+ when Layers::Infrastructure::Errors::ScriptConfigInvalidValueError
140
160
  {
141
161
  cause_of_error: ShopifyCLI::Context.message(
142
162
  "script.error.configuration_invalid_value_error_cause",
@@ -144,7 +164,7 @@ module Script
144
164
  ),
145
165
  help_suggestion: ShopifyCLI::Context.message("script.error.configuration_invalid_value_error_help"),
146
166
  }
147
- when Layers::Infrastructure::Errors::ScriptJsonFieldsMissingKeysError
167
+ when Layers::Infrastructure::Errors::ScriptConfigFieldsMissingKeysError
148
168
  {
149
169
  cause_of_error: ShopifyCLI::Context.message(
150
170
  "script.error.configuration_schema_field_missing_keys_error_cause",
@@ -154,7 +174,7 @@ module Script
154
174
  "script.error.configuration_definition_schema_field_missing_keys_error_help"
155
175
  ),
156
176
  }
157
- when Layers::Infrastructure::Errors::ScriptJsonFieldsInvalidValueError
177
+ when Layers::Infrastructure::Errors::ScriptConfigFieldsInvalidValueError
158
178
  {
159
179
  cause_of_error: ShopifyCLI::Context.message(
160
180
  "script.error.configuration_schema_field_invalid_value_error_cause",
@@ -9,7 +9,9 @@ module Theme
9
9
  options do |parser, flags|
10
10
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
11
11
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
12
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
12
13
  parser.on("-l", "--live") { flags[:live] = true }
14
+ parser.on("-d", "--development") { flags[:development] = true }
13
15
  parser.on("-x", "--ignore=PATTERN") do |pattern|
14
16
  flags[:ignores] ||= []
15
17
  flags[:ignores] << pattern
@@ -19,21 +21,8 @@ module Theme
19
21
  def call(args, _name)
20
22
  root = args.first || "."
21
23
  delete = !options.flags[:nodelete]
22
-
23
- theme = if (theme_id = options.flags[:theme_id])
24
- ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
25
- elsif options.flags[:live]
26
- ShopifyCLI::Theme::Theme.live(@ctx, root: root)
27
- else
28
- form = Forms::Select.ask(
29
- @ctx,
30
- [],
31
- title: @ctx.message("theme.pull.select"),
32
- root: root,
33
- )
34
- return unless form
35
- form.theme
36
- end
24
+ theme = find_theme(root, **options.flags)
25
+ return if theme.nil?
37
26
 
38
27
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
39
28
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
@@ -46,7 +35,7 @@ module Theme
46
35
  end
47
36
  @ctx.done(@ctx.message("theme.pull.done"))
48
37
  rescue ShopifyCLI::API::APIRequestNotFoundError
49
- @ctx.abort(@ctx.message("theme.pull.theme_not_found", theme.id))
38
+ @ctx.abort(@ctx.message("theme.pull.theme_not_found", "##{theme.id}"))
50
39
  ensure
51
40
  syncer.shutdown
52
41
  end
@@ -55,6 +44,40 @@ module Theme
55
44
  def self.help
56
45
  ShopifyCLI::Context.message("theme.pull.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
57
46
  end
47
+
48
+ private
49
+
50
+ def find_theme(root, theme_id: nil, theme: nil, live: nil, development: nil, **_args)
51
+ if theme_id
52
+ @ctx.warn(@ctx.message("theme.pull.deprecated_themeid"))
53
+ return ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
54
+ end
55
+
56
+ if theme
57
+ selected_theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: theme)
58
+ return selected_theme || @ctx.abort(@ctx.message("theme.pull.theme_not_found", theme))
59
+ end
60
+
61
+ if live
62
+ return ShopifyCLI::Theme::Theme.live(@ctx, root: root)
63
+ end
64
+
65
+ if development
66
+ return ShopifyCLI::Theme::Theme.development(@ctx, root: root)
67
+ end
68
+
69
+ select_theme(root)
70
+ end
71
+
72
+ def select_theme(root)
73
+ form = Forms::Select.ask(
74
+ @ctx,
75
+ [],
76
+ title: @ctx.message("theme.pull.select"),
77
+ root: root,
78
+ )
79
+ form&.theme
80
+ end
58
81
  end
59
82
  end
60
83
  end
@@ -10,6 +10,7 @@ module Theme
10
10
  options do |parser, flags|
11
11
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
12
12
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
13
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
13
14
  parser.on("-l", "--live") { flags[:live] = true }
14
15
  parser.on("-d", "--development") { flags[:development] = true }
15
16
  parser.on("-u", "--unpublished") { flags[:unpublished] = true }
@@ -25,34 +26,13 @@ module Theme
25
26
  def call(args, _name)
26
27
  root = args.first || "."
27
28
  delete = !options.flags[:nodelete]
29
+ theme = find_theme(root, **options.flags)
30
+ return if theme.nil?
28
31
 
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
52
-
53
- is_confirm_required = !options.flags[:allow_live] && !options.flags[:live]
54
- if theme.live? && is_confirm_required
55
- return unless CLI::UI::Prompt.confirm(@ctx.message("theme.push.live"))
32
+ if theme.live? && !options.flags[:allow_live]
33
+ question = @ctx.message("theme.push.live")
34
+ question += @ctx.message("theme.push.theme", theme.name, theme.id) if options.flags[:live]
35
+ return unless CLI::UI::Prompt.confirm(question)
56
36
  end
57
37
 
58
38
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
@@ -77,16 +57,67 @@ module Theme
77
57
  end
78
58
  end
79
59
  raise ShopifyCLI::AbortSilent if syncer.has_any_error?
80
- rescue ShopifyCLI::API::APIRequestNotFoundError
81
- @ctx.abort(@ctx.message("theme.push.theme_not_found", theme.id))
82
60
  ensure
83
61
  syncer.shutdown
84
62
  end
63
+ rescue ShopifyCLI::API::APIRequestNotFoundError
64
+ @ctx.abort(@ctx.message("theme.push.theme_not_found", "##{theme.id}"))
85
65
  end
86
66
 
87
67
  def self.help
88
68
  ShopifyCLI::Context.message("theme.push.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
89
69
  end
70
+
71
+ private
72
+
73
+ def find_theme(root, theme_id: nil, theme: nil, live: nil, development: nil, unpublished: nil, **_args)
74
+ if theme_id
75
+ @ctx.warn(@ctx.message("theme.push.deprecated_themeid"))
76
+ return ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
77
+ end
78
+
79
+ if live
80
+ return ShopifyCLI::Theme::Theme.live(@ctx, root: root)
81
+ end
82
+
83
+ if development
84
+ new_theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
85
+ new_theme.ensure_exists!
86
+ return new_theme
87
+ end
88
+
89
+ if unpublished
90
+ name = theme || ask_theme_name
91
+ new_theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
92
+ new_theme.create
93
+ return new_theme
94
+ end
95
+
96
+ if theme
97
+ selected_theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: theme)
98
+ return selected_theme || @ctx.abort(@ctx.message("theme.push.theme_not_found", theme))
99
+ end
100
+
101
+ select_theme(root)
102
+ end
103
+
104
+ def ask_theme_name
105
+ CLI::UI::Prompt.ask(@ctx.message("theme.push.name"), allow_empty: false)
106
+ end
107
+
108
+ def select_theme(root)
109
+ form = Forms::Select.ask(
110
+ @ctx,
111
+ [],
112
+ title: @ctx.message("theme.push.select"),
113
+ root: root,
114
+ )
115
+ form&.theme
116
+ end
117
+
118
+ def themes(root)
119
+ ShopifyCLI::Theme::Theme.all(@ctx, root: root)
120
+ end
90
121
  end
91
122
  end
92
123
  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,14 @@ 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
68
 
69
69
  Run without options to select theme from a list.
70
70
  HELP
@@ -74,7 +74,11 @@ module Theme
74
74
  push: "Pushing theme files to Shopify",
75
75
  select: "Select theme to push to",
76
76
  live: "Are you sure you want to push to your live theme?",
77
- theme_not_found: "Theme #%s doesn't exist",
77
+ theme: "\n Theme: {{blue:%s #%s}} {{green:[live]}}",
78
+ deprecated_themeid: <<~WARN,
79
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
80
+ WARN
81
+ theme_not_found: "Theme \"%s\" doesn't exist",
78
82
  done: <<~DONE,
79
83
  {{green:Your theme was pushed successfully}}
80
84
 
@@ -93,10 +97,16 @@ module Theme
93
97
  Usage: {{command:%s theme serve}}
94
98
 
95
99
  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.
100
+ {{command:--port=PORT}} Local port to serve theme preview from.
101
+ {{command:--poll}} Force polling to detect file changes.
102
+ {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
103
+ {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
104
+ - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
105
+ - {{command:full-page}} Always refreshes the entire page
106
+ - {{command:off}} Deactivate live reload
99
107
  HELP
108
+ reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
109
+ try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
100
110
  viewing_theme: "Viewing theme…",
101
111
  syncing_theme: "Syncing theme #%s on %s",
102
112
  open_fail: "Couldn't open the theme",
@@ -130,9 +140,7 @@ module Theme
130
140
  You are not authorized to edit themes on %s.
131
141
  Make sure you are a user of that store, and allowed to edit themes.
132
142
  ENSURE_USER
133
- already_in_use_error: "Error",
134
143
  address_already_in_use: "The address \"%s\" is already in use.",
135
- try_this: "Try this",
136
144
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
137
145
  },
138
146
  check: {
@@ -188,16 +196,20 @@ module Theme
188
196
  Usage: {{command:%s theme pull [ ROOT ]}}
189
197
 
190
198
  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.
199
+ {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
200
+ {{command:-l, --live}} Pull theme files from your remote live theme.
201
+ {{command:-d, --development}} Pull theme files from your remote development theme.
202
+ {{command:-n, --nodelete}} Runs the pull command without deleting local files.
194
203
 
195
204
  Run without options to select theme from a list.
196
205
  HELP
197
206
  select: "Select a theme to pull from",
198
207
  pulling: "Pulling theme files from %s (#%s) on %s",
199
208
  done: "Theme pulled successfully",
200
- not_found: "{{x}} Theme #%s doesn't exist",
209
+ deprecated_themeid: <<~WARN,
210
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
211
+ WARN
212
+ theme_not_found: "Theme \"%s\" doesn't exist",
201
213
  },
202
214
  },
203
215
  }.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 }
@@ -15,8 +15,6 @@ module ShopifyCLI
15
15
 
16
16
  def call(*)
17
17
  shop = (options.flags[:shop] || @ctx.getenv("SHOPIFY_SHOP" || nil))
18
- ShopifyCLI::DB.set(shop: self.class.validate_shop(shop, context: @ctx)) unless shop.nil?
19
-
20
18
  if shop.nil? && Shopifolk.check
21
19
  Shopifolk.reset
22
20
  @ctx.puts(@ctx.message("core.tasks.select_org_and_shop.identified_as_shopify"))
@@ -33,17 +31,25 @@ module ShopifyCLI
33
31
  IdentityAuth.new(ctx: @ctx).authenticate
34
32
  org = select_organization
35
33
  ShopifyCLI::DB.set(organization_id: org["id"].to_i) unless org.nil?
36
- Whoami.call([], "whoami")
34
+
37
35
  end
36
+ # validate that shop belongs to organization
37
+ ShopifyCLI::DB.set(shop: self.class.validate_shop(shop: shop, org: org, context: @ctx)) unless shop.nil?
38
+ Whoami.call([], "whoami")
38
39
  end
39
40
 
40
41
  def self.help
41
42
  ShopifyCLI::Context.message("core.login.help", ShopifyCLI::TOOL_NAME)
42
43
  end
43
44
 
44
- def self.validate_shop(shop, context:)
45
+ def self.validate_shop(shop:, org:, context:)
45
46
  permanent_domain = shop_to_permanent_domain(shop)
46
47
  context.abort(context.message("core.login.invalid_shop", shop)) unless permanent_domain
48
+ if org
49
+ stores_owned = org["stores"]
50
+ is_verified = stores_owned.any? { |store| store["shopDomain"] == permanent_domain }
51
+ context.abort(context.message("core.login.invalid_shop", shop)) unless is_verified
52
+ end
47
53
  permanent_domain
48
54
  end
49
55
 
@@ -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,6 +30,7 @@ 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.
@@ -61,5 +62,9 @@ module ShopifyCLI
61
62
  module Links
62
63
  NEW_ISSUE = "https://github.com/Shopify/shopify-cli/issues/new"
63
64
  end
65
+
66
+ module Extension
67
+ DEFAULT_PORT = 39351
68
+ end
64
69
  end
65
70
  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,13 +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
114
+ return :mac_m1 if /arm64.*darwin/i.match(host)
64
115
  return :mac if /darwin/i.match(host)
65
116
  return :windows if /mswin|mingw|cygwin/i.match(host)
66
117
  return :linux if /linux|bsd/i.match(host)
67
118
  :unknown
68
119
  end
69
120
 
70
- # 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.
71
127
  def mac?
72
128
  os == :mac
73
129
  end
@@ -89,7 +145,7 @@ module ShopifyCLI
89
145
 
90
146
  # will return true if being launched from a tty
91
147
  def tty?
92
- $stdin.tty? && !testing?
148
+ $stdin.tty?
93
149
  end
94
150
 
95
151
  # will return true if the cli is being run from an installation, and not a
@@ -328,7 +384,7 @@ module ShopifyCLI
328
384
  system("xdg-open", uri.to_s)
329
385
  elsif windows?
330
386
  system("start \"\" \"#{uri}\"")
331
- elsif mac?
387
+ elsif mac? || mac_m1?
332
388
  system("open", uri.to_s)
333
389
  else
334
390
  open_url!(uri)
@@ -348,13 +404,13 @@ module ShopifyCLI
348
404
  puts "{{yellow:*}} #{text}"
349
405
  end
350
406
 
351
- # a wrapper around Kernel.puts to allow for easy formatting
407
+ # proxy call to Context.puts.
352
408
  #
353
409
  # #### Parameters
354
410
  # * `text` - a string message to output
355
411
  #
356
412
  def puts(*args)
357
- Kernel.puts(CLI::UI.fmt(*args))
413
+ Context.puts(*args)
358
414
  end
359
415
 
360
416
  # a wrapper around $stderr.puts to allow for easy formatting
@@ -384,14 +440,13 @@ module ShopifyCLI
384
440
  puts("{{v}} #{text}")
385
441
  end
386
442
 
387
- # aborts the current running command and outputs an error message, prefixed
388
- # by a red x
443
+ # proxy call to Context.abort.
389
444
  #
390
445
  # #### Parameters
391
- # * `text` - a string message to output
392
- #
393
- def abort(text)
394
- 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)
395
450
  end
396
451
 
397
452
  # outputs a message, prefixed by a red `DEBUG` tag. This will only output to
@@ -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,
@@ -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