capistrano-node-dotenv 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capistrano
4
+ module Node
5
+ module Dotenv
6
+ module Parser
7
+ def parse_dotenv(content, stage_name)
8
+ global_env = {}
9
+ stage_env = {}
10
+ current_section = :stage
11
+ markers_found = false
12
+
13
+ content.each_line do |line|
14
+ new_section, is_marker = resolve_section_marker(line.strip, stage_name)
15
+ if is_marker
16
+ current_section = new_section
17
+ markers_found = true
18
+ next
19
+ end
20
+
21
+ collect_dotenv_pair(line.strip, global_env, stage_env, current_section, markers_found)
22
+ end
23
+
24
+ build_dotenv_result(global_env, stage_env, stage_name)
25
+ end
26
+
27
+ def resolve_section_marker(stripped, stage_name)
28
+ return [:global, true] if stripped.match?(/\A#\s*Begin\s+global\s+envs\b/i)
29
+ return [nil, true] if stripped.match?(/\A#\s*End\s+global\s+envs\b/i)
30
+ return [nil, true] if stripped.match?(/\A#\s*End\s+.+?\s+envs\b/i)
31
+
32
+ stage_begin = stripped.match(/\A#\s*Begin\s+(.+?)\s+envs\b/i)
33
+ if stage_begin
34
+ section = stage_begin[1].strip.casecmp(stage_name.to_s).zero? ? :stage : nil
35
+ return [section, true]
36
+ end
37
+
38
+ [nil, false]
39
+ end
40
+
41
+ def collect_dotenv_pair(stripped, global_env, stage_env, current_section, markers_found)
42
+ parsed_line = parse_dotenv_line(stripped)
43
+ return unless parsed_line
44
+
45
+ key, value = parsed_line
46
+ if markers_found
47
+ global_env[key] = value if current_section == :global
48
+ stage_env[key] = value if current_section == :stage
49
+ else
50
+ stage_env[key] = value
51
+ end
52
+ end
53
+
54
+ def build_dotenv_result(global_env, stage_env, stage_name)
55
+ global_env.sort.to_h.merge(stage_name.to_s => stage_env.sort.to_h)
56
+ end
57
+
58
+ def parse_dotenv_line(line)
59
+ return nil if line.empty? || line.start_with?('#')
60
+
61
+ stripped = line.sub(/\Aexport\s+/, '')
62
+ key, raw_value = stripped.split('=', 2)
63
+ return nil if key.nil? || raw_value.nil?
64
+
65
+ parsed_key = key.strip
66
+ value = raw_value.strip
67
+
68
+ if value.start_with?('"') && value.end_with?('"')
69
+ value = value[1..-2]
70
+ value = value.gsub('\\n', "\n").gsub('\\"', '"').gsub('\\\\', '\\')
71
+ elsif value.start_with?("'") && value.end_with?("'")
72
+ value = value[1..-2]
73
+ else
74
+ value = value.sub(/\s+#.*\z/, '').strip
75
+ end
76
+
77
+ [parsed_key, value]
78
+ end
79
+
80
+ def generate_dotenv_content(global_env_hash, env_hash)
81
+ date = Time.now.strftime('%Y-%m-%d %H:%M:%S')
82
+ global_start = "# Begin global envs #{date}\n"
83
+ global_end = "# End global envs #{date}\n"
84
+ stage_start = "# Begin #{fetch(:dotenv_env) || fetch(:stage)} envs #{date}\n"
85
+ stage_end = "# End #{fetch(:dotenv_env) || fetch(:stage)} envs #{date}\n"
86
+
87
+ content = global_start
88
+ global_env_hash.each do |key, value|
89
+ content += "#{key}=#{value}\n"
90
+ end
91
+ content += global_end
92
+ content += stage_start
93
+ env_hash.each do |key, value|
94
+ content += "#{key}=#{value}\n"
95
+ end
96
+ content += stage_end
97
+ content
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Capistrano
6
+ module Node
7
+ module Dotenv
8
+ module Paths
9
+ def dotenv_local_path
10
+ Pathname.new fetch(:dotenv_local_path)
11
+ end
12
+
13
+ def dotenv_remote_path
14
+ shared_path.join fetch(:dotenv_remote_path)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ desc 'Backup remote .env file'
7
+ task :backup do
8
+ on release_roles :all do
9
+ unless remote_file_exists?
10
+ puts 'No remote .env file to backup.'
11
+ next
12
+ end
13
+
14
+ begin
15
+ puts 'Creating backup of remote .env file...'
16
+ create_remote_backup
17
+ puts 'Backup created successfully.'
18
+
19
+ puts 'Cleaning up remote backups...'
20
+ cleanup_remote_backups
21
+ puts 'Remote backups cleaned up successfully.'
22
+ rescue StandardError => e
23
+ puts "Error during backup process: #{e.message}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ desc 'dotenv `application.yml` file checks'
7
+ task :check do
8
+ invoke 'dotenv:check_dotenv_file_exists'
9
+ invoke 'dotenv:check_git_tracking'
10
+ invoke 'dotenv:check_config_present'
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ task :check_config_present do
7
+ next unless local_dotenv(dotenv_env).empty?
8
+
9
+ check_config_present_error
10
+ exit 1
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ task :check_dotenv_file_exists do
7
+ next if File.exist?(dotenv_local_path)
8
+
9
+ check_dotenv_file_exists_error
10
+ exit 1
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ task :check_git_tracking do
7
+ next unless system("git ls-files #{fetch(:dotenv_local_path)} --error-unmatch >/dev/null 2>&1")
8
+
9
+ check_git_tracking_error
10
+ exit 1
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ include Capistrano::Node::Dotenv::Parser
7
+
8
+ task :create_local do
9
+ run_locally do
10
+ puts "found #{stages.count} stages"
11
+ yamls_combined = {}
12
+ empty_stages = {}
13
+ stages.each do |f|
14
+ stage = File.basename(f, '.rb')
15
+ puts "download #{stage} .env"
16
+ begin
17
+ res = capture "cap #{stage} dotenv:get_stage"
18
+ yamls_combined.merge!(parse_dotenv(res, stage))
19
+ rescue StandardError
20
+ yamls_combined.merge!(stage => { 'KEY' => 'VALUE' }) # add empty stage to combined yml to avoid losing data from other stages
21
+ empty_stages.merge!(stage => { "#{stage.upcase}_KEY" => 'VALUE' })
22
+ puts "could not get #{stage} .env"
23
+ end
24
+ end
25
+
26
+ if empty_stages.count == stages.count
27
+ puts "no .env files found for any stage. creating empty local env file with all stages: #{empty_stages.keys.join(', ')}"
28
+ yamls_combined.merge!({ 'GLOBAL' => 'VALUE' })
29
+ yamls_combined.merge!(empty_stages)
30
+ puts yamls_combined.sort.to_h.to_yaml
31
+ end
32
+ write_combined_yaml(yamls_combined.sort.to_h)
33
+ end
34
+ end
35
+ desc 'Initializes local env file (config/application.yml)'
36
+ task init: :create_local
37
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ task :dotenv_symlink do
7
+ set :linked_files, fetch(:linked_files, []).push(fetch(:dotenv_remote_path))
8
+ end
9
+ after 'deploy:started', 'dotenv:dotenv_symlink'
10
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ desc 'Compares the remote .env file with the local one and allows to update the local file with remote values'
7
+ task :get do
8
+ if File.exist?(dotenv_local_path)
9
+ local_yml = local_dotenv(dotenv_env)
10
+ local_global_env, local_stage_env, local_rest = configs(local_yml, dotenv_env)
11
+ on release_roles :all do
12
+ unless remote_file_exists?
13
+ puts 'Remote .env file does not exist. Nothing to get.'
14
+ exit
15
+ end
16
+ remote = parse_dotenv(capture("cat #{dotenv_remote_path}"), dotenv_env)
17
+ remote_global_env, remote_stage_env, _remote_rest = configs(remote, dotenv_env)
18
+
19
+ differences_global = compare_hashes(remote_global_env, local_global_env || {})
20
+ differences_stage = compare_hashes(remote_stage_env, local_stage_env || {})
21
+ print_changes(differences_global,
22
+ 'Remote env file has extra/different global entries compared to local.')
23
+ if differences_stage
24
+ print_changes(differences_stage,
25
+ "Remote env file has extra/different entries in #{dotenv_env} section compared to local.")
26
+ end
27
+ puts 'No Differences found between remote and local env file' unless differences_global || differences_stage
28
+
29
+ # puts 'No Differences found between remote and local application.yml' unless differences_global || differences_stage
30
+
31
+ # # ask to overwrite local yml if differences found
32
+ stage_overwrite_local = ask_to_overwrite("Overwrite local env file #{dotenv_env} section") if differences_stage
33
+ global_overwrite_local = ask_to_overwrite('Overwrite local env file globals') if differences_global
34
+ puts 'Nothing written to local env file' unless stage_overwrite_local || global_overwrite_local
35
+ exit unless stage_overwrite_local || global_overwrite_local
36
+ # stage_overwrite = ask_to_overwrite("Overwrite local application.yml #{figaro_yml_env} section") if differences_stage
37
+ # global_overwrite = ask_to_overwrite('Overwrite local application.yml globals') if differences_global
38
+ # puts 'Nothing written to local application.yml' unless stage_overwrite || global_overwrite
39
+ # exit unless stage_overwrite || global_overwrite
40
+ # compose new yml
41
+
42
+ composed_yml = {}
43
+ composed_yml.merge!(local_rest) # local yml is always included to avoid losing any data
44
+ composed_yml.merge!(local_global_env) unless global_overwrite_local
45
+ composed_yml.merge!(remote_global_env) if global_overwrite_local
46
+ composed_yml[dotenv_env.to_s] = stage_overwrite_local ? remote_stage_env : local_stage_env
47
+ puts composed_yml.to_yaml
48
+ # # write to new file
49
+ write_combined_yaml(composed_yml.sort.to_h)
50
+ end
51
+ else
52
+ invoke 'dotenv:create_local'
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+
7
+ rake_roles = fetch(:rake_roles, :app)
8
+ task :get_stage do
9
+ on roles(rake_roles) do
10
+ raise 'The file does not exist.' unless remote_file_exists?
11
+
12
+ puts capture "cat #{dotenv_remote_path}"
13
+ end
14
+ end
15
+
16
+ desc 'shows the content of the remote .env file for the current stage'
17
+ task show: :get_stage
18
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :load do
4
+ task :defaults do
5
+ set :dotenv_local_path, 'config/application.yml'
6
+ set :dotenv_remote_path, '.env'
7
+ set :dotenv_env, -> { fetch(:node_env) || fetch(:stage) }
8
+ set :dotenv_remote_backup, false
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ desc 'Rolls back the remote .env file to the previous version if a backup exists'
7
+ task :rollback do
8
+ on release_roles :all do
9
+ unless remote_backup_exists?
10
+ puts 'No backup of remote .env file to rollback.'
11
+ next
12
+ end
13
+
14
+ begin
15
+ puts 'Rolling back remote .env file...'
16
+ rollback_remote_backup
17
+ puts 'Rollback completed successfully.'
18
+ rescue StandardError => e
19
+ puts "Error during rollback process: #{e.message}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ include Capistrano::Node::Dotenv::Parser
7
+ desc 'Uploads the local env file to the server and saves it in the shared folder. This task is used to set up the remote .env file for the first time or to update it with local values. It will overwrite the remote .env file if it already exists.'
8
+ task :setup do
9
+ content = local_dotenv(dotenv_env)
10
+
11
+ on release_roles :all do
12
+ global_yml, stage_yml, _other_stages_yml = configs(content, dotenv_env)
13
+ dotenv_string = generate_dotenv_content(global_yml, stage_yml)
14
+
15
+ execute :mkdir, '-pv', File.dirname(dotenv_remote_path)
16
+ upload! StringIO.new(dotenv_string), dotenv_remote_path
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :dotenv do
4
+ include Capistrano::Node::Dotenv::Paths
5
+ include Capistrano::Node::Dotenv::Helpers
6
+ include Capistrano::Node::Dotenv::Parser
7
+ desc 'Compares the remote .env file with the local one and allows to update the remote file with local values'
8
+ task :update do
9
+ local = local_dotenv(dotenv_env)
10
+ local_global_env, local_stage_env, _local_rest = configs(local, dotenv_env)
11
+ on release_roles :all do
12
+ remote = parse_dotenv(capture("cat #{dotenv_remote_path}"), fetch(:dotenv_env))
13
+ remote_global_env, remote_stage_env, _remote_rest = configs(remote, dotenv_env)
14
+ # Compare hashes and handle nil results with empty hashes
15
+ differences_global = compare_hashes(local_global_env, remote_global_env)
16
+ differences_stage = compare_hashes(local_stage_env, remote_stage_env)
17
+
18
+ print_changes(differences_global, 'Local application.yml has extra/different global entries compared to remote.')
19
+ if differences_stage
20
+ print_changes(differences_stage, "Local application.yml has extra/different entries in #{dotenv_env} section compared to remote.")
21
+ end
22
+
23
+ puts 'No Differences found between remote and local application.yml' unless differences_global || differences_stage
24
+
25
+ stage_overwrite_remote = ask_to_overwrite("Overwrite remote #{dotenv_env} section") if differences_stage
26
+ global_overwrite_remote = ask_to_overwrite('Overwrite remote globals') if differences_global
27
+
28
+ puts 'Nothing written to remote application.yml' unless stage_overwrite_remote || global_overwrite_remote
29
+ exit unless stage_overwrite_remote || global_overwrite_remote
30
+ global_yaml = global_overwrite_remote ? local_global_env : remote_global_env
31
+ stage_yaml = stage_overwrite_remote ? local_stage_env : remote_stage_env
32
+ # write to new file
33
+ dotenv_string = generate_dotenv_content(global_yaml, stage_yaml)
34
+
35
+ invoke 'dotenv:backup'
36
+ # execute :mkdir, '-pv', File.dirname(dotenv_remote_path)
37
+ upload! StringIO.new(dotenv_string), dotenv_remote_path
38
+ end
39
+ end
40
+ # before 'dotenv:update', 'dotenv:backup'
41
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capistrano
4
+ module Node
5
+ module Dotenv
6
+ VERSION = '1.0.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capistrano
4
+ module Node
5
+ module Dotenv
6
+ require 'capistrano/node/dotenv/paths'
7
+ require 'capistrano/node/dotenv/helpers'
8
+ require 'capistrano/node/dotenv/parser'
9
+
10
+ def self.path
11
+ Dir.pwd
12
+ end
13
+ task_files = Dir.glob("#{File.expand_path(__dir__)}/dotenv/tasks/**/*.rake")
14
+ task_files.each do |file|
15
+ load file
16
+ rescue StandardError => e
17
+ puts "Failed to load #{file}: #{e.message}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Version management script for Capistrano Node Dotenv Gem
5
+ # Usage: ruby scripts/bump_version.rb [patch|minor|major]
6
+
7
+ require 'fileutils'
8
+ require_relative '../lib/capistrano/node/dotenv/version'
9
+
10
+ class VersionBumper
11
+ VALID_TYPES = %w[patch minor major].freeze
12
+ VERSION_FILE = 'lib/capistrano/node/dotenv/version.rb'
13
+ CHANGELOG_FILE = 'CHANGELOG.md'
14
+
15
+ def initialize(bump_type)
16
+ @bump_type = bump_type.to_s.downcase
17
+ @current_version = Capistrano::Node::Dotenv::VERSION
18
+ validate_bump_type!
19
+ end
20
+
21
+ def bump!
22
+ puts "Current version: #{@current_version}"
23
+
24
+ new_version = calculate_new_version
25
+ puts "New version: #{new_version}"
26
+
27
+ # Update version file
28
+ update_version_file(new_version)
29
+
30
+ # Update changelog
31
+ update_changelog(new_version)
32
+
33
+ # Create git commit
34
+ create_git_commit(new_version) if git_available?
35
+
36
+ puts 'Version bumped successfully!'
37
+ puts ''
38
+ puts '📋 Next steps:'
39
+ puts '1. Review the changes (git diff)'
40
+ puts '2. Fill in CHANGELOG.md with actual changes'
41
+ puts '3. Commit and tag:'
42
+ puts " git add #{VERSION_FILE} #{CHANGELOG_FILE}"
43
+ puts " git commit -m 'Release version #{new_version}'"
44
+ puts " git tag -a v#{new_version} -m 'Release version #{new_version}'"
45
+ puts '4. Push to GitHub:'
46
+ puts " git push origin main && git push origin v#{new_version}"
47
+ puts '5. Create GitHub Release:'
48
+ puts " gh release create v#{new_version} --title 'v#{new_version}' \\"
49
+ puts " --notes-file <(sed -n '/## \\[#{new_version}\\]/,/## \\[/p' CHANGELOG.md | sed '$d')"
50
+ puts ''
51
+ puts '→ GitHub Actions will automatically publish to RubyGems.org! 🚀'
52
+ end
53
+
54
+ private
55
+
56
+ def validate_bump_type!
57
+ return if VALID_TYPES.include?(@bump_type)
58
+
59
+ puts "Error: Invalid bump type '#{@bump_type}'"
60
+ puts "Valid types: #{VALID_TYPES.join(', ')}"
61
+ exit 1
62
+ end
63
+
64
+ def calculate_new_version
65
+ parts = @current_version.split('.').map(&:to_i)
66
+
67
+ case @bump_type
68
+ when 'patch'
69
+ parts[2] += 1
70
+ when 'minor'
71
+ parts[1] += 1
72
+ parts[2] = 0
73
+ when 'major'
74
+ parts[0] += 1
75
+ parts[1] = 0
76
+ parts[2] = 0
77
+ end
78
+
79
+ parts.join('.')
80
+ end
81
+
82
+ def update_version_file(new_version)
83
+ puts "Updating #{VERSION_FILE}..."
84
+
85
+ content = File.read(VERSION_FILE)
86
+ updated_content = content.gsub(
87
+ /VERSION = ['"][^'"]*['"]/,
88
+ "VERSION = '#{new_version}'"
89
+ )
90
+
91
+ File.write(VERSION_FILE, updated_content)
92
+ puts '✓ Version file updated'
93
+ end
94
+
95
+ def update_changelog(new_version)
96
+ return unless File.exist?(CHANGELOG_FILE)
97
+
98
+ puts "Updating #{CHANGELOG_FILE}..."
99
+
100
+ # Read existing changelog
101
+ content = File.read(CHANGELOG_FILE)
102
+
103
+ # Find the [Unreleased] section and replace it
104
+ lines = content.lines
105
+
106
+ today = Time.now.strftime('%Y-%m-%d')
107
+
108
+ # Replace [Unreleased] header with new version
109
+ unreleased_index = lines.find_index { |line| line.start_with?('## [Unreleased]') }
110
+
111
+ if unreleased_index
112
+ # Create new version entry
113
+ new_entry = [
114
+ "## [Unreleased]\n",
115
+ "\n",
116
+ "## [#{new_version}] - #{today}\n"
117
+ ]
118
+
119
+ # Replace [Unreleased] line
120
+ lines[unreleased_index] = new_entry.join
121
+
122
+ # Update comparison links at the bottom
123
+ # Find and update the [Unreleased] link
124
+ unreleased_link_index = lines.rindex { |line| line.start_with?('[Unreleased]:') }
125
+ if unreleased_link_index
126
+ # Update [Unreleased] to compare from new version to HEAD
127
+ lines[unreleased_link_index] = "[Unreleased]: https://github.com/zauberware/capistrano-node-dotenv/compare/v#{new_version}...HEAD\n"
128
+ # Add new version link
129
+ lines.insert(unreleased_link_index + 1, "[#{new_version}]: https://github.com/zauberware/capistrano-node-dotenv/releases/tag/v#{new_version}\n")
130
+ end
131
+ end
132
+
133
+ # Write back to file
134
+ File.write(CHANGELOG_FILE, lines.join)
135
+ puts '✓ Changelog updated (please fill in the release notes under the new version)'
136
+ end
137
+
138
+ def git_available?
139
+ system('git --version > /dev/null 2>&1')
140
+ end
141
+
142
+ def create_git_commit(new_version)
143
+ puts 'Creating git commit...'
144
+
145
+ # Add modified files
146
+ system("git add #{VERSION_FILE}")
147
+ system("git add #{CHANGELOG_FILE}") if File.exist?(CHANGELOG_FILE)
148
+
149
+ # Create commit
150
+ commit_message = "Bump version to #{new_version}"
151
+ if system("git commit -m '#{commit_message}'")
152
+ puts "✓ Git commit created: #{commit_message}"
153
+ else
154
+ puts 'âš  Failed to create git commit (you may need to commit manually)'
155
+ end
156
+ end
157
+ end
158
+
159
+ # Main execution
160
+ if ARGV.empty?
161
+ puts "Usage: #{$PROGRAM_NAME} [patch|minor|major]"
162
+ puts ''
163
+ puts 'Examples:'
164
+ puts " #{$PROGRAM_NAME} patch # 1.0.0 -> 1.0.1"
165
+ puts " #{$PROGRAM_NAME} minor # 1.0.0 -> 1.1.0"
166
+ puts " #{$PROGRAM_NAME} major # 1.0.0 -> 2.0.0"
167
+ exit 1
168
+ end
169
+
170
+ bump_type = ARGV[0]
171
+ bumper = VersionBumper.new(bump_type)
172
+ bumper.bump!