diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e3f60..0f15547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/README.md b/README.md index eba066e..39e0854 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/bonchi/cli.rb b/lib/bonchi/cli.rb index 41323ba..6306a75 100644 --- a/lib/bonchi/cli.rb +++ b/lib/bonchi/cli.rb @@ -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 diff --git a/lib/bonchi/git.rb b/lib/bonchi/git.rb index 92522e3..513cf14 100644 --- a/lib/bonchi/git.rb +++ b/lib/bonchi/git.rb @@ -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 diff --git a/lib/bonchi/setup.rb b/lib/bonchi/setup.rb index 36561c5..ed549e3 100644 --- a/lib/bonchi/setup.rb +++ b/lib/bonchi/setup.rb @@ -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 diff --git a/test/test_setup.rb b/test/test_setup.rb index 990316d..8272e88 100644 --- a/test/test_setup.rb +++ b/test/test_setup.rb @@ -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