shopify-cli 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,24 @@
1
+ module CLI
2
+ module UI
3
+ module Prompt
4
+ # A class that handles the various options of an InteractivePrompt and their callbacks
5
+ class OptionsHandler
6
+ def initialize
7
+ @options = {}
8
+ end
9
+
10
+ def options
11
+ @options.keys
12
+ end
13
+
14
+ def option(option, &handler)
15
+ @options[option] = handler
16
+ end
17
+
18
+ def call(option)
19
+ @options[option].call(option)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ require 'cli/ui'
2
+
3
+ module CLI
4
+ module UI
5
+ module Spinner
6
+ autoload :Async, 'cli/ui/spinner/async'
7
+ autoload :SpinGroup, 'cli/ui/spinner/spin_group'
8
+
9
+ PERIOD = 0.1 # seconds
10
+ TASK_FAILED = :task_failed
11
+
12
+ begin
13
+ runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
14
+ colors = [CLI::UI::Color::CYAN.code] * 5 + [CLI::UI::Color::MAGENTA.code] * 5
15
+ raise unless runes.size == colors.size
16
+ GLYPHS = colors.zip(runes).map(&:join)
17
+ end
18
+
19
+ # Adds a single spinner
20
+ # Uses an interactive session to allow the user to pick an answer
21
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
22
+ #
23
+ # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
24
+ #
25
+ # ==== Attributes
26
+ #
27
+ # * +title+ - Title of the spinner to use
28
+ #
29
+ # ==== Options
30
+ #
31
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
32
+ #
33
+ # ==== Block
34
+ #
35
+ # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
36
+ #
37
+ # ==== Example Usage:
38
+ #
39
+ # CLI::UI::Spinner.spin('Title') { sleep 1.0 }
40
+ #
41
+ def self.spin(title, auto_debrief: true, &block)
42
+ sg = SpinGroup.new(auto_debrief: auto_debrief)
43
+ sg.add(title, &block)
44
+ sg.wait
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ module CLI
2
+ module UI
3
+ module Spinner
4
+ class Async
5
+ # Convenience method for +initialize+
6
+ #
7
+ def self.start(title)
8
+ new(title)
9
+ end
10
+
11
+ # Initializes a new asynchronous spinner with no specific end.
12
+ # Must call +.stop+ to end the spinner
13
+ #
14
+ # ==== Attributes
15
+ #
16
+ # * +title+ - Title of the spinner to use
17
+ #
18
+ # ==== Example Usage:
19
+ #
20
+ # CLI::UI::Spinner::Async.new('Title')
21
+ #
22
+ def initialize(title)
23
+ require 'thread'
24
+ sg = CLI::UI::Spinner::SpinGroup.new
25
+ @m = Mutex.new
26
+ @cv = ConditionVariable.new
27
+ sg.add(title) { @m.synchronize { @cv.wait(@m) } }
28
+ @t = Thread.new { sg.wait }
29
+ end
30
+
31
+ # Stops an asynchronous spinner
32
+ #
33
+ def stop
34
+ @m.synchronize { @cv.signal }
35
+ @t.value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,241 @@
1
+ module CLI
2
+ module UI
3
+ module Spinner
4
+ class SpinGroup
5
+ # Initializes a new spin group
6
+ # This lets you add +Task+ objects to the group to multi-thread work
7
+ #
8
+ # ==== Options
9
+ #
10
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
11
+ #
12
+ # ==== Example Usage
13
+ #
14
+ # spin_group = CLI::UI::SpinGroup.new
15
+ # spin_group.add('Title') { |spinner| sleep 3.0 }
16
+ # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
17
+ # spin_group.wait
18
+ #
19
+ # Output:
20
+ #
21
+ # https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
22
+ #
23
+ def initialize(auto_debrief: true)
24
+ @m = Mutex.new
25
+ @consumed_lines = 0
26
+ @tasks = []
27
+ @auto_debrief = auto_debrief
28
+ end
29
+
30
+ class Task
31
+ attr_reader :title, :exception, :success, :stdout, :stderr
32
+
33
+ # Initializes a new Task
34
+ # This is managed entirely internally by +SpinGroup+
35
+ #
36
+ # ==== Attributes
37
+ #
38
+ # * +title+ - Title of the task
39
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
40
+ #
41
+ def initialize(title, &block)
42
+ @title = title
43
+ @thread = Thread.new do
44
+ cap = CLI::UI::StdoutRouter::Capture.new(self, with_frame_inset: false, &block)
45
+ begin
46
+ cap.run
47
+ ensure
48
+ @stdout = cap.stdout
49
+ @stderr = cap.stderr
50
+ end
51
+ end
52
+
53
+ @force_full_render = false
54
+ @done = false
55
+ @exception = nil
56
+ @success = false
57
+ end
58
+
59
+ # Checks if a task is finished
60
+ #
61
+ def check
62
+ return true if @done
63
+ return false if @thread.alive?
64
+
65
+ @done = true
66
+ begin
67
+ status = @thread.join.status
68
+ @success = (status == false)
69
+ @success = false if @thread.value == TASK_FAILED
70
+ rescue => exc
71
+ @exception = exc
72
+ @success = false
73
+ end
74
+
75
+ @done
76
+ end
77
+
78
+ # Re-renders the task if required
79
+ #
80
+ # ==== Attributes
81
+ #
82
+ # * +index+ - index of the task
83
+ # * +force+ - force rerender of the task
84
+ # * +width+ - current terminal width to format for
85
+ #
86
+ def render(index, force = true, width: CLI::UI::Terminal.width)
87
+ return full_render(index, width) if force || @force_full_render
88
+ partial_render(index)
89
+ ensure
90
+ @force_full_render = false
91
+ end
92
+
93
+ # Update the spinner title
94
+ #
95
+ # ==== Attributes
96
+ #
97
+ # * +title+ - title to change the spinner to
98
+ #
99
+ def update_title(new_title)
100
+ @title = new_title
101
+ @force_full_render = true
102
+ end
103
+
104
+ private
105
+
106
+ def full_render(index, terminal_width)
107
+ prefix = inset +
108
+ glyph(index) +
109
+ CLI::UI::Color::RESET.code +
110
+ ' '
111
+
112
+ truncation_width = terminal_width - CLI::UI::ANSI.printing_width(prefix)
113
+
114
+ prefix +
115
+ CLI::UI.resolve_text(title, truncate_to: truncation_width) +
116
+ "\e[K"
117
+ end
118
+
119
+ def partial_render(index)
120
+ CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
121
+ end
122
+
123
+ def glyph(index)
124
+ if @done
125
+ @success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
126
+ else
127
+ GLYPHS[index]
128
+ end
129
+ end
130
+
131
+ def inset
132
+ @inset ||= CLI::UI::Frame.prefix
133
+ end
134
+
135
+ def inset_width
136
+ @inset_width ||= CLI::UI::ANSI.printing_width(inset)
137
+ end
138
+ end
139
+
140
+ # Add a new task
141
+ #
142
+ # ==== Attributes
143
+ #
144
+ # * +title+ - Title of the task
145
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
146
+ #
147
+ # ==== Example Usage:
148
+ # spin_group = CLI::UI::SpinGroup.new
149
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
150
+ # spin_group.wait
151
+ #
152
+ def add(title, &block)
153
+ @m.synchronize do
154
+ @tasks << Task.new(title, &block)
155
+ end
156
+ end
157
+
158
+ # Tells the group you're done adding tasks and to wait for all of them to finish
159
+ #
160
+ # ==== Example Usage:
161
+ # spin_group = CLI::UI::SpinGroup.new
162
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
163
+ # spin_group.wait
164
+ #
165
+ def wait
166
+ idx = 0
167
+
168
+ loop do
169
+ all_done = true
170
+
171
+ width = CLI::UI::Terminal.width
172
+
173
+ @m.synchronize do
174
+ CLI::UI.raw do
175
+ @tasks.each.with_index do |task, int_index|
176
+ nat_index = int_index + 1
177
+ task_done = task.check
178
+ all_done = false unless task_done
179
+
180
+ if nat_index > @consumed_lines
181
+ print(task.render(idx, true, width: width) + "\n")
182
+ @consumed_lines += 1
183
+ else
184
+ offset = @consumed_lines - int_index
185
+ move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
186
+ move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
187
+
188
+ print(move_to + task.render(idx, idx.zero?, width: width) + move_from)
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ break if all_done
195
+
196
+ idx = (idx + 1) % GLYPHS.size
197
+ sleep(PERIOD)
198
+ end
199
+
200
+ if @auto_debrief
201
+ debrief
202
+ else
203
+ @m.synchronize do
204
+ @tasks.all?(&:success)
205
+ end
206
+ end
207
+ end
208
+
209
+ # Debriefs failed tasks is +auto_debrief+ is true
210
+ #
211
+ def debrief
212
+ @m.synchronize do
213
+ @tasks.each do |task|
214
+ next if task.success
215
+
216
+ e = task.exception
217
+ out = task.stdout
218
+ err = task.stderr
219
+
220
+ CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red) do
221
+ if e
222
+ puts "#{e.class}: #{e.message}"
223
+ puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
224
+ end
225
+
226
+ CLI::UI::Frame.divider('STDOUT')
227
+ out = "(empty)" if out.nil? || out.strip.empty?
228
+ puts out
229
+
230
+ CLI::UI::Frame.divider('STDERR')
231
+ err = "(empty)" if err.nil? || err.strip.empty?
232
+ puts err
233
+ end
234
+ end
235
+ @tasks.all?(&:success)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,227 @@
1
+ require 'cli/ui'
2
+ require 'stringio'
3
+
4
+ module CLI
5
+ module UI
6
+ module StdoutRouter
7
+ class << self
8
+ attr_accessor :duplicate_output_to
9
+ end
10
+
11
+ class Writer
12
+ def initialize(stream, name)
13
+ @stream = stream
14
+ @name = name
15
+ end
16
+
17
+ def write(*args)
18
+ args = args.map do |str|
19
+ if auto_frame_inset?
20
+ str = str.dup # unfreeze
21
+ str = str.force_encoding(Encoding::UTF_8)
22
+ apply_line_prefix(str, CLI::UI::Frame.prefix)
23
+ else
24
+ @pending_newline = false
25
+ str
26
+ end
27
+ end
28
+
29
+ hook = Thread.current[:cliui_output_hook]
30
+ # hook return of false suppresses output.
31
+ if !hook || hook.call(args.first, @name) != false
32
+ @stream.write_without_cli_ui(*prepend_id(@stream, args))
33
+ if dup = StdoutRouter.duplicate_output_to
34
+ dup.write(*prepend_id(dup, args))
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def prepend_id(stream, args)
42
+ return args unless prepend_id_for_stream(stream)
43
+ args.map do |a|
44
+ next a if a.chomp.empty? # allow new lines to be new lines
45
+ "[#{Thread.current[:cliui_output_id][:id]}] #{a}"
46
+ end
47
+ end
48
+
49
+ def prepend_id_for_stream(stream)
50
+ return false unless Thread.current[:cliui_output_id]
51
+ return true if Thread.current[:cliui_output_id][:streams].include?(stream)
52
+ false
53
+ end
54
+
55
+ def auto_frame_inset?
56
+ !Thread.current[:no_cliui_frame_inset]
57
+ end
58
+
59
+ def apply_line_prefix(str, prefix)
60
+ return '' if str.empty?
61
+ prefixed = +''
62
+ str.force_encoding(Encoding::UTF_8).lines.each do |line|
63
+ if @pending_newline
64
+ prefixed << line
65
+ @pending_newline = false
66
+ else
67
+ prefixed << prefix << line
68
+ end
69
+ end
70
+ @pending_newline = !str.end_with?("\n")
71
+ prefixed
72
+ end
73
+ end
74
+
75
+ class Capture
76
+ @m = Mutex.new
77
+ @active_captures = 0
78
+ @saved_stdin = nil
79
+
80
+ def self.with_stdin_masked
81
+ @m.synchronize do
82
+ if @active_captures.zero?
83
+ @saved_stdin = $stdin
84
+ $stdin, w = IO.pipe
85
+ $stdin.close
86
+ w.close
87
+ end
88
+ @active_captures += 1
89
+ end
90
+
91
+ yield
92
+ ensure
93
+ @m.synchronize do
94
+ @active_captures -= 1
95
+ if @active_captures.zero?
96
+ $stdin = @saved_stdin
97
+ end
98
+ end
99
+ end
100
+
101
+ def initialize(*block_args, with_frame_inset: true, &block)
102
+ @with_frame_inset = with_frame_inset
103
+ @block_args = block_args
104
+ @block = block
105
+ end
106
+
107
+ attr_reader :stdout, :stderr
108
+
109
+ def run
110
+ require 'stringio'
111
+
112
+ StdoutRouter.assert_enabled!
113
+
114
+ out = StringIO.new
115
+ err = StringIO.new
116
+
117
+ prev_frame_inset = Thread.current[:no_cliui_frame_inset]
118
+ prev_hook = Thread.current[:cliui_output_hook]
119
+
120
+ self.class.with_stdin_masked do
121
+ Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
122
+ Thread.current[:cliui_output_hook] = ->(data, stream) do
123
+ case stream
124
+ when :stdout then out.write(data)
125
+ when :stderr then err.write(data)
126
+ else raise
127
+ end
128
+ false # suppress writing to terminal
129
+ end
130
+
131
+ begin
132
+ @block.call(*@block_args)
133
+ ensure
134
+ @stdout = out.string
135
+ @stderr = err.string
136
+ end
137
+ end
138
+ ensure
139
+ Thread.current[:cliui_output_hook] = prev_hook
140
+ Thread.current[:no_cliui_frame_inset] = prev_frame_inset
141
+ end
142
+ end
143
+
144
+ class << self
145
+ WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
146
+
147
+ NotEnabled = Class.new(StandardError)
148
+
149
+ def with_id(on_streams:)
150
+ unless on_streams.is_a?(Array) && on_streams.all? { |s| s.respond_to?(:write) }
151
+ raise ArgumentError, <<~EOF
152
+ on_streams must be an array of objects that respond to `write`
153
+ These do not respond to write
154
+ #{on_streams.reject { |s| s.respond_to?(:write) }.map.with_index { |s| s.class.to_s }.join("\n")}
155
+ EOF
156
+ end
157
+
158
+ require 'securerandom'
159
+ id = format("%05d", rand(10**5))
160
+ Thread.current[:cliui_output_id] = {
161
+ id: id,
162
+ streams: on_streams
163
+ }
164
+ yield(id)
165
+ ensure
166
+ Thread.current[:cliui_output_id] = nil
167
+ end
168
+
169
+ def current_id
170
+ Thread.current[:cliui_output_id]
171
+ end
172
+
173
+ def assert_enabled!
174
+ raise NotEnabled unless enabled?
175
+ end
176
+
177
+ def with_enabled
178
+ enable
179
+ yield
180
+ ensure
181
+ disable
182
+ end
183
+
184
+ # TODO: remove this
185
+ def ensure_activated
186
+ enable unless enabled?
187
+ end
188
+
189
+ def enable
190
+ return false if enabled?($stdout) || enabled?($stderr)
191
+ activate($stdout, :stdout)
192
+ activate($stderr, :stderr)
193
+ true
194
+ end
195
+
196
+ def enabled?(stream = $stdout)
197
+ stream.respond_to?(WRITE_WITHOUT_CLI_UI)
198
+ end
199
+
200
+ def disable
201
+ return false unless enabled?($stdout) && enabled?($stderr)
202
+ deactivate($stdout)
203
+ deactivate($stderr)
204
+ true
205
+ end
206
+
207
+ private
208
+
209
+ def deactivate(stream)
210
+ sc = stream.singleton_class
211
+ sc.send(:remove_method, :write)
212
+ sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
213
+ end
214
+
215
+ def activate(stream, streamname)
216
+ writer = StdoutRouter::Writer.new(stream, streamname)
217
+
218
+ raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
219
+ stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
220
+ stream.define_singleton_method(:write) do |*args|
221
+ writer.write(*args)
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end