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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Ruby Style & RuboCop Compliance Rules
|
|
2
|
+
|
|
3
|
+
This file defines the Ruby coding standards enforced by RuboCop in this project. You **must** follow these rules when writing or editing any Ruby source code (excluding `spec/` files, which are exempt from Metrics cops).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. General
|
|
8
|
+
|
|
9
|
+
- **Target Ruby version:** 3.0+
|
|
10
|
+
- **String literals:** Always use double quotes (`"`), including inside interpolation.
|
|
11
|
+
- **Line length:** Maximum **120 characters** per line.
|
|
12
|
+
- Do **not** add top-level documentation comments to classes or modules (`Style/Documentation` is disabled).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 2. Method Length
|
|
17
|
+
|
|
18
|
+
**Max 10 lines** per method body (excluding `def`/`end`).
|
|
19
|
+
|
|
20
|
+
- If a method exceeds 10 lines, extract logic into private helper methods.
|
|
21
|
+
- Each helper should have a single, clear responsibility.
|
|
22
|
+
|
|
23
|
+
### Example — Too long:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
def process(data)
|
|
27
|
+
# 15 lines of mixed validation, transformation, and output
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Example — Correct:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
def process(data)
|
|
35
|
+
validate(data)
|
|
36
|
+
result = transform(data)
|
|
37
|
+
output(result)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def validate(data)
|
|
43
|
+
# ...
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def transform(data)
|
|
47
|
+
# ...
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output(result)
|
|
51
|
+
# ...
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. ABC Size (Assignment Branch Condition)
|
|
58
|
+
|
|
59
|
+
**Max 17.** This measures a combination of assignments, branches (method calls), and conditions.
|
|
60
|
+
|
|
61
|
+
To reduce ABC size:
|
|
62
|
+
|
|
63
|
+
- Extract sub-expressions into local variables or helper methods.
|
|
64
|
+
- Avoid deeply chained method calls on a single line.
|
|
65
|
+
- Move conditional logic into dedicated predicate methods.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Cyclomatic Complexity
|
|
70
|
+
|
|
71
|
+
**Max 7.** Counts the number of independent code paths (each `if`, `elsif`, `unless`, `when`, `while`, `until`, `for`, `rescue`, `&&`, `||` adds 1).
|
|
72
|
+
|
|
73
|
+
To reduce cyclomatic complexity:
|
|
74
|
+
|
|
75
|
+
- Use early returns / guard clauses instead of nested `if`/`else`.
|
|
76
|
+
- Extract `case`/`when` branches into a dispatch hash or strategy pattern.
|
|
77
|
+
- Break complex conditionals into named predicate methods.
|
|
78
|
+
|
|
79
|
+
### Example — Too complex:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
def dispatch(cmd)
|
|
83
|
+
if cmd == "a"
|
|
84
|
+
do_a
|
|
85
|
+
elsif cmd == "b"
|
|
86
|
+
do_b
|
|
87
|
+
elsif cmd == "c"
|
|
88
|
+
do_c
|
|
89
|
+
# ... 10 more branches
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Example — Correct:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
DISPATCH = {
|
|
98
|
+
"a" => :do_a,
|
|
99
|
+
"b" => :do_b,
|
|
100
|
+
"c" => :do_c,
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
def dispatch(cmd)
|
|
104
|
+
handler = DISPATCH[cmd]
|
|
105
|
+
raise "Unknown command: #{cmd}" unless handler
|
|
106
|
+
send(handler)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 5. Perceived Complexity
|
|
113
|
+
|
|
114
|
+
**Max 8.** Similar to cyclomatic complexity but also weighs `else` and `when` branches.
|
|
115
|
+
|
|
116
|
+
Same mitigation strategies as cyclomatic complexity apply.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 6. Class Length
|
|
121
|
+
|
|
122
|
+
**Max 100 lines** (excluding comments and blank lines).
|
|
123
|
+
|
|
124
|
+
- If a class exceeds 100 lines, extract cohesive groups of methods into modules (mixins) or separate classes.
|
|
125
|
+
- Use composition: delegate behavior to collaborator objects.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 7. Block Nesting
|
|
130
|
+
|
|
131
|
+
**Max 3 levels** of block nesting.
|
|
132
|
+
|
|
133
|
+
To reduce nesting:
|
|
134
|
+
|
|
135
|
+
- Use `next` / `return` guard clauses to skip iterations early.
|
|
136
|
+
- Extract inner blocks into helper methods.
|
|
137
|
+
|
|
138
|
+
### Example — Too nested:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
items.each do |item|
|
|
142
|
+
if item.valid?
|
|
143
|
+
item.parts.each do |part|
|
|
144
|
+
if part.active?
|
|
145
|
+
# level 4 — violation
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Example — Correct:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
items.each do |item|
|
|
156
|
+
next unless item.valid?
|
|
157
|
+
process_parts(item.parts)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def process_parts(parts)
|
|
161
|
+
parts.each do |part|
|
|
162
|
+
next unless part.active?
|
|
163
|
+
# ...
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 8. Parameter Lists
|
|
171
|
+
|
|
172
|
+
**Max 5 parameters** per method.
|
|
173
|
+
|
|
174
|
+
To reduce parameters:
|
|
175
|
+
|
|
176
|
+
- Group related parameters into a hash or keyword arguments with a config/options object.
|
|
177
|
+
- Consider whether some parameters should be instance state instead.
|
|
178
|
+
|
|
179
|
+
### Example — Too many:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
def add(config_path:, name:, repo:, source:, branch:, commit:, label:, dir:)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Example — Correct:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
def add(config_path:, name:, repo:, **options)
|
|
189
|
+
source = options[:source]
|
|
190
|
+
branch = options[:branch]
|
|
191
|
+
# ...
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Or use a dedicated parameter object / struct.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 9. Naming
|
|
200
|
+
|
|
201
|
+
- Do **not** prefix writer methods with `set_`. Use Ruby's assignment-style naming: `def foo=(value)` instead of `def set_foo(value)`.
|
|
202
|
+
- File names must use `snake_case` (exception: `lib/Kobold.rb` is excluded).
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 10. Style
|
|
207
|
+
|
|
208
|
+
- Do **not** write empty `else` clauses. If an `else` branch does nothing, omit it entirely.
|
|
209
|
+
- Prefer guard clauses (`return unless ...`) over wrapping entire method bodies in `if`.
|
|
210
|
+
- Use `$stderr.puts` (not `warn`) inside custom `warn`/`error`/`hint` output methods to avoid infinite recursion with Kernel#warn.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 11. Layout
|
|
215
|
+
|
|
216
|
+
- Maximum line length: **120 characters**.
|
|
217
|
+
- When a string interpolation makes a line too long, split it with a backslash continuation:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
success "#{name}: #{short_sha(old)} -> " \
|
|
221
|
+
"#{short_sha(new)} (#{branch})"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 12. Spec Files
|
|
227
|
+
|
|
228
|
+
Spec files under `spec/` are **exempt** from all Metrics cops. Write tests for clarity and completeness without worrying about method/block length limits.
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# Kobold Ruby API Reference
|
|
2
|
+
|
|
3
|
+
Kobold is a Git-based package manager gem. It clones repositories as bare caches, creates Git worktrees for specific branches/commits, and symlinks them into project directories. All write operations (merge, commit, push) are **out of scope** — Kobold is read-only with respect to repo content.
|
|
4
|
+
|
|
5
|
+
**Gem:** `Kobold` (v0.4.0)
|
|
6
|
+
**Ruby:** >= 3.0
|
|
7
|
+
**Dependencies:** `git` (~> 4.3), `toml-rb` (~> 3.0)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Require
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "Kobold"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Constants
|
|
20
|
+
|
|
21
|
+
| Constant | Value | Description |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `Kobold::VERSION` | `"0.4.0"` | Gem version |
|
|
24
|
+
| `Kobold::FORMAT_VERSION` | `"0.4.0"` | `.kobold` config file format version |
|
|
25
|
+
| `Kobold::KOBOLD_DIR` | `~/.local/share/Kobold` | Root storage directory |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Kobold::Manager
|
|
30
|
+
|
|
31
|
+
Primary API entry point. All repo operations go through a `Manager` instance.
|
|
32
|
+
|
|
33
|
+
### Constructor
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
manager = Kobold::Manager.new(database: "my-db")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Param | Type | Default | Description |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `database:` | `String` | `"default"` | Named database (isolated cache namespace) |
|
|
42
|
+
|
|
43
|
+
Creates the database on disk if it does not exist.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### Registration
|
|
48
|
+
|
|
49
|
+
#### `register(repo, source:)` → `Hash`
|
|
50
|
+
|
|
51
|
+
Register a repository in the database.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
manager.register("owner/repo", source: "https://github.com")
|
|
55
|
+
# => { slug: "owner-repo", source_url: "https://github.com/owner/repo.git", already_registered: false }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Param | Type | Description |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `repo` | `String` | Repo identifier, e.g. `"owner/repo"` |
|
|
61
|
+
| `source:` | `String` | Git host base URL |
|
|
62
|
+
|
|
63
|
+
#### `unregister(repo)` → `Hash`
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
manager.unregister("owner/repo")
|
|
67
|
+
# => { slug: "owner-repo", removed: true }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Raises `Kobold::Errors::RepoNotFound` if not registered.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Fetching
|
|
75
|
+
|
|
76
|
+
#### `fetch(repo)` → `Hash`
|
|
77
|
+
|
|
78
|
+
Fetch latest objects for a single registered repo. Clones if not yet cached.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
manager.fetch("owner/repo")
|
|
82
|
+
# => { slug: "owner-repo", action: :fetched } # or action: :cloned
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### `fetch_all(config_path: Dir.pwd)` → `Array<Hash>`
|
|
86
|
+
|
|
87
|
+
Fetch all repos referenced in a `.kobold` config file. Registers any unregistered repos automatically.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
manager.fetch_all(config_path: "/path/to/.kobold")
|
|
91
|
+
# => [{ name: "dep-name", slug: "owner-repo", action: :fetched }, ...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Checkout & Worktrees
|
|
97
|
+
|
|
98
|
+
#### `checkout(repo, branch: nil, commit: nil, label: nil, dir: nil)` → `Hash`
|
|
99
|
+
|
|
100
|
+
Create a worktree for a repo. Clones the repo if not yet cached. Optionally creates a symlink.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
result = manager.checkout("owner/repo", branch: "main", dir: "/project/vendor/repo")
|
|
104
|
+
# => { slug: "owner-repo", worktree_path: "/home/.../.local/share/Kobold/...", symlink_path: "/project/vendor/repo" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Param | Type | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `repo` | `String` | Repo identifier |
|
|
110
|
+
| `branch:` | `String?` | Branch to check out |
|
|
111
|
+
| `commit:` | `String?` | Specific commit SHA |
|
|
112
|
+
| `label:` | `String?` | Named label (requires `commit:`) |
|
|
113
|
+
| `dir:` | `String?` | If set, symlink the worktree here |
|
|
114
|
+
|
|
115
|
+
Resolution priority: `commit` > `branch` > default branch. Worktrees are detached-HEAD checkouts.
|
|
116
|
+
|
|
117
|
+
**Worktree storage paths:**
|
|
118
|
+
|
|
119
|
+
- Branch: `<repo_cache>/worktrees/branches/<branch>`
|
|
120
|
+
- Commit: `<repo_cache>/worktrees/commits/<sha>`
|
|
121
|
+
- Label: `<repo_cache>/worktrees/labelled/<label>/<sha>`
|
|
122
|
+
|
|
123
|
+
#### `create_branch(repo, name:, from: "main", dir: nil)` → `Hash`
|
|
124
|
+
|
|
125
|
+
Create a new branch in the bare cache, then create a worktree for it.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
result = manager.create_branch("owner/repo", name: "feature-x", from: "main", dir: "/project/vendor/repo")
|
|
129
|
+
# => { slug: "owner-repo", branch: "feature-x", worktree_path: "...", symlink_path: "..." }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Param | Type | Default | Description |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `name:` | `String` | — | New branch name |
|
|
135
|
+
| `from:` | `String` | `"main"` | Source ref (branch, tag, or SHA) |
|
|
136
|
+
| `dir:` | `String?` | `nil` | Symlink destination |
|
|
137
|
+
|
|
138
|
+
If the branch already exists, it is reused (no error).
|
|
139
|
+
|
|
140
|
+
#### `remove_worktree(repo, dir:)` → `Hash`
|
|
141
|
+
|
|
142
|
+
Remove a worktree. If `dir:` is a symlink, follows it to the worktree and removes both.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
manager.remove_worktree("owner/repo", dir: "/project/vendor/repo")
|
|
146
|
+
# => { slug: "owner-repo", removed_worktree: "...", removed_symlink: true }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Listing
|
|
152
|
+
|
|
153
|
+
#### `list_repos` → `Hash`
|
|
154
|
+
|
|
155
|
+
All registered repos in the database.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
manager.list_repos
|
|
159
|
+
# => { "owner-repo" => { "source_url" => "https://github.com/owner/repo.git" } }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `list_worktrees(repo)` → `Array<String>`
|
|
163
|
+
|
|
164
|
+
All worktree paths for a repo.
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
manager.list_worktrees("owner/repo")
|
|
168
|
+
# => ["/home/.../.local/share/Kobold/.../worktrees/branches/main"]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `list_branches(repo)` → `Array<String>`
|
|
172
|
+
|
|
173
|
+
All branch names in the bare cache.
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
manager.list_branches("owner/repo")
|
|
177
|
+
# => ["main", "develop", "feature-x"]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Lifecycle (Config-Based)
|
|
183
|
+
|
|
184
|
+
These methods operate on `.kobold` TOML config files.
|
|
185
|
+
|
|
186
|
+
#### `add(config_path:, repo:, source:, name: nil, **options)` → `Hash`
|
|
187
|
+
|
|
188
|
+
Add a dependency to a `.kobold` config file. Registers and clones the repo.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
manager.add(
|
|
192
|
+
config_path: "/project/.kobold",
|
|
193
|
+
repo: "owner/repo",
|
|
194
|
+
source: "https://github.com",
|
|
195
|
+
name: "my-dep",
|
|
196
|
+
branch: "main",
|
|
197
|
+
commit: "abc1234",
|
|
198
|
+
dir: "vendor/"
|
|
199
|
+
)
|
|
200
|
+
# => { config_path: "...", name: "my-dep", slug: "owner-repo", action: :cloned }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| Option | Type | Description |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `name:` | `String?` | Dependency name (defaults to repo basename) |
|
|
206
|
+
| `branch:` | `String?` | Branch name |
|
|
207
|
+
| `commit:` | `String?` | Pinned commit SHA |
|
|
208
|
+
| `label:` | `String?` | Label (requires commit) |
|
|
209
|
+
| `dir:` | `String?` | Symlink directory in config |
|
|
210
|
+
|
|
211
|
+
#### `remove(config_path:, dependency_name:, cleanup: false)` → `Hash`
|
|
212
|
+
|
|
213
|
+
Remove a dependency from a `.kobold` config. With `cleanup: true`, also removes the worktree and symlink.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
manager.remove(config_path: "/project/.kobold", dependency_name: "my-dep", cleanup: true)
|
|
217
|
+
# => { config_path: "...", name: "my-dep", removed_worktree: "...", removed_symlink: true }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### `update(config_path:, dependency_name:)` → `Hash`
|
|
221
|
+
|
|
222
|
+
Update a dependency's commit SHA to the latest HEAD of its configured branch. Fetches first.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
manager.update(config_path: "/project/.kobold", dependency_name: "my-dep")
|
|
226
|
+
# => { config_path: "...", name: "my-dep", old_commit: "abc...", new_commit: "def...", branch: "main" }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Raises `Kobold::Errors::DependencyNotFound` if missing. Raises `Kobold::Errors::ConfigError` if no branch is set.
|
|
230
|
+
|
|
231
|
+
#### `update_all(config_path: Dir.pwd)` → `Array<Hash>`
|
|
232
|
+
|
|
233
|
+
Update all dependencies that have a `branch` field. Errors are caught and returned inline.
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
manager.update_all(config_path: "/project/.kobold")
|
|
237
|
+
# => [{ config_path: "...", name: "my-dep", old_commit: "...", new_commit: "...", branch: "main" }, ...]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Invoke
|
|
243
|
+
|
|
244
|
+
#### `invoke(config_path: Dir.pwd)` → `Array<Hash>`
|
|
245
|
+
|
|
246
|
+
Process a `.kobold` config end-to-end: register, clone, create worktrees, and symlink all dependencies. Recursively processes `includes`.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
manager.invoke(config_path: "/project/.kobold")
|
|
250
|
+
# => [{ name: "dep", slug: "owner-repo", worktree_path: "...", symlink_path: "..." }, ...]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### Init
|
|
256
|
+
|
|
257
|
+
#### `init(dir: Dir.pwd)` → `Hash`
|
|
258
|
+
|
|
259
|
+
Create a new `.kobold` TOML template file.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
manager.init(dir: "/project")
|
|
263
|
+
# => { path: "/project/.kobold", created: true }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Returns `created: false` if the file already exists.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Config Generation
|
|
271
|
+
|
|
272
|
+
#### `generate_config(path:, dependencies: [], includes: [])` → `Hash`
|
|
273
|
+
|
|
274
|
+
Programmatically generate a `.kobold` config file.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
manager.generate_config(
|
|
278
|
+
path: "/project/.kobold",
|
|
279
|
+
dependencies: [
|
|
280
|
+
{ name: "my-dep", repo: "owner/repo", source: "https://github.com", branch: "main" }
|
|
281
|
+
],
|
|
282
|
+
includes: ["subdir/"]
|
|
283
|
+
)
|
|
284
|
+
# => { path: "/project/.kobold", dependencies_count: 1 }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Each dependency hash must include `:name`, `:repo`, `:source`. Optional: `:branch`, `:commit`, `:label`, `:dir`.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### Cleaning
|
|
292
|
+
|
|
293
|
+
#### `clean(config_path: nil)` → `Hash`
|
|
294
|
+
|
|
295
|
+
Purge repos not referenced by the given config. Without a config path, returns an empty purge list.
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
manager.clean(config_path: "/project/.kobold")
|
|
299
|
+
# => { purged_repos: [{ slug: "unused-repo" }] }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Class Methods on Kobold::Manager
|
|
305
|
+
|
|
306
|
+
### Database Management
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
Kobold::Manager.create_database("my-db")
|
|
310
|
+
# => { name: "my-db", path: "...", created: true }
|
|
311
|
+
|
|
312
|
+
Kobold::Manager.delete_database("my-db")
|
|
313
|
+
# => { name: "my-db", deleted: true }
|
|
314
|
+
|
|
315
|
+
Kobold::Manager.list_databases
|
|
316
|
+
# => ["default", "my-db"]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`create_database` raises `Kobold::Errors::DatabaseExists` if it already exists.
|
|
320
|
+
`delete_database` raises `Kobold::Errors::DatabaseNotFound` if missing.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Kobold::Settings
|
|
325
|
+
|
|
326
|
+
Module for global user-level settings stored at `~/.local/share/Kobold/config.toml`.
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
Kobold::Settings.default_source
|
|
330
|
+
# => "https://github.com"
|
|
331
|
+
|
|
332
|
+
Kobold::Settings.default_source = "https://gitlab.com"
|
|
333
|
+
|
|
334
|
+
Kobold::Settings.get("defaults.source")
|
|
335
|
+
# => "https://gitlab.com"
|
|
336
|
+
|
|
337
|
+
Kobold::Settings.set("defaults.source", "https://github.com")
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Kobold::Database
|
|
343
|
+
|
|
344
|
+
Lower-level API. Normally accessed through `Manager`, but available directly.
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
db = Kobold::Database.new("my-db")
|
|
348
|
+
db.setup
|
|
349
|
+
db.register_repo("owner-repo", "https://github.com/owner/repo.git")
|
|
350
|
+
db.list_repos # => { "owner-repo" => { "source_url" => "..." } }
|
|
351
|
+
repo = db.repo("owner-repo") # => Kobold::Repo instance
|
|
352
|
+
db.unregister_repo("owner-repo")
|
|
353
|
+
db.delete
|
|
354
|
+
|
|
355
|
+
Kobold::Database.slugify("owner/repo") # => "owner-repo"
|
|
356
|
+
Kobold::Database.list_databases # => ["default", ...]
|
|
357
|
+
Kobold::Database.exists?("my-db") # => true/false
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Kobold::Repo
|
|
363
|
+
|
|
364
|
+
Represents a cached repository. Obtained via `Database#repo(slug)`.
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
repo = db.repo("owner-repo")
|
|
368
|
+
|
|
369
|
+
repo.slug # => "owner-repo"
|
|
370
|
+
repo.source_url # => "https://github.com/owner/repo.git"
|
|
371
|
+
repo.path # => "/home/.../.local/share/Kobold/databases/my-db/repos/owner-repo"
|
|
372
|
+
repo.source_path # => ".../source" (bare clone location)
|
|
373
|
+
repo.cloned? # => true/false
|
|
374
|
+
|
|
375
|
+
repo.clone # Clone if not cached; returns Git::Base
|
|
376
|
+
repo.fetch # Fetch all remotes
|
|
377
|
+
|
|
378
|
+
repo.create_worktree(branch: "main")
|
|
379
|
+
repo.create_worktree(commit: "abc1234")
|
|
380
|
+
repo.create_worktree(commit: "abc1234", label: "v1")
|
|
381
|
+
repo.remove_worktree("/path/to/worktree")
|
|
382
|
+
|
|
383
|
+
repo.create_branch("feature-x", from: "main") # => "feature-x"
|
|
384
|
+
|
|
385
|
+
repo.list_worktrees # => ["/path/to/worktree", ...]
|
|
386
|
+
repo.list_branches # => ["main", "develop", ...]
|
|
387
|
+
repo.resolve_branch_head("main") # => "abc1234..." (full SHA)
|
|
388
|
+
|
|
389
|
+
repo.worktree_path_for(branch: "main") # => predicted path without creating
|
|
390
|
+
repo.purge # Delete entire repo cache
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Kobold::Linker
|
|
396
|
+
|
|
397
|
+
Stateless symlink utilities.
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
Kobold::Linker.link("/source/path", "/dest/path") # => "/dest/path"
|
|
401
|
+
Kobold::Linker.link("/source/path", "/dest/dir/") # => "/dest/dir/path" (basename)
|
|
402
|
+
Kobold::Linker.unlink("/dest/path") # removes symlink
|
|
403
|
+
Kobold::Linker.relink("/new/source", "/dest/path") # unlink + link
|
|
404
|
+
Kobold::Linker.valid?("/dest/path") # => true if symlink target exists
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Kobold::Config
|
|
410
|
+
|
|
411
|
+
Reads and manipulates `.kobold` TOML config files.
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
config = Kobold::Config.read("/project/.kobold")
|
|
415
|
+
config.format_version # => "0.4.0"
|
|
416
|
+
config.includes # => ["subdir/"]
|
|
417
|
+
config.dependencies # => { "name" => { "repo" => "...", ... } }
|
|
418
|
+
config.path # => "/project/.kobold"
|
|
419
|
+
|
|
420
|
+
config.each_dependency { |name, dep| ... }
|
|
421
|
+
config.dependency("name") # => Hash or nil
|
|
422
|
+
config.dependency?("name") # => true/false
|
|
423
|
+
config.add_dependency("name", { "repo" => "owner/repo", "source" => "https://github.com" })
|
|
424
|
+
config.remove_dependency("name")
|
|
425
|
+
config.update_dependency("name", "commit", "abc1234")
|
|
426
|
+
config.to_h # => full Hash for serialization
|
|
427
|
+
|
|
428
|
+
Kobold::Config.write("/project/.kobold", config)
|
|
429
|
+
Kobold::Config.generate(dependencies: [...], includes: [...])
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Dependency required keys:** `repo`, `source`
|
|
433
|
+
**Dependency optional keys:** `dir`, `branch`, `commit`, `label`
|
|
434
|
+
**Constraint:** `label` requires `commit` to be set.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Kobold::GitOps
|
|
439
|
+
|
|
440
|
+
Low-level stateless Git operations. All methods are `module_function`.
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
Kobold::GitOps.clone(url, path) # => Git::Base (bare clone)
|
|
444
|
+
Kobold::GitOps.open(path) # => Git::Base (bare or non-bare)
|
|
445
|
+
Kobold::GitOps.bare_repo?(path) # => true/false
|
|
446
|
+
Kobold::GitOps.git_dir(repo) # => String (path to .git or bare dir)
|
|
447
|
+
Kobold::GitOps.fetch(repo, label: "...") # fetch --all
|
|
448
|
+
Kobold::GitOps.resolve_ref(repo, ref) # => full SHA
|
|
449
|
+
Kobold::GitOps.resolve_branch_head(repo, branch) # => full SHA (checks origin/ first)
|
|
450
|
+
Kobold::GitOps.default_branch(repo) # => "main" (reads symbolic-ref HEAD)
|
|
451
|
+
Kobold::GitOps.create_worktree(repo, path, ref) # worktree add --detach
|
|
452
|
+
Kobold::GitOps.remove_worktree(repo, path) # worktree remove --force
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Errors
|
|
458
|
+
|
|
459
|
+
All under `Kobold::Errors`:
|
|
460
|
+
|
|
461
|
+
| Error | Raised When |
|
|
462
|
+
|---|---|
|
|
463
|
+
| `RepoNotFound` | Repo slug not in database or not cloned |
|
|
464
|
+
| `WorktreeExists` | Worktree path already exists |
|
|
465
|
+
| `ConfigError` | Missing/invalid `.kobold` config |
|
|
466
|
+
| `DatabaseNotFound` | Database directory does not exist |
|
|
467
|
+
| `DatabaseExists` | Trying to create a database that exists |
|
|
468
|
+
| `InvalidFormat` | Config format_version mismatch |
|
|
469
|
+
| `CloneError` | `git clone` fails |
|
|
470
|
+
| `FetchError` | `git fetch` fails |
|
|
471
|
+
| `GitError` | General git operation failure |
|
|
472
|
+
| `DependencyNotFound` | Named dependency missing from config |
|
|
473
|
+
| `LinkError` | Symlink operation failure |
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Convenience Wrappers (Module-Level)
|
|
478
|
+
|
|
479
|
+
These print to stdout and are thin wrappers around `Manager`:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
Kobold.init(dir: "/project")
|
|
483
|
+
Kobold.invoke(config_path: "/project", database_name: "default")
|
|
484
|
+
Kobold.fetch(config_path: "/project", database_name: "default")
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Slugification
|
|
490
|
+
|
|
491
|
+
Repo identifiers (`"owner/repo"`) are converted to slugs (`"owner-repo"`) via `Database.slugify`. All internal lookups use slugs.
|