shopify-cli 2.7.2 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
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