kettle-dev 1.2.3 → 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 (133) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +176 -3
  4. data/CITATION.cff +2 -2
  5. data/CONTRIBUTING.md +11 -17
  6. data/README.md +390 -319
  7. data/exe/kettle-dev-setup +12 -63
  8. data/exe/kettle-gh-release +82 -0
  9. data/lib/kettle/dev/gem_spec_reader.rb +2 -2
  10. data/lib/kettle/dev/open_collective_config.rb +12 -0
  11. data/lib/kettle/dev/rakelib/yard.rake +15 -0
  12. data/lib/kettle/dev/tasks/ci_task.rb +4 -4
  13. data/lib/kettle/dev/version.rb +1 -1
  14. data/lib/kettle/dev.rb +4 -12
  15. data/sig/kettle/dev/source_merger.rbs +40 -56
  16. data.tar.gz.sig +0 -0
  17. metadata +15 -144
  18. metadata.gz.sig +0 -0
  19. data/.aiignore.example +0 -19
  20. data/.devcontainer/apt-install/devcontainer-feature.json +0 -9
  21. data/.devcontainer/apt-install/install.sh +0 -11
  22. data/.devcontainer/devcontainer.json +0 -28
  23. data/.env.local.example +0 -31
  24. data/.envrc +0 -47
  25. data/.envrc.example +0 -51
  26. data/.envrc.no-osc.example +0 -51
  27. data/.git-hooks/commit-msg +0 -54
  28. data/.git-hooks/commit-subjects-goalie.txt +0 -8
  29. data/.git-hooks/footer-template.erb.txt +0 -16
  30. data/.git-hooks/prepare-commit-msg +0 -8
  31. data/.github/.codecov.yml.example +0 -14
  32. data/.github/FUNDING.yml +0 -13
  33. data/.github/FUNDING.yml.no-osc.example +0 -13
  34. data/.github/dependabot.yml +0 -13
  35. data/.github/workflows/ancient.yml +0 -83
  36. data/.github/workflows/ancient.yml.example +0 -81
  37. data/.github/workflows/auto-assign.yml +0 -21
  38. data/.github/workflows/codeql-analysis.yml +0 -70
  39. data/.github/workflows/coverage.yml +0 -127
  40. data/.github/workflows/coverage.yml.example +0 -127
  41. data/.github/workflows/current.yml +0 -116
  42. data/.github/workflows/current.yml.example +0 -115
  43. data/.github/workflows/dep-heads.yml +0 -117
  44. data/.github/workflows/dependency-review.yml +0 -20
  45. data/.github/workflows/discord-notifier.yml.example +0 -39
  46. data/.github/workflows/heads.yml +0 -117
  47. data/.github/workflows/heads.yml.example +0 -116
  48. data/.github/workflows/jruby.yml +0 -82
  49. data/.github/workflows/jruby.yml.example +0 -72
  50. data/.github/workflows/legacy.yml +0 -76
  51. data/.github/workflows/license-eye.yml +0 -40
  52. data/.github/workflows/locked_deps.yml +0 -85
  53. data/.github/workflows/opencollective.yml +0 -40
  54. data/.github/workflows/style.yml +0 -67
  55. data/.github/workflows/supported.yml +0 -75
  56. data/.github/workflows/truffle.yml +0 -99
  57. data/.github/workflows/unlocked_deps.yml +0 -84
  58. data/.github/workflows/unsupported.yml +0 -76
  59. data/.gitignore +0 -50
  60. data/.gitlab-ci.yml.example +0 -134
  61. data/.idea/.gitignore +0 -45
  62. data/.junie/guidelines-rbs.md +0 -49
  63. data/.junie/guidelines.md +0 -141
  64. data/.junie/guidelines.md.example +0 -140
  65. data/.licenserc.yaml +0 -7
  66. data/.opencollective.yml +0 -3
  67. data/.opencollective.yml.example +0 -3
  68. data/.qlty/qlty.toml +0 -79
  69. data/.rspec +0 -9
  70. data/.rubocop.yml +0 -13
  71. data/.rubocop_rspec.yml +0 -33
  72. data/.simplecov +0 -16
  73. data/.simplecov.example +0 -11
  74. data/.tool-versions +0 -1
  75. data/.yardignore +0 -13
  76. data/.yardopts +0 -14
  77. data/Appraisal.root.gemfile +0 -10
  78. data/Appraisals +0 -151
  79. data/Appraisals.example +0 -102
  80. data/CHANGELOG.md.example +0 -47
  81. data/CONTRIBUTING.md.example +0 -227
  82. data/FUNDING.md.no-osc.example +0 -63
  83. data/Gemfile +0 -40
  84. data/Gemfile.example +0 -34
  85. data/README.md.example +0 -570
  86. data/README.md.no-osc.example +0 -536
  87. data/Rakefile.example +0 -68
  88. data/gemfiles/modular/coverage.gemfile +0 -6
  89. data/gemfiles/modular/debug.gemfile +0 -13
  90. data/gemfiles/modular/documentation.gemfile +0 -14
  91. data/gemfiles/modular/erb/r2/v3.0.gemfile +0 -1
  92. data/gemfiles/modular/erb/r2.3/default.gemfile +0 -6
  93. data/gemfiles/modular/erb/r2.6/v2.2.gemfile +0 -3
  94. data/gemfiles/modular/erb/r3/v5.0.gemfile +0 -1
  95. data/gemfiles/modular/erb/r3.1/v4.0.gemfile +0 -2
  96. data/gemfiles/modular/erb/vHEAD.gemfile +0 -2
  97. data/gemfiles/modular/injected.gemfile +0 -60
  98. data/gemfiles/modular/mutex_m/r2/v0.3.gemfile +0 -2
  99. data/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile +0 -3
  100. data/gemfiles/modular/mutex_m/r3/v0.3.gemfile +0 -2
  101. data/gemfiles/modular/mutex_m/vHEAD.gemfile +0 -2
  102. data/gemfiles/modular/optional.gemfile +0 -8
  103. data/gemfiles/modular/optional.gemfile.example +0 -5
  104. data/gemfiles/modular/runtime_heads.gemfile +0 -10
  105. data/gemfiles/modular/runtime_heads.gemfile.example +0 -8
  106. data/gemfiles/modular/stringio/r2/v3.0.gemfile +0 -5
  107. data/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile +0 -4
  108. data/gemfiles/modular/stringio/r3/v3.0.gemfile +0 -5
  109. data/gemfiles/modular/stringio/vHEAD.gemfile +0 -2
  110. data/gemfiles/modular/style.gemfile +0 -25
  111. data/gemfiles/modular/style.gemfile.example +0 -25
  112. data/gemfiles/modular/templating.gemfile +0 -3
  113. data/gemfiles/modular/x_std_libs/r2/libs.gemfile +0 -3
  114. data/gemfiles/modular/x_std_libs/r2.3/libs.gemfile +0 -3
  115. data/gemfiles/modular/x_std_libs/r2.4/libs.gemfile +0 -3
  116. data/gemfiles/modular/x_std_libs/r2.6/libs.gemfile +0 -3
  117. data/gemfiles/modular/x_std_libs/r3/libs.gemfile +0 -3
  118. data/gemfiles/modular/x_std_libs/r3.1/libs.gemfile +0 -3
  119. data/gemfiles/modular/x_std_libs/vHEAD.gemfile +0 -3
  120. data/gemfiles/modular/x_std_libs.gemfile +0 -2
  121. data/kettle-dev.gemspec.example +0 -154
  122. data/lib/kettle/dev/modular_gemfiles.rb +0 -119
  123. data/lib/kettle/dev/prism_appraisals.rb +0 -351
  124. data/lib/kettle/dev/prism_gemfile.rb +0 -177
  125. data/lib/kettle/dev/prism_gemspec.rb +0 -284
  126. data/lib/kettle/dev/prism_utils.rb +0 -201
  127. data/lib/kettle/dev/rakelib/install.rake +0 -10
  128. data/lib/kettle/dev/rakelib/template.rake +0 -10
  129. data/lib/kettle/dev/setup_cli.rb +0 -403
  130. data/lib/kettle/dev/source_merger.rb +0 -622
  131. data/lib/kettle/dev/tasks/install_task.rb +0 -553
  132. data/lib/kettle/dev/tasks/template_task.rb +0 -975
  133. data/lib/kettle/dev/template_helpers.rb +0 -685
@@ -1,403 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "shellwords"
5
- require "open3"
6
- require "optparse"
7
-
8
- module Kettle
9
- module Dev
10
- # SetupCLI bootstraps a host gem repository to use kettle-dev tooling.
11
- # It performs prechecks, syncs development dependencies, ensures bin/setup and
12
- # Rakefile templates, runs setup tasks, and invokes kettle:dev:install.
13
- #
14
- # Usage:
15
- # Kettle::Dev::SetupCLI.new(ARGV).run!
16
- #
17
- # Options are parsed from argv and passed through to the rake task as
18
- # key=value pairs (e.g., --force => force=true).
19
- class SetupCLI
20
- # @param argv [Array<String>] CLI arguments
21
- def initialize(argv)
22
- @argv = argv
23
- @passthrough = []
24
- @options = {}
25
- parse!
26
- end
27
-
28
- # Execute the full setup workflow.
29
- # @return [void]
30
- def run!
31
- say("Starting kettle-dev setup…")
32
- prechecks!
33
- ensure_dev_deps!
34
- ensure_gemfile_from_example!
35
- ensure_modular_gemfiles!
36
- ensure_bin_setup!
37
- ensure_rakefile!
38
- run_bin_setup!
39
- run_bundle_binstubs!
40
- commit_bootstrap_changes!
41
- run_kettle_install!
42
- say("kettle-dev setup complete.")
43
- end
44
-
45
- private
46
-
47
- def debug(msg)
48
- return if ENV.fetch("DEBUG", "false").casecmp("true").nonzero?
49
-
50
- $stderr.puts("[kettle-dev-setup] DEBUG: #{msg}")
51
- end
52
-
53
- # Attempt to derive a funding organization from the git remote 'origin' when
54
- # not explicitly provided via env or .opencollective.yml.
55
- # This is a soft helper that only sets ENV["FUNDING_ORG"] if a plausible
56
- # GitHub org can be parsed from the origin URL.
57
- # @return [void]
58
- def derive_funding_org_from_git_if_missing!
59
- # Respect explicit bypass
60
- env_val = ENV["FUNDING_ORG"]
61
- return if env_val && env_val.to_s.strip.casecmp("false").zero?
62
-
63
- # If already provided via env, do nothing
64
- return if ENV["FUNDING_ORG"].to_s.strip != ""
65
- return if ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip != ""
66
-
67
- # If project provides an .opencollective.yml with org, do nothing
68
- begin
69
- oc_path = File.join(Dir.pwd, ".opencollective.yml")
70
- if File.file?(oc_path)
71
- txt = File.read(oc_path)
72
- return if txt =~ /\borg:\s*([\w\-]+)/i
73
- end
74
- rescue StandardError => e
75
- debug("Reading .opencollective.yml failed: #{e.class}: #{e.message}")
76
- end
77
-
78
- # Attempt to get origin URL and parse GitHub org
79
- begin
80
- ga = Kettle::Dev::GitAdapter.new
81
- origin_url = nil
82
- origin_url = ga.remote_url("origin") if ga.respond_to?(:remote_url)
83
- if origin_url.nil? && ga.respond_to?(:remotes_with_urls)
84
- begin
85
- urls = ga.remotes_with_urls
86
- origin_url = urls["origin"] if urls
87
- rescue StandardError => e
88
- # graceful fallback if adapter backend errs; keep silent behavior
89
- debug("remotes_with_urls failed: #{e.class}: #{e.message}")
90
- end
91
- end
92
- origin_url = origin_url.to_s.strip
93
- if (m = origin_url.match(%r{github\.com[/:]([^/]+)/}i))
94
- org = m[1].to_s
95
- if !org.empty?
96
- ENV["FUNDING_ORG"] = org
97
- debug("Derived FUNDING_ORG from git origin: #{org}")
98
- end
99
- end
100
- rescue StandardError => e
101
- # Be silent; this is a best-effort and shouldn't fail setup
102
- debug("Could not derive funding org from git: #{e.class}: #{e.message}")
103
- end
104
- end
105
-
106
- def parse!
107
- parser = OptionParser.new do |opts|
108
- opts.banner = "Usage: kettle-dev-setup [options]"
109
- opts.on("--allowed=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "allowed=#{v}" }
110
- opts.on("--force", "Pass through to kettle:dev:install") do
111
- # Ensure in-process helpers (TemplateHelpers.ask) also see force mode
112
- ENV["force"] = "true"
113
- @passthrough << "force=true"
114
- end
115
- opts.on("--hook_templates=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "hook_templates=#{v}" }
116
- opts.on("--only=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "only=#{v}" }
117
- opts.on("--include=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "include=#{v}" }
118
- opts.on("-h", "--help", "Show help") do
119
- puts opts
120
- Kettle::Dev::ExitAdapter.exit(0)
121
- end
122
- end
123
- begin
124
- parser.parse!(@argv)
125
- rescue OptionParser::ParseError => e
126
- warn("[kettle-dev-setup] #{e.class}: #{e.message}")
127
- puts parser
128
- Kettle::Dev::ExitAdapter.exit(2)
129
- end
130
- @passthrough.concat(@argv)
131
- end
132
-
133
- def say(msg)
134
- puts "[kettle-dev-setup] #{msg}"
135
- end
136
-
137
- def abort!(msg)
138
- Kettle::Dev::ExitAdapter.abort("[kettle-dev-setup] ERROR: #{msg}")
139
- end
140
-
141
- def sh!(cmd, env: {})
142
- say("exec: #{cmd}")
143
- stdout_str, stderr_str, status = Open3.capture3(env, cmd)
144
- $stdout.print(stdout_str) unless stdout_str.empty?
145
- $stderr.print(stderr_str) unless stderr_str.empty?
146
- abort!("Command failed: #{cmd}") unless status.success?
147
- end
148
-
149
- # 1. Prechecks
150
- def prechecks!
151
- abort!("Not inside a git repository (missing .git).") unless Dir.exist?(".git")
152
-
153
- # Ensure clean working tree
154
- begin
155
- if defined?(Kettle::Dev::GitAdapter)
156
- dirty = !Kettle::Dev::GitAdapter.new.clean?
157
- else
158
- stdout, _stderr, _status = Open3.capture3("git status --porcelain")
159
- dirty = !stdout.strip.empty?
160
- end
161
- abort!("Git working tree is not clean. Please commit/stash changes and try again.") if dirty
162
- rescue StandardError
163
- stdout, _stderr, _status = Open3.capture3("git status --porcelain")
164
- abort!("Git working tree is not clean. Please commit/stash changes and try again.") unless stdout.strip.empty?
165
- end
166
-
167
- # gemspec
168
- gemspecs = Dir["*.gemspec"]
169
- abort!("No gemspec found in current directory.") if gemspecs.empty?
170
- @gemspec_path = gemspecs.first
171
-
172
- # Gemfile
173
- abort!("No Gemfile found; bundler is required.") unless File.exist?("Gemfile")
174
-
175
- # Seed FUNDING_ORG from git remote origin org when not provided elsewhere
176
- derive_funding_org_from_git_if_missing!
177
- end
178
-
179
- # 3. Sync dev dependencies from this gem's example gemspec into target gemspec
180
- def ensure_dev_deps!
181
- source_example = installed_path("kettle-dev.gemspec.example")
182
- abort!("Internal error: kettle-dev.gemspec.example not found within the installed gem.") unless source_example && File.exist?(source_example)
183
-
184
- example = File.read(source_example)
185
- example = example.gsub("{KETTLE|DEV|GEM}", "kettle-dev")
186
-
187
- wanted_lines = example.each_line.map(&:rstrip).select { |line| line =~ /add_development_dependency\s*\(?/ }
188
- return if wanted_lines.empty?
189
-
190
- target = File.read(@gemspec_path)
191
-
192
- # Build gem=>desired line map
193
- wanted = {}
194
- wanted_lines.each do |line|
195
- if (m = line.match(/add_development_dependency\s*\(?\s*["']([^"']+)["']/))
196
- wanted[m[1]] = line
197
- end
198
- end
199
-
200
- # Use Prism-based gemspec edit to ensure development dependencies match
201
- begin
202
- modified = Kettle::Dev::PrismGemspec.ensure_development_dependencies(target, wanted)
203
- # Check if any actual changes were made to development dependency declarations.
204
- # Extract dependency lines from both and compare sets to avoid false positives
205
- # from whitespace/formatting differences.
206
- extract_deps = lambda do |content|
207
- content.to_s.lines.select { |ln| ln =~ /add_development_dependency\s*\(?/ }.map(&:strip).sort
208
- end
209
- target_deps = extract_deps.call(target)
210
- modified_deps = extract_deps.call(modified)
211
- if modified_deps != target_deps
212
- File.write(@gemspec_path, modified)
213
- say("Updated development dependencies in #{@gemspec_path}.")
214
- else
215
- say("Development dependencies already up to date.")
216
- end
217
- rescue StandardError => e
218
- Kettle::Dev.debug_error(e, __method__)
219
- # Fall back to previous behavior: write nothing and report up-to-date
220
- say("Development dependencies already up to date.")
221
- end
222
- end
223
-
224
- # 4. Ensure bin/setup present (copy from gem if missing)
225
- def ensure_bin_setup!
226
- target = File.join("bin", "setup")
227
- return say("bin/setup present.") if File.exist?(target)
228
-
229
- source = installed_path(File.join("bin", "setup"))
230
- abort!("Internal error: source bin/setup not found within installed gem.") unless source && File.exist?(source)
231
- FileUtils.mkdir_p("bin")
232
- FileUtils.cp(source, target)
233
- FileUtils.chmod("+x", target)
234
- say("Copied bin/setup.")
235
- end
236
-
237
- # 3b. Ensure Gemfile contains required lines from example without duplicating directives
238
- # - Copies source, git_source, gemspec, and eval_gemfile lines that are missing
239
- # - Idempotent (running multiple times does not duplicate entries)
240
- def ensure_gemfile_from_example!
241
- source_path = installed_path("Gemfile.example")
242
- abort!("Internal error: Gemfile.example not found within installed gem.") unless source_path && File.exist?(source_path)
243
-
244
- example = File.read(source_path)
245
- target_path = "Gemfile"
246
- target = File.exist?(target_path) ? File.read(target_path) : ""
247
-
248
- # Extract interesting lines from example
249
- ex_sources = []
250
- ex_git_sources = [] # names (e.g., :github)
251
- ex_git_source_lines = {}
252
- ex_has_gemspec = false
253
- ex_eval_paths = []
254
-
255
- example.each_line do |ln|
256
- s = ln.strip
257
- next if s.empty?
258
-
259
- if s.start_with?("source ")
260
- ex_sources << ln.rstrip
261
- elsif (m = s.match(/^git_source\(\s*:(\w+)\s*\)/))
262
- name = m[1]
263
- ex_git_sources << name
264
- ex_git_source_lines[name] = ln.rstrip
265
- elsif s.start_with?("gemspec")
266
- ex_has_gemspec = true
267
- elsif (m = s.match(/^eval_gemfile\s+["']([^"']+)["']/))
268
- ex_eval_paths << m[1]
269
- end
270
- end
271
-
272
- # Scan target for presence
273
- tg_sources = target.each_line.map(&:rstrip).select { |l| l.strip.start_with?("source ") }
274
- tg_git_sources = {}
275
- target.each_line do |ln|
276
- if (m = ln.strip.match(/^git_source\(\s*:(\w+)\s*\)/))
277
- tg_git_sources[m[1]] = true
278
- end
279
- end
280
- tg_has_gemspec = !!target.each_line.find { |l| l.strip.start_with?("gemspec") }
281
- tg_eval_paths = target.each_line.map do |ln|
282
- if (m = ln.strip.match(/^eval_gemfile\s+["']([^"']+)["']/))
283
- m[1]
284
- end
285
- end.compact
286
-
287
- additions = []
288
- # Add missing sources (exact line match)
289
- ex_sources.each do |src_line|
290
- additions << src_line unless tg_sources.include?(src_line)
291
- end
292
- # Add missing git_source by name
293
- ex_git_sources.each do |name|
294
- additions << ex_git_source_lines[name] unless tg_git_sources[name]
295
- end
296
- # Add gemspec if example has it and target lacks it
297
- additions << "gemspec" if ex_has_gemspec && !tg_has_gemspec
298
- # Add missing eval_gemfile paths (recreate the exact example line when possible)
299
- ex_eval_paths.each do |path|
300
- next if tg_eval_paths.include?(path)
301
-
302
- additions << "eval_gemfile \"#{path}\""
303
- end
304
-
305
- return say("Gemfile already contains required entries from example.") if additions.empty?
306
-
307
- # Ensure file ends with a newline
308
- target << "\n" unless target.end_with?("\n") || target.empty?
309
- new_content = target + additions.join("\n") + "\n"
310
- File.write(target_path, new_content)
311
- say("Updated Gemfile with entries from Gemfile.example (added #{additions.size}).")
312
- end
313
-
314
- # 3c. Ensure gemfiles/modular/* are present (copied like template task)
315
- def ensure_modular_gemfiles!
316
- helpers = Kettle::Dev::TemplateHelpers
317
- project_root = helpers.project_root
318
- gem_checkout_root = helpers.gem_checkout_root
319
- # Gather min_ruby for style.gemfile adjustments
320
- min_ruby = begin
321
- md = helpers.gemspec_metadata(project_root)
322
- md[:min_ruby]
323
- rescue StandardError
324
- nil
325
- end
326
- Kettle::Dev::ModularGemfiles.sync!(
327
- helpers: helpers,
328
- project_root: project_root,
329
- gem_checkout_root: gem_checkout_root,
330
- min_ruby: min_ruby,
331
- )
332
- end
333
-
334
- # 5. Ensure Rakefile matches example (replace or create)
335
- def ensure_rakefile!
336
- source = installed_path("Rakefile.example")
337
- abort!("Internal error: Rakefile.example not found within installed gem.") unless source && File.exist?(source)
338
-
339
- content = File.read(source)
340
- if File.exist?("Rakefile")
341
- say("Replacing existing Rakefile with kettle-dev Rakefile.example.")
342
- else
343
- say("Creating Rakefile from kettle-dev Rakefile.example.")
344
- end
345
- File.write("Rakefile", content)
346
- end
347
-
348
- # 6. Run bin/setup
349
- def run_bin_setup!
350
- sh!(Shellwords.join([File.join("bin", "setup")]))
351
- end
352
-
353
- # 7. Run bundle binstubs --all
354
- def run_bundle_binstubs!
355
- sh!("bundle exec bundle binstubs --all")
356
- end
357
-
358
- # 8. Commit template bootstrap changes if any
359
- def commit_bootstrap_changes!
360
- dirty = begin
361
- if defined?(Kettle::Dev::GitAdapter)
362
- !Kettle::Dev::GitAdapter.new.clean?
363
- else
364
- out, _st = Open3.capture2("git", "status", "--porcelain")
365
- !out.strip.empty?
366
- end
367
- rescue StandardError
368
- out, _st = Open3.capture2("git", "status", "--porcelain")
369
- !out.strip.empty?
370
- end
371
- unless dirty
372
- say("No changes to commit from template bootstrap.")
373
- return
374
- end
375
- sh!(Shellwords.join(["git", "add", "-A"]))
376
- msg = "🎨 Template bootstrap by kettle-dev-setup v#{Kettle::Dev::Version::VERSION}"
377
- sh!(Shellwords.join(["git", "commit", "-m", msg]))
378
- say("Committed template bootstrap changes.")
379
- end
380
-
381
- # 9. Invoke rake install task with passthrough
382
- def run_kettle_install!
383
- cmd = ["bin/rake", "kettle:dev:install"] + @passthrough
384
- sh!(Shellwords.join(cmd))
385
- end
386
-
387
- # Resolve a path to files shipped within the gem or repo checkout
388
- # @param rel [String]
389
- # @return [String, nil]
390
- def installed_path(rel)
391
- if defined?(Gem) && (spec = Gem.loaded_specs["kettle-dev"])
392
- path = File.join(spec.full_gem_path, rel)
393
- return path if File.exist?(path)
394
- end
395
- here = File.expand_path(File.join(__dir__, "..", "..", "..")) # lib/kettle/dev/ -> project root
396
- path = File.join(here, rel)
397
- return path if File.exist?(path)
398
-
399
- nil
400
- end
401
- end
402
- end
403
- end