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,553 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kettle
4
- module Dev
5
- module Tasks
6
- module InstallTask
7
- module_function
8
-
9
- # Abort wrapper that avoids terminating the current rake task process.
10
- # Always raise Kettle::Dev::Error so callers can decide whether to handle
11
- # it without terminating the process (e.g., in tests or non-interactive runs).
12
- def task_abort(msg)
13
- raise Kettle::Dev::Error, msg
14
- end
15
-
16
- def run
17
- helpers = Kettle::Dev::TemplateHelpers
18
- project_root = helpers.project_root
19
-
20
- # Run file templating via dedicated task first
21
- Rake::Task["kettle:dev:template"].invoke
22
-
23
- # .tool-versions cleanup offers
24
- tool_versions_path = File.join(project_root, ".tool-versions")
25
- if File.file?(tool_versions_path)
26
- rv = File.join(project_root, ".ruby-version")
27
- rg = File.join(project_root, ".ruby-gemset")
28
- to_remove = [rv, rg].select { |p| File.exist?(p) }
29
- unless to_remove.empty?
30
- if helpers.ask("Remove #{to_remove.map { |p| File.basename(p) }.join(" and ")} (managed by .tool-versions)?", true)
31
- to_remove.each { |p| FileUtils.rm_f(p) }
32
- puts "Removed #{to_remove.map { |p| File.basename(p) }.join(" and ")}"
33
- end
34
- end
35
- end
36
-
37
- # Trim MRI Ruby version badges in README.md to >= required_ruby_version from gemspec
38
- begin
39
- readme_path = File.join(project_root, "README.md")
40
- if File.file?(readme_path)
41
- md = helpers.gemspec_metadata(project_root)
42
- min_ruby = md[:min_ruby] # an instance of Gem::Version
43
- if min_ruby
44
- content = File.read(readme_path)
45
-
46
- # Detect all MRI ruby badge labels present
47
- removed_labels = []
48
-
49
- content.scan(/\[(?<label>💎ruby-(?<ver>\d+\.\d+)i)\]/) do |arr|
50
- label, ver_s = arr
51
- begin
52
- ver = Gem::Version.new(ver_s)
53
- if ver < min_ruby
54
- # Remove occurrences of badges using this label
55
- label_re = Regexp.escape(label)
56
- # Linked form: [![...][label]][...]
57
- content = content.gsub(/\[!\[[^\]]*?\]\s*\[#{label_re}\]\s*\]\s*\[[^\]]+\]/, "")
58
- # Unlinked form: ![...][label]
59
- content = content.gsub(/!\[[^\]]*?\]\s*\[#{label_re}\]/, "")
60
- removed_labels << label
61
- end
62
- rescue StandardError => e
63
- Kettle::Dev.debug_error(e, __method__)
64
- # ignore
65
- end
66
- end
67
-
68
- # Fix leading <br/> in MRI rows and remove rows that end up empty. Also normalize leading whitespace in badge cell to a single space.
69
- content = content.lines.map { |ln|
70
- if ln.start_with?("| Works with MRI Ruby")
71
- cells = ln.split("|", -1)
72
- # cells[0] is empty (leading |), cells[1] = label cell, cells[2] = badges cell
73
- badge_cell = cells[2] || ""
74
- # If badge cell is only a <br/> (possibly with whitespace), treat as empty (row will be removed later)
75
- if badge_cell.strip == "<br/>"
76
- cells[2] = " "
77
- cells.join("|")
78
- elsif badge_cell =~ /\A\s*<br\/>/i
79
- # If badge cell starts with <br/> and there are no badges before it, strip the leading <br/>
80
- # We consider "no badges before" as any leading whitespace followed immediately by <br/>
81
- cleaned = badge_cell.sub(/\A\s*<br\/>\s*/i, "")
82
- cells[2] = " #{cleaned}" # prefix with a single space
83
- cells.join("|")
84
- elsif badge_cell =~ /\A[ \t]{2,}\S/
85
- # Collapse multiple leading spaces/tabs to exactly one
86
- cells[2] = " " + badge_cell.lstrip
87
- cells.join("|")
88
- elsif badge_cell =~ /\A[ \t]+\S/
89
- # If there is any leading whitespace at all, normalize it to exactly one space
90
- cells[2] = " " + badge_cell.lstrip
91
- cells.join("|")
92
- else
93
- ln
94
- end
95
- else
96
- ln
97
- end
98
- }.reject { |ln|
99
- if ln.start_with?("| Works with MRI Ruby")
100
- cells = ln.split("|", -1)
101
- badge_cell = cells[2] || ""
102
- badge_cell.strip.empty?
103
- else
104
- false
105
- end
106
- }.join
107
-
108
- # Clean up extra repeated whitespace only when it appears between word characters, and only for non-table lines.
109
- # This preserves Markdown table alignment and spacing around punctuation/symbols.
110
- content = content.lines.map do |ln|
111
- if ln.start_with?("|")
112
- ln
113
- else
114
- # Squish only runs of spaces/tabs between word characters
115
- ln.gsub(/(\w)[ \t]{2,}(\w)/u, "\\1 \\2")
116
- end
117
- end.join
118
-
119
- # Remove reference definitions for removed labels that are no longer used
120
- unless removed_labels.empty?
121
- # Unique
122
- removed_labels.uniq!
123
- # Determine which labels are still referenced after edits
124
- still_referenced = {}
125
- removed_labels.each do |lbl|
126
- lbl_re = Regexp.escape(lbl)
127
- # Consider a label referenced only when it appears not as a definition (i.e., not followed by colon)
128
- still_referenced[lbl] = !!(content =~ /\[#{lbl_re}\](?!:)/)
129
- end
130
-
131
- new_lines = content.lines.map do |line|
132
- if line =~ /^\[(?<lab>[^\]]+)\]:/ && removed_labels.include?(Regexp.last_match(:lab))
133
- # Only drop if not referenced anymore
134
- still_referenced[Regexp.last_match(:lab)] ? line : nil
135
- else
136
- line
137
- end
138
- end.compact
139
- content = new_lines.join
140
- end
141
-
142
- File.open(readme_path, "w") { |f| f.write(content) }
143
- end
144
- end
145
- rescue StandardError => e
146
- Kettle::Dev.debug_error(e, __method__)
147
- puts "WARNING: Skipped trimming MRI Ruby badges in README.md due to #{e.class}: #{e.message}"
148
- end
149
-
150
- # Synchronize leading grapheme (emoji) between README H1 and gemspec summary/description
151
- begin
152
- readme_path = File.join(project_root, "README.md")
153
- gemspecs = Dir.glob(File.join(project_root, "*.gemspec"))
154
- if File.file?(readme_path) && !gemspecs.empty?
155
- gemspec_path = gemspecs.first
156
- readme = File.read(readme_path)
157
- first_h1_idx = readme.lines.index { |ln| ln =~ /^#\s+/ }
158
- chosen_grapheme = nil
159
- if first_h1_idx
160
- lines = readme.split("\n", -1)
161
- h1 = lines[first_h1_idx]
162
- tail = h1.sub(/^#\s+/, "")
163
- begin
164
- emoji_re = Kettle::EmojiRegex::REGEX
165
- # Extract first emoji grapheme cluster if present
166
- if tail =~ /\A#{emoji_re.source}/u
167
- cluster = tail[/\A\X/u]
168
- chosen_grapheme = cluster unless cluster.to_s.empty?
169
- end
170
- rescue StandardError => e
171
- Kettle::Dev.debug_error(e, __method__)
172
- # Fallback: take first Unicode grapheme if any non-space char
173
- chosen_grapheme ||= tail[/\A\X/u]
174
- end
175
- end
176
-
177
- # If no grapheme found in README H1, either use a default in force mode, or ask the user.
178
- if chosen_grapheme.nil? || chosen_grapheme.empty?
179
- if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
180
- # Non-interactive install: default to pizza slice to match template style.
181
- chosen_grapheme = "🍕"
182
- else
183
- puts "No grapheme found after README H1. Enter a grapheme (emoji/symbol) to use for README, summary, and description:"
184
- print("Grapheme: ")
185
- ans = Kettle::Dev::InputAdapter.gets&.strip.to_s
186
- chosen_grapheme = ans[/\A\X/u].to_s
187
- # If still empty, skip synchronization silently
188
- chosen_grapheme = nil if chosen_grapheme.empty?
189
- end
190
- end
191
-
192
- if chosen_grapheme
193
- # 1) Normalize README H1 to exactly one grapheme + single space after '#'
194
- begin
195
- lines = readme.split("\n", -1)
196
- idx = lines.index { |ln| ln =~ /^#\s+/ }
197
- if idx
198
- rest = lines[idx].sub(/^#\s+/, "")
199
- begin
200
- emoji_re = Kettle::EmojiRegex::REGEX
201
- # Remove any leading emojis from the H1 by peeling full grapheme clusters
202
- tmp = rest.dup
203
- while tmp =~ /\A#{emoji_re.source}/u
204
- cluster = tmp[/\A\X/u]
205
- tmp = tmp[cluster.length..-1].to_s
206
- end
207
- rest_wo_emoji = tmp.sub(/\A\s+/, "")
208
- rescue StandardError => e
209
- Kettle::Dev.debug_error(e, __method__)
210
- rest_wo_emoji = rest.sub(/\A\s+/, "")
211
- end
212
- # Build H1 with single spaces only around separators; preserve inner spacing in rest_wo_emoji
213
- new_line = ["#", chosen_grapheme, rest_wo_emoji].join(" ").sub(/^#\s+/, "# ")
214
- lines[idx] = new_line
215
- new_readme = lines.join("\n")
216
- File.open(readme_path, "w") { |f| f.write(new_readme) }
217
- end
218
- rescue StandardError => e
219
- Kettle::Dev.debug_error(e, __method__)
220
- # ignore README normalization errors
221
- end
222
-
223
- # 2) Update gemspec summary and description to start with grapheme + single space
224
- begin
225
- gspec = File.read(gemspec_path)
226
-
227
- normalize_field = lambda do |text, field|
228
- # Match the assignment line and the first quoted string
229
- text.gsub(/(\b#{Regexp.escape(field)}\s*=\s*)(["'])([^\"']*)(\2)/) do
230
- pre = Regexp.last_match(1)
231
- q = Regexp.last_match(2)
232
- body = Regexp.last_match(3)
233
- # Strip existing leading emojis and spaces
234
- begin
235
- emoji_re = Kettle::EmojiRegex::REGEX
236
- tmp = body.dup
237
- tmp = tmp.sub(/\A\s+/, "")
238
- while tmp =~ /\A#{emoji_re.source}/u
239
- cluster = tmp[/\A\X/u]
240
- tmp = tmp[cluster.length..-1].to_s
241
- end
242
- tmp = tmp.sub(/\A\s+/, "")
243
- body_wo = tmp
244
- rescue StandardError => e
245
- Kettle::Dev.debug_error(e, __method__)
246
- body_wo = body.sub(/\A\s+/, "")
247
- end
248
- pre + q + ("#{chosen_grapheme} " + body_wo) + q
249
- end
250
- end
251
-
252
- gspec2 = normalize_field.call(gspec, "spec.summary")
253
- gspec3 = normalize_field.call(gspec2, "spec.description")
254
- if gspec3 != gspec
255
- File.open(gemspec_path, "w") { |f| f.write(gspec3) }
256
- end
257
- rescue StandardError => e
258
- Kettle::Dev.debug_error(e, __method__)
259
- # ignore gemspec edits on error
260
- end
261
- end
262
- end
263
- rescue StandardError => e
264
- puts "WARNING: Skipped grapheme synchronization due to #{e.class}: #{e.message}"
265
- end
266
-
267
- # Perform final whitespace normalization for README: only squish whitespace between word characters (non-table lines)
268
- begin
269
- readme_path = File.join(project_root, "README.md")
270
- if File.file?(readme_path)
271
- content = File.read(readme_path)
272
- content = content.lines.map do |ln|
273
- if ln.start_with?("|")
274
- ln
275
- else
276
- ln.gsub(/(\w)[ \t]{2,}(\w)/u, "\\1 \\2")
277
- end
278
- end.join
279
- File.open(readme_path, "w") { |f| f.write(content) }
280
- end
281
- rescue StandardError => e
282
- Kettle::Dev.debug_error(e, __method__)
283
- # ignore whitespace normalization errors
284
- end
285
-
286
- # Validate gemspec homepage points to GitHub and is a non-interpolated string
287
- begin
288
- gemspecs = Dir.glob(File.join(project_root, "*.gemspec"))
289
- if gemspecs.empty?
290
- puts
291
- puts "No .gemspec found in #{project_root}; skipping homepage check."
292
- else
293
- gemspec_path = gemspecs.first
294
- if gemspecs.size > 1
295
- puts
296
- puts "Multiple gemspecs found; defaulting to #{File.basename(gemspec_path)} for homepage check."
297
- end
298
-
299
- content = File.read(gemspec_path)
300
- homepage_line = content.lines.find { |l| l =~ /\bspec\.homepage\s*=\s*/ }
301
- if homepage_line.nil?
302
- puts
303
- puts "WARNING: spec.homepage not found in #{File.basename(gemspec_path)}."
304
- puts "This gem should declare a GitHub homepage: https://github.com/<org>/<repo>"
305
- else
306
- assigned = homepage_line.split("=", 2).last.to_s.strip
307
- interpolated = assigned.include?('#{')
308
-
309
- if assigned.start_with?("\"", "'")
310
- begin
311
- assigned = assigned[1..-2]
312
- rescue
313
- # leave as-is
314
- end
315
- end
316
-
317
- github_repo_from_url = lambda do |url|
318
- return unless url
319
-
320
- url = url.strip
321
- m = url.match(%r{github\.com[/:]([^/\s:]+)/([^/\s]+?)(?:\.git)?/?\z}i)
322
- return unless m
323
-
324
- [m[1], m[2]]
325
- end
326
-
327
- github_homepage_literal = lambda do |val|
328
- return false unless val
329
- return false if val.include?('#{')
330
-
331
- v = val.to_s.strip
332
- if (v.start_with?("\"") && v.end_with?("\"")) || (v.start_with?("'") && v.end_with?("'"))
333
- v = begin
334
- v[1..-2]
335
- rescue
336
- v
337
- end
338
- end
339
- return false unless v =~ %r{\Ahttps?://github\.com/}i
340
-
341
- !!github_repo_from_url.call(v)
342
- end
343
-
344
- valid_literal = github_homepage_literal.call(assigned)
345
-
346
- if interpolated || !valid_literal
347
- puts
348
- puts "Checking git remote 'origin' to derive GitHub homepage..."
349
- origin_url = ""
350
- # Use GitAdapter to avoid hanging and to simplify testing.
351
- begin
352
- ga = Kettle::Dev::GitAdapter.new
353
- origin_url = ga.remote_url("origin") || ga.remotes_with_urls["origin"]
354
- origin_url = origin_url.to_s.strip
355
- rescue StandardError => e
356
- Kettle::Dev.debug_error(e, __method__)
357
- end
358
-
359
- org_repo = github_repo_from_url.call(origin_url)
360
- unless org_repo
361
- puts "ERROR: git remote 'origin' is not a GitHub URL (or not found): #{origin_url.empty? ? "(none)" : origin_url}"
362
- puts "To complete installation: set your GitHub repository as the 'origin' remote, and move any other forge to an alternate name."
363
- puts "Example:"
364
- puts " git remote rename origin something_else"
365
- puts " git remote add origin https://github.com/<org>/<repo>.git"
366
- puts "After fixing, re-run: rake kettle:dev:install"
367
- task_abort("Aborting: homepage cannot be corrected without a GitHub origin remote.")
368
- end
369
-
370
- org, repo = org_repo
371
- suggested = "https://github.com/#{org}/#{repo}"
372
-
373
- puts "Current spec.homepage appears #{interpolated ? "interpolated" : "invalid"}: #{assigned}"
374
- puts "Suggested literal homepage: \"#{suggested}\""
375
- print("Update #{File.basename(gemspec_path)} to use this homepage? [Y/n]: ")
376
- do_update =
377
- if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
378
- true
379
- else
380
- ans = Kettle::Dev::InputAdapter.gets&.strip
381
- ans.nil? || ans.empty? || ans =~ /\Ay(es)?\z/i
382
- end
383
-
384
- if do_update
385
- new_line = homepage_line.sub(/=.*/, "= \"#{suggested}\"\n")
386
- new_content = content.sub(homepage_line, new_line)
387
- File.open(gemspec_path, "w") { |f| f.write(new_content) }
388
- puts "Updated spec.homepage in #{File.basename(gemspec_path)} to #{suggested}"
389
- else
390
- puts "Skipping update of spec.homepage. You should set it to: #{suggested}"
391
- end
392
- end
393
- end
394
- end
395
- rescue StandardError => e
396
- # Do not swallow intentional task aborts signaled via Kettle::Dev::Error
397
- raise if e.is_a?(Kettle::Dev::Error)
398
-
399
- puts "WARNING: An error occurred while checking gemspec homepage: #{e.class}: #{e.message}"
400
- end
401
-
402
- # Summary of templating changes
403
- begin
404
- results = helpers.template_results
405
- meaningful = results.select { |_, rec| [:create, :replace, :dir_create, :dir_replace].include?(rec[:action]) }
406
- puts
407
- puts "Summary of templating changes:"
408
- if meaningful.empty?
409
- puts " (no files were created or replaced by kettle:dev:template)"
410
- else
411
- action_labels = {
412
- create: "Created",
413
- replace: "Replaced",
414
- dir_create: "Directory created",
415
- dir_replace: "Directory replaced",
416
- }
417
- [:create, :replace, :dir_create, :dir_replace].each do |sym|
418
- items = meaningful.select { |_, rec| rec[:action] == sym }.map { |path, _| path }
419
- next if items.empty?
420
-
421
- puts " #{action_labels[sym]}:"
422
- items.sort.each do |abs|
423
- rel = begin
424
- abs.start_with?(project_root.to_s) ? abs.sub(/^#{Regexp.escape(project_root.to_s)}\/?/, "") : abs
425
- rescue
426
- abs
427
- end
428
- puts " - #{rel}"
429
- end
430
- end
431
- end
432
- rescue StandardError => e
433
- puts
434
- puts "Summary of templating changes: (unavailable: #{e.class}: #{e.message})"
435
- end
436
-
437
- puts
438
- puts "Next steps:"
439
- puts "1) Configure a shared git hooks path (optional, recommended):"
440
- puts " git config --global core.hooksPath .git-hooks"
441
- puts
442
- puts "2) Install binstubs for this gem so the commit-msg tool is available in ./bin:"
443
- puts " bundle binstubs kettle-dev --path bin"
444
- puts " # After running, you should have bin/kettle-commit-msg (wrapper)."
445
- puts
446
- # Step 3: direnv and .envrc
447
- envrc_path = File.join(project_root, ".envrc")
448
- puts "3) Install direnv (if not already):"
449
- puts " brew install direnv"
450
- if helpers.modified_by_template?(envrc_path)
451
- puts " Your .envrc was created/updated by kettle:dev:template."
452
- puts " It includes PATH_add bin so that executables in ./bin are on PATH when direnv is active."
453
- puts " This allows running tools without the bin/ prefix inside the project directory."
454
- else
455
- begin
456
- current = File.file?(envrc_path) ? File.read(envrc_path) : ""
457
- rescue StandardError => e
458
- Kettle::Dev.debug_error(e, __method__)
459
- current = ""
460
- end
461
- has_path_add = current.lines.any? { |l| l.strip =~ /^PATH_add\s+bin\b/ }
462
- if has_path_add
463
- puts " Your .envrc already contains PATH_add bin."
464
- else
465
- puts " Adding PATH_add bin to your project's .envrc is recommended to expose ./bin on PATH."
466
- if helpers.ask("Add PATH_add bin to #{envrc_path}?", false)
467
- content = current.dup
468
- insertion = "# Run any command in this project's bin/ without the bin/ prefix\nPATH_add bin\n"
469
- if content.empty?
470
- content = insertion
471
- else
472
- content = insertion + "\n" + content unless content.start_with?(insertion)
473
- end
474
- # Ensure a stale directory at .envrc is removed so the file can be written
475
- FileUtils.rm_rf(envrc_path) if File.directory?(envrc_path)
476
- File.open(envrc_path, "w") { |f| f.write(content) }
477
- puts " Updated #{envrc_path} with PATH_add bin"
478
- updated_envrc_by_install = true
479
- else
480
- puts " Skipping modification of .envrc. You may add 'PATH_add bin' manually at the top."
481
- end
482
- end
483
- end
484
-
485
- if defined?(updated_envrc_by_install) && updated_envrc_by_install
486
- allowed_truthy = ENV.fetch("allowed", "").to_s =~ ENV_TRUE_RE
487
- if allowed_truthy
488
- puts "Proceeding after .envrc update because allowed=true."
489
- else
490
- puts
491
- puts "IMPORTANT: .envrc was updated during kettle:dev:install."
492
- puts "Please review it and then run:"
493
- puts " direnv allow"
494
- puts
495
- puts "After that, re-run to resume:"
496
- puts " bundle exec rake kettle:dev:install allowed=true"
497
- task_abort("Aborting: direnv allow required after .envrc changes.")
498
- end
499
- end
500
-
501
- # Warn about .env.local and offer to add it to .gitignore
502
- puts
503
- puts "WARNING: Do not commit .env.local; it often contains machine-local secrets."
504
- puts "Ensure your .gitignore includes:"
505
- puts " # direnv - brew install direnv"
506
- puts " .env.local"
507
-
508
- gitignore_path = File.join(project_root, ".gitignore")
509
- unless helpers.modified_by_template?(gitignore_path)
510
- begin
511
- gitignore_current = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
512
- rescue StandardError => e
513
- Kettle::Dev.debug_error(e, __method__)
514
- gitignore_current = ""
515
- end
516
- has_env_local = gitignore_current.lines.any? { |l| l.strip == ".env.local" }
517
- unless has_env_local
518
- puts
519
- puts "Would you like to add '.env.local' to #{gitignore_path}?"
520
- print("Add to .gitignore now [Y/n]: ")
521
- answer = Kettle::Dev::InputAdapter.gets&.strip
522
- # Respect an explicit negative answer even when force=true
523
- add_it = if answer && answer =~ /\An(o)?\z/i
524
- false
525
- elsif ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
526
- true
527
- else
528
- answer.nil? || answer.empty? || answer =~ /\Ay(es)?\z/i
529
- end
530
- if add_it
531
- FileUtils.mkdir_p(File.dirname(gitignore_path))
532
- mode = File.exist?(gitignore_path) ? "a" : "w"
533
- File.open(gitignore_path, mode) do |f|
534
- f.write("\n") unless gitignore_current.empty? || gitignore_current.end_with?("\n")
535
- unless gitignore_current.lines.any? { |l| l.strip == "# direnv - brew install direnv" }
536
- f.write("# direnv - brew install direnv\n")
537
- end
538
- f.write(".env.local\n")
539
- end
540
- puts "Added .env.local to #{gitignore_path}"
541
- else
542
- puts "Skipping modification of .gitignore. Remember to add .env.local to avoid committing it."
543
- end
544
- end
545
- end
546
-
547
- puts
548
- puts "kettle:dev:install complete."
549
- end
550
- end
551
- end
552
- end
553
- end