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
@@ -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.