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.
- checksums.yaml +4 -4
- data/.rspec +5 -0
- data/.rubocop.yml +21 -1
- data/.rules/bugs/untestable.md +27 -0
- data/.rules/changelog/2026-03/30/01.md +55 -0
- data/.rules/changelog/2026-03/30/02.md +27 -0
- data/.rules/changelog/2026-03/30/03.md +36 -0
- data/.rules/changelog/2026-03/30/04.md +48 -0
- data/.rules/changelog/2026-03/30/05.md +19 -0
- data/.rules/changelog/2026-03/30/06.md +16 -0
- data/.rules/changelog/2026-03/30/07.md +28 -0
- data/.rules/changelog/2026-03/30/08.md +29 -0
- data/.rules/changelog/2026-03/30/09.md +33 -0
- data/.rules/changelog/2026-03/30/10.md +12 -0
- data/.rules/changelog/2026-03/30/11.md +47 -0
- data/.rules/changelog/2026-03/30/12.md +18 -0
- data/.rules/changelog/2026-03/30/13.md +36 -0
- data/.rules/changelog/2026-03/30/14.md +13 -0
- data/.rules/changelog/2026-03/30/15.md +24 -0
- data/.rules/default/rubocop.md +228 -0
- data/.rules/docs/kobold_api.md +491 -0
- data/README.md +131 -29
- data/Rakefile +19 -2
- data/exe/kobold +3 -57
- data/lib/Kobold/cli/admin_commands.rb +124 -0
- data/lib/Kobold/cli/checkout_commands.rb +73 -0
- data/lib/Kobold/cli/error_handling.rb +50 -0
- data/lib/Kobold/cli/flag_parser.rb +109 -0
- data/lib/Kobold/cli/init_commands.rb +108 -0
- data/lib/Kobold/cli/lifecycle_commands.rb +116 -0
- data/lib/Kobold/cli/list_commands.rb +80 -0
- data/lib/Kobold/cli/output.rb +40 -0
- data/lib/Kobold/cli/repo_commands.rb +101 -0
- data/lib/Kobold/cli/update_commands.rb +71 -0
- data/lib/Kobold/cli.rb +120 -0
- data/lib/Kobold/config.rb +136 -0
- data/lib/Kobold/database.rb +169 -0
- data/lib/Kobold/errors.rb +59 -0
- data/lib/Kobold/fetch.rb +19 -0
- data/lib/Kobold/git_ops.rb +162 -0
- data/lib/Kobold/init.rb +17 -13
- data/lib/Kobold/invoke.rb +12 -192
- data/lib/Kobold/linker.rb +87 -0
- data/lib/Kobold/manager/checkout.rb +78 -0
- data/lib/Kobold/manager/cleaning.rb +47 -0
- data/lib/Kobold/manager/fetching.rb +58 -0
- data/lib/Kobold/manager/invoking.rb +67 -0
- data/lib/Kobold/manager/lifecycle.rb +133 -0
- data/lib/Kobold/manager/registration.rb +32 -0
- data/lib/Kobold/manager.rb +140 -0
- data/lib/Kobold/repo/worktree_helpers.rb +56 -0
- data/lib/Kobold/repo.rb +135 -0
- data/lib/Kobold/settings.rb +103 -0
- data/lib/Kobold/version.rb +2 -2
- data/lib/Kobold.rb +14 -13
- data/prototyping/.kobold +19 -24
- data/sample-project-ideas/.kobold +19 -27
- data/sig/Kobold.rbs +217 -1
- metadata +60 -59
- data/lib/Kobold/first_time_setup.rb +0 -14
- data/lib/Kobold/read_config.rb +0 -15
data/README.md
CHANGED
|
@@ -1,35 +1,137 @@
|
|
|
1
1
|
# Kobold
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
130
|
+
The `kobold` executable will be available on your `PATH` after installation.
|
|
131
|
+
|
|
132
|
+
---
|
|
6
133
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
require 'tty-option'
|
|
5
|
-
#require 'pry'
|
|
4
|
+
require "Kobold"
|
|
6
5
|
|
|
7
|
-
|
|
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
|