hanami-cli 2.1.0.beta1 → 2.1.0.rc1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/Gemfile +5 -2
  4. data/hanami-cli.gemspec +1 -1
  5. data/lib/hanami/cli/bundler.rb +19 -5
  6. data/lib/hanami/cli/command.rb +4 -4
  7. data/lib/hanami/cli/commands/app/assets/command.rb +69 -0
  8. data/lib/hanami/cli/commands/app/assets/compile.rb +32 -0
  9. data/lib/hanami/cli/commands/app/assets/watch.rb +32 -0
  10. data/lib/hanami/cli/commands/app/assets.rb +16 -0
  11. data/lib/hanami/cli/commands/app/command.rb +2 -2
  12. data/lib/hanami/cli/commands/app/dev.rb +45 -0
  13. data/lib/hanami/cli/commands/app/generate/action.rb +3 -2
  14. data/lib/hanami/cli/commands/app/generate/part.rb +49 -0
  15. data/lib/hanami/cli/commands/app/install.rb +16 -1
  16. data/lib/hanami/cli/commands/app.rb +9 -0
  17. data/lib/hanami/cli/commands/gem/new.rb +57 -7
  18. data/lib/hanami/cli/errors.rb +30 -0
  19. data/lib/hanami/cli/files.rb +8 -2
  20. data/lib/hanami/cli/generators/app/action/action.erb +5 -1
  21. data/lib/hanami/cli/generators/app/action/slice_action.erb +5 -1
  22. data/lib/hanami/cli/generators/app/action.rb +49 -12
  23. data/lib/hanami/cli/generators/app/part/app_base_part.erb +9 -0
  24. data/lib/hanami/cli/generators/app/part/app_part.erb +13 -0
  25. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +9 -0
  26. data/lib/hanami/cli/generators/app/part/slice_part.erb +13 -0
  27. data/lib/hanami/cli/generators/app/part.rb +101 -0
  28. data/lib/hanami/cli/generators/app/part_context.rb +98 -0
  29. data/lib/hanami/cli/generators/app/slice/app_css.erb +5 -0
  30. data/lib/hanami/cli/generators/app/slice/app_js.erb +1 -0
  31. data/lib/hanami/cli/generators/app/slice/app_layout.erb +18 -0
  32. data/lib/hanami/cli/generators/app/slice.rb +9 -3
  33. data/lib/hanami/cli/generators/app/slice_context.rb +18 -0
  34. data/lib/hanami/cli/generators/context.rb +70 -3
  35. data/lib/hanami/cli/generators/gem/app/404.html +76 -5
  36. data/lib/hanami/cli/generators/gem/app/500.html +76 -5
  37. data/lib/hanami/cli/generators/gem/app/app_css.erb +5 -0
  38. data/lib/hanami/cli/generators/gem/app/app_js.erb +1 -0
  39. data/lib/hanami/cli/generators/gem/app/app_layout.erb +18 -0
  40. data/lib/hanami/cli/generators/gem/app/assets.mjs +14 -0
  41. data/lib/hanami/cli/generators/gem/app/dev +8 -0
  42. data/lib/hanami/cli/generators/gem/app/favicon.ico +0 -0
  43. data/lib/hanami/cli/generators/gem/app/gemfile.erb +14 -8
  44. data/lib/hanami/cli/generators/gem/app/gitignore.erb +4 -0
  45. data/lib/hanami/cli/generators/gem/app/package.json.erb +10 -0
  46. data/lib/hanami/cli/generators/gem/app/procfile.erb +4 -0
  47. data/lib/hanami/cli/generators/gem/app/puma.erb +37 -7
  48. data/lib/hanami/cli/generators/gem/app/routes.erb +1 -1
  49. data/lib/hanami/cli/generators/gem/app.rb +21 -4
  50. data/lib/hanami/cli/generators/version.rb +12 -0
  51. data/lib/hanami/cli/interactive_system_call.rb +64 -0
  52. data/lib/hanami/cli/system_call.rb +8 -2
  53. data/lib/hanami/cli/version.rb +1 -1
  54. metadata +28 -6
  55. data/lib/hanami/cli/generators/app/slice/layouts_app.html.erb +0 -1
  56. data/lib/hanami/cli/generators/gem/app/layouts_app.html.erb +0 -1
@@ -12,9 +12,10 @@ module Hanami
12
12
  class Context
13
13
  # @since 2.0.0
14
14
  # @api private
15
- def initialize(inflector, app)
15
+ def initialize(inflector, app, **options)
16
16
  @inflector = inflector
17
17
  @app = app
18
+ @options = options
18
19
  end
19
20
 
20
21
  # @since 2.0.0
@@ -23,10 +24,30 @@ module Hanami
23
24
  binding
24
25
  end
25
26
 
27
+ def hanami_gem(name)
28
+ gem_name = name == "hanami" ? "hanami" : "hanami-#{name}"
29
+
30
+ %(gem "#{gem_name}", #{hanami_gem_version(name)})
31
+ end
32
+
26
33
  # @since 2.0.0
27
34
  # @api private
28
- def hanami_version
29
- Version.gem_requirement
35
+ def hanami_gem_version(gem_name)
36
+ if hanami_head?
37
+ %(github: "hanami/#{gem_name}", branch: "main")
38
+ else
39
+ %("#{Version.gem_requirement}")
40
+ end
41
+ end
42
+
43
+ # @since 2.1.0
44
+ # @api private
45
+ def hanami_assets_npm_package
46
+ if hanami_head?
47
+ %("hanami-assets": "hanami/assets-js#main")
48
+ else
49
+ %("hanami-assets": "#{Version.npm_package_requirement}")
50
+ end
30
51
  end
31
52
 
32
53
  # @since 2.0.0
@@ -41,11 +62,57 @@ module Hanami
41
62
  inflector.underscore(app)
42
63
  end
43
64
 
65
+ # @since 2.1.0
66
+ # @api private
67
+ def humanized_app_name
68
+ inflector.humanize(app)
69
+ end
70
+
71
+ # @since 2.1.0
72
+ # @api private
73
+ def hanami_head?
74
+ options.fetch(:head)
75
+ end
76
+
77
+ # @since 2.1.0
78
+ # @api private
79
+ def generate_assets?
80
+ !options.fetch(:skip_assets, false)
81
+ end
82
+
83
+ # @since 2.1.0
84
+ # @api private
85
+ def bundled_views?
86
+ Hanami.bundled?("hanami-view")
87
+ end
88
+
89
+ # @since 2.1.0
90
+ # @api private
91
+ def bundled_assets?
92
+ Hanami.bundled?("hanami-assets")
93
+ end
94
+
95
+ # @since 2.1.0
96
+ # @api private
97
+ #
98
+ # @see https://rubyreferences.github.io/rubychanges/3.1.html#values-in-hash-literals-and-keyword-arguments-can-be-omitted
99
+ def ruby_omit_hash_values?
100
+ RUBY_VERSION >= "3.1"
101
+ end
102
+
44
103
  private
45
104
 
105
+ # @since 2.0.0
106
+ # @api private
46
107
  attr_reader :inflector
47
108
 
109
+ # @since 2.0.0
110
+ # @api private
48
111
  attr_reader :app
112
+
113
+ # @since 2.1.0
114
+ # @api private
115
+ attr_reader :options
49
116
  end
50
117
  end
51
118
  end
@@ -1,11 +1,82 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>The page you were looking for doesn’t exist (404)</title>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>The page you were looking for doesn’t exist (404)</title>
7
+ <style>
8
+ :root {
9
+ --foreground-rgb: 0, 0, 0;
10
+ --background-rgb: 255, 255, 255;
11
+ --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --foreground-rgb: 255, 255, 255;
17
+ --background-rgb: 0, 0, 0;
18
+ }
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ body,
28
+ html {
29
+ max-width: 100vw;
30
+ overflow-x: hidden;
31
+ font-size: 100%;
32
+ }
33
+
34
+ body {
35
+ color: rgb(var(--foreground-rgb));
36
+ background: rgb(var(--background-rgb));
37
+ font-family: var(--font-sans);
38
+ font-style: normal;
39
+ }
40
+
41
+ main {
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 100vh;
47
+ padding: 0 4vw;
48
+ }
49
+
50
+ .message {
51
+ display: flex;
52
+ gap: 1rem;
53
+ flex-direction: column;
54
+ text-align: center;
55
+ }
56
+
57
+ .message h1 {
58
+ font-size: 2rem;
59
+ font-weight: 500;
60
+ }
61
+
62
+ p {
63
+ line-height: 1.6;
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ html {
68
+ color-scheme: dark;
69
+ }
70
+ }
71
+ </style>
5
72
  </head>
6
73
  <body>
7
74
  <!-- This file lives in public/404.html -->
8
- <h1>The page you were looking for doesn’t exist.</h1>
9
- <p>You may have mistyped the address or the page may have moved.</p>
75
+ <main>
76
+ <div class="message">
77
+ <h1>404</h1>
78
+ <p>The page you were looking for doesn’t exist.</p>
79
+ </div>
80
+ </main>
10
81
  </body>
11
82
  </html>
@@ -1,11 +1,82 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>We’re sorry, but something went wrong (500)</title>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>We’re sorry, but something went wrong (500)</title>
7
+ <style>
8
+ :root {
9
+ --foreground-rgb: 0, 0, 0;
10
+ --background-rgb: 255, 255, 255;
11
+ --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --foreground-rgb: 255, 255, 255;
17
+ --background-rgb: 0, 0, 0;
18
+ }
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ body,
28
+ html {
29
+ max-width: 100vw;
30
+ overflow-x: hidden;
31
+ font-size: 100%;
32
+ }
33
+
34
+ body {
35
+ color: rgb(var(--foreground-rgb));
36
+ background: rgb(var(--background-rgb));
37
+ font-family: var(--font-sans);
38
+ font-style: normal;
39
+ }
40
+
41
+ main {
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 100vh;
47
+ padding: 0 4vw;
48
+ }
49
+
50
+ .message {
51
+ display: flex;
52
+ gap: 1rem;
53
+ flex-direction: column;
54
+ text-align: center;
55
+ }
56
+
57
+ .message h1 {
58
+ font-size: 2rem;
59
+ font-weight: 500;
60
+ }
61
+
62
+ p {
63
+ line-height: 1.6;
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ html {
68
+ color-scheme: dark;
69
+ }
70
+ }
71
+ </style>
5
72
  </head>
6
73
  <body>
7
74
  <!-- This file lives in public/500.html -->
8
- <h1>We’re sorry, but something went wrong.</h1>
9
- <p>If you are the application owner, check the logs for more information.</p>
75
+ <main>
76
+ <div class="message">
77
+ <h1>500</h1>
78
+ <p>We’re sorry, but something went wrong.</p>
79
+ </div>
80
+ </main>
10
81
  </body>
11
82
  </html>
@@ -0,0 +1,5 @@
1
+ body {
2
+ background-color: #fff;
3
+ color: #000;
4
+ font-family: sans-serif;
5
+ }
@@ -0,0 +1 @@
1
+ import "../css/app.css";
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= humanized_app_name %></title>
7
+ <%- if generate_assets? -%>
8
+ <%%= favicon %>
9
+ <%%= css "app" %>
10
+ <%- end -%>
11
+ </head>
12
+ <body>
13
+ <%%= yield %>
14
+ <%- if generate_assets? -%>
15
+ <%%= js "app" %>
16
+ <%- end -%>
17
+ </body>
18
+ </html>
@@ -0,0 +1,14 @@
1
+ import * as assets from "hanami-assets";
2
+
3
+ await assets.run();
4
+
5
+ // To provide additional esbuild (https://esbuild.github.io) options, use the following:
6
+ //
7
+ // await assets.run({
8
+ // esbuildOptionsFn: (args, esbuildOptions) => {
9
+ // // Add to esbuildOptions here. Use `args.watch` as a condition for different options for
10
+ // // compile vs watch.
11
+ //
12
+ // return esbuildOptions;
13
+ // }
14
+ // });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env sh
2
+
3
+ if ! gem list foreman -i --silent; then
4
+ echo "Installing foreman..."
5
+ gem install foreman
6
+ fi
7
+
8
+ exec foreman start -f Procfile.dev "$@"
@@ -2,25 +2,31 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "hanami", "<%= hanami_version %>"
6
- gem "hanami-router", "<%= hanami_version %>"
7
- gem "hanami-controller", "<%= hanami_version %>"
8
- gem "hanami-validations", "<%= hanami_version %>"
9
- gem "hanami-view", "<%= hanami_version %>"
10
- gem "hanami-webconsole", "<%= hanami_version %>"
5
+ <%= hanami_gem("hanami") %>
6
+ <%= hanami_gem("router") %>
7
+ <%= hanami_gem("controller") %>
8
+ <%= hanami_gem("validations") %>
9
+ <%= hanami_gem("view") %>
10
+ <%- if generate_assets? -%>
11
+ <%= hanami_gem("assets") %>
12
+ <%- end -%>
11
13
 
12
14
  gem "dry-types", "~> 1.0", ">= 1.6.1"
13
15
  gem "puma"
14
16
  gem "rake"
15
17
 
18
+ group :development do
19
+ <%= hanami_gem("webconsole") %>
20
+ end
21
+
16
22
  group :development, :test do
17
23
  gem "dotenv"
18
24
  end
19
25
 
20
26
  group :cli, :development do
21
- gem "hanami-reloader"
27
+ <%= hanami_gem("reloader") %>
22
28
  end
23
29
 
24
30
  group :cli, :development, :test do
25
- gem "hanami-rspec"
31
+ <%= hanami_gem("rspec") %>
26
32
  end
@@ -1,2 +1,6 @@
1
1
  .env
2
2
  log/*
3
+ <%- if generate_assets? -%>
4
+ public/
5
+ node_modules/
6
+ <%- end -%>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "<%= underscored_app_name %>",
3
+ "private": true,
4
+ "scripts": {
5
+ "assets": "node config/assets.mjs"
6
+ },
7
+ "dependencies": {
8
+ <%= hanami_assets_npm_package %>
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ web: bundle exec hanami server
2
+ <%- if generate_assets? -%>
3
+ assets: bundle exec hanami assets watch
4
+ <%- end -%>
@@ -1,17 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #
4
+ # Environment and port
5
+ #
6
+ port ENV.fetch("<%= Hanami::Port::ENV_VAR %>", <%= Hanami::Port::DEFAULT %>)
7
+ environment ENV.fetch("HANAMI_ENV", "development")
8
+
9
+ #
10
+ # Threads within each Puma/Ruby process (aka worker)
11
+ #
12
+
13
+ # Configure the minimum and maximum number of threads to use to answer requests.
3
14
  max_threads_count = ENV.fetch("HANAMI_MAX_THREADS", 5)
4
15
  min_threads_count = ENV.fetch("HANAMI_MIN_THREADS") { max_threads_count }
16
+
5
17
  threads min_threads_count, max_threads_count
6
18
 
7
- port ENV.fetch("<%= Hanami::Port::ENV_VAR %>", <%= Hanami::Port::DEFAULT %>)
8
- environment ENV.fetch("HANAMI_ENV", "development")
9
- workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 0)
19
+ #
20
+ # Workers (aka Puma/Ruby processes)
21
+ #
10
22
 
11
- if ENV.fetch("HANAMI_WEB_CONCURRENCY", 0) > 0
12
- on_worker_boot do
23
+ puma_concurrency = Integer(ENV.fetch("HANAMI_WEB_CONCURRENCY", 0))
24
+ puma_cluster_mode = puma_concurrency > 1
25
+
26
+ # How many worker (Puma/Ruby) processes to run.
27
+ # Typically this is set to the number of available cores.
28
+ workers puma_concurrency
29
+
30
+ #
31
+ # Cluster mode (aka multiple workers)
32
+ #
33
+
34
+ if puma_cluster_mode
35
+ # Preload the application before starting the workers. Only in cluster mode.
36
+ preload_app!
37
+
38
+ # Code to run immediately before master process forks workers (once on boot).
39
+ #
40
+ # These hooks can block if necessary to wait for background operations unknown
41
+ # to puma to finish before the process terminates. This can be used to close
42
+ # any connections to remote servers (database, redis, …) that were opened when
43
+ # preloading the code.
44
+ before_fork do
13
45
  Hanami.shutdown
14
46
  end
15
47
  end
16
-
17
- preload_app!
@@ -2,6 +2,6 @@
2
2
 
3
3
  module <%= camelized_app_name %>
4
4
  class Routes < Hanami::Routes
5
- root { "Hello from Hanami" }
5
+ # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details.
6
6
  end
7
7
  end
@@ -40,8 +40,12 @@ module Hanami
40
40
  fs.write("README.md", t("readme.erb", context))
41
41
  fs.write("Gemfile", t("gemfile.erb", context))
42
42
  fs.write("Rakefile", t("rakefile.erb", context))
43
+ fs.write("Procfile.dev", t("procfile.erb", context))
43
44
  fs.write("config.ru", t("config_ru.erb", context))
44
45
 
46
+ fs.write("bin/dev", file("dev"))
47
+ fs.chmod("bin/dev", 0o755)
48
+
45
49
  fs.write("config/app.rb", t("app.erb", context))
46
50
  fs.write("config/settings.rb", t("settings.erb", context))
47
51
  fs.write("config/routes.rb", t("routes.erb", context))
@@ -54,21 +58,34 @@ module Hanami
54
58
  fs.write("app/action.rb", t("action.erb", context))
55
59
  fs.write("app/view.rb", t("view.erb", context))
56
60
  fs.write("app/views/helpers.rb", t("helpers.erb", context))
57
- fs.write("app/templates/layouts/app.html.erb", File.read(File.join(__dir__, "app", "layouts_app.html.erb")))
61
+ fs.write("app/templates/layouts/app.html.erb", t("app_layout.erb", context))
62
+
63
+ if context.generate_assets?
64
+ fs.write("package.json", t("package.json.erb", context))
65
+ fs.write("config/assets.mjs", file("assets.mjs"))
66
+ fs.write("app/assets/js/app.js", t("app_js.erb", context))
67
+ fs.write("app/assets/css/app.css", t("app_css.erb", context))
68
+ fs.write("app/assets/images/favicon.ico", file("favicon.ico"))
69
+ end
58
70
 
59
- fs.write("public/404.html", File.read(File.join(__dir__, "app", "404.html")))
60
- fs.write("public/500.html", File.read(File.join(__dir__, "app", "500.html")))
71
+ fs.write("public/404.html", file("404.html"))
72
+ fs.write("public/500.html", file("500.html"))
61
73
  end
62
74
 
63
75
  def template(path, context)
64
76
  require "erb"
65
77
 
66
78
  ERB.new(
67
- File.read(File.join(__dir__, "app", path))
79
+ File.read(File.join(__dir__, "app", path)),
80
+ trim_mode: "-"
68
81
  ).result(context.ctx)
69
82
  end
70
83
 
71
84
  alias_method :t, :template
85
+
86
+ def file(path)
87
+ File.read(File.join(__dir__, "app", path))
88
+ end
72
89
  end
73
90
  end
74
91
  end
@@ -26,6 +26,18 @@ module Hanami
26
26
  "~> #{result}"
27
27
  end
28
28
 
29
+ def self.npm_package_requirement
30
+ result = version
31
+ # Change "2.1.0.beta2.1" to "2.1.0-beta.2" (the only format tolerable by `npm install`)
32
+ if prerelease?
33
+ result = result
34
+ .sub(/\.(alpha|beta|rc)/, '-\1')
35
+ .sub(/(alpha|beta|rc)(.+)\.(.+)$/, '\1.\2')
36
+ end
37
+
38
+ "^#{result}"
39
+ end
40
+
29
41
  # @since 2.0.0
30
42
  # @api private
31
43
  def self.prerelease?
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Hanami
6
+ module CLI
7
+ # @api private
8
+ # @since 2.1.0
9
+ class InteractiveSystemCall
10
+ # @api private
11
+ # @since 2.1.0
12
+ def initialize(out: $stdout, err: $stderr)
13
+ @out = out
14
+ @err = err
15
+ super()
16
+ end
17
+
18
+ # @api private
19
+ # @since 2.1.0
20
+ def call(cmd, *args, env: {})
21
+ ::Bundler.with_unbundled_env do
22
+ threads = []
23
+ exit_status = 0
24
+
25
+ # rubocop:disable Lint/SuppressedException
26
+ Open3.popen3(env, command(cmd, *args)) do |_stdin, stdout, stderr, wait_thr|
27
+ threads << Thread.new do
28
+ stdout.each_line do |line|
29
+ out.puts(line)
30
+ end
31
+ rescue IOError # FIXME: Check if this is legit
32
+ end
33
+
34
+ threads << Thread.new do
35
+ stderr.each_line do |line|
36
+ err.puts(line)
37
+ end
38
+ rescue IOError # FIXME: Check if this is legit
39
+ end
40
+
41
+ threads.each(&:join)
42
+
43
+ exit_status = wait_thr.value
44
+ end
45
+ # rubocop:enable Lint/SuppressedException
46
+
47
+ exit(exit_status.exitstatus)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # @api private
54
+ # @since 2.1.0
55
+ attr_reader :out, :err
56
+
57
+ # @since 2.1.0
58
+ # @api public
59
+ def command(cmd, *args)
60
+ [cmd, args].flatten(1).compact.join(" ")
61
+ end
62
+ end
63
+ end
64
+ end
@@ -85,13 +85,13 @@ module Hanami
85
85
  #
86
86
  # @since 2.0.0
87
87
  # @api public
88
- def call(cmd, env: {})
88
+ def call(cmd, *args, env: {})
89
89
  exitstatus = nil
90
90
  out = nil
91
91
  err = nil
92
92
 
93
93
  ::Bundler.with_unbundled_env do
94
- Open3.popen3(env, cmd) do |stdin, stdout, stderr, wait_thr|
94
+ Open3.popen3(env, command(cmd, *args)) do |stdin, stdout, stderr, wait_thr|
95
95
  yield stdin, stdout, stderr, wait_thr if block_given?
96
96
 
97
97
  stdin.close
@@ -104,6 +104,12 @@ module Hanami
104
104
 
105
105
  Result.new(exit_code: exitstatus, out: out, err: err)
106
106
  end
107
+
108
+ # @since 2.1.0
109
+ # @api public
110
+ def command(cmd, *args)
111
+ [cmd, args].flatten(1).compact.join(" ")
112
+ end
107
113
  end
108
114
  end
109
115
  end
@@ -6,6 +6,6 @@ module Hanami
6
6
  #
7
7
  # @api public
8
8
  # @since 2.0.0
9
- VERSION = "2.1.0.beta1"
9
+ VERSION = "2.1.0.rc1"
10
10
  end
11
11
  end