shopify-cli 1.14.0 → 2.0.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.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -1
- data/.github/CONTRIBUTING.md +7 -7
- data/.github/DESIGN.md +3 -3
- data/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +35 -29
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +4 -1
- data/README.md +92 -26
- data/RELEASING.md +29 -7
- data/Rakefile +2 -2
- data/SECURITY.md +1 -1
- data/bin/load_shopify.rb +1 -1
- data/bin/shopify +3 -3
- data/dev.yml +1 -1
- data/docs/app/node/index.md +1 -1
- data/docs/app/rails/index.md +1 -1
- data/docs/core/index.md +1 -1
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/install/index.md +1 -1
- data/docs/getting-started/migrate/index.md +1 -1
- data/docs/getting-started/uninstall/index.md +1 -1
- data/docs/getting-started/upgrade/index.md +1 -1
- data/docs/help/start-app/index.md +1 -1
- data/docs/index.md +1 -1
- data/ext/shopify-cli/extconf.rb +17 -5
- data/install.sh +1 -1
- data/lib/docgen/index_template.md.erb +2 -2
- data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
- data/lib/graphql/find_organization.graphql +2 -1
- data/lib/project_types/extension/cli.rb +18 -15
- data/lib/project_types/extension/commands/build.rb +4 -5
- data/lib/project_types/extension/commands/connect.rb +35 -0
- data/lib/project_types/extension/commands/create.rb +12 -16
- data/lib/project_types/extension/commands/extension_command.rb +2 -2
- data/lib/project_types/extension/commands/info.rb +86 -0
- data/lib/project_types/extension/commands/push.rb +8 -7
- data/lib/project_types/extension/commands/register.rb +4 -5
- data/lib/project_types/extension/commands/serve.rb +5 -8
- data/lib/project_types/extension/commands/tunnel.rb +3 -1
- data/lib/project_types/extension/errors.rb +9 -0
- data/lib/project_types/extension/extension_project.rb +5 -0
- data/lib/project_types/extension/features/argo.rb +6 -6
- data/lib/project_types/extension/features/argo_runtime.rb +22 -66
- data/lib/project_types/extension/features/argo_serve.rb +25 -18
- data/lib/project_types/extension/forms/connect.rb +42 -0
- data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
- data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
- data/lib/project_types/extension/messages/messages.rb +75 -11
- data/lib/project_types/extension/models/specification.rb +1 -0
- data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
- data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +86 -0
- data/lib/project_types/extension/models/specifications.rb +1 -0
- data/lib/project_types/extension/tasks/configure_features.rb +6 -7
- data/lib/project_types/extension/tasks/configure_options.rb +20 -0
- data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
- data/lib/project_types/node/cli.rb +9 -21
- data/lib/project_types/node/commands/connect.rb +8 -2
- data/lib/project_types/node/commands/create.rb +9 -5
- data/lib/project_types/node/commands/deploy.rb +15 -5
- data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
- data/lib/project_types/node/commands/generate.rb +4 -2
- data/lib/project_types/node/commands/open.rb +4 -2
- data/lib/project_types/node/commands/serve.rb +3 -2
- data/lib/project_types/node/commands/tunnel.rb +4 -2
- data/lib/project_types/node/messages/messages.rb +46 -89
- data/lib/project_types/rails/cli.rb +9 -21
- data/lib/project_types/rails/commands/connect.rb +8 -2
- data/lib/project_types/rails/commands/create.rb +10 -6
- data/lib/project_types/rails/commands/deploy.rb +15 -5
- data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
- data/lib/project_types/rails/commands/generate.rb +15 -5
- data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
- data/lib/project_types/rails/commands/open.rb +4 -2
- data/lib/project_types/rails/commands/serve.rb +3 -2
- data/lib/project_types/rails/commands/tunnel.rb +4 -2
- data/lib/project_types/rails/messages/messages.rb +54 -101
- data/lib/project_types/script/cli.rb +5 -7
- data/lib/project_types/script/commands/create.rb +3 -1
- data/lib/project_types/script/commands/push.rb +4 -2
- data/lib/project_types/script/messages/messages.rb +52 -45
- data/lib/project_types/script/ui/error_handler.rb +2 -2
- data/lib/project_types/theme/cli.rb +15 -27
- data/lib/project_types/theme/commands/check.rb +33 -0
- data/lib/project_types/theme/commands/delete.rb +64 -0
- data/lib/project_types/theme/commands/language_server.rb +16 -0
- data/lib/project_types/theme/commands/package.rb +55 -0
- data/lib/project_types/theme/commands/publish.rb +43 -0
- data/lib/project_types/theme/commands/pull.rb +51 -0
- data/lib/project_types/theme/commands/push.rb +58 -32
- data/lib/project_types/theme/commands/serve.rb +7 -17
- data/lib/project_types/theme/forms/confirm_store.rb +15 -0
- data/lib/project_types/theme/forms/select.rb +59 -0
- data/lib/project_types/theme/messages/messages.rb +110 -106
- data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
- data/lib/shopify-cli/admin_api.rb +53 -38
- data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
- data/lib/shopify-cli/admin_api/schema.rb +1 -10
- data/lib/shopify-cli/api.rb +29 -14
- data/lib/shopify-cli/command.rb +15 -3
- data/lib/shopify-cli/commands.rb +7 -2
- data/lib/shopify-cli/commands/help.rb +2 -29
- data/lib/shopify-cli/commands/login.rb +95 -0
- data/lib/shopify-cli/commands/logout.rb +24 -8
- data/lib/shopify-cli/commands/populate.rb +23 -0
- data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
- data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
- data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
- data/lib/shopify-cli/commands/store.rb +15 -0
- data/lib/shopify-cli/commands/switch.rb +39 -0
- data/lib/shopify-cli/commands/system.rb +12 -0
- data/lib/shopify-cli/commands/whoami.rb +28 -0
- data/lib/shopify-cli/connect.rb +32 -0
- data/lib/shopify-cli/context.rb +52 -4
- data/lib/shopify-cli/core/entry_point.rb +3 -22
- data/lib/shopify-cli/db.rb +4 -4
- data/lib/shopify-cli/http_request.rb +10 -0
- data/lib/shopify-cli/identity_auth.rb +282 -0
- data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
- data/lib/shopify-cli/messages/messages.rb +132 -39
- data/lib/shopify-cli/partners_api.rb +21 -44
- data/lib/shopify-cli/partners_api/organizations.rb +8 -0
- data/lib/shopify-cli/project_commands.rb +16 -0
- data/lib/shopify-cli/project_type.rb +0 -31
- data/lib/shopify-cli/shopifolk.rb +8 -11
- data/lib/shopify-cli/sub_command.rb +1 -0
- data/lib/shopify-cli/tasks.rb +3 -0
- data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
- data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
- data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
- data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
- data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
- data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
- data/lib/shopify-cli/theme/dev_server.rb +98 -0
- data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
- data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
- data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
- data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
- data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
- data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
- data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
- data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
- data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
- data/lib/shopify-cli/theme/development_theme.rb +69 -0
- data/lib/shopify-cli/theme/file.rb +112 -0
- data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
- data/lib/shopify-cli/theme/mime_type.rb +34 -0
- data/lib/shopify-cli/theme/syncer.rb +328 -0
- data/lib/shopify-cli/theme/theme.rb +204 -0
- data/lib/shopify-cli/version.rb +1 -1
- data/lib/shopify_cli.rb +18 -11
- data/shopify-cli.gemspec +12 -5
- data/shopify.fish +1 -1
- data/shopify.sh +1 -1
- metadata +88 -34
- data/.github/workflows/release.yml +0 -59
- data/lib/project_types/extension/features/argo_serve_options.rb +0 -42
- data/lib/project_types/node/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate/customer.rb +0 -31
- data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
- data/lib/project_types/rails/commands/populate/product.rb +0 -30
- data/lib/project_types/theme/commands/connect.rb +0 -54
- data/lib/project_types/theme/commands/create.rb +0 -48
- data/lib/project_types/theme/commands/deploy.rb +0 -38
- data/lib/project_types/theme/commands/generate.rb +0 -20
- data/lib/project_types/theme/commands/generate/env.rb +0 -79
- data/lib/project_types/theme/forms/connect.rb +0 -34
- data/lib/project_types/theme/forms/create.rb +0 -22
- data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
- data/lib/project_types/theme/themekit.rb +0 -113
- data/lib/shopify-cli/commands/connect.rb +0 -64
- data/lib/shopify-cli/commands/create.rb +0 -50
- data/lib/shopify-cli/oauth.rb +0 -198
@@ -1,6 +1,6 @@
|
|
1
1
|
require "shopify_cli"
|
2
2
|
|
3
|
-
module
|
3
|
+
module ShopifyCli
|
4
4
|
module Commands
|
5
5
|
class Populate
|
6
6
|
class Customer < ShopifyCli::AdminAPI::PopulateResourceCommand
|
@@ -17,13 +17,7 @@ module Node
|
|
17
17
|
def message(data)
|
18
18
|
ret = data["customerCreate"]["customer"]
|
19
19
|
id = ShopifyCli::API.gid_to_id(ret["id"])
|
20
|
-
@ctx.message(
|
21
|
-
"node.populate.customer.added",
|
22
|
-
ret["displayName"],
|
23
|
-
ShopifyCli::Project.current.env.shop,
|
24
|
-
admin_url,
|
25
|
-
id
|
26
|
-
)
|
20
|
+
@ctx.message("core.populate.customer.added", ret["displayName"], @shop, admin_url, id)
|
27
21
|
end
|
28
22
|
end
|
29
23
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "shopify_cli"
|
2
2
|
|
3
|
-
module
|
3
|
+
module ShopifyCli
|
4
4
|
module Commands
|
5
5
|
class Populate
|
6
6
|
class DraftOrder < ShopifyCli::AdminAPI::PopulateResourceCommand
|
@@ -20,7 +20,7 @@ module Node
|
|
20
20
|
def message(data)
|
21
21
|
ret = data["draftOrderCreate"]["draftOrder"]
|
22
22
|
id = ShopifyCli::API.gid_to_id(ret["id"])
|
23
|
-
@ctx.message("
|
23
|
+
@ctx.message("core.populate.draft_order.added", @shop, admin_url, id)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "shopify_cli"
|
2
2
|
|
3
|
-
module
|
3
|
+
module ShopifyCli
|
4
4
|
module Commands
|
5
5
|
class Populate
|
6
6
|
class Product < ShopifyCli::AdminAPI::PopulateResourceCommand
|
@@ -16,13 +16,7 @@ module Node
|
|
16
16
|
def message(data)
|
17
17
|
ret = data["productCreate"]["product"]
|
18
18
|
id = ShopifyCli::API.gid_to_id(ret["id"])
|
19
|
-
@ctx.message(
|
20
|
-
"node.populate.product.added",
|
21
|
-
ret["title"],
|
22
|
-
ShopifyCli::Project.current.env.shop,
|
23
|
-
admin_url,
|
24
|
-
id
|
25
|
-
)
|
19
|
+
@ctx.message("core.populate.product.added", ret["title"], @shop, admin_url, id)
|
26
20
|
end
|
27
21
|
end
|
28
22
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "shopify_cli"
|
2
|
+
|
3
|
+
module ShopifyCli
|
4
|
+
module Commands
|
5
|
+
class Store < ShopifyCli::Command
|
6
|
+
def call(_args, _name)
|
7
|
+
@ctx.puts(@ctx.message("core.store.shop", ShopifyCli::AdminAPI.get_shop_or_abort(@ctx)))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.help
|
11
|
+
ShopifyCli::Context.message("core.store.help", ShopifyCli::TOOL_NAME)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "shopify_cli"
|
2
|
+
|
3
|
+
module ShopifyCli
|
4
|
+
module Commands
|
5
|
+
class Switch < ShopifyCli::Command
|
6
|
+
options do |parser, flags|
|
7
|
+
parser.on("--store=STORE") { |url| flags[:shop] = url }
|
8
|
+
# backwards compatibility allow 'shop' for now
|
9
|
+
parser.on("--shop=SHOP") { |url| flags[:shop] = url }
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*)
|
13
|
+
if Shopifolk.acting_as_shopify_organization?
|
14
|
+
@ctx.puts(@ctx.message("core.switch.disabled_as_shopify_org"))
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
shop = if options.flags[:shop]
|
19
|
+
Login.validate_shop(options.flags[:shop])
|
20
|
+
elsif (org_id = DB.get(:organization_id))
|
21
|
+
res = ShopifyCli::Tasks::SelectOrgAndShop.call(@ctx, organization_id: org_id)
|
22
|
+
res[:shop_domain]
|
23
|
+
else
|
24
|
+
AdminAPI.get_shop_or_abort(@ctx)
|
25
|
+
res = ShopifyCli::Tasks::SelectOrgAndShop.call(@ctx)
|
26
|
+
res[:shop_domain]
|
27
|
+
end
|
28
|
+
DB.set(shop: shop)
|
29
|
+
IdentityAuth.new(ctx: @ctx).reauthenticate
|
30
|
+
|
31
|
+
@ctx.puts(@ctx.message("core.switch.success", shop))
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.help
|
35
|
+
ShopifyCli::Context.message("core.switch.help", ShopifyCli::TOOL_NAME)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -20,6 +20,7 @@ module ShopifyCli
|
|
20
20
|
display_environment if show_all_details
|
21
21
|
|
22
22
|
display_cli_constants(show_all_details)
|
23
|
+
display_shopify_store(show_all_details)
|
23
24
|
display_cli_ruby(show_all_details)
|
24
25
|
display_utility_commands(show_all_details)
|
25
26
|
display_project_commands(show_all_details)
|
@@ -59,6 +60,17 @@ module ShopifyCli
|
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
63
|
+
def display_shopify_store(_show_all_details)
|
64
|
+
shop = if ShopifyCli::DB.exists?(:shop)
|
65
|
+
ShopifyCli::AdminAPI.get_shop_or_abort(@ctx)
|
66
|
+
else
|
67
|
+
@ctx.message("core.populate.error.no_shop", ShopifyCli::TOOL_NAME)
|
68
|
+
end
|
69
|
+
|
70
|
+
@ctx.puts("\n" + @ctx.message("core.system.shop_header"))
|
71
|
+
@ctx.puts(" " + shop)
|
72
|
+
end
|
73
|
+
|
62
74
|
def display_cli_ruby(_show_all_details)
|
63
75
|
rbconfig_constants = %w(host RUBY_VERSION_NAME)
|
64
76
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "shopify_cli"
|
2
|
+
|
3
|
+
module ShopifyCli
|
4
|
+
module Commands
|
5
|
+
class Whoami < ShopifyCli::Command
|
6
|
+
def call(_args, _name)
|
7
|
+
shop = ShopifyCli::DB.get(:shop)
|
8
|
+
org_id = ShopifyCli::DB.get(:organization_id)
|
9
|
+
org = ShopifyCli::PartnersAPI::Organizations.fetch(@ctx, id: org_id) unless org_id.nil?
|
10
|
+
|
11
|
+
output = if shop.nil? && org.nil?
|
12
|
+
@ctx.message("core.whoami.not_logged_in", ShopifyCli::TOOL_NAME)
|
13
|
+
elsif !shop.nil? && org.nil?
|
14
|
+
@ctx.message("core.whoami.logged_in_shop_only", shop)
|
15
|
+
elsif shop.nil? && !org.nil?
|
16
|
+
@ctx.message("core.whoami.logged_in_partner_only", org["businessName"])
|
17
|
+
else
|
18
|
+
@ctx.message("core.whoami.logged_in_partner_and_shop", shop, org["businessName"])
|
19
|
+
end
|
20
|
+
@ctx.puts(output)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.help
|
24
|
+
ShopifyCli::Context.message("core.whoami.help", ShopifyCli::TOOL_NAME)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "shopify_cli"
|
2
|
+
|
3
|
+
module ShopifyCli
|
4
|
+
class Connect
|
5
|
+
def initialize(ctx)
|
6
|
+
@ctx = ctx
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_connect(project_type)
|
10
|
+
if Project.current&.env
|
11
|
+
@ctx.puts(@ctx.message("core.connect.already_connected_warning"))
|
12
|
+
end
|
13
|
+
org = ShopifyCli::Tasks::EnsureEnv.call(@ctx, regenerate: true)
|
14
|
+
write_cli_yml(project_type, org["id"]) unless Project.has_current?
|
15
|
+
api_key = Project.current(force_reload: true).env["api_key"]
|
16
|
+
get_app(org["apps"], api_key).first["title"]
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_cli_yml(project_type, org_id)
|
20
|
+
ShopifyCli::Project.write(
|
21
|
+
@ctx,
|
22
|
+
project_type: project_type,
|
23
|
+
organization_id: org_id,
|
24
|
+
)
|
25
|
+
@ctx.done(@ctx.message("core.connect.cli_yml_saved"))
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_app(apps, api_key)
|
29
|
+
apps.select { |app| app["apiKey"] == api_key }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/shopify-cli/context.rb
CHANGED
@@ -61,9 +61,10 @@ module ShopifyCli
|
|
61
61
|
# will return which operating system that the cli is running on [:mac, :linux]
|
62
62
|
def os
|
63
63
|
host = uname
|
64
|
-
return :mac if /darwin
|
65
|
-
return :
|
66
|
-
return :
|
64
|
+
return :mac if /darwin/i.match(host)
|
65
|
+
return :windows if /mswin|mingw|cygwin/i.match(host)
|
66
|
+
return :linux if /linux|bsd/i.match(host)
|
67
|
+
:unknown
|
67
68
|
end
|
68
69
|
|
69
70
|
# will return true if the cli is running on an apple computer.
|
@@ -81,6 +82,16 @@ module ShopifyCli
|
|
81
82
|
os == :windows
|
82
83
|
end
|
83
84
|
|
85
|
+
# will return true if the os is unknown
|
86
|
+
def unknown_os?
|
87
|
+
os == :unknown
|
88
|
+
end
|
89
|
+
|
90
|
+
# will return true if being launched from a tty
|
91
|
+
def tty?
|
92
|
+
$stdin.tty? && !testing?
|
93
|
+
end
|
94
|
+
|
84
95
|
# will return true if the cli is being run from an installation, and not a
|
85
96
|
# development instance. The gem installation will not have a 'test' directory.
|
86
97
|
# See `#development?` for checking for development environment.
|
@@ -106,6 +117,12 @@ module ShopifyCli
|
|
106
117
|
ENV["CI"]
|
107
118
|
end
|
108
119
|
|
120
|
+
##
|
121
|
+
# will return true if the cli is running with the DEBUG flag
|
122
|
+
def debug?
|
123
|
+
getenv("DEBUG")
|
124
|
+
end
|
125
|
+
|
109
126
|
# get a environment variable value by name.
|
110
127
|
#
|
111
128
|
# #### Parameters
|
@@ -299,6 +316,28 @@ module ShopifyCli
|
|
299
316
|
puts(help)
|
300
317
|
end
|
301
318
|
|
319
|
+
# will output to the console a link for the user to either copy/paste
|
320
|
+
# or click on.
|
321
|
+
#
|
322
|
+
# #### Parameters
|
323
|
+
# * `uri` - a http URI to open in a browser
|
324
|
+
#
|
325
|
+
def open_browser_url!(uri)
|
326
|
+
if tty?
|
327
|
+
if linux? && which("xdg-open")
|
328
|
+
system("xdg-open", uri.to_s)
|
329
|
+
elsif windows?
|
330
|
+
system("start", uri.to_s)
|
331
|
+
elsif mac?
|
332
|
+
system("open", uri.to_s)
|
333
|
+
else
|
334
|
+
open_url!(uri)
|
335
|
+
end
|
336
|
+
else
|
337
|
+
open_url!(uri)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
302
341
|
# will output a message, prefixed by a yellow star, indicating that task
|
303
342
|
# started.
|
304
343
|
#
|
@@ -318,6 +357,15 @@ module ShopifyCli
|
|
318
357
|
Kernel.puts(CLI::UI.fmt(*args))
|
319
358
|
end
|
320
359
|
|
360
|
+
# a wrapper around Kernel.warn to allow for easy formatting
|
361
|
+
#
|
362
|
+
# #### Parameters
|
363
|
+
# * `text` - a string message to output
|
364
|
+
#
|
365
|
+
def warn(*args)
|
366
|
+
Kernel.warn(CLI::UI.fmt(*args))
|
367
|
+
end
|
368
|
+
|
321
369
|
# outputs a message, prefixed by a checkmark indicating that something completed
|
322
370
|
#
|
323
371
|
# #### Parameters
|
@@ -344,7 +392,7 @@ module ShopifyCli
|
|
344
392
|
# * `text` - a string message to output
|
345
393
|
#
|
346
394
|
def debug(text)
|
347
|
-
puts("{{red:DEBUG}} #{text}") if
|
395
|
+
puts("{{red:DEBUG}} #{text}") if debug?
|
348
396
|
end
|
349
397
|
|
350
398
|
# proxy call to Context.message.
|
@@ -5,35 +5,16 @@ module ShopifyCli
|
|
5
5
|
module EntryPoint
|
6
6
|
class << self
|
7
7
|
def call(args, ctx = Context.new)
|
8
|
-
# Check if the shim is set up by checking whether the old Finalizer FD exists
|
9
|
-
begin
|
10
|
-
is_shell_shim = false
|
11
|
-
IO.open(9) { is_shell_shim = true }
|
12
|
-
rescue Errno::EBADF
|
13
|
-
# This is expected if the descriptor doesn't exist
|
14
|
-
rescue ArgumentError => e
|
15
|
-
# This can happen on RVM, because it can use fd 9 itself and block access to it. That only happens if the fd
|
16
|
-
# did not exist beforehand, so that means there was no fd 9 before Ruby started.
|
17
|
-
unless e.message == "The given fd is not accessible because RubyVM reserves it"
|
18
|
-
raise e
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
if !ctx.testing? && is_shell_shim
|
23
|
-
ctx.puts(ctx.message("core.warning.shell_shim"))
|
24
|
-
return
|
25
|
-
end
|
26
|
-
|
27
8
|
if ctx.development?
|
28
|
-
ctx.
|
9
|
+
ctx.warn(
|
29
10
|
ctx.message("core.warning.development_version", File.join(ShopifyCli::ROOT, "bin", ShopifyCli::TOOL_NAME))
|
30
11
|
)
|
31
12
|
elsif !ctx.testing?
|
32
13
|
new_version = ctx.new_version
|
33
|
-
ctx.
|
14
|
+
ctx.warn(ctx.message("core.warning.new_version", ShopifyCli::VERSION, new_version)) unless new_version.nil?
|
34
15
|
end
|
35
16
|
|
36
|
-
ProjectType.
|
17
|
+
ProjectType.load_all
|
37
18
|
|
38
19
|
task_registry = ShopifyCli::Tasks::Registry
|
39
20
|
|
data/lib/shopify-cli/db.rb
CHANGED
@@ -42,7 +42,7 @@ module ShopifyCli
|
|
42
42
|
#
|
43
43
|
# #### Usage
|
44
44
|
#
|
45
|
-
# exists = ShopifyCli::DB.exists?('
|
45
|
+
# exists = ShopifyCli::DB.exists?('shopify_exchange_token')
|
46
46
|
#
|
47
47
|
def exists?(key)
|
48
48
|
db.transaction(true) { db.root?(key) }
|
@@ -55,7 +55,7 @@ module ShopifyCli
|
|
55
55
|
#
|
56
56
|
# #### Usage
|
57
57
|
#
|
58
|
-
# ShopifyCli::DB.set(
|
58
|
+
# ShopifyCli::DB.set(shopify_exchange_token: 'token', metric_consent: true)
|
59
59
|
#
|
60
60
|
def set(**args)
|
61
61
|
db.transaction do
|
@@ -80,7 +80,7 @@ module ShopifyCli
|
|
80
80
|
#
|
81
81
|
# #### Usage
|
82
82
|
#
|
83
|
-
# ShopifyCli::DB.get(:
|
83
|
+
# ShopifyCli::DB.get(:shopify_exchange_token)
|
84
84
|
#
|
85
85
|
def get(key)
|
86
86
|
val = db.transaction(true) { db[key] }
|
@@ -95,7 +95,7 @@ module ShopifyCli
|
|
95
95
|
#
|
96
96
|
# #### Usage
|
97
97
|
#
|
98
|
-
# ShopifyCli::DB.del(:
|
98
|
+
# ShopifyCli::DB.del(:shopify_exchange_token)
|
99
99
|
#
|
100
100
|
def del(*args)
|
101
101
|
db.transaction { args.each { |key| db.delete(key) } }
|
@@ -8,11 +8,21 @@ module ShopifyCli
|
|
8
8
|
request(uri, body, headers, req)
|
9
9
|
end
|
10
10
|
|
11
|
+
def put(uri, body, headers)
|
12
|
+
req = ::Net::HTTP::Put.new(uri.request_uri)
|
13
|
+
request(uri, body, headers, req)
|
14
|
+
end
|
15
|
+
|
11
16
|
def get(uri, body, headers)
|
12
17
|
req = ::Net::HTTP::Get.new(uri.request_uri)
|
13
18
|
request(uri, body, headers, req)
|
14
19
|
end
|
15
20
|
|
21
|
+
def delete(uri, body, headers)
|
22
|
+
req = ::Net::HTTP::Delete.new(uri.request_uri)
|
23
|
+
request(uri, body, headers, req)
|
24
|
+
end
|
25
|
+
|
16
26
|
def request(uri, body, headers, req)
|
17
27
|
http = ::Net::HTTP.new(uri.host, uri.port)
|
18
28
|
http.use_ssl = true
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest"
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "securerandom"
|
6
|
+
require "openssl"
|
7
|
+
require "shopify_cli"
|
8
|
+
require "uri"
|
9
|
+
require "webrick"
|
10
|
+
|
11
|
+
module ShopifyCli
|
12
|
+
class IdentityAuth
|
13
|
+
include SmartProperties
|
14
|
+
|
15
|
+
autoload :Servlet, "shopify-cli/identity_auth/servlet"
|
16
|
+
|
17
|
+
class Error < StandardError; end
|
18
|
+
class Timeout < StandardError; end
|
19
|
+
LocalRequest = Struct.new(:method, :path, :query, :protocol)
|
20
|
+
LOCAL_DEBUG = "SHOPIFY_APP_CLI_LOCAL_PARTNERS"
|
21
|
+
|
22
|
+
DEFAULT_PORT = 3456
|
23
|
+
REDIRECT_HOST = "http://127.0.0.1:#{DEFAULT_PORT}"
|
24
|
+
|
25
|
+
APPLICATION_SCOPES = {
|
26
|
+
"shopify" => %w[https://api.shopify.com/auth/shop.admin.graphql https://api.shopify.com/auth/shop.admin.themes https://api.shopify.com/auth/partners.collaborator-relationships.readonly],
|
27
|
+
"storefront_renderer_production" => %w[https://api.shopify.com/auth/shop.storefront-renderer.devtools],
|
28
|
+
"partners" => %w[https://api.shopify.com/auth/partners.app.cli.access],
|
29
|
+
}
|
30
|
+
|
31
|
+
APPLICATION_CLIENT_IDS = {
|
32
|
+
"shopify" => "7ee65a63608843c577db8b23c4d7316ea0a01bd2f7594f8a9c06ea668c1b775c",
|
33
|
+
"storefront_renderer_production" => "ee139b3d-5861-4d45-b387-1bc3ada7811c",
|
34
|
+
"partners" => "271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6",
|
35
|
+
}
|
36
|
+
|
37
|
+
DEV_APPLICATION_CLIENT_IDS = {
|
38
|
+
"shopify" => "e92482cebb9bfb9fb5a0199cc770fde3de6c8d16b798ee73e36c9d815e070e52",
|
39
|
+
"storefront_renderer_production" => "46f603de-894f-488d-9471-5b721280ff49",
|
40
|
+
"partners" => "df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978",
|
41
|
+
}
|
42
|
+
|
43
|
+
EXCHANGE_TOKENS = APPLICATION_SCOPES.keys.map do |key|
|
44
|
+
"#{key}_exchange_token".to_sym
|
45
|
+
end
|
46
|
+
|
47
|
+
IDENTITY_ACCESS_TOKENS = %i[
|
48
|
+
identity_access_token
|
49
|
+
identity_refresh_token
|
50
|
+
]
|
51
|
+
|
52
|
+
property! :ctx
|
53
|
+
property :store, default: -> { ShopifyCli::DB.new }
|
54
|
+
property :state_token, accepts: String, default: SecureRandom.hex(30)
|
55
|
+
property :code_verifier, accepts: String, default: SecureRandom.hex(30)
|
56
|
+
|
57
|
+
attr_accessor :response_query
|
58
|
+
|
59
|
+
def authenticate
|
60
|
+
return if refresh_exchange_tokens || refresh_access_tokens
|
61
|
+
|
62
|
+
initiate_authentication
|
63
|
+
|
64
|
+
begin
|
65
|
+
request_access_token(code: receive_access_code)
|
66
|
+
rescue IdentityAuth::Timeout => e
|
67
|
+
ctx.abort(e.message)
|
68
|
+
end
|
69
|
+
request_exchange_tokens
|
70
|
+
end
|
71
|
+
|
72
|
+
def reauthenticate
|
73
|
+
return if refresh_exchange_tokens || refresh_access_tokens
|
74
|
+
ctx.abort(ctx.message("core.identity_auth.error.reauthenticate", ShopifyCli::TOOL_NAME))
|
75
|
+
end
|
76
|
+
|
77
|
+
def code_challenge
|
78
|
+
@code_challenge ||= Base64.urlsafe_encode64(
|
79
|
+
OpenSSL::Digest::SHA256.digest(code_verifier),
|
80
|
+
padding: false,
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def server
|
85
|
+
@server ||= begin
|
86
|
+
server = WEBrick::HTTPServer.new(
|
87
|
+
Port: DEFAULT_PORT,
|
88
|
+
Logger: WEBrick::Log.new(File.open(File::NULL, "w")),
|
89
|
+
AccessLog: [],
|
90
|
+
)
|
91
|
+
server.mount("/", Servlet, self, state_token)
|
92
|
+
server
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.delete_tokens_and_keys
|
97
|
+
ShopifyCli::DB.del(*IDENTITY_ACCESS_TOKENS)
|
98
|
+
ShopifyCli::DB.del(*EXCHANGE_TOKENS)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def initiate_authentication
|
104
|
+
@server_thread = Thread.new { server.start }
|
105
|
+
params = {
|
106
|
+
client_id: client_id,
|
107
|
+
scope: scopes(APPLICATION_SCOPES.values.flatten),
|
108
|
+
redirect_uri: REDIRECT_HOST,
|
109
|
+
state: state_token,
|
110
|
+
response_type: :code,
|
111
|
+
}
|
112
|
+
params.merge!(challange_params)
|
113
|
+
uri = URI.parse("#{auth_url}/authorize")
|
114
|
+
uri.query = URI.encode_www_form(params)
|
115
|
+
open_browser_authentication(uri)
|
116
|
+
end
|
117
|
+
|
118
|
+
def open_browser_authentication(uri)
|
119
|
+
ctx.open_browser_url!(uri)
|
120
|
+
end
|
121
|
+
|
122
|
+
def receive_access_code
|
123
|
+
@access_code ||= begin
|
124
|
+
@server_thread.join(240)
|
125
|
+
raise Timeout, ctx.message("core.identity_auth.error.timeout") if response_query.nil?
|
126
|
+
raise Error, response_query["error_description"] unless response_query["error"].nil?
|
127
|
+
response_query["code"]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def request_access_token(code:)
|
132
|
+
resp = post_token_request(
|
133
|
+
grant_type: :authorization_code,
|
134
|
+
code: code,
|
135
|
+
redirect_uri: REDIRECT_HOST,
|
136
|
+
client_id: client_id,
|
137
|
+
code_verifier: code_verifier,
|
138
|
+
)
|
139
|
+
store.set(
|
140
|
+
identity_access_token: resp["access_token"],
|
141
|
+
identity_refresh_token: resp["refresh_token"],
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
def refresh_access_tokens
|
146
|
+
return false unless IDENTITY_ACCESS_TOKENS.all? { |key| store.exists?(key) }
|
147
|
+
|
148
|
+
resp = post_token_request(
|
149
|
+
grant_type: :refresh_token,
|
150
|
+
access_token: store.get(:identity_access_token),
|
151
|
+
refresh_token: store.get(:identity_refresh_token),
|
152
|
+
client_id: client_id,
|
153
|
+
)
|
154
|
+
store.set(
|
155
|
+
identity_access_token: resp["access_token"],
|
156
|
+
identity_refresh_token: resp["refresh_token"],
|
157
|
+
)
|
158
|
+
|
159
|
+
# Need to refresh the exchange token on successful access token refresh
|
160
|
+
request_exchange_tokens
|
161
|
+
|
162
|
+
true
|
163
|
+
rescue
|
164
|
+
store.del(*IDENTITY_ACCESS_TOKENS)
|
165
|
+
false
|
166
|
+
end
|
167
|
+
|
168
|
+
def refresh_exchange_tokens
|
169
|
+
return false unless EXCHANGE_TOKENS.all? { |key| store.exists?(key) }
|
170
|
+
|
171
|
+
request_exchange_tokens
|
172
|
+
|
173
|
+
true
|
174
|
+
rescue
|
175
|
+
store.del(*EXCHANGE_TOKENS)
|
176
|
+
false
|
177
|
+
end
|
178
|
+
|
179
|
+
def request_exchange_tokens
|
180
|
+
APPLICATION_SCOPES.each do |key, scopes|
|
181
|
+
request_exchange_token(key, client_id_for_application(key), scopes)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def request_exchange_token(name, audience, additional_scopes)
|
186
|
+
return if name == "shopify" && !store.exists?(:shop)
|
187
|
+
|
188
|
+
params = {
|
189
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
190
|
+
requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
191
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
192
|
+
client_id: client_id,
|
193
|
+
audience: audience,
|
194
|
+
scope: scopes(additional_scopes),
|
195
|
+
subject_token: store.get(:identity_access_token),
|
196
|
+
}.tap do |result|
|
197
|
+
if name == "shopify"
|
198
|
+
result[:destination] = "https://#{store.get(:shop)}/admin"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# ctx.debug(params)
|
202
|
+
resp = post_token_request(params)
|
203
|
+
store.set("#{name}_exchange_token".to_sym => resp["access_token"])
|
204
|
+
ctx.debug("#{name}_exchange_token: " + resp["access_token"])
|
205
|
+
end
|
206
|
+
|
207
|
+
def post_token_request(params)
|
208
|
+
post_request("/token", params)
|
209
|
+
end
|
210
|
+
|
211
|
+
def post_request(endpoint, params)
|
212
|
+
uri = URI.parse("#{auth_url}#{endpoint}")
|
213
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
214
|
+
https.use_ssl = true
|
215
|
+
request = Net::HTTP::Post.new(uri.path)
|
216
|
+
request["User-Agent"] = "Shopify CLI #{::ShopifyCli::VERSION}"
|
217
|
+
request.body = URI.encode_www_form(params)
|
218
|
+
res = https.request(request)
|
219
|
+
unless res.is_a?(Net::HTTPSuccess)
|
220
|
+
error_msg = JSON.parse(res.body)["error_description"]
|
221
|
+
shop = store.get(:shop)
|
222
|
+
if error_msg.include?("destination")
|
223
|
+
store.del(:shop)
|
224
|
+
ctx.abort(ctx.message("core.identity_auth.error.invalid_destination", shop))
|
225
|
+
end
|
226
|
+
raise Error, error_msg
|
227
|
+
end
|
228
|
+
JSON.parse(res.body)
|
229
|
+
end
|
230
|
+
|
231
|
+
def challange_params
|
232
|
+
{
|
233
|
+
code_challenge: code_challenge,
|
234
|
+
code_challenge_method: "S256",
|
235
|
+
}
|
236
|
+
end
|
237
|
+
|
238
|
+
def auth_url
|
239
|
+
return "https://accounts.shopify.com/oauth" if ENV[LOCAL_DEBUG].nil?
|
240
|
+
"https://identity.myshopify.io/oauth"
|
241
|
+
end
|
242
|
+
|
243
|
+
def client_id_for_application(application_name)
|
244
|
+
client_ids = if ENV[LOCAL_DEBUG]
|
245
|
+
DEV_APPLICATION_CLIENT_IDS
|
246
|
+
else
|
247
|
+
APPLICATION_CLIENT_IDS
|
248
|
+
end
|
249
|
+
|
250
|
+
client_ids[application_name]
|
251
|
+
end
|
252
|
+
|
253
|
+
def scopes(additional_scopes = [])
|
254
|
+
(["openid"] + additional_scopes).tap do |result|
|
255
|
+
result << "employee" if ShopifyCli::Shopifolk.acting_as_shopify_organization?
|
256
|
+
end.join(" ")
|
257
|
+
end
|
258
|
+
|
259
|
+
def client_id
|
260
|
+
return "fbdb2649-e327-4907-8f67-908d24cfd7e3" if ENV[LOCAL_DEBUG].nil?
|
261
|
+
|
262
|
+
ctx.abort(ctx.message("core.identity_auth.error.local_identity_not_running")) unless local_identity_running?
|
263
|
+
|
264
|
+
# Fetch the client ID from the local Identity Dynamic Registration endpoint
|
265
|
+
response = post_request("/client", {
|
266
|
+
name: "shopify-cli-development",
|
267
|
+
public_type: "native",
|
268
|
+
})
|
269
|
+
|
270
|
+
response["client_id"]
|
271
|
+
end
|
272
|
+
|
273
|
+
def local_identity_running?
|
274
|
+
Net::HTTP.start("identity.myshopify.io", 443, use_ssl: true, open_timeout: 1, read_timeout: 10) do |http|
|
275
|
+
req = Net::HTTP::Get.new(URI.join("https://identity.myshopify.io", "/services/ping"))
|
276
|
+
http.request(req).is_a?(Net::HTTPSuccess)
|
277
|
+
end
|
278
|
+
rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::EHOSTDOWN, Errno::EADDRNOTAVAIL, Errno::ECONNREFUSED
|
279
|
+
false
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|