kettle-dev 1.0.10 → 1.0.11

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "kettle/dev/exit_adapter"
4
+ require "kettle/dev/input_adapter"
4
5
 
5
6
  module Kettle
6
7
  module Dev
@@ -38,93 +39,221 @@ module Kettle
38
39
  end
39
40
  end
40
41
 
41
- puts
42
- puts "Next steps:"
43
- puts "1) Configure a shared git hooks path (optional, recommended):"
44
- puts " git config --global core.hooksPath .git-hooks"
45
- puts
46
- puts "2) Install binstubs for this gem so the commit-msg tool is available in ./bin:"
47
- puts " bundle binstubs kettle-dev --path bin"
48
- puts " # After running, you should have bin/kettle-commit-msg (wrapper)."
49
- puts
50
- # Step 3: direnv and .envrc
51
- envrc_path = File.join(project_root, ".envrc")
52
- puts "3) Install direnv (if not already):"
53
- puts " brew install direnv"
54
- if helpers.modified_by_template?(envrc_path)
55
- puts " Your .envrc was created/updated by kettle:dev:template."
56
- puts " It includes PATH_add bin so that executables in ./bin are on PATH when direnv is active."
57
- puts " This allows running tools without the bin/ prefix inside the project directory."
58
- else
59
- begin
60
- current = File.file?(envrc_path) ? File.read(envrc_path) : ""
61
- rescue StandardError
62
- current = ""
63
- end
64
- has_path_add = current.lines.any? { |l| l.strip =~ /^PATH_add\s+bin\b/ }
65
- if has_path_add
66
- puts " Your .envrc already contains PATH_add bin."
67
- else
68
- puts " Adding PATH_add bin to your project's .envrc is recommended to expose ./bin on PATH."
69
- if helpers.ask("Add PATH_add bin to #{envrc_path}?", false)
70
- content = current.dup
71
- insertion = "# Run any command in this project's bin/ without the bin/ prefix\nPATH_add bin\n"
72
- if content.empty?
73
- content = insertion
74
- else
75
- content = insertion + "\n" + content unless content.start_with?(insertion)
42
+ # Trim MRI Ruby version badges in README.md to >= required_ruby_version from gemspec
43
+ begin
44
+ readme_path = File.join(project_root, "README.md")
45
+ if File.file?(readme_path)
46
+ md = helpers.gemspec_metadata(project_root)
47
+ min_ruby = md[:min_ruby].to_s.strip
48
+ if !min_ruby.empty?
49
+ # Compare using Gem::Version on major.minor
50
+ require "rubygems"
51
+ min_ver = begin
52
+ Gem::Version.new(min_ruby.sub(/\A(~>\s*|>=\s*)/, ""))
53
+ rescue
54
+ nil
55
+ end
56
+ if min_ver
57
+ content = File.read(readme_path)
58
+
59
+ # Detect all MRI ruby badge labels present
60
+ removed_labels = []
61
+
62
+ content.scan(/\[(?<label>💎ruby-(?<ver>\d+\.\d+)i)\]/) do |arr|
63
+ label, ver_s = arr
64
+ begin
65
+ ver = Gem::Version.new(ver_s)
66
+ if ver < min_ver
67
+ # Remove occurrences of badges using this label
68
+ label_re = Regexp.escape(label)
69
+ # Linked form: [![...][label]][...]
70
+ content = content.gsub(/\[!\[[^\]]*?\]\s*\[#{label_re}\]\s*\]\s*\[[^\]]+\]/, "")
71
+ # Unlinked form: ![...][label]
72
+ content = content.gsub(/!\[[^\]]*?\]\s*\[#{label_re}\]/, "")
73
+ removed_labels << label
74
+ end
75
+ rescue StandardError
76
+ # ignore
77
+ end
78
+ end
79
+
80
+ # Remove any MRI Ruby rows that are left with no badges/links after trimming
81
+ content = content.lines.reject { |ln|
82
+ if ln.start_with?("| Works with MRI Ruby")
83
+ cells = ln.split("|", -1)
84
+ # cells[0] is empty (leading |), cells[1] = label cell, cells[2] = badges cell
85
+ badge_cell = cells[2] || ""
86
+ badge_cell.strip.empty?
87
+ else
88
+ false
89
+ end
90
+ }.join
91
+
92
+ # Clean up extra repeated whitespace only when it appears between word characters, and only for non-table lines.
93
+ # This preserves Markdown table alignment and spacing around punctuation/symbols.
94
+ content = content.lines.map do |ln|
95
+ if ln.start_with?("|")
96
+ ln
97
+ else
98
+ # Squish only runs of spaces/tabs between word characters
99
+ ln.gsub(/(\w)[ \t]{2,}(\w)/u, "\\1 \\2")
100
+ end
101
+ end.join
102
+
103
+ # Remove reference definitions for removed labels that are no longer used
104
+ unless removed_labels.empty?
105
+ # Unique
106
+ removed_labels.uniq!
107
+ # Determine which labels are still referenced after edits
108
+ still_referenced = {}
109
+ removed_labels.each do |lbl|
110
+ lbl_re = Regexp.escape(lbl)
111
+ # Consider a label referenced only when it appears not as a definition (i.e., not followed by colon)
112
+ still_referenced[lbl] = !!(content =~ /\[#{lbl_re}\](?!:)/)
113
+ end
114
+
115
+ new_lines = content.lines.map do |line|
116
+ if line =~ /^\[(?<lab>[^\]]+)\]:/ && removed_labels.include?(Regexp.last_match(:lab))
117
+ # Only drop if not referenced anymore
118
+ still_referenced[Regexp.last_match(:lab)] ? line : nil
119
+ else
120
+ line
121
+ end
122
+ end.compact
123
+ content = new_lines.join
124
+ end
125
+
126
+ File.open(readme_path, "w") { |f| f.write(content) }
76
127
  end
77
- # Ensure a stale directory at .envrc is removed so the file can be written
78
- FileUtils.rm_rf(envrc_path) if File.directory?(envrc_path)
79
- File.open(envrc_path, "w") { |f| f.write(content) }
80
- puts " Updated #{envrc_path} with PATH_add bin"
81
- updated_envrc_by_install = true
82
- else
83
- puts " Skipping modification of .envrc. You may add 'PATH_add bin' manually at the top."
84
128
  end
85
129
  end
130
+ rescue StandardError => e
131
+ puts "WARNING: Skipped trimming MRI Ruby badges in README.md due to #{e.class}: #{e.message}"
86
132
  end
87
133
 
88
- # Warn about .env.local and offer to add it to .gitignore
89
- puts
90
- puts "WARNING: Do not commit .env.local; it often contains machine-local secrets."
91
- puts "Ensure your .gitignore includes:"
92
- puts " # direnv - brew install direnv"
93
- puts " .env.local"
134
+ # Synchronize leading grapheme (emoji) between README H1 and gemspec summary/description
135
+ begin
136
+ readme_path = File.join(project_root, "README.md")
137
+ gemspecs = Dir.glob(File.join(project_root, "*.gemspec"))
138
+ if File.file?(readme_path) && !gemspecs.empty?
139
+ gemspec_path = gemspecs.first
140
+ readme = File.read(readme_path)
141
+ first_h1_idx = readme.lines.index { |ln| ln =~ /^#\s+/ }
142
+ chosen_grapheme = nil
143
+ if first_h1_idx
144
+ lines = readme.split("\n", -1)
145
+ h1 = lines[first_h1_idx]
146
+ tail = h1.sub(/^#\s+/, "")
147
+ begin
148
+ emoji_re = Kettle::EmojiRegex::REGEX
149
+ # Extract first emoji grapheme cluster if present
150
+ if tail =~ /\A#{emoji_re.source}/u
151
+ cluster = tail[/\A\X/u]
152
+ chosen_grapheme = cluster unless cluster.to_s.empty?
153
+ end
154
+ rescue StandardError
155
+ # Fallback: take first Unicode grapheme if any non-space char
156
+ chosen_grapheme ||= tail[/\A\X/u]
157
+ end
158
+ end
94
159
 
95
- gitignore_path = File.join(project_root, ".gitignore")
96
- unless helpers.modified_by_template?(gitignore_path)
97
- begin
98
- gitignore_current = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
99
- rescue StandardError
100
- gitignore_current = ""
101
- end
102
- has_env_local = gitignore_current.lines.any? { |l| l.strip == ".env.local" }
103
- unless has_env_local
104
- puts
105
- puts "Would you like to add '.env.local' to #{gitignore_path}?"
106
- print("Add to .gitignore now [Y/n]: ")
107
- answer = $stdin.gets&.strip
108
- add_it = if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
109
- true
110
- else
111
- answer.nil? || answer.empty? || answer =~ /\Ay(es)?\z/i
160
+ # If no grapheme found in README H1, ask the user which to use
161
+ if chosen_grapheme.nil? || chosen_grapheme.empty?
162
+ puts "No grapheme found after README H1. Enter a grapheme (emoji/symbol) to use for README, summary, and description:"
163
+ print("Grapheme: ")
164
+ ans = Kettle::Dev::InputAdapter.gets&.strip.to_s
165
+ chosen_grapheme = ans[/\A\X/u].to_s
166
+ # If still empty, skip synchronization silently
167
+ chosen_grapheme = nil if chosen_grapheme.empty?
112
168
  end
113
- if add_it
114
- FileUtils.mkdir_p(File.dirname(gitignore_path))
115
- mode = File.exist?(gitignore_path) ? "a" : "w"
116
- File.open(gitignore_path, mode) do |f|
117
- f.write("\n") unless gitignore_current.empty? || gitignore_current.end_with?("\n")
118
- unless gitignore_current.lines.any? { |l| l.strip == "# direnv - brew install direnv" }
119
- f.write("# direnv - brew install direnv\n")
169
+
170
+ if chosen_grapheme
171
+ # 1) Normalize README H1 to exactly one grapheme + single space after '#'
172
+ begin
173
+ lines = readme.split("\n", -1)
174
+ idx = lines.index { |ln| ln =~ /^#\s+/ }
175
+ if idx
176
+ rest = lines[idx].sub(/^#\s+/, "")
177
+ begin
178
+ emoji_re = Kettle::EmojiRegex::REGEX
179
+ # Remove any leading emojis from the H1 by peeling full grapheme clusters
180
+ tmp = rest.dup
181
+ while tmp =~ /\A#{emoji_re.source}/u
182
+ cluster = tmp[/\A\X/u]
183
+ tmp = tmp[cluster.length..-1].to_s
184
+ end
185
+ rest_wo_emoji = tmp.sub(/\A\s+/, "")
186
+ rescue StandardError
187
+ rest_wo_emoji = rest.sub(/\A\s+/, "")
188
+ end
189
+ # Build H1 with single spaces only around separators; preserve inner spacing in rest_wo_emoji
190
+ new_line = ["#", chosen_grapheme, rest_wo_emoji].join(" ").sub(/^#\s+/, "# ")
191
+ lines[idx] = new_line
192
+ new_readme = lines.join("\n")
193
+ File.open(readme_path, "w") { |f| f.write(new_readme) }
120
194
  end
121
- f.write(".env.local\n")
195
+ rescue StandardError
196
+ # ignore README normalization errors
197
+ end
198
+
199
+ # 2) Update gemspec summary and description to start with grapheme + single space
200
+ begin
201
+ gspec = File.read(gemspec_path)
202
+
203
+ normalize_field = lambda do |text, field|
204
+ # Match the assignment line and the first quoted string
205
+ text.gsub(/(\b#{Regexp.escape(field)}\s*=\s*)(["'])([^\"']*)(\2)/) do
206
+ pre = Regexp.last_match(1)
207
+ q = Regexp.last_match(2)
208
+ body = Regexp.last_match(3)
209
+ # Strip existing leading emojis and spaces
210
+ begin
211
+ emoji_re = Kettle::EmojiRegex::REGEX
212
+ tmp = body.dup
213
+ tmp = tmp.sub(/\A\s+/, "")
214
+ while tmp =~ /\A#{emoji_re.source}/u
215
+ cluster = tmp[/\A\X/u]
216
+ tmp = tmp[cluster.length..-1].to_s
217
+ end
218
+ tmp = tmp.sub(/\A\s+/, "")
219
+ body_wo = tmp
220
+ rescue StandardError
221
+ body_wo = body.sub(/\A\s+/, "")
222
+ end
223
+ pre + q + ("#{chosen_grapheme} " + body_wo) + q
224
+ end
225
+ end
226
+
227
+ gspec2 = normalize_field.call(gspec, "spec.summary")
228
+ gspec3 = normalize_field.call(gspec2, "spec.description")
229
+ if gspec3 != gspec
230
+ File.open(gemspec_path, "w") { |f| f.write(gspec3) }
231
+ end
232
+ rescue StandardError
233
+ # ignore gemspec edits on error
122
234
  end
123
- puts "Added .env.local to #{gitignore_path}"
124
- else
125
- puts "Skipping modification of .gitignore. Remember to add .env.local to avoid committing it."
126
235
  end
127
236
  end
237
+ rescue StandardError => e
238
+ puts "WARNING: Skipped grapheme synchronization due to #{e.class}: #{e.message}"
239
+ end
240
+
241
+ # Perform final whitespace normalization for README: only squish whitespace between word characters (non-table lines)
242
+ begin
243
+ readme_path = File.join(project_root, "README.md")
244
+ if File.file?(readme_path)
245
+ content = File.read(readme_path)
246
+ content = content.lines.map do |ln|
247
+ if ln.start_with?("|")
248
+ ln
249
+ else
250
+ ln.gsub(/(\w)[ \t]{2,}(\w)/u, "\\1 \\2")
251
+ end
252
+ end.join
253
+ File.open(readme_path, "w") { |f| f.write(content) }
254
+ end
255
+ rescue StandardError
256
+ # ignore whitespace normalization errors
128
257
  end
129
258
 
130
259
  # Validate gemspec homepage points to GitHub and is a non-interpolated string
@@ -213,7 +342,7 @@ module Kettle
213
342
  puts "Current spec.homepage appears #{interpolated ? "interpolated" : "invalid"}: #{assigned}"
214
343
  puts "Suggested literal homepage: \"#{suggested}\""
215
344
  print("Update #{File.basename(gemspec_path)} to use this homepage? [Y/n]: ")
216
- ans = $stdin.gets&.strip
345
+ ans = Kettle::Dev::InputAdapter.gets&.strip
217
346
  do_update = if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
218
347
  true
219
348
  else
@@ -237,24 +366,6 @@ module Kettle
237
366
  puts "WARNING: An error occurred while checking gemspec homepage: #{e.class}: #{e.message}"
238
367
  end
239
368
 
240
- if defined?(updated_envrc_by_install) && updated_envrc_by_install
241
- allowed_truthy = ENV.fetch("allowed", "").to_s =~ /\A(1|true|y|yes)\z/i
242
- force_truthy = ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
243
- if allowed_truthy || force_truthy
244
- reason = allowed_truthy ? "allowed=true" : "force=true"
245
- puts "Proceeding after .envrc update because #{reason}."
246
- else
247
- puts
248
- puts "IMPORTANT: .envrc was updated during kettle:dev:install."
249
- puts "Please review it and then run:"
250
- puts " direnv allow"
251
- puts
252
- puts "After that, re-run to resume:"
253
- puts " bundle exec rake kettle:dev:install allowed=true"
254
- task_abort("Aborting: direnv allow required after .envrc changes.")
255
- end
256
- end
257
-
258
369
  # Summary of templating changes
259
370
  begin
260
371
  results = helpers.template_results
@@ -289,6 +400,113 @@ module Kettle
289
400
  puts "Summary of templating changes: (unavailable: #{e.class}: #{e.message})"
290
401
  end
291
402
 
403
+ puts
404
+ puts "Next steps:"
405
+ puts "1) Configure a shared git hooks path (optional, recommended):"
406
+ puts " git config --global core.hooksPath .git-hooks"
407
+ puts
408
+ puts "2) Install binstubs for this gem so the commit-msg tool is available in ./bin:"
409
+ puts " bundle binstubs kettle-dev --path bin"
410
+ puts " # After running, you should have bin/kettle-commit-msg (wrapper)."
411
+ puts
412
+ # Step 3: direnv and .envrc
413
+ envrc_path = File.join(project_root, ".envrc")
414
+ puts "3) Install direnv (if not already):"
415
+ puts " brew install direnv"
416
+ if helpers.modified_by_template?(envrc_path)
417
+ puts " Your .envrc was created/updated by kettle:dev:template."
418
+ puts " It includes PATH_add bin so that executables in ./bin are on PATH when direnv is active."
419
+ puts " This allows running tools without the bin/ prefix inside the project directory."
420
+ else
421
+ begin
422
+ current = File.file?(envrc_path) ? File.read(envrc_path) : ""
423
+ rescue StandardError
424
+ current = ""
425
+ end
426
+ has_path_add = current.lines.any? { |l| l.strip =~ /^PATH_add\s+bin\b/ }
427
+ if has_path_add
428
+ puts " Your .envrc already contains PATH_add bin."
429
+ else
430
+ puts " Adding PATH_add bin to your project's .envrc is recommended to expose ./bin on PATH."
431
+ if helpers.ask("Add PATH_add bin to #{envrc_path}?", false)
432
+ content = current.dup
433
+ insertion = "# Run any command in this project's bin/ without the bin/ prefix\nPATH_add bin\n"
434
+ if content.empty?
435
+ content = insertion
436
+ else
437
+ content = insertion + "\n" + content unless content.start_with?(insertion)
438
+ end
439
+ # Ensure a stale directory at .envrc is removed so the file can be written
440
+ FileUtils.rm_rf(envrc_path) if File.directory?(envrc_path)
441
+ File.open(envrc_path, "w") { |f| f.write(content) }
442
+ puts " Updated #{envrc_path} with PATH_add bin"
443
+ updated_envrc_by_install = true
444
+ else
445
+ puts " Skipping modification of .envrc. You may add 'PATH_add bin' manually at the top."
446
+ end
447
+ end
448
+ end
449
+
450
+ if defined?(updated_envrc_by_install) && updated_envrc_by_install
451
+ allowed_truthy = ENV.fetch("allowed", "").to_s =~ /\A(1|true|y|yes)\z/i
452
+ force_truthy = ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
453
+ if allowed_truthy || force_truthy
454
+ reason = allowed_truthy ? "allowed=true" : "force=true"
455
+ puts "Proceeding after .envrc update because #{reason}."
456
+ else
457
+ puts
458
+ puts "IMPORTANT: .envrc was updated during kettle:dev:install."
459
+ puts "Please review it and then run:"
460
+ puts " direnv allow"
461
+ puts
462
+ puts "After that, re-run to resume:"
463
+ puts " bundle exec rake kettle:dev:install allowed=true"
464
+ task_abort("Aborting: direnv allow required after .envrc changes.")
465
+ end
466
+ end
467
+
468
+ # Warn about .env.local and offer to add it to .gitignore
469
+ puts
470
+ puts "WARNING: Do not commit .env.local; it often contains machine-local secrets."
471
+ puts "Ensure your .gitignore includes:"
472
+ puts " # direnv - brew install direnv"
473
+ puts " .env.local"
474
+
475
+ gitignore_path = File.join(project_root, ".gitignore")
476
+ unless helpers.modified_by_template?(gitignore_path)
477
+ begin
478
+ gitignore_current = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
479
+ rescue StandardError
480
+ gitignore_current = ""
481
+ end
482
+ has_env_local = gitignore_current.lines.any? { |l| l.strip == ".env.local" }
483
+ unless has_env_local
484
+ puts
485
+ puts "Would you like to add '.env.local' to #{gitignore_path}?"
486
+ print("Add to .gitignore now [Y/n]: ")
487
+ answer = Kettle::Dev::InputAdapter.gets&.strip
488
+ add_it = if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
489
+ true
490
+ else
491
+ answer.nil? || answer.empty? || answer =~ /\Ay(es)?\z/i
492
+ end
493
+ if add_it
494
+ FileUtils.mkdir_p(File.dirname(gitignore_path))
495
+ mode = File.exist?(gitignore_path) ? "a" : "w"
496
+ File.open(gitignore_path, mode) do |f|
497
+ f.write("\n") unless gitignore_current.empty? || gitignore_current.end_with?("\n")
498
+ unless gitignore_current.lines.any? { |l| l.strip == "# direnv - brew install direnv" }
499
+ f.write("# direnv - brew install direnv\n")
500
+ end
501
+ f.write(".env.local\n")
502
+ end
503
+ puts "Added .env.local to #{gitignore_path}"
504
+ else
505
+ puts "Skipping modification of .gitignore. Remember to add .env.local to avoid committing it."
506
+ end
507
+ end
508
+ end
509
+
292
510
  puts
293
511
  puts "kettle:dev:install complete."
294
512
  end