timberline 0.1.0__tar.gz

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 (37) hide show
  1. timberline-0.1.0/.gitignore +9 -0
  2. timberline-0.1.0/.timberline.toml +30 -0
  3. timberline-0.1.0/Makefile +29 -0
  4. timberline-0.1.0/PKG-INFO +112 -0
  5. timberline-0.1.0/README.md +100 -0
  6. timberline-0.1.0/pyproject.toml +42 -0
  7. timberline-0.1.0/tests/__init__.py +0 -0
  8. timberline-0.1.0/tests/conftest.py +24 -0
  9. timberline-0.1.0/tests/test_agent.py +116 -0
  10. timberline-0.1.0/tests/test_cli.py +249 -0
  11. timberline-0.1.0/tests/test_config.py +54 -0
  12. timberline-0.1.0/tests/test_display.py +52 -0
  13. timberline-0.1.0/tests/test_env.py +84 -0
  14. timberline-0.1.0/tests/test_git.py +116 -0
  15. timberline-0.1.0/tests/test_init_deps.py +105 -0
  16. timberline-0.1.0/tests/test_names.py +37 -0
  17. timberline-0.1.0/tests/test_shell.py +94 -0
  18. timberline-0.1.0/tests/test_state.py +109 -0
  19. timberline-0.1.0/tests/test_submodules.py +18 -0
  20. timberline-0.1.0/tests/test_types.py +126 -0
  21. timberline-0.1.0/tests/test_worktree.py +108 -0
  22. timberline-0.1.0/timberline/__init__.py +1 -0
  23. timberline-0.1.0/timberline/__main__.py +4 -0
  24. timberline-0.1.0/timberline/agent.py +105 -0
  25. timberline-0.1.0/timberline/cli.py +775 -0
  26. timberline-0.1.0/timberline/config.py +150 -0
  27. timberline-0.1.0/timberline/display.py +139 -0
  28. timberline-0.1.0/timberline/env.py +74 -0
  29. timberline-0.1.0/timberline/git.py +137 -0
  30. timberline-0.1.0/timberline/init_deps.py +150 -0
  31. timberline-0.1.0/timberline/names.py +166 -0
  32. timberline-0.1.0/timberline/shell.py +138 -0
  33. timberline-0.1.0/timberline/state.py +110 -0
  34. timberline-0.1.0/timberline/submodules.py +51 -0
  35. timberline-0.1.0/timberline/types.py +93 -0
  36. timberline-0.1.0/timberline/worktree.py +161 -0
  37. timberline-0.1.0/uv.lock +272 -0
@@ -0,0 +1,9 @@
1
+ plans/
2
+ .lj/
3
+ __pycache__/
4
+ .venv/
5
+ dist/
6
+ *.egg-info
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ CLAUDE.md
@@ -0,0 +1,30 @@
1
+ [timberline]
2
+ worktree_dir = ".tl"
3
+ branch_template = "{user}/{type}/{name}"
4
+ user = "nc9"
5
+ default_type = "feature"
6
+ base_branch = "main"
7
+ naming_scheme = "minerals"
8
+ default_agent = "claude"
9
+
10
+ [timberline.init]
11
+ init_command = "uv sync"
12
+ auto_init = true
13
+
14
+ [timberline.env]
15
+ auto_copy = true
16
+ patterns = [
17
+ ".env",
18
+ ".env.*",
19
+ "!.env.example",
20
+ "!.env.template",
21
+ ]
22
+ scan_depth = 3
23
+
24
+ [timberline.submodules]
25
+ auto_init = true
26
+ recursive = true
27
+
28
+ [timberline.agent]
29
+ auto_launch = false
30
+ inject_context = true
@@ -0,0 +1,29 @@
1
+ .PHONY: dev test lint fmt check link unlink
2
+
3
+ dev: ## Install in development mode
4
+ uv sync
5
+ @echo "✓ Development mode ready"
6
+
7
+ test: ## Run tests
8
+ uv run pytest -v
9
+
10
+ lint: ## Lint + type check
11
+ uv run ruff check timberline/ tests/
12
+ uv run basedpyright timberline/
13
+
14
+ fmt: ## Format + fix imports
15
+ uv run ruff format timberline/ tests/
16
+ uv run ruff check --fix timberline/ tests/
17
+
18
+ check: fmt lint test ## Format, lint, type check, test
19
+
20
+ link: dev ## Symlink tl to ~/.local/bin for testing
21
+ @mkdir -p $(HOME)/.local/bin
22
+ @ln -sf $(CURDIR)/.venv/bin/tl $(HOME)/.local/bin/tl
23
+ @echo "✓ tl linked to ~/.local/bin/tl"
24
+
25
+ unlink: ## Remove tl symlink
26
+ @rm -f $(HOME)/.local/bin/tl
27
+ @echo "✓ tl unlinked"
28
+
29
+ .DEFAULT_GOAL := dev
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: timberline
3
+ Version: 0.1.0
4
+ Summary: Git worktree manager for parallel Claude Code development
5
+ Author-email: Nik Cubrilovic <git@nikcub.me>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: tomli-w>=1.0
10
+ Requires-Dist: typer>=0.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Timberline
14
+
15
+ Git worktree manager for parallel coding agent development.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ uv tool install .
21
+ # or
22
+ uv pip install -e .
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ cd your-repo
29
+ tl init --defaults # create .timberline.toml
30
+ tl new auth-refactor # create worktree + branch
31
+ tl new --type fix # auto-named fix worktree
32
+ tl ls # list all worktrees
33
+ cd $(tl cd auth-refactor) # jump into worktree
34
+ tl rm auth-refactor # clean up
35
+ ```
36
+
37
+ ## Commands
38
+
39
+ | Command | Description |
40
+ |---------|-------------|
41
+ | `tl init` | Interactive setup, write `.timberline.toml` |
42
+ | `tl new [name]` | Create worktree (aliases: `create`) |
43
+ | `tl ls` | List worktrees (aliases: `list`). `--json`, `--paths` |
44
+ | `tl rm <name>` | Remove worktree (aliases: `remove`). `--force`, `--keep-branch`, `--all` |
45
+ | `tl cd <name>` | Print worktree path. `--shell` for subshell |
46
+ | `tl status` | Git status across all worktrees |
47
+ | `tl sync [name]` | Rebase/merge on base branch. `--all`, `--merge` |
48
+ | `tl agent [name]` | Launch coding agent in worktree. `--new` |
49
+ | `tl run-init [name]` | Re-run dependency install |
50
+ | `tl env sync [name]` | Re-copy .env files from main repo |
51
+ | `tl env ls` | List discovered .env files |
52
+ | `tl env diff [name]` | Show .env differences |
53
+ | `tl pr [name]` | Create PR via gh CLI. `--draft` |
54
+ | `tl clean` | Prune stale worktrees. `--dry-run` |
55
+ | `tl config show` | Print resolved config |
56
+ | `tl config set <k> <v>` | Set config value |
57
+ | `tl config edit` | Open config in $EDITOR |
58
+ | `tl shell-init` | Output shell integration script |
59
+
60
+ ## Config
61
+
62
+ `.timberline.toml` in repo root:
63
+
64
+ ```toml
65
+ [timberline]
66
+ worktree_dir = ".tl"
67
+ branch_template = "{user}/{type}/{name}"
68
+ user = "nc9"
69
+ default_type = "feature"
70
+ base_branch = "main"
71
+ naming_scheme = "minerals" # minerals | cities | compound
72
+ default_agent = "claude" # claude | codex | opencode | aider
73
+
74
+ [timberline.init]
75
+ auto_init = true
76
+ # init_command = "bun run init"
77
+ # post_init = ["echo done"]
78
+
79
+ [timberline.env]
80
+ auto_copy = true
81
+ patterns = [".env", ".env.*", "!.env.example", "!.env.template"]
82
+ scan_depth = 3
83
+
84
+ [timberline.submodules]
85
+ auto_init = true
86
+ recursive = true
87
+
88
+ [timberline.agent]
89
+ auto_launch = false
90
+ inject_context = true
91
+ ```
92
+
93
+ ## Shell Integration
94
+
95
+ ```bash
96
+ # Add to .zshrc / .bashrc:
97
+ eval "$(tl shell-init)"
98
+
99
+ # Then use:
100
+ tlcd obsidian # cd into worktree
101
+ tl-prompt # worktree name for PS1
102
+ ```
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ uv sync
108
+ make test # pytest
109
+ make lint # ruff + basedpyright
110
+ make fmt # ruff format
111
+ make check # all of the above
112
+ ```
@@ -0,0 +1,100 @@
1
+ # Timberline
2
+
3
+ Git worktree manager for parallel coding agent development.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uv tool install .
9
+ # or
10
+ uv pip install -e .
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ cd your-repo
17
+ tl init --defaults # create .timberline.toml
18
+ tl new auth-refactor # create worktree + branch
19
+ tl new --type fix # auto-named fix worktree
20
+ tl ls # list all worktrees
21
+ cd $(tl cd auth-refactor) # jump into worktree
22
+ tl rm auth-refactor # clean up
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `tl init` | Interactive setup, write `.timberline.toml` |
30
+ | `tl new [name]` | Create worktree (aliases: `create`) |
31
+ | `tl ls` | List worktrees (aliases: `list`). `--json`, `--paths` |
32
+ | `tl rm <name>` | Remove worktree (aliases: `remove`). `--force`, `--keep-branch`, `--all` |
33
+ | `tl cd <name>` | Print worktree path. `--shell` for subshell |
34
+ | `tl status` | Git status across all worktrees |
35
+ | `tl sync [name]` | Rebase/merge on base branch. `--all`, `--merge` |
36
+ | `tl agent [name]` | Launch coding agent in worktree. `--new` |
37
+ | `tl run-init [name]` | Re-run dependency install |
38
+ | `tl env sync [name]` | Re-copy .env files from main repo |
39
+ | `tl env ls` | List discovered .env files |
40
+ | `tl env diff [name]` | Show .env differences |
41
+ | `tl pr [name]` | Create PR via gh CLI. `--draft` |
42
+ | `tl clean` | Prune stale worktrees. `--dry-run` |
43
+ | `tl config show` | Print resolved config |
44
+ | `tl config set <k> <v>` | Set config value |
45
+ | `tl config edit` | Open config in $EDITOR |
46
+ | `tl shell-init` | Output shell integration script |
47
+
48
+ ## Config
49
+
50
+ `.timberline.toml` in repo root:
51
+
52
+ ```toml
53
+ [timberline]
54
+ worktree_dir = ".tl"
55
+ branch_template = "{user}/{type}/{name}"
56
+ user = "nc9"
57
+ default_type = "feature"
58
+ base_branch = "main"
59
+ naming_scheme = "minerals" # minerals | cities | compound
60
+ default_agent = "claude" # claude | codex | opencode | aider
61
+
62
+ [timberline.init]
63
+ auto_init = true
64
+ # init_command = "bun run init"
65
+ # post_init = ["echo done"]
66
+
67
+ [timberline.env]
68
+ auto_copy = true
69
+ patterns = [".env", ".env.*", "!.env.example", "!.env.template"]
70
+ scan_depth = 3
71
+
72
+ [timberline.submodules]
73
+ auto_init = true
74
+ recursive = true
75
+
76
+ [timberline.agent]
77
+ auto_launch = false
78
+ inject_context = true
79
+ ```
80
+
81
+ ## Shell Integration
82
+
83
+ ```bash
84
+ # Add to .zshrc / .bashrc:
85
+ eval "$(tl shell-init)"
86
+
87
+ # Then use:
88
+ tlcd obsidian # cd into worktree
89
+ tl-prompt # worktree name for PS1
90
+ ```
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ uv sync
96
+ make test # pytest
97
+ make lint # ruff + basedpyright
98
+ make fmt # ruff format
99
+ make check # all of the above
100
+ ```
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "timberline"
3
+ version = "0.1.0"
4
+ description = "Git worktree manager for parallel Claude Code development"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Nik Cubrilovic", email = "git@nikcub.me" }]
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "typer>=0.12",
11
+ "rich>=13.0",
12
+ "tomli-w>=1.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ tl = "timberline.cli:app"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["timberline"]
24
+
25
+ [tool.uv]
26
+ dev-dependencies = [
27
+ "pytest>=8.0",
28
+ "pytest-tmp-files>=0.0.2",
29
+ "basedpyright>=1.20",
30
+ "ruff>=0.8",
31
+ ]
32
+
33
+ [tool.ruff]
34
+ target-version = "py311"
35
+ line-length = 100
36
+
37
+ [tool.ruff.lint]
38
+ select = ["E", "F", "I", "UP", "B", "SIM"]
39
+
40
+ [tool.basedpyright]
41
+ pythonVersion = "3.11"
42
+ typeCheckingMode = "standard"
File without changes
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+
9
+ @pytest.fixture
10
+ def tmp_git_repo(tmp_path: Path) -> Path:
11
+ """Create a temporary git repo with an initial commit."""
12
+ repo = tmp_path / "repo"
13
+ repo.mkdir()
14
+ subprocess.run(["git", "init", "-b", "main"], cwd=repo, capture_output=True, check=True)
15
+ subprocess.run(
16
+ ["git", "config", "user.email", "test@test.com"], cwd=repo, capture_output=True, check=True
17
+ )
18
+ subprocess.run(
19
+ ["git", "config", "user.name", "Test"], cwd=repo, capture_output=True, check=True
20
+ )
21
+ (repo / "README.md").write_text("# Test")
22
+ subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
23
+ subprocess.run(["git", "commit", "-m", "init"], cwd=repo, capture_output=True, check=True)
24
+ return repo
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from timberline.agent import (
9
+ KNOWN_AGENTS,
10
+ buildContextBlock,
11
+ buildEnvVars,
12
+ detectInstalledAgents,
13
+ getAgentDef,
14
+ injectAgentContext,
15
+ )
16
+ from timberline.types import WorktreeInfo
17
+
18
+
19
+ def _makeInfo(name: str = "obsidian") -> WorktreeInfo:
20
+ return WorktreeInfo(
21
+ name=name,
22
+ branch=f"nik/feature/{name}",
23
+ base_branch="main",
24
+ type="feature",
25
+ path=f"/repo/.tl/{name}",
26
+ )
27
+
28
+
29
+ def test_buildEnvVars():
30
+ info = _makeInfo()
31
+ env = buildEnvVars(info, Path("/repo"))
32
+ assert env["TL_WORKTREE"] == "obsidian"
33
+ assert env["TL_BRANCH"] == "nik/feature/obsidian"
34
+ assert env["TL_BASE"] == "main"
35
+ assert env["TL_ROOT"] == "/repo"
36
+ assert env["TL_TYPE"] == "feature"
37
+
38
+
39
+ def test_buildContextBlock():
40
+ info = _makeInfo()
41
+ others = [_makeInfo("alpha"), _makeInfo("beta")]
42
+ block = buildContextBlock(info, [info, *others], Path("/repo"))
43
+ assert "obsidian" in block
44
+ assert "nik/feature/obsidian" in block
45
+ assert "alpha" in block
46
+ assert "beta" in block
47
+ assert "<!-- timberline:start -->" in block
48
+ assert "<!-- timberline:end -->" in block
49
+
50
+
51
+ def test_injectAgentContext_new_file(tmp_path: Path):
52
+ info = _makeInfo()
53
+ agent = KNOWN_AGENTS["claude"]
54
+ injectAgentContext(agent, tmp_path, info, [info], Path("/repo"))
55
+ content = (tmp_path / "CLAUDE.md").read_text()
56
+ assert "<!-- timberline:start -->" in content
57
+ assert "obsidian" in content
58
+
59
+
60
+ def test_injectAgentContext_existing_without_markers(tmp_path: Path):
61
+ (tmp_path / "CLAUDE.md").write_text("# Existing content\n")
62
+ info = _makeInfo()
63
+ agent = KNOWN_AGENTS["claude"]
64
+ injectAgentContext(agent, tmp_path, info, [info], Path("/repo"))
65
+ content = (tmp_path / "CLAUDE.md").read_text()
66
+ assert "# Existing content" in content
67
+ assert "<!-- timberline:start -->" in content
68
+
69
+
70
+ def test_injectAgentContext_replaces_existing_markers(tmp_path: Path):
71
+ (tmp_path / "CLAUDE.md").write_text(
72
+ "# Header\n<!-- timberline:start -->\nold content\n<!-- timberline:end -->\n# Footer\n"
73
+ )
74
+ info = _makeInfo()
75
+ agent = KNOWN_AGENTS["claude"]
76
+ injectAgentContext(agent, tmp_path, info, [info], Path("/repo"))
77
+ content = (tmp_path / "CLAUDE.md").read_text()
78
+ assert "old content" not in content
79
+ assert "obsidian" in content
80
+ assert "# Header" in content
81
+ assert "# Footer" in content
82
+
83
+
84
+ def test_injectAgentContext_codex_uses_agents_md(tmp_path: Path):
85
+ info = _makeInfo()
86
+ agent = KNOWN_AGENTS["codex"]
87
+ injectAgentContext(agent, tmp_path, info, [info], Path("/repo"))
88
+ assert (tmp_path / "AGENTS.md").exists()
89
+ assert not (tmp_path / "CLAUDE.md").exists()
90
+ content = (tmp_path / "AGENTS.md").read_text()
91
+ assert "obsidian" in content
92
+
93
+
94
+ def test_getAgentDef_known():
95
+ agent = getAgentDef("claude")
96
+ assert agent.binary == "claude"
97
+ assert agent.context_file == "CLAUDE.md"
98
+
99
+
100
+ def test_getAgentDef_unknown():
101
+ with pytest.raises(KeyError, match="Unknown agent"):
102
+ getAgentDef("nonexistent")
103
+
104
+
105
+ def test_detectInstalledAgents_none():
106
+ with patch("timberline.agent.shutil.which", return_value=None):
107
+ assert detectInstalledAgents() == []
108
+
109
+
110
+ def test_detectInstalledAgents_some():
111
+ def mock_which(binary: str) -> str | None:
112
+ return "/usr/bin/claude" if binary == "claude" else None
113
+
114
+ with patch("timberline.agent.shutil.which", side_effect=mock_which):
115
+ result = detectInstalledAgents()
116
+ assert result == ["claude"]