ikuzo 1.0.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03a3e2b9c8aa4312b959d5cbffa4e6ba30097d85ca1a96fd5b406edb79aaa2ed
4
- data.tar.gz: f47271882fbd082d5c7a63462868178c7d6677d3613b82faaabae98162b69efd
3
+ metadata.gz: '099190e70536dbf6c718f5b10e3bbfe5e6422a8791357432b44f457c38d2eea6'
4
+ data.tar.gz: 13ae73024dbb2b86a6693731a9a767c284cb341a5f8a63533a96daf1825b7c83
5
5
  SHA512:
6
- metadata.gz: c66c00de06271d20e75791a4842cd856fa6f44ac011147e247a0e5061ac48f3efe6bd801049437280cacd959a7cbdd6431310ca140f13782cdfa069378f7ea5e
7
- data.tar.gz: 843fefb3a5b098b4ae53ce98df86477262415a38cc17c294e8047ed6294d8c2fc708ca3b28c90f1e1a453af28c8144f65ae7bb0d6330244659258e3f26a5dd6b
6
+ metadata.gz: 76e94226bf2a67848cbb1293f6b5c1f50c501af0f78f0372fac51b4439bf8d632309c21e054c0228dab87b7b862cc7c1f2098a0970e9c015ab7935ce1f51deb1
7
+ data.tar.gz: 4dc503e5b4c8788a2fde76772eb845e6d9d77ac426a94bd099563ced4ec3687147fae36e91aa501e8d4de731249ca0f60cb9c43930b47724e31a29c0deef65a7
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Ikuzo
1
+ # ikuzo 行くぞ (いくぞ)
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ikuzo.svg)](https://badge.fury.io/rb/ikuzo)
4
4
 
@@ -27,7 +27,7 @@ bundle install
27
27
  Outputs vary because messages are chosen at random. Ikuzo commits automatically; add `--no-commit` if you only want to print the message.
28
28
 
29
29
  Commit with a random message (default: feat):
30
-
30
+ ```
31
31
  $ ikuzo
32
32
  git commit -m "feat: main linted the vibes not the code"
33
33
  [main abc1234] feat: main linted the vibes not the code
@@ -35,13 +35,13 @@ git commit -m "feat: main linted the vibes not the code"
35
35
  ```
36
36
 
37
37
  Print a random message from a specific category without committing:
38
-
38
+ ```
39
39
  $ ikuzo funny --no-commit
40
40
  This commit was pair-programmed with caffeine.
41
41
  ```
42
42
 
43
43
  Commit with a random message explicitly (also default behavior):
44
-
44
+ ```
45
45
  $ ikuzo commit
46
46
  git commit -m "It compiled on my machine, scout's honor."
47
47
  [main abc1234] It compiled on my machine, scout's honor.
@@ -49,13 +49,13 @@ git commit -m "It compiled on my machine, scout's honor."
49
49
  ```
50
50
 
51
51
  Skip committing and just print the message:
52
-
52
+ ```
53
53
  $ ikuzo --no-commit
54
54
  feat: main linted the vibes not the code
55
55
  ```
56
56
 
57
57
  Specify a category explicitly and commit:
58
-
58
+ ```
59
59
  $ ikuzo --category dev
60
60
  git commit -m "Logs cleaned, metrics gleam."
61
61
  [main ghi9012] Logs cleaned, metrics gleam.
@@ -63,23 +63,33 @@ git commit -m "Logs cleaned, metrics gleam."
63
63
  ```
64
64
 
65
65
  Generate a Conventional Commit message from the current branch (omit `--no-commit` to auto commit):
66
-
66
+ ```
67
67
  $ ikuzo feat --no-commit
68
68
  feat: main no rubber duck was harmed in this fix
69
69
  ```
70
70
 
71
- Pick any Conventional Commit type that you need:
71
+ Append BTC or XAU pricing to the generated message (use `xau` or `gold`):
72
+ ```
73
+ $ ikuzo --price=btc --no-commit
74
+ fix: stack trace more like snack trace (BTC: $64,123.45)
72
75
 
76
+ $ ikuzo --price=xau --no-commit
77
+ chore: merge dreams into main (XAU: $2,045.67)
78
+ ```
79
+ > Pricing data comes from CoinGecko (BTC) and GoldPrice.org (XAU). If the lookup fails, Ikuzo keeps the original message without the price suffix. When Ruby cannot complete the HTTPS request, Ikuzo retries with `curl` if it is available. TLS certificate checks are disabled by default for CoinGecko requests; set `IKUZO_INSECURE_SSL=0` if you want to enforce verification instead.
80
+
81
+ Pick any Conventional Commit type that you need:
82
+ ```
73
83
  $ ikuzo fix
74
84
  git commit -m "fix: main stack trace more like snack trace"
75
85
  [main jkl3456] fix: main stack trace more like snack trace
76
86
  1 file changed, 1 insertion(+)
77
87
  ```
78
88
 
79
- Supported types align with the Conventional Commits 1.0.0 spec: build, ci, chore, docs, feat, fix, perf, refactor, revert, style, and test.
89
+ Supported types align with the Conventional Commits 1.1.0 spec: build, ci, chore, docs, feat, fix, perf, refactor, revert, style, and test.
80
90
 
81
91
  List available categories:
82
-
92
+ ```
83
93
  $ ikuzo --list-categories
84
94
  Available categories:
85
95
  - general
@@ -100,9 +110,9 @@ Available categories:
100
110
  ```
101
111
 
102
112
  Show the installed version:
103
-
113
+ ```
104
114
  $ ikuzo --version
105
- 1.0.0
115
+ 1.1.0
106
116
  ```
107
117
 
108
118
  Each Conventional Commit category builds a `<type>: ...` message from your current Git branch, appends a cleaned-up motivational quip, and falls back to a default subject if the branch cannot be detected.
data/lib/ikuzo/cli.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require "ikuzo/pricing"
4
5
 
5
6
  module Ikuzo
6
7
  # CLI entry point for generating random commit messages.
@@ -21,6 +22,7 @@ module Ikuzo
21
22
  end
22
23
 
23
24
  message = Messages.random(options[:category])
25
+ message = append_price(message, options[:price]) if options[:price]
24
26
  @stdout.puts(message)
25
27
 
26
28
  return exit_success unless options[:commit]
@@ -38,7 +40,8 @@ module Ikuzo
38
40
  options = {
39
41
  commit: true,
40
42
  list_categories: false,
41
- category: nil
43
+ category: nil,
44
+ price: nil
42
45
  }
43
46
 
44
47
  categories_text = Messages.categories.join(", ")
@@ -58,6 +61,10 @@ module Ikuzo
58
61
  options[:list_categories] = true
59
62
  end
60
63
 
64
+ option_parser.on("-pASSET", "--price=ASSET", "Append pricing for btc or xau (gold) to the message") do |asset|
65
+ options[:price] = normalize_price_asset(asset)
66
+ end
67
+
61
68
  option_parser.on("-v", "--version", "Print version") do
62
69
  @stdout.puts(Ikuzo::VERSION)
63
70
  exit_success
@@ -86,6 +93,27 @@ module Ikuzo
86
93
  end
87
94
  end
88
95
 
96
+ def normalize_price_asset(asset)
97
+ normalized = asset.to_s.strip.downcase
98
+ return :btc if %w[btc bitcoin].include?(normalized)
99
+ return :xau if normalized == "xau" || normalized == "gold"
100
+
101
+ raise OptionParser::InvalidArgument, "Unknown price asset '#{asset}'. Supported values: btc, xau."
102
+ end
103
+
104
+ def append_price(message, asset)
105
+ suffix = Pricing.suffix_for(asset)
106
+ unless suffix
107
+ @stderr.puts("Unable to fetch pricing for #{asset.to_s.upcase}; continuing without price.")
108
+ return message
109
+ end
110
+
111
+ "#{message} #{suffix}"
112
+ rescue StandardError => e
113
+ @stderr.puts("Failed to append #{asset.to_s.upcase} price: #{e.message}")
114
+ message
115
+ end
116
+
89
117
  def apply_positional_arguments(options)
90
118
  @argv.each do |arg|
91
119
  normalized = arg.to_s.strip.downcase
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "openssl"
7
+ require "open3"
8
+
9
+ module Ikuzo
10
+ # Fetches cryptocurrency and metal pricing information for use in commit messages.
11
+ class Pricing
12
+ DEFAULT_CURRENCY = "USD"
13
+ COINGECKO_ENDPOINT = "https://api.coingecko.com/api/v3/simple/price"
14
+
15
+ SUPPORTED_ASSETS = {
16
+ btc: { id: "bitcoin", label: "BTC", provider: :coingecko },
17
+ xau: { symbol: "XAU", label: "XAU", provider: :metal }
18
+ }.freeze
19
+
20
+ def self.suffix_for(asset, currency: DEFAULT_CURRENCY)
21
+ asset = asset.to_sym
22
+ return unless SUPPORTED_ASSETS.key?(asset)
23
+
24
+ price = fetch_price(asset, currency: currency)
25
+ return unless price
26
+
27
+ "#{suffix_prefix(asset)} #{format_amount(price, currency)}"
28
+ end
29
+
30
+ def self.fetch_price(asset, currency: DEFAULT_CURRENCY)
31
+ info = SUPPORTED_ASSETS.fetch(asset)
32
+ case info.fetch(:provider)
33
+ when :coingecko
34
+ fetch_coingecko_price(info.fetch(:id), currency)
35
+ when :metal
36
+ fetch_metal_price(currency)
37
+ else
38
+ nil
39
+ end
40
+ end
41
+ private_class_method :fetch_price
42
+
43
+ def self.fetch_coingecko_price(asset_id, currency)
44
+ response = request_coingecko_price(asset_id, currency: currency)
45
+ response = request_coingecko_price_via_curl(asset_id, currency: currency) if response.nil? && curl_available?
46
+ return unless response
47
+
48
+ asset_data = response[asset_id] || response[asset_id.to_sym]
49
+ return unless asset_data.is_a?(Hash)
50
+
51
+ lookup_key = currency.downcase
52
+ value = asset_data[lookup_key]
53
+ return unless value
54
+ Float(value)
55
+ rescue ArgumentError
56
+ nil
57
+ end
58
+ private_class_method :fetch_coingecko_price
59
+
60
+ def self.fetch_metal_price(currency)
61
+ return unless currency.upcase == DEFAULT_CURRENCY
62
+
63
+ uri = URI("https://data-asg.goldprice.org/dbXRates/#{currency.upcase}")
64
+ response = http_get(uri)
65
+ return unless response.is_a?(Net::HTTPSuccess)
66
+
67
+ data = JSON.parse(response.body)
68
+ items = data["items"]
69
+ return unless items.is_a?(Array) && !items.empty?
70
+
71
+ value = items.first["xauPrice"]
72
+ Float(value)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+ private_class_method :fetch_metal_price
77
+
78
+ def self.request_coingecko_price(asset_id, currency: DEFAULT_CURRENCY)
79
+ uri = URI(COINGECKO_ENDPOINT)
80
+ uri.query = URI.encode_www_form(ids: asset_id, vs_currencies: currency.downcase)
81
+
82
+ response = http_get(uri)
83
+ return unless response
84
+
85
+ return unless response.is_a?(Net::HTTPSuccess)
86
+
87
+ data = JSON.parse(response.body)
88
+ data
89
+ rescue StandardError
90
+ nil
91
+ end
92
+ private_class_method :request_coingecko_price
93
+
94
+ def self.request_coingecko_price_via_curl(asset_id, currency: DEFAULT_CURRENCY)
95
+ uri = URI(COINGECKO_ENDPOINT)
96
+ uri.query = URI.encode_www_form(ids: asset_id, vs_currencies: currency.downcase)
97
+
98
+ stdout, status = Open3.capture2("curl", "-sS", uri.to_s)
99
+ return nil unless status.success?
100
+
101
+ JSON.parse(stdout)
102
+ rescue StandardError
103
+ nil
104
+ end
105
+ private_class_method :request_coingecko_price_via_curl
106
+
107
+ def self.http_get(uri)
108
+ insecure = insecure_ssl?
109
+ response = perform_http_get(uri, verify: !insecure)
110
+ response
111
+ rescue OpenSSL::SSL::SSLError
112
+ unless insecure
113
+ return perform_http_get(uri, verify: false)
114
+ end
115
+ nil
116
+ rescue StandardError
117
+ nil
118
+ end
119
+ private_class_method :http_get
120
+
121
+ def self.perform_http_get(uri, verify:)
122
+ http = Net::HTTP.new(uri.host, uri.port)
123
+ http.use_ssl = uri.scheme == "https"
124
+ http.open_timeout = 5
125
+ http.read_timeout = 5
126
+ if http.use_ssl?
127
+ if verify
128
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
129
+ http.cert_store = default_cert_store
130
+ else
131
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
132
+ end
133
+ end
134
+
135
+ request = Net::HTTP::Get.new(uri)
136
+ request["User-Agent"] = USER_AGENT
137
+ request["Accept"] = "application/json"
138
+
139
+ http.request(request)
140
+ end
141
+ private_class_method :perform_http_get
142
+
143
+ USER_AGENT = "Ikuzo CLI/1.0"
144
+
145
+ def self.insecure_ssl?
146
+ ENV.fetch("IKUZO_INSECURE_SSL", "1") != "0"
147
+ end
148
+ private_class_method :insecure_ssl?
149
+
150
+ def self.default_cert_store
151
+ @default_cert_store ||= OpenSSL::X509::Store.new.tap do |store|
152
+ store.set_default_paths
153
+ end
154
+ end
155
+ private_class_method :default_cert_store
156
+
157
+ def self.curl_available?
158
+ @curl_available ||= system("command -v curl > /dev/null 2>&1")
159
+ end
160
+ private_class_method :curl_available?
161
+
162
+ def self.suffix_prefix(asset)
163
+ "(#{SUPPORTED_ASSETS.fetch(asset).fetch(:label)}:"
164
+ end
165
+ private_class_method :suffix_prefix
166
+
167
+ def self.format_amount(value, currency)
168
+ formatted_number = delimited(format("%.2f", value))
169
+ "#{currency_symbol(currency)}#{formatted_number})"
170
+ end
171
+ private_class_method :format_amount
172
+
173
+ def self.delimited(value)
174
+ integer, fraction = value.split(".")
175
+ integer_with_commas = integer.gsub(/(\d)(?=(\d{3})+(?!\d))/, "\\1,")
176
+ [integer_with_commas, fraction].compact.join(".")
177
+ end
178
+ private_class_method :delimited
179
+
180
+ def self.currency_symbol(currency)
181
+ currency.upcase == "USD" ? "$" : "#{currency.upcase} "
182
+ end
183
+ private_class_method :currency_symbol
184
+ end
185
+ end
data/lib/ikuzo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ikuzo
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ikuzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dang Quang Minh
@@ -54,6 +54,7 @@ files:
54
54
  - lib/ikuzo.rb
55
55
  - lib/ikuzo/cli.rb
56
56
  - lib/ikuzo/messages.rb
57
+ - lib/ikuzo/pricing.rb
57
58
  - lib/ikuzo/version.rb
58
59
  homepage: https://github.com/ojisanchamchi/ruby_ikuzo#readme
59
60
  licenses:
@@ -76,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
77
  - !ruby/object:Gem::Version
77
78
  version: '0'
78
79
  requirements: []
79
- rubygems_version: 3.6.9
80
+ rubygems_version: 3.6.7
80
81
  specification_version: 4
81
82
  summary: Generate convention-ready commit messages without overthinking them.
82
83
  test_files: []