snakommit 0.1.1

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.
@@ -0,0 +1,472 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require 'tty-spinner'
5
+ require 'tty-screen'
6
+ require 'pastel'
7
+
8
+ module Snakommit
9
+ # Handles interactive prompts
10
+ class Prompt
11
+ class PromptError < StandardError; end
12
+
13
+ BANNER = "SNAKOMMIT - A commit manager"
14
+
15
+ def initialize
16
+ @config = Config.load
17
+ @git = Git.new
18
+ @tty_prompt = TTY::Prompt.new
19
+ @templates = Templates.new
20
+ @batch_processor = Performance::BatchProcessor.new(20) # Batch size optimized for file operations
21
+ @monitor = Performance::Monitor.new # Performance monitoring
22
+ validate_config
23
+ @pastel = Pastel.new
24
+ @width = TTY::Screen.width > 100 ? 100 : TTY::Screen.width
25
+ rescue => e
26
+ raise PromptError, "Failed to initialize prompt: #{e.message}"
27
+ end
28
+
29
+ # Main interactive commit flow
30
+ def commit_flow
31
+ return { error: 'Not in a Git repository' } unless Git.in_repo?
32
+
33
+ puts @pastel.cyan(BANNER)
34
+ puts "=" * BANNER.length
35
+
36
+ # Get the status of the repository with performance monitoring
37
+ repo_status = @monitor.measure(:repository_status) do
38
+ {
39
+ unstaged: @git.unstaged_files,
40
+ untracked: @git.untracked_files,
41
+ staged: @git.staged_files
42
+ }
43
+ end
44
+
45
+ unstaged = repo_status[:unstaged]
46
+ untracked = repo_status[:untracked]
47
+ staged = repo_status[:staged]
48
+
49
+ # Check if there are any changes to work with
50
+ if unstaged.empty? && untracked.empty? && staged.empty?
51
+ return { error: 'No changes detected in the repository' }
52
+ end
53
+
54
+ # Show file status
55
+ puts "\nRepository status:"
56
+ puts "- #{staged.length} file(s) staged for commit"
57
+ puts "- #{unstaged.length} file(s) modified but not staged"
58
+ puts "- #{untracked.length} untracked file(s)"
59
+
60
+ # Report performance if debug is enabled
61
+ if ENV['SNAKOMMIT_DEBUG']
62
+ puts "\nPerformance report:"
63
+ @monitor.report.each { |line| puts " #{line}" }
64
+ end
65
+
66
+ # Check for saved selections from a previous run
67
+ saved_selections = @git.get_saved_selections
68
+ if saved_selections && !saved_selections.empty?
69
+ puts "\nFound selections from a previous session."
70
+ if @tty_prompt.yes?("Would you like to use your previous file selections?", default: true)
71
+ # Stage the previously selected files
72
+ stage_files(@git, saved_selections)
73
+ else
74
+ # If user doesn't want to use previous selections, clear them
75
+ @git.clear_saved_selections
76
+ # Always show file selection to user
77
+ select_files(@git)
78
+ end
79
+ else
80
+ # Always show file selection to user
81
+ select_files(@git)
82
+ end
83
+
84
+ # Re-check staged files after selection
85
+ current_staged = @git.staged_files
86
+
87
+ # After file selection, check if we have staged files
88
+ if current_staged.empty?
89
+ return { error: 'No changes staged for commit. Please select files to commit.' }
90
+ end
91
+
92
+ # Divider
93
+ puts "\n" + "-" * 40
94
+
95
+ # Now get the commit info
96
+ commit_info = @monitor.measure(:get_commit_info) do
97
+ get_commit_info
98
+ end
99
+ return commit_info if commit_info[:error]
100
+
101
+ # Format the commit message
102
+ message = format_commit_message(commit_info)
103
+
104
+ # Show which files will be committed
105
+ puts "\nFiles to be committed:"
106
+ staged_for_commit = @git.staged_files
107
+ staged_for_commit.each do |file|
108
+ puts "- #{file}"
109
+ end
110
+
111
+ # Preview the commit message
112
+ puts "\nCommit message preview:"
113
+ puts "-" * 40
114
+ puts message
115
+ puts "-" * 40
116
+
117
+ # Confirm the commit
118
+ return { error: 'Commit aborted by user' } unless @tty_prompt.yes?('Do you want to proceed with this commit?', default: true)
119
+
120
+ # Commit the changes
121
+ spinner = TTY::Spinner.new("[:spinner] Committing changes... ", format: :dots)
122
+ spinner.auto_spin
123
+
124
+ @monitor.measure(:git_commit) do
125
+ @git.commit(message)
126
+ end
127
+
128
+ spinner.success("Changes committed successfully!")
129
+
130
+ # Final success message
131
+ puts "\n✓ Successfully committed: #{message.split("\n").first}"
132
+
133
+ # Show performance stats in debug mode
134
+ if ENV['SNAKOMMIT_DEBUG']
135
+ puts "\nFinal performance report:"
136
+ @monitor.report.each { |line| puts " #{line}" }
137
+ end
138
+
139
+ { success: true, message: message }
140
+ rescue Git::GitError => e
141
+ puts "\nError: #{e.message}"
142
+ { error: "Git error: #{e.message}" }
143
+ rescue => e
144
+ puts "\nError: #{e.message}"
145
+ { error: "Error during commit flow: #{e.message}" }
146
+ end
147
+
148
+ private
149
+
150
+ # Select files to add
151
+ def select_files(git)
152
+ begin
153
+ # Get file lists with performance monitoring
154
+ repo_status = @monitor.measure(:get_files_for_selection) do
155
+ {
156
+ unstaged: git.unstaged_files,
157
+ untracked: git.untracked_files,
158
+ staged: git.staged_files
159
+ }
160
+ end
161
+
162
+ unstaged = repo_status[:unstaged]
163
+ untracked = repo_status[:untracked]
164
+ staged = repo_status[:staged]
165
+
166
+ # Combine all files that could be staged
167
+ all_stageable_files = unstaged + untracked
168
+
169
+ # If there are no files to stage or unstage, return early
170
+ if all_stageable_files.empty? && staged.empty?
171
+ puts "No changes detected in the repository."
172
+ return
173
+ end
174
+
175
+ # If we only have staged files but nothing new to stage
176
+ if all_stageable_files.empty? && !staged.empty?
177
+ puts "\nCurrently staged files:"
178
+ staged.each { |file| puts "- #{file}" }
179
+
180
+ # Ask if user wants to unstage any files
181
+ if @tty_prompt.yes?("Do you want to unstage any files?", default: false)
182
+ unstage_files(git, staged)
183
+ end
184
+ return
185
+ end
186
+
187
+ # First show the user what's currently staged
188
+ unless staged.empty?
189
+ puts "\nCurrently staged files:"
190
+ staged.each { |file| puts "- #{file}" }
191
+ end
192
+
193
+ # Create options for the file selection menu
194
+ options = []
195
+
196
+ # Add "ALL FILES" option at the top if we have unstaged or untracked files
197
+ if !all_stageable_files.empty?
198
+ options << { name: "[ ALL FILES ]", value: :all_files }
199
+ end
200
+
201
+ # Add modified files with index numbers
202
+ unstaged.each_with_index do |file, idx|
203
+ options << { name: "#{idx+1}. Modified: #{file}", value: file }
204
+ end
205
+
206
+ # Add untracked files with continuing index numbers
207
+ untracked.each_with_index do |file, idx|
208
+ options << { name: "#{unstaged.length + idx + 1}. Untracked: #{file}", value: file }
209
+ end
210
+
211
+ # Skip if no options
212
+ if options.empty?
213
+ puts "No files available to stage."
214
+ return
215
+ end
216
+
217
+ # Prompt user to select files
218
+ puts "\nSelect files to stage for commit:"
219
+ selected = @tty_prompt.multi_select("Choose files (use space to select, enter to confirm):", options, per_page: 15, echo: true)
220
+
221
+ # Check if anything was selected
222
+ if selected.empty?
223
+ puts "No files selected for staging."
224
+
225
+ # If we already have staged files, ask if user wants to continue with those
226
+ unless staged.empty?
227
+ puts "You already have #{staged.length} file(s) staged."
228
+ return unless @tty_prompt.yes?("Do you want to select files again?", default: true)
229
+ return select_files(git) # Recursive call to try again
230
+ end
231
+
232
+ return
233
+ end
234
+
235
+ puts "\nSelected files to stage:"
236
+
237
+ # Handle "ALL FILES" option
238
+ if selected.include?(:all_files)
239
+ selected = all_stageable_files
240
+ puts "- All files (#{selected.length})"
241
+ else
242
+ selected.each { |file| puts "- #{file}" }
243
+ end
244
+
245
+ # Add a confirmation step
246
+ if @tty_prompt.yes?("Proceed with staging these files?", default: true)
247
+ # Stage the selected files
248
+ stage_files(git, selected)
249
+ else
250
+ puts "Staging canceled by user."
251
+ return
252
+ end
253
+
254
+ # After staging, check if the user wants to unstage any files
255
+ newly_staged = git.staged_files
256
+ unless newly_staged.empty?
257
+ if @tty_prompt.yes?("Do you want to unstage any files?", default: false)
258
+ unstage_files(git, newly_staged)
259
+ end
260
+ end
261
+ rescue TTY::Reader::InputInterrupt
262
+ puts "\nFile selection aborted. Press Ctrl+C again to exit completely or continue."
263
+ return
264
+ rescue => e
265
+ puts "\nError during file selection: #{e.message}"
266
+ puts "Would you like to try again?"
267
+ return if @tty_prompt.yes?("Try selecting files again?", default: true)
268
+ raise PromptError, "Failed to select files: #{e.message}"
269
+ end
270
+ end
271
+
272
+ # Stage the selected files
273
+ def stage_files(git, selected)
274
+ # Save the selections for future use
275
+ git.save_selections(selected)
276
+
277
+ spinner = TTY::Spinner.new("[:spinner] Adding files... ", format: :dots)
278
+ spinner.auto_spin
279
+
280
+ # Use batch processing for more efficient staging
281
+ @monitor.measure(:batch_stage_files) do
282
+ @batch_processor.process_files(selected) do |batch|
283
+ # Use parallel helper if available and appropriate
284
+ Performance::ParallelHelper.process(batch, threshold: 5) do |file|
285
+ git.add(file)
286
+ end
287
+ end
288
+ end
289
+
290
+ # Verify staging worked
291
+ newly_staged = git.staged_files
292
+ if newly_staged.empty?
293
+ spinner.error("Failed to stage files!")
294
+ puts "Warning: No files appear to be staged after add operation."
295
+ puts "This might be a Git or permission issue."
296
+ raise PromptError, "Failed to stage files"
297
+ else
298
+ spinner.success("Files added to staging area (#{newly_staged.length} file(s))")
299
+ end
300
+ rescue => e
301
+ raise PromptError, "Failed to stage files: #{e.message}"
302
+ end
303
+
304
+ # Unstage selected files
305
+ def unstage_files(git, staged_files)
306
+ return if staged_files.empty?
307
+
308
+ # Create options for unstaging
309
+ unstage_options = staged_files.map { |f| { name: f, value: f } }
310
+
311
+ # Unstage heading
312
+ puts "\nUnstage Files:"
313
+
314
+ # Select files to unstage
315
+ to_unstage = @tty_prompt.multi_select("Select files to unstage:", unstage_options, per_page: 15)
316
+
317
+ unless to_unstage.empty?
318
+ spinner = TTY::Spinner.new("[:spinner] Unstaging files... ", format: :dots)
319
+ spinner.auto_spin
320
+
321
+ # Use batch processing for more efficient unstaging
322
+ @monitor.measure(:batch_unstage_files) do
323
+ @batch_processor.process_files(to_unstage) do |batch|
324
+ batch.each { |file| git.reset(file) }
325
+ end
326
+ end
327
+
328
+ spinner.success("Files unstaged")
329
+ end
330
+ rescue => e
331
+ raise PromptError, "Failed to unstage files: #{e.message}"
332
+ end
333
+
334
+ # Get commit information
335
+ def get_commit_info
336
+ info = {}
337
+
338
+ puts "\nCommit Details:"
339
+
340
+ # Select commit type
341
+ info[:type] = select_type
342
+
343
+ # Enter scope (optional)
344
+ suggested_scopes = @config['scopes']
345
+ if suggested_scopes && !suggested_scopes.empty?
346
+ scope_options = suggested_scopes.map { |s| { name: s, value: s } }
347
+ scope_options << { name: '[none]', value: nil }
348
+
349
+ info[:scope] = @tty_prompt.select('Select the scope of this change (optional, press Enter to skip):',
350
+ scope_options,
351
+ per_page: 10)
352
+ else
353
+ info[:scope] = @tty_prompt.ask('Enter the scope of this change (optional, press Enter to skip):')
354
+ end
355
+
356
+ # Enter subject
357
+ info[:subject] = @tty_prompt.ask('Enter a short description:') do |q|
358
+ q.required true
359
+ q.validate(/^.{1,#{@config['max_subject_length']}}$/, "Subject must be less than #{@config['max_subject_length']} characters")
360
+ end
361
+
362
+ # Enter longer description (optional)
363
+ puts "Enter a longer description (optional, press Enter to skip):"
364
+ puts "Type your message and press Enter when done. Leave empty to skip."
365
+
366
+ body_lines = []
367
+ # Read input until an empty line is entered
368
+ loop do
369
+ line = @tty_prompt.ask("")
370
+ break if line.nil? || line.empty?
371
+ body_lines << line
372
+ end
373
+
374
+ info[:body] = body_lines
375
+
376
+ # Is this a breaking change?
377
+ info[:breaking] = @tty_prompt.yes?('Is this a breaking change?', default: false)
378
+
379
+ # Breaking change description
380
+ if info[:breaking]
381
+ info[:breaking_description] = @tty_prompt.ask('Enter breaking change description:') do |q|
382
+ q.required true
383
+ end
384
+ end
385
+
386
+ # Any issues closed?
387
+ if @tty_prompt.yes?('Does this commit close any issues?', default: false)
388
+ info[:issues] = @tty_prompt.ask('Enter issue references (e.g., "fix #123, close #456"):') do |q|
389
+ q.required true
390
+ end
391
+ end
392
+
393
+ info
394
+ rescue Interrupt
395
+ { error: 'Commit aborted' }
396
+ rescue => e
397
+ { error: "Failed to gather commit information: #{e.message}" }
398
+ end
399
+
400
+ # Format the commit message according to convention
401
+ def format_commit_message(info)
402
+ begin
403
+ header = ''
404
+
405
+ # Format the type and scope
406
+ commit_type = @templates.emoji_enabled? ? @templates.format_commit_type(info[:type]) : info[:type]
407
+
408
+ if info[:scope] && !info[:scope].empty?
409
+ header = "#{commit_type}(#{info[:scope]}): #{info[:subject]}"
410
+ else
411
+ header = "#{commit_type}: #{info[:subject]}"
412
+ end
413
+
414
+ # Format the body
415
+ body = info[:body].empty? ? '' : "\n\n#{info[:body].join("\n")}"
416
+
417
+ # Format breaking change
418
+ breaking = ''
419
+ if info[:breaking]
420
+ breaking = "\n\nBREAKING CHANGE: #{info[:breaking_description]}"
421
+ end
422
+
423
+ # Format issues
424
+ issues = info[:issues] ? "\n\n#{info[:issues]}" : ''
425
+
426
+ # Return the full commit message
427
+ message = "#{header}#{body}#{breaking}#{issues}"
428
+
429
+ message
430
+ rescue => e
431
+ raise PromptError, "Failed to format commit message: #{e.message}"
432
+ end
433
+ end
434
+
435
+ def select_type
436
+ commit_types = @config['types']
437
+
438
+ choices = commit_types.map do |type|
439
+ value = type['name']
440
+
441
+ # Format the display name based on emoji settings
442
+ if @templates.emoji_enabled?
443
+ emoji = @templates.get_emoji_for_type(value)
444
+ # Ensure there's a space between emoji and type
445
+ name = emoji ? "#{emoji} #{value}: #{type['description']}" : "#{value}: #{type['description']}"
446
+ else
447
+ name = "#{value}: #{type['description']}"
448
+ end
449
+
450
+ { name: name, value: value }
451
+ end
452
+
453
+ @tty_prompt.select('Choose a type:', choices, filter: true, per_page: 10)
454
+ end
455
+
456
+ # Validates the configuration loaded from file
457
+ def validate_config
458
+ unless @config.is_a?(Hash) && @config['types'].is_a?(Array)
459
+ raise PromptError, "Invalid configuration format: 'types' must be an array"
460
+ end
461
+
462
+ if @config['types'].empty?
463
+ raise PromptError, "Configuration error: No commit types defined"
464
+ end
465
+
466
+ # Ensure all required keys are present
467
+ @config['max_subject_length'] ||= 100
468
+ @config['max_body_line_length'] ||= 72
469
+ @config['scopes'] ||= []
470
+ end
471
+ end
472
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'fileutils'
5
+
6
+ module Snakommit
7
+ # Manages commit message formatting and emoji options
8
+ class Templates
9
+ class TemplateError < StandardError; end
10
+
11
+ # Emoji mappings for commit types
12
+ DEFAULT_EMOJI_MAP = {
13
+ 'feat' => '✨', # sparkles
14
+ 'fix' => '🐛', # bug
15
+ 'docs' => '📝', # memo
16
+ 'style' => '💄', # lipstick
17
+ 'refactor' => '♻️', # recycle
18
+ 'perf' => '⚡️', # zap
19
+ 'test' => '✅', # check mark
20
+ 'build' => '🔧', # wrench
21
+ 'ci' => '👷', # construction worker
22
+ 'chore' => '🔨', # hammer
23
+ 'revert' => '⏪️', # rewind
24
+ }.freeze
25
+
26
+ # Configuration file path
27
+ CONFIG_FILE = File.join(Snakommit.config_dir, 'emoji_config.yml')
28
+
29
+ def initialize
30
+ ensure_config_directory
31
+ @emoji_formatted_types = {} # Cache for formatted commit types
32
+
33
+ # Initialiser les valeurs par défaut avant de charger la configuration
34
+ @emoji_enabled = false
35
+ @emoji_map = DEFAULT_EMOJI_MAP.dup
36
+
37
+ load_config
38
+ end
39
+
40
+ def toggle_emoji(enable = nil)
41
+ return @emoji_enabled if enable.nil? && defined?(@emoji_enabled)
42
+
43
+ @emoji_enabled = enable.nil? ? !@emoji_enabled : enable
44
+ save_config
45
+ # Clear cached formatted types since the formatting changed
46
+ @emoji_formatted_types.clear
47
+ @emoji_enabled
48
+ end
49
+
50
+ def emoji_enabled?
51
+ @emoji_enabled
52
+ end
53
+
54
+ def format_commit_type(type)
55
+ return type unless @emoji_enabled
56
+ return type unless @emoji_map.key?(type)
57
+
58
+ # Check cache first
59
+ @emoji_formatted_types[type] ||= begin
60
+ emoji = @emoji_map[type]
61
+ "#{emoji} #{type}"
62
+ end
63
+ end
64
+
65
+ def get_emoji_for_type(type)
66
+ @emoji_map[type]
67
+ end
68
+
69
+ def list_emoji_mappings
70
+ @emoji_map.map { |type, emoji| { type: type, emoji: emoji } }
71
+ end
72
+
73
+ def update_emoji_mapping(type, emoji)
74
+ unless @emoji_map.key?(type)
75
+ raise TemplateError, "Unknown commit type: #{type}"
76
+ end
77
+
78
+ @emoji_map[type] = emoji
79
+ # Clear specific cached entry
80
+ @emoji_formatted_types.delete(type)
81
+ save_config
82
+ end
83
+
84
+ def reset_emoji_mappings
85
+ @emoji_map = DEFAULT_EMOJI_MAP.dup
86
+ # Clear all cached entries
87
+ @emoji_formatted_types.clear
88
+ save_config
89
+ end
90
+
91
+ private
92
+
93
+ def load_config
94
+ if File.exist?(CONFIG_FILE)
95
+ begin
96
+ load_existing_config
97
+ rescue => e
98
+ handle_config_error(e, "Failed to load")
99
+ initialize_default_config
100
+ end
101
+ else
102
+ initialize_default_config
103
+ end
104
+ end
105
+
106
+ def load_existing_config
107
+ config = YAML.load_file(CONFIG_FILE) || {}
108
+ @emoji_map = config['emoji_map'] || DEFAULT_EMOJI_MAP.dup
109
+ @emoji_enabled = !!config['emoji_enabled'] # Convert to boolean
110
+ end
111
+
112
+ def initialize_default_config
113
+ @emoji_map = DEFAULT_EMOJI_MAP.dup
114
+ @emoji_enabled = false
115
+ save_config
116
+ end
117
+
118
+ def save_config
119
+ config = {
120
+ 'emoji_map' => @emoji_map,
121
+ 'emoji_enabled' => @emoji_enabled
122
+ }
123
+
124
+ begin
125
+ ensure_config_directory
126
+ # Écrire le fichier en une seule opération
127
+ File.write(CONFIG_FILE, config.to_yaml)
128
+ rescue => e
129
+ handle_config_error(e, "Failed to save")
130
+ end
131
+ end
132
+
133
+ def handle_config_error(error, action_description)
134
+ warn "Warning: #{action_description} emoji config: #{error.message}"
135
+ @emoji_map ||= DEFAULT_EMOJI_MAP.dup
136
+ @emoji_enabled = false unless defined?(@emoji_enabled)
137
+ end
138
+
139
+ def ensure_config_directory
140
+ config_dir = File.dirname(CONFIG_FILE)
141
+ FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
142
+ rescue Errno::EACCES => e
143
+ warn "Warning: Permission denied creating config directory: #{e.message}"
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snakommit
4
+ VERSION = '0.1.1'
5
+ end
data/lib/snakommit.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load version first
4
+ require 'snakommit/version'
5
+
6
+ # Main module for the Snakommit application
7
+ module Snakommit
8
+ class Error < StandardError; end
9
+
10
+ # Returns the configuration directory path
11
+ # @return [String] Path to configuration directory
12
+ def self.config_dir
13
+ File.join(ENV['HOME'] || Dir.home, '.snakommit')
14
+ end
15
+
16
+ # Returns the current version string
17
+ # @return [String] Current version
18
+ def self.version
19
+ VERSION
20
+ end
21
+ end
22
+
23
+ # Now require the rest of the modules
24
+ # Core functionality
25
+ require 'snakommit/config'
26
+ require 'snakommit/git'
27
+
28
+ # User interface
29
+ require 'snakommit/prompt'
30
+ require 'snakommit/cli'
31
+
32
+ # Extensions
33
+ require 'snakommit/performance'
34
+ require 'snakommit/templates'
35
+ require 'snakommit/hooks'
data/snakommit.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ require_relative 'lib/snakommit/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "snakommit"
5
+ spec.version = Snakommit::VERSION
6
+ spec.authors = ["Antonia PL"]
7
+ spec.email = ["antonia.dev@icloud.com"]
8
+
9
+ spec.summary = "A high-performance, interactive commit manager tool similar to Commitizen"
10
+ spec.description = "Snakommit helps teams maintain consistent commit message formats by guiding developers through the process of creating standardized commit messages"
11
+ spec.homepage = "https://github.com/antonia-pl/snakommit"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "bin"
25
+ spec.executables = ["snakommit", "sk"]
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "tty-prompt", "~> 0.23.1"
29
+ spec.add_dependency "tty-spinner", "~> 0.9.3"
30
+ spec.add_dependency "tty-color", "~> 0.6.0"
31
+ spec.add_dependency "git", "~> 1.12"
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.0"
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rspec", "~> 3.10"
36
+ spec.add_development_dependency "rubocop", "~> 1.25.1"
37
+ spec.add_development_dependency "parallel", "~> 1.21"
38
+ end