ratatui_ruby-devtools 0.1.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/.builds/ruby-4.0.yml +38 -0
- data/.pre-commit-config.yaml +16 -0
- data/.rubocop.yml +8 -0
- data/AGENTS.md +72 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +661 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/LICENSES/MIT.txt +18 -0
- data/README.md +199 -0
- data/REUSE.toml +18 -0
- data/Rakefile +13 -0
- data/bin/agent_rake +13 -0
- data/bin/announce +13 -0
- data/bin/console +14 -0
- data/bin/consolidate_md +13 -0
- data/bin/hbs +13 -0
- data/bin/setup +17 -0
- data/doc/contributors/documentation_style.md +121 -0
- data/doc/custom.css +22 -0
- data/exe/agent_rake +96 -0
- data/exe/announce +1120 -0
- data/exe/consolidate_md +246 -0
- data/exe/hbs +670 -0
- data/exe/scaffold +662 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/examples.rb +133 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/member.rb +116 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/name.rb +33 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +21 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/cargo_lockfile.rb +38 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +67 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/header.rb +43 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +50 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +78 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +77 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +75 -0
- data/lib/ratatui_ruby/devtools/tasks/bump.rake +80 -0
- data/lib/ratatui_ruby/devtools/tasks/cargo.rake +47 -0
- data/lib/ratatui_ruby/devtools/tasks/doc.rake +887 -0
- data/lib/ratatui_ruby/devtools/tasks/example_viewer.html.erb +172 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +276 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +236 -0
- data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +143 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +353 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +186 -0
- data/lib/ratatui_ruby/devtools/tasks/license.rake +91 -0
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +84 -0
- data/lib/ratatui_ruby/devtools/tasks/rdoc_config.rb +45 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +54 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +7 -0
- data/lib/ratatui_ruby/devtools/tasks/reuse.rake +104 -0
- data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +94 -0
- data/lib/ratatui_ruby/devtools/tasks/test.rake +18 -0
- data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +47 -0
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/.pre-commit-config.yaml.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +8 -0
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +65 -0
- data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +32 -0
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +127 -0
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +33 -0
- data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +29 -0
- data/lib/ratatui_ruby/devtools/templates/bin/console.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +49 -0
- data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +56 -0
- data/lib/ratatui_ruby/devtools/templates/doc/images/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +25 -0
- data/lib/ratatui_ruby/devtools/templates/exe/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +58 -0
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +12 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +174 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +62 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +46 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +9 -0
- data/lib/ratatui_ruby/devtools/templates/vendor/goodcop/base.yml +1047 -0
- data/lib/ratatui_ruby/devtools/version.rb +13 -0
- data/lib/ratatui_ruby/devtools.rb +137 -0
- data/mise.toml +7 -0
- data/sig/ratatui_ruby/devtools.rbs +15 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +252 -0
data/exe/scaffold
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#--
|
|
5
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
# Scaffolds a new RatatuiRuby ecosystem gem.
|
|
10
|
+
#
|
|
11
|
+
# Creating a new ecosystem gem requires many files with consistent formatting:
|
|
12
|
+
# gemspec, Rakefile, AGENTS.md, CI configs, license headers, and more.
|
|
13
|
+
# Manually copying and editing templates is tedious and error-prone.
|
|
14
|
+
#
|
|
15
|
+
# This script renders all devtools templates into a new gem directory with
|
|
16
|
+
# the correct gem name, copyright holder, and license. Run it to bootstrap
|
|
17
|
+
# a new ecosystem gem with all quality gates pre-configured.
|
|
18
|
+
#
|
|
19
|
+
# == Usage
|
|
20
|
+
#
|
|
21
|
+
# scaffold GEM_NAME [OPTIONS]
|
|
22
|
+
#
|
|
23
|
+
# == Options
|
|
24
|
+
#
|
|
25
|
+
# --author NAME Copyright holder name (default: from git config)
|
|
26
|
+
# --email EMAIL Copyright holder email (default: from git config)
|
|
27
|
+
# --license ID SPDX license identifier (default: AGPL-3.0-or-later)
|
|
28
|
+
# --dir PATH Parent directory for new gem (default: current directory)
|
|
29
|
+
# --help Show this help
|
|
30
|
+
#
|
|
31
|
+
# == Examples
|
|
32
|
+
#
|
|
33
|
+
# scaffold ratatui_ruby-tea
|
|
34
|
+
# scaffold ratatui_ruby-kit --license MIT
|
|
35
|
+
# scaffold ratatui_ruby-framework --dir ~/Developer
|
|
36
|
+
|
|
37
|
+
require "erb"
|
|
38
|
+
require "fileutils"
|
|
39
|
+
require "optparse"
|
|
40
|
+
|
|
41
|
+
# Configuration for scaffold generation.
|
|
42
|
+
#
|
|
43
|
+
# Holds all parameters needed to render templates. Auto-discovers author info
|
|
44
|
+
# from git config if not provided.
|
|
45
|
+
class ScaffoldConfig
|
|
46
|
+
# The gem name to scaffold.
|
|
47
|
+
attr_accessor :gem_name
|
|
48
|
+
|
|
49
|
+
# The copyright holder's name.
|
|
50
|
+
attr_accessor :author
|
|
51
|
+
|
|
52
|
+
# The copyright holder's email.
|
|
53
|
+
attr_accessor :email
|
|
54
|
+
|
|
55
|
+
# The SPDX license identifier.
|
|
56
|
+
attr_accessor :license
|
|
57
|
+
|
|
58
|
+
# The output directory path.
|
|
59
|
+
attr_accessor :output_dir
|
|
60
|
+
|
|
61
|
+
# Whether this gem includes a Rust native extension.
|
|
62
|
+
attr_accessor :has_rust
|
|
63
|
+
|
|
64
|
+
# Creates a new config with defaults from git.
|
|
65
|
+
#
|
|
66
|
+
# Reads author name and email from git config. Sets default license to
|
|
67
|
+
# LGPL-3.0-or-later. Override any accessor after creation.
|
|
68
|
+
def initialize
|
|
69
|
+
@license = "LGPL-3.0-or-later"
|
|
70
|
+
@author = `git config user.name`.strip
|
|
71
|
+
@email = `git config user.email`.strip
|
|
72
|
+
@has_rust = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The gem name with underscores (for module names).
|
|
76
|
+
def module_name
|
|
77
|
+
gem_name.split("-").map { |s| s.split("_").map(&:capitalize).join }.join("::")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The gem name with slashes (for require paths).
|
|
81
|
+
def require_path
|
|
82
|
+
gem_name.tr("-", "/")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Copyright holder string for SPDX headers.
|
|
86
|
+
def copyright_holder
|
|
87
|
+
"#{author} <#{email}>"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Current year for copyright.
|
|
91
|
+
def year
|
|
92
|
+
Time.now.year
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Ruby versions to test in CI.
|
|
96
|
+
def ruby_versions
|
|
97
|
+
%w[3.2 3.3 3.4 4.0.0]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Renders ERB templates into the target directory.
|
|
102
|
+
#
|
|
103
|
+
# Templates use ERB to inject gem-specific values. This class handles the
|
|
104
|
+
# rendering and file creation with proper directory structure.
|
|
105
|
+
class TemplateRenderer
|
|
106
|
+
# Creates a new renderer with config and paths.
|
|
107
|
+
#
|
|
108
|
+
# [config] The ScaffoldConfig with gem parameters.
|
|
109
|
+
# [templates_dir] Path to the ERB templates.
|
|
110
|
+
# [output_dir] Path where rendered files are written.
|
|
111
|
+
def initialize(config, templates_dir, output_dir)
|
|
112
|
+
@config = config
|
|
113
|
+
@templates_dir = templates_dir
|
|
114
|
+
@output_dir = output_dir
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Renders all templates into the output directory.
|
|
118
|
+
def render_all
|
|
119
|
+
FileUtils.mkdir_p(@output_dir)
|
|
120
|
+
|
|
121
|
+
render_template(".gitignore.erb", ".gitignore")
|
|
122
|
+
render_template(".pre-commit-config.yaml.erb", ".pre-commit-config.yaml")
|
|
123
|
+
render_template(".rubocop.yml.erb", ".rubocop.yml")
|
|
124
|
+
render_template("AGENTS.md.erb", "AGENTS.md")
|
|
125
|
+
render_template("CHANGELOG.md.erb", "CHANGELOG.md")
|
|
126
|
+
render_template("Gemfile.erb", "Gemfile")
|
|
127
|
+
render_template("README.md.erb", "README.md")
|
|
128
|
+
render_template("REUSE.toml.erb", "REUSE.toml")
|
|
129
|
+
render_template("Rakefile.erb", "Rakefile")
|
|
130
|
+
render_template("gemspec.erb", "#{@config.gem_name}.gemspec")
|
|
131
|
+
render_template("mise.toml.erb", "mise.toml")
|
|
132
|
+
|
|
133
|
+
# bin/ scripts
|
|
134
|
+
render_template("bin/setup.erb", "bin/setup", executable: true)
|
|
135
|
+
render_template("bin/console.erb", "bin/console", executable: true)
|
|
136
|
+
|
|
137
|
+
# task resources for CI manifest generation
|
|
138
|
+
copy_task_resources
|
|
139
|
+
|
|
140
|
+
# vendor files (RuboCop base config)
|
|
141
|
+
copy_vendor_files
|
|
142
|
+
|
|
143
|
+
# doc files (RDoc styling)
|
|
144
|
+
copy_doc_files
|
|
145
|
+
|
|
146
|
+
# lib/ structure
|
|
147
|
+
create_lib_structure
|
|
148
|
+
|
|
149
|
+
# Rust extension (if enabled)
|
|
150
|
+
create_rust_extension if @config.has_rust
|
|
151
|
+
|
|
152
|
+
puts "Scaffolded #{@config.gem_name} in #{@output_dir}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private def render_template(template_name, output_name, executable: false)
|
|
156
|
+
template_path = File.join(@templates_dir, template_name)
|
|
157
|
+
output_path = File.join(@output_dir, output_name)
|
|
158
|
+
|
|
159
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
160
|
+
|
|
161
|
+
if File.exist?(template_path)
|
|
162
|
+
if template_name.end_with?(".erb")
|
|
163
|
+
content = ERB.new(File.read(template_path), trim_mode: "-").result(binding_for_config)
|
|
164
|
+
else
|
|
165
|
+
content = File.read(template_path)
|
|
166
|
+
end
|
|
167
|
+
File.write(output_path, content)
|
|
168
|
+
FileUtils.chmod(0o755, output_path) if executable
|
|
169
|
+
puts " Created #{output_name}"
|
|
170
|
+
else
|
|
171
|
+
warn " WARNING: Template not found: #{template_name}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private def copy_task_resources
|
|
176
|
+
resources_dir = File.join(@output_dir, "tasks", "resources")
|
|
177
|
+
tasks_dir = File.join(@output_dir, "tasks")
|
|
178
|
+
FileUtils.mkdir_p(resources_dir)
|
|
179
|
+
|
|
180
|
+
# Copy task resource files
|
|
181
|
+
template_resources = File.join(@templates_dir, "tasks", "resources")
|
|
182
|
+
# Render ERB templates to tasks/resources/
|
|
183
|
+
render_template("tasks/resources/build.yml.erb", "tasks/resources/build.yml.erb")
|
|
184
|
+
render_template("tasks/resources/rubies.yml.erb", "tasks/resources/rubies.yml")
|
|
185
|
+
render_template("tasks/resources/index.html.erb", "tasks/resources/index.html.erb")
|
|
186
|
+
|
|
187
|
+
# Render example_viewer.html.erb to tasks/
|
|
188
|
+
render_template("tasks/example_viewer.html.erb", "tasks/example_viewer.html.erb")
|
|
189
|
+
|
|
190
|
+
# Create exe/.gitkeep
|
|
191
|
+
exe_dir = File.join(@output_dir, "exe")
|
|
192
|
+
FileUtils.mkdir_p(exe_dir)
|
|
193
|
+
gitkeep = File.join(exe_dir, ".gitkeep")
|
|
194
|
+
FileUtils.touch(gitkeep)
|
|
195
|
+
puts " Created exe/.gitkeep"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private def copy_vendor_files
|
|
199
|
+
vendor_dir = File.join(@output_dir, "vendor", "goodcop")
|
|
200
|
+
FileUtils.mkdir_p(vendor_dir)
|
|
201
|
+
|
|
202
|
+
src = File.join(@templates_dir, "vendor", "goodcop", "base.yml")
|
|
203
|
+
dst = File.join(vendor_dir, "base.yml")
|
|
204
|
+
if File.exist?(src)
|
|
205
|
+
FileUtils.cp(src, dst)
|
|
206
|
+
puts " Copied vendor/goodcop/base.yml"
|
|
207
|
+
else
|
|
208
|
+
warn " WARNING: vendor/goodcop/base.yml not found"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private def copy_doc_files
|
|
213
|
+
doc_dir = File.join(@output_dir, "doc")
|
|
214
|
+
FileUtils.mkdir_p(doc_dir)
|
|
215
|
+
FileUtils.mkdir_p(File.join(doc_dir, "getting_started"))
|
|
216
|
+
FileUtils.mkdir_p(File.join(doc_dir, "concepts"))
|
|
217
|
+
FileUtils.mkdir_p(File.join(doc_dir, "images"))
|
|
218
|
+
|
|
219
|
+
# Render custom.css (now an ERB template)
|
|
220
|
+
render_template("doc/custom.css.erb", "doc/custom.css")
|
|
221
|
+
|
|
222
|
+
# Create images/.gitkeep
|
|
223
|
+
FileUtils.touch(File.join(doc_dir, "images", ".gitkeep"))
|
|
224
|
+
puts " Created doc/images/.gitkeep"
|
|
225
|
+
|
|
226
|
+
# Render doc templates
|
|
227
|
+
render_template("doc/index.md.erb", "doc/index.md")
|
|
228
|
+
render_template("doc/getting_started/quickstart.md.erb", "doc/getting_started/quickstart.md")
|
|
229
|
+
render_template("doc/concepts/application_architecture.md.erb", "doc/concepts/application_architecture.md")
|
|
230
|
+
render_template("doc/concepts/application_testing.md.erb", "doc/concepts/application_testing.md")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private def create_lib_structure
|
|
234
|
+
lib_path = File.join(@output_dir, "lib", @config.require_path)
|
|
235
|
+
FileUtils.mkdir_p(lib_path)
|
|
236
|
+
|
|
237
|
+
# Split the module name into parts for proper nesting
|
|
238
|
+
module_parts = @config.module_name.split("::")
|
|
239
|
+
|
|
240
|
+
# Main require file
|
|
241
|
+
main_file = File.join(@output_dir, "lib", "#{@config.require_path}.rb")
|
|
242
|
+
# REUSE-IgnoreStart
|
|
243
|
+
File.write(main_file, <<~RUBY)
|
|
244
|
+
# frozen_string_literal: true
|
|
245
|
+
|
|
246
|
+
#--
|
|
247
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
248
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
249
|
+
#++
|
|
250
|
+
|
|
251
|
+
require_relative "#{@config.gem_name.split('-').last}/version"
|
|
252
|
+
|
|
253
|
+
# Entry point for the #{@config.gem_name} gem.
|
|
254
|
+
#
|
|
255
|
+
# Ruby libraries benefit from a clear namespace. Gems need a central module.
|
|
256
|
+
#
|
|
257
|
+
# This module serves as the namespace root. All classes and utilities live here.
|
|
258
|
+
#
|
|
259
|
+
# Require this file to load the library.
|
|
260
|
+
#{generate_nested_module(module_parts, '# Namespace for library functionality.')}
|
|
261
|
+
RUBY
|
|
262
|
+
# REUSE-IgnoreEnd
|
|
263
|
+
puts " Created lib/#{@config.require_path}.rb"
|
|
264
|
+
|
|
265
|
+
# Version file
|
|
266
|
+
version_file = File.join(lib_path, "version.rb")
|
|
267
|
+
# REUSE-IgnoreStart
|
|
268
|
+
File.write(version_file, <<~RUBY)
|
|
269
|
+
# frozen_string_literal: true
|
|
270
|
+
|
|
271
|
+
#--
|
|
272
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
273
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
274
|
+
#++
|
|
275
|
+
|
|
276
|
+
#{generate_nested_module_with_version(module_parts)}
|
|
277
|
+
RUBY
|
|
278
|
+
# REUSE-IgnoreEnd
|
|
279
|
+
puts " Created lib/#{@config.require_path}/version.rb"
|
|
280
|
+
|
|
281
|
+
# Test directory with placeholder
|
|
282
|
+
test_path = File.join(@output_dir, "test")
|
|
283
|
+
FileUtils.mkdir_p(test_path)
|
|
284
|
+
# REUSE-IgnoreStart
|
|
285
|
+
File.write(File.join(test_path, "test_helper.rb"), <<~RUBY)
|
|
286
|
+
# frozen_string_literal: true
|
|
287
|
+
|
|
288
|
+
#--
|
|
289
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
290
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
291
|
+
#++
|
|
292
|
+
|
|
293
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
294
|
+
require "#{@config.require_path}"
|
|
295
|
+
require "minitest/autorun"
|
|
296
|
+
RUBY
|
|
297
|
+
# REUSE-IgnoreEnd
|
|
298
|
+
puts " Created test/test_helper.rb"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private def create_rust_extension
|
|
302
|
+
ext_name = @config.gem_name.tr("-", "_")
|
|
303
|
+
ext_path = File.join(@output_dir, "ext", ext_name)
|
|
304
|
+
cargo_path = File.join(ext_path, ".cargo")
|
|
305
|
+
FileUtils.mkdir_p(cargo_path)
|
|
306
|
+
|
|
307
|
+
# Cargo.toml
|
|
308
|
+
# REUSE-IgnoreStart
|
|
309
|
+
File.write(File.join(ext_path, "Cargo.toml"), <<~TOML)
|
|
310
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
311
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
312
|
+
|
|
313
|
+
[package]
|
|
314
|
+
name = "#{ext_name}"
|
|
315
|
+
version = "0.1.0"
|
|
316
|
+
edition = "2021"
|
|
317
|
+
|
|
318
|
+
[lib]
|
|
319
|
+
crate-type = ["cdylib", "staticlib"]
|
|
320
|
+
|
|
321
|
+
[dependencies]
|
|
322
|
+
magnus = "0.8"
|
|
323
|
+
ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info"] }
|
|
324
|
+
|
|
325
|
+
# Optional: Arena allocation to avoid Box::leak for long-lived Ruby objects
|
|
326
|
+
# bumpalo = "3.16"
|
|
327
|
+
# lazy_static = "1.4"
|
|
328
|
+
TOML
|
|
329
|
+
# REUSE-IgnoreEnd
|
|
330
|
+
puts " Created ext/#{ext_name}/Cargo.toml"
|
|
331
|
+
|
|
332
|
+
# extconf.rb
|
|
333
|
+
# REUSE-IgnoreStart
|
|
334
|
+
File.write(File.join(ext_path, "extconf.rb"), <<~RUBY)
|
|
335
|
+
# frozen_string_literal: true
|
|
336
|
+
|
|
337
|
+
#--
|
|
338
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
339
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
340
|
+
#++
|
|
341
|
+
|
|
342
|
+
require "mkmf"
|
|
343
|
+
require "rb_sys/mkmf"
|
|
344
|
+
|
|
345
|
+
create_rust_makefile("#{ext_name}/#{ext_name}") do |r|
|
|
346
|
+
# Optional: Force release profile if needed
|
|
347
|
+
# r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
|
|
348
|
+
|
|
349
|
+
# Force static linking on musl to avoid "cdylib" issues
|
|
350
|
+
if RbConfig::CONFIG["target_os"].include?("linux-musl") || RbConfig::CONFIG["host_os"].include?("linux-musl")
|
|
351
|
+
r.extra_rustc_args = ["--crate-type", "staticlib"]
|
|
352
|
+
else
|
|
353
|
+
r.extra_rustc_args = ["--crate-type", "cdylib"]
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
RUBY
|
|
357
|
+
# REUSE-IgnoreEnd
|
|
358
|
+
puts " Created ext/#{ext_name}/extconf.rb"
|
|
359
|
+
|
|
360
|
+
# .cargo/config.toml
|
|
361
|
+
# REUSE-IgnoreStart
|
|
362
|
+
File.write(File.join(cargo_path, "config.toml"), <<~TOML)
|
|
363
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
364
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
365
|
+
|
|
366
|
+
[target.aarch64-apple-darwin]
|
|
367
|
+
rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
|
|
368
|
+
|
|
369
|
+
[target.x86_64-apple-darwin]
|
|
370
|
+
rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
|
|
371
|
+
|
|
372
|
+
# Force dynamic linking on Alpine (musl) to fix "Dynamic loading not supported"
|
|
373
|
+
# errors when bindgen tries to load libclang.
|
|
374
|
+
[target.'cfg(target_env = "musl")']
|
|
375
|
+
rustflags = ["-C", "target-feature=-crt-static"]
|
|
376
|
+
TOML
|
|
377
|
+
# REUSE-IgnoreEnd
|
|
378
|
+
puts " Created ext/#{ext_name}/.cargo/config.toml"
|
|
379
|
+
|
|
380
|
+
# .gitignore
|
|
381
|
+
# REUSE-IgnoreStart
|
|
382
|
+
File.write(File.join(ext_path, ".gitignore"), <<~GITIGNORE)
|
|
383
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
384
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
385
|
+
|
|
386
|
+
/target/
|
|
387
|
+
GITIGNORE
|
|
388
|
+
# REUSE-IgnoreEnd
|
|
389
|
+
puts " Created ext/#{ext_name}/.gitignore"
|
|
390
|
+
|
|
391
|
+
# clippy.toml
|
|
392
|
+
# REUSE-IgnoreStart
|
|
393
|
+
File.write(File.join(ext_path, "clippy.toml"), <<~TOML)
|
|
394
|
+
# SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
395
|
+
# SPDX-License-Identifier: #{@config.license}
|
|
396
|
+
|
|
397
|
+
disallowed-methods = [
|
|
398
|
+
{ path = "std::boxed::Box::leak", reason = "Use bumpalo arena allocation instead." },
|
|
399
|
+
{ path = "std::vec::Vec::leak", reason = "Use bumpalo arena allocation instead." },
|
|
400
|
+
]
|
|
401
|
+
TOML
|
|
402
|
+
# REUSE-IgnoreEnd
|
|
403
|
+
puts " Created ext/#{ext_name}/clippy.toml"
|
|
404
|
+
|
|
405
|
+
# src/lib.rs stub
|
|
406
|
+
src_path = File.join(ext_path, "src")
|
|
407
|
+
FileUtils.mkdir_p(src_path)
|
|
408
|
+
# REUSE-IgnoreStart
|
|
409
|
+
File.write(File.join(src_path, "lib.rs"), <<~RUST)
|
|
410
|
+
// SPDX-FileCopyrightText: #{@config.year} #{@config.copyright_holder}
|
|
411
|
+
// SPDX-License-Identifier: #{@config.license}
|
|
412
|
+
|
|
413
|
+
use magnus::{define_module, prelude::*, Error};
|
|
414
|
+
|
|
415
|
+
#[magnus::init]
|
|
416
|
+
fn init() -> Result<(), Error> {
|
|
417
|
+
let module = define_module("#{@config.module_name.split('::').first}")?;
|
|
418
|
+
// Add your native methods here
|
|
419
|
+
Ok(())
|
|
420
|
+
}
|
|
421
|
+
RUST
|
|
422
|
+
# REUSE-IgnoreEnd
|
|
423
|
+
puts " Created ext/#{ext_name}/src/lib.rs"
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
private def generate_nested_module(parts, content, indent = 0, is_outermost = true)
|
|
427
|
+
return content if parts.empty?
|
|
428
|
+
|
|
429
|
+
prefix = " " * indent
|
|
430
|
+
first, *rest = parts
|
|
431
|
+
inner = generate_nested_module(rest, content, indent + 1, false)
|
|
432
|
+
inner_prefix = " " * (indent + 1)
|
|
433
|
+
|
|
434
|
+
if rest.empty?
|
|
435
|
+
# Innermost module - content is a doc comment that goes BEFORE the module
|
|
436
|
+
if content.empty?
|
|
437
|
+
"#{prefix}module #{first}\n#{prefix}end"
|
|
438
|
+
else
|
|
439
|
+
"#{prefix}#{content}\n#{prefix}module #{first}\n#{prefix}end"
|
|
440
|
+
end
|
|
441
|
+
else
|
|
442
|
+
# Outer modules get :nodoc: to avoid "missing documentation" warnings
|
|
443
|
+
# (they're documented in the main require file or the core gem)
|
|
444
|
+
nodoc = is_outermost ? " # :nodoc: Documented in the ratatui_ruby gem." : ""
|
|
445
|
+
"#{prefix}module #{first}#{nodoc}\n#{inner}\n#{prefix}end"
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
private def generate_nested_module_with_error(parts, indent = 0)
|
|
450
|
+
prefix = " " * indent
|
|
451
|
+
if parts.length == 1
|
|
452
|
+
# Innermost module with documented Error class
|
|
453
|
+
<<~RUBY.chomp
|
|
454
|
+
#{prefix}module #{parts.first}
|
|
455
|
+
#{prefix} # Base error class for this library.
|
|
456
|
+
#{prefix} #
|
|
457
|
+
#{prefix} # All library-specific exceptions inherit from this class.
|
|
458
|
+
#{prefix} # Catch this to handle any library error generically.
|
|
459
|
+
#{prefix} class Error < StandardError; end
|
|
460
|
+
#{prefix}end
|
|
461
|
+
RUBY
|
|
462
|
+
else
|
|
463
|
+
first, *rest = parts
|
|
464
|
+
inner = generate_nested_module_with_error(rest, indent + 1)
|
|
465
|
+
"#{prefix}module #{first}\n#{inner}\n#{prefix}end"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
private def generate_nested_module_with_version(parts, indent = 0, is_outermost = true)
|
|
470
|
+
prefix = " " * indent
|
|
471
|
+
if parts.length == 1
|
|
472
|
+
# Innermost module with documented VERSION constant
|
|
473
|
+
<<~RUBY.chomp
|
|
474
|
+
#{prefix}module #{parts.first}
|
|
475
|
+
#{prefix} # The version of this gem.
|
|
476
|
+
#{prefix} # See https://semver.org/spec/v2.0.0.html
|
|
477
|
+
#{prefix} VERSION = "0.1.0"
|
|
478
|
+
#{prefix}end
|
|
479
|
+
RUBY
|
|
480
|
+
else
|
|
481
|
+
first, *rest = parts
|
|
482
|
+
inner = generate_nested_module_with_version(rest, indent + 1, false)
|
|
483
|
+
# Outer modules get :nodoc: to avoid "missing documentation" warnings
|
|
484
|
+
# (they're documented in the main require file or the core gem)
|
|
485
|
+
nodoc = is_outermost ? " # :nodoc: Documented in the ratatui_ruby gem." : ""
|
|
486
|
+
"#{prefix}module #{first}#{nodoc}\n#{inner}\n#{prefix}end"
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
private def binding_for_config
|
|
491
|
+
# Create a binding with all config values available to templates
|
|
492
|
+
b = binding
|
|
493
|
+
b.local_variable_set(:gem_name, @config.gem_name)
|
|
494
|
+
b.local_variable_set(:module_name, @config.module_name)
|
|
495
|
+
b.local_variable_set(:require_path, @config.require_path)
|
|
496
|
+
b.local_variable_set(:copyright_holder, @config.copyright_holder)
|
|
497
|
+
b.local_variable_set(:license, @config.license)
|
|
498
|
+
b.local_variable_set(:author, @config.author)
|
|
499
|
+
b.local_variable_set(:email, @config.email)
|
|
500
|
+
b.local_variable_set(:year, @config.year)
|
|
501
|
+
b.local_variable_set(:has_rdoc, true)
|
|
502
|
+
b.local_variable_set(:has_rust, @config.has_rust)
|
|
503
|
+
b.local_variable_set(:dependencies, [])
|
|
504
|
+
b.local_variable_set(:summary, "Part of the RatatuiRuby ecosystem")
|
|
505
|
+
b.local_variable_set(:description, "#{@config.module_name} - part of the RatatuiRuby TUI framework ecosystem")
|
|
506
|
+
b.local_variable_set(:version_file_require, "lib/#{@config.require_path}/version")
|
|
507
|
+
b.local_variable_set(:version_file, "lib/#{@config.require_path}/version.rb")
|
|
508
|
+
b
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Parse command line options
|
|
513
|
+
config = ScaffoldConfig.new
|
|
514
|
+
parent_dir = Dir.pwd
|
|
515
|
+
force = false
|
|
516
|
+
|
|
517
|
+
OptionParser.new do |opts|
|
|
518
|
+
opts.banner = "Usage: scaffold GEM_NAME [OPTIONS]"
|
|
519
|
+
|
|
520
|
+
opts.on("--author NAME", "Copyright holder name") do |name|
|
|
521
|
+
config.author = name
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
opts.on("--email EMAIL", "Copyright holder email") do |email|
|
|
525
|
+
config.email = email
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
opts.on("--license ID", "SPDX license identifier") do |license|
|
|
529
|
+
config.license = license
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
opts.on("--dir PATH", "Parent directory for new gem") do |dir|
|
|
533
|
+
parent_dir = File.expand_path(dir)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
opts.on("-f", "--force", "Remove existing directory if present") do
|
|
537
|
+
force = true
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
opts.on("--rust", "Include Rust native extension support") do
|
|
541
|
+
config.has_rust = true
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
opts.on("-h", "--help", "Show this help") do
|
|
545
|
+
puts opts
|
|
546
|
+
exit
|
|
547
|
+
end
|
|
548
|
+
end.parse!
|
|
549
|
+
|
|
550
|
+
if ARGV.empty?
|
|
551
|
+
warn "Error: GEM_NAME is required"
|
|
552
|
+
warn "Usage: scaffold GEM_NAME [OPTIONS]"
|
|
553
|
+
exit 1
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
config.gem_name = ARGV[0]
|
|
557
|
+
config.output_dir = File.join(parent_dir, config.gem_name)
|
|
558
|
+
|
|
559
|
+
if File.exist?(config.output_dir)
|
|
560
|
+
if force
|
|
561
|
+
FileUtils.rm_rf(config.output_dir)
|
|
562
|
+
puts "Removed existing directory: #{config.output_dir}"
|
|
563
|
+
else
|
|
564
|
+
warn "Error: Directory already exists: #{config.output_dir}"
|
|
565
|
+
warn "Use --force to remove it"
|
|
566
|
+
exit 1
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Find templates directory
|
|
571
|
+
templates_dir = File.expand_path("../lib/ratatui_ruby/devtools/templates", __dir__)
|
|
572
|
+
unless File.directory?(templates_dir)
|
|
573
|
+
# Fallback for installed gem
|
|
574
|
+
spec = Gem.loaded_specs["ratatui_ruby-devtools"]
|
|
575
|
+
if spec
|
|
576
|
+
templates_dir = File.join(spec.gem_dir, "lib/ratatui_ruby/devtools/templates")
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
unless File.directory?(templates_dir)
|
|
581
|
+
warn "Error: Could not find templates directory"
|
|
582
|
+
exit 1
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
puts "Scaffolding #{config.gem_name}..."
|
|
586
|
+
puts " Author: #{config.copyright_holder}"
|
|
587
|
+
puts " License: #{config.license}"
|
|
588
|
+
puts ""
|
|
589
|
+
|
|
590
|
+
renderer = TemplateRenderer.new(config, templates_dir, config.output_dir)
|
|
591
|
+
renderer.render_all
|
|
592
|
+
|
|
593
|
+
# Initialize git repo with proper branch structure
|
|
594
|
+
puts ""
|
|
595
|
+
puts "Initializing git repository..."
|
|
596
|
+
Dir.chdir(config.output_dir) do
|
|
597
|
+
# Initialize git with trunk as the default branch (not main/master)
|
|
598
|
+
system("git init --quiet --initial-branch=trunk")
|
|
599
|
+
|
|
600
|
+
# Add remote origin for SourceHut
|
|
601
|
+
remote_url = "git@git.sr.ht:~kerrick/#{config.gem_name}"
|
|
602
|
+
system("git remote add origin #{remote_url}")
|
|
603
|
+
puts " Set remote origin: #{remote_url}"
|
|
604
|
+
|
|
605
|
+
# Initial add (before setup runs)
|
|
606
|
+
system("git add -A")
|
|
607
|
+
end
|
|
608
|
+
puts " Initialized git with trunk branch"
|
|
609
|
+
|
|
610
|
+
# Run setup (installs gems, pre-commit, downloads licenses, etc.)
|
|
611
|
+
puts ""
|
|
612
|
+
puts "Running bin/setup..."
|
|
613
|
+
Dir.chdir(config.output_dir) do
|
|
614
|
+
system("mise trust --yes --raw")
|
|
615
|
+
system("bin/setup")
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Copy LICENSE from primary license for gem distribution
|
|
619
|
+
puts ""
|
|
620
|
+
puts "Setting up LICENSE..."
|
|
621
|
+
Dir.chdir(config.output_dir) do
|
|
622
|
+
license_file = "LICENSES/#{config.license}.txt"
|
|
623
|
+
if File.exist?(license_file)
|
|
624
|
+
FileUtils.cp(license_file, "LICENSE")
|
|
625
|
+
puts " Copied #{license_file} to LICENSE"
|
|
626
|
+
else
|
|
627
|
+
system("mise x -- reuse download #{config.license}")
|
|
628
|
+
if File.exist?(license_file)
|
|
629
|
+
FileUtils.cp(license_file, "LICENSE")
|
|
630
|
+
puts " Downloaded and copied #{config.license} to LICENSE"
|
|
631
|
+
else
|
|
632
|
+
warn " WARNING: Could not find or download license: #{config.license}"
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Create initial commit and tags
|
|
638
|
+
puts ""
|
|
639
|
+
puts "Creating initial commit and tags..."
|
|
640
|
+
Dir.chdir(config.output_dir) do
|
|
641
|
+
# Stage ALL files including those created by bin/setup
|
|
642
|
+
system("git add -A")
|
|
643
|
+
|
|
644
|
+
# Commit (don't use --quiet so we see any errors)
|
|
645
|
+
unless system("git commit -m 'chore: scaffold #{config.gem_name} with ratatui_ruby-devtools'")
|
|
646
|
+
warn " WARNING: Initial commit failed, retrying with --allow-empty"
|
|
647
|
+
system("git commit --allow-empty -m 'chore: scaffold #{config.gem_name} with ratatui_ruby-devtools'")
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Create tag and stable branch
|
|
651
|
+
system("git tag v0.1.0 -m 'Initial scaffold release'")
|
|
652
|
+
system("git branch stable")
|
|
653
|
+
puts " Created branches: trunk (default), stable"
|
|
654
|
+
puts " Created tag: v0.1.0"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
puts ""
|
|
658
|
+
puts "Done! Your gem is ready at: #{config.output_dir}"
|
|
659
|
+
puts ""
|
|
660
|
+
puts "Next steps:"
|
|
661
|
+
puts " cd #{config.gem_name}"
|
|
662
|
+
puts " bundle exec rake"
|