shopify-cli 1.12.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/CONTRIBUTING.md +7 -7
  4. data/.github/DESIGN.md +3 -3
  5. data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  6. data/.github/workflows/build.yml +1 -1
  7. data/.gitignore +3 -0
  8. data/.rubocop.yml +3 -1
  9. data/.ruby-version +1 -1
  10. data/CHANGELOG.md +52 -21
  11. data/Gemfile +4 -0
  12. data/Gemfile.lock +32 -0
  13. data/LICENSE +4 -1
  14. data/README.md +92 -26
  15. data/RELEASING.md +31 -7
  16. data/Rakefile +2 -2
  17. data/SECURITY.md +1 -1
  18. data/bin/load_shopify.rb +1 -1
  19. data/bin/shopify +3 -3
  20. data/dev.yml +1 -1
  21. data/docs/app/node/index.md +1 -1
  22. data/docs/app/rails/index.md +1 -1
  23. data/docs/core/index.md +1 -1
  24. data/docs/getting-started/index.md +1 -1
  25. data/docs/getting-started/install/index.md +1 -1
  26. data/docs/getting-started/migrate/index.md +1 -1
  27. data/docs/getting-started/uninstall/index.md +1 -1
  28. data/docs/getting-started/upgrade/index.md +1 -1
  29. data/docs/help/start-app/index.md +1 -1
  30. data/docs/index.md +1 -1
  31. data/ext/shopify-cli/extconf.rb +17 -5
  32. data/install.sh +1 -1
  33. data/lib/docgen/index_template.md.erb +2 -2
  34. data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
  35. data/lib/graphql/find_organization.graphql +2 -1
  36. data/lib/project_types/extension/cli.rb +18 -15
  37. data/lib/project_types/extension/commands/build.rb +4 -5
  38. data/lib/project_types/extension/commands/connect.rb +35 -0
  39. data/lib/project_types/extension/commands/create.rb +12 -16
  40. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  41. data/lib/project_types/extension/commands/info.rb +86 -0
  42. data/lib/project_types/extension/commands/push.rb +8 -7
  43. data/lib/project_types/extension/commands/register.rb +4 -5
  44. data/lib/project_types/extension/commands/serve.rb +5 -8
  45. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  46. data/lib/project_types/extension/errors.rb +9 -0
  47. data/lib/project_types/extension/extension_project.rb +5 -0
  48. data/lib/project_types/extension/features/argo.rb +6 -6
  49. data/lib/project_types/extension/features/argo_runtime.rb +22 -59
  50. data/lib/project_types/extension/features/argo_serve.rb +25 -21
  51. data/lib/project_types/extension/forms/connect.rb +42 -0
  52. data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
  53. data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
  54. data/lib/project_types/extension/messages/messages.rb +75 -11
  55. data/lib/project_types/extension/models/specification.rb +1 -0
  56. data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
  57. data/lib/project_types/extension/models/specification_handlers/default.rb +13 -13
  58. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +89 -0
  59. data/lib/project_types/extension/models/specifications.rb +1 -0
  60. data/lib/project_types/extension/tasks/configure_features.rb +6 -7
  61. data/lib/project_types/extension/tasks/configure_options.rb +20 -0
  62. data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
  63. data/lib/project_types/node/cli.rb +9 -21
  64. data/lib/project_types/node/commands/connect.rb +8 -2
  65. data/lib/project_types/node/commands/create.rb +9 -5
  66. data/lib/project_types/node/commands/deploy.rb +15 -5
  67. data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
  68. data/lib/project_types/node/commands/generate.rb +4 -2
  69. data/lib/project_types/node/commands/open.rb +4 -2
  70. data/lib/project_types/node/commands/serve.rb +3 -2
  71. data/lib/project_types/node/commands/tunnel.rb +4 -2
  72. data/lib/project_types/node/messages/messages.rb +46 -89
  73. data/lib/project_types/rails/cli.rb +9 -21
  74. data/lib/project_types/rails/commands/connect.rb +8 -2
  75. data/lib/project_types/rails/commands/create.rb +10 -6
  76. data/lib/project_types/rails/commands/deploy.rb +15 -5
  77. data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
  78. data/lib/project_types/rails/commands/generate.rb +15 -5
  79. data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
  80. data/lib/project_types/rails/commands/open.rb +4 -2
  81. data/lib/project_types/rails/commands/serve.rb +3 -2
  82. data/lib/project_types/rails/commands/tunnel.rb +4 -2
  83. data/lib/project_types/rails/messages/messages.rb +54 -101
  84. data/lib/project_types/script/cli.rb +18 -20
  85. data/lib/project_types/script/commands/create.rb +3 -1
  86. data/lib/project_types/script/commands/push.rb +12 -5
  87. data/lib/project_types/script/config/extension_points.yml +0 -3
  88. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +9 -3
  89. data/lib/project_types/script/layers/application/create_script.rb +6 -5
  90. data/lib/project_types/script/layers/application/push_script.rb +2 -1
  91. data/lib/project_types/script/layers/domain/errors.rb +6 -11
  92. data/lib/project_types/script/layers/domain/push_package.rb +4 -8
  93. data/lib/project_types/script/layers/domain/script_json.rb +32 -0
  94. data/lib/project_types/script/layers/domain/script_project.rb +1 -1
  95. data/lib/project_types/script/layers/infrastructure/errors.rb +14 -18
  96. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +105 -0
  97. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +103 -0
  98. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +26 -0
  99. data/lib/project_types/script/layers/infrastructure/languages/rust_project_creator.rb +73 -0
  100. data/lib/project_types/script/layers/infrastructure/languages/rust_task_runner.rb +60 -0
  101. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +21 -0
  102. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -4
  103. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -34
  104. data/lib/project_types/script/layers/infrastructure/script_service.rb +37 -16
  105. data/lib/project_types/script/messages/messages.rb +66 -55
  106. data/lib/project_types/script/tasks/ensure_env.rb +22 -1
  107. data/lib/project_types/script/ui/error_handler.rb +32 -32
  108. data/lib/project_types/theme/cli.rb +16 -27
  109. data/lib/project_types/theme/commands/check.rb +33 -0
  110. data/lib/project_types/theme/commands/delete.rb +64 -0
  111. data/lib/project_types/theme/commands/init.rb +42 -0
  112. data/lib/project_types/theme/commands/language_server.rb +16 -0
  113. data/lib/project_types/theme/commands/package.rb +55 -0
  114. data/lib/project_types/theme/commands/publish.rb +43 -0
  115. data/lib/project_types/theme/commands/pull.rb +51 -0
  116. data/lib/project_types/theme/commands/push.rb +58 -32
  117. data/lib/project_types/theme/commands/serve.rb +8 -16
  118. data/lib/project_types/theme/forms/confirm_store.rb +15 -0
  119. data/lib/project_types/theme/forms/select.rb +59 -0
  120. data/lib/project_types/theme/messages/messages.rb +117 -102
  121. data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
  122. data/lib/shopify-cli/admin_api.rb +53 -35
  123. data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
  124. data/lib/shopify-cli/admin_api/schema.rb +1 -10
  125. data/lib/shopify-cli/api.rb +29 -14
  126. data/lib/shopify-cli/command.rb +15 -3
  127. data/lib/shopify-cli/commands.rb +7 -2
  128. data/lib/shopify-cli/commands/help.rb +2 -29
  129. data/lib/shopify-cli/commands/login.rb +95 -0
  130. data/lib/shopify-cli/commands/logout.rb +24 -8
  131. data/lib/shopify-cli/commands/populate.rb +23 -0
  132. data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
  133. data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
  134. data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
  135. data/lib/shopify-cli/commands/store.rb +15 -0
  136. data/lib/shopify-cli/commands/switch.rb +39 -0
  137. data/lib/shopify-cli/commands/system.rb +12 -0
  138. data/lib/shopify-cli/commands/whoami.rb +28 -0
  139. data/lib/shopify-cli/connect.rb +32 -0
  140. data/lib/shopify-cli/context.rb +65 -4
  141. data/lib/shopify-cli/core/entry_point.rb +3 -22
  142. data/lib/shopify-cli/db.rb +4 -4
  143. data/lib/shopify-cli/http_request.rb +16 -0
  144. data/lib/shopify-cli/identity_auth.rb +282 -0
  145. data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
  146. data/lib/shopify-cli/messages/messages.rb +133 -39
  147. data/lib/shopify-cli/partners_api.rb +21 -41
  148. data/lib/shopify-cli/partners_api/organizations.rb +8 -0
  149. data/lib/shopify-cli/project_commands.rb +16 -0
  150. data/lib/shopify-cli/project_type.rb +0 -31
  151. data/lib/shopify-cli/resources/env_file.rb +1 -1
  152. data/lib/shopify-cli/shopifolk.rb +8 -11
  153. data/lib/shopify-cli/sub_command.rb +1 -0
  154. data/lib/shopify-cli/tasks.rb +3 -0
  155. data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
  156. data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
  157. data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
  158. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
  159. data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
  160. data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
  161. data/lib/shopify-cli/theme/dev_server.rb +98 -0
  162. data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
  163. data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
  164. data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
  165. data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
  166. data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
  167. data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
  168. data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
  169. data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
  170. data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
  171. data/lib/shopify-cli/theme/development_theme.rb +69 -0
  172. data/lib/shopify-cli/theme/file.rb +112 -0
  173. data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
  174. data/lib/shopify-cli/theme/mime_type.rb +34 -0
  175. data/lib/shopify-cli/theme/syncer.rb +328 -0
  176. data/lib/shopify-cli/theme/theme.rb +204 -0
  177. data/lib/shopify-cli/version.rb +1 -1
  178. data/lib/shopify_cli.rb +18 -11
  179. data/shopify-cli.gemspec +12 -5
  180. data/shopify.fish +1 -1
  181. data/shopify.sh +1 -1
  182. metadata +96 -41
  183. data/.github/workflows/release.yml +0 -61
  184. data/lib/project_types/extension/features/argo_serve_options.rb +0 -41
  185. data/lib/project_types/node/commands/populate.rb +0 -23
  186. data/lib/project_types/rails/commands/populate.rb +0 -23
  187. data/lib/project_types/rails/commands/populate/customer.rb +0 -31
  188. data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
  189. data/lib/project_types/rails/commands/populate/product.rb +0 -30
  190. data/lib/project_types/script/layers/domain/config_ui.rb +0 -16
  191. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +0 -95
  192. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +0 -101
  193. data/lib/project_types/script/layers/infrastructure/project_creator.rb +0 -24
  194. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +0 -71
  195. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +0 -58
  196. data/lib/project_types/script/layers/infrastructure/task_runner.rb +0 -19
  197. data/lib/project_types/theme/commands/connect.rb +0 -54
  198. data/lib/project_types/theme/commands/create.rb +0 -48
  199. data/lib/project_types/theme/commands/deploy.rb +0 -38
  200. data/lib/project_types/theme/commands/generate.rb +0 -20
  201. data/lib/project_types/theme/commands/generate/env.rb +0 -79
  202. data/lib/project_types/theme/forms/connect.rb +0 -34
  203. data/lib/project_types/theme/forms/create.rb +0 -22
  204. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
  205. data/lib/project_types/theme/themekit.rb +0 -113
  206. data/lib/shopify-cli/commands/connect.rb +0 -64
  207. data/lib/shopify-cli/commands/create.rb +0 -50
  208. data/lib/shopify-cli/oauth.rb +0 -198
@@ -0,0 +1,20 @@
1
+ module Theme
2
+ module UI
3
+ class SyncProgressBar
4
+ def initialize(syncer)
5
+ @syncer = syncer
6
+ end
7
+
8
+ def progress(method, **args)
9
+ @syncer.delay_errors!
10
+ CLI::UI::Progress.progress do |bar|
11
+ @syncer.public_send(method, **args) do |left, total|
12
+ bar.tick(set_percent: 1 - left.to_f / total)
13
+ end
14
+ bar.tick(set_percent: 1)
15
+ end
16
+ @syncer.report_errors!
17
+ end
18
+ end
19
+ end
20
+ end
@@ -38,9 +38,15 @@ module ShopifyCli
38
38
  # ShopifyCli::AdminAPI.query(@ctx, 'all_organizations')
39
39
  #
40
40
  def query(ctx, query_name, shop:, api_version: nil, **variables)
41
- authenticated_req(ctx, shop) do
41
+ CLI::Kit::Util.begin do
42
42
  api_client(ctx, api_version, shop).query(query_name, variables: variables)
43
+ end.retry_after(API::APIRequestUnauthorizedError, retries: 1) do
44
+ ShopifyCli::IdentityAuth.new(ctx: ctx).reauthenticate
43
45
  end
46
+ rescue API::APIRequestUnauthorizedError
47
+ ctx.abort(ctx.message("core.api.error.failed_auth"))
48
+ rescue API::APIRequestForbiddenError
49
+ ctx.abort(ctx.message("core.api.error.forbidden", ShopifyCli::TOOL_NAME))
44
50
  end
45
51
 
46
52
  ##
@@ -75,49 +81,47 @@ module ShopifyCli
75
81
  # path: 'data.json',
76
82
  # token: 'password')
77
83
  #
78
- def rest_request(ctx, shop:, path:, body: nil, method: "GET", api_version: nil, token: nil)
79
- ShopifyCli::DB.set(admin_access_token: token) unless token.nil?
80
- url = URI::HTTPS.build(host: shop, path: "/admin/api/#{fetch_api_version(ctx, api_version, shop)}/#{path}")
81
- resp = api_client(ctx, api_version, shop, path: path).request(url: url.to_s, body: body, method: method)
82
- ShopifyCli::DB.set(admin_access_token: nil) unless token.nil?
83
- resp
84
+ def rest_request(ctx, shop:, path:, query: nil, body: nil, method: "GET", api_version: nil, token: nil)
85
+ CLI::Kit::Util.begin do
86
+ ShopifyCli::DB.set(shopify_exchange_token: token) unless token.nil?
87
+ url = URI::HTTPS.build(
88
+ host: shop,
89
+ path: "/admin/api/#{fetch_api_version(ctx, api_version, shop)}/#{path}",
90
+ query: query,
91
+ )
92
+ resp = api_client(ctx, api_version, shop, path: path).request(url: url.to_s, body: body, method: method)
93
+ ShopifyCli::DB.set(shopify_exchange_token: nil) unless token.nil?
94
+ resp
95
+ end.retry_after(API::APIRequestUnauthorizedError) do
96
+ ShopifyCli::IdentityAuth.new(ctx: ctx).reauthenticate
97
+ end
84
98
  end
85
99
 
86
- private
87
-
88
- def authenticated_req(ctx, shop)
89
- yield
90
- rescue API::APIRequestUnauthorizedError
91
- authenticate(ctx, shop)
92
- retry
100
+ def get_shop_or_abort(ctx)
101
+ ctx.abort(
102
+ ctx.message("core.populate.error.no_shop", ShopifyCli::TOOL_NAME)
103
+ ) unless ShopifyCli::DB.exists?(:shop)
104
+ ShopifyCli::DB.get(:shop)
93
105
  end
94
106
 
95
- def authenticate(ctx, shop)
96
- env = Project.current.env
97
- ShopifyCli::OAuth.new(
98
- ctx: ctx,
99
- service: "admin",
100
- client_id: env.api_key,
101
- secret: env.secret,
102
- scopes: env.scopes,
103
- token_path: "/access_token",
104
- options: { "grant_options[]" => "per user" },
105
- ).authenticate("https://#{shop}/admin/oauth")
107
+ private
108
+
109
+ def authenticate(ctx, _shop)
110
+ ShopifyCli::IdentityAuth.new(ctx: ctx).authenticate
106
111
  end
107
112
 
108
113
  def api_client(ctx, api_version, shop, path: "graphql.json")
109
114
  new(
110
115
  ctx: ctx,
111
- auth_header: "X-Shopify-Access-Token",
112
- token: admin_access_token(ctx, shop),
116
+ token: access_token(ctx, shop),
113
117
  url: "https://#{shop}/admin/api/#{fetch_api_version(ctx, api_version, shop)}/#{path}",
114
118
  )
115
119
  end
116
120
 
117
- def admin_access_token(ctx, shop)
118
- ShopifyCli::DB.get(:admin_access_token) do
121
+ def access_token(ctx, shop)
122
+ ShopifyCli::DB.get(:shopify_exchange_token) do
119
123
  authenticate(ctx, shop)
120
- ShopifyCli::DB.get(:admin_access_token)
124
+ ShopifyCli::DB.get(:shopify_exchange_token)
121
125
  end
122
126
  end
123
127
 
@@ -125,14 +129,28 @@ module ShopifyCli
125
129
  return api_version unless api_version.nil?
126
130
  client = new(
127
131
  ctx: ctx,
128
- auth_header: "X-Shopify-Access-Token",
129
- token: admin_access_token(ctx, shop),
132
+ token: access_token(ctx, shop),
130
133
  url: "https://#{shop}/admin/api/unstable/graphql.json",
131
134
  )
132
- versions = client.query("api_versions")["data"]["publicApiVersions"]
133
- latest = versions.find { |version| version["displayName"].include?("Latest") }
134
- latest["handle"]
135
+ CLI::Kit::Util.begin do
136
+ versions = client.query("api_versions")["data"]["publicApiVersions"]
137
+ latest = versions.find { |version| version["displayName"].include?("Latest") }
138
+ latest["handle"]
139
+ end.retry_after(API::APIRequestUnauthorizedError, retries: 1) do
140
+ ShopifyCli::IdentityAuth.new(ctx: ctx).reauthenticate
141
+ end
142
+ rescue API::APIRequestUnauthorizedError
143
+ ctx.abort(ctx.message("core.api.error.failed_auth"))
144
+ rescue API::APIRequestForbiddenError
145
+ ctx.abort(ctx.message("core.api.error.forbidden", ShopifyCli::TOOL_NAME))
135
146
  end
136
147
  end
148
+
149
+ def auth_headers(token)
150
+ {
151
+ Authorization: "Bearer #{token}",
152
+ "X-Shopify-Access-Token" => token, # TODO: Remove when we no longer need private apps
153
+ }
154
+ end
137
155
  end
138
156
  end
@@ -25,12 +25,11 @@ module ShopifyCli
25
25
  end
26
26
 
27
27
  def call(args, _)
28
- return unless Project.current
29
- Tasks::EnsureEnv.call(@ctx)
30
28
  @args = args
31
29
  @input = Hash.new
32
30
  @count = DEFAULT_COUNT
33
31
  @help = false
32
+ @skip_shop_confirmation = false
34
33
  input_options
35
34
  resource_options.parse(@args)
36
35
 
@@ -41,8 +40,8 @@ module ShopifyCli
41
40
  return @ctx.puts(output)
42
41
  end
43
42
 
44
- @shop ||= Project.current.env.shop || get_shop(@ctx)
45
-
43
+ ShopifyCli::Tasks::ConfirmStore.call(@ctx) unless @skip_shop_confirmation
44
+ @shop = AdminAPI.get_shop_or_abort(@ctx)
46
45
  if @silent
47
46
  spin_group = CLI::UI::SpinGroup.new
48
47
  spin_group.add(@ctx.message("core.populate.populating", @count, camel_case_resource_type)) do |spinner|
@@ -89,7 +88,7 @@ module ShopifyCli
89
88
 
90
89
  opts.on("--silent") { |v| @silent = v }
91
90
 
92
- opts.on("--shop=", "-s") { |value| @shop = value }
91
+ opts.on("--skip-shop-confirmation") { |v| @skip_shop_confirmation = v }
93
92
  end
94
93
  end
95
94
 
@@ -130,7 +129,7 @@ module ShopifyCli
130
129
  "core.populate.completion_message",
131
130
  @count,
132
131
  "#{camel_case_resource_type}#{plural}",
133
- Project.current.env.shop,
132
+ @shop,
134
133
  camel_case_resource_type,
135
134
  admin_url,
136
135
  snake_case_resource_type
@@ -138,7 +137,7 @@ module ShopifyCli
138
137
  end
139
138
 
140
139
  def admin_url
141
- "https://#{Project.current.env.shop}/admin/"
140
+ "https://#{@shop}/admin/"
142
141
  end
143
142
 
144
143
  def price
@@ -147,13 +146,6 @@ module ShopifyCli
147
146
 
148
147
  private
149
148
 
150
- def get_shop(ctx)
151
- res = ShopifyCli::Tasks::SelectOrgAndShop.call(ctx)
152
- domain = res[:shop_domain]
153
- Project.current.env.update(ctx, :shop, domain)
154
- domain
155
- end
156
-
157
149
  def camel_case_resource_type
158
150
  @camel_case_resource_type ||= self.class.to_s.split("::").last
159
151
  end
@@ -6,7 +6,7 @@ module ShopifyCli
6
6
  class << self
7
7
  def get(ctx)
8
8
  unless ShopifyCli::DB.exists?(:shopify_admin_schema)
9
- shop = Project.current.env.shop || get_shop(ctx)
9
+ shop = AdminAPI.get_shop_or_abort(ctx)
10
10
  schema = AdminAPI.query(ctx, "admin_introspection", shop: shop)
11
11
  ShopifyCli::DB.set(shopify_admin_schema: JSON.dump(schema))
12
12
  end
@@ -15,15 +15,6 @@ module ShopifyCli
15
15
  # available
16
16
  self[JSON.parse(ShopifyCli::DB.get(:shopify_admin_schema))]
17
17
  end
18
-
19
- private
20
-
21
- def get_shop(ctx)
22
- res = ShopifyCli::Tasks::SelectOrgAndShop.call(ctx)
23
- domain = res[:shop_domain]
24
- Project.current.env.update(ctx, :shop, domain)
25
- domain
26
- end
27
18
  end
28
19
 
29
20
  def type(name)
@@ -9,12 +9,22 @@ module ShopifyCli
9
9
  property :auth_header, accepts: String
10
10
  property! :url, accepts: String
11
11
 
12
- class APIRequestError < StandardError; end
12
+ class APIRequestError < StandardError
13
+ attr_reader :response
14
+
15
+ def initialize(message = nil, response: nil)
16
+ super(message)
17
+ @response = response
18
+ end
19
+ end
20
+
13
21
  class APIRequestNotFoundError < APIRequestError; end
14
22
  class APIRequestClientError < APIRequestError; end
15
23
  class APIRequestUnauthorizedError < APIRequestClientError; end
24
+ class APIRequestForbiddenError < APIRequestClientError; end
16
25
  class APIRequestUnexpectedError < APIRequestError; end
17
26
  class APIRequestRetriableError < APIRequestError; end
27
+ class APIRequestTimeoutError < APIRequestRetriableError; end
18
28
  class APIRequestServerError < APIRequestRetriableError; end
19
29
  class APIRequestThrottledError < APIRequestRetriableError; end
20
30
 
@@ -46,26 +56,33 @@ module ShopifyCli
46
56
  headers = default_headers.merge(headers)
47
57
  response = if method == "POST"
48
58
  HttpRequest.post(uri, body, headers)
59
+ elsif method == "PUT"
60
+ HttpRequest.put(uri, body, headers)
49
61
  elsif method == "GET"
50
62
  HttpRequest.get(uri, body, headers)
63
+ elsif method == "DELETE"
64
+ HttpRequest.delete(uri, body, headers)
51
65
  end
52
-
53
66
  case response.code.to_i
54
67
  when 200..399
55
- [response.code.to_i, JSON.parse(response.body)]
68
+ [response.code.to_i, JSON.parse(response.body), response]
56
69
  when 401
57
- raise APIRequestUnauthorizedError, "#{response.code}\n#{response.body}"
70
+ raise APIRequestUnauthorizedError.new("#{response.code}\n#{response.body}", response: response)
71
+ when 403
72
+ raise APIRequestForbiddenError.new("#{response.code}\n#{response.body}", response: response)
58
73
  when 404
59
- raise APIRequestNotFoundError, "#{response.code}\n#{response.body}"
74
+ raise APIRequestNotFoundError.new("#{response.code}\n#{response.body}", response: response)
60
75
  when 429
61
- raise APIRequestThrottledError, "#{response.code}\n#{response.body}"
76
+ raise APIRequestThrottledError.new("#{response.code}\n#{response.body}", response: response)
62
77
  when 400..499
63
- raise APIRequestClientError, "#{response.code}\n#{response.body}"
78
+ raise APIRequestClientError.new("#{response.code}\n#{response.body}", response: response)
64
79
  when 500..599
65
- raise APIRequestServerError, "#{response.code}\n#{response.body}"
80
+ raise APIRequestServerError.new("#{response.code}\n#{response.body}", response: response)
66
81
  else
67
- raise APIRequestUnexpectedError, "#{response.code}\n#{response.body}"
82
+ raise APIRequestUnexpectedError.new("#{response.code}\n#{response.body}", response: response)
68
83
  end
84
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ETIMEDOUT, Timeout::Error
85
+ raise APIRequestTimeoutError.new("Timeout")
69
86
  end.retry_after(APIRequestRetriableError, retries: 3) do |e|
70
87
  sleep(1) if e.is_a?(APIRequestThrottledError)
71
88
  end
@@ -87,13 +104,11 @@ module ShopifyCli
87
104
 
88
105
  private
89
106
 
90
- def current_sha
91
- @current_sha ||= Git.sha(dir: ShopifyCli::ROOT)
92
- end
93
-
94
107
  def default_headers
95
108
  {
96
- "User-Agent" => "Shopify App CLI #{ShopifyCli::VERSION} #{current_sha} | #{ctx.uname}",
109
+ "User-Agent" => "Shopify CLI; v=#{ShopifyCli::VERSION}",
110
+ "Sec-CH-UA" => "Shopify CLI; v=#{ShopifyCli::VERSION} sha=#{ShopifyCli.sha}",
111
+ "Sec-CH-UA-PLATFORM" => ctx.os,
97
112
  }.tap do |headers|
98
113
  headers["X-Shopify-Cli-Employee"] = "1" if Shopifolk.acting_as_shopify_organization?
99
114
  end.merge(auth_headers(token))
@@ -42,13 +42,16 @@ module ShopifyCli
42
42
  )
43
43
  end
44
44
 
45
- def prerequisite_task(*tasks)
45
+ def prerequisite_task(*tasks_without_args, **tasks_with_args)
46
46
  @prerequisite_tasks ||= []
47
- @prerequisite_tasks += tasks
47
+ @prerequisite_tasks += tasks_without_args.map { |t| PrerequisiteTask.new(t) }
48
+ @prerequisite_tasks += tasks_with_args.map { |t, args| PrerequisiteTask.new(t, args) }
48
49
  end
49
50
 
50
51
  def run_prerequisites
51
- (@prerequisite_tasks || []).each { |task| task_registry[task]&.call(@ctx) }
52
+ (@prerequisite_tasks || []).each do |task|
53
+ task_registry[task.name]&.call(@ctx, *task.args)
54
+ end
52
55
  end
53
56
 
54
57
  def task_registry
@@ -59,6 +62,15 @@ module ShopifyCli
59
62
  help = Commands::Help.new(@ctx)
60
63
  help.call(cmds, nil)
61
64
  end
65
+
66
+ class PrerequisiteTask
67
+ attr_reader :name, :args
68
+
69
+ def initialize(name, args = [])
70
+ @name = name
71
+ @args = args
72
+ end
73
+ end
62
74
  end
63
75
 
64
76
  def initialize(ctx = nil)
@@ -19,11 +19,16 @@ module ShopifyCli
19
19
  end
20
20
 
21
21
  register :Config, "config", "shopify-cli/commands/config", true
22
- register :Connect, "connect", "shopify-cli/commands/connect", true
23
- register :Create, "create", "shopify-cli/commands/create", true
24
22
  register :Help, "help", "shopify-cli/commands/help", true
23
+ register :Login, "login", "shopify-cli/commands/login", true
25
24
  register :Logout, "logout", "shopify-cli/commands/logout", true
25
+ register :Populate, "populate", "shopify-cli/commands/populate", true
26
+ register :Store, "store", "shopify-cli/commands/store", true
27
+ register :Switch, "switch", "shopify-cli/commands/switch", true
26
28
  register :System, "system", "shopify-cli/commands/system", true
27
29
  register :Version, "version", "shopify-cli/commands/version", true
30
+ register :Whoami, "whoami", "shopify-cli/commands/whoami", true
31
+
32
+ autoload :Connect, "shopify-cli/commands/connect"
28
33
  end
29
34
  end
@@ -23,17 +23,9 @@ module ShopifyCli
23
23
  preamble = @ctx.message("core.help.preamble", ShopifyCli::TOOL_NAME)
24
24
  @ctx.puts(preamble)
25
25
 
26
- core_commands.each do |name, klass|
27
- next if name == "help"
28
- @ctx.puts("{{command:#{name}}}: #{klass.help}\n")
29
- end
30
-
31
- return unless inside_supported_project?
26
+ available_commands = resolved_commands.select { |_name, c| !c.hidden? }
32
27
 
33
- @ctx.puts("{{bold:Project: #{Project.project_name} (#{project_type_name})}}")
34
- @ctx.puts("{{bold:Available commands for #{project_type_name} projects:}}\n\n")
35
-
36
- local_commands.each do |name, klass|
28
+ available_commands.each do |name, klass|
37
29
  next if name == "help"
38
30
  @ctx.puts("{{command:#{name}}}: #{klass.help}\n")
39
31
  end
@@ -41,21 +33,6 @@ module ShopifyCli
41
33
 
42
34
  private
43
35
 
44
- def project_type_name
45
- ProjectType.load_type(Project.current_project_type).project_name
46
- end
47
-
48
- def core_commands
49
- resolved_commands
50
- .select { |_name, c| !c.hidden? }
51
- .select { |name, _c| Commands.core_command?(name) }
52
- end
53
-
54
- def local_commands
55
- resolved_commands
56
- .reject { |name, _c| Commands.core_command?(name) }
57
- end
58
-
59
36
  def display_help(klass)
60
37
  output = klass.help
61
38
  if klass.respond_to?(:extended_help)
@@ -70,10 +47,6 @@ module ShopifyCli
70
47
  .resolved_commands
71
48
  .sort
72
49
  end
73
-
74
- def inside_supported_project?
75
- Project.current_project_type && ProjectType.load_type(Project.current_project_type)
76
- end
77
50
  end
78
51
  end
79
52
  end
@@ -0,0 +1,95 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCli
4
+ module Commands
5
+ class Login < ShopifyCli::Command
6
+ PROTOCOL_REGEX = /^https?\:\/\//
7
+ PERMANENT_DOMAIN_SUFFIX = /\.myshopify\.(com|io)$/
8
+
9
+ options do |parser, flags|
10
+ parser.on("--store=STORE") { |url| flags[:shop] = url }
11
+ # backwards compatibility allow 'shop' for now
12
+ parser.on("--shop=SHOP") { |url| flags[:shop] = url }
13
+ parser.on("--password=PASSWORD") { |password| flags[:password] = password }
14
+ end
15
+
16
+ def call(*)
17
+ shop = (options.flags[:shop] || @ctx.getenv("SHOPIFY_SHOP" || nil))
18
+ ShopifyCli::DB.set(shop: self.class.validate_shop(shop)) unless shop.nil?
19
+
20
+ if shop.nil? && Shopifolk.check
21
+ Shopifolk.reset
22
+ @ctx.puts(@ctx.message("core.tasks.select_org_and_shop.identified_as_shopify"))
23
+ message = @ctx.message("core.tasks.select_org_and_shop.first_party")
24
+ if CLI::UI::Prompt.confirm(message, default: false)
25
+ Shopifolk.act_as_shopify_organization
26
+ end
27
+ end
28
+
29
+ # As password auth will soon be deprecated, we enable only in CI
30
+ if @ctx.ci? && (password = options.flags[:password] || @ctx.getenv("SHOPIFY_PASSWORD"))
31
+ ShopifyCli::DB.set(shopify_exchange_token: password)
32
+ else
33
+ IdentityAuth.new(ctx: @ctx).authenticate
34
+ org = select_organization
35
+ ShopifyCli::DB.set(organization_id: org["id"].to_i) unless org.nil?
36
+ Whoami.call([], "whoami")
37
+ end
38
+ end
39
+
40
+ def self.help
41
+ ShopifyCli::Context.message("core.login.help", ShopifyCli::TOOL_NAME)
42
+ end
43
+
44
+ def self.validate_shop(shop)
45
+ permanent_domain = shop_to_permanent_domain(shop)
46
+ @ctx.abort(@ctx.message("core.login.invalid_shop", shop)) unless permanent_domain
47
+ permanent_domain
48
+ end
49
+
50
+ def self.shop_to_permanent_domain(shop)
51
+ url = if PROTOCOL_REGEX =~ shop
52
+ shop
53
+ elsif shop.include?(".")
54
+ "https://#{shop}"
55
+ else
56
+ "https://#{shop}.myshopify.com"
57
+ end
58
+
59
+ # Make a request to see if it exists or if we get redirected to the permanent domain one
60
+ uri = URI.parse(url)
61
+ Net::HTTP.start(uri.host, use_ssl: true) do |http|
62
+ response = http.request_head("/admin")
63
+ case response
64
+ when Net::HTTPSuccess, Net::HTTPSeeOther
65
+ uri.host
66
+ when Net::HTTPFound
67
+ domain = URI.parse(response["location"]).host
68
+ if PERMANENT_DOMAIN_SUFFIX =~ domain
69
+ domain
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def select_organization
78
+ organizations = ShopifyCli::PartnersAPI::Organizations.fetch_all(@ctx)
79
+
80
+ if organizations.count == 0
81
+ nil
82
+ elsif organizations.count == 1
83
+ organizations.first
84
+ else
85
+ org_id = CLI::UI::Prompt.ask(@ctx.message("core.tasks.select_org_and_shop.organization_select")) do |handler|
86
+ organizations.each do |o|
87
+ handler.option(@ctx.message("core.partners_api.org_name_and_id", o["businessName"], o["id"])) { o["id"] }
88
+ end
89
+ end
90
+ organizations.find { |o| o["id"] == org_id }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end