ikuzo 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 341ee328303de83855560af0ab990e8c599d9fae811cf94de96e86057c83b26e
4
+ data.tar.gz: e062e1eb9fef1001223c9b144419e7e776d4f45e3e9d32d62d324aa7df08646b
5
+ SHA512:
6
+ metadata.gz: c178da9edd66460dbe86f01fc95478423f62c917ca5c62df24c3f87cd2b14fc9e1ef0222951d29bd8f3b097f5161d192880a66937fe1ac7b906953ed891cb881
7
+ data.tar.gz: 651e3429246c11d481bf39e57b629f968b09cc8be1f44958ff1aaca63c6f841783c4c1eda74f5535d4f7dc204719719c60977836456f6c94ada09878773f17af
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dang Quang Minh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Ikuzo
2
+
3
+ Ikuzo is a Ruby gem that generates short, motivational commit messages for developers. Use it as a CLI tool or as a library inside your scripts when you need an instant morale boost.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install ikuzo
9
+ ```
10
+
11
+ If you are developing locally:
12
+
13
+ ```bash
14
+ bundle install
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### CLI
20
+
21
+ Outputs vary because messages are chosen at random. Ikuzo commits automatically; add `--no-commit` if you only want to print the message.
22
+
23
+ Commit with a random message (default: feat):
24
+
25
+ $ ikuzo
26
+ git commit -m "feat: main linted the vibes not the code"
27
+ [main abc1234] feat: main linted the vibes not the code
28
+ 1 file changed, 1 insertion(+)
29
+ ```
30
+
31
+ Print a random message from a specific category without committing:
32
+
33
+ $ ikuzo funny --no-commit
34
+ This commit was pair-programmed with caffeine.
35
+ ```
36
+
37
+ Commit with a random message explicitly (also default behavior):
38
+
39
+ $ ikuzo commit
40
+ git commit -m "It compiled on my machine, scout's honor."
41
+ [main abc1234] It compiled on my machine, scout's honor.
42
+ 1 file changed, 1 insertion(+)
43
+ ```
44
+
45
+ Skip committing and just print the message:
46
+
47
+ $ ikuzo --no-commit
48
+ feat: main linted the vibes not the code
49
+ ```
50
+
51
+ Specify a category explicitly and commit:
52
+
53
+ $ ikuzo --category dev
54
+ git commit -m "Logs cleaned, metrics gleam."
55
+ [main ghi9012] Logs cleaned, metrics gleam.
56
+ 1 file changed, 1 insertion(+)
57
+ ```
58
+
59
+ Generate a Conventional Commit message from the current branch (omit `--no-commit` to auto commit):
60
+
61
+ $ ikuzo feat --no-commit
62
+ feat: main no rubber duck was harmed in this fix
63
+ ```
64
+
65
+ Pick any Conventional Commit type that you need:
66
+
67
+ $ ikuzo fix
68
+ git commit -m "fix: main stack trace more like snack trace"
69
+ [main jkl3456] fix: main stack trace more like snack trace
70
+ 1 file changed, 1 insertion(+)
71
+ ```
72
+
73
+ Supported types align with the Conventional Commits 1.0.0 spec: build, ci, chore, docs, feat, fix, perf, refactor, revert, style, and test.
74
+
75
+ List available categories:
76
+
77
+ $ ikuzo --list-categories
78
+ Available categories:
79
+ - general
80
+ - funny
81
+ - motivation
82
+ - dev
83
+ - build - Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
84
+ - ci - Changes to CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
85
+ - chore - Changes which don't affect source code or tests e.g. build process, auxiliary tools, libraries
86
+ - docs - Documentation only changes
87
+ - feat
88
+ - fix
89
+ - perf
90
+ - refactor
91
+ - revert
92
+ - style
93
+ - test
94
+ ```
95
+
96
+ Show the installed version:
97
+
98
+ $ ikuzo --version
99
+ 0.1.0
100
+ ```
101
+
102
+ 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.
103
+
104
+ ### Library
105
+
106
+ ```ruby
107
+ require "ikuzo"
108
+
109
+ message = Ikuzo.random
110
+ # Defaults to the feat category
111
+ puts message
112
+ ```
113
+
114
+ ## Development
115
+
116
+ After checking out the repo, run `bundle install` to install dependencies. You can run `bundle exec rake release` to build and release the gem.
117
+
118
+ ## License
119
+
120
+ MIT License © 2025 Dang Quang Minh
data/bin/ikuzo ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib_path = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
6
+
7
+ require "ikuzo"
8
+
9
+ Ikuzo::CLI.new(ARGV).run
data/ikuzo.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ require_relative "lib/ikuzo/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "ikuzo"
5
+ spec.version = Ikuzo::VERSION
6
+ spec.authors = ["Dang Quang Minh"]
7
+ spec.email = ["ojisanchamchi@gmail.com"]
8
+
9
+ spec.summary = "Generate fun, motivational commit messages."
10
+ spec.description = "Ikuzo delivers short, humorous, and motivational commit messages for developers via both CLI and library usage."
11
+ spec.homepage = "https://github.com/ojisanchamchi/ruby_ikuzo#readme"
12
+ spec.license = "MIT"
13
+
14
+ spec.required_ruby_version = ">= 2.7"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/ojisanchamchi/ruby_ikuzo"
18
+ spec.metadata["changelog_uri"] = "https://github.com/ojisanchamchi/ruby_ikuzo/releases"
19
+
20
+ spec.files = Dir.glob("lib/**/*.rb") + %w[README.md LICENSE ikuzo.gemspec bin/ikuzo]
21
+ spec.bindir = "bin"
22
+ spec.executables = ["ikuzo"]
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 2.0"
26
+ spec.add_development_dependency "rake", "~> 13.0"
27
+ end
data/lib/ikuzo/cli.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Ikuzo
6
+ # CLI entry point for generating random commit messages.
7
+ class CLI
8
+ def initialize(argv, stdout: $stdout, stderr: $stderr, kernel: Kernel)
9
+ @argv = argv.dup
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ @kernel = kernel
13
+ end
14
+
15
+ def run
16
+ options = parse_options
17
+
18
+ if options[:list_categories]
19
+ print_categories
20
+ return exit_success
21
+ end
22
+
23
+ message = Messages.random(options[:category])
24
+ @stdout.puts(message)
25
+
26
+ return exit_success unless options[:commit]
27
+
28
+ commit_with(message)
29
+ rescue OptionParser::ParseError => e
30
+ @stderr.puts(e.message)
31
+ @stderr.puts(option_parser.summarize.join)
32
+ exit_failure
33
+ end
34
+
35
+ private
36
+
37
+ def parse_options
38
+ options = {
39
+ commit: true,
40
+ list_categories: false,
41
+ category: nil
42
+ }
43
+
44
+ categories_text = Messages.categories.join(", ")
45
+ option_parser.on("-cCATEGORY", "--category=CATEGORY", "Filter by category (#{categories_text}). Default: #{Messages::DEFAULT_CATEGORY}") do |category|
46
+ options[:category] = category
47
+ end
48
+
49
+ option_parser.on("--commit", "Run `git commit -m` with the generated message") do
50
+ options[:commit] = true
51
+ end
52
+
53
+ option_parser.on("--no-commit", "Only print the message; skip running git commit") do
54
+ options[:commit] = false
55
+ end
56
+
57
+ option_parser.on("--list-categories", "List available message categories") do
58
+ options[:list_categories] = true
59
+ end
60
+
61
+ option_parser.on("-v", "--version", "Print version") do
62
+ @stdout.puts(Ikuzo::VERSION)
63
+ exit_success
64
+ end
65
+
66
+ option_parser.on("-h", "--help", "Show this help message") do
67
+ @stdout.puts(option_parser)
68
+ exit_success
69
+ end
70
+
71
+ option_parser.parse!(@argv)
72
+ apply_positional_arguments(options)
73
+ options
74
+ end
75
+
76
+ def option_parser
77
+ @option_parser ||= OptionParser.new do |opts|
78
+ opts.banner = "Usage: ikuzo [options] [category]"
79
+ opts.separator("")
80
+ opts.separator("Examples:")
81
+ opts.separator(" ikuzo")
82
+ opts.separator(" ikuzo funny")
83
+ opts.separator(" ikuzo feat --no-commit")
84
+ opts.separator(" ikuzo fix")
85
+ opts.separator(" ikuzo --category motivation --no-commit")
86
+ end
87
+ end
88
+
89
+ def apply_positional_arguments(options)
90
+ @argv.each do |arg|
91
+ normalized = arg.to_s.strip.downcase
92
+ next if normalized.empty?
93
+
94
+ if category_argument?(normalized) && options[:category].nil?
95
+ options[:category] = normalized
96
+ elsif normalized == "commit"
97
+ options[:commit] = true
98
+ elsif normalized == "no-commit" || normalized == "print"
99
+ options[:commit] = false
100
+ end
101
+ end
102
+ end
103
+
104
+ def category_argument?(value)
105
+ Messages.categories.include?(value.to_sym)
106
+ end
107
+
108
+ def commit_with(message)
109
+ success = @kernel.system("git", "commit", "-m", message)
110
+ return exit_success if success
111
+
112
+ @stderr.puts("git commit failed. Please ensure you're inside a Git repository with staged changes.")
113
+ exit_failure
114
+ end
115
+
116
+ def print_categories
117
+ @stdout.puts("Available categories:")
118
+ Messages.categories.each do |category|
119
+ description = Messages.conventional_type_description(category)
120
+ if description
121
+ @stdout.puts(" - #{category} - #{description}")
122
+ else
123
+ @stdout.puts(" - #{category}")
124
+ end
125
+ end
126
+ end
127
+
128
+ def exit_success
129
+ @kernel.exit(0)
130
+ end
131
+
132
+ def exit_failure
133
+ @kernel.exit(1)
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Ikuzo
6
+ # Provides random commit messages grouped by category.
7
+ class Messages
8
+ DEFAULT_CATEGORY = :feat
9
+ DEFAULT_SUBJECT_CATEGORY = :funny
10
+ DEFAULT_CONVENTIONAL_SUBJECT = "update functionality"
11
+
12
+ CONVENTIONAL_TYPES = {
13
+ build: "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
14
+ ci: "Changes to CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
15
+ chore: "Changes which don't affect source code or tests e.g. build process, auxiliary tools, libraries",
16
+ docs: "Documentation only changes",
17
+ feat: "A new feature",
18
+ fix: "A bug fix",
19
+ perf: "A change that improves performance",
20
+ refactor: "A change that neither fixes a bug nor adds a feature",
21
+ revert: "Revert something",
22
+ style: "Changes that do not affect the meaning of the code (formatting, missing semi-colons, etc.)",
23
+ test: "Adding missing tests or correcting existing tests"
24
+ }.freeze
25
+
26
+ STATIC_CATALOG = {
27
+ general: [
28
+ "Ship it! 🚀",
29
+ "Refactor today, thrive tomorrow.",
30
+ "Tests green, vibes high.",
31
+ "Merge dreams into main.",
32
+ "Less yak, more ship. 🪒",
33
+ "Deploy like nobody's watching.",
34
+ "Debugging wizard at work.",
35
+ "Automate the boring brilliance.",
36
+ "Feature flag? More like feature brag.",
37
+ "Keyboard fueled by optimism.",
38
+ "Pixels aligned, spirits lifted.",
39
+ "Compile confidence, execute courage.",
40
+ "Keep calm and push to main 🧘‍♂️",
41
+ "Documentation whispers success.",
42
+ "Breakpoints? Not today!",
43
+ "Commit like you mean it.",
44
+ "Sprint with a smile 🙂",
45
+ "Code, coffee, conquer.",
46
+ "Patching bugs with high-fives.",
47
+ "From TODO to ta-da!"
48
+ ].freeze,
49
+ funny: [
50
+ "It compiled on my machine, scout's honor.",
51
+ "Embracing the chaos-driven-dev methodology.",
52
+ "This commit was pair-programmed with caffeine.",
53
+ "¯\\_(ツ)_/¯ ship first, debug later.",
54
+ "Plot twist: it actually worked.",
55
+ "Stack trace? More like snack trace.",
56
+ "Linted the vibes, not the code.",
57
+ "Documenting bugs as future features.",
58
+ "No rubber duck was harmed in this fix.",
59
+ "Debugger? I barely know her!"
60
+ ].freeze,
61
+ motivation: [
62
+ "Great code starts with this commit.",
63
+ "Refine, refuel, release. 🔁",
64
+ "Iteration builds innovation.",
65
+ "Little wins power big releases.",
66
+ "Stay curious, ship bravely.",
67
+ "Progress is a pull request away.",
68
+ "Keep building, keep believing.",
69
+ "Quality in, confidence out.",
70
+ "Momentum loves momentum.",
71
+ "One more push toward greatness."
72
+ ].freeze,
73
+ dev: [
74
+ "Optimized and ready for prod traffic.",
75
+ "Refactoring debts into assets.",
76
+ "Test coverage just leveled up.",
77
+ "Unlocking feature flags with flair.",
78
+ "API contracts honored, scouts promise.",
79
+ "Latency drops, morale pops.",
80
+ "Logs cleaned, metrics gleam.",
81
+ "DevEx boosted, friction busted.",
82
+ "Main branch stays evergreen.",
83
+ "CI lights are greener than ever."
84
+ ].freeze
85
+ }.freeze
86
+
87
+ CONVENTIONAL_CATALOG = CONVENTIONAL_TYPES.each_with_object({}) do |(type, _), hash|
88
+ hash[type] = -> { conventional_message(type) }
89
+ end.freeze
90
+
91
+ CATALOG = STATIC_CATALOG.merge(CONVENTIONAL_CATALOG).freeze
92
+
93
+ def self.random(category = nil)
94
+ category = normalize_category(category)
95
+ candidates = messages_for(category)
96
+ return candidates.sample unless candidates.empty?
97
+
98
+ messages_for(DEFAULT_CATEGORY).sample
99
+ end
100
+
101
+ def self.categories
102
+ CATALOG.keys
103
+ end
104
+
105
+ def self.conventional_types
106
+ CONVENTIONAL_TYPES.keys
107
+ end
108
+
109
+ def self.conventional_type_description(type)
110
+ CONVENTIONAL_TYPES[type.to_sym] if type
111
+ end
112
+
113
+ def self.conventional_type?(value)
114
+ return false unless value
115
+
116
+ CONVENTIONAL_TYPES.key?(value.to_sym)
117
+ end
118
+
119
+ def self.messages_for(category)
120
+ category = normalize_category(category)
121
+ entry = CATALOG.fetch(category) { CATALOG.fetch(DEFAULT_CATEGORY) }
122
+ resolve_entry(entry)
123
+ end
124
+
125
+ def self.normalize_category(category)
126
+ return DEFAULT_CATEGORY unless category
127
+
128
+ sym = category.to_s.strip.downcase.to_sym
129
+ categories.include?(sym) ? sym : DEFAULT_CATEGORY
130
+ end
131
+
132
+ def self.resolve_entry(entry)
133
+ case entry
134
+ when Proc
135
+ value = entry.call
136
+ return resolve_entry(CATALOG.fetch(DEFAULT_CATEGORY)) if value.nil? || (value.respond_to?(:empty?) && value.empty?)
137
+
138
+ Array(value)
139
+ when Array
140
+ entry
141
+ else
142
+ Array(entry)
143
+ end
144
+ end
145
+ private_class_method :resolve_entry
146
+
147
+ def self.conventional_message(type)
148
+ subject_parts = [branch_subject, sanitized_default_subject].compact.reject(&:empty?)
149
+ subject = subject_parts.join(" ").strip
150
+ subject = DEFAULT_CONVENTIONAL_SUBJECT if subject.empty?
151
+ "#{type}: #{subject}"
152
+ end
153
+ private_class_method :conventional_message
154
+
155
+ def self.current_branch
156
+ stdout, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
157
+ return unless status.success?
158
+
159
+ branch = stdout.strip
160
+ return if branch.empty? || branch == "HEAD"
161
+
162
+ branch
163
+ rescue StandardError
164
+ nil
165
+ end
166
+ private_class_method :current_branch
167
+
168
+ def self.branch_subject
169
+ branch = current_branch
170
+ return unless branch
171
+
172
+ short_name = branch.split("/").last
173
+ sanitize_subject(short_name)
174
+ end
175
+ private_class_method :branch_subject
176
+
177
+ def self.sanitized_default_subject
178
+ source_category = STATIC_CATALOG.key?(DEFAULT_CATEGORY) ? DEFAULT_CATEGORY : DEFAULT_SUBJECT_CATEGORY
179
+ entry = STATIC_CATALOG[source_category] || STATIC_CATALOG[DEFAULT_SUBJECT_CATEGORY]
180
+ return unless entry
181
+
182
+ candidates = resolve_entry(entry)
183
+ message = candidates.sample
184
+ sanitized = sanitize_subject(message)
185
+ sanitized unless sanitized.empty?
186
+ end
187
+ private_class_method :sanitized_default_subject
188
+
189
+ def self.sanitize_subject(value)
190
+ return "" unless value
191
+
192
+ value
193
+ .to_s
194
+ .downcase
195
+ .gsub(/[^a-z0-9\s\-]/, " ")
196
+ .gsub(/\s+/, " ")
197
+ .strip
198
+ end
199
+ private_class_method :sanitize_subject
200
+ end
201
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ikuzo
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ikuzo.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ikuzo/version"
4
+ require "ikuzo/messages"
5
+ require "ikuzo/cli"
6
+
7
+ module Ikuzo
8
+ def self.random(category = nil)
9
+ Messages.random(category)
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ikuzo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dang Quang Minh
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: Ikuzo delivers short, humorous, and motivational commit messages for
41
+ developers via both CLI and library usage.
42
+ email:
43
+ - ojisanchamchi@gmail.com
44
+ executables:
45
+ - ikuzo
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE
50
+ - README.md
51
+ - bin/ikuzo
52
+ - ikuzo.gemspec
53
+ - lib/ikuzo.rb
54
+ - lib/ikuzo/cli.rb
55
+ - lib/ikuzo/messages.rb
56
+ - lib/ikuzo/version.rb
57
+ homepage: https://github.com/ojisanchamchi/ruby_ikuzo#readme
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/ojisanchamchi/ruby_ikuzo#readme
62
+ source_code_uri: https://github.com/ojisanchamchi/ruby_ikuzo
63
+ changelog_uri: https://github.com/ojisanchamchi/ruby_ikuzo/releases
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '2.7'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.6.9
79
+ specification_version: 4
80
+ summary: Generate fun, motivational commit messages.
81
+ test_files: []