Kobold 0.3.3 → 0.4.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +5 -0
  3. data/.rubocop.yml +21 -1
  4. data/.rules/bugs/untestable.md +27 -0
  5. data/.rules/changelog/2026-03/30/01.md +55 -0
  6. data/.rules/changelog/2026-03/30/02.md +27 -0
  7. data/.rules/changelog/2026-03/30/03.md +36 -0
  8. data/.rules/changelog/2026-03/30/04.md +48 -0
  9. data/.rules/changelog/2026-03/30/05.md +19 -0
  10. data/.rules/changelog/2026-03/30/06.md +16 -0
  11. data/.rules/changelog/2026-03/30/07.md +28 -0
  12. data/.rules/changelog/2026-03/30/08.md +29 -0
  13. data/.rules/changelog/2026-03/30/09.md +33 -0
  14. data/.rules/changelog/2026-03/30/10.md +12 -0
  15. data/.rules/changelog/2026-03/30/11.md +47 -0
  16. data/.rules/changelog/2026-03/30/12.md +18 -0
  17. data/.rules/changelog/2026-03/30/13.md +36 -0
  18. data/.rules/changelog/2026-03/30/14.md +13 -0
  19. data/.rules/changelog/2026-03/30/15.md +24 -0
  20. data/.rules/default/rubocop.md +228 -0
  21. data/.rules/docs/kobold_api.md +491 -0
  22. data/README.md +131 -29
  23. data/Rakefile +19 -2
  24. data/exe/kobold +3 -57
  25. data/lib/Kobold/cli/admin_commands.rb +124 -0
  26. data/lib/Kobold/cli/checkout_commands.rb +73 -0
  27. data/lib/Kobold/cli/error_handling.rb +50 -0
  28. data/lib/Kobold/cli/flag_parser.rb +109 -0
  29. data/lib/Kobold/cli/init_commands.rb +108 -0
  30. data/lib/Kobold/cli/lifecycle_commands.rb +116 -0
  31. data/lib/Kobold/cli/list_commands.rb +80 -0
  32. data/lib/Kobold/cli/output.rb +40 -0
  33. data/lib/Kobold/cli/repo_commands.rb +101 -0
  34. data/lib/Kobold/cli/update_commands.rb +71 -0
  35. data/lib/Kobold/cli.rb +120 -0
  36. data/lib/Kobold/config.rb +136 -0
  37. data/lib/Kobold/database.rb +169 -0
  38. data/lib/Kobold/errors.rb +59 -0
  39. data/lib/Kobold/fetch.rb +19 -0
  40. data/lib/Kobold/git_ops.rb +162 -0
  41. data/lib/Kobold/init.rb +17 -13
  42. data/lib/Kobold/invoke.rb +12 -192
  43. data/lib/Kobold/linker.rb +87 -0
  44. data/lib/Kobold/manager/checkout.rb +78 -0
  45. data/lib/Kobold/manager/cleaning.rb +47 -0
  46. data/lib/Kobold/manager/fetching.rb +58 -0
  47. data/lib/Kobold/manager/invoking.rb +67 -0
  48. data/lib/Kobold/manager/lifecycle.rb +133 -0
  49. data/lib/Kobold/manager/registration.rb +32 -0
  50. data/lib/Kobold/manager.rb +140 -0
  51. data/lib/Kobold/repo/worktree_helpers.rb +56 -0
  52. data/lib/Kobold/repo.rb +135 -0
  53. data/lib/Kobold/settings.rb +103 -0
  54. data/lib/Kobold/version.rb +2 -2
  55. data/lib/Kobold.rb +14 -13
  56. data/prototyping/.kobold +19 -24
  57. data/sample-project-ideas/.kobold +19 -27
  58. data/sig/Kobold.rbs +217 -1
  59. metadata +60 -59
  60. data/lib/Kobold/first_time_setup.rb +0 -14
  61. data/lib/Kobold/read_config.rb +0 -15
data/README.md CHANGED
@@ -1,35 +1,137 @@
1
1
  # Kobold
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Kobold is a Git-based package manager for libraries and projects. It clones repositories into a local cache, creates Git worktrees for specific branches or commits, and symlinks them into your project directories. Deduplication is automatic - the same repo is only stored once regardless of how many projects reference it.
4
+
5
+ Kobold provides both a **Ruby API** for programmatic use and a **CLI** for standalone command-line use.
6
+
7
+ ---
8
+
9
+ ### CLI Commands
10
+
11
+ **init** — Create a new `.kobold` config file in the current directory.
12
+ ```
13
+ kobold init
14
+ kobold init --repo raysan5/raylib --branch master
15
+ kobold init --repo raysan5/raylib --source https://gitlab.com --branch master
16
+ ```
17
+
18
+ **invoke** — Process a `.kobold` file: clone repos, create worktrees, and symlink them into the project.
19
+ ```
20
+ kobold invoke
21
+ kobold invoke --config path/to/project/
22
+ ```
23
+
24
+ **fetch** — Fetch updates for registered repositories.
25
+ ```
26
+ kobold fetch
27
+ kobold fetch raysan5/raylib
28
+ ```
29
+
30
+ **register** — Register a repository in the database cache. Source defaults to `https://github.com`.
31
+ ```
32
+ kobold register raysan5/raylib
33
+ kobold register raysan5/raylib --source https://gitlab.com
34
+ ```
35
+
36
+ **unregister** — Remove a repository from the database registry.
37
+ ```
38
+ kobold unregister raysan5/raylib
39
+ ```
40
+
41
+ **checkout** — Create a worktree for a repo and optionally symlink it.
42
+ ```
43
+ kobold checkout raysan5/raylib --branch master --dir external/raylib
44
+ ```
45
+
46
+ **branch** — Create a new branch in a cached repo with a worktree.
47
+ ```
48
+ kobold branch raysan5/raylib --name my-feature --from main --dir /tmp/workspace
49
+ ```
50
+
51
+ **list** — List registered repos, worktrees, or branches.
52
+ ```
53
+ kobold list repos
54
+ kobold list worktrees raysan5/raylib
55
+ kobold list branches raysan5/raylib
56
+ ```
57
+
58
+ **add** — Add a dependency to an existing `.kobold` file. Source defaults to `https://github.com`.
59
+ ```
60
+ kobold add --repo raysan5/raylib --branch master --dir external/
61
+ kobold add --repo raysan5/raylib --source https://gitlab.com --branch master
62
+ ```
63
+
64
+ **remove** — Remove a dependency from a `.kobold` file.
65
+ ```
66
+ kobold remove raylib
67
+ kobold remove raylib --cleanup
68
+ ```
69
+
70
+ **update** — Update dependency commit SHAs to the latest on their branch.
71
+ ```
72
+ kobold update raylib
73
+ kobold update
74
+ ```
75
+
76
+ **clean** — Remove stale worktrees and optionally purge unreferenced repos.
77
+ ```
78
+ kobold clean
79
+ kobold clean --purge --config .
80
+ ```
81
+
82
+ **db** — Manage databases (isolated cache directories).
83
+ ```
84
+ kobold db create my-session
85
+ kobold db delete my-session
86
+ kobold db list
87
+ ```
88
+
89
+ **config** — View or change global settings like the default source URL.
90
+ ```
91
+ kobold config get-source
92
+ kobold config set-source https://gitlab.com
93
+ ```
94
+
95
+ **version** — Print version info.
96
+ ```
97
+ kobold version
98
+ ```
99
+
100
+ Use `--database NAME` before any command to operate on a specific database (default: `default`).
101
+
102
+ ```
103
+ kobold --database orchestrator-session-1 invoke
104
+ ```
105
+
106
+ ---
107
+
108
+ ### Building and Installing Locally
109
+
110
+ Clone the repository and install dependencies:
111
+
112
+ ```
113
+ git clone https://github.com/CatsAtTheRodeo/Kobold.git
114
+ cd Kobold
115
+ bundle install
116
+ ```
117
+
118
+ Build the gem:
119
+
120
+ ```
121
+ gem build Kobold.gemspec
122
+ ```
123
+
124
+ Install the built gem:
125
+
126
+ ```
127
+ gem install Kobold-0.4.0.gem
128
+ ```
4
129
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/Kobold`. To experiment with that code, run `bin/console` for an interactive prompt.
130
+ The `kobold` executable will be available on your `PATH` after installation.
131
+
132
+ ---
6
133
 
7
- ## Installation
8
-
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
- Install the gem and add to the application's Gemfile by executing:
12
-
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
-
15
- If bundler is not being used to manage dependencies, install the gem by executing:
16
-
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
-
19
- ## Usage
20
-
21
- TODO: Write usage instructions here
22
-
23
- ## Development
24
-
25
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
-
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
-
29
- ## Contributing
30
-
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/Kobold.
32
-
33
- ## License
134
+ ### License
34
135
 
35
136
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
137
+
data/Rakefile CHANGED
@@ -2,7 +2,24 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
6
+ require "fileutils"
5
7
 
6
- RuboCop::RakeTask.new
8
+ RuboCop::RakeTask.new do |task|
9
+ task.options = [
10
+ "--format", "progress",
11
+ "--format", "html", "-o", "tmp/reports/rubocop_results.html"
12
+ ]
13
+ end
7
14
 
8
- task default: :rubocop
15
+ RSpec::Core::RakeTask.new(:spec)
16
+
17
+ # Ensure reports directory exists before running tasks
18
+ task :setup_reports do
19
+ FileUtils.mkdir_p("tmp/reports")
20
+ end
21
+
22
+ task spec: :setup_reports
23
+ task rubocop: :setup_reports
24
+
25
+ task default: %i[spec rubocop]
data/exe/kobold CHANGED
@@ -1,60 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require 'Kobold'
4
- require 'tty-option'
5
- #require 'pry'
4
+ require "Kobold"
6
5
 
7
- module Kobold
8
- class Command
9
- include TTY::Option
10
-
11
- usage do
12
- program "kobold"
13
-
14
- commands "init", "invoke"
15
-
16
- end
17
-
18
- argument :command do
19
- optional
20
- desc "Accepts commands for Kobold to execute."
21
- end
22
-
23
- flag :help do
24
- short "-h"
25
- long "--help"
26
- desc "Print usage."
27
- end
28
-
29
- def run
30
- if params[:help]
31
- print help
32
- elsif params.errors.any?
33
- puts params.errors.summary
34
- else
35
- #pp params.to_h
36
- end
37
- end
38
- end
39
- end
40
-
41
-
42
- cmd = Kobold::Command.new
43
- parse = cmd.parse
44
- run = cmd.run
45
-
46
- if cmd.params[:command] == nil || cmd.params == "invoke"
47
- Kobold.invoke
48
- else
49
- case cmd.params[:command]
50
- when "add"
51
- when "remove"
52
- when "update"
53
- when "list"
54
- Kobold.list
55
- when "init"
56
- Kobold.init
57
- else
58
- print cmd.help
59
- end
60
- end
6
+ Kobold::CLI.new(ARGV).run
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobold
4
+ class CLI
5
+ # Database and config management commands.
6
+ module AdminCommands
7
+ private
8
+
9
+ def cmd_db
10
+ sub, remaining = extract_positional(@args)
11
+ name_arg, remaining = extract_positional(remaining) if sub
12
+ opts = parse_flags(remaining || [], {}, {})
13
+ return show_db_help_text(sub) if opts[:help] || sub.nil?
14
+
15
+ dispatch_db(sub, name_arg)
16
+ end
17
+
18
+ DB_HELP = <<~HELP
19
+ Usage: kobold db <subcommand> [NAME]
20
+
21
+ Manage databases.
22
+
23
+ Subcommands:
24
+ create NAME Create a new named database
25
+ delete NAME Delete a named database and all its contents
26
+ list List all databases
27
+
28
+ Options:
29
+ -h, --help Show this help
30
+ HELP
31
+
32
+ def show_db_help_text(sub)
33
+ puts DB_HELP
34
+ sub
35
+ end
36
+
37
+ def dispatch_db(sub, name_arg)
38
+ case sub
39
+ when "create" then db_create(name_arg)
40
+ when "delete" then db_delete(name_arg)
41
+ when "list" then db_list
42
+ else
43
+ error "Unknown db subcommand: #{sub}"
44
+ hint "Available: create, delete, list"
45
+ exit EXIT_USAGE
46
+ end
47
+ end
48
+
49
+ def db_create(name_arg)
50
+ require_arg!(name_arg, "NAME", "kobold db create NAME")
51
+ result = Manager.create_database(name_arg)
52
+ success "Created database '#{result[:name]}' at #{result[:path]}"
53
+ end
54
+
55
+ def db_delete(name_arg)
56
+ require_arg!(name_arg, "NAME", "kobold db delete NAME")
57
+ result = Manager.delete_database(name_arg)
58
+ success "Deleted database '#{result[:name]}'"
59
+ end
60
+
61
+ def db_list
62
+ databases = Manager.list_databases
63
+ if databases.empty?
64
+ info "No databases found."
65
+ else
66
+ puts "Databases:"
67
+ databases.each do |name|
68
+ marker = name == "default" ? " (default)" : ""
69
+ puts " #{name}#{marker}"
70
+ end
71
+ end
72
+ end
73
+
74
+ def cmd_config
75
+ sub, remaining = extract_positional(@args)
76
+ value_arg, remaining = extract_positional(remaining) if sub
77
+ opts = parse_flags(remaining || [], {}, {})
78
+ return show_config_help_text(sub) if opts[:help] || sub.nil?
79
+
80
+ dispatch_config(sub, value_arg)
81
+ end
82
+
83
+ CONFIG_HELP = <<~HELP
84
+ Usage: kobold config <subcommand> [VALUE]
85
+
86
+ View and change global settings.
87
+
88
+ Subcommands:
89
+ get-source Show the default source URL
90
+ set-source URL Set the default source URL
91
+
92
+ Options:
93
+ -h, --help Show this help
94
+ HELP
95
+
96
+ def show_config_help_text(sub)
97
+ puts CONFIG_HELP
98
+ sub
99
+ end
100
+
101
+ def dispatch_config(sub, value_arg)
102
+ case sub
103
+ when "get-source" then puts "Default source: #{Settings.default_source}"
104
+ when "set-source" then config_set_source(value_arg)
105
+ else
106
+ error "Unknown config subcommand: #{sub}"
107
+ hint "Available: get-source, set-source"
108
+ exit EXIT_USAGE
109
+ end
110
+ end
111
+
112
+ def config_set_source(value_arg)
113
+ require_arg!(value_arg, "URL", "kobold config set-source https://github.com")
114
+ Settings.default_source = value_arg
115
+ success "Default source set to: #{value_arg}"
116
+ end
117
+
118
+ def cmd_version
119
+ puts "Kobold: #{VERSION}"
120
+ puts "Kobold Format: #{FORMAT_VERSION}"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobold
4
+ class CLI
5
+ # Checkout and branch creation commands.
6
+ module CheckoutCommands
7
+ private
8
+
9
+ def cmd_checkout
10
+ repo_arg, remaining = extract_positional(@args)
11
+ opts = parse_flags(remaining, {}, { "branch" => nil, "commit" => nil, "label" => nil, "dir" => nil })
12
+ return puts(CHECKOUT_HELP) if opts[:help]
13
+
14
+ require_arg!(repo_arg, "REPO", "kobold checkout REPO [--branch NAME] [--commit SHA] [--dir PATH]")
15
+
16
+ result = manager.checkout(
17
+ repo_arg, branch: opts["branch"], commit: opts["commit"], label: opts["label"], dir: opts["dir"]
18
+ )
19
+ success "Worktree created at #{result[:worktree_path]}"
20
+ info "Symlinked to #{result[:symlink_path]}" if result[:symlink_path]
21
+ end
22
+
23
+ CHECKOUT_HELP = <<~HELP
24
+ Usage: kobold checkout REPO [options]
25
+
26
+ Create a worktree for a repo and optionally symlink it.
27
+
28
+ Arguments:
29
+ REPO Repository identifier (e.g. 'raysan5/raylib')
30
+
31
+ Options:
32
+ --branch NAME Branch to check out
33
+ --commit SHA Commit SHA to check out
34
+ --label NAME Label for the worktree (requires --commit)
35
+ --dir PATH Destination path for a symlink
36
+ -h, --help Show this help
37
+ HELP
38
+
39
+ def cmd_branch
40
+ repo_arg, remaining = extract_positional(@args)
41
+ opts = parse_flags(remaining, { "name" => :required }, { "from" => "main", "dir" => nil })
42
+ return puts(BRANCH_HELP) if opts[:help]
43
+
44
+ require_arg!(repo_arg, "REPO", "kobold branch REPO --name BRANCH [--from REF] [--dir PATH]")
45
+ require_option!(opts["name"], "--name", "kobold branch #{repo_arg} --name my-branch")
46
+
47
+ result = manager.create_branch(repo_arg, name: opts["name"], from: opts["from"], dir: opts["dir"])
48
+ display_branch_result(result)
49
+ end
50
+
51
+ BRANCH_HELP = <<~HELP
52
+ Usage: kobold branch REPO --name BRANCH [options]
53
+
54
+ Create a new branch in a cached repo with a worktree.
55
+
56
+ Arguments:
57
+ REPO Repository identifier (e.g. 'raysan5/raylib')
58
+
59
+ Options:
60
+ --name BRANCH Name for the new branch (required)
61
+ --from REF Source branch/ref to branch from (default: 'main')
62
+ --dir PATH Destination path for a symlink
63
+ -h, --help Show this help
64
+ HELP
65
+
66
+ def display_branch_result(result)
67
+ success "Branch '#{result[:branch]}' created for #{result[:slug]}"
68
+ info "Worktree at #{result[:worktree_path]}"
69
+ info "Symlinked to #{result[:symlink_path]}" if result[:symlink_path]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobold
4
+ class CLI
5
+ # Error handling dispatch table for CLI.
6
+ module ErrorHandling
7
+ ERROR_HANDLERS = [
8
+ [Errors::InvalidFormat, ->(e) { CLI.report_format_error(e) }],
9
+ [Errors::ConfigError, ->(e) { CLI.report_error("Config error: #{e.message}") }],
10
+ [Errors::RepoNotFound, ->(e) { CLI.report_with_hint(e.message, "Run 'kobold register' first.") }],
11
+ [Errors::WorktreeExists, ->(e) { CLI.report_error(e.message) }],
12
+ [Errors::CloneError, ->(e) { CLI.report_with_hint(e.message, "Check URL and network.") }],
13
+ [Errors::FetchError, ->(e) { CLI.report_with_hint(e.message, "Check network and retry.") }],
14
+ [Errors::GitError, ->(e) { CLI.report_error("Git error: #{e.message}") }],
15
+ [Errors::DatabaseNotFound, ->(e) { CLI.report_error(e.message) }],
16
+ [Errors::DatabaseExists, ->(e) { CLI.report_error(e.message) }],
17
+ [Errors::DependencyNotFound, ->(e) { CLI.report_with_hint(e.message, "Check dependency name.") }],
18
+ [Errors::LinkError, ->(e) { CLI.report_error("Link error: #{e.message}") }]
19
+ ].freeze
20
+
21
+ private
22
+
23
+ def handle_error(exception)
24
+ handler = find_error_handler(exception)
25
+ return raise unless handler
26
+
27
+ handler.call(exception)
28
+ exit EXIT_ERROR
29
+ end
30
+
31
+ def find_error_handler(exception)
32
+ ERROR_HANDLERS.find { |klass, _| exception.is_a?(klass) }&.last
33
+ end
34
+ end
35
+
36
+ def self.report_error(msg)
37
+ $stderr.puts "\e[31m\u2717\e[0m #{msg}" # rubocop:disable Style/StderrPuts
38
+ end
39
+
40
+ def self.report_format_error(err)
41
+ report_error("Format error: #{err.message}")
42
+ $stderr.puts " \e[90mYour .kobold file may need format version #{FORMAT_VERSION}.\e[0m" # rubocop:disable Style/StderrPuts
43
+ end
44
+
45
+ def self.report_with_hint(msg, hint_msg)
46
+ report_error(msg)
47
+ $stderr.puts " \e[90m#{hint_msg}\e[0m" # rubocop:disable Style/StderrPuts
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Kobold
6
+ class CLI
7
+ # Global and subcommand flag parsing.
8
+ module FlagParser
9
+ private
10
+
11
+ def parse_global_flags
12
+ state = { remaining: [], seen_command: false, i: 0 }
13
+ process_global_args(state) while state[:i] < @argv.length
14
+ @command = state[:remaining].shift
15
+ @args = state[:remaining]
16
+ end
17
+
18
+ def process_global_args(state)
19
+ arg = @argv[state[:i]]
20
+ if state[:seen_command]
21
+ handle_post_command_arg(arg, state)
22
+ else
23
+ handle_global_arg(arg, state)
24
+ end
25
+ state[:i] += 1
26
+ end
27
+
28
+ def handle_post_command_arg(arg, state)
29
+ case arg
30
+ when "--database" then @database = consume_next_arg(state, "--database")
31
+ when /\A--database=(.+)\z/ then @database = Regexp.last_match(1)
32
+ else state[:remaining] << arg
33
+ end
34
+ end
35
+
36
+ def handle_global_arg(arg, state)
37
+ case arg
38
+ when "--database" then @database = consume_next_arg(state, "--database")
39
+ when /\A--database=(.+)\z/ then @database = Regexp.last_match(1)
40
+ when "-h", "--help" then print_help_and_exit
41
+ else
42
+ state[:remaining] << arg
43
+ state[:seen_command] = true unless arg.start_with?("-")
44
+ end
45
+ end
46
+
47
+ def print_help_and_exit
48
+ print_help
49
+ exit EXIT_SUCCESS
50
+ end
51
+
52
+ def consume_next_arg(state, flag_name)
53
+ state[:i] += 1
54
+ if state[:i] >= @argv.length
55
+ error "Missing value for #{flag_name}"
56
+ exit EXIT_USAGE
57
+ end
58
+ @argv[state[:i]]
59
+ end
60
+
61
+ # Simple flag parser for subcommand options.
62
+ def parse_flags(argv, _required, defaults, boolean_flags = [])
63
+ opts = defaults.dup
64
+ opts[:help] = false
65
+ boolean_set = boolean_flags.to_set
66
+ idx = 0
67
+
68
+ while idx < argv.length
69
+ idx = parse_single_flag(argv, idx, opts, boolean_set)
70
+ idx += 1
71
+ end
72
+
73
+ opts
74
+ end
75
+
76
+ def parse_single_flag(argv, idx, opts, boolean_set)
77
+ arg = argv[idx]
78
+ case arg
79
+ when "-h", "--help"
80
+ opts[:help] = true
81
+ when /\A--([a-z][-a-z0-9]*)=(.+)\z/
82
+ opts[Regexp.last_match(1)] = Regexp.last_match(2)
83
+ when /\A--([a-z][-a-z0-9]*)\z/
84
+ idx = parse_named_flag(argv, idx, Regexp.last_match(1), opts, boolean_set)
85
+ end
86
+ idx
87
+ end
88
+
89
+ def parse_named_flag(argv, idx, key, opts, boolean_set)
90
+ if boolean_set.include?(key)
91
+ opts[key] = true
92
+ return idx
93
+ end
94
+
95
+ idx = consume_flag_value(argv, idx, key)
96
+ opts[key] = argv[idx]
97
+ idx
98
+ end
99
+
100
+ def consume_flag_value(argv, idx, key)
101
+ idx += 1
102
+ return idx if idx < argv.length
103
+
104
+ error "Missing value for --#{key}"
105
+ exit EXIT_USAGE
106
+ end
107
+ end
108
+ end
109
+ end