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.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/.builds/ruby-4.0.yml +38 -0
  3. data/.pre-commit-config.yaml +16 -0
  4. data/.rubocop.yml +8 -0
  5. data/AGENTS.md +72 -0
  6. data/CHANGELOG.md +23 -0
  7. data/LICENSE +661 -0
  8. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  9. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  10. data/LICENSES/CC0-1.0.txt +121 -0
  11. data/LICENSES/MIT-0.txt +16 -0
  12. data/LICENSES/MIT.txt +18 -0
  13. data/README.md +199 -0
  14. data/REUSE.toml +18 -0
  15. data/Rakefile +13 -0
  16. data/bin/agent_rake +13 -0
  17. data/bin/announce +13 -0
  18. data/bin/console +14 -0
  19. data/bin/consolidate_md +13 -0
  20. data/bin/hbs +13 -0
  21. data/bin/setup +17 -0
  22. data/doc/contributors/documentation_style.md +121 -0
  23. data/doc/custom.css +22 -0
  24. data/exe/agent_rake +96 -0
  25. data/exe/announce +1120 -0
  26. data/exe/consolidate_md +246 -0
  27. data/exe/hbs +670 -0
  28. data/exe/scaffold +662 -0
  29. data/lib/ratatui_ruby/devtools/tasks/autodoc/examples.rb +133 -0
  30. data/lib/ratatui_ruby/devtools/tasks/autodoc/member.rb +116 -0
  31. data/lib/ratatui_ruby/devtools/tasks/autodoc/name.rb +33 -0
  32. data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +21 -0
  33. data/lib/ratatui_ruby/devtools/tasks/bump/cargo_lockfile.rb +38 -0
  34. data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +67 -0
  35. data/lib/ratatui_ruby/devtools/tasks/bump/header.rb +43 -0
  36. data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +50 -0
  37. data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +78 -0
  38. data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +63 -0
  39. data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +77 -0
  40. data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +63 -0
  41. data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +75 -0
  42. data/lib/ratatui_ruby/devtools/tasks/bump.rake +80 -0
  43. data/lib/ratatui_ruby/devtools/tasks/cargo.rake +47 -0
  44. data/lib/ratatui_ruby/devtools/tasks/doc.rake +887 -0
  45. data/lib/ratatui_ruby/devtools/tasks/example_viewer.html.erb +172 -0
  46. data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +276 -0
  47. data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +236 -0
  48. data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +143 -0
  49. data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +353 -0
  50. data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +186 -0
  51. data/lib/ratatui_ruby/devtools/tasks/license.rake +91 -0
  52. data/lib/ratatui_ruby/devtools/tasks/lint.rake +84 -0
  53. data/lib/ratatui_ruby/devtools/tasks/rdoc_config.rb +45 -0
  54. data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +54 -0
  55. data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +7 -0
  56. data/lib/ratatui_ruby/devtools/tasks/reuse.rake +104 -0
  57. data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +94 -0
  58. data/lib/ratatui_ruby/devtools/tasks/test.rake +18 -0
  59. data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +47 -0
  60. data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +18 -0
  61. data/lib/ratatui_ruby/devtools/templates/.pre-commit-config.yaml.erb +16 -0
  62. data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +8 -0
  63. data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +65 -0
  64. data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +18 -0
  65. data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +32 -0
  66. data/lib/ratatui_ruby/devtools/templates/README.md.erb +127 -0
  67. data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +33 -0
  68. data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +29 -0
  69. data/lib/ratatui_ruby/devtools/templates/bin/console.erb +18 -0
  70. data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +24 -0
  71. data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +16 -0
  72. data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +49 -0
  73. data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +24 -0
  74. data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +56 -0
  75. data/lib/ratatui_ruby/devtools/templates/doc/images/.gitkeep +0 -0
  76. data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +25 -0
  77. data/lib/ratatui_ruby/devtools/templates/exe/.gitkeep +0 -0
  78. data/lib/ratatui_ruby/devtools/templates/gemspec.erb +58 -0
  79. data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +12 -0
  80. data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +174 -0
  81. data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +62 -0
  82. data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +46 -0
  83. data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +9 -0
  84. data/lib/ratatui_ruby/devtools/templates/vendor/goodcop/base.yml +1047 -0
  85. data/lib/ratatui_ruby/devtools/version.rb +13 -0
  86. data/lib/ratatui_ruby/devtools.rb +137 -0
  87. data/mise.toml +7 -0
  88. data/sig/ratatui_ruby/devtools.rbs +15 -0
  89. data/vendor/goodcop/base.yml +1047 -0
  90. 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"