loading
Generated 2025-10-11T17:19:52+00:00

All Files ( 100.0% covered at 3.99 hits/line )

29 files in total.
509 relevant lines, 509 lines covered and 0 lines missed. ( 100.0% )
50 total branches, 50 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
Gemfile 100.00 % 30 18 18 0 1.00 100.00 % 0 0 0
lib/gemsmith.rb 100.00 % 18 10 10 0 1.60 100.00 % 0 0 0
lib/gemsmith/../../gemsmith.gemspec 100.00 % 45 30 30 0 1.00 100.00 % 0 0 0
lib/gemsmith/builders/bundler.rb 100.00 % 25 9 9 0 3.33 100.00 % 0 0 0
lib/gemsmith/builders/circle_ci.rb 100.00 % 27 12 12 0 2.33 100.00 % 2 2 0
lib/gemsmith/builders/cli.rb 100.00 % 77 29 29 0 16.97 100.00 % 6 6 0
lib/gemsmith/builders/console.rb 100.00 % 22 10 10 0 3.40 100.00 % 2 2 0
lib/gemsmith/builders/documentation/readme.rb 100.00 % 141 29 29 0 3.69 100.00 % 8 8 0
lib/gemsmith/builders/git/commit.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
lib/gemsmith/builders/git/ignore.rb 100.00 % 30 11 11 0 2.82 100.00 % 2 2 0
lib/gemsmith/builders/rspec/helper.rb 100.00 % 27 12 12 0 3.08 100.00 % 4 4 0
lib/gemsmith/builders/specification.rb 100.00 % 26 9 9 0 5.33 100.00 % 0 0 0
lib/gemsmith/cli/actions/cli.rb 100.00 % 22 10 10 0 2.50 100.00 % 0 0 0
lib/gemsmith/cli/actions/edit.rb 100.00 % 41 23 23 0 3.13 100.00 % 3 3 0
lib/gemsmith/cli/actions/install.rb 100.00 % 46 26 26 0 3.54 100.00 % 3 3 0
lib/gemsmith/cli/actions/publish.rb 100.00 % 46 26 26 0 3.54 100.00 % 3 3 0
lib/gemsmith/cli/actions/view.rb 100.00 % 41 23 23 0 3.13 100.00 % 3 3 0
lib/gemsmith/cli/commands/build.rb 100.00 % 116 55 55 0 4.87 100.00 % 0 0 0
lib/gemsmith/cli/shell.rb 100.00 % 43 25 25 0 6.60 100.00 % 0 0 0
lib/gemsmith/dependencies.rb 100.00 % 7 3 3 0 1.00 100.00 % 0 0 0
lib/gemsmith/tools/cleaner.rb 100.00 % 29 15 15 0 2.07 100.00 % 0 0 0
lib/gemsmith/tools/editor.rb 100.00 % 23 11 11 0 1.18 100.00 % 2 2 0
lib/gemsmith/tools/installer.rb 100.00 % 38 19 19 0 3.53 100.00 % 2 2 0
lib/gemsmith/tools/packager.rb 100.00 % 33 17 17 0 1.88 100.00 % 0 0 0
lib/gemsmith/tools/publisher.rb 100.00 % 34 13 13 0 2.54 100.00 % 0 0 0
lib/gemsmith/tools/pusher.rb 100.00 % 53 28 28 0 5.04 100.00 % 6 6 0
lib/gemsmith/tools/validator.rb 100.00 % 19 9 9 0 1.44 100.00 % 2 2 0
lib/gemsmith/tools/versioner.rb 100.00 % 29 13 13 0 1.85 100.00 % 0 0 0
lib/gemsmith/tools/viewer.rb 100.00 % 19 9 9 0 1.22 100.00 % 2 2 0

Gemfile

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 ruby file: ".ruby-version"
  3. 1 source "https://rubygems.org"
  4. 1 gemspec
  5. 1 group :quality do
  6. 1 gem "caliber", "~> 0.82"
  7. 1 gem "git-lint", "~> 9.0"
  8. 1 gem "reek", "~> 6.5", require: false
  9. 1 gem "simplecov", "~> 0.22", require: false
  10. end
  11. 1 group :development do
  12. 1 gem "rake", "~> 13.3"
  13. end
  14. 1 group :test do
  15. 1 gem "rspec", "~> 3.13"
  16. 1 gem "warning", "~> 1.5"
  17. end
  18. 1 group :tools do
  19. 1 gem "amazing_print", "~> 2.0"
  20. 1 gem "debug", "~> 1.11"
  21. 1 gem "irb-kit", "~> 1.1"
  22. 1 gem "repl_type_completor", "~> 0.1"
  23. end

lib/gemsmith.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rubysmith"
  3. 1 require "zeitwerk"
  4. 1 Zeitwerk::Loader.new.then do |loader|
  5. 1 loader.inflector.inflect "cli" => "CLI", "circle_ci" => "CircleCI", "rspec" => "RSpec"
  6. 1 loader.tag = File.basename __FILE__, ".rb"
  7. 1 loader.push_dir __dir__
  8. 1 loader.setup
  9. end
  10. # Main namespace.
  11. 1 module Gemsmith
  12. 1 def self.loader registry = Zeitwerk::Registry
  13. 7 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  14. end
  15. end

lib/gemsmith/../../gemsmith.gemspec

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Gem::Specification.new do |spec|
  3. 1 spec.name = "gemsmith"
  4. 1 spec.version = "23.7.0"
  5. 1 spec.authors = ["Brooke Kuhlmann"]
  6. 1 spec.email = ["brooke@alchemists.io"]
  7. 1 spec.homepage = "https://alchemists.io/projects/gemsmith"
  8. 1 spec.summary = "A command line interface for smithing Ruby gems."
  9. 1 spec.license = "Hippocratic-2.1"
  10. 1 spec.metadata = {
  11. "bug_tracker_uri" => "https://github.com/bkuhlmann/gemsmith/issues",
  12. "changelog_uri" => "https://alchemists.io/projects/gemsmith/versions",
  13. "homepage_uri" => "https://alchemists.io/projects/gemsmith",
  14. "funding_uri" => "https://github.com/sponsors/bkuhlmann",
  15. "label" => "Gemsmith",
  16. "rubygems_mfa_required" => "true",
  17. "source_code_uri" => "https://github.com/bkuhlmann/gemsmith"
  18. }
  19. 1 spec.signing_key = Gem.default_key_path
  20. 1 spec.cert_chain = [Gem.default_cert_path]
  21. 1 spec.required_ruby_version = "~> 3.4"
  22. 1 spec.add_dependency "cogger", "~> 1.0"
  23. 1 spec.add_dependency "containable", "~> 1.1"
  24. 1 spec.add_dependency "core", "~> 2.0"
  25. 1 spec.add_dependency "dry-monads", "~> 1.9"
  26. 1 spec.add_dependency "dry-schema", "~> 1.13"
  27. 1 spec.add_dependency "etcher", "~> 3.0"
  28. 1 spec.add_dependency "infusible", "~> 4.0"
  29. 1 spec.add_dependency "milestoner", "~> 19.8"
  30. 1 spec.add_dependency "refinements", "~> 13.5"
  31. 1 spec.add_dependency "rubysmith", "~> 8.7"
  32. 1 spec.add_dependency "runcom", "~> 12.0"
  33. 1 spec.add_dependency "sod", "~> 1.0"
  34. 1 spec.add_dependency "spek", "~> 4.0"
  35. 1 spec.add_dependency "zeitwerk", "~> 2.7"
  36. 1 spec.bindir = "exe"
  37. 1 spec.executables << "gemsmith"
  38. 1 spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
  39. 1 spec.files = Dir.glob ["*.gemspec", "lib/**/*"], File::FNM_DOTMATCH
  40. end

lib/gemsmith/builders/bundler.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. # Builds project skeleton with Gemfile configuration.
  6. 1 class Bundler < Rubysmith::Builders::Bundler
  7. 1 using Refinements::Struct
  8. 1 def call
  9. 8 super
  10. 8 builder.call(settings.with(template_path: "%project_name%/Gemfile.erb"))
  11. .insert_after("source", "\ngemspec\n")
  12. .replace(/spec\n\n\Z/m, "spec\n")
  13. .replace(/.+(dry-monads|refinements|zeitwerk).+/, "")
  14. .replace(/"\s+group/m, %("\n\ngroup))
  15. .replace("\n\n\n\n", "\n")
  16. 8 true
  17. end
  18. end
  19. end
  20. end

lib/gemsmith/builders/circle_ci.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. # Builds project skeleton Circle CI settings.
  6. 1 class CircleCI < Rubysmith::Builders::CircleCI
  7. 1 using Refinements::Struct
  8. 1 def call
  9. 8 else: 4 then: 4 return false unless settings.build_circle_ci
  10. 4 super
  11. 4 builder.call(settings.with(template_path: "%project_name%/.circleci/config.yml.erb"))
  12. .replace %({{checksum "Gemfile.lock"}}),
  13. %({{checksum "Gemfile"}}-{{checksum "#{project_name}.gemspec"}})
  14. 4 true
  15. end
  16. 1 private
  17. 1 def project_name = settings.project_name
  18. end
  19. end
  20. end

lib/gemsmith/builders/cli.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. # Builds project skeleton CLI templates.
  6. 1 class CLI < Rubysmith::Builders::Abstract
  7. 1 using Refinements::Struct
  8. 1 def call
  9. 27 else: 20 then: 7 return false unless settings.build_cli
  10. 20 render
  11. 20 true
  12. end
  13. 1 private
  14. 121 def render = private_methods.sort.grep(/render_/).each { |method| __send__ method }
  15. 1 def render_exe
  16. 20 builder.call(settings.with(template_path: "%project_name%/exe/%project_name%.erb"))
  17. .render
  18. .permit 0o755
  19. end
  20. 1 def render_core
  21. 20 content = settings.with template_path: "%project_name%/lib/%project_path%.rb.erb"
  22. 20 builder.call(content)
  23. .insert_before(/tag/, %( loader.inflector.inflect "cli" => "CLI"\n))
  24. end
  25. 1 def render_configuration
  26. 20 [
  27. "%project_name%/lib/%project_path%/configuration/contract.rb.erb",
  28. "%project_name%/lib/%project_path%/configuration/model.rb.erb",
  29. "%project_name%/lib/%project_path%/configuration/defaults.yml.erb",
  30. "%project_name%/lib/%project_path%/container.rb.erb",
  31. "%project_name%/lib/%project_path%/dependencies.rb.erb"
  32. 100 ].each { |path| builder.call(settings.with(template_path: path)).render }
  33. end
  34. 1 def render_shell
  35. 20 path = "%project_name%/lib/%project_path%/cli/shell.rb.erb"
  36. 20 builder.call(settings.with(template_path: path)).render
  37. end
  38. 1 def render_requirements
  39. 20 then: 19 else: 1 return if settings.build_zeitwerk
  40. 1 builder.call(settings.with(template_path: "%project_name%/lib/%project_path%.rb.erb"))
  41. .render
  42. .prepend <<~CONTENT
  43. require "demo/configuration/contract"
  44. require "demo/configuration/model"
  45. require "demo/container"
  46. require "demo/dependencies"
  47. require "demo/cli/shell"
  48. # Main namespace.
  49. CONTENT
  50. end
  51. 1 def render_specs
  52. 20 else: 10 then: 10 return unless settings.build_rspec
  53. 10 [
  54. "%project_name%/spec/lib/%project_path%/cli/shell_spec.rb.erb",
  55. "%project_name%/spec/support/shared_contexts/application_dependencies.rb.erb"
  56. 20 ].each { |path| builder.call(settings.with(template_path: path)).render }
  57. end
  58. end
  59. end
  60. end

lib/gemsmith/builders/console.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. # Builds project skeleton console for object inspection and exploration.
  6. 1 class Console < Rubysmith::Builders::Console
  7. 1 using Refinements::Struct
  8. 1 def call
  9. 10 else: 6 then: 4 return false unless settings.build_console
  10. 6 super
  11. 6 builder.call(settings.with(template_path: "%project_name%/bin/console.erb"))
  12. .replace(/require Bundler.root.+/, %(require "#{settings.project_path}"))
  13. 6 true
  14. end
  15. end
  16. end
  17. end

lib/gemsmith/builders/documentation/readme.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. 1 module Documentation
  6. # Builds project skeleton README documentation.
  7. 1 class Readme < Rubysmith::Builders::Documentation::Readme
  8. 1 using Refinements::Struct
  9. 1 def call
  10. 14 else: 10 then: 4 return false unless settings.build_readme
  11. 10 super
  12. 10 builder.call(settings.with(template_path: "%project_name%/README.#{kind}.erb"))
  13. .replace(/Setup.+Usage/m, setup)
  14. .replace("Rubysmith", "Gemsmith")
  15. .replace("rubysmith", "gemsmith")
  16. 10 true
  17. end
  18. 1 private
  19. 11 then: 6 else: 4 def setup = kind == "adoc" ? ascii_setup : markdown_setup
  20. 7 then: 4 else: 2 def ascii_setup = settings.build_security ? ascii_secure : ascii_insecure
  21. 1 def ascii_secure
  22. 4 project_name = settings.project_name
  23. 4 <<~CONTENT.strip
  24. Setup
  25. To install _with_ security, run:
  26. [source,bash]
  27. ----
  28. # 💡 Skip this line if you already have the public certificate installed.
  29. gem cert --add <(curl --compressed --location #{settings.organization_uri}/gems.pem)
  30. gem install #{project_name} --trust-policy HighSecurity
  31. ----
  32. To install _without_ security, run:
  33. [source,bash]
  34. ----
  35. gem install #{project_name}
  36. ----
  37. #{ascii_common}
  38. CONTENT
  39. end
  40. 1 def ascii_insecure
  41. 2 <<~CONTENT.strip
  42. Setup
  43. To install, run:
  44. [source,bash]
  45. ----
  46. gem install #{settings.project_name}
  47. ----
  48. #{ascii_common}
  49. CONTENT
  50. end
  51. 1 def ascii_common
  52. 6 <<~CONTENT.strip
  53. You can also add the gem directly to your project:
  54. [source,bash]
  55. ----
  56. bundle add #{settings.project_name}
  57. ----
  58. Once the gem is installed, you only need to require it:
  59. [source,ruby]
  60. ----
  61. require "#{settings.project_path}"
  62. ----
  63. == Usage
  64. CONTENT
  65. end
  66. 5 then: 2 else: 2 def markdown_setup = settings.build_security ? markdown_secure : markdown_insecure
  67. 1 def markdown_secure
  68. 2 project_name = settings.project_name
  69. 2 <<~CONTENT.strip
  70. Setup
  71. To install _with_ security, run:
  72. # 💡 Skip this line if you already have the public certificate installed.
  73. gem cert --add <(curl --compressed --location #{settings.organization_uri}/gems.pem)
  74. gem install #{project_name} --trust-policy HighSecurity
  75. To install _without_ security, run:
  76. gem install #{project_name}
  77. #{markdown_common}
  78. CONTENT
  79. end
  80. 1 def markdown_insecure
  81. 2 <<~CONTENT.strip
  82. Setup
  83. To install, run:
  84. gem install #{settings.project_name}
  85. #{markdown_common}
  86. CONTENT
  87. end
  88. 1 def markdown_common
  89. 4 <<~CONTENT.strip
  90. You can also add the gem directly to your project:
  91. bundle add #{settings.project_name}
  92. Once the gem is installed, you only need to require it:
  93. require "#{settings.project_path}"
  94. ## Usage
  95. CONTENT
  96. end
  97. end
  98. end
  99. end
  100. end

lib/gemsmith/builders/git/commit.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Gemsmith
  3. 1 module Builders
  4. 1 module Git
  5. # Builds project skeleton initial Git commit message.
  6. 1 class Commit < Rubysmith::Builders::Git::Commit
  7. 1 include Dependencies[:specification]
  8. end
  9. end
  10. end
  11. end

lib/gemsmith/builders/git/ignore.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. 1 module Git
  6. # Builds project skeleton Git ignore.
  7. 1 class Ignore < Rubysmith::Builders::Git::Ignore
  8. 1 using Refinements::Struct
  9. 1 def call
  10. 8 super
  11. 8 else: 4 then: 4 return false unless settings.build_git
  12. 4 builder.call(settings.with(template_path: "%project_name%/.gitignore.erb"))
  13. .touch
  14. .prepend("*.gem\n")
  15. .insert_before "tmp\n", <<~CONTENT
  16. Gemfile.lock
  17. pkg
  18. CONTENT
  19. 4 true
  20. end
  21. end
  22. end
  23. end
  24. end

lib/gemsmith/builders/rspec/helper.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. 1 module RSpec
  6. # Builds RSpec spec helper for project skeleton.
  7. 1 class Helper < Rubysmith::Builders::RSpec::Helper
  8. 1 using Refinements::Struct
  9. 1 def call
  10. 10 else: 6 then: 4 return false unless settings.build_rspec
  11. 6 super
  12. 6 else: 4 then: 2 return false unless settings.build_cli
  13. 4 builder.call(settings.with(template_path: "%project_name%/spec/spec_helper.rb.erb"))
  14. .replace("%r(^/spec/)", "%r((.+/container\\.rb|^/spec/))")
  15. 4 true
  16. end
  17. end
  18. end
  19. end
  20. end

lib/gemsmith/builders/specification.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/struct"
  3. 1 module Gemsmith
  4. 1 module Builders
  5. # Builds project skeleton gem specification for use by RubyGems.
  6. 1 class Specification < Rubysmith::Builders::Abstract
  7. 1 using Refinements::Struct
  8. 1 def call
  9. 14 config = settings.with template_path: "%project_name%/%project_name%.gemspec.erb"
  10. 14 builder.call(config)
  11. .render
  12. .replace(" \n", "")
  13. .replace(" ", " ")
  14. .replace(" \n", "")
  15. .replace(" spec", " spec")
  16. .replace(/\}\s+s/m, "}\n\n s")
  17. 14 true
  18. end
  19. end
  20. end
  21. end

lib/gemsmith/cli/actions/cli.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Gemsmith
  4. 1 module CLI
  5. 1 module Actions
  6. # Stores CLI flag.
  7. 1 class CLI < Sod::Action
  8. 1 include Dependencies[:settings]
  9. 1 description "Add command line interface."
  10. 1 on "--[no-]cli"
  11. 16 default { Container[:settings].build_cli }
  12. 1 def call(value) = settings.build_cli = value
  13. end
  14. end
  15. end
  16. end

lib/gemsmith/cli/actions/edit.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "sod"
  4. 1 require "spek"
  5. 1 module Gemsmith
  6. 1 module CLI
  7. 1 module Actions
  8. # Handles the edit action for editing an installed gem.
  9. 1 class Edit < Sod::Action
  10. 1 include Dependencies[:logger]
  11. 1 include Dry::Monads[:result]
  12. 1 description "Edit installed gem in default editor."
  13. 1 on %w[-e --edit], argument: "GEM"
  14. 1 def initialize(picker: Spek::Picker, editor: Tools::Editor.new, **)
  15. 14 super(**)
  16. 14 @picker = picker
  17. 14 @editor = editor
  18. end
  19. 1 def call gem_name
  20. 6 in: 1 case picker.call(gem_name).bind { |spec| editor.call spec }
  21. 2 in: 2 in Success(spec) then logger.info { "Editing: #{spec.named_version}." }
  22. 4 else: 1 in Failure(message) then log_error { message }
  23. 2 else log_error { "Unable to handle edit action." }
  24. end
  25. end
  26. 1 private
  27. 1 attr_reader :picker, :editor
  28. 1 def log_error(&) = logger.error(&)
  29. end
  30. end
  31. end
  32. end

lib/gemsmith/cli/actions/install.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 require "sod"
  5. 1 require "spek"
  6. 1 module Gemsmith
  7. 1 module CLI
  8. 1 module Actions
  9. # Handles the install action.
  10. 1 class Install < Sod::Action
  11. 1 include Dependencies[:logger]
  12. 1 include Dry::Monads[:result]
  13. 1 description "Install gem for local development."
  14. 1 ancillary "Optionally computes gem package based on current directory."
  15. 1 on %w[-i --install], argument: "[GEM]"
  16. 16 default { Pathname.pwd.basename }
  17. 1 def initialize(installer: Tools::Installer.new, loader: Spek::Loader, **)
  18. 15 super(**)
  19. 15 @installer = installer
  20. 15 @loader = loader
  21. end
  22. 1 def call name = default
  23. 5 in: 2 case installer.call loader.call("#{name}.gemspec")
  24. 4 in: 1 in Success(spec) then logger.info { "Installed: #{spec.package_name}." }
  25. 2 else: 1 in Failure(message) then log_error { message }
  26. 2 else log_error { "Unable to handle install action." }
  27. end
  28. end
  29. 1 private
  30. 1 attr_reader :installer, :loader
  31. 1 def log_error(&) = logger.error(&)
  32. end
  33. end
  34. end
  35. end

lib/gemsmith/cli/actions/publish.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 require "sod"
  5. 1 require "spek"
  6. 1 module Gemsmith
  7. 1 module CLI
  8. 1 module Actions
  9. # Handles the publish action.
  10. 1 class Publish < Sod::Action
  11. 1 include Dependencies[:logger]
  12. 1 include Dry::Monads[:result]
  13. 1 description "Publish gem to remote gem server."
  14. 1 ancillary "Optionally computes gem package based on current directory."
  15. 1 on %w[-p --publish], argument: "[GEM]"
  16. 16 default { Pathname.pwd.basename }
  17. 1 def initialize(publisher: Tools::Publisher.new, loader: Spek::Loader, **)
  18. 15 super(**)
  19. 15 @publisher = publisher
  20. 15 @loader = loader
  21. end
  22. 1 def call name = default
  23. 5 in: 2 case publisher.call loader.call("#{name}.gemspec")
  24. 4 in: 1 in Success(spec) then logger.info { "Published: #{spec.package_name}." }
  25. 1 else: 1 in Failure(message) then log_error message
  26. 1 else log_error "Publish failed, unable to parse result."
  27. end
  28. end
  29. 1 private
  30. 1 attr_reader :publisher, :loader
  31. 3 def log_error(message) = logger.error { message }
  32. end
  33. end
  34. end
  35. end

lib/gemsmith/cli/actions/view.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "sod"
  4. 1 require "spek"
  5. 1 module Gemsmith
  6. 1 module CLI
  7. 1 module Actions
  8. # Handles the view action for viewing an installed gem in default browser.
  9. 1 class View < Sod::Action
  10. 1 include Dependencies[:logger]
  11. 1 include Dry::Monads[:result]
  12. 1 description "View installed gem in default browser."
  13. 1 on %w[-V --view], argument: "GEM"
  14. 1 def initialize(picker: Spek::Picker, viewer: Tools::Viewer.new, **)
  15. 14 super(**)
  16. 14 @picker = picker
  17. 14 @viewer = viewer
  18. end
  19. 1 def call gem_name
  20. 6 in: 1 case picker.call(gem_name).bind { |spec| viewer.call spec }
  21. 2 in: 2 in Success(spec) then logger.info { "Viewing: #{spec.named_version}." }
  22. 4 else: 1 in Failure(message) then log_error { message }
  23. 2 else log_error { "Unable to handle view action." }
  24. end
  25. end
  26. 1 private
  27. 1 attr_reader :picker, :viewer
  28. 1 def log_error(&) = logger.error(&)
  29. end
  30. end
  31. end
  32. end

lib/gemsmith/cli/commands/build.rb

100.0% lines covered

100.0% branches covered

55 relevant lines. 55 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Gemsmith
  4. 1 module CLI
  5. 1 module Commands
  6. # Handles the build action.
  7. 1 class Build < Sod::Command
  8. 1 include Dependencies[:settings, :logger]
  9. # Order is important.
  10. BUILDERS = [
  11. 1 Rubysmith::Builders::Init,
  12. Rubysmith::Builders::Core,
  13. Rubysmith::Builders::Version,
  14. Builders::Specification,
  15. Rubysmith::Builders::Documentation::Readme,
  16. Builders::Documentation::Readme,
  17. Rubysmith::Builders::Documentation::Citation,
  18. Rubysmith::Builders::Documentation::License,
  19. Rubysmith::Builders::Documentation::Version,
  20. Rubysmith::Builders::Git::Setup,
  21. Builders::Git::Ignore,
  22. Rubysmith::Builders::Git::Safe,
  23. Builders::Bundler,
  24. Builders::CLI,
  25. Rubysmith::Builders::Rake::Binstub,
  26. Rubysmith::Builders::Rake::Configuration,
  27. Builders::Console,
  28. Builders::CircleCI,
  29. Rubysmith::Builders::Setup,
  30. Rubysmith::Builders::GitHub::Template,
  31. Rubysmith::Builders::GitHub::Funding,
  32. Rubysmith::Builders::GitHub::CI,
  33. Rubysmith::Builders::Reek,
  34. Rubysmith::Builders::RSpec::Binstub,
  35. Rubysmith::Builders::RSpec::Context,
  36. Builders::RSpec::Helper,
  37. Rubysmith::Builders::Caliber,
  38. Rubysmith::Builders::DevContainer::Dockerfile,
  39. Rubysmith::Builders::DevContainer::Compose,
  40. Rubysmith::Builders::DevContainer::Configuration,
  41. Rubysmith::Builders::Docker::Build,
  42. Rubysmith::Builders::Docker::Console,
  43. Rubysmith::Builders::Docker::Entrypoint,
  44. Rubysmith::Builders::Docker::File,
  45. Rubysmith::Builders::Docker::Ignore,
  46. Rubysmith::Extensions::Bundler,
  47. Rubysmith::Extensions::Pragmater,
  48. Rubysmith::Extensions::Tocer,
  49. Rubysmith::Extensions::Rubocop,
  50. Builders::Git::Commit
  51. ].freeze
  52. 1 handle "build"
  53. 1 description "Build new project."
  54. 1 on Rubysmith::CLI::Actions::Name, settings: Container[:settings]
  55. 1 on Rubysmith::CLI::Actions::AmazingPrint, settings: Container[:settings]
  56. 1 on Rubysmith::CLI::Actions::Bootsnap, settings: Container[:settings]
  57. 1 on Rubysmith::CLI::Actions::Caliber, settings: Container[:settings]
  58. 1 on Rubysmith::CLI::Actions::CircleCI, settings: Container[:settings]
  59. 1 on Rubysmith::CLI::Actions::Citation, settings: Container[:settings]
  60. 1 on Actions::CLI
  61. 1 on Rubysmith::CLI::Actions::Community, settings: Container[:settings]
  62. 1 on Rubysmith::CLI::Actions::Conduct, settings: Container[:settings]
  63. 1 on Rubysmith::CLI::Actions::Console, settings: Container[:settings]
  64. 1 on Rubysmith::CLI::Actions::Contributions, settings: Container[:settings]
  65. 1 on Rubysmith::CLI::Actions::DCOO, settings: Container[:settings]
  66. 1 on Rubysmith::CLI::Actions::Debug, settings: Container[:settings]
  67. 1 on Rubysmith::CLI::Actions::DevContainer, settings: Container[:settings]
  68. 1 on Rubysmith::CLI::Actions::Docker, settings: Container[:settings]
  69. 1 on Rubysmith::CLI::Actions::Funding, settings: Container[:settings]
  70. 1 on Rubysmith::CLI::Actions::Git, settings: Container[:settings]
  71. 1 on Rubysmith::CLI::Actions::GitHub, settings: Container[:settings]
  72. 1 on Rubysmith::CLI::Actions::GitHubCI, settings: Container[:settings]
  73. 1 on Rubysmith::CLI::Actions::GitLint, settings: Container[:settings]
  74. 1 on Rubysmith::CLI::Actions::IRBKit, settings: Container[:settings]
  75. 1 on Rubysmith::CLI::Actions::License, settings: Container[:settings]
  76. 1 on Rubysmith::CLI::Actions::Maximum, settings: Container[:settings]
  77. 1 on Rubysmith::CLI::Actions::Minimum, settings: Container[:settings]
  78. 1 on Rubysmith::CLI::Actions::Monads, settings: Container[:settings]
  79. 1 on Rubysmith::CLI::Actions::Rake, settings: Container[:settings]
  80. 1 on Rubysmith::CLI::Actions::Readme, settings: Container[:settings]
  81. 1 on Rubysmith::CLI::Actions::Reek, settings: Container[:settings]
  82. 1 on Rubysmith::CLI::Actions::Refinements, settings: Container[:settings]
  83. 1 on Rubysmith::CLI::Actions::RSpec, settings: Container[:settings]
  84. 1 on Rubysmith::CLI::Actions::RTC, settings: Container[:settings]
  85. 1 on Rubysmith::CLI::Actions::Security, settings: Container[:settings]
  86. 1 on Rubysmith::CLI::Actions::Setup, settings: Container[:settings]
  87. 1 on Rubysmith::CLI::Actions::SimpleCov, settings: Container[:settings]
  88. 1 on Rubysmith::CLI::Actions::Versions, settings: Container[:settings]
  89. 1 on Rubysmith::CLI::Actions::Zeitwerk, settings: Container[:settings]
  90. 1 def initialize(builders: BUILDERS, **)
  91. 13 super(**)
  92. 13 @builders = builders
  93. end
  94. 1 def call
  95. 6 log_info "Building project skeleton: #{settings.project_name}..."
  96. 168 builders.each { |builder| builder.new(settings:, logger:).call }
  97. 6 log_info "Project skeleton complete!"
  98. end
  99. 1 private
  100. 1 attr_reader :builders
  101. 13 def log_info(message) = logger.info { message }
  102. end
  103. end
  104. end
  105. end

lib/gemsmith/cli/shell.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Gemsmith
  4. 1 module CLI
  5. # The main Command Line Interface (CLI) object.
  6. 1 class Shell
  7. 1 include Dependencies[:defaults_path, :xdg_config, :specification]
  8. 1 def initialize(context: Sod::Context, dsl: Sod, **)
  9. 11 super(**)
  10. 11 @context = context
  11. 11 @dsl = dsl
  12. end
  13. 1 def call(...) = cli.call(...)
  14. 1 private
  15. 1 attr_reader :context, :dsl
  16. 1 def cli
  17. 11 context = build_context
  18. 11 dsl.new :gemsmith, banner: specification.banner do
  19. 11 on(Sod::Prefabs::Commands::Config, context:)
  20. 11 on Commands::Build
  21. 11 on Actions::Install
  22. 11 on Actions::Publish
  23. 11 on Actions::Edit
  24. 11 on Actions::View
  25. 11 on(Sod::Prefabs::Actions::Version, context:)
  26. 11 on Sod::Prefabs::Actions::Help, self
  27. end
  28. end
  29. 1 def build_context
  30. 11 context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
  31. end
  32. end
  33. end
  34. end

lib/gemsmith/dependencies.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "infusible"
  3. 1 module Gemsmith
  4. 1 Dependencies = Infusible[Container]
  5. end

lib/gemsmith/tools/cleaner.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/pathname"
  4. 1 module Gemsmith
  5. 1 module Tools
  6. # Cleans gem artifacts.
  7. 1 class Cleaner
  8. 1 include Dry::Monads[:result]
  9. 1 using Refinements::Pathname
  10. 1 def initialize root_dir: Pathname.pwd
  11. 5 @root_dir = root_dir
  12. end
  13. 1 def call specification
  14. 5 root_dir.join("pkg").rmtree
  15. 5 root_dir.files("**/*.gem").each(&:delete)
  16. 5 Success specification
  17. end
  18. 1 private
  19. 1 attr_reader :root_dir
  20. end
  21. end
  22. end

lib/gemsmith/tools/editor.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Gemsmith
  4. 1 module Tools
  5. # Edits a gem within default editor.
  6. 1 class Editor
  7. 1 include Dependencies[:executor, :environment]
  8. 1 include Dry::Monads[:result]
  9. 1 def call specification
  10. 2 executor.capture3(client, specification.source_path.to_s).then do |_stdout, stderr, status|
  11. 2 then: 1 else: 1 status.success? ? Success(specification) : Failure(stderr)
  12. end
  13. end
  14. 1 private
  15. 1 def client = environment.fetch("EDITOR")
  16. end
  17. end
  18. end

lib/gemsmith/tools/installer.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Gemsmith
  4. 1 module Tools
  5. # Installs a locally built gem.
  6. 1 class Installer
  7. 1 include Dependencies[:executor]
  8. 1 include Dry::Monads[:result, :do]
  9. # Order matters.
  10. 1 STEPS = [Tools::Cleaner.new, Tools::Packager.new].freeze
  11. 1 def initialize(steps: STEPS, **)
  12. 15 super(**)
  13. 15 @steps = steps
  14. end
  15. 1 def call specification
  16. 13 steps.each { |step| yield step.call(specification) }
  17. 3 run specification
  18. end
  19. 1 private
  20. 1 attr_reader :steps
  21. 1 def run specification
  22. 3 path = specification.package_path
  23. 3 executor.capture3("gem", "install", path.to_s).then do |_stdout, _stderr, status|
  24. 3 then: 2 else: 1 status.success? ? Success(specification) : Failure("Unable to install: #{path}.")
  25. end
  26. end
  27. end
  28. end
  29. end

lib/gemsmith/tools/packager.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/pathname"
  4. 1 require "rubygems/command_manager"
  5. 1 module Gemsmith
  6. 1 module Tools
  7. # Builds a gem package for distribution.
  8. 1 class Packager
  9. 1 include Dry::Monads[:result]
  10. 1 using Refinements::Pathname
  11. 1 def initialize command: Gem::CommandManager.new
  12. 6 @command = command
  13. end
  14. # :reek:TooManyStatements
  15. 1 def call specification
  16. 6 command.run ["build", "#{specification.name}.gemspec"]
  17. 4 specification.package_path.then { |path| path.make_ancestors.basename.copy path.parent }
  18. 2 Success specification
  19. rescue Gem::Exception => error
  20. 2 Failure error.message
  21. end
  22. 1 private
  23. 1 attr_reader :command
  24. end
  25. end
  26. end

lib/gemsmith/tools/publisher.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Gemsmith
  4. 1 module Tools
  5. # Handles the publication of a gem version.
  6. 1 class Publisher
  7. 1 include Dry::Monads[:result, :do]
  8. # Order matters.
  9. STEPS = [
  10. 1 Tools::Cleaner.new,
  11. Tools::Validator.new,
  12. Tools::Packager.new,
  13. Tools::Versioner.new,
  14. Tools::Pusher.new
  15. ].freeze
  16. 1 def initialize steps: STEPS
  17. 13 @steps = steps
  18. end
  19. 1 def call specification
  20. 9 steps.each { |step| yield step.call(specification) }
  21. 1 Success specification
  22. end
  23. 1 private
  24. 1 attr_reader :steps
  25. end
  26. end
  27. end

lib/gemsmith/tools/pusher.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "rubygems/command_manager"
  4. 1 module Gemsmith
  5. 1 module Tools
  6. # Pushes a gem package to remote gem server.
  7. 1 class Pusher
  8. 1 include Dependencies[:executor, :logger]
  9. 1 include Dry::Monads[:result]
  10. 1 def initialize(command: Gem::CommandManager.new, **)
  11. 12 super(**)
  12. 12 @command = command
  13. end
  14. 1 def call specification
  15. 11 command.run ["push", specification.package_path.to_s, *one_time_password]
  16. 10 Success specification
  17. rescue Gem::Exception => error
  18. 1 Failure error.message
  19. end
  20. 1 private
  21. 1 attr_reader :command
  22. # :reek:TooManyStatements
  23. 1 def one_time_password
  24. 11 then: 3 else: 5 return Core::EMPTY_ARRAY if check_yubikey.failure?
  25. 5 executor.capture3(check_yubikey.success, "oath", "accounts", "code", "--single", "RubyGems")
  26. 5 then: 3 else: 2 .then { |stdout, _stderr, status| status.success? ? ["--otp", stdout.chomp] : [] }
  27. rescue Errno::ENOENT => error
  28. 6 logger.debug { "Unable to obtain YubiKey One-Time Password. #{error}." }
  29. 3 Core::EMPTY_ARRAY
  30. end
  31. 1 def check_yubikey
  32. 16 executor.capture3("command", "-v", "ykman")
  33. .then do |stdout, stderr, status|
  34. 13 then: 8 if status.success?
  35. 8 Success stdout.chomp
  36. else: 5 else
  37. 10 logger.debug { "Unable to find YubiKey Manager. #{stderr}." }
  38. 5 Failure()
  39. end
  40. end
  41. end
  42. end
  43. end
  44. end

lib/gemsmith/tools/validator.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Gemsmith
  4. 1 module Tools
  5. # Validates whether a gem can be published or not.
  6. 1 class Validator
  7. 1 include Dependencies[:executor]
  8. 1 include Dry::Monads[:result]
  9. 1 def call specification
  10. 3 executor.capture3("git", "status", "--porcelain").then do |_stdout, _stderr, status|
  11. 3 then: 2 else: 1 status.success? ? Success(specification) : Failure("Project has uncommitted changes.")
  12. end
  13. end
  14. end
  15. end
  16. end

lib/gemsmith/tools/versioner.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "milestoner"
  4. 1 module Gemsmith
  5. 1 module Tools
  6. # Versions (tags: local and remote) current project.
  7. 1 class Versioner
  8. 1 include Dry::Monads[:result]
  9. 1 def initialize(
  10. publisher: Milestoner::Tags::Publisher.new,
  11. model: Milestoner::Configuration::Model,
  12. **
  13. )
  14. 4 super(**)
  15. 4 @publisher = publisher
  16. 4 @model = model
  17. end
  18. 3 def call(specification) = publisher.call(specification.version).fmap { specification }
  19. 1 private
  20. 1 attr_reader :publisher, :model
  21. end
  22. end
  23. end

lib/gemsmith/tools/viewer.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Gemsmith
  4. 1 module Tools
  5. # Views a gem within default browser.
  6. 1 class Viewer
  7. 1 include Dependencies[:executor]
  8. 1 include Dry::Monads[:result]
  9. 1 def call specification
  10. 2 executor.capture3("open", specification.homepage_url).then do |_stdout, stderr, status|
  11. 2 then: 1 else: 1 status.success? ? Success(specification) : Failure(stderr)
  12. end
  13. end
  14. end
  15. end
  16. end