gempilot 0.2.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.
- checksums.yaml +7 -0
- data/.claude/skills/using-command-kit/SKILL.md +119 -0
- data/.claude/skills/using-command-kit/cli-example.rb +84 -0
- data/.claude/skills/using-command-kit/generator-pattern.rb +62 -0
- data/.rspec +3 -0
- data/.rubocop.yml +281 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/Rakefile +44 -0
- data/data/templates/gem/Gemfile.erb +23 -0
- data/data/templates/gem/LICENSE.txt.erb +21 -0
- data/data/templates/gem/README.md.erb +25 -0
- data/data/templates/gem/Rakefile.erb +36 -0
- data/data/templates/gem/bin/console.erb +7 -0
- data/data/templates/gem/bin/setup.erb +5 -0
- data/data/templates/gem/dotfiles/github/workflows/ci.yml.erb +33 -0
- data/data/templates/gem/dotfiles/gitignore +11 -0
- data/data/templates/gem/dotfiles/rubocop.yml.erb +209 -0
- data/data/templates/gem/dotfiles/ruby-version.erb +1 -0
- data/data/templates/gem/exe/gem_name.erb +3 -0
- data/data/templates/gem/gemspec.erb +27 -0
- data/data/templates/gem/lib/gem_name/version.rb.erb +7 -0
- data/data/templates/gem/lib/gem_name.rb.erb +16 -0
- data/data/templates/gem/lib/gem_name_extension.rb.erb +20 -0
- data/data/templates/gem/rspec.erb +3 -0
- data/data/templates/gem/spec/gem_name_spec.rb.erb +5 -0
- data/data/templates/gem/spec/spec_helper.rb.erb +10 -0
- data/data/templates/gem/spec/zeitwerk_spec.rb.erb +5 -0
- data/data/templates/gem/test/gem_name_test.rb.erb +7 -0
- data/data/templates/gem/test/test_helper.rb.erb +7 -0
- data/data/templates/gem/test/zeitwerk_test.rb.erb +9 -0
- data/data/templates/new/.keep +0 -0
- data/data/templates/new/command.rb.erb +15 -0
- data/docs/command_kit_comparison.md +249 -0
- data/docs/command_kit_reference.md +517 -0
- data/docs/plans/2026-02-18-gempilot-add-command.md +718 -0
- data/docs/superpowers/plans/2026-04-01-rubocop-new-config.md +838 -0
- data/docs/superpowers/plans/2026-04-06-dogfood-inflectable.md +659 -0
- data/docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md +166 -0
- data/docs/superpowers/plans/2026-04-06-integrate-version-tools.md +162 -0
- data/docs/superpowers/plans/2026-04-06-new-readme.md +185 -0
- data/docs/version-management-redesign.md +44 -0
- data/exe/gempilot +12 -0
- data/issues.rec +77 -0
- data/lib/core_ext/string/inflection_methods.rb +68 -0
- data/lib/core_ext/string/refinements/inflectable.rb +15 -0
- data/lib/gempilot/cli/command.rb +17 -0
- data/lib/gempilot/cli/commands/bump.rb +49 -0
- data/lib/gempilot/cli/commands/console.rb +38 -0
- data/lib/gempilot/cli/commands/create.rb +183 -0
- data/lib/gempilot/cli/commands/destroy.rb +136 -0
- data/lib/gempilot/cli/commands/new.rb +226 -0
- data/lib/gempilot/cli/commands/release.rb +40 -0
- data/lib/gempilot/cli/gem_builder.rb +105 -0
- data/lib/gempilot/cli/gem_context.rb +40 -0
- data/lib/gempilot/cli/generator.rb +90 -0
- data/lib/gempilot/cli.rb +19 -0
- data/lib/gempilot/github_release.rb +30 -0
- data/lib/gempilot/project/version.rb +39 -0
- data/lib/gempilot/project.rb +111 -0
- data/lib/gempilot/strict_shell.rb +18 -0
- data/lib/gempilot/version.rb +3 -0
- data/lib/gempilot/version_tag.rb +53 -0
- data/lib/gempilot/version_task.rb +108 -0
- data/lib/gempilot.rb +17 -0
- data/notes.md +31 -0
- metadata +165 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
class CLI
|
|
3
|
+
module Commands
|
|
4
|
+
## Generates a new class, module, or command inside an existing gem.
|
|
5
|
+
class New < Command
|
|
6
|
+
using String::Inflectable
|
|
7
|
+
|
|
8
|
+
include Generator
|
|
9
|
+
include GemContext
|
|
10
|
+
|
|
11
|
+
template_dir File.join(Gempilot::ROOT, "data", "templates", "new")
|
|
12
|
+
|
|
13
|
+
usage "[options] TYPE CONSTANT"
|
|
14
|
+
description "Generate a class, module, or command in an existing gem"
|
|
15
|
+
|
|
16
|
+
examples [
|
|
17
|
+
"class MyGem::Authentication",
|
|
18
|
+
"class MyGem::Services::Authentication",
|
|
19
|
+
"module MyGem::Middleware",
|
|
20
|
+
"command deploy",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
argument :type, required: false,
|
|
24
|
+
desc: "Type to generate (class, module, command)"
|
|
25
|
+
|
|
26
|
+
argument :path, required: false,
|
|
27
|
+
desc: "Fully-qualified constant (e.g., MyGem::Services::Auth) or command name"
|
|
28
|
+
|
|
29
|
+
def run(type = nil, path = nil)
|
|
30
|
+
type ||= prompt_for_type
|
|
31
|
+
detect_gem_context
|
|
32
|
+
path ||= prompt_for_path
|
|
33
|
+
dispatch_add(type, path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def prompt_for_type
|
|
39
|
+
puts colors.bright_black("What kind of component do you want to add?")
|
|
40
|
+
ask_multiple_choice(colors.green("Type"), %w[class module command])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prompt_for_path
|
|
44
|
+
puts
|
|
45
|
+
puts colors.bright_black("Fully-qualified constant name (e.g., #{@gem_module}::Services::Authentication).")
|
|
46
|
+
ask(colors.green("Constant"), required: true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dispatch_add(type, path)
|
|
50
|
+
case type
|
|
51
|
+
when "class" then add_class(path)
|
|
52
|
+
when "module" then add_module(path)
|
|
53
|
+
when "command" then add_command(path)
|
|
54
|
+
else
|
|
55
|
+
puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_namespace_lines(namespaces)
|
|
61
|
+
namespaces.each_with_index.map do |ns, i|
|
|
62
|
+
"#{" " * i}module #{ns}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_closing_lines(namespaces)
|
|
67
|
+
Array.new(namespaces.length) do |i|
|
|
68
|
+
"#{" " * (namespaces.length - 1 - i)}end"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_nested_source(namespaces, type_keyword, name)
|
|
73
|
+
depth = namespaces.length
|
|
74
|
+
lines = build_namespace_lines(namespaces)
|
|
75
|
+
lines << "#{" " * depth}#{type_keyword} #{name}"
|
|
76
|
+
lines << "#{" " * depth}end"
|
|
77
|
+
lines.concat(build_closing_lines(namespaces))
|
|
78
|
+
"#{lines.join("\n")}\n"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def print_adding_banner(kind, label)
|
|
82
|
+
puts
|
|
83
|
+
puts colors.bright_white("Adding #{kind} ") +
|
|
84
|
+
colors.bold(colors.cyan(label)) +
|
|
85
|
+
colors.bright_white("...")
|
|
86
|
+
puts
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def prepare_constant(constant)
|
|
90
|
+
namespaces, name, segments = parse_constant(constant)
|
|
91
|
+
validate_gem_root!(namespaces.first)
|
|
92
|
+
file_path = "#{File.join("lib", *segments)}.rb"
|
|
93
|
+
ensure_directory(File.dirname(file_path))
|
|
94
|
+
[namespaces, name, segments, file_path]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def ensure_directory(dir)
|
|
98
|
+
mkdir(dir) unless File.directory?(dir)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_class(constant)
|
|
102
|
+
namespaces, class_name, segments, file_path = prepare_constant(constant)
|
|
103
|
+
print_adding_banner("class", "#{namespaces.join("::")}::#{class_name}")
|
|
104
|
+
source = build_nested_source(namespaces, "class", class_name)
|
|
105
|
+
create_file(file_path, source)
|
|
106
|
+
add_test_file(namespaces, class_name, segments)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_module(constant)
|
|
110
|
+
namespaces, mod_name, _segments, file_path = prepare_constant(constant)
|
|
111
|
+
print_adding_banner("module", "#{namespaces.join("::")}::#{mod_name}")
|
|
112
|
+
source = build_nested_source(namespaces, "module", mod_name)
|
|
113
|
+
create_file(file_path, source)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_command(name)
|
|
117
|
+
file_name = name.underscore
|
|
118
|
+
command_name = name.camelize
|
|
119
|
+
file_path = File.join("lib", @require_path, "cli", "commands", "#{file_name}.rb")
|
|
120
|
+
|
|
121
|
+
print_adding_banner("command", command_name)
|
|
122
|
+
ensure_directory(File.dirname(file_path))
|
|
123
|
+
|
|
124
|
+
@command_name = command_name
|
|
125
|
+
@command_file_name = file_name
|
|
126
|
+
erb "command.rb.erb", file_path
|
|
127
|
+
add_command_test_file(command_name, file_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def command_test_path(file_name)
|
|
131
|
+
if @test_framework == :rspec
|
|
132
|
+
File.join("spec", @require_path, "cli", "commands", "#{file_name}_spec.rb")
|
|
133
|
+
else
|
|
134
|
+
File.join("test", @require_path, "cli", "commands", "#{file_name}_test.rb")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def rspec_command_content(command_name)
|
|
139
|
+
<<~RUBY
|
|
140
|
+
require "spec_helper"
|
|
141
|
+
|
|
142
|
+
RSpec.describe #{@gem_module}::CLI::Commands::#{command_name} do
|
|
143
|
+
it "is defined" do
|
|
144
|
+
expect(described_class).not_to be_nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def minitest_command_content(command_name)
|
|
151
|
+
<<~RUBY
|
|
152
|
+
require "test_helper"
|
|
153
|
+
require "#{@require_path}/cli"
|
|
154
|
+
require "stringio"
|
|
155
|
+
|
|
156
|
+
module #{@gem_module}
|
|
157
|
+
class CLI
|
|
158
|
+
class #{command_name}Test < Minitest::Test
|
|
159
|
+
def test_placeholder
|
|
160
|
+
stdout = StringIO.new
|
|
161
|
+
command = Commands::#{command_name}.new(stdout: stdout)
|
|
162
|
+
assert command
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
RUBY
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def add_command_test_file(command_name, file_name)
|
|
171
|
+
test_path = command_test_path(file_name)
|
|
172
|
+
ensure_directory(File.dirname(test_path))
|
|
173
|
+
content = if @test_framework == :rspec
|
|
174
|
+
rspec_command_content(command_name)
|
|
175
|
+
else
|
|
176
|
+
minitest_command_content(command_name)
|
|
177
|
+
end
|
|
178
|
+
create_file(test_path, content)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def class_test_path(segments)
|
|
182
|
+
if @test_framework == :rspec
|
|
183
|
+
"#{File.join("spec", @require_path, *segments[1..])}_spec.rb"
|
|
184
|
+
else
|
|
185
|
+
"#{File.join("test", @require_path, *segments[1..])}_test.rb"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def rspec_class_content(namespaces, class_name)
|
|
190
|
+
<<~RUBY
|
|
191
|
+
require "spec_helper"
|
|
192
|
+
|
|
193
|
+
RSpec.describe #{namespaces.join("::")}::#{class_name} do
|
|
194
|
+
pending "add some examples"
|
|
195
|
+
end
|
|
196
|
+
RUBY
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def minitest_class_content(namespaces, class_name)
|
|
200
|
+
<<~RUBY
|
|
201
|
+
require "test_helper"
|
|
202
|
+
|
|
203
|
+
module #{namespaces.first}
|
|
204
|
+
class #{(namespaces[1..] + [class_name]).join("::")}Test < Minitest::Test
|
|
205
|
+
def test_placeholder
|
|
206
|
+
assert true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
RUBY
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def add_test_file(namespaces, class_name, segments)
|
|
214
|
+
test_path = class_test_path(segments)
|
|
215
|
+
ensure_directory(File.dirname(test_path))
|
|
216
|
+
content = if @test_framework == :rspec
|
|
217
|
+
rspec_class_content(namespaces, class_name)
|
|
218
|
+
else
|
|
219
|
+
minitest_class_content(namespaces, class_name)
|
|
220
|
+
end
|
|
221
|
+
create_file(test_path, content)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "bundler"
|
|
2
|
+
|
|
3
|
+
module Gempilot
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
## Delegates to +rake release+ to build and push the gem.
|
|
7
|
+
class Release < Command
|
|
8
|
+
include GemContext
|
|
9
|
+
|
|
10
|
+
description "Release the gem (delegates to rake release)"
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
detect_gem_context
|
|
14
|
+
print_release_banner
|
|
15
|
+
run_rake_release
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def print_release_banner
|
|
21
|
+
puts colors.bright_white("Releasing ") +
|
|
22
|
+
colors.bold(colors.cyan(@gem_name)) +
|
|
23
|
+
colors.bright_white("...")
|
|
24
|
+
puts
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run_rake_release
|
|
28
|
+
success = Bundler.with_unbundled_env do
|
|
29
|
+
system("bundle", "exec", "rake", "release")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return if success
|
|
33
|
+
|
|
34
|
+
puts colors.red("Release failed. Check the output above for errors.")
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
class CLI
|
|
3
|
+
## File rendering and directory creation logic for scaffolding a new gem.
|
|
4
|
+
##
|
|
5
|
+
## Expects the including class to provide Generator methods (+mkdir+, +erb+,
|
|
6
|
+
## +chmod+, +cp+, +cd+, +sh+) and the following instance variables:
|
|
7
|
+
## +@gem_name+, +@require_path+, +@module_name+, +@hyphenated+,
|
|
8
|
+
## +@test_framework+, +@branch+.
|
|
9
|
+
module GemBuilder
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def create_directories
|
|
13
|
+
mkdir @gem_name
|
|
14
|
+
mkdir "#{@gem_name}/lib"
|
|
15
|
+
create_lib_subdirectories
|
|
16
|
+
mkdir "#{@gem_name}/bin"
|
|
17
|
+
mkdir "#{@gem_name}/exe" if options[:exe]
|
|
18
|
+
create_test_directory
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_lib_subdirectories
|
|
22
|
+
@require_path.split("/").reduce("#{@gem_name}/lib") do |dir, part|
|
|
23
|
+
path = "#{dir}/#{part}"
|
|
24
|
+
mkdir path
|
|
25
|
+
path
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_test_directory
|
|
30
|
+
mkdir @test_framework == :rspec ? "#{@gem_name}/spec" : "#{@gem_name}/test"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_core_templates
|
|
34
|
+
erb "gemspec.erb", "#{@gem_name}/#{@gem_name}.gemspec"
|
|
35
|
+
erb "Gemfile.erb", "#{@gem_name}/Gemfile"
|
|
36
|
+
erb "Rakefile.erb", "#{@gem_name}/Rakefile"
|
|
37
|
+
erb "README.md.erb", "#{@gem_name}/README.md"
|
|
38
|
+
erb "LICENSE.txt.erb", "#{@gem_name}/LICENSE.txt"
|
|
39
|
+
erb "lib/gem_name.rb.erb", "#{@gem_name}/lib/#{@gem_name}.rb"
|
|
40
|
+
erb "lib/gem_name/version.rb.erb", "#{@gem_name}/lib/#{@require_path}/version.rb"
|
|
41
|
+
erb "lib/gem_name_extension.rb.erb", "#{@gem_name}/lib/#{@require_path}.rb" if @hyphenated
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_test_templates
|
|
45
|
+
@test_framework == :rspec ? render_rspec_templates : render_minitest_templates
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_rspec_templates
|
|
49
|
+
erb "spec/spec_helper.rb.erb", "#{@gem_name}/spec/spec_helper.rb"
|
|
50
|
+
erb "spec/gem_name_spec.rb.erb", "#{@gem_name}/spec/#{@gem_name.tr("-", "_")}_spec.rb"
|
|
51
|
+
erb "spec/zeitwerk_spec.rb.erb", "#{@gem_name}/spec/zeitwerk_spec.rb"
|
|
52
|
+
erb "rspec.erb", "#{@gem_name}/.rspec"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_minitest_templates
|
|
56
|
+
erb "test/test_helper.rb.erb", "#{@gem_name}/test/test_helper.rb"
|
|
57
|
+
erb "test/gem_name_test.rb.erb", "#{@gem_name}/test/#{@gem_name.tr("-", "_")}_test.rb"
|
|
58
|
+
erb "test/zeitwerk_test.rb.erb", "#{@gem_name}/test/zeitwerk_test.rb"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_dev_files
|
|
62
|
+
erb "bin/console.erb", "#{@gem_name}/bin/console"
|
|
63
|
+
erb "bin/setup.erb", "#{@gem_name}/bin/setup"
|
|
64
|
+
chmod "+x", "#{@gem_name}/bin/console"
|
|
65
|
+
chmod "+x", "#{@gem_name}/bin/setup"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_config_files
|
|
69
|
+
erb "dotfiles/rubocop.yml.erb", "#{@gem_name}/.rubocop.yml"
|
|
70
|
+
cp "dotfiles/gitignore", "#{@gem_name}/.gitignore"
|
|
71
|
+
erb "dotfiles/ruby-version.erb", "#{@gem_name}/.ruby-version"
|
|
72
|
+
render_ci_workflow
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_ci_workflow
|
|
76
|
+
mkdir "#{@gem_name}/.github"
|
|
77
|
+
mkdir "#{@gem_name}/.github/workflows"
|
|
78
|
+
erb "dotfiles/github/workflows/ci.yml.erb", "#{@gem_name}/.github/workflows/ci.yml"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_executable
|
|
82
|
+
return unless options[:exe]
|
|
83
|
+
|
|
84
|
+
erb "exe/gem_name.erb", "#{@gem_name}/exe/#{@gem_name}"
|
|
85
|
+
chmod "+x", "#{@gem_name}/exe/#{@gem_name}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_bundle_install
|
|
89
|
+
cd @gem_name do
|
|
90
|
+
sh "bundle", "install"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def initialize_git_repo
|
|
95
|
+
return unless options[:git]
|
|
96
|
+
|
|
97
|
+
cd @gem_name do
|
|
98
|
+
sh "git", "init", "-q", "-b", @branch
|
|
99
|
+
sh "git", "add", "."
|
|
100
|
+
sh "git", "commit", "-q", "-m", "Initial commit."
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
class CLI
|
|
3
|
+
## Shared context for commands that operate inside an existing gem.
|
|
4
|
+
module GemContext
|
|
5
|
+
using String::Inflectable
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def detect_gem_context
|
|
10
|
+
gemspec = Dir.glob("*.gemspec").first
|
|
11
|
+
|
|
12
|
+
unless gemspec
|
|
13
|
+
puts colors.red("No gemspec found in current directory. Run this from your gem's root.")
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@gem_name = File.basename(gemspec, ".gemspec")
|
|
18
|
+
@require_path = @gem_name.tr("-", "/")
|
|
19
|
+
@gem_module = @require_path.camelize
|
|
20
|
+
@test_framework = File.directory?("spec") ? :rspec : :minitest
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_constant(constant)
|
|
24
|
+
parts = constant.split("::")
|
|
25
|
+
namespaces = parts[0...-1]
|
|
26
|
+
name = parts.last
|
|
27
|
+
segments = parts.map(&:underscore)
|
|
28
|
+
[namespaces, name, segments]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_gem_root!(root)
|
|
32
|
+
expected = @gem_module.split("::").first
|
|
33
|
+
return if root == expected
|
|
34
|
+
|
|
35
|
+
puts colors.red("Expected constant to start with #{expected}, got #{root}")
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "bundler"
|
|
2
|
+
require "command_kit/colors"
|
|
3
|
+
require "command_kit/file_utils"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "erb"
|
|
6
|
+
|
|
7
|
+
module Gempilot
|
|
8
|
+
class CLI
|
|
9
|
+
## File generation utilities for scaffolding gems and components.
|
|
10
|
+
module Generator
|
|
11
|
+
include CommandKit::Colors
|
|
12
|
+
include CommandKit::FileUtils
|
|
13
|
+
|
|
14
|
+
def self.included(command)
|
|
15
|
+
command.extend ClassMethods
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
## Class-level helpers for Generator, including +template_dir+ resolution.
|
|
19
|
+
module ClassMethods
|
|
20
|
+
def template_dir(path = nil)
|
|
21
|
+
if path
|
|
22
|
+
@template_dir = File.expand_path(path)
|
|
23
|
+
else
|
|
24
|
+
@template_dir || (superclass.template_dir if superclass.respond_to?(:template_dir))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :template_dir
|
|
30
|
+
|
|
31
|
+
def initialize(**kwargs)
|
|
32
|
+
super
|
|
33
|
+
|
|
34
|
+
return if (@template_dir = self.class.template_dir)
|
|
35
|
+
|
|
36
|
+
raise NotImplementedError, "#{self.class} did not define template_dir"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def print_action(command, dest, source: nil)
|
|
40
|
+
line = +""
|
|
41
|
+
line << "\t" << colors.bold(colors.green(command))
|
|
42
|
+
line << "\t" << colors.green(source) if source
|
|
43
|
+
line << "\t" << colors.green(dest) if dest
|
|
44
|
+
|
|
45
|
+
puts(line)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def mkdir(path)
|
|
49
|
+
print_action "mkdir", path
|
|
50
|
+
::FileUtils.mkdir_p(path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def touch(path)
|
|
54
|
+
print_action "touch", path
|
|
55
|
+
::FileUtils.touch(path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_file(path, content)
|
|
59
|
+
print_action "create", path
|
|
60
|
+
File.write(path, content)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def chmod(mode, path)
|
|
64
|
+
print_action "chmod", path
|
|
65
|
+
::FileUtils.chmod(mode, path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cp(source, dest)
|
|
69
|
+
print_action "cp", dest, source: source
|
|
70
|
+
::FileUtils.cp(File.join(@template_dir, source), dest)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def erb(source, dest = nil)
|
|
74
|
+
print_action("erb", dest, source: source) if dest
|
|
75
|
+
|
|
76
|
+
source_path = File.join(@template_dir, source)
|
|
77
|
+
super(source_path, dest)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def sh(command, *arguments)
|
|
81
|
+
print_action "run", [command, *arguments].join(" ")
|
|
82
|
+
success = Bundler.with_unbundled_env do
|
|
83
|
+
system(command, *arguments)
|
|
84
|
+
end
|
|
85
|
+
puts colors.yellow("Warning: '#{[command, *arguments].join(" ")}' exited with non-zero status") unless success
|
|
86
|
+
success
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/gempilot/cli.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "command_kit/commands"
|
|
2
|
+
require "command_kit/commands/auto_load"
|
|
3
|
+
require "command_kit/options/version"
|
|
4
|
+
require_relative "../gempilot"
|
|
5
|
+
|
|
6
|
+
module Gempilot
|
|
7
|
+
## Top-level command router for the gempilot CLI.
|
|
8
|
+
class CLI
|
|
9
|
+
include CommandKit::Commands
|
|
10
|
+
include CommandKit::Commands::AutoLoad.new(
|
|
11
|
+
dir: "#{__dir__}/cli/commands",
|
|
12
|
+
namespace: "#{self}::Commands",
|
|
13
|
+
)
|
|
14
|
+
include CommandKit::Options::Version
|
|
15
|
+
|
|
16
|
+
command_name "gempilot"
|
|
17
|
+
version Gempilot::VERSION
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
## Manages GitHub releases for a version tag.
|
|
3
|
+
class GithubRelease
|
|
4
|
+
include StrictShell
|
|
5
|
+
|
|
6
|
+
attr_reader :tag
|
|
7
|
+
|
|
8
|
+
def initialize(tag)
|
|
9
|
+
@tag = tag
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
sh "git", "push"
|
|
14
|
+
sh "git", "push", "--tags"
|
|
15
|
+
sh "gh", "release", "create",
|
|
16
|
+
"--generate-notes", "--fail-on-no-commits",
|
|
17
|
+
tag
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
sh "gh", "release", "delete",
|
|
22
|
+
"--yes", "--cleanup-tag",
|
|
23
|
+
tag
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list
|
|
27
|
+
sh "gh", "release", "list"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
class Project
|
|
3
|
+
Version = Data.define(:path, :value) do
|
|
4
|
+
def tag
|
|
5
|
+
"v#{value}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def bump(segment = :patch)
|
|
9
|
+
case segment.to_sym
|
|
10
|
+
when :major, :minor, :patch then bump_semver(segment)
|
|
11
|
+
when :dev then bump_dev
|
|
12
|
+
else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, or :dev"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def bump_semver(segment)
|
|
19
|
+
major, minor, patch = value.split(".").first(3).map(&:to_i)
|
|
20
|
+
new_value = case segment.to_sym
|
|
21
|
+
when :major then "#{major + 1}.0.0"
|
|
22
|
+
when :minor then "#{major}.#{minor + 1}.0"
|
|
23
|
+
when :patch then "#{major}.#{minor}.#{patch + 1}"
|
|
24
|
+
end
|
|
25
|
+
with(value: new_value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def bump_dev
|
|
29
|
+
if value.match?(/\.dev\d+\z/)
|
|
30
|
+
with(value: value.sub(/\d+\z/) { it.to_i + 1 })
|
|
31
|
+
else
|
|
32
|
+
with(value: "#{value}.dev1")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias_method :next_version, :bump
|
|
37
|
+
end.freeze
|
|
38
|
+
end
|
|
39
|
+
end
|