shopify-cli 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (273) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/CODE_OF_CONDUCT.md +73 -0
  4. data/.github/CONTRIBUTING.md +51 -0
  5. data/.github/DESIGN.md +153 -0
  6. data/.github/ISSUE_TEMPLATE.md +38 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  8. data/.github/probots.yml +3 -0
  9. data/.gitignore +19 -0
  10. data/.rubocop.yml +47 -0
  11. data/.ruby-version +1 -0
  12. data/.travis.yml +12 -0
  13. data/Gemfile +22 -0
  14. data/Gemfile.lock +77 -0
  15. data/LICENSE.md +7 -0
  16. data/README.md +13 -0
  17. data/Rakefile +101 -0
  18. data/SECURITY.md +59 -0
  19. data/Vagrantfile +17 -0
  20. data/bin/load_shopify.rb +20 -0
  21. data/bin/shopify +32 -0
  22. data/dev.yml +17 -0
  23. data/docs/Gemfile +5 -0
  24. data/docs/Gemfile.lock +248 -0
  25. data/docs/_config.yml +16 -0
  26. data/docs/_data/nav.yml +26 -0
  27. data/docs/_includes/footer.html +15 -0
  28. data/docs/_includes/head.html +19 -0
  29. data/docs/_includes/sidebar_nav.html +22 -0
  30. data/docs/_includes/toc.html +112 -0
  31. data/docs/_layouts/default.html +79 -0
  32. data/docs/app/node/commands/index.md +82 -0
  33. data/docs/app/node/index.md +35 -0
  34. data/docs/app/rails/commands/index.md +80 -0
  35. data/docs/app/rails/index.md +36 -0
  36. data/docs/core/index.md +70 -0
  37. data/docs/css/docs.css +157 -0
  38. data/docs/getting-started/index.md +61 -0
  39. data/docs/help/start-app/index.md +6 -0
  40. data/docs/images/header.png +0 -0
  41. data/docs/index.md +27 -0
  42. data/docs/installing-ruby.md +28 -0
  43. data/ext/shopify-cli/extconf.rb +27 -0
  44. data/install.sh +7 -0
  45. data/lib/docgen/class_template.md.erb +81 -0
  46. data/lib/docgen/index_template.md.erb +5 -0
  47. data/lib/docgen/markdown.rb +101 -0
  48. data/lib/graphql/admin_introspection.graphql +87 -0
  49. data/lib/graphql/all_organizations.graphql +19 -0
  50. data/lib/graphql/all_orgs_with_apps.graphql +30 -0
  51. data/lib/graphql/api_versions.graphql +6 -0
  52. data/lib/graphql/convert_dev_to_test_store.graphql +10 -0
  53. data/lib/graphql/create_app.graphql +20 -0
  54. data/lib/graphql/create_customer.graphql +9 -0
  55. data/lib/graphql/create_draft_order.graphql +8 -0
  56. data/lib/graphql/create_product.graphql +9 -0
  57. data/lib/graphql/extension_create.graphql +21 -0
  58. data/lib/graphql/extension_update_draft.graphql +18 -0
  59. data/lib/graphql/find_organization.graphql +17 -0
  60. data/lib/graphql/get_app_urls.graphql +6 -0
  61. data/lib/graphql/update_dashboard_urls.graphql +8 -0
  62. data/lib/project_types/extension/cli.rb +71 -0
  63. data/lib/project_types/extension/commands/build.rb +29 -0
  64. data/lib/project_types/extension/commands/create.rb +49 -0
  65. data/lib/project_types/extension/commands/extension_command.rb +22 -0
  66. data/lib/project_types/extension/commands/push.rb +69 -0
  67. data/lib/project_types/extension/commands/register.rb +78 -0
  68. data/lib/project_types/extension/commands/serve.rb +24 -0
  69. data/lib/project_types/extension/commands/tunnel.rb +69 -0
  70. data/lib/project_types/extension/extension_project.rb +85 -0
  71. data/lib/project_types/extension/extension_project_keys.rb +10 -0
  72. data/lib/project_types/extension/features/argo.rb +48 -0
  73. data/lib/project_types/extension/features/argo_dependencies.rb +28 -0
  74. data/lib/project_types/extension/features/argo_setup.rb +54 -0
  75. data/lib/project_types/extension/features/argo_setup_step.rb +31 -0
  76. data/lib/project_types/extension/features/argo_setup_steps.rb +53 -0
  77. data/lib/project_types/extension/features/tunnel_url.rb +20 -0
  78. data/lib/project_types/extension/forms/create.rb +52 -0
  79. data/lib/project_types/extension/forms/register.rb +48 -0
  80. data/lib/project_types/extension/messages/message_loading.rb +37 -0
  81. data/lib/project_types/extension/messages/messages.rb +126 -0
  82. data/lib/project_types/extension/models/app.rb +14 -0
  83. data/lib/project_types/extension/models/registration.rb +19 -0
  84. data/lib/project_types/extension/models/type.rb +76 -0
  85. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +20 -0
  86. data/lib/project_types/extension/models/types/subscription_management.rb +20 -0
  87. data/lib/project_types/extension/models/validation_error.rb +17 -0
  88. data/lib/project_types/extension/models/version.rb +15 -0
  89. data/lib/project_types/extension/tasks/converters/registration_converter.rb +26 -0
  90. data/lib/project_types/extension/tasks/converters/validation_error_converter.rb +25 -0
  91. data/lib/project_types/extension/tasks/converters/version_converter.rb +28 -0
  92. data/lib/project_types/extension/tasks/create_extension.rb +31 -0
  93. data/lib/project_types/extension/tasks/get_apps.rb +34 -0
  94. data/lib/project_types/extension/tasks/update_draft.rb +29 -0
  95. data/lib/project_types/extension/tasks/user_errors.rb +45 -0
  96. data/lib/project_types/node/cli.rb +37 -0
  97. data/lib/project_types/node/commands/create.rb +117 -0
  98. data/lib/project_types/node/commands/deploy.rb +22 -0
  99. data/lib/project_types/node/commands/deploy/heroku.rb +91 -0
  100. data/lib/project_types/node/commands/generate.rb +51 -0
  101. data/lib/project_types/node/commands/generate/billing.rb +37 -0
  102. data/lib/project_types/node/commands/generate/page.rb +55 -0
  103. data/lib/project_types/node/commands/generate/webhook.rb +33 -0
  104. data/lib/project_types/node/commands/open.rb +16 -0
  105. data/lib/project_types/node/commands/populate.rb +23 -0
  106. data/lib/project_types/node/commands/populate/customer.rb +31 -0
  107. data/lib/project_types/node/commands/populate/draft_order.rb +28 -0
  108. data/lib/project_types/node/commands/populate/product.rb +30 -0
  109. data/lib/project_types/node/commands/serve.rb +45 -0
  110. data/lib/project_types/node/commands/tunnel.rb +39 -0
  111. data/lib/project_types/node/forms/create.rb +87 -0
  112. data/lib/project_types/node/messages/messages.rb +260 -0
  113. data/lib/project_types/rails/cli.rb +41 -0
  114. data/lib/project_types/rails/commands/create.rb +126 -0
  115. data/lib/project_types/rails/commands/deploy.rb +22 -0
  116. data/lib/project_types/rails/commands/deploy/heroku.rb +113 -0
  117. data/lib/project_types/rails/commands/generate.rb +49 -0
  118. data/lib/project_types/rails/commands/generate/webhook.rb +39 -0
  119. data/lib/project_types/rails/commands/open.rb +16 -0
  120. data/lib/project_types/rails/commands/populate.rb +23 -0
  121. data/lib/project_types/rails/commands/populate/customer.rb +31 -0
  122. data/lib/project_types/rails/commands/populate/draft_order.rb +28 -0
  123. data/lib/project_types/rails/commands/populate/product.rb +30 -0
  124. data/lib/project_types/rails/commands/serve.rb +47 -0
  125. data/lib/project_types/rails/commands/tunnel.rb +39 -0
  126. data/lib/project_types/rails/forms/create.rb +116 -0
  127. data/lib/project_types/rails/gem.rb +56 -0
  128. data/lib/project_types/rails/messages/messages.rb +283 -0
  129. data/lib/project_types/rails/ruby.rb +17 -0
  130. data/lib/project_types/script/cli.rb +76 -0
  131. data/lib/project_types/script/commands/create.rb +45 -0
  132. data/lib/project_types/script/commands/disable.rb +36 -0
  133. data/lib/project_types/script/commands/enable.rb +46 -0
  134. data/lib/project_types/script/commands/push.rb +39 -0
  135. data/lib/project_types/script/config/extension_points.yml +18 -0
  136. data/lib/project_types/script/errors.rb +16 -0
  137. data/lib/project_types/script/forms/create.rb +29 -0
  138. data/lib/project_types/script/forms/enable.rb +24 -0
  139. data/lib/project_types/script/forms/push.rb +19 -0
  140. data/lib/project_types/script/forms/script_form.rb +66 -0
  141. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +27 -0
  142. data/lib/project_types/script/graphql/script_service_proxy.graphql +8 -0
  143. data/lib/project_types/script/graphql/shop_script_delete.graphql +14 -0
  144. data/lib/project_types/script/graphql/shop_script_update_or_create.graphql +28 -0
  145. data/lib/project_types/script/layers/application/build_script.rb +43 -0
  146. data/lib/project_types/script/layers/application/create_script.rb +47 -0
  147. data/lib/project_types/script/layers/application/disable_script.rb +19 -0
  148. data/lib/project_types/script/layers/application/enable_script.rb +21 -0
  149. data/lib/project_types/script/layers/application/extension_points.rb +17 -0
  150. data/lib/project_types/script/layers/application/project_dependencies.rb +34 -0
  151. data/lib/project_types/script/layers/application/push_script.rb +30 -0
  152. data/lib/project_types/script/layers/domain/errors.rb +25 -0
  153. data/lib/project_types/script/layers/domain/extension_point.rb +29 -0
  154. data/lib/project_types/script/layers/domain/push_package.rb +29 -0
  155. data/lib/project_types/script/layers/domain/script.rb +18 -0
  156. data/lib/project_types/script/layers/infrastructure/assemblyscript_dependency_manager.rb +73 -0
  157. data/lib/project_types/script/layers/infrastructure/assemblyscript_tsconfig.rb +38 -0
  158. data/lib/project_types/script/layers/infrastructure/assemblyscript_wasm_builder.rb +39 -0
  159. data/lib/project_types/script/layers/infrastructure/dependency_manager.rb +36 -0
  160. data/lib/project_types/script/layers/infrastructure/errors.rb +38 -0
  161. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +31 -0
  162. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +47 -0
  163. data/lib/project_types/script/layers/infrastructure/script_builder.rb +34 -0
  164. data/lib/project_types/script/layers/infrastructure/script_repository.rb +89 -0
  165. data/lib/project_types/script/layers/infrastructure/script_service.rb +165 -0
  166. data/lib/project_types/script/layers/infrastructure/test_suite_repository.rb +59 -0
  167. data/lib/project_types/script/messages/messages.rb +204 -0
  168. data/lib/project_types/script/script_project.rb +37 -0
  169. data/lib/project_types/script/templates/ts/as-pect.config.js +21 -0
  170. data/lib/project_types/script/ui/error_handler.rb +136 -0
  171. data/lib/project_types/script/ui/strict_spinner.rb +22 -0
  172. data/lib/rubygems_plugin.rb +18 -0
  173. data/lib/shopify-cli/admin_api.rb +99 -0
  174. data/lib/shopify-cli/admin_api/populate_resource_command.rb +165 -0
  175. data/lib/shopify-cli/admin_api/schema.rb +32 -0
  176. data/lib/shopify-cli/api.rb +104 -0
  177. data/lib/shopify-cli/command.rb +67 -0
  178. data/lib/shopify-cli/commands.rb +28 -0
  179. data/lib/shopify-cli/commands/connect.rb +108 -0
  180. data/lib/shopify-cli/commands/create.rb +50 -0
  181. data/lib/shopify-cli/commands/help.rb +79 -0
  182. data/lib/shopify-cli/commands/logout.rb +23 -0
  183. data/lib/shopify-cli/commands/system.rb +135 -0
  184. data/lib/shopify-cli/commands/version.rb +15 -0
  185. data/lib/shopify-cli/context.rb +372 -0
  186. data/lib/shopify-cli/core.rb +9 -0
  187. data/lib/shopify-cli/core/entry_point.rb +40 -0
  188. data/lib/shopify-cli/core/executor.rb +21 -0
  189. data/lib/shopify-cli/core/help_resolver.rb +20 -0
  190. data/lib/shopify-cli/core/monorail.rb +118 -0
  191. data/lib/shopify-cli/db.rb +114 -0
  192. data/lib/shopify-cli/form.rb +40 -0
  193. data/lib/shopify-cli/git.rb +141 -0
  194. data/lib/shopify-cli/helpers.rb +5 -0
  195. data/lib/shopify-cli/helpers/haikunator.rb +92 -0
  196. data/lib/shopify-cli/heroku.rb +97 -0
  197. data/lib/shopify-cli/js_deps.rb +110 -0
  198. data/lib/shopify-cli/js_system.rb +98 -0
  199. data/lib/shopify-cli/messages/messages.rb +287 -0
  200. data/lib/shopify-cli/oauth.rb +192 -0
  201. data/lib/shopify-cli/oauth/servlet.rb +61 -0
  202. data/lib/shopify-cli/options.rb +40 -0
  203. data/lib/shopify-cli/packager.rb +116 -0
  204. data/lib/shopify-cli/partners_api.rb +114 -0
  205. data/lib/shopify-cli/partners_api/organizations.rb +32 -0
  206. data/lib/shopify-cli/process_supervision.rb +187 -0
  207. data/lib/shopify-cli/project.rb +191 -0
  208. data/lib/shopify-cli/project_type.rb +83 -0
  209. data/lib/shopify-cli/resources.rb +5 -0
  210. data/lib/shopify-cli/resources/env_file.rb +96 -0
  211. data/lib/shopify-cli/sub_command.rb +15 -0
  212. data/lib/shopify-cli/task.rb +10 -0
  213. data/lib/shopify-cli/tasks.rb +32 -0
  214. data/lib/shopify-cli/tasks/create_api_client.rb +29 -0
  215. data/lib/shopify-cli/tasks/ensure_dev_store.rb +41 -0
  216. data/lib/shopify-cli/tasks/ensure_env.rb +31 -0
  217. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +20 -0
  218. data/lib/shopify-cli/tasks/update_dashboard_urls.rb +44 -0
  219. data/lib/shopify-cli/tunnel.rb +154 -0
  220. data/lib/shopify-cli/version.rb +3 -0
  221. data/lib/shopify_cli.rb +132 -0
  222. data/shopify-cli.gemspec +40 -0
  223. data/shopify.fish +12 -0
  224. data/shopify.sh +11 -0
  225. data/vendor/deps/cli-kit/REVISION +1 -0
  226. data/vendor/deps/cli-kit/lib/cli/kit.rb +60 -0
  227. data/vendor/deps/cli-kit/lib/cli/kit/autocall.rb +21 -0
  228. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +49 -0
  229. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +94 -0
  230. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +133 -0
  231. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +115 -0
  232. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +81 -0
  233. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +102 -0
  234. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +82 -0
  235. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +76 -0
  236. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +60 -0
  237. data/vendor/deps/cli-kit/lib/cli/kit/ruby_backports/enumerable.rb +6 -0
  238. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +9 -0
  239. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +244 -0
  240. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +207 -0
  241. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +189 -0
  242. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +5 -0
  243. data/vendor/deps/cli-ui/REVISION +1 -0
  244. data/vendor/deps/cli-ui/lib/cli/ui.rb +187 -0
  245. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +153 -0
  246. data/vendor/deps/cli-ui/lib/cli/ui/box.rb +15 -0
  247. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +79 -0
  248. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +179 -0
  249. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +310 -0
  250. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +78 -0
  251. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +88 -0
  252. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +248 -0
  253. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +472 -0
  254. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +24 -0
  255. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +48 -0
  256. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  257. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +241 -0
  258. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +227 -0
  259. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +36 -0
  260. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +102 -0
  261. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +5 -0
  262. data/vendor/deps/smart_properties/REVISION +1 -0
  263. data/vendor/deps/smart_properties/lib/smart_properties.rb +174 -0
  264. data/vendor/deps/smart_properties/lib/smart_properties/errors.rb +114 -0
  265. data/vendor/deps/smart_properties/lib/smart_properties/property.rb +162 -0
  266. data/vendor/deps/smart_properties/lib/smart_properties/property_collection.rb +83 -0
  267. data/vendor/deps/smart_properties/lib/smart_properties/validations.rb +8 -0
  268. data/vendor/deps/smart_properties/lib/smart_properties/validations/ancestor.rb +27 -0
  269. data/vendor/deps/smart_properties/lib/smart_properties/version.rb +3 -0
  270. data/vendor/lib/semantic/LICENSE +20 -0
  271. data/vendor/lib/semantic/semantic.rb +4 -0
  272. data/vendor/lib/semantic/version.rb +180 -0
  273. metadata +374 -0
@@ -0,0 +1,60 @@
1
+ require 'cli/kit'
2
+
3
+ module CLI
4
+ module Kit
5
+ class Resolver
6
+ def initialize(tool_name:, command_registry:)
7
+ @tool_name = tool_name
8
+ @command_registry = command_registry
9
+ end
10
+
11
+ def call(args)
12
+ args = args.dup
13
+ command_name = args.shift
14
+
15
+ command, resolved_name = @command_registry.lookup_command(command_name)
16
+
17
+ if command.nil?
18
+ command_not_found(command_name)
19
+ raise CLI::Kit::AbortSilent # Already output message
20
+ end
21
+
22
+ [command, resolved_name, args]
23
+ end
24
+
25
+ private
26
+
27
+ def command_not_found(name)
28
+ CLI::UI::Frame.open("Command not found", color: :red, timing: false) do
29
+ $stderr.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
30
+ end
31
+
32
+ cmds = commands_and_aliases
33
+ if cmds.all? { |cmd| cmd.is_a?(String) }
34
+ possible_matches = cmds.min_by(2) do |cmd|
35
+ CLI::Kit::Levenshtein.distance(cmd, name)
36
+ end
37
+
38
+ # We don't want to match against any possible command
39
+ # so reject anything that is too far away
40
+ possible_matches.reject! do |possible_match|
41
+ CLI::Kit::Levenshtein.distance(possible_match, name) > 3
42
+ end
43
+
44
+ # If we have any matches left, tell the user
45
+ if possible_matches.any?
46
+ CLI::UI::Frame.open("{{bold:Did you mean?}}", timing: false, color: :blue) do
47
+ possible_matches.each do |possible_match|
48
+ $stderr.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def commands_and_aliases
56
+ @command_registry.command_names + @command_registry.aliases.keys
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ module Enumerable
2
+ def min_by(n = nil, &block)
3
+ return sort_by(&block).first unless n
4
+ sort_by(&block).first(n)
5
+ end if instance_method(:min_by).arity == 0
6
+ end
@@ -0,0 +1,9 @@
1
+ require 'cli/kit'
2
+
3
+ module CLI
4
+ module Kit
5
+ module Support
6
+ autoload :TestHelper, 'cli/kit/support/test_helper'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,244 @@
1
+ module CLI
2
+ module Kit
3
+ module Support
4
+ module TestHelper
5
+ def setup
6
+ super
7
+ CLI::Kit::System.reset!
8
+ end
9
+
10
+ def assert_all_commands_run(should_raise: true)
11
+ errors = CLI::Kit::System.error_message
12
+ CLI::Kit::System.reset!
13
+ assert false, errors if should_raise && !errors.nil?
14
+ errors
15
+ end
16
+
17
+ def teardown
18
+ super
19
+ assert_all_commands_run
20
+ end
21
+
22
+ module FakeConfig
23
+ require 'tmpdir'
24
+ require 'fileutils'
25
+
26
+ def setup
27
+ super
28
+ @tmpdir = Dir.mktmpdir
29
+ @prev_xdg = ENV['XDG_CONFIG_HOME']
30
+ ENV['XDG_CONFIG_HOME'] = @tmpdir
31
+ end
32
+
33
+ def teardown
34
+ FileUtils.rm_rf(@tmpdir)
35
+ ENV['XDG_CONFIG_HOME'] = @prev_xdg
36
+ super
37
+ end
38
+ end
39
+
40
+ class FakeSuccess
41
+ def initialize(success)
42
+ @success = success
43
+ end
44
+
45
+ def success?
46
+ @success
47
+ end
48
+ end
49
+
50
+ module ::CLI
51
+ module Kit
52
+ module System
53
+ class << self
54
+ alias_method :original_system, :system
55
+ def system(*a, sudo: false, env: {}, **kwargs)
56
+ expected_command = expected_command(*a, sudo: sudo, env: env)
57
+
58
+ # In the case of an unexpected command, expected_command will be nil
59
+ return FakeSuccess.new(false) if expected_command.nil?
60
+
61
+ # Otherwise handle the command
62
+ if expected_command[:allow]
63
+ original_system(*a, sudo: sudo, env: env, **kwargs)
64
+ else
65
+ FakeSuccess.new(expected_command[:success])
66
+ end
67
+ end
68
+
69
+ alias_method :original_capture2, :capture2
70
+ def capture2(*a, sudo: false, env: {}, **kwargs)
71
+ expected_command = expected_command(*a, sudo: sudo, env: env)
72
+
73
+ # In the case of an unexpected command, expected_command will be nil
74
+ return [nil, FakeSuccess.new(false)] if expected_command.nil?
75
+
76
+ # Otherwise handle the command
77
+ if expected_command[:allow]
78
+ original_capture2(*a, sudo: sudo, env: env, **kwargs)
79
+ else
80
+ [
81
+ expected_command[:stdout],
82
+ FakeSuccess.new(expected_command[:success]),
83
+ ]
84
+ end
85
+ end
86
+
87
+ alias_method :original_capture2e, :capture2e
88
+ def capture2e(*a, sudo: false, env: {}, **kwargs)
89
+ expected_command = expected_command(*a, sudo: sudo, env: env)
90
+
91
+ # In the case of an unexpected command, expected_command will be nil
92
+ return [nil, FakeSuccess.new(false)] if expected_command.nil?
93
+
94
+ # Otherwise handle the command
95
+ if expected_command[:allow]
96
+ original_capture2ecapture2e(*a, sudo: sudo, env: env, **kwargs)
97
+ else
98
+ [
99
+ expected_command[:stdout],
100
+ FakeSuccess.new(expected_command[:success]),
101
+ ]
102
+ end
103
+ end
104
+
105
+ alias_method :original_capture3, :capture3
106
+ def capture3(*a, sudo: false, env: {}, **kwargs)
107
+ expected_command = expected_command(*a, sudo: sudo, env: env)
108
+
109
+ # In the case of an unexpected command, expected_command will be nil
110
+ return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
111
+
112
+ # Otherwise handle the command
113
+ if expected_command[:allow]
114
+ original_capture3(*a, sudo: sudo, env: env, **kwargs)
115
+ else
116
+ [
117
+ expected_command[:stdout],
118
+ expected_command[:stderr],
119
+ FakeSuccess.new(expected_command[:success]),
120
+ ]
121
+ end
122
+ end
123
+
124
+ # Sets up an expectation for a command and stubs out the call (unless allow is true)
125
+ #
126
+ # #### Parameters
127
+ # `*a` : the command, represented as a splat
128
+ # `stdout` : stdout to stub the command with (defaults to empty string)
129
+ # `stderr` : stderr to stub the command with (defaults to empty string)
130
+ # `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub)
131
+ # `success` : success status to stub the command with (Defaults to nil)
132
+ # `sudo` : expectation of sudo being set or not (defaults to false)
133
+ # `env` : expectation of env being set or not (defaults to {})
134
+ #
135
+ # Note: Must set allow or success
136
+ #
137
+ def fake(*a, stdout: "", stderr: "", allow: nil, success: nil, sudo: false, env: {})
138
+ raise ArgumentError, "success or allow must be set" if success.nil? && allow.nil?
139
+
140
+ @delegate_open3 ||= {}
141
+ @delegate_open3[a.join(' ')] = {
142
+ expected: {
143
+ sudo: sudo,
144
+ env: env,
145
+ },
146
+ actual: {
147
+ sudo: nil,
148
+ env: nil,
149
+ },
150
+ stdout: stdout,
151
+ stderr: stderr,
152
+ allow: allow,
153
+ success: success,
154
+ run: false,
155
+ }
156
+ end
157
+
158
+ # Resets the faked commands
159
+ #
160
+ def reset!
161
+ @delegate_open3 = {}
162
+ end
163
+
164
+ # Returns the errors associated to a test run
165
+ #
166
+ # #### Returns
167
+ # `errors` (String) a string representing errors found on this run, nil if none
168
+ def error_message
169
+ errors = {
170
+ unexpected: [],
171
+ not_run: [],
172
+ other: {},
173
+ }
174
+
175
+ @delegate_open3.each do |cmd, opts|
176
+ if opts[:unexpected]
177
+ errors[:unexpected] << cmd
178
+ elsif opts[:run]
179
+ error = []
180
+
181
+ if opts[:expected][:sudo] != opts[:actual][:sudo]
182
+ error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
183
+ end
184
+
185
+ if opts[:expected][:env] != opts[:actual][:env]
186
+ error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
187
+ end
188
+
189
+ errors[:other][cmd] = error.join("\n") unless error.empty?
190
+ else
191
+ errors[:not_run] << cmd
192
+ end
193
+ end
194
+
195
+ final_error = []
196
+
197
+ unless errors[:unexpected].empty?
198
+ final_error << CLI::UI.fmt(<<~EOF)
199
+ {{bold:Unexpected command invocations:}}
200
+ {{command:#{errors[:unexpected].join("\n")}}}
201
+ EOF
202
+ end
203
+
204
+ unless errors[:not_run].empty?
205
+ final_error << CLI::UI.fmt(<<~EOF)
206
+ {{bold:Expected commands were not run:}}
207
+ {{command:#{errors[:not_run].join("\n")}}}
208
+ EOF
209
+ end
210
+
211
+ unless errors[:other].empty?
212
+ final_error << CLI::UI.fmt(<<~EOF)
213
+ {{bold:Commands were not run as expected:}}
214
+ #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
215
+ EOF
216
+ end
217
+
218
+ return nil if final_error.empty?
219
+ "\n" + final_error.join("\n") # Initial new line for formatting reasons
220
+ end
221
+
222
+ private
223
+
224
+ def expected_command(*a, sudo: raise, env: raise)
225
+ expected_cmd = @delegate_open3[a.join(' ')]
226
+
227
+ if expected_cmd.nil?
228
+ @delegate_open3[a.join(' ')] = { unexpected: true }
229
+ return nil
230
+ end
231
+
232
+ expected_cmd[:run] = true
233
+ expected_cmd[:actual][:sudo] = sudo
234
+ expected_cmd[:actual][:env] = env
235
+ expected_cmd
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,207 @@
1
+ require 'cli/kit'
2
+
3
+ require 'open3'
4
+ require 'English'
5
+
6
+ module CLI
7
+ module Kit
8
+ module System
9
+ SUDO_PROMPT = CLI::UI.fmt("{{info:(sudo)}} Password: ")
10
+ class << self
11
+ # Ask for sudo access with a message explaning the need for it
12
+ # Will make subsequent commands capable of running with sudo for a period of time
13
+ #
14
+ # #### Parameters
15
+ # - `msg`: A message telling the user why sudo is needed
16
+ #
17
+ # #### Usage
18
+ # `ctx.sudo_reason("We need to do a thing")`
19
+ #
20
+ def sudo_reason(msg)
21
+ # See if sudo has a cached password
22
+ `env SUDO_ASKPASS=/usr/bin/false sudo -A true`
23
+ return if $CHILD_STATUS.success?
24
+ CLI::UI.with_frame_color(:blue) do
25
+ puts(CLI::UI.fmt("{{i}} #{msg}"))
26
+ end
27
+ end
28
+
29
+ # Execute a command in the user's environment
30
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
31
+ # Captures the results of the command without output to the console
32
+ #
33
+ # #### Parameters
34
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
35
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
36
+ # - `env`: process environment with which to execute this command
37
+ # - `**kwargs`: additional arguments to pass to Open3.capture2
38
+ #
39
+ # #### Returns
40
+ # - `output`: output (STDOUT) of the command execution
41
+ # - `status`: boolean success status of the command execution
42
+ #
43
+ # #### Usage
44
+ # `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
45
+ #
46
+ def capture2(*a, sudo: false, env: ENV, **kwargs)
47
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
48
+ end
49
+
50
+ # Execute a command in the user's environment
51
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
52
+ # Captures the results of the command without output to the console
53
+ #
54
+ # #### Parameters
55
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
56
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
57
+ # - `env`: process environment with which to execute this command
58
+ # - `**kwargs`: additional arguments to pass to Open3.capture2e
59
+ #
60
+ # #### Returns
61
+ # - `output`: output (STDOUT merged with STDERR) of the command execution
62
+ # - `status`: boolean success status of the command execution
63
+ #
64
+ # #### Usage
65
+ # `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
66
+ #
67
+ def capture2e(*a, sudo: false, env: ENV, **kwargs)
68
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
69
+ end
70
+
71
+ # Execute a command in the user's environment
72
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
73
+ # Captures the results of the command without output to the console
74
+ #
75
+ # #### Parameters
76
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
77
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
78
+ # - `env`: process environment with which to execute this command
79
+ # - `**kwargs`: additional arguments to pass to Open3.capture3
80
+ #
81
+ # #### Returns
82
+ # - `output`: STDOUT of the command execution
83
+ # - `error`: STDERR of the command execution
84
+ # - `status`: boolean success status of the command execution
85
+ #
86
+ # #### Usage
87
+ # `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
88
+ #
89
+ def capture3(*a, sudo: false, env: ENV, **kwargs)
90
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
91
+ end
92
+
93
+ # Execute a command in the user's environment
94
+ # Outputs result of the command without capturing it
95
+ #
96
+ # #### Parameters
97
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
98
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
99
+ # - `env`: process environment with which to execute this command
100
+ # - `**kwargs`: additional keyword arguments to pass to Process.spawn
101
+ #
102
+ # #### Returns
103
+ # - `status`: boolean success status of the command execution
104
+ #
105
+ # #### Usage
106
+ # `stat = CLI::Kit::System.system('ls', 'a_folder')`
107
+ #
108
+ def system(*a, sudo: false, env: ENV, **kwargs)
109
+ a = apply_sudo(*a, sudo)
110
+
111
+ out_r, out_w = IO.pipe
112
+ err_r, err_w = IO.pipe
113
+ in_stream = STDIN.closed? ? :close : STDIN
114
+ pid = Process.spawn(env, *resolve_path(a, env), 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
115
+ out_w.close
116
+ err_w.close
117
+
118
+ handlers = if block_given?
119
+ { out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
120
+ err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) } }
121
+ else
122
+ { out_r => ->(data) { STDOUT.write(data) },
123
+ err_r => ->(data) { STDOUT.write(data) } }
124
+ end
125
+
126
+ previous_trailing = Hash.new('')
127
+ loop do
128
+ ios = [err_r, out_r].reject(&:closed?)
129
+ break if ios.empty?
130
+
131
+ readers, = IO.select(ios)
132
+ readers.each do |io|
133
+ begin
134
+ data, trailing = split_partial_characters(io.readpartial(4096))
135
+ handlers[io].call(previous_trailing[io] + data)
136
+ previous_trailing[io] = trailing
137
+ rescue IOError
138
+ io.close
139
+ end
140
+ end
141
+ end
142
+
143
+ Process.wait(pid)
144
+ $CHILD_STATUS
145
+ end
146
+
147
+ # Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
148
+ # how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
149
+ # algorithm will split off a whole trailing multi-byte character.
150
+ def split_partial_characters(data)
151
+ last_byte = data.getbyte(-1)
152
+ return [data, ''] if (last_byte & 0b1000_0000).zero?
153
+
154
+ # UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
155
+ # allocating an array for the whole of data with bytes
156
+ min_bound = -[6, data.bytesize].min
157
+ final_bytes = data.byteslice(min_bound..-1).bytes
158
+ partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
159
+ # Bail out for non UTF-8
160
+ return [data, ''] unless partial_character_sub_index
161
+ partial_character_index = min_bound + partial_character_sub_index
162
+
163
+ [data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
164
+ end
165
+
166
+ private
167
+
168
+ def apply_sudo(*a, sudo)
169
+ a.unshift('sudo', '-S', '-p', SUDO_PROMPT, '--') if sudo
170
+ sudo_reason(sudo) if sudo.is_a?(String)
171
+ a
172
+ end
173
+
174
+ def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs)
175
+ a = apply_sudo(*a, sudo)
176
+ Open3.send(method, env, *resolve_path(a, env), **kwargs)
177
+ rescue Errno::EINTR
178
+ raise(Errno::EINTR, "command interrupted: #{a.join(' ')}")
179
+ end
180
+
181
+ # Ruby resolves the program to execute using its own PATH, but we want it to
182
+ # use the provided one, so we ensure ruby chooses to spawn a shell, which will
183
+ # parse our command and properly spawn our target using the provided environment.
184
+ #
185
+ # This is important because dev clobbers its own environment such that ruby
186
+ # means /usr/bin/ruby, but we want it to select the ruby targeted by the active
187
+ # project.
188
+ #
189
+ # See https://github.com/Shopify/dev/pull/625 for more details.
190
+ def resolve_path(a, env)
191
+ # If only one argument was provided, make sure it's interpreted by a shell.
192
+ return ["true ; " + a[0]] if a.size == 1
193
+ return a if a.first.include?('/')
194
+
195
+ paths = env.fetch('PATH', '').split(':')
196
+ item = paths.detect do |f|
197
+ command_path = "#{f}/#{a.first}"
198
+ File.executable?(command_path) && File.file?(command_path)
199
+ end
200
+
201
+ a[0] = "#{item}/#{a.first}" if item
202
+ a
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end