├── .ruby-version ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── lib ├── gemsmith │ ├── templates │ │ └── %project_name% │ │ │ ├── lib │ │ │ └── %project_path% │ │ │ │ ├── configuration │ │ │ │ ├── defaults.yml.erb │ │ │ │ ├── model.rb.erb │ │ │ │ └── contract.rb.erb │ │ │ │ ├── dependencies.rb.erb │ │ │ │ ├── cli │ │ │ │ └── shell.rb.erb │ │ │ │ └── container.rb.erb │ │ │ ├── exe │ │ │ └── %project_name%.erb │ │ │ ├── spec │ │ │ ├── support │ │ │ │ └── shared_contexts │ │ │ │ │ └── application_dependencies.rb.erb │ │ │ └── lib │ │ │ │ └── %project_path% │ │ │ │ └── cli │ │ │ │ └── shell_spec.rb.erb │ │ │ └── %project_name%.gemspec.erb │ ├── dependencies.rb │ ├── builders │ │ ├── git │ │ │ ├── commit.rb │ │ │ └── ignore.rb │ │ ├── console.rb │ │ ├── rspec │ │ │ └── helper.rb │ │ ├── bundler.rb │ │ ├── circle_ci.rb │ │ ├── specification.rb │ │ ├── cli.rb │ │ └── documentation │ │ │ └── readme.rb │ ├── cli │ │ ├── actions │ │ │ ├── cli.rb │ │ │ ├── edit.rb │ │ │ ├── view.rb │ │ │ ├── install.rb │ │ │ └── publish.rb │ │ ├── shell.rb │ │ └── commands │ │ │ └── build.rb │ ├── tools │ │ ├── viewer.rb │ │ ├── validator.rb │ │ ├── editor.rb │ │ ├── cleaner.rb │ │ ├── versioner.rb │ │ ├── publisher.rb │ │ ├── packager.rb │ │ ├── installer.rb │ │ └── pusher.rb │ └── container.rb └── gemsmith.rb ├── exe └── gemsmith ├── bin ├── rake ├── rspec ├── rubocop ├── console └── setup ├── .reek.yml ├── spec ├── support │ ├── fixtures │ │ ├── gemsmith-test.gemspec │ │ ├── spec │ │ │ ├── support │ │ │ │ └── shared_contexts │ │ │ │ │ └── application_dependencies.rb │ │ │ └── lib │ │ │ │ └── cli │ │ │ │ ├── shell_without_refinements_proof.rb │ │ │ │ ├── shell_with_refinements_proof.rb │ │ │ │ └── shell_dash_proof.rb │ │ ├── arguments │ │ │ ├── maximum.txt │ │ │ └── minimum.txt │ │ ├── test-minimum.gemspec │ │ ├── test-minimum-monads.gemspec │ │ ├── test-minimum-security.gemspec │ │ ├── readmes │ │ │ ├── minimum.md │ │ │ ├── minimum.adoc │ │ │ ├── maximum.md │ │ │ └── maximum.adoc │ │ ├── lib │ │ │ ├── container.rb │ │ │ └── cli │ │ │ │ └── shell.rb │ │ ├── test-minimum-cli.gemspec │ │ ├── boms │ │ │ └── maximum.txt │ │ └── test-maximum.gemspec │ ├── settings.yml │ └── shared_contexts │ │ └── application_dependencies.rb ├── lib │ ├── gemsmith_spec.rb │ └── gemsmith │ │ ├── cli │ │ ├── actions │ │ │ ├── cli_spec.rb │ │ │ ├── edit_spec.rb │ │ │ ├── view_spec.rb │ │ │ ├── install_spec.rb │ │ │ └── publish_spec.rb │ │ ├── commands │ │ │ └── build_spec.rb │ │ └── shell_spec.rb │ │ ├── tools │ │ ├── editor_spec.rb │ │ ├── viewer_spec.rb │ │ ├── validator_spec.rb │ │ ├── publisher_spec.rb │ │ ├── versioner_spec.rb │ │ ├── cleaner_spec.rb │ │ ├── installer_spec.rb │ │ ├── packager_spec.rb │ │ └── pusher_spec.rb │ │ └── builders │ │ ├── git │ │ └── ignore_spec.rb │ │ ├── rspec │ │ └── helper_spec.rb │ │ ├── console_spec.rb │ │ ├── bundler_spec.rb │ │ ├── circle_ci_spec.rb │ │ ├── documentation │ │ └── readme_spec.rb │ │ ├── specification_spec.rb │ │ └── cli_spec.rb └── spec_helper.rb ├── Rakefile ├── Gemfile ├── CITATION.cff ├── .circleci └── config.yml ├── .config └── rubocop │ └── config.yml ├── gemsmith.gemspec ├── LICENSE.adoc └── README.adoc /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.8 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bkuhlmann] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg 5 | tmp 6 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/defaults.yml.erb: -------------------------------------------------------------------------------- 1 | todo: "Add your own attributes here." 2 | -------------------------------------------------------------------------------- /exe/gemsmith: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "gemsmith" 5 | 6 | Gemsmith::CLI::Shell.new.call 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rake", "rake" 7 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rspec-core", "rspec" 7 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rubocop", "rubocop" 7 | -------------------------------------------------------------------------------- /lib/gemsmith/dependencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "infusible" 4 | 5 | module Gemsmith 6 | Dependencies = Infusible[Container] 7 | end 8 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/dependencies.rb.erb: -------------------------------------------------------------------------------- 1 | require "infusible" 2 | 3 | <% namespace do %> 4 | Dependencies = Infusible[Container] 5 | <% end %> 6 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/exe/%project_name%.erb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require "<%= settings.project_path %>" 4 | 5 | <%= settings.project_namespaced_class %>::CLI::Shell.new.call 6 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - tmp 3 | - vendor 4 | 5 | detectors: 6 | LongParameterList: 7 | enabled: false 8 | TooManyStatements: 9 | exclude: 10 | - "Gemsmith::CLI::Shell#cli" 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | Bundler.require :tools 6 | 7 | require "gemsmith" 8 | require "irb" 9 | 10 | IRB.start __FILE__ 11 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/model.rb.erb: -------------------------------------------------------------------------------- 1 | <% namespace do %> 2 | module Configuration 3 | # Defines the configuration model for use throughout the gem. 4 | Model = Struct.new 5 | end 6 | <% end %> 7 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/contract.rb.erb: -------------------------------------------------------------------------------- 1 | require "dry/schema" 2 | 3 | Dry::Schema.load_extensions :monads 4 | 5 | <% namespace do %> 6 | module Configuration 7 | Contract = Dry::Schema.Params 8 | end 9 | <% end %> 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community 4 | url: https://alchemists.io/community 5 | about: Please ask questions or discuss specifics here. 6 | - name: Security 7 | url: https://alchemists.io/policies/security 8 | about: Please report security vulnerabilities here. 9 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/git/commit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gemsmith 4 | module Builders 5 | module Git 6 | # Builds project skeleton initial Git commit message. 7 | class Commit < Rubysmith::Builders::Git::Commit 8 | include Dependencies[:specification] 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/fixtures/gemsmith-test.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "gemsmith-test" 5 | spec.version = "0.0.0" 6 | spec.authors = ["Gemsmith Tester"] 7 | spec.summary = "A test gem." 8 | spec.email = ["gemsmith@example.com"] 9 | spec.license = "MIT" 10 | spec.metadata = {"rubygems_mfa_required" => "true"} 11 | end 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | 4 | ## Screenshots/Screencasts 5 | 6 | 7 | ## Details 8 | 9 | -------------------------------------------------------------------------------- /spec/support/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :gem_platform: Gem::Platform::CURRENT 3 | :author_name: "Testy Tester" 4 | :author_email: "test@test.com" 5 | :author_uri: "https://www.test.com" 6 | :gem_url: "https://www.gem.com" 7 | :company_name: "ACME" 8 | :company_url: "https://www.acme.com" 9 | :github_user: "tester" 10 | :year: 1920 11 | :ruby_version: 2.0.0 12 | :rails_version: 4.0.0 13 | :post_install_message: "Follow @tester on Twitter for more info." 14 | -------------------------------------------------------------------------------- /spec/lib/gemsmith_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith do 6 | describe ".loader" do 7 | it "eager loads" do 8 | expectation = proc { described_class.loader.eager_load force: true } 9 | expect(&expectation).not_to raise_error 10 | end 11 | 12 | it "answers unique tag" do 13 | expect(described_class.loader.tag).to eq("gemsmith") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "git/lint/rake/register" 5 | require "reek/rake/task" 6 | require "rspec/core/rake_task" 7 | require "rubocop/rake_task" 8 | 9 | Git::Lint::Rake::Register.call 10 | Reek::Rake::Task.new 11 | RSpec::Core::RakeTask.new { |task| task.verbose = false } 12 | RuboCop::RakeTask.new 13 | 14 | desc "Run code quality checks" 15 | task quality: %i[git_lint reek rubocop] 16 | 17 | task default: %i[quality spec] 18 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "debug" 5 | require "fileutils" 6 | require "pathname" 7 | 8 | APP_ROOT = Pathname(__dir__).join("..").expand_path 9 | 10 | Runner = lambda do |*arguments, kernel: Kernel| 11 | kernel.system(*arguments) || kernel.abort("\nERROR: Command #{arguments.inspect} failed.") 12 | end 13 | 14 | FileUtils.chdir APP_ROOT do 15 | puts "Installing dependencies..." 16 | Runner.call "bundle install" 17 | end 18 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/actions/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Gemsmith 6 | module CLI 7 | module Actions 8 | # Stores CLI flag. 9 | class CLI < Sod::Action 10 | include Dependencies[:settings] 11 | 12 | description "Add command line interface." 13 | 14 | on "--[no-]cli" 15 | 16 | default { Container[:settings].build_cli } 17 | 18 | def call(value) = settings.build_cli = value 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/fixtures/spec/support/shared_contexts/application_dependencies.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context "with application dependencies" do 2 | using Refinements::Struct 3 | 4 | let(:settings) { Test::Container[:settings] } 5 | let(:logger) { Cogger.new id: "test", io: StringIO.new, level: :debug } 6 | let(:io) { StringIO.new } 7 | 8 | before do 9 | settings.with! Etcher.call(Test::Container[:registry].remove_loader(1)) 10 | Test::Container.stub! logger:, io: 11 | end 12 | 13 | after { Test::Container.restore } 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/fixtures/arguments/maximum.txt: -------------------------------------------------------------------------------- 1 | build 2 | --name 3 | test 4 | --amazing_print 5 | --bootsnap 6 | --caliber 7 | --circle_ci 8 | --citation 9 | --cli 10 | --community 11 | --conduct 12 | --console 13 | --contributions 14 | --dcoo 15 | --debug 16 | --devcontainer 17 | --docker 18 | --funding 19 | --git 20 | --git_hub 21 | --git_hub_ci 22 | --git-lint 23 | --irb-kit 24 | --license 25 | --rake 26 | --readme 27 | --reek 28 | --refinements 29 | --rspec 30 | --rtc 31 | --security 32 | --setup 33 | --simple_cov 34 | --versions 35 | --zeitwerk 36 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/viewer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | module Gemsmith 6 | module Tools 7 | # Views a gem within default browser. 8 | class Viewer 9 | include Dependencies[:executor] 10 | include Dry::Monads[:result] 11 | 12 | def call specification 13 | executor.capture3("open", specification.homepage_url).then do |_stdout, stderr, status| 14 | status.success? ? Success(specification) : Failure(stderr) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gemsmith.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubysmith" 4 | require "zeitwerk" 5 | 6 | Zeitwerk::Loader.new.then do |loader| 7 | loader.inflector.inflect "cli" => "CLI", "circle_ci" => "CircleCI", "rspec" => "RSpec" 8 | loader.tag = File.basename __FILE__, ".rb" 9 | loader.push_dir __dir__ 10 | loader.setup 11 | end 12 | 13 | # Main namespace. 14 | module Gemsmith 15 | def self.loader registry = Zeitwerk::Registry 16 | @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/fixtures/test-minimum.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "test" 3 | spec.version = "0.0.0" 4 | spec.authors = ["Jill Smith"] 5 | spec.email = ["jill@acme.io"] 6 | spec.homepage = "" 7 | spec.summary = "" 8 | spec.license = "Hippocratic-2.1" 9 | 10 | spec.metadata = { 11 | "label" => "Test", 12 | "rubygems_mfa_required" => "true" 13 | } 14 | 15 | spec.required_ruby_version = ">= 3.4" 16 | 17 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 18 | spec.files = Dir["*.gemspec", "lib/**/*"] 19 | end 20 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/actions/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::CLI::Actions::CLI do 6 | subject(:action) { described_class.new } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe "#call" do 11 | it "answers true when true" do 12 | action.call true 13 | expect(settings.build_cli).to be(true) 14 | end 15 | 16 | it "answers false when false" do 17 | action.call false 18 | expect(settings.build_cli).to be(false) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | module Gemsmith 6 | module Tools 7 | # Validates whether a gem can be published or not. 8 | class Validator 9 | include Dependencies[:executor] 10 | include Dry::Monads[:result] 11 | 12 | def call specification 13 | executor.capture3("git", "status", "--porcelain").then do |_stdout, _stderr, status| 14 | status.success? ? Success(specification) : Failure("Project has uncommitted changes.") 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/fixtures/test-minimum-monads.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "test" 3 | spec.version = "0.0.0" 4 | spec.authors = ["Jill Smith"] 5 | spec.email = ["jill@acme.io"] 6 | spec.homepage = "" 7 | spec.summary = "" 8 | spec.license = "Hippocratic-2.1" 9 | 10 | spec.metadata = { 11 | "label" => "Test", 12 | "rubygems_mfa_required" => "true" 13 | } 14 | 15 | spec.required_ruby_version = ">= 3.4" 16 | spec.add_dependency "dry-monads", "~> 1.9" 17 | 18 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 19 | spec.files = Dir["*.gemspec", "lib/**/*"] 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/fixtures/arguments/minimum.txt: -------------------------------------------------------------------------------- 1 | build 2 | --name 3 | test 4 | --no-amazing_print 5 | --no-bootsnap 6 | --no-caliber 7 | --no-circle_ci 8 | --no-citation 9 | --no-cli 10 | --no-community 11 | --no-conduct 12 | --no-console 13 | --no-contributions 14 | --no-dcoo 15 | --no-debug 16 | --no-devcontainer 17 | --no-docker 18 | --no-funding 19 | --no-git 20 | --no-git_hub 21 | --no-git_hub_ci 22 | --no-git-lint 23 | --no-irb-kit 24 | --no-license 25 | --no-rake 26 | --no-readme 27 | --no-reek 28 | --no-refinements 29 | --no-rspec 30 | --no-rtc 31 | --no-security 32 | --no-setup 33 | --no-simple_cov 34 | --no-versions 35 | --no-zeitwerk 36 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/editor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | module Gemsmith 6 | module Tools 7 | # Edits a gem within default editor. 8 | class Editor 9 | include Dependencies[:executor, :environment] 10 | include Dry::Monads[:result] 11 | 12 | def call specification 13 | executor.capture3(client, specification.source_path.to_s).then do |_stdout, stderr, status| 14 | status.success? ? Success(specification) : Failure(stderr) 15 | end 16 | end 17 | 18 | private 19 | 20 | def client = environment.fetch("EDITOR") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ruby file: ".ruby-version" 4 | 5 | source "https://rubygems.org" 6 | 7 | gemspec 8 | 9 | group :quality do 10 | gem "caliber", "~> 0.82" 11 | gem "git-lint", "~> 9.0" 12 | gem "reek", "~> 6.5", require: false 13 | gem "simplecov", "~> 0.22", require: false 14 | end 15 | 16 | group :development do 17 | gem "rake", "~> 13.3" 18 | end 19 | 20 | group :test do 21 | gem "rspec", "~> 3.13" 22 | gem "warning", "~> 1.5" 23 | end 24 | 25 | group :tools do 26 | gem "amazing_print", "~> 2.0" 27 | gem "debug", "~> 1.11" 28 | gem "irb-kit", "~> 1.1" 29 | gem "repl_type_completor", "~> 0.1" 30 | end 31 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/console.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | # Builds project skeleton console for object inspection and exploration. 8 | class Console < Rubysmith::Builders::Console 9 | using Refinements::Struct 10 | 11 | def call 12 | return false unless settings.build_console 13 | 14 | super 15 | builder.call(settings.with(template_path: "%project_name%/bin/console.erb")) 16 | .replace(/require Bundler.root.+/, %(require "#{settings.project_path}")) 17 | 18 | true 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/fixtures/test-minimum-security.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "test" 3 | spec.version = "0.0.0" 4 | spec.authors = ["Jill Smith"] 5 | spec.email = ["jill@acme.io"] 6 | spec.homepage = "" 7 | spec.summary = "" 8 | spec.license = "Hippocratic-2.1" 9 | 10 | spec.metadata = { 11 | "label" => "Test", 12 | "rubygems_mfa_required" => "true" 13 | } 14 | 15 | spec.signing_key = Gem.default_key_path 16 | spec.cert_chain = [Gem.default_cert_path] 17 | 18 | spec.required_ruby_version = ">= 3.4" 19 | 20 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 21 | spec.files = Dir["*.gemspec", "lib/**/*"] 22 | end 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | title: "Add|Update|Fix|Remove|Refactor " 4 | about: Report an issue. Please use only one of the subject prefixes. 5 | --- 6 | 7 | 10 | 11 | ## Why 12 | 13 | 14 | ## How 15 | 16 | 17 | ## Notes 18 | 19 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "refinements/pathname" 5 | 6 | module Gemsmith 7 | module Tools 8 | # Cleans gem artifacts. 9 | class Cleaner 10 | include Dry::Monads[:result] 11 | 12 | using Refinements::Pathname 13 | 14 | def initialize root_dir: Pathname.pwd 15 | @root_dir = root_dir 16 | end 17 | 18 | def call specification 19 | root_dir.join("pkg").rmtree 20 | root_dir.files("**/*.gem").each(&:delete) 21 | Success specification 22 | end 23 | 24 | private 25 | 26 | attr_reader :root_dir 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: Please use the following metadata when citing this project in your work. 3 | title: Gemsmith 4 | abstract: A command line interface for smithing Ruby gems. 5 | version: 23.8.0 6 | license: Hippocratic-2.1 7 | date-released: 2025-11-07 8 | authors: 9 | - family-names: Kuhlmann 10 | given-names: Brooke 11 | affiliation: Alchemists 12 | orcid: https://orcid.org/0000-0002-5810-6268 13 | keywords: 14 | - ruby 15 | - gems 16 | - command line interface 17 | - generator 18 | repository-code: https://github.com/bkuhlmann/gemsmith 19 | repository-artifact: https://rubygems.org/gems/gemsmith 20 | url: https://alchemists.io/projects/gemsmith 21 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/commands/build_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::CLI::Commands::Build do 6 | using Refinements::Pathname 7 | 8 | subject(:command) { described_class.new builders: [Rubysmith::Builders::Version] } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | it "builds skeleton" do 14 | temp_dir.change_dir { command.call } 15 | expect(temp_dir.join("test/.ruby-version").exist?).to be(true) 16 | end 17 | 18 | it "logs information" do 19 | temp_dir.change_dir { command.call } 20 | expect(logger.reread).to match(%r(🟢.+Rendering: test/.ruby-version)) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/versioner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "milestoner" 5 | 6 | module Gemsmith 7 | module Tools 8 | # Versions (tags: local and remote) current project. 9 | class Versioner 10 | include Dry::Monads[:result] 11 | 12 | def initialize( 13 | publisher: Milestoner::Tags::Publisher.new, 14 | model: Milestoner::Configuration::Model, 15 | ** 16 | ) 17 | super(**) 18 | @publisher = publisher 19 | @model = model 20 | end 21 | 22 | def call(specification) = publisher.call(specification.version).fmap { specification } 23 | 24 | private 25 | 26 | attr_reader :publisher, :model 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/rspec/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | module RSpec 8 | # Builds RSpec spec helper for project skeleton. 9 | class Helper < Rubysmith::Builders::RSpec::Helper 10 | using Refinements::Struct 11 | 12 | def call 13 | return false unless settings.build_rspec 14 | 15 | super 16 | 17 | return false unless settings.build_cli 18 | 19 | builder.call(settings.with(template_path: "%project_name%/spec/spec_helper.rb.erb")) 20 | .replace("%r(^/spec/)", "%r((.+/container\\.rb|^/spec/))") 21 | 22 | true 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/bundler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | # Builds project skeleton with Gemfile configuration. 8 | class Bundler < Rubysmith::Builders::Bundler 9 | using Refinements::Struct 10 | 11 | def call 12 | super 13 | 14 | builder.call(settings.with(template_path: "%project_name%/Gemfile.erb")) 15 | .insert_after("source", "\ngemspec\n") 16 | .replace(/spec\n\n\Z/m, "spec\n") 17 | .replace(/.+(dry-monads|refinements|zeitwerk).+/, "") 18 | .replace(/"\s+group/m, %("\n\ngroup)) 19 | .replace("\n\n\n\n", "\n") 20 | 21 | true 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/circle_ci.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | # Builds project skeleton Circle CI settings. 8 | class CircleCI < Rubysmith::Builders::CircleCI 9 | using Refinements::Struct 10 | 11 | def call 12 | return false unless settings.build_circle_ci 13 | 14 | super 15 | builder.call(settings.with(template_path: "%project_name%/.circleci/config.yml.erb")) 16 | .replace %({{checksum "Gemfile.lock"}}), 17 | %({{checksum "Gemfile"}}-{{checksum "#{project_name}.gemspec"}}) 18 | 19 | true 20 | end 21 | 22 | private 23 | 24 | def project_name = settings.project_name 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/specification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | # Builds project skeleton gem specification for use by RubyGems. 8 | class Specification < Rubysmith::Builders::Abstract 9 | using Refinements::Struct 10 | 11 | def call 12 | config = settings.with template_path: "%project_name%/%project_name%.gemspec.erb" 13 | 14 | builder.call(config) 15 | .render 16 | .replace(" \n", "") 17 | .replace(" ", " ") 18 | .replace(" \n", "") 19 | .replace(" spec", " spec") 20 | .replace(/\}\s+s/m, "}\n\n s") 21 | 22 | true 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/git/ignore.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | module Git 8 | # Builds project skeleton Git ignore. 9 | class Ignore < Rubysmith::Builders::Git::Ignore 10 | using Refinements::Struct 11 | 12 | def call 13 | super 14 | 15 | return false unless settings.build_git 16 | 17 | builder.call(settings.with(template_path: "%project_name%/.gitignore.erb")) 18 | .touch 19 | .prepend("*.gem\n") 20 | .insert_before "tmp\n", <<~CONTENT 21 | Gemfile.lock 22 | pkg 23 | CONTENT 24 | 25 | true 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/publisher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | module Gemsmith 6 | module Tools 7 | # Handles the publication of a gem version. 8 | class Publisher 9 | include Dry::Monads[:result, :do] 10 | 11 | # Order matters. 12 | STEPS = [ 13 | Tools::Cleaner.new, 14 | Tools::Validator.new, 15 | Tools::Packager.new, 16 | Tools::Versioner.new, 17 | Tools::Pusher.new 18 | ].freeze 19 | 20 | def initialize steps: STEPS 21 | @steps = steps 22 | end 23 | 24 | def call specification 25 | steps.each { |step| yield step.call(specification) } 26 | Success specification 27 | end 28 | 29 | private 30 | 31 | attr_reader :steps 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/editor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Editor do 6 | subject(:editor) { described_class.new } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe "#call" do 11 | it "answers specification when success" do 12 | result = editor.call specification 13 | expect(result.success).to eq(specification) 14 | end 15 | 16 | context "when failure" do 17 | let :executor do 18 | class_double Open3, 19 | capture3: ["", "Error.", instance_double(Process::Status, success?: false)] 20 | end 21 | 22 | it "answers standard error" do 23 | result = editor.call specification 24 | expect(result.failure).to eq("Error.") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/viewer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Viewer do 6 | subject(:viewer) { described_class.new } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe "#call" do 11 | it "answers specification when success" do 12 | result = viewer.call specification 13 | expect(result.success).to eq(specification) 14 | end 15 | 16 | context "with failure" do 17 | let :executor do 18 | class_double Open3, 19 | capture3: ["", "Error.", instance_double(Process::Status, success?: false)] 20 | end 21 | 22 | it "answers standard error" do 23 | result = viewer.call specification 24 | expect(result.failure).to eq("Error.") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Validator do 6 | subject(:validator) { described_class.new } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe "#call" do 11 | it "answers specification when success" do 12 | result = validator.call specification 13 | expect(result.success).to eq(specification) 14 | end 15 | 16 | context "when invalid" do 17 | let :executor do 18 | class_double Open3, capture3: ["", "", instance_double(Process::Status, success?: false)] 19 | end 20 | 21 | it "answers failure" do 22 | result = validator.call specification 23 | expect(result.failure).to eq("Project has uncommitted changes.") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/fixtures/spec/lib/cli/shell_without_refinements_proof.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Test::CLI::Shell do 4 | 5 | subject(:shell) { described_class.new } 6 | 7 | include_context "with application dependencies" 8 | 9 | before { Sod::Container.stub! logger:, io: } 10 | 11 | after { Sod::Container.restore } 12 | 13 | describe "#call" do 14 | it "prints configuration usage" do 15 | shell.call %w[config] 16 | expect(io.tap(&:rewind).read).to match(/Manage configuration.+/m) 17 | end 18 | 19 | it "prints version" do 20 | shell.call %w[--version] 21 | expect(io.tap(&:rewind).read).to match(/Test\s\d+\.\d+\.\d+/) 22 | end 23 | 24 | it "prints help" do 25 | shell.call %w[--help] 26 | expect(io.tap(&:rewind).read).to match(/Test.+USAGE.+/m) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/fixtures/spec/lib/cli/shell_with_refinements_proof.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Test::CLI::Shell do 4 | using Refinements::Pathname 5 | using Refinements::StringIO 6 | 7 | subject(:shell) { described_class.new } 8 | 9 | include_context "with application dependencies" 10 | 11 | before { Sod::Container.stub! logger:, io: } 12 | 13 | after { Sod::Container.restore } 14 | 15 | describe "#call" do 16 | it "prints configuration usage" do 17 | shell.call %w[config] 18 | expect(io.reread).to match(/Manage configuration.+/m) 19 | end 20 | 21 | it "prints version" do 22 | shell.call %w[--version] 23 | expect(io.reread).to match(/Test\s\d+\.\d+\.\d+/) 24 | end 25 | 26 | it "prints help" do 27 | shell.call %w[--help] 28 | expect(io.reread).to match(/Test.+USAGE.+/m) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/spec/support/shared_contexts/application_dependencies.rb.erb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context "with application dependencies" do 2 | <% if settings.build_refinements %> 3 | using Refinements::Struct 4 | <% end %> 5 | 6 | let(:settings) { <%= settings.project_namespaced_class %>::Container[:settings] } 7 | let(:logger) { Cogger.new id: "<%= settings.project_name %>", io: StringIO.new, level: :debug } 8 | let(:io) { StringIO.new } 9 | 10 | <% if settings.build_refinements %> 11 | before do 12 | settings.with! Etcher.call(<%= settings.project_namespaced_class %>::Container[:registry].remove_loader(1)) 13 | <%= settings.project_namespaced_class %>::Container.stub! logger:, io: 14 | end 15 | <% else %> 16 | before { Demo::Container.stub! logger:, io: } 17 | <% end %> 18 | 19 | after { <%= settings.project_namespaced_class %>::Container.restore } 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/fixtures/spec/lib/cli/shell_dash_proof.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Demo::Test::CLI::Shell do 4 | using Refinements::Pathname 5 | using Refinements::StringIO 6 | 7 | subject(:shell) { described_class.new } 8 | 9 | include_context "with application dependencies" 10 | 11 | before { Sod::Container.stub! logger:, io: } 12 | 13 | after { Sod::Container.restore } 14 | 15 | describe "#call" do 16 | it "prints configuration usage" do 17 | shell.call %w[config] 18 | expect(io.reread).to match(/Manage configuration.+/m) 19 | end 20 | 21 | it "prints version" do 22 | shell.call %w[--version] 23 | expect(io.reread).to match(/Demo\sTest\s\d+\.\d+\.\d+/) 24 | end 25 | 26 | it "prints help" do 27 | shell.call %w[--help] 28 | expect(io.reread).to match(/Demo\sTest.+USAGE.+/m) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/fixtures/readmes/minimum.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | 4 | 5 | 6 | ## Features 7 | 8 | ## Requirements 9 | 10 | 1. [Ruby](https://www.ruby-lang.org) 11 | 12 | ## Setup 13 | 14 | To install, run: 15 | 16 | gem install test 17 | 18 | You can also add the gem directly to your project: 19 | 20 | bundle add test 21 | 22 | Once the gem is installed, you only need to require it: 23 | 24 | require "test" 25 | 26 | ## Usage 27 | 28 | ## Development 29 | 30 | To contribute, run: 31 | 32 | git clone https://github.com/undefined/test 33 | cd test 34 | 35 | ## Tests 36 | 37 | To test, run: 38 | 39 | bin/rake 40 | 41 | ## Credits 42 | 43 | - Built with [Gemsmith](https://alchemists.io/projects/gemsmith). 44 | - Engineered by [Jill Smith](https://undefined.io/team/undefined). 45 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/publisher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Publisher do 6 | subject(:publisher) { described_class.new steps: } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe "#call" do 11 | context "when success" do 12 | let(:steps) { [proc { Success(specification) }, proc { Success(specification) }] } 13 | 14 | it "answers specification" do 15 | result = publisher.call specification 16 | expect(result.success).to eq(specification) 17 | end 18 | end 19 | 20 | context "when failure" do 21 | let(:steps) { [proc { Failure "Danger!" }, proc { Success(specification) }] } 22 | 23 | it "answers failure" do 24 | result = publisher.call specification 25 | expect(result.failure).to eq("Danger!") 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/packager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "refinements/pathname" 5 | require "rubygems/command_manager" 6 | 7 | module Gemsmith 8 | module Tools 9 | # Builds a gem package for distribution. 10 | class Packager 11 | include Dry::Monads[:result] 12 | 13 | using Refinements::Pathname 14 | 15 | def initialize command: Gem::CommandManager.new 16 | @command = command 17 | end 18 | 19 | # :reek:TooManyStatements 20 | def call specification 21 | command.run ["build", "#{specification.name}.gemspec"] 22 | specification.package_path.then { |path| path.make_ancestors.basename.copy path.parent } 23 | Success specification 24 | rescue Gem::Exception => error 25 | Failure error.message 26 | end 27 | 28 | private 29 | 30 | attr_reader :command 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/versioner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Versioner do 6 | subject(:versioner) { described_class.new publisher: } 7 | 8 | include_context "with application dependencies" 9 | 10 | let(:publisher) { instance_spy Milestoner::Tags::Publisher, call: Success(specification) } 11 | 12 | describe "#call" do 13 | it "delegates to publisher" do 14 | versioner.call specification 15 | expect(publisher).to have_received(:call).with(/0.0.0/) 16 | end 17 | 18 | it "answers specification when success" do 19 | expect(versioner.call(specification)).to be_success(specification) 20 | end 21 | 22 | it "answers failure when publish fails" do 23 | allow(publisher).to receive(:call).and_return(Failure("Danger!")) 24 | expect(versioner.call(specification)).to be_failure("Danger!") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/fixtures/lib/container.rb: -------------------------------------------------------------------------------- 1 | require "cogger" 2 | require "containable" 3 | require "etcher" 4 | require "runcom" 5 | require "spek" 6 | 7 | module Test 8 | # Provides a global gem container for injection into other objects. 9 | module Container 10 | extend Containable 11 | 12 | register :registry, as: :fresh do 13 | Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model) 14 | .add_loader(:yaml, self[:defaults_path]) 15 | .add_loader(:yaml, self[:xdg_config].active) 16 | end 17 | 18 | register(:settings) { Etcher.call(self[:registry]).dup } 19 | register(:specification) { Spek::Loader.call "#{__dir__}/../../test.gemspec" } 20 | register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") } 21 | register(:xdg_config) { Runcom::Config.new "test/configuration.yml" } 22 | register(:logger) { Cogger.new id: "test" } 23 | register :io, STDOUT 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/fixtures/lib/cli/shell.rb: -------------------------------------------------------------------------------- 1 | require "sod" 2 | 3 | module Test 4 | module CLI 5 | # The main Command Line Interface (CLI) object. 6 | class Shell 7 | include Dependencies[:defaults_path, :xdg_config, :specification] 8 | 9 | def initialize(context: Sod::Context, dsl: Sod, **) 10 | super(**) 11 | @context = context 12 | @dsl = dsl 13 | end 14 | 15 | def call(...) = cli.call(...) 16 | 17 | private 18 | 19 | attr_reader :context, :dsl 20 | 21 | def cli 22 | context = build_context 23 | 24 | dsl.new :test, banner: specification.banner do 25 | on(Sod::Prefabs::Commands::Config, context:) 26 | on(Sod::Prefabs::Actions::Version, context:) 27 | on Sod::Prefabs::Actions::Help, self 28 | end 29 | end 30 | 31 | def build_context 32 | context[defaults_path:, xdg_config:, version_label: specification.labeled_version] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/installer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | module Gemsmith 6 | module Tools 7 | # Installs a locally built gem. 8 | class Installer 9 | include Dependencies[:executor] 10 | include Dry::Monads[:result, :do] 11 | 12 | # Order matters. 13 | STEPS = [Tools::Cleaner.new, Tools::Packager.new].freeze 14 | 15 | def initialize(steps: STEPS, **) 16 | super(**) 17 | @steps = steps 18 | end 19 | 20 | def call specification 21 | steps.each { |step| yield step.call(specification) } 22 | run specification 23 | end 24 | 25 | private 26 | 27 | attr_reader :steps 28 | 29 | def run specification 30 | path = specification.package_path 31 | 32 | executor.capture3("gem", "install", path.to_s).then do |_stdout, _stderr, status| 33 | status.success? ? Success(specification) : Failure("Unable to install: #{path}.") 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/fixtures/readmes/minimum.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | :toclevels: 5 3 | :figure-caption!: 4 | 5 | = Test 6 | 7 | toc::[] 8 | 9 | == Features 10 | 11 | == Requirements 12 | 13 | . link:https://www.ruby-lang.org[Ruby]. 14 | 15 | == Setup 16 | 17 | To install, run: 18 | 19 | [source,bash] 20 | ---- 21 | gem install test 22 | ---- 23 | 24 | You can also add the gem directly to your project: 25 | 26 | [source,bash] 27 | ---- 28 | bundle add test 29 | ---- 30 | 31 | Once the gem is installed, you only need to require it: 32 | 33 | [source,ruby] 34 | ---- 35 | require "test" 36 | ---- 37 | 38 | == Usage 39 | 40 | == Development 41 | 42 | To contribute, run: 43 | 44 | [source,bash] 45 | ---- 46 | git clone https://github.com/undefined/test 47 | cd test 48 | ---- 49 | 50 | == Tests 51 | 52 | To test, run: 53 | 54 | [source,bash] 55 | ---- 56 | bin/rake 57 | ---- 58 | 59 | == Credits 60 | 61 | * Built with link:https://alchemists.io/projects/gemsmith[Gemsmith]. 62 | * Engineered by link:https://undefined.io/team/undefined[Jill Smith]. 63 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | working_directory: ~/project 5 | docker: 6 | - image: bkuhlmann/alpine-ruby:latest 7 | steps: 8 | - checkout 9 | 10 | - restore_cache: 11 | name: Gems Restore 12 | keys: 13 | - gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "gemsmith.gemspec"}} 14 | - gem-cache- 15 | 16 | - run: 17 | name: Gems Install 18 | command: | 19 | gem update --system 20 | bundle config set path "vendor/bundle" 21 | bundle install 22 | 23 | - save_cache: 24 | name: Gems Store 25 | key: gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "gemsmith.gemspec"}} 26 | paths: 27 | - vendor/bundle 28 | 29 | - run: 30 | name: Rake 31 | command: bundle exec rake 32 | 33 | - store_artifacts: 34 | name: SimpleCov Report 35 | path: ~/project/coverage 36 | destination: coverage 37 | -------------------------------------------------------------------------------- /.config/rubocop/config.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | caliber: config/all.yml 3 | 4 | Gemspec/RequiredRubyVersion: 5 | Exclude: 6 | - "spec/support/fixtures/*.gemspec" 7 | Layout/RedundantLineBreak: 8 | Exclude: 9 | - "spec/support/fixtures/*.gemspec" 10 | Layout/EmptyLinesAroundBlockBody: 11 | Exclude: 12 | - "spec/support/fixtures/spec/lib/cli/shell_without_refinements_proof.rb" 13 | Metrics/BlockLength: 14 | Exclude: 15 | - "lib/gemsmith/container.rb" 16 | Metrics/CollectionLiteralLength: 17 | Exclude: 18 | - "lib/gemsmith/cli/commands/build.rb" 19 | Metrics/MethodLength: 20 | Exclude: 21 | - "lib/gemsmith/cli/shell.rb" 22 | - "lib/gemsmith/builders/documentation/readme.rb" 23 | RSpec/MultipleMemoizedHelpers: 24 | Exclude: 25 | - "spec/support/shared_contexts/application_dependencies.rb" 26 | RSpec/SpecFilePathFormat: 27 | Exclude: 28 | - "spec/support/fixtures/**/*" 29 | RSpec/SpecFilePathSuffix: 30 | Exclude: 31 | - "spec/support/fixtures/**/*" 32 | Style/FrozenStringLiteralComment: 33 | Exclude: 34 | - "spec/support/fixtures/**/*" 35 | -------------------------------------------------------------------------------- /spec/support/fixtures/test-minimum-cli.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "test" 3 | spec.version = "0.0.0" 4 | spec.authors = ["Jill Smith"] 5 | spec.email = ["jill@acme.io"] 6 | spec.homepage = "" 7 | spec.summary = "" 8 | spec.license = "Hippocratic-2.1" 9 | 10 | spec.metadata = { 11 | "label" => "Test", 12 | "rubygems_mfa_required" => "true" 13 | } 14 | 15 | spec.required_ruby_version = ">= 3.4" 16 | spec.add_dependency "cogger", "~> 1.0" 17 | spec.add_dependency "containable", "~> 1.1" 18 | spec.add_dependency "dry-monads", "~> 1.9" 19 | spec.add_dependency "etcher", "~> 3.0" 20 | spec.add_dependency "infusible", "~> 4.0" 21 | spec.add_dependency "refinements", "~> 13.6" 22 | spec.add_dependency "runcom", "~> 12.0" 23 | spec.add_dependency "sod", "~> 1.5" 24 | spec.add_dependency "spek", "~> 4.0" 25 | spec.add_dependency "zeitwerk", "~> 2.7" 26 | 27 | spec.bindir = "exe" 28 | spec.executables << "test" 29 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 30 | spec.files = Dir["*.gemspec", "lib/**/*"] 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/application_dependencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "with application dependencies" do 4 | using Refinements::Struct 5 | 6 | include_context "with temporary directory" 7 | 8 | let(:settings) { Gemsmith::Container[:settings] } 9 | let(:specification) { Spek::Loader.call SPEC_ROOT.join("support/fixtures/gemsmith-test.gemspec") } 10 | let(:executor) { class_spy Open3, capture3: ["Output.", "Error.", Process::Status.allocate] } 11 | let(:logger) { Cogger.new id: :gemsmith, io: StringIO.new, level: :debug } 12 | let(:io) { StringIO.new } 13 | 14 | before do 15 | settings.with! Etcher.call( 16 | Gemsmith::Container[:registry].remove_loader(1), 17 | author_family_name: "Smith", 18 | author_given_name: "Jill", 19 | author_email: "jill@acme.io", 20 | loaded_at: Time.utc(2020, 1, 1, 0, 0, 0), 21 | target_root: temp_dir, 22 | project_name: "test" 23 | ) 24 | 25 | Gemsmith::Container.stub! executor:, logger:, io: 26 | end 27 | 28 | after { Gemsmith::Container.restore } 29 | end 30 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/cli/shell.rb.erb: -------------------------------------------------------------------------------- 1 | require "sod" 2 | 3 | <% namespace do %> 4 | module CLI 5 | # The main Command Line Interface (CLI) object. 6 | class Shell 7 | include Dependencies[:defaults_path, :xdg_config, :specification] 8 | 9 | def initialize(context: Sod::Context, dsl: Sod, **) 10 | super(**) 11 | @context = context 12 | @dsl = dsl 13 | end 14 | 15 | def call(...) = cli.call(...) 16 | 17 | private 18 | 19 | attr_reader :context, :dsl 20 | 21 | def cli 22 | context = build_context 23 | 24 | dsl.new <%= settings.project_levels.positive? ? settings.project_name.inspect : settings.project_name.to_sym.inspect %>, banner: specification.banner do 25 | on(Sod::Prefabs::Commands::Config, context:) 26 | on(Sod::Prefabs::Actions::Version, context:) 27 | on Sod::Prefabs::Actions::Help, self 28 | end 29 | end 30 | 31 | def build_context 32 | context[defaults_path:, xdg_config:, version_label: specification.labeled_version] 33 | end 34 | end 35 | end 36 | <% end %> 37 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/lib/%project_path%/container.rb.erb: -------------------------------------------------------------------------------- 1 | require "cogger" 2 | require "containable" 3 | require "etcher" 4 | require "runcom" 5 | require "spek" 6 | 7 | <% namespace do %> 8 | # Provides a global gem container for injection into other objects. 9 | module Container 10 | extend Containable 11 | 12 | register :registry, as: :fresh do 13 | Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model) 14 | .add_loader(:yaml, self[:defaults_path]) 15 | .add_loader(:yaml, self[:xdg_config].active) 16 | end 17 | 18 | register(:settings) { Etcher.call(self[:registry]).dup } 19 | register(:specification) { Spek::Loader.call "#{__dir__}/<%= Array.new(2 + settings.project_levels, "../").join %><%= settings.project_name %>.gemspec" } 20 | register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") } 21 | register(:xdg_config) { Runcom::Config.new "<%= settings.project_path %>/configuration.yml" } 22 | register(:logger) { Cogger.new id: "<%= settings.project_name %>" } 23 | register :io, STDOUT 24 | end 25 | <% end %> 26 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Cleaner do 6 | using Refinements::Pathname 7 | 8 | subject(:cleaner) { described_class.new } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | it "deletes Gemsmith artifacts" do 14 | temp_dir.change_dir do 15 | path = temp_dir.join("tmp/gemsmith-test-0.0.0.gem").touch_deep 16 | cleaner.call specification 17 | 18 | expect(path.exist?).to be(false) 19 | end 20 | end 21 | 22 | it "deletes Bundler artifacts" do 23 | temp_dir.change_dir do 24 | temp_dir.join("pkg/gemsmith-test-0.0.0.gem").touch_deep 25 | cleaner.call specification 26 | 27 | expect(temp_dir.join("pkg").exist?).to be(false) 28 | end 29 | end 30 | 31 | it "deletes extraneous root artifacts" do 32 | temp_dir.change_dir do 33 | path = temp_dir.join("gemsmith-test-0.0.0.gem").touch 34 | cleaner.call specification 35 | 36 | expect(path.exist?).to be(false) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/shell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Gemsmith 6 | module CLI 7 | # The main Command Line Interface (CLI) object. 8 | class Shell 9 | include Dependencies[:defaults_path, :xdg_config, :specification] 10 | 11 | def initialize(context: Sod::Context, dsl: Sod, **) 12 | super(**) 13 | @context = context 14 | @dsl = dsl 15 | end 16 | 17 | def call(...) = cli.call(...) 18 | 19 | private 20 | 21 | attr_reader :context, :dsl 22 | 23 | def cli 24 | context = build_context 25 | 26 | dsl.new :gemsmith, banner: specification.banner do 27 | on(Sod::Prefabs::Commands::Config, context:) 28 | on Commands::Build 29 | on Actions::Install 30 | on Actions::Publish 31 | on Actions::Edit 32 | on Actions::View 33 | on(Sod::Prefabs::Actions::Version, context:) 34 | on Sod::Prefabs::Actions::Help, self 35 | end 36 | end 37 | 38 | def build_context 39 | context[defaults_path:, xdg_config:, version_label: specification.labeled_version] 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/actions/edit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "sod" 5 | require "spek" 6 | 7 | module Gemsmith 8 | module CLI 9 | module Actions 10 | # Handles the edit action for editing an installed gem. 11 | class Edit < Sod::Action 12 | include Dependencies[:logger] 13 | include Dry::Monads[:result] 14 | 15 | description "Edit installed gem in default editor." 16 | 17 | on %w[-e --edit], argument: "GEM" 18 | 19 | def initialize(picker: Spek::Picker, editor: Tools::Editor.new, **) 20 | super(**) 21 | @picker = picker 22 | @editor = editor 23 | end 24 | 25 | def call gem_name 26 | case picker.call(gem_name).bind { |spec| editor.call spec } 27 | in Success(spec) then logger.info { "Editing: #{spec.named_version}." } 28 | in Failure(message) then log_error { message } 29 | else log_error { "Unable to handle edit action." } 30 | end 31 | end 32 | 33 | private 34 | 35 | attr_reader :picker, :editor 36 | 37 | def log_error(&) = logger.error(&) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/actions/view.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "sod" 5 | require "spek" 6 | 7 | module Gemsmith 8 | module CLI 9 | module Actions 10 | # Handles the view action for viewing an installed gem in default browser. 11 | class View < Sod::Action 12 | include Dependencies[:logger] 13 | include Dry::Monads[:result] 14 | 15 | description "View installed gem in default browser." 16 | 17 | on %w[-V --view], argument: "GEM" 18 | 19 | def initialize(picker: Spek::Picker, viewer: Tools::Viewer.new, **) 20 | super(**) 21 | @picker = picker 22 | @viewer = viewer 23 | end 24 | 25 | def call gem_name 26 | case picker.call(gem_name).bind { |spec| viewer.call spec } 27 | in Success(spec) then logger.info { "Viewing: #{spec.named_version}." } 28 | in Failure(message) then log_error { message } 29 | else log_error { "Unable to handle view action." } 30 | end 31 | end 32 | 33 | private 34 | 35 | attr_reader :picker, :viewer 36 | 37 | def log_error(&) = logger.error(&) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/git/ignore_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::Git::Ignore do 6 | using Refinements::Pathname 7 | using Refinements::Struct 8 | 9 | subject(:builder) { described_class.new settings:, logger: } 10 | 11 | include_context "with application dependencies" 12 | 13 | describe "#call" do 14 | context "when enabled" do 15 | before { settings.minimize.with build_git: true } 16 | 17 | it "builds file" do 18 | builder.call 19 | 20 | expect(temp_dir.join("test/.gitignore").read).to eq(<<~CONTENT) 21 | *.gem 22 | .bundle 23 | Gemfile.lock 24 | pkg 25 | tmp 26 | CONTENT 27 | end 28 | 29 | it "answers true" do 30 | expect(builder.call).to be(true) 31 | end 32 | end 33 | 34 | context "when disabled" do 35 | before { settings.with! settings.minimize } 36 | 37 | it "doesn't build file" do 38 | builder.call 39 | expect(temp_dir.join("test/.gitignore").exist?).to be(false) 40 | end 41 | 42 | it "answers false" do 43 | expect(builder.call).to be(false) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/support/fixtures/boms/maximum.txt: -------------------------------------------------------------------------------- 1 | test/.circleci/config.yml 2 | test/.config/rubocop/config.yml 3 | test/.devcontainer/compose.yaml 4 | test/.devcontainer/devcontainer.json 5 | test/.devcontainer/Dockerfile 6 | test/.dockerignore 7 | test/.git/HEAD 8 | test/.github/FUNDING.yml 9 | test/.github/ISSUE_TEMPLATE/config.yml 10 | test/.github/ISSUE_TEMPLATE/issue.md 11 | test/.github/PULL_REQUEST_TEMPLATE.md 12 | test/.github/workflows/ci.yml 13 | test/.gitignore 14 | test/.reek.yml 15 | test/.ruby-version 16 | test/CITATION.cff 17 | test/Dockerfile 18 | test/Gemfile 19 | test/LICENSE.adoc 20 | test/README.adoc 21 | test/Rakefile 22 | test/VERSIONS.adoc 23 | test/bin/console 24 | test/bin/docker/build 25 | test/bin/docker/console 26 | test/bin/docker/entrypoint 27 | test/bin/rake 28 | test/bin/rspec 29 | test/bin/rubocop 30 | test/bin/setup 31 | test/exe/test 32 | test/lib/test.rb 33 | test/lib/test/cli/shell.rb 34 | test/lib/test/configuration/contract.rb 35 | test/lib/test/configuration/defaults.yml 36 | test/lib/test/configuration/model.rb 37 | test/lib/test/container.rb 38 | test/lib/test/dependencies.rb 39 | test/spec/lib/test/cli/shell_spec.rb 40 | test/spec/lib/test_spec.rb 41 | test/spec/spec_helper.rb 42 | test/spec/support/shared_contexts/application_dependencies.rb 43 | test/spec/support/shared_contexts/temp_dir.rb 44 | test/test.gemspec 45 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/actions/install.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "pathname" 5 | require "sod" 6 | require "spek" 7 | 8 | module Gemsmith 9 | module CLI 10 | module Actions 11 | # Handles the install action. 12 | class Install < Sod::Action 13 | include Dependencies[:logger] 14 | include Dry::Monads[:result] 15 | 16 | description "Install gem for local development." 17 | 18 | ancillary "Optionally computes gem package based on current directory." 19 | 20 | on %w[-i --install], argument: "[GEM]" 21 | 22 | default { Pathname.pwd.basename } 23 | 24 | def initialize(installer: Tools::Installer.new, loader: Spek::Loader, **) 25 | super(**) 26 | @installer = installer 27 | @loader = loader 28 | end 29 | 30 | def call name = default 31 | case installer.call loader.call("#{name}.gemspec") 32 | in Success(spec) then logger.info { "Installed: #{spec.package_name}." } 33 | in Failure(message) then log_error { message } 34 | else log_error { "Unable to handle install action." } 35 | end 36 | end 37 | 38 | private 39 | 40 | attr_reader :installer, :loader 41 | 42 | def log_error(&) = logger.error(&) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/actions/publish.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "pathname" 5 | require "sod" 6 | require "spek" 7 | 8 | module Gemsmith 9 | module CLI 10 | module Actions 11 | # Handles the publish action. 12 | class Publish < Sod::Action 13 | include Dependencies[:logger] 14 | include Dry::Monads[:result] 15 | 16 | description "Publish gem to remote gem server." 17 | 18 | ancillary "Optionally computes gem package based on current directory." 19 | 20 | on %w[-p --publish], argument: "[GEM]" 21 | 22 | default { Pathname.pwd.basename } 23 | 24 | def initialize(publisher: Tools::Publisher.new, loader: Spek::Loader, **) 25 | super(**) 26 | @publisher = publisher 27 | @loader = loader 28 | end 29 | 30 | def call name = default 31 | case publisher.call loader.call("#{name}.gemspec") 32 | in Success(spec) then logger.info { "Published: #{spec.package_name}." } 33 | in Failure(message) then log_error message 34 | else log_error "Publish failed, unable to parse result." 35 | end 36 | end 37 | 38 | private 39 | 40 | attr_reader :publisher, :loader 41 | 42 | def log_error(message) = logger.error { message } 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/actions/edit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "spec_helper" 5 | 6 | RSpec.describe Gemsmith::CLI::Actions::Edit do 7 | include Dry::Monads[:result, :maybe] 8 | 9 | subject(:action) { described_class.new picker:, editor: } 10 | 11 | include_context "with application dependencies" 12 | 13 | let(:picker) { instance_double Spek::Picker, call: result } 14 | let(:editor) { instance_double Gemsmith::Tools::Editor, call: result } 15 | 16 | let :specification do 17 | Spek::Loader.call SPEC_ROOT.join("support/fixtures/gemsmith-test.gemspec") 18 | end 19 | 20 | describe "#call" do 21 | context "when success" do 22 | let(:result) { Success specification } 23 | 24 | it "edits gem" do 25 | action.call "gemsmith-test" 26 | expect(logger.reread).to match(/🟢.+Editing: gemsmith-test 0.0.0./) 27 | end 28 | end 29 | 30 | context "when failure" do 31 | let(:result) { Failure "Danger!" } 32 | 33 | it "logs error" do 34 | action.call "test" 35 | expect(logger.reread).to match(/🛑.+Danger!/) 36 | end 37 | end 38 | 39 | context "when unknown" do 40 | let(:result) { Maybe "bogus" } 41 | 42 | it "logs error" do 43 | action.call "test" 44 | expect(logger.reread).to match(/🛑.+Unable to handle edit action./) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/actions/view_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "spec_helper" 5 | 6 | RSpec.describe Gemsmith::CLI::Actions::View do 7 | include Dry::Monads[:result, :maybe] 8 | 9 | subject(:action) { described_class.new picker:, viewer: } 10 | 11 | include_context "with application dependencies" 12 | 13 | let(:picker) { instance_double Spek::Picker, call: result } 14 | let(:viewer) { instance_double Gemsmith::Tools::Viewer, call: result } 15 | 16 | let :specification do 17 | Spek::Loader.call SPEC_ROOT.join("support/fixtures/gemsmith-test.gemspec") 18 | end 19 | 20 | describe "#call" do 21 | context "when success" do 22 | let(:result) { Success specification } 23 | 24 | it "views gem" do 25 | action.call "gemsmith-test" 26 | expect(logger.reread).to match(/🟢.+Viewing: gemsmith-test 0.0.0./) 27 | end 28 | end 29 | 30 | context "when failure" do 31 | let(:result) { Failure "Danger!" } 32 | 33 | it "logs error" do 34 | action.call "test" 35 | expect(logger.reread).to match(/🛑.+Danger!/) 36 | end 37 | end 38 | 39 | context "when unknown" do 40 | let(:result) { Maybe "bogus" } 41 | 42 | it "logs error" do 43 | action.call "test" 44 | expect(logger.reread).to match(/🛑.+Unable to handle view action./) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/fixtures/test-maximum.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "test" 3 | spec.version = "0.0.0" 4 | spec.authors = ["Jill Smith"] 5 | spec.email = ["jill@acme.io"] 6 | spec.homepage = "https://undefined.io/projects/test" 7 | spec.summary = "" 8 | spec.license = "Hippocratic-2.1" 9 | 10 | spec.metadata = { 11 | "bug_tracker_uri" => "https://github.com/undefined/test/issues", 12 | "changelog_uri" => "https://undefined.io/projects/test/versions", 13 | "homepage_uri" => "https://undefined.io/projects/test", 14 | "funding_uri" => "https://github.com/sponsors/undefined", 15 | "label" => "Test", 16 | "rubygems_mfa_required" => "true", 17 | "source_code_uri" => "https://github.com/undefined/test" 18 | } 19 | 20 | spec.signing_key = Gem.default_key_path 21 | spec.cert_chain = [Gem.default_cert_path] 22 | 23 | spec.required_ruby_version = ">= 3.4" 24 | spec.add_dependency "cogger", "~> 1.0" 25 | spec.add_dependency "containable", "~> 1.1" 26 | spec.add_dependency "dry-monads", "~> 1.9" 27 | spec.add_dependency "etcher", "~> 3.0" 28 | spec.add_dependency "infusible", "~> 4.0" 29 | spec.add_dependency "refinements", "~> 13.6" 30 | spec.add_dependency "runcom", "~> 12.0" 31 | spec.add_dependency "sod", "~> 1.5" 32 | spec.add_dependency "spek", "~> 4.0" 33 | spec.add_dependency "zeitwerk", "~> 2.7" 34 | 35 | spec.bindir = "exe" 36 | spec.executables << "test" 37 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 38 | spec.files = Dir["*.gemspec", "lib/**/*"] 39 | end 40 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/cli/shell_spec.rb.erb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe <%= settings.project_namespaced_class %>::CLI::Shell do 4 | <% if settings.build_refinements %> 5 | using Refinements::Pathname 6 | using Refinements::StringIO 7 | <% end %> 8 | 9 | subject(:shell) { described_class.new } 10 | 11 | include_context "with application dependencies" 12 | 13 | before { Sod::Container.stub! logger:, io: } 14 | 15 | after { Sod::Container.restore } 16 | 17 | describe "#call" do 18 | it "prints configuration usage" do 19 | shell.call %w[config] 20 | <% if settings.build_refinements %> 21 | expect(io.reread).to match(/Manage configuration.+/m) 22 | <% else %> 23 | expect(io.tap(&:rewind).read).to match(/Manage configuration.+/m) 24 | <% end %> 25 | end 26 | 27 | it "prints version" do 28 | shell.call %w[--version] 29 | <% if settings.build_refinements %> 30 | expect(io.reread).to match(/<%= settings.project_label.gsub(" ", "\\s") %>\s\d+\.\d+\.\d+/) 31 | <% else %> 32 | expect(io.tap(&:rewind).read).to match(/<%= settings.project_label.gsub(" ", "\\s") %>\s\d+\.\d+\.\d+/) 33 | <% end %> 34 | end 35 | 36 | it "prints help" do 37 | shell.call %w[--help] 38 | <% if settings.build_refinements %> 39 | expect(io.reread).to match(/<%= settings.project_label.gsub(" ", "\\s") %>.+USAGE.+/m) 40 | <% else %> 41 | expect(io.tap(&:rewind).read).to match(/<%= settings.project_label.gsub(" ", "\\s") %>.+USAGE.+/m) 42 | <% end %> 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/installer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Installer do 6 | using Refinements::Pathname 7 | 8 | subject(:installer) { described_class.new steps: } 9 | 10 | include_context "with application dependencies" 11 | 12 | let(:cleaner) { instance_spy Gemsmith::Tools::Cleaner, call: Success() } 13 | 14 | describe "#call" do 15 | context "with success" do 16 | let(:steps) { [proc { Success() }, proc { Success() }] } 17 | 18 | it "installs gem" do 19 | installer.call specification 20 | 21 | expect(executor).to have_received(:capture3).with( 22 | "gem", 23 | "install", 24 | "tmp/gemsmith-test-0.0.0.gem" 25 | ) 26 | end 27 | 28 | it "answers specification" do 29 | result = installer.call specification 30 | expect(result.success).to eq(specification) 31 | end 32 | end 33 | 34 | context "with step failure" do 35 | let(:steps) { [proc { Failure "Danger!" }, proc { Success() }] } 36 | 37 | it "answers failure" do 38 | result = installer.call specification 39 | expect(result.failure).to eq("Danger!") 40 | end 41 | end 42 | 43 | context "with install failure" do 44 | let(:steps) { [proc { Success() }] } 45 | 46 | it "answers failure" do 47 | status = instance_spy Process::Status, success?: false 48 | allow(executor).to receive(:capture3).and_return ["", "", status] 49 | 50 | result = installer.call specification 51 | expect(result.failure).to eq("Unable to install: tmp/gemsmith-test-0.0.0.gem.") 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/gemsmith/tools/pusher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "rubygems/command_manager" 5 | 6 | module Gemsmith 7 | module Tools 8 | # Pushes a gem package to remote gem server. 9 | class Pusher 10 | include Dependencies[:executor, :logger] 11 | include Dry::Monads[:result] 12 | 13 | def initialize(command: Gem::CommandManager.new, **) 14 | super(**) 15 | @command = command 16 | end 17 | 18 | def call specification 19 | command.run ["push", specification.package_path.to_s, *one_time_password] 20 | Success specification 21 | rescue Gem::Exception => error 22 | Failure error.message 23 | end 24 | 25 | private 26 | 27 | attr_reader :command 28 | 29 | # :reek:TooManyStatements 30 | def one_time_password 31 | return Core::EMPTY_ARRAY if check_yubikey.failure? 32 | 33 | executor.capture3(check_yubikey.success, "oath", "accounts", "code", "--single", "RubyGems") 34 | .then { |stdout, _stderr, status| status.success? ? ["--otp", stdout.chomp] : [] } 35 | rescue Errno::ENOENT => error 36 | logger.debug { "Unable to obtain YubiKey One-Time Password. #{error}." } 37 | Core::EMPTY_ARRAY 38 | end 39 | 40 | def check_yubikey 41 | executor.capture3("command", "-v", "ykman") 42 | .then do |stdout, stderr, status| 43 | if status.success? 44 | Success stdout.chomp 45 | else 46 | logger.debug { "Unable to find YubiKey Manager. #{stderr}." } 47 | Failure() 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/packager_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Packager do 6 | using Refinements::Pathname 7 | 8 | subject(:packager) { described_class.new } 9 | 10 | include_context "with application dependencies" 11 | 12 | before { Gemsmith::Container.stub executor: Open3 } 13 | 14 | describe "#call" do 15 | before do 16 | Bundler.root 17 | .join("spec/support/fixtures/gemsmith-test.gemspec") 18 | .copy temp_dir.join("gemsmith-test.gemspec") 19 | end 20 | 21 | it "builds gem when successful" do 22 | temp_dir.change_dir do 23 | packager.call specification 24 | expect(temp_dir.join("gemsmith-test-0.0.0.gem").exist?).to be(true) 25 | end 26 | end 27 | 28 | it "answers specification when success" do 29 | temp_dir.change_dir do 30 | result = packager.call specification 31 | expect(result.success).to eq(specification) 32 | end 33 | end 34 | 35 | context "with failure" do 36 | subject(:packager) { described_class.new command: } 37 | 38 | let(:command) { instance_double Gem::CommandManager } 39 | 40 | before { allow(command).to receive(:run).and_raise(Gem::Exception, "Danger!") } 41 | 42 | it "doesn't build gem" do 43 | temp_dir.change_dir do 44 | packager.call specification 45 | expect(Pathname("gemsmith-test-0.0.0.gem").exist?).to be(false) 46 | end 47 | end 48 | 49 | it "answers error message" do 50 | temp_dir.change_dir do 51 | result = packager.call specification 52 | expect(result.failure).to eq("Danger!") 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/rspec/helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::RSpec::Helper do 6 | using Refinements::Struct 7 | using Refinements::Pathname 8 | 9 | subject(:builder) { described_class.new settings:, logger: } 10 | 11 | include_context "with application dependencies" 12 | 13 | describe "#call" do 14 | let(:path) { temp_dir.join "test/spec/spec_helper.rb" } 15 | 16 | context "when enabled with CLI and SimpleCov" do 17 | before do 18 | settings.with! settings.minimize.with( 19 | build_rspec: true, 20 | build_simple_cov: true, 21 | build_cli: true 22 | ) 23 | end 24 | 25 | it "updates file" do 26 | builder.call 27 | expect(path.read).to include("add_filter %r((.+/container\\.rb|^/spec/))") 28 | end 29 | 30 | it "answers true" do 31 | expect(builder.call).to be(true) 32 | end 33 | end 34 | 35 | context "when enabled without CLI" do 36 | before { settings.with! settings.minimize.with(build_rspec: true, build_simple_cov: true) } 37 | 38 | it "updates file" do 39 | builder.call 40 | expect(path.read).not_to include("add_filter %r((.+/container\\.rb|^/spec/))") 41 | end 42 | 43 | it "answers false" do 44 | expect(builder.call).to be(false) 45 | end 46 | end 47 | 48 | context "when disabled" do 49 | before { settings.with! settings.minimize } 50 | 51 | it "doesn't update file" do 52 | builder.call 53 | expect(path.exist?).to be(false) 54 | end 55 | 56 | it "answers false" do 57 | expect(builder.call).to be(false) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/actions/install_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "spec_helper" 5 | 6 | RSpec.describe Gemsmith::CLI::Actions::Install do 7 | using Refinements::Pathname 8 | 9 | subject(:action) { described_class.new installer: } 10 | 11 | include_context "with application dependencies" 12 | 13 | let(:installer) { instance_spy Gemsmith::Tools::Installer, call: result } 14 | let(:specification) { Spek::Loader.call temp_dir.join("gemsmith-test.gemspec") } 15 | 16 | describe "#call" do 17 | before { SPEC_ROOT.join("support/fixtures/gemsmith-test.gemspec").copy temp_dir } 18 | 19 | context "when success" do 20 | let(:result) { Success specification } 21 | 22 | it "installs gem" do 23 | temp_dir.change_dir do 24 | action.call 25 | expect(installer).to have_received(:call).with(kind_of(Spek::Presenter)) 26 | end 27 | end 28 | 29 | it "logs gem was installed" do 30 | temp_dir.change_dir do 31 | action.call 32 | expect(logger.reread).to match(/🟢.+Installed: gemsmith-test-0.0.0.gem./) 33 | end 34 | end 35 | end 36 | 37 | context "when failure" do 38 | let(:result) { Failure "Danger!" } 39 | 40 | it "logs error" do 41 | temp_dir.change_dir do 42 | action.call 43 | expect(logger.reread).to match(/🛑.+Danger!/) 44 | end 45 | end 46 | end 47 | 48 | context "when unknown" do 49 | let(:result) { "bogus" } 50 | 51 | it "logs error" do 52 | temp_dir.change_dir do 53 | action.call 54 | expect(logger.reread).to match(/🛑.+Unable to handle install action./) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/actions/publish_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "spec_helper" 5 | 6 | RSpec.describe Gemsmith::CLI::Actions::Publish do 7 | using Refinements::Pathname 8 | 9 | subject(:action) { described_class.new publisher: } 10 | 11 | include_context "with application dependencies" 12 | 13 | let(:publisher) { instance_spy Gemsmith::Tools::Publisher, call: result } 14 | let(:specification) { Spek::Loader.call temp_dir.join("gemsmith-test.gemspec") } 15 | 16 | describe "#call" do 17 | before { SPEC_ROOT.join("support/fixtures/gemsmith-test.gemspec").copy temp_dir } 18 | 19 | context "when success" do 20 | let(:result) { Success specification } 21 | 22 | it "publishes gem" do 23 | temp_dir.change_dir do 24 | action.call 25 | expect(publisher).to have_received(:call).with(kind_of(Spek::Presenter)) 26 | end 27 | end 28 | 29 | it "logs gem was published" do 30 | temp_dir.change_dir do 31 | action.call 32 | expect(logger.reread).to match(/🟢.+Published: gemsmith-test-0.0.0.gem./) 33 | end 34 | end 35 | end 36 | 37 | context "when failure" do 38 | let(:result) { Failure "Danger!" } 39 | 40 | it "logs error" do 41 | temp_dir.change_dir do 42 | action.call 43 | expect(logger.reread).to match(/🛑.+Danger!/) 44 | end 45 | end 46 | end 47 | 48 | context "when unknown" do 49 | let(:result) { "bogus" } 50 | 51 | it "logs error" do 52 | temp_dir.change_dir do 53 | action.call 54 | expect(logger.reread).to match(/🛑.+Publish failed, unable to parse result./) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | require "warning" 5 | 6 | if ENV["COVERAGE"] == "no" 7 | puts "SimpleCov skipped due to being disabled." 8 | else 9 | SimpleCov.start do 10 | add_filter %r((.+/container\.rb|^/spec/)) 11 | enable_coverage :branch 12 | enable_coverage_for_eval 13 | minimum_coverage_by_file line: 95, branch: 95 14 | end 15 | end 16 | 17 | Bundler.require :tools 18 | 19 | require "dry/monads" 20 | require "gemsmith" 21 | require "gitt/rspec/shared_contexts/git_repo" 22 | require "gitt/rspec/shared_contexts/temp_dir" 23 | require "refinements" 24 | 25 | SPEC_ROOT = Pathname(__dir__).realpath.freeze 26 | 27 | using Refinements::Pathname 28 | 29 | Pathname.require_tree SPEC_ROOT.join("support/shared_contexts") 30 | 31 | Gem.path.each { |path| Warning.ignore(/rouge/, path) } 32 | 33 | RSpec.configure do |config| 34 | config.color = true 35 | config.disable_monkey_patching! 36 | config.example_status_persistence_file_path = "./tmp/rspec-examples.txt" 37 | config.filter_run_when_matching :focus 38 | config.formatter = ENV.fetch("CI", false) == "true" ? :progress : :documentation 39 | config.order = :random 40 | config.pending_failure_output = :no_backtrace 41 | config.shared_context_metadata_behavior = :apply_to_host_groups 42 | config.warnings = true 43 | 44 | config.expect_with :rspec do |expectations| 45 | expectations.syntax = :expect 46 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 47 | end 48 | 49 | config.mock_with :rspec do |mocks| 50 | mocks.verify_doubled_constant_names = true 51 | mocks.verify_partial_doubles = true 52 | end 53 | 54 | config.before(:suite) { Dry::Monads.load_extensions :rspec } 55 | 56 | Kernel.srand config.seed 57 | end 58 | -------------------------------------------------------------------------------- /spec/support/fixtures/readmes/maximum.md: -------------------------------------------------------------------------------- 1 | # Test Example 2 | 3 | 4 | 5 | 6 | ## Features 7 | 8 | ## Requirements 9 | 10 | 1. [Ruby](https://www.ruby-lang.org) 11 | 12 | ## Setup 13 | 14 | To install _with_ security, run: 15 | 16 | # 💡 Skip this line if you already have the public certificate installed. 17 | gem cert --add <(curl --compressed --location https://undefined.io/gems.pem) 18 | gem install test-example --trust-policy HighSecurity 19 | 20 | To install _without_ security, run: 21 | 22 | gem install test-example 23 | 24 | You can also add the gem directly to your project: 25 | 26 | bundle add test-example 27 | 28 | Once the gem is installed, you only need to require it: 29 | 30 | require "test/example" 31 | 32 | ## Usage 33 | 34 | ## Development 35 | 36 | To contribute, run: 37 | 38 | git clone https://github.com/undefined/test-example 39 | cd test-example 40 | bin/setup 41 | 42 | You can also use the IRB console for direct access to all objects: 43 | 44 | bin/console 45 | 46 | ## Tests 47 | 48 | To test, run: 49 | 50 | bin/rake 51 | 52 | ## [License](https://undefined.io/policies/license) 53 | 54 | ## [Security](https://undefined.io/policies/security) 55 | 56 | ## [Code of Conduct](https://undefined.io/policies/code_of_conduct) 57 | 58 | ## [Contributions](https://undefined.io/policies/contributions) 59 | 60 | ## [Developer Certificate of Origin](https://undefined.io/policies/developer_certificate_of_origin) 61 | 62 | ## [Versions](https://undefined.io/projects/test-example/versions) 63 | 64 | ## [Community](https://undefined.io/community) 65 | 66 | ## Credits 67 | 68 | - Built with [Gemsmith](https://alchemists.io/projects/gemsmith). 69 | - Engineered by [Jill Smith](https://undefined.io/team/undefined). 70 | -------------------------------------------------------------------------------- /gemsmith.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "gemsmith" 5 | spec.version = "23.8.0" 6 | spec.authors = ["Brooke Kuhlmann"] 7 | spec.email = ["brooke@alchemists.io"] 8 | spec.homepage = "https://alchemists.io/projects/gemsmith" 9 | spec.summary = "A command line interface for smithing Ruby gems." 10 | spec.license = "Hippocratic-2.1" 11 | 12 | spec.metadata = { 13 | "bug_tracker_uri" => "https://github.com/bkuhlmann/gemsmith/issues", 14 | "changelog_uri" => "https://alchemists.io/projects/gemsmith/versions", 15 | "homepage_uri" => "https://alchemists.io/projects/gemsmith", 16 | "funding_uri" => "https://github.com/sponsors/bkuhlmann", 17 | "label" => "Gemsmith", 18 | "rubygems_mfa_required" => "true", 19 | "source_code_uri" => "https://github.com/bkuhlmann/gemsmith" 20 | } 21 | 22 | spec.signing_key = Gem.default_key_path 23 | spec.cert_chain = [Gem.default_cert_path] 24 | 25 | spec.required_ruby_version = ">= 3.4" 26 | spec.add_dependency "cogger", "~> 1.0" 27 | spec.add_dependency "containable", "~> 1.1" 28 | spec.add_dependency "core", "~> 2.5" 29 | spec.add_dependency "dry-monads", "~> 1.9" 30 | spec.add_dependency "dry-schema", "~> 1.13" 31 | spec.add_dependency "etcher", "~> 3.0" 32 | spec.add_dependency "infusible", "~> 4.0" 33 | spec.add_dependency "milestoner", "~> 19.8" 34 | spec.add_dependency "refinements", "~> 13.6" 35 | spec.add_dependency "rubysmith", "~> 8.9" 36 | spec.add_dependency "runcom", "~> 12.0" 37 | spec.add_dependency "sod", "~> 1.5" 38 | spec.add_dependency "spek", "~> 4.0" 39 | spec.add_dependency "zeitwerk", "~> 2.7" 40 | 41 | spec.bindir = "exe" 42 | spec.executables << "gemsmith" 43 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 44 | spec.files = Dir.glob ["*.gemspec", "lib/**/*"], File::FNM_DOTMATCH 45 | end 46 | -------------------------------------------------------------------------------- /spec/support/fixtures/readmes/maximum.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | :toclevels: 5 3 | :figure-caption!: 4 | 5 | = Test Example 6 | 7 | toc::[] 8 | 9 | == Features 10 | 11 | == Requirements 12 | 13 | . link:https://www.ruby-lang.org[Ruby]. 14 | 15 | == Setup 16 | 17 | To install _with_ security, run: 18 | 19 | [source,bash] 20 | ---- 21 | # 💡 Skip this line if you already have the public certificate installed. 22 | gem cert --add <(curl --compressed --location https://undefined.io/gems.pem) 23 | gem install test-example --trust-policy HighSecurity 24 | ---- 25 | 26 | To install _without_ security, run: 27 | 28 | [source,bash] 29 | ---- 30 | gem install test-example 31 | ---- 32 | 33 | You can also add the gem directly to your project: 34 | 35 | [source,bash] 36 | ---- 37 | bundle add test-example 38 | ---- 39 | 40 | Once the gem is installed, you only need to require it: 41 | 42 | [source,ruby] 43 | ---- 44 | require "test/example" 45 | ---- 46 | 47 | == Usage 48 | 49 | == Development 50 | 51 | To contribute, run: 52 | 53 | [source,bash] 54 | ---- 55 | git clone https://github.com/undefined/test-example 56 | cd test-example 57 | bin/setup 58 | ---- 59 | 60 | You can also use the IRB console for direct access to all objects: 61 | 62 | [source,bash] 63 | ---- 64 | bin/console 65 | ---- 66 | 67 | == Tests 68 | 69 | To test, run: 70 | 71 | [source,bash] 72 | ---- 73 | bin/rake 74 | ---- 75 | 76 | == link:https://undefined.io/policies/license[License] 77 | 78 | == link:https://undefined.io/policies/security[Security] 79 | 80 | == link:https://undefined.io/policies/code_of_conduct[Code of Conduct] 81 | 82 | == link:https://undefined.io/policies/contributions[Contributions] 83 | 84 | == link:https://undefined.io/policies/developer_certificate_of_origin[Developer Certificate of Origin] 85 | 86 | == link:https://undefined.io/projects/test-example/versions[Versions] 87 | 88 | == link:https://undefined.io/community[Community] 89 | 90 | == Credits 91 | 92 | * Built with link:https://alchemists.io/projects/gemsmith[Gemsmith]. 93 | * Engineered by link:https://undefined.io/team/undefined[Jill Smith]. 94 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/console_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::Console do 6 | using Refinements::Struct 7 | 8 | subject(:builder) { described_class.new settings:, logger: } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | let(:path) { temp_dir.join "test/bin/console" } 14 | 15 | context "when enabled" do 16 | let(:test_configuration) { configuration.minimize.merge build_console: true } 17 | 18 | it "builds file" do 19 | builder.call 20 | 21 | expect(path.read).to eq(<<~CONTENT) 22 | #! /usr/bin/env ruby 23 | 24 | require "bundler/setup" 25 | Bundler.require :tools 26 | 27 | require "test" 28 | require "irb" 29 | 30 | IRB.start __FILE__ 31 | CONTENT 32 | end 33 | 34 | it "answers true" do 35 | expect(builder.call).to be(true) 36 | end 37 | end 38 | 39 | context "when enabled with dashed project name" do 40 | before do 41 | settings.with! settings.minimize.with project_name: "demo-test", build_console: true 42 | end 43 | 44 | let(:path) { temp_dir.join "demo-test/bin/console" } 45 | 46 | it "builds file" do 47 | builder.call 48 | 49 | expect(path.read).to eq(<<~CONTENT) 50 | #! /usr/bin/env ruby 51 | 52 | require "bundler/setup" 53 | Bundler.require :tools 54 | 55 | require "demo/test" 56 | require "irb" 57 | 58 | IRB.start __FILE__ 59 | CONTENT 60 | end 61 | 62 | it "answers true" do 63 | expect(builder.call).to be(true) 64 | end 65 | end 66 | 67 | context "when disabled" do 68 | before { settings.with! settings.minimize } 69 | 70 | it "doesn't build file" do 71 | builder.call 72 | expect(path.exist?).to be(false) 73 | end 74 | 75 | it "answers false" do 76 | expect(builder.call).to be(false) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/bundler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::Bundler do 6 | using Refinements::Struct 7 | 8 | subject(:builder) { described_class.new settings:, logger: } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | context "with minimum flags" do 14 | before { settings.with! settings.minimize } 15 | 16 | it "builds gemspec" do 17 | builder.call 18 | 19 | expect(temp_dir.join("test", "Gemfile").read).to eq(<<~CONTENT) 20 | ruby file: ".ruby-version" 21 | 22 | source "https://rubygems.org" 23 | 24 | gemspec 25 | CONTENT 26 | end 27 | 28 | it "answers true" do 29 | expect(builder.call).to be(true) 30 | end 31 | end 32 | 33 | context "with maximum flags" do 34 | before { settings.with! settings.maximize } 35 | 36 | let :proof do 37 | <<~CONTENT 38 | ruby file: ".ruby-version" 39 | 40 | source "https://rubygems.org" 41 | 42 | gemspec 43 | 44 | gem "bootsnap", "~> 1.18" 45 | 46 | group :quality do 47 | gem "caliber", "~> 0.82" 48 | gem "git-lint", "~> 9.0" 49 | gem "reek", "~> 6.5", require: false 50 | gem "simplecov", "~> 0.22", require: false 51 | end 52 | 53 | group :development do 54 | gem "rake", "~> 13.3" 55 | end 56 | 57 | group :test do 58 | gem "rspec", "~> 3.13" 59 | end 60 | 61 | group :tools do 62 | gem "amazing_print", "~> 2.0" 63 | gem "debug", "~> 1.11" 64 | gem "irb-kit", "~> 1.1" 65 | gem "repl_type_completor", "~> 0.1" 66 | end 67 | CONTENT 68 | end 69 | 70 | it "builds gemspec" do 71 | builder.call 72 | expect(temp_dir.join("test", "Gemfile").read).to eq(proof) 73 | end 74 | 75 | it "answers true" do 76 | expect(builder.call).to be(true) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/circle_ci_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::CircleCI do 6 | using Refinements::Struct 7 | 8 | subject(:builder) { described_class.new settings:, logger: } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | let(:path) { temp_dir.join "test/.circleci/config.yml" } 14 | 15 | context "when enabled" do 16 | before { settings.with! settings.with build_circle_ci: true } 17 | 18 | it "updates file to use gemspec for cache" do 19 | builder.call 20 | 21 | expect(path.read).to include(<<~CONTENT) 22 | version: 2.1 23 | jobs: 24 | build: 25 | working_directory: ~/project 26 | docker: 27 | - image: bkuhlmann/alpine-ruby:latest 28 | steps: 29 | - checkout 30 | 31 | - restore_cache: 32 | name: Gems Restore 33 | keys: 34 | - gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "test.gemspec"}} 35 | - gem-cache- 36 | 37 | - run: 38 | name: Gems Install 39 | command: | 40 | gem update --system 41 | bundle config set path "vendor/bundle" 42 | bundle install 43 | 44 | - save_cache: 45 | name: Gems Store 46 | key: gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "test.gemspec"}} 47 | paths: 48 | - vendor/bundle 49 | 50 | - run: 51 | name: Rake 52 | command: bundle exec rake 53 | CONTENT 54 | end 55 | 56 | it "answers true" do 57 | expect(builder.call).to be(true) 58 | end 59 | end 60 | 61 | context "when disabled" do 62 | before { settings.with! settings.minimize } 63 | 64 | it "doesn't build file" do 65 | builder.call 66 | expect(path.exist?).to be(false) 67 | end 68 | 69 | it "answers false" do 70 | expect(builder.call).to be(false) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | # Builds project skeleton CLI templates. 8 | class CLI < Rubysmith::Builders::Abstract 9 | using Refinements::Struct 10 | 11 | def call 12 | return false unless settings.build_cli 13 | 14 | render 15 | true 16 | end 17 | 18 | private 19 | 20 | def render = private_methods.sort.grep(/render_/).each { |method| __send__ method } 21 | 22 | def render_exe 23 | builder.call(settings.with(template_path: "%project_name%/exe/%project_name%.erb")) 24 | .render 25 | .permit 0o755 26 | end 27 | 28 | def render_core 29 | content = settings.with template_path: "%project_name%/lib/%project_path%.rb.erb" 30 | 31 | builder.call(content) 32 | .insert_before(/tag/, %( loader.inflector.inflect "cli" => "CLI"\n)) 33 | end 34 | 35 | def render_configuration 36 | [ 37 | "%project_name%/lib/%project_path%/configuration/contract.rb.erb", 38 | "%project_name%/lib/%project_path%/configuration/model.rb.erb", 39 | "%project_name%/lib/%project_path%/configuration/defaults.yml.erb", 40 | "%project_name%/lib/%project_path%/container.rb.erb", 41 | "%project_name%/lib/%project_path%/dependencies.rb.erb" 42 | ].each { |path| builder.call(settings.with(template_path: path)).render } 43 | end 44 | 45 | def render_shell 46 | path = "%project_name%/lib/%project_path%/cli/shell.rb.erb" 47 | builder.call(settings.with(template_path: path)).render 48 | end 49 | 50 | def render_requirements 51 | return if settings.build_zeitwerk 52 | 53 | builder.call(settings.with(template_path: "%project_name%/lib/%project_path%.rb.erb")) 54 | .render 55 | .prepend <<~CONTENT 56 | require "demo/configuration/contract" 57 | require "demo/configuration/model" 58 | require "demo/container" 59 | require "demo/dependencies" 60 | 61 | require "demo/cli/shell" 62 | 63 | # Main namespace. 64 | CONTENT 65 | end 66 | 67 | def render_specs 68 | return unless settings.build_rspec 69 | 70 | [ 71 | "%project_name%/spec/lib/%project_path%/cli/shell_spec.rb.erb", 72 | "%project_name%/spec/support/shared_contexts/application_dependencies.rb.erb" 73 | ].each { |path| builder.call(settings.with(template_path: path)).render } 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/gemsmith/templates/%project_name%/%project_name%.gemspec.erb: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "<%= settings.project_name %>" 3 | spec.version = "<%= settings.project_version %>" 4 | spec.authors = ["<%= settings.author_name %>"] 5 | spec.email = ["<%= settings.author_email %>"] 6 | spec.homepage = "<%= settings.computed_project_uri_home %>" 7 | spec.summary = "" 8 | spec.license = "<%= settings.license_label_version %>" 9 | 10 | spec.metadata = { 11 | <% unless settings.computed_project_uri_issues.empty? %> 12 | "bug_tracker_uri" => "<%= settings.computed_project_uri_issues %>", 13 | <% end %> 14 | <% unless settings.computed_project_uri_versions.empty? %> 15 | "changelog_uri" => "<%= settings.computed_project_uri_versions %>", 16 | <% end %> 17 | <% unless settings.computed_project_uri_home.empty? %> 18 | "homepage_uri" => "<%= settings.computed_project_uri_home %>", 19 | <% end %> 20 | <% unless settings.computed_project_uri_funding.empty? %> 21 | "funding_uri" => "<%= settings.computed_project_uri_funding %>", 22 | <% end %> 23 | "label" => "<%= settings.project_label %>", 24 | <% unless settings.computed_project_uri_source.empty? %> 25 | "rubygems_mfa_required" => "true", 26 | "source_code_uri" => "<%= settings.computed_project_uri_source %>" 27 | <% else %> 28 | "rubygems_mfa_required" => "true" 29 | <% end %> 30 | } 31 | 32 | <% if settings.build_security %> 33 | spec.signing_key = Gem.default_key_path 34 | spec.cert_chain = [Gem.default_cert_path] 35 | <% end %> 36 | 37 | spec.required_ruby_version = ">= <%= RUBY_VERSION[/\d+\.\d+/] %>" 38 | <% if settings.build_cli %> 39 | spec.add_dependency "cogger", "~> 1.0" 40 | <% end %> 41 | <% if settings.build_cli %> 42 | spec.add_dependency "containable", "~> 1.1" 43 | <% end %> 44 | <% if settings.build_cli || settings.build_monads %> 45 | spec.add_dependency "dry-monads", "~> 1.9" 46 | <% end %> 47 | <% if settings.build_cli %> 48 | spec.add_dependency "etcher", "~> 3.0" 49 | <% end %> 50 | <% if settings.build_cli %> 51 | spec.add_dependency "infusible", "~> 4.0" 52 | <% end %> 53 | <% if settings.build_refinements %> 54 | spec.add_dependency "refinements", "~> 13.6" 55 | <% end %> 56 | <% if settings.build_cli %> 57 | spec.add_dependency "runcom", "~> 12.0" 58 | <% end %> 59 | <% if settings.build_cli %> 60 | spec.add_dependency "sod", "~> 1.5" 61 | <% end %> 62 | <% if settings.build_cli %> 63 | spec.add_dependency "spek", "~> 4.0" 64 | <% end %> 65 | <% if settings.build_zeitwerk %> 66 | spec.add_dependency "zeitwerk", "~> 2.7" 67 | <% end %> 68 | 69 | <% if settings.build_cli %> 70 | spec.bindir = "exe" 71 | spec.executables << "<%= settings.project_name %>" 72 | <% end %> 73 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 74 | spec.files = Dir["*.gemspec", "lib/**/*"] 75 | end 76 | -------------------------------------------------------------------------------- /lib/gemsmith/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cogger" 4 | require "containable" 5 | require "etcher" 6 | require "open3" 7 | require "runcom" 8 | require "spek" 9 | 10 | module Gemsmith 11 | # Provides a global gem container for injection into other objects. 12 | module Container 13 | extend Containable 14 | 15 | register :registry, as: :fresh do 16 | contract = Rubysmith::Configuration::Contract 17 | model = Rubysmith::Configuration::Model 18 | 19 | Etcher::Registry.new(contract:, model:) 20 | .add_loader(:yaml, self[:defaults_path]) 21 | .add_loader(:yaml, self[:xdg_config].active) 22 | .add_transformer(Rubysmith::Configuration::Transformers::GitHubUser.new) 23 | .add_transformer(Rubysmith::Configuration::Transformers::GitEmail.new) 24 | .add_transformer(Rubysmith::Configuration::Transformers::GitUser.new) 25 | .add_transformer(Rubysmith::Configuration::Transformers::TemplateRoot.new) 26 | .add_transformer( 27 | Rubysmith::Configuration::Transformers::TemplateRoot.new( 28 | default: Pathname(__dir__).join("templates") 29 | ) 30 | ) 31 | .add_transformer(:root, :target_root) 32 | .add_transformer(:format, :author_uri) 33 | .add_transformer(:format, :citation_affiliation) 34 | .add_transformer(:format, :project_uri_community) 35 | .add_transformer(:format, :project_uri_conduct) 36 | .add_transformer(:format, :project_uri_contributions) 37 | .add_transformer(:format, :project_uri_dcoo) 38 | .add_transformer(:format, :project_uri_download, :project_name) 39 | .add_transformer(:format, :project_uri_funding) 40 | .add_transformer(:format, :project_uri_home, :project_name) 41 | .add_transformer(:format, :project_uri_issues, :project_name) 42 | .add_transformer(:format, :project_uri_license) 43 | .add_transformer(:format, :project_uri_security) 44 | .add_transformer(:format, :project_uri_source, :project_name) 45 | .add_transformer(:format, :project_uri_versions, :project_name) 46 | .add_transformer(:time, :loaded_at) 47 | end 48 | 49 | register(:settings) { Etcher.call(self[:registry]).dup } 50 | register(:specification) { Spek::Loader.call "#{__dir__}/../../gemsmith.gemspec" } 51 | register(:defaults_path) { Rubysmith::Container[:defaults_path] } 52 | register(:xdg_config) { Runcom::Config.new "gemsmith/configuration.yml" } 53 | register :environment, ENV 54 | register(:logger) { Cogger.new id: :gemsmith } 55 | register :executor, Open3 56 | register :io, STDOUT 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/documentation/readme_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::Documentation::Readme do 6 | using Refinements::Struct 7 | 8 | subject(:builder) { described_class.new settings:, logger: } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | context "when enabled with ASCII Doc format (minimum)" do 14 | before do 15 | settings.with! settings.minimize.with(build_readme: true, documentation_format: "adoc") 16 | end 17 | 18 | it "builds file" do 19 | builder.call 20 | 21 | expect(temp_dir.join("test/README.adoc").read).to eq( 22 | SPEC_ROOT.join("support/fixtures/readmes/minimum.adoc").read 23 | ) 24 | end 25 | 26 | it "answers true" do 27 | expect(builder.call).to be(true) 28 | end 29 | end 30 | 31 | context "when enabled with ASCII Doc format (maximum)" do 32 | before do 33 | settings.with! settings.maximize.with( 34 | documentation_format: "adoc", 35 | project_name: "test-example" 36 | ) 37 | end 38 | 39 | it "builds file" do 40 | builder.call 41 | 42 | expect(temp_dir.join("test-example/README.adoc").read).to eq( 43 | SPEC_ROOT.join("support/fixtures/readmes/maximum.adoc").read 44 | ) 45 | end 46 | 47 | it "answers true" do 48 | expect(builder.call).to be(true) 49 | end 50 | end 51 | 52 | context "when enabled with Markdown format (minimum)" do 53 | before do 54 | settings.with! settings.minimize.with(build_readme: true, documentation_format: "md") 55 | end 56 | 57 | it "builds file" do 58 | builder.call 59 | 60 | expect(temp_dir.join("test/README.md").read).to eq( 61 | SPEC_ROOT.join("support/fixtures/readmes/minimum.md").read 62 | ) 63 | end 64 | 65 | it "answers true" do 66 | expect(builder.call).to be(true) 67 | end 68 | end 69 | 70 | context "when enabled with Markdown format (maximum)" do 71 | before do 72 | settings.with! settings.maximize.with( 73 | documentation_format: "md", 74 | project_name: "test-example" 75 | ) 76 | end 77 | 78 | it "builds file" do 79 | builder.call 80 | 81 | expect(temp_dir.join("test-example/README.md").read).to eq( 82 | SPEC_ROOT.join("support/fixtures/readmes/maximum.md").read 83 | ) 84 | end 85 | 86 | it "answers true" do 87 | expect(builder.call).to be(true) 88 | end 89 | end 90 | 91 | context "when disabled" do 92 | before { settings.with! settings.minimize } 93 | 94 | it "doesn't build file" do 95 | builder.call 96 | expect(temp_dir.join("test/README.adoc").exist?).to be(false) 97 | end 98 | 99 | it "answers false" do 100 | expect(builder.call).to be(false) 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/tools/pusher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Tools::Pusher do 6 | subject(:pusher) { described_class.new command: } 7 | 8 | include_context "with application dependencies" 9 | 10 | let(:command) { instance_spy Gem::CommandManager, run: nil } 11 | 12 | describe "#call" do 13 | let(:executor) { class_double Open3, capture3: ["abc", "Unknown", status] } 14 | let(:status) { instance_double Process::Status } 15 | 16 | context "when YubiKey Manager exists and One-Time Password is obtained" do 17 | before { allow(status).to receive(:success?).and_return(true, true) } 18 | 19 | it "pushes with One-Time Password" do 20 | pusher.call specification 21 | 22 | expect(command).to have_received(:run).with( 23 | ["push", "tmp/gemsmith-test-0.0.0.gem", "--otp", "abc"] 24 | ) 25 | end 26 | 27 | it "answers specification" do 28 | result = pusher.call specification 29 | expect(result.success).to eq(specification) 30 | end 31 | 32 | it "answers error message with gem exception failure" do 33 | allow(command).to receive(:run).and_raise Gem::Exception, "Exception!" 34 | result = pusher.call specification 35 | 36 | expect(result.failure).to eq("Exception!") 37 | end 38 | end 39 | 40 | context "when YubiKey Manager exists and One-Time Password can't be obtained" do 41 | before { allow(status).to receive(:success?).and_return(true, false) } 42 | 43 | it "pushes without One-Time Password" do 44 | pusher.call specification 45 | expect(command).to have_received(:run).with(["push", "tmp/gemsmith-test-0.0.0.gem"]) 46 | end 47 | 48 | it "answers specification" do 49 | result = pusher.call specification 50 | expect(result.success).to eq(specification) 51 | end 52 | end 53 | 54 | context "when YubiKey Manager doesn't exist" do 55 | before { allow(status).to receive(:success?).and_return(false, true) } 56 | 57 | it "pushes without One-Time Password" do 58 | pusher.call specification 59 | expect(command).to have_received(:run).with(["push", "tmp/gemsmith-test-0.0.0.gem"]) 60 | end 61 | 62 | it "logs warning" do 63 | pusher.call specification 64 | expect(logger.reread).to match(/🔎.+Unable to find YubiKey Manager. Unknown./) 65 | end 66 | 67 | it "answers specification" do 68 | result = pusher.call specification 69 | expect(result.success).to eq(specification) 70 | end 71 | end 72 | 73 | context "when system error is raised" do 74 | before { allow(executor).to receive(:capture3).and_raise(Errno::ENOENT, "Danger") } 75 | 76 | it "pushes without One-Time Password" do 77 | pusher.call specification 78 | expect(command).to have_received(:run).with(["push", "tmp/gemsmith-test-0.0.0.gem"]) 79 | end 80 | 81 | it "logs warning" do 82 | pusher.call specification 83 | 84 | expect(logger.reread).to match( 85 | /🔎.+Unable to obtain YubiKey One-Time Password. No such file or directory - Danger./ 86 | ) 87 | end 88 | 89 | it "answers specification" do 90 | result = pusher.call specification 91 | expect(result.success).to eq(specification) 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/cli/shell_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::CLI::Shell do 6 | using Refinements::Pathname 7 | using Refinements::Struct 8 | using Refinements::StringIO 9 | 10 | subject(:shell) { described_class.new } 11 | 12 | include_context "with application dependencies" 13 | 14 | before { Sod::Container.stub! logger:, io: } 15 | 16 | after { Sod::Container.restore } 17 | 18 | describe "#call" do 19 | let :bom_minimum do 20 | ["test/.ruby-version", "test/Gemfile", "test/lib/test.rb", "test/test.gemspec"].compact 21 | end 22 | 23 | let :bom_maximum do 24 | SPEC_ROOT.join("support/fixtures/boms/maximum.txt").each_line(chomp: true).compact 25 | end 26 | 27 | let :project_files do 28 | temp_dir.join("test") 29 | .files("**/*", flag: File::FNM_DOTMATCH) 30 | .reject { |path| path.fnmatch?("*git/*") && !path.fnmatch?("*git/HEAD") } 31 | .reject { |path| path.fnmatch? "*tags" } 32 | .map { |path| path.relative_path_from(temp_dir).to_s } 33 | end 34 | 35 | it "prints configuration usage" do 36 | shell.call %w[config] 37 | expect(io.reread).to match(/Manage configuration.+/m) 38 | end 39 | 40 | context "with minimum forced build" do 41 | let(:options) { %w[build --name test --min] } 42 | 43 | it "builds minimum skeleton" do 44 | temp_dir.change_dir do 45 | shell.call options 46 | expect(project_files).to match_array(bom_minimum) 47 | end 48 | end 49 | end 50 | 51 | context "with minimum optional build" do 52 | let :options do 53 | SPEC_ROOT.join("support/fixtures/arguments/minimum.txt").readlines chomp: true 54 | end 55 | 56 | it "builds minimum skeleton" do 57 | temp_dir.change_dir do 58 | shell.call options 59 | expect(project_files).to match_array(bom_minimum) 60 | end 61 | end 62 | end 63 | 64 | context "with maximum forced build" do 65 | let(:options) { %w[build --name test --max] } 66 | 67 | it "builds maximum skeleton" do 68 | temp_dir.change_dir do 69 | shell.call options 70 | expect(project_files).to match_array(bom_maximum) 71 | end 72 | end 73 | end 74 | 75 | context "with maximum optional build" do 76 | let :options do 77 | SPEC_ROOT.join("support/fixtures/arguments/maximum.txt").readlines chomp: true 78 | end 79 | 80 | it "builds maximum skeleton" do 81 | temp_dir.change_dir do 82 | shell.call options 83 | expect(project_files).to match_array(bom_maximum) 84 | end 85 | end 86 | end 87 | 88 | it "attempts to install gem" do 89 | temp_dir.change_dir do 90 | expectation = proc { shell.call %w[--install test-0.0.0.gem] } 91 | expect(&expectation).to raise_error(Gem::SystemExitException) 92 | end 93 | end 94 | 95 | it "attempts to publish gem" do 96 | temp_dir.change_dir do 97 | expectation = proc { shell.call %w[--publish test-0.0.0.gem] } 98 | expect(&expectation).to raise_error(Gem::SystemExitException) 99 | end 100 | end 101 | 102 | it "attempts to edit gem" do 103 | shell.call %w[--edit test] 104 | expect(logger.reread).to match(/🛑.+Unknown.+gem.+test/) 105 | end 106 | 107 | it "attempts to view gem" do 108 | shell.call %w[--view test] 109 | expect(logger.reread).to match(/🛑.+Unknown.+gem.+test/) 110 | end 111 | 112 | it "prints version" do 113 | shell.call %w[--version] 114 | expect(io.reread).to match(/Gemsmith\s\d+\.\d+\.\d+/) 115 | end 116 | 117 | it "prints help" do 118 | shell.call %w[--help] 119 | expect(io.reread).to match(/Gemsmith.+USAGE.+/m) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/gemsmith/builders/documentation/readme.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/struct" 4 | 5 | module Gemsmith 6 | module Builders 7 | module Documentation 8 | # Builds project skeleton README documentation. 9 | class Readme < Rubysmith::Builders::Documentation::Readme 10 | using Refinements::Struct 11 | 12 | def call 13 | return false unless settings.build_readme 14 | 15 | super 16 | builder.call(settings.with(template_path: "%project_name%/README.#{kind}.erb")) 17 | .replace(/Setup.+Usage/m, setup) 18 | .replace("Rubysmith", "Gemsmith") 19 | .replace("rubysmith", "gemsmith") 20 | 21 | true 22 | end 23 | 24 | private 25 | 26 | def setup = kind == "adoc" ? ascii_setup : markdown_setup 27 | 28 | def ascii_setup = settings.build_security ? ascii_secure : ascii_insecure 29 | 30 | def ascii_secure 31 | project_name = settings.project_name 32 | 33 | <<~CONTENT.strip 34 | Setup 35 | 36 | To install _with_ security, run: 37 | 38 | [source,bash] 39 | ---- 40 | # 💡 Skip this line if you already have the public certificate installed. 41 | gem cert --add <(curl --compressed --location #{settings.organization_uri}/gems.pem) 42 | gem install #{project_name} --trust-policy HighSecurity 43 | ---- 44 | 45 | To install _without_ security, run: 46 | 47 | [source,bash] 48 | ---- 49 | gem install #{project_name} 50 | ---- 51 | 52 | #{ascii_common} 53 | CONTENT 54 | end 55 | 56 | def ascii_insecure 57 | <<~CONTENT.strip 58 | Setup 59 | 60 | To install, run: 61 | 62 | [source,bash] 63 | ---- 64 | gem install #{settings.project_name} 65 | ---- 66 | 67 | #{ascii_common} 68 | CONTENT 69 | end 70 | 71 | def ascii_common 72 | <<~CONTENT.strip 73 | You can also add the gem directly to your project: 74 | 75 | [source,bash] 76 | ---- 77 | bundle add #{settings.project_name} 78 | ---- 79 | 80 | Once the gem is installed, you only need to require it: 81 | 82 | [source,ruby] 83 | ---- 84 | require "#{settings.project_path}" 85 | ---- 86 | 87 | == Usage 88 | CONTENT 89 | end 90 | 91 | def markdown_setup = settings.build_security ? markdown_secure : markdown_insecure 92 | 93 | def markdown_secure 94 | project_name = settings.project_name 95 | 96 | <<~CONTENT.strip 97 | Setup 98 | 99 | To install _with_ security, run: 100 | 101 | # 💡 Skip this line if you already have the public certificate installed. 102 | gem cert --add <(curl --compressed --location #{settings.organization_uri}/gems.pem) 103 | gem install #{project_name} --trust-policy HighSecurity 104 | 105 | To install _without_ security, run: 106 | 107 | gem install #{project_name} 108 | 109 | #{markdown_common} 110 | CONTENT 111 | end 112 | 113 | def markdown_insecure 114 | <<~CONTENT.strip 115 | Setup 116 | 117 | To install, run: 118 | 119 | gem install #{settings.project_name} 120 | 121 | #{markdown_common} 122 | CONTENT 123 | end 124 | 125 | def markdown_common 126 | <<~CONTENT.strip 127 | You can also add the gem directly to your project: 128 | 129 | bundle add #{settings.project_name} 130 | 131 | Once the gem is installed, you only need to require it: 132 | 133 | require "#{settings.project_path}" 134 | 135 | ## Usage 136 | CONTENT 137 | end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/specification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::Specification do 6 | using Refinements::Struct 7 | 8 | subject(:builder) { described_class.new settings:, logger: } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe "#call" do 13 | context "with minimum flags" do 14 | before do 15 | settings.with! settings.minimize.with( 16 | project_uri_community: nil, 17 | project_uri_conduct: nil, 18 | project_uri_contributions: nil, 19 | project_uri_download: nil, 20 | project_uri_funding: nil, 21 | project_uri_home: nil, 22 | project_uri_issues: nil, 23 | project_uri_license: nil, 24 | project_uri_security: nil, 25 | project_uri_source: nil, 26 | project_uri_versions: nil 27 | ) 28 | end 29 | 30 | it "builds gemspec" do 31 | builder.call 32 | 33 | expect(temp_dir.join("test", "test.gemspec").read).to eq( 34 | SPEC_ROOT.join("support/fixtures/test-minimum.gemspec").read 35 | ) 36 | end 37 | 38 | it "answers true" do 39 | expect(builder.call).to be(true) 40 | end 41 | end 42 | 43 | context "with minimum flags plus security" do 44 | before do 45 | settings.with! settings.minimize.with( 46 | build_security: true, 47 | project_uri_community: nil, 48 | project_uri_conduct: nil, 49 | project_uri_contributions: nil, 50 | project_uri_download: nil, 51 | project_uri_funding: nil, 52 | project_uri_home: nil, 53 | project_uri_issues: nil, 54 | project_uri_license: nil, 55 | project_uri_security: nil, 56 | project_uri_source: nil, 57 | project_uri_versions: nil 58 | ) 59 | end 60 | 61 | it "builds gemspec" do 62 | builder.call 63 | 64 | expect(temp_dir.join("test", "test.gemspec").read).to eq( 65 | SPEC_ROOT.join("support/fixtures/test-minimum-security.gemspec").read 66 | ) 67 | end 68 | 69 | it "answers true" do 70 | expect(builder.call).to be(true) 71 | end 72 | end 73 | 74 | context "with minimum flags plus CLI" do 75 | before do 76 | settings.with! settings.minimize.with( 77 | build_cli: true, 78 | build_refinements: true, 79 | build_zeitwerk: true, 80 | project_uri_community: nil, 81 | project_uri_conduct: nil, 82 | project_uri_contributions: nil, 83 | project_uri_download: nil, 84 | project_uri_funding: nil, 85 | project_uri_home: nil, 86 | project_uri_issues: nil, 87 | project_uri_license: nil, 88 | project_uri_security: nil, 89 | project_uri_source: nil, 90 | project_uri_versions: nil 91 | ) 92 | end 93 | 94 | it "builds gemspec" do 95 | builder.call 96 | 97 | expect(temp_dir.join("test", "test.gemspec").read).to eq( 98 | SPEC_ROOT.join("support/fixtures/test-minimum-cli.gemspec").read 99 | ) 100 | end 101 | 102 | it "answers true" do 103 | expect(builder.call).to be(true) 104 | end 105 | end 106 | 107 | context "with minimum flags plus monads" do 108 | before do 109 | settings.with! settings.minimize.with( 110 | build_monads: true, 111 | project_uri_community: nil, 112 | project_uri_conduct: nil, 113 | project_uri_contributions: nil, 114 | project_uri_download: nil, 115 | project_uri_funding: nil, 116 | project_uri_home: nil, 117 | project_uri_issues: nil, 118 | project_uri_license: nil, 119 | project_uri_security: nil, 120 | project_uri_source: nil, 121 | project_uri_versions: nil 122 | ) 123 | end 124 | 125 | it "builds gemspec" do 126 | builder.call 127 | 128 | expect(temp_dir.join("test", "test.gemspec").read).to eq( 129 | SPEC_ROOT.join("support/fixtures/test-minimum-monads.gemspec").read 130 | ) 131 | end 132 | 133 | it "answers true" do 134 | expect(builder.call).to be(true) 135 | end 136 | end 137 | 138 | context "with maximum flags" do 139 | before { settings.with! settings.maximize } 140 | 141 | it "builds gemspec" do 142 | builder.call 143 | 144 | expect(temp_dir.join("test", "test.gemspec").read).to eq( 145 | SPEC_ROOT.join("support/fixtures/test-maximum.gemspec").read 146 | ) 147 | end 148 | 149 | it "answers true" do 150 | expect(builder.call).to be(true) 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/gemsmith/cli/commands/build.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Gemsmith 6 | module CLI 7 | module Commands 8 | # Handles the build action. 9 | class Build < Sod::Command 10 | include Dependencies[:settings, :logger] 11 | 12 | # Order is important. 13 | BUILDERS = [ 14 | Rubysmith::Builders::Init, 15 | Rubysmith::Builders::Core, 16 | Rubysmith::Builders::Version, 17 | Builders::Specification, 18 | Rubysmith::Builders::Documentation::Readme, 19 | Builders::Documentation::Readme, 20 | Rubysmith::Builders::Documentation::Citation, 21 | Rubysmith::Builders::Documentation::License, 22 | Rubysmith::Builders::Documentation::Version, 23 | Rubysmith::Builders::Git::Setup, 24 | Builders::Git::Ignore, 25 | Rubysmith::Builders::Git::Safe, 26 | Builders::Bundler, 27 | Builders::CLI, 28 | Rubysmith::Builders::Rake::Binstub, 29 | Rubysmith::Builders::Rake::Configuration, 30 | Builders::Console, 31 | Builders::CircleCI, 32 | Rubysmith::Builders::Setup, 33 | Rubysmith::Builders::GitHub::Template, 34 | Rubysmith::Builders::GitHub::Funding, 35 | Rubysmith::Builders::GitHub::CI, 36 | Rubysmith::Builders::Reek, 37 | Rubysmith::Builders::RSpec::Binstub, 38 | Rubysmith::Builders::RSpec::Context, 39 | Builders::RSpec::Helper, 40 | Rubysmith::Builders::Caliber, 41 | Rubysmith::Builders::DevContainer::Dockerfile, 42 | Rubysmith::Builders::DevContainer::Compose, 43 | Rubysmith::Builders::DevContainer::Configuration, 44 | Rubysmith::Builders::Docker::Build, 45 | Rubysmith::Builders::Docker::Console, 46 | Rubysmith::Builders::Docker::Entrypoint, 47 | Rubysmith::Builders::Docker::File, 48 | Rubysmith::Builders::Docker::Ignore, 49 | Rubysmith::Extensions::Bundler, 50 | Rubysmith::Extensions::Pragmater, 51 | Rubysmith::Extensions::Tocer, 52 | Rubysmith::Extensions::Rubocop, 53 | Builders::Git::Commit 54 | ].freeze 55 | 56 | handle "build" 57 | 58 | description "Build new project." 59 | 60 | on Rubysmith::CLI::Actions::Name, settings: Container[:settings] 61 | on Rubysmith::CLI::Actions::AmazingPrint, settings: Container[:settings] 62 | on Rubysmith::CLI::Actions::Bootsnap, settings: Container[:settings] 63 | on Rubysmith::CLI::Actions::Caliber, settings: Container[:settings] 64 | on Rubysmith::CLI::Actions::CircleCI, settings: Container[:settings] 65 | on Rubysmith::CLI::Actions::Citation, settings: Container[:settings] 66 | on Actions::CLI 67 | on Rubysmith::CLI::Actions::Community, settings: Container[:settings] 68 | on Rubysmith::CLI::Actions::Conduct, settings: Container[:settings] 69 | on Rubysmith::CLI::Actions::Console, settings: Container[:settings] 70 | on Rubysmith::CLI::Actions::Contributions, settings: Container[:settings] 71 | on Rubysmith::CLI::Actions::DCOO, settings: Container[:settings] 72 | on Rubysmith::CLI::Actions::Debug, settings: Container[:settings] 73 | on Rubysmith::CLI::Actions::DevContainer, settings: Container[:settings] 74 | on Rubysmith::CLI::Actions::Docker, settings: Container[:settings] 75 | on Rubysmith::CLI::Actions::Funding, settings: Container[:settings] 76 | on Rubysmith::CLI::Actions::Git, settings: Container[:settings] 77 | on Rubysmith::CLI::Actions::GitHub, settings: Container[:settings] 78 | on Rubysmith::CLI::Actions::GitHubCI, settings: Container[:settings] 79 | on Rubysmith::CLI::Actions::GitLint, settings: Container[:settings] 80 | on Rubysmith::CLI::Actions::IRBKit, settings: Container[:settings] 81 | on Rubysmith::CLI::Actions::License, settings: Container[:settings] 82 | on Rubysmith::CLI::Actions::Maximum, settings: Container[:settings] 83 | on Rubysmith::CLI::Actions::Minimum, settings: Container[:settings] 84 | on Rubysmith::CLI::Actions::Monads, settings: Container[:settings] 85 | on Rubysmith::CLI::Actions::Rake, settings: Container[:settings] 86 | on Rubysmith::CLI::Actions::Readme, settings: Container[:settings] 87 | on Rubysmith::CLI::Actions::Reek, settings: Container[:settings] 88 | on Rubysmith::CLI::Actions::Refinements, settings: Container[:settings] 89 | on Rubysmith::CLI::Actions::RSpec, settings: Container[:settings] 90 | on Rubysmith::CLI::Actions::RTC, settings: Container[:settings] 91 | on Rubysmith::CLI::Actions::Security, settings: Container[:settings] 92 | on Rubysmith::CLI::Actions::Setup, settings: Container[:settings] 93 | on Rubysmith::CLI::Actions::SimpleCov, settings: Container[:settings] 94 | on Rubysmith::CLI::Actions::Versions, settings: Container[:settings] 95 | on Rubysmith::CLI::Actions::Zeitwerk, settings: Container[:settings] 96 | 97 | def initialize(builders: BUILDERS, **) 98 | super(**) 99 | @builders = builders 100 | end 101 | 102 | def call 103 | log_info "Building project skeleton: #{settings.project_name}..." 104 | builders.each { |builder| builder.new(settings:, logger:).call } 105 | log_info "Project skeleton complete!" 106 | end 107 | 108 | private 109 | 110 | attr_reader :builders 111 | 112 | def log_info(message) = logger.info { message } 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /LICENSE.adoc: -------------------------------------------------------------------------------- 1 | = Hippocratic License 2 | 3 | Version: 2.1.0. 4 | 5 | Purpose. The purpose of this License is for the Licensor named above to 6 | permit the Licensee (as defined below) broad permission, if consistent 7 | with Human Rights Laws and Human Rights Principles (as each is defined 8 | below), to use and work with the Software (as defined below) within the 9 | full scope of Licensor’s copyright and patent rights, if any, in the 10 | Software, while ensuring attribution and protecting the Licensor from 11 | liability. 12 | 13 | Permission and Conditions. The Licensor grants permission by this 14 | license ("License"), free of charge, to the extent of Licensor’s 15 | rights under applicable copyright and patent law, to any person or 16 | entity (the "Licensee") obtaining a copy of this software and 17 | associated documentation files (the "Software"), to do everything with 18 | the Software that would otherwise infringe (i) the Licensor’s copyright 19 | in the Software or (ii) any patent claims to the Software that the 20 | Licensor can license or becomes able to license, subject to all of the 21 | following terms and conditions: 22 | 23 | * Acceptance. This License is automatically offered to every person and 24 | entity subject to its terms and conditions. Licensee accepts this 25 | License and agrees to its terms and conditions by taking any action with 26 | the Software that, absent this License, would infringe any intellectual 27 | property right held by Licensor. 28 | * Notice. Licensee must ensure that everyone who gets a copy of any part 29 | of this Software from Licensee, with or without changes, also receives 30 | the License and the above copyright notice (and if included by the 31 | Licensor, patent, trademark and attribution notice). Licensee must cause 32 | any modified versions of the Software to carry prominent notices stating 33 | that Licensee changed the Software. For clarity, although Licensee is 34 | free to create modifications of the Software and distribute only the 35 | modified portion created by Licensee with additional or different terms, 36 | the portion of the Software not modified must be distributed pursuant to 37 | this License. If anyone notifies Licensee in writing that Licensee has 38 | not complied with this Notice section, Licensee can keep this License by 39 | taking all practical steps to comply within 30 days after the notice. If 40 | Licensee does not do so, Licensee’s License (and all rights licensed 41 | hereunder) shall end immediately. 42 | * Compliance with Human Rights Principles and Human Rights Laws. 43 | [arabic] 44 | . Human Rights Principles. 45 | [loweralpha] 46 | .. Licensee is advised to consult the articles of the United Nations 47 | Universal Declaration of Human Rights and the United Nations Global 48 | Compact that define recognized principles of international human rights 49 | (the "Human Rights Principles"). Licensee shall use the Software in a 50 | manner consistent with Human Rights Principles. 51 | .. Unless the Licensor and Licensee agree otherwise, any dispute, 52 | controversy, or claim arising out of or relating to (i) Section 1(a) 53 | regarding Human Rights Principles, including the breach of Section 1(a), 54 | termination of this License for breach of the Human Rights Principles, 55 | or invalidity of Section 1(a) or (ii) a determination of whether any Law 56 | is consistent or in conflict with Human Rights Principles pursuant to 57 | Section 2, below, shall be settled by arbitration in accordance with the 58 | Hague Rules on Business and Human Rights Arbitration (the "Rules"); 59 | provided, however, that Licensee may elect not to participate in such 60 | arbitration, in which event this License (and all rights licensed 61 | hereunder) shall end immediately. The number of arbitrators shall be one 62 | unless the Rules require otherwise. 63 | + 64 | Unless both the Licensor and Licensee agree to the contrary: (1) All 65 | documents and information concerning the arbitration shall be public and 66 | may be disclosed by any party; (2) The repository referred to under 67 | Article 43 of the Rules shall make available to the public in a timely 68 | manner all documents concerning the arbitration which are communicated 69 | to it, including all submissions of the parties, all evidence admitted 70 | into the record of the proceedings, all transcripts or other recordings 71 | of hearings and all orders, decisions and awards of the arbitral 72 | tribunal, subject only to the arbitral tribunal’s powers to take such 73 | measures as may be necessary to safeguard the integrity of the arbitral 74 | process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3) 75 | Article 26(6) of the Rules shall not apply. 76 | . Human Rights Laws. The Software shall not be used by any person or 77 | entity for any systems, activities, or other uses that violate any Human 78 | Rights Laws. "Human Rights Laws" means any applicable laws, 79 | regulations, or rules (collectively, "Laws") that protect human, 80 | civil, labor, privacy, political, environmental, security, economic, due 81 | process, or similar rights; provided, however, that such Laws are 82 | consistent and not in conflict with Human Rights Principles (a dispute 83 | over the consistency or a conflict between Laws and Human Rights 84 | Principles shall be determined by arbitration as stated above). Where 85 | the Human Rights Laws of more than one jurisdiction are applicable or in 86 | conflict with respect to the use of the Software, the Human Rights Laws 87 | that are most protective of the individuals or groups harmed shall 88 | apply. 89 | . Indemnity. Licensee shall hold harmless and indemnify Licensor (and 90 | any other contributor) against all losses, damages, liabilities, 91 | deficiencies, claims, actions, judgments, settlements, interest, awards, 92 | penalties, fines, costs, or expenses of whatever kind, including 93 | Licensor’s reasonable attorneys’ fees, arising out of or relating to 94 | Licensee’s use of the Software in violation of Human Rights Laws or 95 | Human Rights Principles. 96 | * Failure to Comply. Any failure of Licensee to act according to the 97 | terms and conditions of this License is both a breach of the License and 98 | an infringement of the intellectual property rights of the Licensor 99 | (subject to exceptions under Laws, e.g., fair use). In the event of a 100 | breach or infringement, the terms and conditions of this License may be 101 | enforced by Licensor under the Laws of any jurisdiction to which 102 | Licensee is subject. Licensee also agrees that the Licensor may enforce 103 | the terms and conditions of this License against Licensee through 104 | specific performance (or similar remedy under Laws) to the extent 105 | permitted by Laws. For clarity, except in the event of a breach of this 106 | License, infringement, or as otherwise stated in this License, Licensor 107 | may not terminate this License with Licensee. 108 | * Enforceability and Interpretation. If any term or provision of this 109 | License is determined to be invalid, illegal, or unenforceable by a 110 | court of competent jurisdiction, then such invalidity, illegality, or 111 | unenforceability shall not affect any other term or provision of this 112 | License or invalidate or render unenforceable such term or provision in 113 | any other jurisdiction; provided, however, subject to a court 114 | modification pursuant to the immediately following sentence, if any term 115 | or provision of this License pertaining to Human Rights Laws or Human 116 | Rights Principles is deemed invalid, illegal, or unenforceable against 117 | Licensee by a court of competent jurisdiction, all rights in the 118 | Software granted to Licensee shall be deemed null and void as between 119 | Licensor and Licensee. Upon a determination that any term or provision 120 | is invalid, illegal, or unenforceable, to the extent permitted by Laws, 121 | the court may modify this License to affect the original purpose that 122 | the Software be used in compliance with Human Rights Principles and 123 | Human Rights Laws as closely as possible. The language in this License 124 | shall be interpreted as to its fair meaning and not strictly for or 125 | against any party. 126 | * Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES 127 | "AS IS," WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND 128 | ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR 129 | OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE 130 | OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM. 131 | 132 | This Hippocratic License is an link:https://ethicalsource.dev[Ethical Source license] and is offered 133 | for use by licensors and licensees at their own risk, on an "AS IS" basis, and with no warranties 134 | express or implied, to the maximum extent permitted by Laws. 135 | -------------------------------------------------------------------------------- /spec/lib/gemsmith/builders/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Gemsmith::Builders::CLI do 6 | using Refinements::Struct 7 | using Refinements::Pathname 8 | 9 | subject(:builder) { described_class.new settings:, logger: } 10 | 11 | include_context "with application dependencies" 12 | 13 | describe "#call" do 14 | let(:fixtures_root) { SPEC_ROOT.join "support/fixtures" } 15 | 16 | context "when enabled" do 17 | before do 18 | settings.with! settings.minimize.with( 19 | build_cli: true, 20 | build_refinements: true, 21 | build_zeitwerk: true 22 | ) 23 | 24 | Rubysmith::Builders::Core.new(settings:, logger:).call 25 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 26 | end 27 | 28 | it "builds executable" do 29 | builder.call 30 | 31 | expect(temp_dir.join("test/exe/test").read).to eq(<<~CONTENT) 32 | #! /usr/bin/env ruby 33 | 34 | require "test" 35 | 36 | Test::CLI::Shell.new.call 37 | CONTENT 38 | end 39 | 40 | it "sets executable as executable" do 41 | builder.call 42 | expect(temp_dir.join("test/exe/test").stat.mode).to eq(33261) 43 | end 44 | 45 | it "builds CLI shell" do 46 | builder.call 47 | 48 | expect(temp_dir.join("test/lib/test/cli/shell.rb").read).to eq( 49 | SPEC_ROOT.join("support/fixtures/lib/cli/shell.rb").read 50 | ) 51 | end 52 | 53 | it "builds configuration defaults" do 54 | builder.call 55 | expect(temp_dir.join("test/lib/test/configuration/defaults.yml").read).to eq( 56 | %(todo: "Add your own attributes here."\n) 57 | ) 58 | end 59 | 60 | it "builds configuration contract" do 61 | builder.call 62 | 63 | expect(temp_dir.join("test/lib/test/configuration/contract.rb").read).to eq(<<~CONTENT) 64 | require "dry/schema" 65 | 66 | Dry::Schema.load_extensions :monads 67 | 68 | module Test 69 | module Configuration 70 | Contract = Dry::Schema.Params 71 | end 72 | end 73 | CONTENT 74 | end 75 | 76 | it "builds configuration model" do 77 | builder.call 78 | 79 | expect(temp_dir.join("test/lib/test/configuration/model.rb").read).to eq(<<~CONTENT) 80 | module Test 81 | module Configuration 82 | # Defines the configuration model for use throughout the gem. 83 | Model = Struct.new 84 | end 85 | end 86 | CONTENT 87 | end 88 | 89 | it "builds application container" do 90 | builder.call 91 | 92 | expect(temp_dir.join("test/lib/test/container.rb").read).to eq( 93 | SPEC_ROOT.join("support/fixtures/lib/container.rb").read 94 | ) 95 | end 96 | 97 | it "builds application import" do 98 | builder.call 99 | 100 | expect(temp_dir.join("test/lib/test/dependencies.rb").read).to eq(<<~CONTENT) 101 | require "infusible" 102 | 103 | module Test 104 | Dependencies = Infusible[Container] 105 | end 106 | CONTENT 107 | end 108 | end 109 | 110 | context "when enabled without Zeitwerk" do 111 | before do 112 | settings.with! settings.minimize.with(build_cli: true) 113 | 114 | Rubysmith::Builders::Core.new(settings:, logger:).call 115 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 116 | end 117 | 118 | it "builds requirements" do 119 | builder.call 120 | 121 | expect(temp_dir.join("test/lib/test.rb").read).to eq(<<~CONTENT) 122 | require "demo/configuration/contract" 123 | require "demo/configuration/model" 124 | require "demo/container" 125 | require "demo/dependencies" 126 | 127 | require "demo/cli/shell" 128 | 129 | # Main namespace. 130 | module Test 131 | end 132 | CONTENT 133 | end 134 | end 135 | 136 | context "when enabled with RSpec" do 137 | before do 138 | settings.with! settings.minimize.with( 139 | build_cli: true, 140 | build_refinements: true, 141 | build_rspec: true, 142 | build_zeitwerk: true 143 | ) 144 | 145 | Rubysmith::Builders::Core.new(settings:, logger:).call 146 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 147 | end 148 | 149 | it "builds RSpec CLI shell spec" do 150 | builder.call 151 | 152 | expect(temp_dir.join("test/spec/lib/test/cli/shell_spec.rb").read).to eq( 153 | fixtures_root.join("spec/lib/cli/shell_with_refinements_proof.rb").read 154 | ) 155 | end 156 | 157 | it "builds RSpec application container shared context" do 158 | template_path = temp_dir.join( 159 | "test/spec/support/shared_contexts/application_dependencies.rb" 160 | ) 161 | fixture_path = fixtures_root.join "spec/support/shared_contexts/application_dependencies.rb" 162 | 163 | builder.call 164 | 165 | expect(template_path.read).to eq(fixture_path.read) 166 | end 167 | end 168 | 169 | context "when enabled with RSpec but without Refinements" do 170 | before do 171 | settings.with! settings.minimize.with( 172 | build_cli: true, 173 | build_rspec: true, 174 | build_zeitwerk: true 175 | ) 176 | 177 | Rubysmith::Builders::Core.new(settings:, logger:).call 178 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 179 | end 180 | 181 | it "builds RSpec CLI shell spec" do 182 | builder.call 183 | 184 | expect(temp_dir.join("test/spec/lib/test/cli/shell_spec.rb").read).to eq( 185 | fixtures_root.join("spec/lib/cli/shell_without_refinements_proof.rb").read 186 | ) 187 | end 188 | 189 | it "builds RSpec application container shared context" do 190 | template_path = temp_dir.join( 191 | "test/spec/support/shared_contexts/application_dependencies.rb" 192 | ) 193 | 194 | builder.call 195 | 196 | expect(template_path.read).to eq(<<~CONTENT) 197 | RSpec.shared_context "with application dependencies" do 198 | 199 | let(:settings) { Test::Container[:settings] } 200 | let(:logger) { Cogger.new id: "test", io: StringIO.new, level: :debug } 201 | let(:io) { StringIO.new } 202 | 203 | before { Demo::Container.stub! logger:, io: } 204 | 205 | after { Test::Container.restore } 206 | end 207 | CONTENT 208 | end 209 | end 210 | 211 | context "when enabled with simple project name" do 212 | before do 213 | settings.with! settings.minimize.with( 214 | build_cli: true, 215 | build_refinements: true, 216 | build_zeitwerk: true 217 | ) 218 | 219 | Rubysmith::Builders::Core.new(settings:, logger:).call 220 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 221 | end 222 | 223 | it "adds CLI inflection" do 224 | builder.call 225 | 226 | expect(temp_dir.join("test/lib/test.rb").read).to eq(<<~CONTENT) 227 | require "zeitwerk" 228 | 229 | Zeitwerk::Loader.new.then do |loader| 230 | loader.inflector.inflect "cli" => "CLI" 231 | loader.tag = File.basename __FILE__, ".rb" 232 | loader.push_dir __dir__ 233 | loader.setup 234 | end 235 | 236 | # Main namespace. 237 | module Test 238 | def self.loader registry = Zeitwerk::Registry 239 | @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") } 240 | end 241 | 242 | end 243 | CONTENT 244 | end 245 | end 246 | 247 | context "when enabled with dashed project name" do 248 | before do 249 | settings.with! settings.minimize.with( 250 | build_cli: true, 251 | build_refinements: true, 252 | build_rspec: true, 253 | build_zeitwerk: true, 254 | project_name: "demo-test" 255 | ) 256 | 257 | Rubysmith::Builders::Core.new(settings:, logger:).call 258 | Rubysmith::Builders::Bundler.new(settings:, logger:).call 259 | end 260 | 261 | it "builds nested executable" do 262 | builder.call 263 | 264 | expect(temp_dir.join("demo-test/exe/demo-test").read).to eq(<<~CONTENT) 265 | #! /usr/bin/env ruby 266 | 267 | require "demo/test" 268 | 269 | Demo::Test::CLI::Shell.new.call 270 | CONTENT 271 | end 272 | 273 | it "adds CLI inflection" do 274 | builder.call 275 | 276 | expect(temp_dir.join("demo-test/lib/demo/test.rb").read).to eq(<<~CONTENT) 277 | require "zeitwerk" 278 | 279 | Zeitwerk::Loader.new.then do |loader| 280 | loader.inflector.inflect "cli" => "CLI" 281 | loader.tag = "demo-test" 282 | loader.push_dir "\#{__dir__}/.." 283 | loader.setup 284 | end 285 | 286 | module Demo 287 | # Main namespace. 288 | module Test 289 | def self.loader registry = Zeitwerk::Registry 290 | @loader ||= registry.loaders.each.find { |loader| loader.tag == "demo-test" } 291 | end 292 | 293 | end 294 | end 295 | CONTENT 296 | end 297 | 298 | it "builds application container with nested project path to gemspec" do 299 | builder.call 300 | 301 | expect(temp_dir.join("demo-test/lib/demo/test/container.rb").read).to include( 302 | %(Spek::Loader.call "\#{__dir__}/../../../demo-test.gemspec") 303 | ) 304 | end 305 | 306 | it "builds RSpec CLI shell spec" do 307 | builder.call 308 | 309 | expect(temp_dir.join("demo-test/spec/lib/demo/test/cli/shell_spec.rb").read).to eq( 310 | fixtures_root.join("spec/lib/cli/shell_dash_proof.rb").read 311 | ) 312 | end 313 | end 314 | 315 | context "when disabled" do 316 | before { settings.with! settings.minimize } 317 | 318 | it "doesn't build executable" do 319 | builder.call 320 | expect(temp_dir.join("test/exe/test").exist?).to be(false) 321 | end 322 | 323 | it "doesn't build lib folder" do 324 | builder.call 325 | expect(temp_dir.join("test/lib").exist?).to be(false) 326 | end 327 | 328 | it "doesn't build spec folder" do 329 | builder.call 330 | expect(temp_dir.join("test/spec").exist?).to be(false) 331 | end 332 | 333 | it "doesn't build gemfile" do 334 | builder.call 335 | expect(temp_dir.join("test/Gemfile").exist?).to be(false) 336 | end 337 | 338 | it "answers false" do 339 | expect(builder.call).to be(false) 340 | end 341 | end 342 | end 343 | end 344 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | :toclevels: 5 3 | :figure-caption!: 4 | 5 | :containable_link: link:https://alchemists.io/projects/containable[Containable] 6 | :infusible_link: link:https://alchemists.io/projects/infusible[Infusible] 7 | :ruby_gems_link: link:https://rubygems.org[RubyGems] 8 | :sod_link: link:https://alchemists.io/projects/sod[Sod] 9 | 10 | = Gemsmith 11 | 12 | Gemsmith is a command line interface for smithing Ruby gems. Perfect for when you need a professional and robust tool beyond link:https://bundler.io[Bundler]'s basic gem skeletons. While Bundler is great for creating your first gem, you'll quickly outgrow Bundler when creating and maintaining multiple gems. This is where Gemsmith can increase your productivity by providing much of the tooling you need from the start with the ability to customize as desired. 13 | 14 | toc::[] 15 | 16 | == Features 17 | 18 | * Supports all link:https://alchemists.io/projects/rubysmith[Rubysmith] features. 19 | * Supports basic gem or more advanced {sod_link}-based Command Line Interface (CLI) skeletons. 20 | * Supports gem building, installing for local development, and publishing. 21 | * Supports the editing and viewing of installed gems. 22 | 23 | == Requirements 24 | 25 | . A UNIX-based system. 26 | . link:https://www.ruby-lang.org[Ruby]. 27 | . {ruby_gems_link}. 28 | 29 | == Setup 30 | 31 | To install _with_ security, run: 32 | 33 | [source,bash] 34 | ---- 35 | # 💡 Skip this line if you already have the public certificate installed. 36 | gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem) 37 | gem install gemsmith --trust-policy HighSecurity 38 | ---- 39 | 40 | To install _without_ security, run: 41 | 42 | [source,bash] 43 | ---- 44 | gem install gemsmith 45 | ---- 46 | 47 | == Usage 48 | 49 | === Command Line Interface (CLI) 50 | 51 | From the command line, type: `gemsmith --help` 52 | 53 | image:https://alchemists.io/images/projects/gemsmith/screenshots/usage.png[Usage,width=729,height=462,role=focal_point] 54 | 55 | ==== Build 56 | 57 | The core functionality of this gem centers around the `build` command and associated flags. The build options allow you to further customize the kind of gem you want to build. Most build options 58 | are enabled by default. For detailed documentation on all supported flags, see the link:https://alchemists.io/projects/rubysmith/#_build[Rubysmith] documentation. 59 | 60 | The build option which is unique to Gemsmith is the `--cli` option. This allows you to build a gem which has a Command Line Interface (CLI). There are multiple ways a CLI can be built in Ruby but Gemsmith takes an approach which builds upon Ruby's native `OptionParser` with help from the {containable_link} and {infusible_link} gems. All of this culminates in a design that is mix of Objected Oriented + Functional Programming design. Building a gem with CLI support is a simple as running: 61 | 62 | [source,bash] 63 | ---- 64 | gemsmith build --name demo --cli 65 | ---- 66 | 67 | The above will give you a new gem with CLI support which includes working specs. It's the same design used to build this Gemsmith gem. You'll have both a `configuration` and `CLI` namespace for configuring your gem and adding additional CLI support. Out of the box, the CLI gem generated for you supports the following options: 68 | 69 | .... 70 | -v, --version Show version. 71 | -h, --help Show this message. 72 | config Manage configuration. 73 | .... 74 | 75 | From here you can add whatever you wish to make an awesome CLI gem for others to enjoy. 76 | 77 | ==== Install 78 | 79 | After you've designed, implemented, and built your gem, you'll want to test it out within your local 80 | environment by installing it. You can do this by running: 81 | 82 | [source,bash] 83 | ---- 84 | # Implicit 85 | gemsmith --install 86 | 87 | # Explicit 88 | gemsmith --install demo 89 | ---- 90 | 91 | Gemsmith can be used to install any gem, in fact. Doesn't matter if the gem was built by Gemsmith, 92 | Bundler, or some other tool. As long as your gem has a `*.gemspec` file, Gemsmith will be able to 93 | install it. 94 | 95 | ==== Publish 96 | 97 | Once you've built your gem; installed it locally; and thoroughly tested it, you'll want to publish 98 | your gem so anyone in the world can make use of it. You can do this by running the following: 99 | 100 | [source,bash] 101 | ---- 102 | # Implicit 103 | gemsmith --publish 104 | 105 | # Explicit 106 | gemsmith --publish demo 107 | ---- 108 | 109 | Security is important which requires a GPG key for signing your Git tags and 110 | link:https://alchemists.io/articles/ruby_gems_multi_factor_authentication/[RubyGems Multi-Factor 111 | Authentication] for publishing to RubyGems. Both of which are enabled by default. You'll want to 112 | read through the linked article which delves into how Gemsmith automatically makes use of your 113 | YubiKey to authenticate with RubyGems. Spending the time to set this up will allow Gemsmith to use 114 | of your YubiKey for effortless and secure publishing of new versions of your gems so I highly 115 | recommend doing this. 116 | 117 | As with installing a gem, Gemsmith can be used to publish existing gems which were not built by 118 | Gemsmith too. As long as your gem has a `*.gemspec` file with a valid version, Gemsmith will be able 119 | to publish it. 120 | 121 | ==== Edit 122 | 123 | Gemsmith can be used to edit existing gems on your local system. You can do this by running: 124 | 125 | [source,bash] 126 | ---- 127 | gemsmith --edit 128 | ---- 129 | 130 | If multiple versions of the same gem are detected, you'll be prompted to pick which gem you want to 131 | edit. Otherwise, the gem will immediately be opened within your default editor (or whatever you 132 | have set in your `EDITOR` environment variable). 133 | 134 | Editing a local gem is a great way to learn from others or quickly debug issues. 135 | 136 | ==== View 137 | 138 | Gemsmith can be used to view existing gem documentation. You can do this by running: 139 | 140 | [source,bash] 141 | ---- 142 | gemsmith --view 143 | ---- 144 | 145 | If multiple versions of the same gem are detected, you'll be prompted to pick which gem you want to 146 | view. Otherwise, the gem will immediately be opened within your default browser. 147 | 148 | Viewing a gem is a great way to learn more about the gem and documentation in general. 149 | 150 | === Configuration 151 | 152 | This gem can be configured via a global configuration: 153 | 154 | .... 155 | $HOME/.config/gemsmith/configuration.yml 156 | .... 157 | 158 | It can also be configured via link:https://alchemists.io/projects/xdg[XDG] environment 159 | variables. 160 | 161 | The default configuration is everything provided in the 162 | link:https://alchemists.io/projects/rubysmith/#_configuration[Rubysmith] with the addition of 163 | the following: 164 | 165 | [source,yaml] 166 | ---- 167 | build: 168 | cli: false 169 | ---- 170 | 171 | It is recommended that you provide URLs for your project which would be all keys found in this 172 | section: 173 | 174 | [source,yaml] 175 | ---- 176 | project: 177 | uri: 178 | # Add sub-key values here. 179 | ---- 180 | 181 | When these values exist, you'll benefit from having this information added to your generated 182 | `gemspec` and project documentation. Otherwise -- if these values are empty -- they are removed from 183 | new gem generation. 184 | 185 | === Workflows 186 | 187 | When building/testing your gem locally, a typical workflow is: 188 | 189 | [source,bash] 190 | ---- 191 | # Build 192 | gemsmith build --name demo 193 | 194 | # Design, Implement and Test. 195 | cd demo 196 | bundle exec rake 197 | 198 | # Install 199 | gemsmith --install 200 | 201 | # Publish 202 | gemsmith --publish 203 | ---- 204 | 205 | === Security 206 | 207 | ==== Git Signing Key 208 | 209 | To securely sign your Git tags, install and configure link:https://www.gnupg.org[GPG]: 210 | 211 | [source,bash] 212 | ---- 213 | brew install gpg 214 | gpg --gen-key 215 | ---- 216 | 217 | When creating your GPG key, choose these settings: 218 | 219 | * Key kind: RSA and RSA (default) 220 | * Key size: 4096 221 | * Key validity: 0 222 | * Real Name: `` 223 | * Email: `` 224 | * Passphrase: `` 225 | 226 | To obtain your key, run the following and take the part after the forward slash: 227 | 228 | [source,bash] 229 | ---- 230 | gpg --list-keys | grep pub 231 | ---- 232 | 233 | Add your key to your global Git configuration in the `[user]` section. Example: 234 | 235 | .... 236 | [user] 237 | signingkey = 238 | .... 239 | 240 | Now, when publishing your gems with Gemsmith (i.e. `bundle exec rake publish`), signing of your Git 241 | tag will happen automatically. 242 | 243 | ==== Gem Certificates 244 | 245 | To create a certificate for your gems, run the following: 246 | 247 | [source,bash] 248 | ---- 249 | cd ~/.gem 250 | gem cert build you@example.com --days 730 251 | gem cert --add gem-public_cert.pem 252 | cp gem-public_cert.pem /gems.pem 253 | ---- 254 | 255 | The above breaks down as follows: 256 | 257 | * *Source*: The `~/.gem` directory is where your credentials and certificates are stored. This is also where the `Gem.default_key_path` and `Gem.default_cert_path` methods look for your certificates. I'll talk more about these shortly. 258 | * *Build*: Builds your `gem-private_key.pem` and `gem-public_cert.pem` certificates with a two year duration (i.e. `365 * 2`) before expiring. You can also see this information on the {ruby_gems_link} page for your gem (scroll to the bottom). Security-wise, this isn't great but the way {ruby_gems_link} certification is implemented and enforced is weak to begin with. Regardless, this is important to do in order to be a good citizen within the ecosystem. You'll also be prompted for a private key passphrase so make sure it is long and complicated and then store it in your favorite password manager. 259 | * *Add*: Once your public certificate has been built, you'll need to add it to your registry so {ruby_gems_link} can look up and verify your certificate upon gem install. 260 | * *Web*: You'll need to copy your public certificate to the public folder of your web server so you can host this certificate for others to install. I rename my public certificate as `gems.pem` to keep the URL simple but you can name it how you like and document usage for others. For example, here's how you'd add my public certificate (same as done locally but via a URL this time): `gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)`. 261 | 262 | Earlier, I mentioned `Gem.default_key_path` and `Gem.default_cert_path` are paths to where your certificates are stored in your `~/.gem` directory. Well, the `signing_key` and `cert_chain` of your `.gemspec` needs to use these paths. Gemsmith automates for you when the `--security` build option is used (enabled by default). For example, when using Gemsmith to build a new gem, you'll see the following configuration generated in your `.gemspec`: 263 | 264 | [source,ruby] 265 | ---- 266 | # frozen_string_literal: true 267 | 268 | Gem::Specification.new do |spec| 269 | # Truncated for brevity. 270 | spec.signing_key = Gem.default_key_path 271 | spec.cert_chain = [Gem.default_cert_path] 272 | end 273 | ---- 274 | 275 | The above wires all of this functionality together so you can easily build and publish your gems with minimal effort while increasing your security. 🎉 To test the security of your newly minted gem, you can install it with the `--trust-policy` set to high security for maximum benefit. Example: 276 | 277 | [source,bash] 278 | ---- 279 | gem install --trust-policy HighSecurity 280 | ---- 281 | 282 | To learn more about gem certificates, check out the RubyGems 283 | link:https://guides.rubygems.org/security[Security] documentation. 284 | 285 | === Private Gem Servers 286 | 287 | By default, the following command will publicly publish your gem to {ruby_gems_link}: 288 | 289 | [source,bash] 290 | ---- 291 | gemsmith --publish 292 | ---- 293 | 294 | You can change this behavior by adding metadata to your gemspec that will allow Gemsmith to publish 295 | your gem to an alternate/private gem server instead. This can be done by updating your gem 296 | specification and RubyGems credentials. 297 | 298 | ==== Gem Specification Metadata 299 | 300 | Add the following gemspec metadata to privately publish new versions of your gem: 301 | 302 | [source,ruby] 303 | ---- 304 | Gem::Specification.new do |spec| 305 | spec.metadata = {"allowed_push_host" => "https://private.example.com"} 306 | end 307 | ---- 308 | 309 | 💡 The gemspec metadata (i.e. keys and values) _must_ be strings per the 310 | link:https://guides.rubygems.org/specification-reference/#metadata[RubyGems Specification]. 311 | 312 | Use of the `allowed_push_host` key provides two important capabilities: 313 | 314 | * Prevents you from accidentally publishing your private gem to the public RubyGems server (default 315 | behavior). 316 | * Defines the lookup key in your `$HOME/.gem/credentials` file which contains your private 317 | credentials for authentication to your private server (more on this below). 318 | 319 | ==== Gem Credentials 320 | 321 | With your gem specification metadata established, you are ready to publish your gem to a public or 322 | private server. If this is your first time publishing a gem and no gem credentials have been 323 | configured, you'll be prompted for them. Gem credentials are stored in the RubyGems 324 | `$HOME/.gem/credentials` file. From this point forward, future gem publishing will use your stored 325 | credentials instead. 326 | 327 | Multiple credentials can be stored in the `$HOME/.gem/credentials` file as well. Example: 328 | 329 | [source,yaml] 330 | ---- 331 | :rubygems_api_key: 2a0b460650e67d9b85a60e183defa376 332 | https://private.example.com: Basic dXNlcjpwYXNzd29yZA== 333 | ---- 334 | 335 | Notice how the first line contains credentials for the public RubyGems server while the second line 336 | is for our private example server. You'll also notice that the key is not a symbol but a URL string 337 | to our private server. This is important because this is how we link our gem specification metadata 338 | to our private credentials. To illustrate further, here are both files truncated and shown together: 339 | 340 | .... 341 | # Gem Specification: The metadata which defines the private host to publish to. 342 | spec.metadata = {"allowed_push_host" => "https://private.example.com"} 343 | 344 | # Gem Credentials: The URL value -- shown above -- which becomes the key for enabling authentication. 345 | https://private.example.com: Basic dXNlcjpwYXNzd29yZA== 346 | .... 347 | 348 | When the above are linked together, you enable Gemsmith to publish your gem using only the following 349 | command: 350 | 351 | [source,bash] 352 | ---- 353 | gemsmith --publish 354 | ---- 355 | 356 | This is especially powerful when publishing to 357 | link:https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-rubygems-registry[GitHub 358 | Packages] which would look like this when properly configured (truncated for brevity while using 359 | fake data): 360 | 361 | .... 362 | # Gem specification 363 | spec.metadata = {"allowed_push_host" => "https://rubygems.pkg.github.com/alchemists"} 364 | 365 | # Gem credentials 366 | https://rubygems.pkg.github.com/alchemists: Bearer ghp_c5b8d394abefebbf45c7b27b379c74978923 367 | .... 368 | 369 | Lastly, should you need to delete a credential (due to a bad login/password for example), you can 370 | open the `$HOME/.gem/credentials` in your default editor and remove the line(s) you don't need. Upon 371 | next publish of your gem, you'll be prompted for the missing credentials. 372 | 373 | ==== Bundler Configuration 374 | 375 | So far, I've shown how to privately _publish_ a gem but now we need to teach Bundler how to install 376 | the gem as dependency within your upstream project. For demonstration purposes, I'm going to assume 377 | you are using GitHub Packages as your private gem server. You should be able to quickly translate 378 | this documentation if using an alternate private gem server, though. 379 | 380 | The first step is to create your own GitHub Personal Access Token (PAT) which is fast to do by 381 | following GitHub's own 382 | link:https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token[documentation]. 383 | At a minimum, you'll need to enable _repo_ and _packages_ scopes with read/write access. 384 | 385 | With your PAT in hand, you'll need to ensure link:https://bundler.io[Bundler] can authenticate to 386 | the private GitHub Packages gem server by running the following: 387 | 388 | [source,bash] 389 | ---- 390 | bundle config set --global rubygems.pkg.github.com : 391 | # Example: bundle config set --global rubygems.pkg.github.com jdoe:ghp_c5b8d394abefebbf45c7b27b379c74978923 392 | ---- 393 | 394 | 💡 Using Bundler's `--global` flag ensures you only have to define these credentials once for _all_ 395 | projects which reduces maintenance burden on you. The path to this global configuration can be found 396 | here: `$HOME/.config/bundler/configuration.yml`. 397 | 398 | Lastly, you can add this gem to your `Gemfile` as follows: 399 | 400 | [source,ruby] 401 | ---- 402 | source "https://rubygems.pkg.github.com/alchemists" do 403 | gem "demo", "~> 0.0" 404 | end 405 | ---- 406 | 407 | At this point -- if you run `bundle install` -- you should see the following in your console: 408 | 409 | .... 410 | Fetching gem metadata from https://rubygems.pkg.github.com/alchemists/... 411 | Resolving dependencies...Fetching gem metadata from https://rubygems.org/..... 412 | .... 413 | 414 | If so, you're all set! 415 | 416 | ==== GitHub Actions/Packages Automation 417 | 418 | Earlier, I hinted at using GitHub Packages but what if you could automate the entire publishing 419 | process? Well, good news, you can by using GitHub Actions to publish your packages. Here's the YAML 420 | necessary to accomplish this endeavor: 421 | 422 | ``` yaml 423 | name: Gemsmith 424 | 425 | on: 426 | push: 427 | branches: main 428 | 429 | jobs: 430 | build: 431 | runs-on: ubuntu-latest 432 | container: 433 | image: ruby:latest 434 | permissions: 435 | contents: write 436 | packages: write 437 | 438 | steps: 439 | - name: Checkout 440 | uses: actions/checkout@v4 441 | with: 442 | fetch-depth: '0' 443 | ref: ${{github.head_ref}} 444 | - name: Setup 445 | run: | 446 | git config user.email "engineering@example.com" 447 | git config user.name "Gemsmith Publisher" 448 | mkdir -p $HOME/.gem 449 | printf "%s\n" "https://rubygems.pkg.github.com/example: Bearer ${{secrets.GITHUB_TOKEN}}" > $HOME/.gem/credentials 450 | chmod 0600 $HOME/.gem/credentials 451 | - name: Install 452 | run: gem install gemsmith 453 | - name: Publish 454 | run: | 455 | if git describe --tags --abbrev=0 > /dev/null 2>&1; then 456 | gemsmith --publish 457 | else 458 | printf "%s\n" "First gem version must be manually created. Skipping." 459 | fi 460 | ``` 461 | 462 | The above will ensure the following: 463 | 464 | * Only the first version requires manual publishing (hence the check for existing Git tags). 465 | * Duplicate versions are always skipped. 466 | * Only when a new version is detected (by changing your gemspec version) and you are on the `main` 467 | branch will a new version be automatically published. 468 | 469 | This entire workflow is explained in my 470 | link:https://alchemists.io/talks/ruby_git_hub_packages[talk] on this exact subject too. 471 | 472 | == Development 473 | 474 | To contribute, run: 475 | 476 | [source,bash] 477 | ---- 478 | git clone https://github.com/bkuhlmann/gemsmith 479 | cd gemsmith 480 | bin/setup 481 | ---- 482 | 483 | You can also use the IRB console for direct access to all objects: 484 | 485 | [source,bash] 486 | ---- 487 | bin/console 488 | ---- 489 | 490 | == Tests 491 | 492 | To test, run: 493 | 494 | [source,bash] 495 | ---- 496 | bin/rake 497 | ---- 498 | 499 | == link:https://alchemists.io/policies/license[License] 500 | 501 | == link:https://alchemists.io/policies/security[Security] 502 | 503 | == link:https://alchemists.io/policies/code_of_conduct[Code of Conduct] 504 | 505 | == link:https://alchemists.io/policies/contributions[Contributions] 506 | 507 | == link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin] 508 | 509 | == link:https://alchemists.io/projects/gemsmith/versions[Versions] 510 | 511 | == link:https://alchemists.io/community[Community] 512 | 513 | == Credits 514 | 515 | Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann]. 516 | --------------------------------------------------------------------------------