hiiro 0.1.313 → 0.1.314

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0760d73ced611f47a517d2faa6cacca8efeb97060d8a4ed4fb3731e0a32dc27
4
- data.tar.gz: 44833c289d4d8519b840597cd8eb276085918b39a2e4dd97a4ea280b22fc747a
3
+ metadata.gz: 6fd4ee6b28c10a3203f70a472a4e65bae62c1636f24dceb4a1b21fe48c96c3ff
4
+ data.tar.gz: 1ddaded4e27043098e685c7af4445e042119ff2bdee80a47ac25a29d4f05ba2d
5
5
  SHA512:
6
- metadata.gz: a2cf3d9a87608e3e37a571e2e177f42383e691c80a7a1a7325ed3fa42a4ab7daf7ebc8d193db753cd9026ae57a56e0d1e75cea31a99332c539dcaf099b48c331
7
- data.tar.gz: 68694cb3fb0de4bcf5be5d2af5e374be9eb48e356ea61215cf6fb23d2a803b04d67b44c00c002e2ef05542557fe5a33bd1c594ef93ce24bbb0198351744ed006
6
+ metadata.gz: 7ff30eb7284958c439bc087bad2e1a74a805c6955a9482d8f4a0b6198f0fc6e35181742537102b7f8d0abeb5fc1ea8bce38a595042a5df2cf1470470a239ee52
7
+ data.tar.gz: 68d4ab840a4535a86f99faf26d910cc2fee354b35fd5a2fdcdec22e8fa4bc4b962a9cca0a18c56250076dd28db28102fe76b798621bd5b7084d516314a54182a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
- ```markdown
2
1
  # Changelog
3
2
 
3
+ ## [0.1.314] - 2026-04-01
4
+
5
+ ### Fixed
6
+ - Rename `Gem` class to `RubyGem` in publish script to avoid conflict with Ruby stdlib
7
+
4
8
  ## [0.1.313] - 2026-04-01
5
9
 
6
10
  ### Fixed
@@ -194,5 +198,4 @@
194
198
  ## [0.1.295]
195
199
 
196
200
  ### Changed
197
- - Filter logic changes for PR management
198
- ```
201
+ - Filter logic changes for PR management
data/exe/h CHANGED
@@ -225,7 +225,7 @@ Hiiro.run(*ARGV, cwd: Dir.pwd, tasks: true) do
225
225
  end
226
226
 
227
227
  unless found
228
- system('say', 'hiiro publish timed out')
228
+ system('terminal-notifier', '-title', 'hiiro publish', '-message', 'Timed out waiting for RubyGems')
229
229
  exit 1
230
230
  end
231
231
 
@@ -239,11 +239,11 @@ Hiiro.run(*ARGV, cwd: Dir.pwd, tasks: true) do
239
239
  output.match?(/hiiro \\([^)]*#{Regexp.escape(expected)}/)
240
240
  end
241
241
  if mismatched.empty?
242
- system('say', 'hiiro updated')
242
+ system('terminal-notifier', '-title', 'hiiro updated', '-message', "v\#{expected} installed across all rbenv versions")
243
243
  elsif mismatched.size == versions.size
244
- system('say', 'no versions updated')
244
+ system('terminal-notifier', '-title', 'hiiro update failed', '-message', "v\#{expected} not installed in any rbenv version")
245
245
  else
246
- system('say', "\#{mismatched.size} version\#{mismatched.size == 1 ? '' : 's'} not updated")
246
+ system('terminal-notifier', '-title', 'hiiro updated', '-message', "v\#{expected} installed — \#{mismatched.size} version\#{mismatched.size == 1 ? '' : 's'} skipped")
247
247
  end
248
248
  RUBY
249
249
 
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.313"
2
+ VERSION = "0.1.314"
3
3
  end
data/script/publish CHANGED
@@ -3,202 +3,238 @@
3
3
  require "net/http"
4
4
  require "json"
5
5
 
6
- # Parse -t flag for pre-release
7
- pre_release = ARGV.include?('-t') || `git branch --show-current`.strip != 'main'
8
-
9
- # Check for uncommitted changes before we modify anything
10
- has_pending_changes = !`git status --porcelain`.strip.empty?
11
-
12
- # Ensure local branch is up to date with remote
13
- system('git', 'fetch', 'origin')
14
- behind = `git rev-list HEAD..origin/main --count`.strip.to_i
15
- if behind > 0
16
- puts "ERROR: Local branch is #{behind} commit(s) behind origin/main. Pull before publishing."
17
- exit 1
18
- end
6
+ # ─── RubyGems API ────────────────────────────────────────────────────────────
19
7
 
20
- puts ""
21
- # Fetch all published versions from RubyGems
22
- def fetch_versions
23
- uri = URI("https://rubygems.org/api/v1/versions/hiiro.json")
24
- response = Net::HTTP.get(uri)
25
- JSON.parse(response).map { |v| v["number"] }
26
- rescue => e
27
- puts "Warning: Could not fetch versions from RubyGems: #{e.message}"
28
- puts "Falling back to local version"
29
- require "hiiro"
30
- [Hiiro::VERSION]
31
- end
8
+ class RubyGems
9
+ def self.published_versions(gem_name)
10
+ uri = URI("https://rubygems.org/api/v1/versions/#{gem_name}.json")
11
+ JSON.parse(Net::HTTP.get(uri)).map { |v| v["number"] }
12
+ rescue => e
13
+ puts "Warning: could not fetch versions from RubyGems (#{e.message}), falling back to local"
14
+ require "hiiro"
15
+ [Hiiro::VERSION]
16
+ end
32
17
 
33
- # Parse version string into comparable components
34
- def parse_version(version)
35
- parts = version.split(?.)
36
- is_pre = parts.length >= 5 && parts[-2] == 'pre'
37
- if is_pre
38
- { major: parts[0].to_i, minor: parts[1].to_i, patch: parts[2].to_i, pre: true, pre_num: parts[-1].to_i }
39
- else
40
- { major: parts[0].to_i, minor: parts[1].to_i, patch: parts[2].to_i, pre: false, pre_num: 0 }
18
+ def self.wait_for(gem_name, version, max_attempts: 40)
19
+ attempts = 0
20
+ loop do
21
+ sleep(attempts == 0 ? 5 : 15)
22
+ attempts += 1
23
+ versions = published_versions(gem_name)
24
+ return true if versions.include?(version)
25
+ puts "Waiting for v#{version} on RubyGems (attempt #{attempts}/#{max_attempts})..."
26
+ return false if attempts >= max_attempts
27
+ end
28
+ rescue => e
29
+ puts "RubyGems poll error: #{e.message}"
30
+ false
41
31
  end
42
32
  end
43
33
 
44
- # Find the latest version (prioritize release over pre for same patch)
45
- def latest_version(versions)
46
- versions.max_by do |v|
47
- p = parse_version(v)
48
- # Sort by major, minor, patch, then non-pre before pre, then pre_num
49
- [p[:major], p[:minor], p[:patch], p[:pre] ? 0 : 1, p[:pre_num]]
34
+ # ─── Version bumping ─────────────────────────────────────────────────────────
35
+
36
+ class Version
37
+ attr_reader :major, :minor, :patch, :pre, :pre_num
38
+
39
+ def self.parse(str)
40
+ parts = str.split(".")
41
+ pre = parts.length >= 5 && parts[-2] == "pre"
42
+ new(parts[0].to_i, parts[1].to_i, parts[2].to_i, pre, pre ? parts[-1].to_i : 0)
43
+ end
44
+
45
+ def self.latest(versions)
46
+ versions.max_by { |v| parse(v).sort_key }
50
47
  end
48
+
49
+ def self.bump(current_str, pre_release:)
50
+ v = parse(current_str)
51
+ if v.pre
52
+ pre_release ? "#{v.major}.#{v.minor}.#{v.patch}.pre.#{v.pre_num + 1}"
53
+ : "#{v.major}.#{v.minor}.#{v.patch}"
54
+ else
55
+ pre_release ? "#{v.major}.#{v.minor}.#{v.patch + 1}.pre.1"
56
+ : "#{v.major}.#{v.minor}.#{v.patch + 1}"
57
+ end
58
+ end
59
+
60
+ def initialize(major, minor, patch, pre, pre_num)
61
+ @major, @minor, @patch, @pre, @pre_num = major, minor, patch, pre, pre_num
62
+ end
63
+
64
+ def sort_key = [major, minor, patch, pre ? 0 : 1, pre_num]
51
65
  end
52
66
 
53
- versions = fetch_versions
54
- current = latest_version(versions)
55
- puts "Latest published version: #{current}"
67
+ # ─── Git ─────────────────────────────────────────────────────────────────────
56
68
 
57
- parts = current.split(?.)
58
- is_pre = parts.length >= 5 && parts[-2] == 'pre'
69
+ class Git
70
+ def self.current_branch = `git branch --show-current`.strip
71
+ def self.pending_changes? = !`git status --porcelain`.strip.empty?
72
+ def self.diff = `git diff HEAD`
73
+ def self.status = `git status --short`
59
74
 
60
- if is_pre
61
- major, minor, patch = parts[0..2]
62
- pre_num = parts[-1].to_i
63
- if pre_release
64
- # pre -> pre: increment pre number
65
- new_version = [major, minor, patch, 'pre', pre_num + 1].join(?.)
66
- else
67
- # pre -> release: remove pre, keep same version number
68
- new_version = [major, minor, patch].join(?.)
75
+ def self.ensure_up_to_date!
76
+ system("git", "fetch", "origin")
77
+ behind = `git rev-list HEAD..origin/main --count`.strip.to_i
78
+ if behind > 0
79
+ puts "ERROR: #{behind} commit(s) behind origin/main pull before publishing."
80
+ exit 1
81
+ end
69
82
  end
70
- else
71
- major, minor, patch = parts[0..2]
72
- if pre_release
73
- # release -> pre: increment patch and add pre.1
74
- new_version = [major, minor, patch.to_i + 1, 'pre', 1].join(?.)
75
- else
76
- # release -> release: increment patch
77
- new_version = [major, minor, patch.to_i + 1].join(?.)
83
+
84
+ def self.commits_since_last_tag
85
+ last_tag = `git describe --tags --abbrev=0 2>/dev/null`.strip
86
+ last_tag.empty? ? `git log --oneline` : `git log #{last_tag}..HEAD --oneline`
87
+ end
88
+
89
+ def self.stage_and_commit(files, message)
90
+ system("git", "reset", "HEAD")
91
+ files.each { |f| system("git", "add", "--", f) }
92
+ system("git", "commit", "-m", message)
93
+ puts " → #{message}"
94
+ end
95
+
96
+ def self.commit_all(message)
97
+ system("git", "add", "--all")
98
+ system("git", "commit", "-m", message)
99
+ end
100
+
101
+ def self.tag(version)
102
+ system("git", "tag", "-a", "v#{version}", "-m", "v#{version}")
103
+ puts "Tagged v#{version}"
104
+ end
105
+
106
+ def self.push
107
+ if system("git", "push", "origin", "main", "--follow-tags")
108
+ puts "Pushed to origin/main"
109
+ else
110
+ branch = "publish-v#{version}"
111
+ system("git", "checkout", "-b", branch)
112
+ system("git", "push", "origin", branch, "--follow-tags") \
113
+ ? puts("Push to main failed — pushed to origin/#{branch} instead")
114
+ : puts("ERROR: unable to push to origin")
115
+ system("git", "checkout", "main")
116
+ end
78
117
  end
79
118
  end
80
119
 
81
- puts "New version: #{new_version}"
120
+ # ─── Claude AI calls ─────────────────────────────────────────────────────────
82
121
 
83
- if has_pending_changes
84
- puts ""
85
- puts "Generating commit plan for pending changes..."
122
+ class Claude
123
+ MODEL = "claude-haiku-4-5-20251001"
86
124
 
87
- diff = `git diff HEAD`
88
- status = `git status --short`
125
+ Result = Struct.new(:commits, :changelog)
89
126
 
90
- commit_prompt = <<~PROMPT
91
- Analyze these git changes and create a logical commit plan.
127
+ # Single call: returns a Result with commit plan + updated changelog
128
+ def self.prepare_release(version:, diff:, status:, git_log:, existing_changelog:)
129
+ prompt = <<~PROMPT
130
+ You are preparing a release for the Ruby gem "hiiro" version #{version}.
131
+ Do two things and return them as a single JSON object — no explanation, no markdown fences.
92
132
 
93
- git status:
94
- #{status.strip}
133
+ ## 1. Commit plan
134
+ Analyze the pending git changes below and group them into logical commits.
135
+ Use conventional commit format (feat:, fix:, chore:, refactor:, docs:, etc.).
136
+ Only include files that appear in the git status output.
95
137
 
96
- git diff:
97
- #{diff.strip}
138
+ git status:
139
+ #{status.strip}
98
140
 
99
- Group related changes into logical commits. Output a JSON array only, no explanation, no markdown fences:
100
- [
101
- {"message": "commit message", "files": ["path/to/file"]},
102
- ...
103
- ]
141
+ git diff:
142
+ #{diff.strip}
104
143
 
105
- Use conventional commit format (feat:, fix:, chore:, refactor:, docs:, etc.).
106
- Only include files that appear in the status output above.
107
- PROMPT
144
+ ## 2. Changelog
145
+ Write a complete updated CHANGELOG.md. Add a new section for v#{version} at the top
146
+ with today's date. Group changes into Added, Changed, Fixed, Removed where appropriate.
147
+ Be concise.
108
148
 
109
- response = IO.popen(['claude', '-p', '--model', 'claude-haiku-4-5-20251001'], 'r+') { |io|
110
- io.write(commit_prompt)
111
- io.close_write
112
- io.read
113
- }
149
+ Recent commits (for changelog context):
150
+ #{git_log.strip}
114
151
 
115
- begin
116
- json_str = response.strip.gsub(/\A```(?:json)?\n?/, '').gsub(/\n?```\z/, '')
117
- commits = JSON.parse(json_str)
118
-
119
- system('git', 'add', '--all')
120
- commits.each do |commit|
121
- system('git', 'reset', 'HEAD')
122
- commit['files'].each { |f| system('git', 'add', '--', f) }
123
- system('git', 'commit', '-m', commit['message'])
124
- puts " #{commit['message']}"
125
- end
126
- rescue => e
127
- puts "Warning: could not parse claude commit plan (#{e.message}), committing all as one"
128
- system('git', 'add', '--all')
129
- system('git', 'commit', '-m', 'chore: pre-publish changes')
152
+ Existing CHANGELOG.md:
153
+ #{existing_changelog.empty? ? "(none)" : existing_changelog}
154
+
155
+ ## Output format (JSON only, no extra text)
156
+ {
157
+ "commits": [
158
+ {"message": "commit message", "files": ["path/to/file"]},
159
+ ...
160
+ ],
161
+ "changelog": "full CHANGELOG.md content here"
162
+ }
163
+ PROMPT
164
+
165
+ raw = IO.popen(["claude", "-p", "--model", MODEL], "r+") { |io|
166
+ io.write(prompt)
167
+ io.close_write
168
+ io.read
169
+ }.strip.gsub(/\A```(?:json)?\n?/, "").gsub(/\n?```\z/, "")
170
+
171
+ parsed = JSON.parse(raw)
172
+ Result.new(parsed["commits"], parsed["changelog"])
130
173
  end
131
174
  end
132
175
 
133
- puts ""
134
- puts "Generating CHANGELOG.md..."
135
- last_tag = `git describe --tags --abbrev=0 2>/dev/null`.strip
136
- commit_log = last_tag.empty? ? `git log --oneline` : `git log #{last_tag}..HEAD --oneline`
137
- existing_changelog = File.exist?('CHANGELOG.md') ? File.read('CHANGELOG.md') : ''
176
+ # ─── Gem publishing ──────────────────────────────────────────────────────────
138
177
 
139
- changelog_prompt = <<~PROMPT
140
- Update or create a CHANGELOG.md for the Ruby gem "hiiro" version #{new_version}.
178
+ class RubyGem
179
+ def self.build(version)
180
+ system("gem", "build", "hiiro.gemspec") || abort("ERROR: gem build failed")
181
+ end
141
182
 
142
- Recent commits (since #{last_tag.empty? ? 'the beginning' : last_tag}):
143
- #{commit_log.strip}
183
+ def self.push(version)
184
+ system("gem", "push", "hiiro-#{version}.gem")
185
+ end
186
+ end
144
187
 
145
- Existing CHANGELOG.md:
146
- #{existing_changelog.empty? ? '(none)' : existing_changelog}
188
+ # ─── Main ─────────────────────────────────────────────────────────────────────
147
189
 
148
- Write the complete updated CHANGELOG.md. Add a new section for v#{new_version} at the top
149
- with today's date. Group changes into Added, Changed, Fixed, Removed where appropriate.
150
- Be concise. Output only the file content, no explanation.
151
- PROMPT
190
+ pre_release = ARGV.include?("-t") || Git.current_branch != "main"
191
+ pending_before = Git.pending_changes?
152
192
 
153
- new_changelog = IO.popen(['claude', '-p', '--model', 'claude-haiku-4-5-20251001'], 'r+') { |io|
154
- io.write(changelog_prompt)
155
- io.close_write
156
- io.read
157
- }
193
+ Git.ensure_up_to_date!
158
194
 
159
- if new_changelog && !new_changelog.strip.empty?
160
- File.write('CHANGELOG.md', new_changelog)
161
- puts "Updated CHANGELOG.md"
162
- else
163
- puts "Warning: could not generate CHANGELOG.md"
164
- end
195
+ current_version = Version.latest(RubyGems.published_versions("hiiro"))
196
+ new_version = Version.bump(current_version, pre_release: pre_release)
197
+ puts "Latest: #{current_version} → New: #{new_version}"
198
+
199
+ puts "\nAsking Claude to plan commits and generate changelog..."
200
+ release = Claude.prepare_release(
201
+ version: new_version,
202
+ diff: pending_before ? Git.diff : "",
203
+ status: pending_before ? Git.status : "",
204
+ git_log: Git.commits_since_last_tag,
205
+ existing_changelog: File.exist?("CHANGELOG.md") ? File.read("CHANGELOG.md") : ""
206
+ )
165
207
 
166
- puts ""
167
- File.open('lib/hiiro/version.rb', 'w+') do |f|
168
- f.puts 'class Hiiro'
169
- f.puts " VERSION = #{new_version.inspect}"
170
- f.puts 'end'
208
+ if pending_before
209
+ begin
210
+ system("git", "add", "--all")
211
+ release.commits.each { |c| Git.stage_and_commit(c["files"], c["message"]) }
212
+ rescue => e
213
+ puts "Warning: could not apply Claude commit plan (#{e.message}) — committing all as one"
214
+ Git.commit_all("chore: pre-publish changes")
215
+ end
171
216
  end
172
217
 
173
- built = system('gem', 'build', 'hiiro.gemspec')
174
- puts "\nERROR: unable to build gem\n" unless built
218
+ File.write("CHANGELOG.md", release.changelog) if release.changelog && !release.changelog.strip.empty?
175
219
 
176
- puts ""
177
- pushed = system('gem', 'push', "hiiro-#{new_version}.gem")
178
- puts "\nERROR: unable to push\n" unless pushed
220
+ File.write("lib/hiiro/version.rb", "class Hiiro\n VERSION = #{new_version.inspect}\nend\n")
179
221
 
180
- puts ""
181
- system 'git', 'add', '--all'
182
- system 'git', 'commit', '-m', "publishing v#{new_version}"
222
+ RubyGem.build(new_version)
183
223
 
184
- system 'git', 'tag', '-a', "v#{new_version}", '-m', "v#{new_version}"
185
- puts "Tagged v#{new_version}"
224
+ unless RubyGem.push(new_version)
225
+ abort("ERROR: gem push failed — skipping git commit, tag, and push")
226
+ end
186
227
 
187
- puts ""
188
- # Try to push to origin main (tags follow via push.followTags), fallback to a branch if needed
189
- if system('git', 'push', 'origin', 'main', '--follow-tags')
190
- puts "Pushed to origin/main"
228
+ Git.commit_all("publishing v#{new_version}")
229
+ Git.tag(new_version)
230
+ Git.push
231
+
232
+ puts "\nWaiting for v#{new_version} to appear on RubyGems..."
233
+ if RubyGems.wait_for("hiiro", new_version)
234
+ puts "v#{new_version} available — running h update -a"
235
+ update_args = ["update", "-a"]
236
+ update_args << "--pre" if pre_release
237
+ system("h", *update_args)
191
238
  else
192
- branch_name = "publish-v#{new_version}"
193
- system 'git', 'checkout', '-b', branch_name
194
- if system('git', 'push', 'origin', branch_name, '--follow-tags')
195
- puts "Push to main failed. Pushed to origin/#{branch_name} instead"
196
- else
197
- puts "\nERROR: unable to push to origin\n"
198
- end
199
- system 'git', 'checkout', 'main'
239
+ puts "WARNING: timed out waiting for v#{new_version} on RubyGems — run `h update -a` manually"
200
240
  end
201
-
202
- puts ""
203
- puts "Running: h delayed_update #{new_version}"
204
- system('h', 'delayed_update', new_version)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.313
4
+ version: 0.1.314
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota