Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

### ✨ Features

- `bonchi init` / `bonchi setup` resolve to the git/worktree root (`git rev-parse --show-toplevel`), so they work from any subdirectory — enabling monorepo layouts with a single root `.worktree.yml`. `init` outside a git repo now errors cleanly.

### 🐛 Fixes

- Port allocation now skips browser-unsafe ports (Chromium's `kRestrictedPorts`, e.g. `4045`/`4190` in the default `4000..5000` range). Previously an allocated port could land on one of these, leaving the dev server reachable on the socket but blocked by the browser with `ERR_UNSAFE_PORT`.
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ copy:

Default is to leave it out, so edits to main's `.worktree.yml` automatically apply to every subsequent `bonchi setup` run.

`bonchi init` and `bonchi setup` resolve to the **git/worktree root** (`git rev-parse --show-toplevel`), so they work from any subdirectory. `init` writes `.worktree.yml` at the root and `setup` anchors all steps there — no need to `cd` to the top first. (`init` outside a git repo errors cleanly.)

### Monorepos

git cannot make a worktree of a subfolder, so a monorepo (e.g. root folders `server/` and `extension/`) becomes a **single** worktree containing all apps, with **one** `.worktree.yml` at the root. Because commands resolve to the git root, `bonchi setup` works the same from `foo/`, `foo/server/`, or anywhere below. Configure per-app concerns with prefixed paths and distinct port names:

```yaml
copy:
- server/.env
- extension/.env
ports:
- SERVER_PORT
- EXTENSION_PORT
# fan out per-app dev/setup from your own orchestrator
setup: bin/worktree-dev
```

### Edit

Use `edit` to modify files during setup. Three actions are available; entries run in order, so you can interleave them. Env vars (`$VAR`) are expanded in replacement values.
Expand Down
4 changes: 3 additions & 1 deletion lib/bonchi/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ def pr(input)
which ports to allocate, and what setup command to run.
DESC
def init
path = File.join(Dir.pwd, ".worktree.yml")
root = Git.toplevel
abort "Error: not inside a git repository" unless root
path = File.join(root, ".worktree.yml")
if File.exist?(path)
abort "Error: .worktree.yml already exists"
end
Expand Down
5 changes: 5 additions & 0 deletions lib/bonchi/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def main_worktree
`git worktree list --porcelain`.lines.first.sub("worktree ", "").strip
end

def toplevel
root = `git rev-parse --show-toplevel 2>/dev/null`.strip
root.empty? ? nil : root
end

def worktree_list
`git worktree list`.lines.map(&:strip).reject(&:empty?)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/bonchi/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Setup
include Colors

def initialize(worktree: nil)
@worktree = worktree || Dir.pwd
@worktree = worktree || Git.toplevel || Dir.pwd
@main_worktree = Git.main_worktree
end

Expand Down
69 changes: 69 additions & 0 deletions test/test_setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,72 @@ def test_aborts_when_neither_worktree_has_config
end
end
end

class TestSetupAnchoring < Minitest::Test
def test_anchors_worktree_to_git_toplevel_when_no_arg
Bonchi::Git.stub(:toplevel, "/repo/root") do
Bonchi::Git.stub(:main_worktree, "/repo/main") do
setup = Bonchi::Setup.new
assert_equal "/repo/root", setup.instance_variable_get(:@worktree)
end
end
end

def test_falls_back_to_pwd_when_not_in_git_repo
Bonchi::Git.stub(:toplevel, nil) do
Bonchi::Git.stub(:main_worktree, "/repo/main") do
setup = Bonchi::Setup.new
assert_equal Dir.pwd, setup.instance_variable_get(:@worktree)
end
end
end

def test_explicit_worktree_arg_overrides_toplevel
Bonchi::Git.stub(:toplevel, "/repo/root") do
Bonchi::Git.stub(:main_worktree, "/repo/main") do
setup = Bonchi::Setup.new(worktree: "/explicit/path")
assert_equal "/explicit/path", setup.instance_variable_get(:@worktree)
end
end
end
end

class TestInitAnchoring < Minitest::Test
def setup
@tmpdir = Dir.mktmpdir
@subdir = File.join(@tmpdir, "server")
FileUtils.mkdir_p(@subdir)
@cli = Bonchi::CLI.new
end

def teardown
FileUtils.remove_entry(@tmpdir)
end

def silenced
out = $stdout
$stdout = StringIO.new
yield
ensure
$stdout = out
end

def test_init_writes_config_at_git_toplevel_from_subdir
Bonchi::Git.stub(:toplevel, @tmpdir) do
Dir.chdir(@subdir) do
silenced { @cli.init }
end
end

assert File.exist?(File.join(@tmpdir, ".worktree.yml"))
refute File.exist?(File.join(@subdir, ".worktree.yml"))
end

def test_init_aborts_outside_git_repo
Bonchi::Git.stub(:toplevel, nil) do
assert_raises(SystemExit) do
silenced { @cli.init }
end
end
end
end