foobar_templates 2.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.beader/.gitignore +6 -0
  3. data/.beader/issues/1-support-flexible-template-topologies.yaml +21 -0
  4. data/.beader/issues/2-add-monorepo-support-for-template-discovery-and-generation.yaml +33 -0
  5. data/.beader/issues/3-implement-monorepo-aware-recursive-template-inventory.yaml +29 -0
  6. data/.beader/issues/4-add-leaf-template-selection-with-ambiguity-and-not-found-handling.yaml +30 -0
  7. data/.beader/issues/5-integrate-leaf-template-resolution-into-generation-flow.yaml +30 -0
  8. data/.beader/issues/6-add-monorepo-discovery-and-generation-test-coverage.yaml +31 -0
  9. data/.beader/issues/7-document-monorepo-template-configuration-and-selection.yaml +30 -0
  10. data/.beader/meta.yaml +1 -0
  11. data/.gitignore +16 -0
  12. data/.rspec +2 -0
  13. data/Gemfile +3 -0
  14. data/LICENSE.txt +22 -0
  15. data/README.md +154 -0
  16. data/Rakefile +55 -0
  17. data/bin/foobar_templates +72 -0
  18. data/changelog +107 -0
  19. data/config/config +8 -0
  20. data/foobar_templates.gemspec +33 -0
  21. data/lib/foobar_templates/cli/cheat_sheet.rb +12 -0
  22. data/lib/foobar_templates/cli/cli.rb +3 -0
  23. data/lib/foobar_templates/cli/dir_to_template.rb +120 -0
  24. data/lib/foobar_templates/cli/setup_personal_templates_repo.rb +135 -0
  25. data/lib/foobar_templates/cli/template_generator.rb +462 -0
  26. data/lib/foobar_templates/configurator.rb +99 -0
  27. data/lib/foobar_templates/core/core.rb +9 -0
  28. data/lib/foobar_templates/core/dir_to_template.rb +114 -0
  29. data/lib/foobar_templates/strings.rb +23 -0
  30. data/lib/foobar_templates/template_manager.rb +119 -0
  31. data/lib/foobar_templates/templates/template-test/.vscode/launch.json +0 -0
  32. data/lib/foobar_templates/templates/template-test/foo-bar/keep +0 -0
  33. data/lib/foobar_templates/templates/template-test/foo-bar.rb +16 -0
  34. data/lib/foobar_templates/templates/template-test/foo_bar/keep +0 -0
  35. data/lib/foobar_templates/templates/template-test/foobar.yml +2 -0
  36. data/lib/foobar_templates/templates/template-test/simple_dir/keep +0 -0
  37. data/lib/foobar_templates/templates/template-test/test_confirmed +0 -0
  38. data/lib/foobar_templates/templates/test_template/foo-bar/keep +0 -0
  39. data/lib/foobar_templates/templates/test_template/foo-bar.rb +21 -0
  40. data/lib/foobar_templates/templates/test_template/foo_bar/keep +0 -0
  41. data/lib/foobar_templates/templates/test_template/foobar.yml +2 -0
  42. data/lib/foobar_templates/templates/test_template/simple_dir/keep +0 -0
  43. data/lib/foobar_templates/version.rb +3 -0
  44. data/lib/foobar_templates.rb +151 -0
  45. data/spec/data/variable_manifest_test.rb +21 -0
  46. data/spec/foobar_templates/cli/dir_to_template_spec.rb +153 -0
  47. data/spec/foobar_templates/core/dir_to_template_spec.rb +104 -0
  48. data/spec/foobar_templates_spec.rb +573 -0
  49. data/spec/spec_helper.rb +106 -0
  50. data/spec/template_manager_spec.rb +68 -0
  51. metadata +157 -0
@@ -0,0 +1,462 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'open3'
4
+ require 'shellwords'
5
+ require 'set'
6
+
7
+ $TRACE = false
8
+
9
+ module FoobarTemplates::CLI
10
+ class TemplateGenerator
11
+
12
+ attr_reader :options, :gem_name, :name, :target
13
+
14
+ def initialize(options, gem_name)
15
+ @options = options
16
+ @gem_name = resolve_name(gem_name)
17
+
18
+ @name = @gem_name
19
+ @target = Pathname.pwd.join(gem_name)
20
+ @template_src = ::FoobarTemplates::TemplateManager.get_template_src(options)
21
+ @configurator = ::FoobarTemplates::Configurator.new
22
+
23
+ @tconf = load_template_configs
24
+ end
25
+
26
+ def config
27
+ @config ||= build_interpolation_config
28
+ end
29
+
30
+ def run
31
+ puts "Beginning run" if $TRACE
32
+ raise_project_with_that_name_already_exists! if File.exist?(target)
33
+
34
+ puts "ensure_safe_project_name" if $TRACE
35
+ ensure_safe_project_name(name, config[:constant_array])
36
+
37
+ puts "run_name_validation" if $TRACE
38
+ run_name_validation
39
+
40
+ template_src = match_template_src
41
+
42
+ puts "dynamically_generate_template_directories" if $TRACE
43
+ time_it("dynamically_generate_template_directories") do
44
+ @template_directories = dynamically_generate_template_directories
45
+ end
46
+
47
+ puts "dynamically_generate_templates_files" if $TRACE
48
+ templates = dynamically_generate_templates_files
49
+
50
+ puts "Creating new project folder '#{name}'\n\n"
51
+ create_template_directories(@template_directories, target)
52
+
53
+ templates.each do |src, dst|
54
+ template("#{template_src}/#{src}", target.join(dst), config)
55
+ end
56
+
57
+ Dir.chdir(target) do
58
+ if @configurator.always_perform_git_init || !inside_git_work_tree?
59
+ `git init`
60
+ end
61
+ `git add .`
62
+ end
63
+
64
+ if @tconf[:bootstrap_command]
65
+ puts "Executing bootstrap_command"
66
+ cmd = safe_gsub_template_variables(@tconf[:bootstrap_command])
67
+ puts cmd
68
+ Dir.chdir(target) do
69
+ `#{cmd}`
70
+ end
71
+ end
72
+
73
+ puts "\nComplete."
74
+ end
75
+
76
+ def build_interpolation_config
77
+ title = name.tr('-', '_').split('_').map(&:capitalize).join(" ")
78
+ pascal_name = name.tr('-', '_').split('_').map(&:capitalize).join
79
+ unprefixed_name = name.sub(/^#{@tconf[:prefix]}/, '')
80
+ underscored_name = name.tr('-', '_')
81
+ constant_name = name.split('_').map{|p| p[0..0].upcase + p[1..-1] unless p.empty?}.join
82
+ constant_name = constant_name.split('-').map{|q| q[0..0].upcase + q[1..-1] }.join('::') if constant_name =~ /-/
83
+ constant_array = constant_name.split('::')
84
+ git_user_name = `git config user.name`.chomp
85
+ git_user_email = `git config user.email`.chomp
86
+
87
+ # Resolve domain values from ~/.foobar/config, prompting if needed
88
+ required_domains = scan_template_for_required_domains
89
+ prompt_for_missing_domains(required_domains)
90
+
91
+ registry_domain = @configurator.domain('registry_domain')
92
+ k8s_domain = @configurator.domain('k8s_domain')
93
+ git_repo_domain = @configurator.domain('repo_domain') || 'github.com'
94
+
95
+ if git_user_name.empty?
96
+ raise FoobarTemplates::CLIError, [
97
+ "Error: git config user.name didn't return a value. You'll probably want to make sure that's configured with your github username:",
98
+ "",
99
+ "git config --global user.name YOUR_GH_NAME",
100
+ ].join("\n")
101
+ else
102
+ # git_repo_path = provider.com/user/name
103
+ git_repo_path = "#{git_repo_domain}/#{git_user_name}/#{name}".downcase # downcasing for languages like go that are creative
104
+ end
105
+
106
+ # git_repo_url = https://provider.com/user/name
107
+ git_repo_url = "https://#{git_repo_domain}/#{git_user_name}/#{name}"
108
+
109
+ image_path = "#{git_user_name}/#{name}".downcase
110
+ registry_repo_path = "#{registry_domain}/#{image_path}".downcase
111
+
112
+ config = {
113
+ :name => name,
114
+ :title => title,
115
+ :unprefixed_name => unprefixed_name,
116
+ unprefixed_pascal: unprefixed_name.tr('-', '_').split('_').map(&:capitalize).join,
117
+ underscored_name: underscored_name,
118
+ :pascal_name => pascal_name,
119
+ :camel_name => pascal_name.sub(/^./, &:downcase),
120
+ :screamcase_name => name.tr('-', '_').upcase,
121
+ :namespaced_path => name.tr('-', '/'),
122
+ :makefile_path => "#{underscored_name}/#{underscored_name}",
123
+ :constant_name => constant_name,
124
+ :constant_array => constant_array,
125
+ :author => git_user_name.empty? ? "TODO: Write your name" : git_user_name,
126
+ :email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email,
127
+ :git_repo_domain => git_repo_domain,
128
+ :git_repo_url => git_repo_url,
129
+ :git_repo_path => git_repo_path,
130
+ :image_path => image_path,
131
+ :registry_domain => registry_domain,
132
+ :registry_repo_path => registry_repo_path,
133
+ :k8s_domain => k8s_domain,
134
+ :template => @options[:template],
135
+ :test => @options[:test],
136
+ }
137
+ end
138
+
139
+
140
+ private
141
+
142
+ def inside_git_work_tree?
143
+ system("git rev-parse --is-inside-work-tree", out: File::NULL, err: File::NULL)
144
+ end
145
+
146
+ def safe_gsub_template_variables(user_string)
147
+ build_content_replacement_pairs.inject(user_string) do |result, (find, replace)|
148
+ result.gsub(find, replace)
149
+ end
150
+ end
151
+
152
+ # Runs declarative name validation rules from the template's foobar.yml:
153
+ #
154
+ # name_validation:
155
+ # reserved_names: [test, std, fmt] # exact-match denylist
156
+ # regex_validator: "^[a-z][a-z0-9-]*$" # name MUST match this pattern
157
+ #
158
+ # Both keys are optional. All checks run in pure Ruby — no shell, no
159
+ # cross-platform concerns.
160
+ def run_name_validation
161
+ rules = @tconf[:name_validation]
162
+ return if rules.nil? || rules.empty?
163
+
164
+ reserved = Array(rules[:reserved_names]).map(&:to_s)
165
+ if reserved.include?(name)
166
+ $stderr.puts "Invalid project name '#{name}': reserved by template '#{@options[:template]}'. Please choose another name."
167
+ raise
168
+ end
169
+
170
+ pattern = rules[:regex_validator]
171
+ if pattern && !pattern.to_s.empty?
172
+ begin
173
+ regex = Regexp.new(pattern.to_s)
174
+ rescue RegexpError => e
175
+ $stderr.puts "Template '#{@options[:template]}' has an invalid name_validation.regex_validator: #{e.message}"
176
+ raise "invalid name_validation.regex_validator"
177
+ end
178
+
179
+ unless regex.match?(name)
180
+ $stderr.puts "Invalid project name '#{name}': does not match #{regex.inspect} required by template '#{@options[:template]}'."
181
+ raise
182
+ end
183
+ end
184
+ end
185
+
186
+ # Domain placeholder → config key mapping
187
+ DOMAIN_PLACEHOLDERS = {
188
+ 'registry_domain' => %w[FOO_REGISTRY_DOMAIN FOO_REGISTRY_REPO_PATH],
189
+ 'k8s_domain' => %w[FOO_K8S_DOMAIN],
190
+ 'repo_domain' => %w[FOO_GIT_REPO_DOMAIN FOO_GIT_REPO_PATH FOO_GIT_REPO_URL],
191
+ }.freeze
192
+
193
+ # Human-readable names for prompting
194
+ DOMAIN_DISPLAY_NAMES = {
195
+ 'registry_domain' => 'registry-domain',
196
+ 'k8s_domain' => 'k8s-domain',
197
+ 'repo_domain' => 'repo-domain',
198
+ }.freeze
199
+
200
+ DOMAIN_DEFAULTS = {
201
+ 'repo_domain' => 'github.com',
202
+ }.freeze
203
+
204
+ def scan_template_for_required_domains
205
+ all_placeholders = DOMAIN_PLACEHOLDERS.values.flatten
206
+ pattern = Regexp.union(all_placeholders)
207
+ found_placeholders = Set.new
208
+
209
+ Dir.glob("#{@template_src}/**/*", File::FNM_DOTMATCH).each do |f|
210
+ next unless File.file?(f)
211
+ base_path = f[@template_src.length+1..-1]
212
+ next if base_path.nil?
213
+ next if base_path.start_with?(".git" + File::SEPARATOR) || base_path == ".git"
214
+ next if binary_file?(f)
215
+
216
+ content = File.read(f)
217
+ all_placeholders.each do |ph|
218
+ found_placeholders << ph if content.include?(ph)
219
+ end
220
+ end
221
+
222
+ # Map found placeholders back to domain config keys
223
+ required = Set.new
224
+ DOMAIN_PLACEHOLDERS.each do |domain_key, placeholders|
225
+ required << domain_key if placeholders.any? { |ph| found_placeholders.include?(ph) }
226
+ end
227
+ required.to_a
228
+ end
229
+
230
+ def prompt_for_missing_domains(required_domains)
231
+ required_domains.each do |domain_key|
232
+ next if @configurator.domain(domain_key) && !@configurator.domain(domain_key).empty?
233
+
234
+ display_name = DOMAIN_DISPLAY_NAMES[domain_key]
235
+ default = DOMAIN_DEFAULTS[domain_key]
236
+ default_hint = default ? " (default: #{default})" : ""
237
+
238
+ puts "This template requires '#{display_name}'. The value will be saved to ~/.foobar/config for future use."
239
+ print "Enter #{display_name}#{default_hint}: "
240
+ value = $stdin.gets&.chomp || ''
241
+
242
+ value = default if value.empty? && default
243
+
244
+ if value.empty?
245
+ puts "Warning: No value provided for '#{display_name}'. Template placeholders may not be fully resolved."
246
+ end
247
+
248
+ @configurator.set_domain(domain_key, value)
249
+ end
250
+ end
251
+
252
+ def load_template_configs
253
+ template_config_path = File.join(@template_src, "foobar.yml")
254
+
255
+ if File.exist?(template_config_path)
256
+ t_config = YAML.load_file(template_config_path, symbolize_names: true)
257
+ else
258
+ t_config = {
259
+ purpose: "tool",
260
+ language: "go"
261
+ }
262
+ end
263
+
264
+ if t_config[:prefix].nil?
265
+ t_config[:prefix] = t_config[:purpose] ? "#{t_config[:purpose]}-" : ""
266
+ t_config[:prefix] += t_config[:language] ? "#{t_config[:language]}-" : ""
267
+ end
268
+
269
+ t_config
270
+ end
271
+
272
+ # Returns a hash of source directory names and their destination mappings
273
+ def dynamically_generate_template_directories
274
+ template_dirs = Dir.glob("#{@template_src}/**/*", File::FNM_DOTMATCH).filter_map do |f|
275
+ base_path = f[@template_src.length+1..-1]
276
+ next if base_path.start_with?(".git" + File::SEPARATOR) || base_path == ".git"
277
+ next if f == "#{@template_src}/." || f == "#{@template_src}/.."
278
+ next unless File.directory?(f)
279
+ # next if ignored_by_git?(@template_src, base_path)
280
+
281
+ [base_path, substitute_template_values(base_path)]
282
+ end.to_h
283
+ filter_ignored_files!(@template_src, template_dirs)
284
+
285
+ template_dirs
286
+ end
287
+
288
+ # Figures out the translation between all template files and their
289
+ # destination names
290
+ def dynamically_generate_templates_files
291
+ template_files = Dir.glob("#{@template_src}/**/*", File::FNM_DOTMATCH).filter_map do |f|
292
+ base_path = f[@template_src.length+1..-1]
293
+ next if base_path.nil?
294
+ next if base_path.start_with?(".git" + File::SEPARATOR) || base_path == ".git"
295
+ next if base_path == "foobar.yml"
296
+ next unless File.file?(f)
297
+
298
+ [base_path, substitute_template_values(base_path)]
299
+ end.to_h
300
+
301
+ raise_no_files_in_template_error! if template_files.empty?
302
+ filter_ignored_files!(@template_src, template_files)
303
+
304
+ return template_files
305
+ end
306
+
307
+
308
+ # Applies literal foo-bar variant substitutions to path strings
309
+ def substitute_template_values(path_str)
310
+ build_filename_replacement_pairs.inject(path_str) do |result, (find, replace)|
311
+ result.gsub(find, replace)
312
+ end
313
+ end
314
+
315
+ def build_filename_replacement_pairs
316
+ [
317
+ ['FOO_BAR', config[:screamcase_name]],
318
+ ['FooBar', config[:pascal_name]],
319
+ ['fooBar', config[:camel_name]],
320
+ ['foo-bar', config[:name]],
321
+ ['foo_bar', config[:underscored_name]],
322
+ ]
323
+ end
324
+
325
+ def build_content_replacement_pairs
326
+ [
327
+ # FOO_ prefixed non-name variables
328
+ ['FOO_REGISTRY_REPO_PATH', config[:registry_repo_path] || ''],
329
+ ['FOO_GIT_REPO_DOMAIN', config[:git_repo_domain]],
330
+ ['FOO_GIT_REPO_PATH', config[:git_repo_path]],
331
+ ['FOO_GIT_REPO_URL', config[:git_repo_url]],
332
+ ['FOO_REGISTRY_DOMAIN', config[:registry_domain] || ''],
333
+ ['FOO_IMAGE_PATH', config[:image_path]],
334
+ ['FOO_K8S_DOMAIN', config[:k8s_domain] || ''],
335
+ ['FOO_AUTHOR', config[:author]],
336
+ ['FOO_EMAIL', config[:email]],
337
+ # Name-derived: compound/longer patterns first
338
+ ['Foo::Bar', config[:constant_name]],
339
+ ['FOO_BAR', config[:screamcase_name]],
340
+ ['FooBar', config[:pascal_name]],
341
+ ['fooBar', config[:camel_name]],
342
+ ['Foo Bar', config[:title]],
343
+ ['foo/bar', config[:namespaced_path]],
344
+ ['foo-bar', config[:name]],
345
+ ['foo_bar', config[:underscored_name]],
346
+ ]
347
+ end
348
+
349
+ def binary_file?(path)
350
+ chunk = File.binread(path, 8192)
351
+ chunk.nil? || chunk.include?("\x00")
352
+ end
353
+
354
+ def filter_ignored_files!(repo_root, path_hash)
355
+ cmd = "git -C #{repo_root} check-ignore #{Shellwords.join(path_hash.keys)}"
356
+ stdout, _, status = Open3.capture3(cmd)
357
+ filter_these_paths = stdout.split
358
+
359
+ path_hash.delete_if { |key, _| filter_these_paths.include?(key) }
360
+ end
361
+
362
+ def create_template_directories(template_directories, target)
363
+ template_directories.each do |k,v|
364
+ d = "#{target}/#{v}"
365
+ puts " mkdir #{d} ..."
366
+ FileUtils.mkdir_p(d)
367
+ end
368
+ end
369
+
370
+ # returns the full path of the template source
371
+ def match_template_src
372
+ template_src = ::FoobarTemplates::TemplateManager.get_template_src(@options)
373
+
374
+ if File.exist?(template_src)
375
+ return template_src # 'newgem' refers to the built in template that comes with the gem
376
+ else
377
+ raise_template_not_found! # else message the user that the template could not be found
378
+ end
379
+ end
380
+
381
+ def resolve_name(name)
382
+ Pathname.pwd.join(name).basename.to_s
383
+ end
384
+
385
+
386
+
387
+ # Reads a template source file, performs literal string replacements
388
+ # of foo-bar variants and FOO_ prefixed placeholders, and writes
389
+ # the result to the destination.
390
+ def template(source, destination, _config = {})
391
+ source = File.expand_path(source.to_s)
392
+
393
+ if binary_file?(source)
394
+ FileUtils.mkdir_p(File.dirname(destination))
395
+ FileUtils.cp(source, destination)
396
+ else
397
+ content = File.read(source)
398
+ content = content.gsub(/>>>\s+(\S+)/) { $1.chars.join("\x00") }
399
+ build_content_replacement_pairs.each do |find, replace|
400
+ content = content.gsub(find, replace)
401
+ end
402
+ content = content.gsub("\x00", '')
403
+ make_file(destination, {}) { content }
404
+ end
405
+
406
+ original_mode = File.stat(source).mode
407
+ File.chmod(original_mode, destination)
408
+ end
409
+
410
+ def make_file(destination, config, &block)
411
+ FileUtils.mkdir_p(File.dirname(destination))
412
+ puts " Writing #{destination} ..."
413
+ File.open(destination, "wb") { |f| f.write block.call }
414
+ end
415
+
416
+ def raise_no_files_in_template_error!
417
+ err_no_files_in_template = <<-HEREDOC
418
+ Ooops, the template was found for '#{@options[:template]}' in ~/.foobar/templates,
419
+ but no files were found within it.
420
+
421
+ Exiting...
422
+ HEREDOC
423
+ puts err_no_files_in_template
424
+ raise
425
+ end
426
+
427
+ def raise_project_with_that_name_already_exists!
428
+ err_project_with_that_name_exists = <<-HEREDOC
429
+ Ooops, a project with the name #{target} already exists.
430
+ Can't make project. Either delete that folder or choose a new project name
431
+
432
+ Exiting...
433
+ HEREDOC
434
+ puts err_project_with_that_name_exists
435
+ raise
436
+ end
437
+
438
+ def raise_template_not_found!
439
+ err_missing_template = "Could not find template folder '#{@options[:template]}' in `~/.foobar/templates/`. Please check to make sure your desired template exists."
440
+ $stderr.puts err_missing_template
441
+ raise
442
+ end
443
+
444
+ def time_it(label = nil)
445
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
446
+ yield
447
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
448
+ elapsed_ms = ((end_time - start_time) * 1000).round(2)
449
+ puts "#{label || 'Elapsed'}: #{elapsed_ms} ms"
450
+ end
451
+
452
+ # This checks to see that the gem_name is a valid ruby gem name and will 'work'
453
+ # and won't overlap with a foobar_templates constant apparently...
454
+ def ensure_safe_project_name(name, constant_array)
455
+ if name =~ /^\d/
456
+ $stderr.puts "Invalid gem name #{name} Please give a name which does not start with numbers."
457
+ raise
458
+ end
459
+ end
460
+
461
+ end
462
+ end
@@ -0,0 +1,99 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module FoobarTemplates
5
+
6
+ class Configurator
7
+ attr_accessor :config_file_data
8
+
9
+ def initialize
10
+ @config_directory_root = "#{ENV['HOME']}/.foobar"
11
+ @config_file = "#{@config_directory_root}/config"
12
+ @user_defined_templates_path = "#{@config_directory_root}/templates"
13
+
14
+ create_config_file_if_needed!
15
+
16
+ # load configurations from config file
17
+ @config_file_data = YAML.load_file @config_file
18
+ end
19
+
20
+ def default_template
21
+ @config_file_data["default_template"]
22
+ end
23
+
24
+ def default_template=(val)
25
+ @config_file_data["default_template"] = val
26
+ save_config!
27
+ end
28
+
29
+ def always_perform_git_init
30
+ @config_file_data.fetch("always_perform_git_init", false)
31
+ end
32
+
33
+ def domain(key)
34
+ @config_file_data[key.to_s]
35
+ end
36
+
37
+ def set_domain(key, value)
38
+ @config_file_data[key.to_s] = value
39
+ save_config!
40
+ end
41
+
42
+ def collect_user_defined_templates
43
+ immediate_subdirectories(@user_defined_templates_path).flat_map do |template_dir|
44
+ collect_templates_from_path(template_dir)
45
+ end
46
+ end
47
+
48
+ def create_config_file_if_needed!
49
+ FileUtils.mkdir_p @user_defined_templates_path
50
+ FileUtils.cp("#{SOURCE_ROOT}/config/config", @config_file) unless File.exist? @config_file
51
+ end
52
+
53
+ private
54
+
55
+ def collect_templates_from_path(template_dir)
56
+ config = read_template_config(template_dir)
57
+
58
+ if monorepo_template?(config)
59
+ immediate_subdirectories(template_dir).flat_map do |child_dir|
60
+ collect_templates_from_path(child_dir)
61
+ end
62
+ else
63
+ category = config[:category] || "misc"
64
+ [{ category => File.basename(template_dir).sub(/^template-/, "") }]
65
+ end
66
+ end
67
+
68
+ def monorepo_template?(config)
69
+ config[:monorepo] == true
70
+ end
71
+
72
+ def read_template_config(template_dir)
73
+ template_config_path = File.join(template_dir, "foobar.yml")
74
+ return {} unless File.exist?(template_config_path)
75
+
76
+ raw_config = YAML.load_file(template_config_path, symbolize_names: true)
77
+ raw_config.is_a?(Hash) ? raw_config : {}
78
+ rescue StandardError
79
+ {}
80
+ end
81
+
82
+ def immediate_subdirectories(dir)
83
+ Dir.children(dir).filter_map do |child_dir|
84
+ # Skip hidden files and dirs like .ds_store, etc.
85
+ if child_dir.start_with?(".")
86
+ nil
87
+ else
88
+ dir_path = File.join(dir, child_dir)
89
+ File.directory?(dir_path) ? dir_path : nil
90
+ end
91
+ end
92
+ end
93
+
94
+ def save_config!
95
+ File.write(@config_file, "# Comments made to this file will not be preserved\n#{YAML.dump(@config_file_data)}")
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,9 @@
1
+ # This is the core logic for the app. CLI stuff should eventually get pulled
2
+ # out so the app is neat and the way I do CLI stuff lately.
3
+ module FoobarTemplates::Core
4
+ end
5
+
6
+ # I saw someone on the internet do requires this way so am giving it a spin :)
7
+ # It's a little odd, right? I'm not sure I like it yet but lets me go to
8
+ # town on namespaces and `class << self` definitions if I want...
9
+ require 'foobar_templates/core/dir_to_template'
@@ -0,0 +1,114 @@
1
+ require 'find'
2
+ require 'open3'
3
+ require 'shellwords'
4
+
5
+
6
+ module FoobarTemplates::Core::DirToTemplate
7
+ class << self
8
+
9
+ # Takes in a file_enumerator such as Find.find('.') and
10
+ # performs literal string replacements of project name variants
11
+ # with foo-bar template placeholders
12
+ def 🧙🪄! file_enumerator, template_name: "asdf-pkg", dry_run: false
13
+ # FOO_* composite values contain the project name (e.g. URLs, paths), so
14
+ # they must be substituted BEFORE the name-variant replacements rewrite
15
+ # `good-dog` → `foo-bar` inside them.
16
+ replacements = build_foo_var_replacement_pairs(template_name) + build_replacement_pairs(template_name)
17
+ files_changed = []
18
+ file_enumerator.each do |path|
19
+ next if should_skip?(path)
20
+
21
+ conduct_pkg_name_to_template_variable_replacements!(path, replacements) unless dry_run
22
+
23
+ files_changed << "Processed: #{path}"
24
+ end
25
+ files_changed
26
+ end
27
+
28
+ private
29
+
30
+ def build_replacement_pairs(template_name)
31
+ underscored = template_name.tr('-', '_')
32
+ screamcase = underscored.upcase
33
+ pascal = underscored.split('_').map(&:capitalize).join
34
+ camel = pascal.sub(/^./, &:downcase)
35
+
36
+ # Order: longer/more-specific patterns first to avoid partial matches
37
+ [
38
+ [screamcase, 'FOO_BAR'],
39
+ [pascal, 'FooBar'],
40
+ [camel, 'fooBar'],
41
+ [template_name, 'foo-bar'],
42
+ [underscored, 'foo_bar'],
43
+ ]
44
+ end
45
+
46
+ # Reverse-substitute the user's real values for the FOO_* placeholders that
47
+ # `FoobarTemplates::CLI::TemplateGenerator#build_interpolation_config` would have
48
+ # interpolated. Keep this list in sync with that method.
49
+ #
50
+ # Ordered longest/most-specific first so composite values (URLs, paths)
51
+ # are consumed before their substrings (bare domain, author).
52
+ def build_foo_var_replacement_pairs(template_name)
53
+ author = `git config user.name`.chomp
54
+ email = `git config user.email`.chomp
55
+
56
+ configurator = FoobarTemplates::Configurator.new
57
+ repo_domain = configurator.domain('repo_domain')
58
+ registry_domain = configurator.domain('registry_domain')
59
+ k8s_domain = configurator.domain('k8s_domain')
60
+
61
+ pairs = []
62
+
63
+ if repo_domain && !repo_domain.empty? && author && !author.empty?
64
+ git_repo_url = "https://#{repo_domain}/#{author}/#{template_name}"
65
+ git_repo_path = "#{repo_domain}/#{author}/#{template_name}".downcase
66
+ pairs << [git_repo_url, 'FOO_GIT_REPO_URL']
67
+ pairs << [git_repo_path, 'FOO_GIT_REPO_PATH']
68
+ end
69
+
70
+ if author && !author.empty?
71
+ image_path = "#{author}/#{template_name}".downcase
72
+ if registry_domain && !registry_domain.empty?
73
+ registry_repo_path = "#{registry_domain}/#{image_path}".downcase
74
+ pairs << [registry_repo_path, 'FOO_REGISTRY_REPO_PATH']
75
+ end
76
+ pairs << [image_path, 'FOO_IMAGE_PATH']
77
+ end
78
+
79
+ pairs << [registry_domain, 'FOO_REGISTRY_DOMAIN'] if registry_domain && !registry_domain.empty?
80
+ pairs << [k8s_domain, 'FOO_K8S_DOMAIN'] if k8s_domain && !k8s_domain.empty?
81
+ pairs << [repo_domain, 'FOO_GIT_REPO_DOMAIN'] if repo_domain && !repo_domain.empty?
82
+ pairs << [email, 'FOO_EMAIL'] if email && !email.empty?
83
+ pairs << [author, 'FOO_AUTHOR'] if author && !author.empty?
84
+
85
+ pairs
86
+ end
87
+
88
+ def conduct_pkg_name_to_template_variable_replacements!(path, replacements)
89
+ content = File.read(path)
90
+ original = content.dup
91
+
92
+ replacements.each do |find, replace|
93
+ content.gsub!(find, replace)
94
+ end
95
+
96
+ File.write(path, content) if content != original
97
+ end
98
+
99
+ def should_skip?(path)
100
+ !File.file?(path) || # skip directories
101
+ path.start_with?('./.git/') || # skip the .git directory
102
+ path == './.gitignore' || # skip .gitignore (must remain for git to work)
103
+ ignored_by_git?(path) || # skip things that are gitignored
104
+ path == "./foobar.yml" # skip the foobar.yml file
105
+ end
106
+
107
+ def ignored_by_git?(path)
108
+ stdout, _, status = Open3.capture3("git check-ignore #{Shellwords.escape(path)}")
109
+ return false unless status.exitstatus == 0 || status.exitstatus == 1
110
+ status.success? && !stdout.strip.empty?
111
+ end
112
+
113
+ end
114
+ end