fujimoto 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.
@@ -0,0 +1,16 @@
1
+ name: pre-commit
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ pre-commit:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: astral-sh/setup-uv@v5
14
+
15
+ - name: Run pre-commit
16
+ uses: j178/prek-action@v1
@@ -0,0 +1,53 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ fetch-depth: 0 # hatch-vcs needs tags to derive the version
14
+
15
+ - uses: astral-sh/setup-uv@v5
16
+
17
+ - name: Build sdist and wheel
18
+ run: uv build
19
+
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish-testpypi:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: testpypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
38
+ with:
39
+ repository-url: https://test.pypi.org/legacy/
40
+
41
+ publish-pypi:
42
+ needs: publish-testpypi
43
+ runs-on: ubuntu-latest
44
+ environment: pypi
45
+ permissions:
46
+ id-token: write
47
+ steps:
48
+ - uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+
53
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,44 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0 # hatch-vcs needs tags to derive the version
18
+
19
+ - uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Install dependencies
22
+ run: uv sync
23
+
24
+ - name: Run tests
25
+ run: uv run pytest --cov-report=json:coverage.json
26
+
27
+ - name: Generate coverage badge
28
+ if: github.ref == 'refs/heads/main'
29
+ run: |
30
+ COVERAGE=$(uv run python -c "import json; print(int(json.load(open('coverage.json'))['totals']['percent_covered_display']))")
31
+ uvx anybadge --value="$COVERAGE" --suffix="%" --file=coverage.svg --label=coverage \
32
+ --overwrite 50=red 60=orange 70=yellow 80=green 90=brightgreen
33
+
34
+ - name: Publish badge
35
+ if: github.ref == 'refs/heads/main'
36
+ run: |
37
+ cp coverage.svg /tmp/coverage.svg
38
+ git config user.name "github-actions[bot]"
39
+ git config user.email "github-actions[bot]@users.noreply.github.com"
40
+ git fetch origin badges:badges 2>/dev/null && git checkout badges || { git checkout --orphan badges && git rm -rf . ; }
41
+ cp /tmp/coverage.svg coverage.svg
42
+ git add coverage.svg
43
+ git commit -m "Update coverage badge" || true
44
+ git push origin badges --force
@@ -0,0 +1,15 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Coverage
13
+ coverage.xml
14
+ .coverage
15
+ htmlcov/
@@ -0,0 +1,25 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: trailing-whitespace
7
+ - id: end-of-file-fixer
8
+ - id: no-commit-to-branch
9
+ args: [--branch, main]
10
+
11
+ - repo: https://github.com/astral-sh/ruff-pre-commit
12
+ rev: v0.15.0
13
+ hooks:
14
+ - id: ruff
15
+ args: [--fix]
16
+ - id: ruff-format
17
+
18
+ - repo: local
19
+ hooks:
20
+ - id: ty
21
+ name: ty
22
+ entry: uv run ty check src/
23
+ language: system
24
+ pass_filenames: false
25
+ types: [python]
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,243 @@
1
+ # fujimoto
2
+
3
+ CLI/TUI tool for managing Claude Code sessions in git worktrees and repositories.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ uv sync # Install dependencies
9
+ uv run fujimoto # Run locally (must be inside a git repo)
10
+ uv run pytest # Run tests with coverage
11
+ uv tool install --force --reinstall . # Install globally (re-run after code changes)
12
+ ```
13
+
14
+ ## Environment Variables (all optional)
15
+
16
+ ```sh
17
+ export FUJIMOTO_WORKTREE_ROOT=~/git/worktrees/ # Optional: where worktrees are created
18
+ export FUJIMOTO_GIT_ROOT=~/git/ # Optional: enables project switching
19
+ ```
20
+
21
+ If `FUJIMOTO_WORKTREE_ROOT` is unset, worktrees are created at
22
+ `<repo_root>/.fujimoto/worktrees/` (the `.fujimoto/` directory is auto-gitignored
23
+ via a `.gitignore` containing `*`). If `FUJIMOTO_GIT_ROOT` is unset, the project
24
+ switcher is silently hidden.
25
+
26
+ ## Prerequisites
27
+
28
+ - Python 3.11+
29
+ - tmux (auto-installs via brew if missing)
30
+ - git
31
+
32
+ ## Project Structure
33
+
34
+ ```
35
+ src/fujimoto/
36
+ ├── __init__.py
37
+ ├── cli.py # Textual TUI app, entry point (main()), all UI screens and event handlers
38
+ ├── config.py # Environment variable loading, path construction, session metadata
39
+ ├── git.py # Git subprocess wrappers (worktree lifecycle, branch operations)
40
+ ├── terminal.py # Open native terminal windows (iTerm2 with Terminal.app fallback)
41
+ ├── vscode.py # Open directories in VS Code via the `code` CLI
42
+ ├── tmux.py # tmux session lifecycle (create, attach, kill, list, install)
43
+ └── claude/
44
+ ├── __init__.py # Re-exports public API
45
+ └── log_parser.py # Parse Claude JSONL session logs (state, metadata, session lookup)
46
+ ```
47
+
48
+ ## Architecture
49
+
50
+ ### Entry Point
51
+
52
+ `cli.py:main()` is the package entry point (`pyproject.toml` `[project.scripts]`). It:
53
+ 1. Runs the Textual `SessionApp` in a loop
54
+ 2. After the TUI exits, calls `launch_claude_in_tmux()` if the user selected a session
55
+ 3. When the tmux session is detached, the loop restarts and the TUI reappears
56
+ 4. The loop exits when the user quits the TUI (q/escape/ctrl+c) without selecting a session
57
+
58
+ ### Session Types
59
+
60
+ **Worktree sessions** — isolated git worktree with its own branch:
61
+ - Creates a new branch + working directory via `git worktree add`
62
+ - Finish flow: Push & Create PR, Cherry-pick to base branch, or Discard & Delete
63
+ - Session metadata (base branch) stored in `.fujimoto/meta.json` (auto-gitignored)
64
+
65
+ **Direct sessions** — Claude launched in an existing repo directory:
66
+ - No worktree creation, uses the repo's current branch
67
+ - Multiple concurrent sessions possible on same repo
68
+ - Named `{project}/direct-N` in tmux
69
+
70
+ **Ad hoc sessions** — Claude launched in a temporary directory, outside any git project:
71
+ - For quick questions, investigations, and one-off tasks
72
+ - Working directory is a `tempfile.mkdtemp(prefix="fujimoto-adhoc-")` temp dir
73
+ - Named `adhoc-N` in tmux (not project-scoped)
74
+ - System prompt tells Claude there is no git repository
75
+
76
+ ### Module Responsibilities
77
+
78
+ **`config.py`** — Pure functions, no side effects except directory creation:
79
+ - `get_worktree_root(project_root=None)` — returns `FUJIMOTO_WORKTREE_ROOT` if set, else falls back to `<project_root>/.fujimoto/worktrees/` (ensures `.fujimoto/.gitignore` exists). Raises `ConfigError` only if both are missing.
80
+ - `get_git_projects_root()` — reads `FUJIMOTO_GIT_ROOT`, returns `None` if unset
81
+ - `list_projects()` — scans git root for directories containing `.git`
82
+ - `slugify(title)` — lowercase, replace non-alphanumeric with hyphens, strip/collapse
83
+ - `build_worktree_path(project, title, project_root=None)` — with env var: `{root}/{project}/{YYYYMMDD}-{slug}`; with fallback: `<project_root>/.fujimoto/worktrees/{YYYYMMDD}-{slug}`
84
+ - `get_project_worktrees_dir(project, project_root=None)` — with env var: `{root}/{project}`; with fallback: `<project_root>/.fujimoto/worktrees/`
85
+ - `store_session_meta(path, base_branch)` / `read_session_meta(path)` — JSON metadata
86
+ - `get_next_direct_session_name(project, sessions)` — computes `{project}/direct-N`
87
+ - `get_next_adhoc_session_name(sessions)` — computes `adhoc-N`
88
+
89
+ **`git.py`** — Thin wrappers around `git` subprocess calls:
90
+ - `_run(args, cwd)` — subprocess runner, raises `GitError` on non-zero exit
91
+ - `get_repo_root()` — `git rev-parse --show-toplevel`
92
+ - `get_project_name()` — basename of repo root
93
+ - `get_current_branch()` — `git branch --show-current`
94
+ - `get_default_branch()` — tries `symbolic-ref`, falls back to checking main/master
95
+ - `fetch_and_rebase_branch(branch)` — `git fetch origin` + `git rebase origin/{branch}`
96
+ - `list_branches()` — sorted list of local branch names
97
+ - `create_worktree(path, base_branch, new_branch)` — `git worktree add -b`
98
+ - `remove_worktree(path)` — `git worktree remove --force`
99
+ - `get_unpushed_commits(branch)` — commits not yet on remote
100
+ - `get_merge_base(branch)` — fork point from default branch
101
+ - `is_branch_merged(branch, into)` — `git merge-base --is-ancestor`
102
+ - `has_remote_branch(branch)` — `git ls-remote --heads`
103
+ - `push_branch(branch)` — `git push -u origin`
104
+ - `delete_branch(branch, remote)` — `git branch -D`, optionally remote
105
+ - `cherry_pick_branch(branch, onto)` — cherry-picks commit range onto target
106
+
107
+ **`terminal.py`** — Open native terminal windows in a session's directory:
108
+ - `open_terminal(directory)` — opens iTerm2 if installed, otherwise Terminal.app. Raises `OSError` on non-macOS.
109
+ - `_has_iterm()` — checks for `/Applications/iTerm.app`
110
+ - `_open_iterm(directory)` — AppleScript to create new iTerm2 window
111
+ - `_open_terminal_app(directory)` — `open -a Terminal` fallback
112
+
113
+ **`vscode.py`** — Open directories in VS Code:
114
+ - `open_vscode(directory)` — runs `code <directory>`. Raises `OSError` if the `code` CLI is not on PATH.
115
+ - `_has_vscode()` — checks for `code` on PATH via `shutil.which`
116
+
117
+ **`tmux.py`** — tmux session management:
118
+ - `is_tmux_installed()` / `install_tmux()` — detection and brew install
119
+ - `list_all_sessions()` — lists all active tmux session names
120
+ - `list_project_sessions(project)` — lists active tmux sessions for a project
121
+ - `session_name(project, dir)` — naming convention: `{project}/{dir}`
122
+ - `create_session(name, dir, system_prompt, resume_session_id)` — creates detached session, sets prefix to Ctrl+A, runs `claude` (with optional `--resume`)
123
+ - `create_session_with_command(name, dir, command)` — like `create_session` but with custom command
124
+ - `kill_session(name)` — `tmux kill-session -t`
125
+ - `attach_session(name)` — prints shortcut banner, then `subprocess.run` tmux attach (returns on detach)
126
+ - `launch_claude_in_tmux(project, path, tmux_name, system_prompt, resume_session_id)` — orchestrates create-or-attach, supports resuming previous Claude sessions
127
+
128
+ **`claude/log_parser.py`** — Parse Claude Code's JSONL session logs:
129
+ - `ClaudeLogError` — raised on empty/unreadable logs
130
+ - `EntryType` / `StopReason` / `SessionState` — StrEnums with lenient `from_raw()` parsing (returns `None` for unrecognized values)
131
+ - `ClaudeSession` — frozen dataclass: session_id, state, cwd, git_branch, last_activity, etc.
132
+ - `encode_project_path(path)` — `str(path).replace("/", "-")` (matches Claude's directory encoding)
133
+ - `get_claude_projects_dir()` — `~/.claude/projects`
134
+ - `parse_session(jsonl_path)` — reads JSONL, tracks last meaningful (non-sidechain) entry, derives state
135
+ - `get_sessions_for_path(project_path)` — encodes path, globs `*.jsonl`, returns sorted sessions
136
+
137
+ **`cli.py`** — Textual TUI with async view management:
138
+ - `SessionInfo` — dataclass for session state (type, project, path, tmux name, active status, claude_session_id, claude_state)
139
+ - `SessionApp` — main app class with CSS styling
140
+ - Module-level helpers: `_claude_state_label(state)`, `_relative_time(dt)`, `_get_claude_sessions(root, worktrees)`
141
+ - Instance helpers: `_build_session_label(session, state_suffix)` — generates label text for session items, used by both `_show_home` initial render and `_poll_session_states` in-place updates
142
+ - Views: home (sessions list), session actions submenu, finish flow, confirm dialog, create form, branch select (3 options), branch picker (filterable list), conflict resolution, project switcher (with autocomplete filter), tmux install, error
143
+ - Home screen sections: actions ("New worktree session", "New session in X", "Ad hoc session"), active sessions (with Claude state indicators), inactive worktrees (with Claude state), previous Claude sessions (resumable, capped at 5), switch project
144
+ - Worktree create flow: title → branch select (default w/ fetch & rebase, current branch, another branch → picker) → create
145
+ - Session actions submenu: Connect/Launch, Resume previous session (worktree/direct), Terminate, Resume (claude sessions), Rename, Open terminal, Open in VS Code, Finish (worktree only)
146
+ - Finish flow: Push & Create PR (background Claude), Cherry-pick to base, Discard & Delete
147
+ - All view transitions are `async` — `await _clear_main()` then `await mount()`
148
+ - Session data stored in `_session_map` dict keyed by ListItem ID
149
+ - `_launch_target` is `(project, path, tmux_name, session_type, resume_id)`, set before `self.exit()`
150
+
151
+ ### Error Handling
152
+
153
+ Three custom exception types, all caught in `main()`:
154
+ - `ConfigError` — missing env var
155
+ - `GitError` — git command failures, not in a repo
156
+ - `TmuxError` — tmux not installed, install failure
157
+
158
+ ### Naming Conventions
159
+
160
+ | Thing | Pattern | Example |
161
+ |-------|---------|---------|
162
+ | Worktree directory | `{YYYYMMDD}-{slug}` | `20260309-fix-unit-tests` |
163
+ | Git branch | `worktree/{dir-name}` | `worktree/20260309-fix-unit-tests` |
164
+ | tmux session (worktree) | `{project}/{dir-name}` | `qsic-data/20260309-fix-unit-tests` |
165
+ | tmux session (direct) | `{project}/direct-{N}` | `qsic-data/direct-1` |
166
+ | tmux session (adhoc) | `adhoc-{N}` | `adhoc-1` |
167
+ | Widget ID (direct) | `ds-{project}--direct-{N}` | `ds-qsic-data--direct-1` |
168
+ | Widget ID (claude session) | `cs-{session-id}` | `cs-abc12345-def6-7890` |
169
+
170
+ ### Key Design Decisions
171
+
172
+ - **TUI loop with tmux detach**: The TUI runs in a `while True` loop. After tmux detach (subprocess.run returns), the loop restarts and the TUI reappears. The loop breaks when the user quits without selecting a session.
173
+ - **Per-session tmux config**: Prefix remapped to Ctrl+A, status bar with shortcut hints — all set via `tmux set-option -t` so the user's global config is untouched.
174
+ - **Global install via `uv tool`**: Requires `--force --reinstall` to rebuild the wheel from source. Plain `--force` reuses cached builds.
175
+ - **Session metadata**: `.fujimoto/meta.json` stored in worktree directory records the base branch for cherry-pick targeting. The `.fujimoto/` directory contains a `.gitignore` with `*` so its contents are automatically ignored by git.
176
+ - **Background PR creation**: Uses `claude -p --allowedTools "Bash(git:*) Bash(gh:*)"` in a tmux session for unattended PR creation.
177
+ - **Claude session integration**: The home screen fetches Claude session state from `~/.claude/projects/` JSONL logs via the log parser. Session states: 👀 awaiting input (`WAITING_FOR_USER`), 🛡️ approve tool (`WAITING_FOR_TOOL_APPROVAL`), ⚙ working (`WORKING`), 💤 idle (`IDLE`), no indicator (`UNKNOWN`). State logic: `last-prompt` marker → `IDLE` (session ended). For assistant entries: `stop_reason=tool_use` without a following `tool_result` → `WAITING_FOR_TOOL_APPROVAL` (pending user approval), `stop_reason=tool_use` with `tool_result` → `WORKING`, any other stop reason or no stop reason → `WAITING_FOR_USER`. Last entry is user → `WORKING`. Previous Claude sessions (from the project root, capped at 5) appear as resumable items. Resuming launches `claude --resume SESSION_ID` in a new tmux session. The latest Claude session per path is "claimed" by the corresponding tmux/worktree item to avoid duplication.
178
+ - **Resume previous session — tmux naming**: When resuming from an inactive worktree, the resumed session reuses the worktree's existing tmux session name (e.g., `project/20260101-feature`) instead of generating a new `direct-N` name. This keeps the session correctly identified as a worktree item on subsequent TUI views, so its path and Claude session lookup remain tied to the worktree directory. For active worktrees (original session still alive), a `direct-N` name is used because the worktree name is occupied. The working directory for resumed sessions always comes from `cs.cwd` (the directory recorded in the Claude session log) rather than `session.path`.
179
+ - **Live polling**: The home screen uses `set_interval(3s)` to poll Claude JSONL logs for state changes. When a session's state changes, labels are updated in-place via `label.update()` — the screen is never cleared or rebuilt, which avoids blank-screen flicker. A snapshot dict (`path → (session_id, state)`) is compared each tick to detect changes efficiently. The timer is stopped when navigating away (`_clear_main` cancels it) and restarted by `_show_home`.
180
+
181
+ ## Testing
182
+
183
+ Tests use pytest with pytest-asyncio for TUI tests and pytest-cov for coverage. Run with:
184
+
185
+ ```sh
186
+ uv run pytest
187
+ ```
188
+
189
+ Coverage is reported automatically (configured in `pyproject.toml`).
190
+
191
+ **Maximize test coverage.** Write tests for all new code — unit tests for logic, async TUI tests using Textual's `app.run_test()` pilot for UI flows. Only skip coverage for lines that are genuinely impractical to test (e.g. defensive error handlers in deeply nested async TUI paths that can't be triggered through the pilot). Use `# pragma: no cover` sparingly and only with justification.
192
+
193
+ TUI tests follow this pattern:
194
+ - Patch external dependencies (`git`, `tmux`, `config`) via `_patch_git_info()` helper
195
+ - Use `async with app.run_test() as pilot:` to drive the UI
196
+ - Use `pilot.press()` to simulate keyboard input
197
+ - Navigate to items by setting `list.index` directly (more reliable than repeated `pilot.press("down")`)
198
+ - Assert on app state (`_launch_target`, `_base_branch`, `_session_map`) and DOM queries (`app.query()`)
199
+
200
+ ## Documentation
201
+
202
+ **Keep documentation in sync with code changes.** When making changes to the codebase:
203
+
204
+ - **CLAUDE.md**: Update architecture, module responsibilities, naming conventions, and design decisions to reflect the current state. This is the primary reference — it must always be accurate.
205
+ - **README.md**: Update user-facing docs (usage, home screen layout, features, configuration) when UI or behaviour changes.
206
+ - **CONTRIBUTING.md**: Update developer guidance (project layout, manual testing steps, view patterns) when internal structure changes.
207
+
208
+ When you discover something new about the codebase, tooling, or patterns during a session — incorporate it into the appropriate documentation file rather than leaving it as tribal knowledge.
209
+
210
+ ## Gotchas and Learnings
211
+
212
+ Things discovered during development that are easy to forget:
213
+
214
+ - **Textual widget IDs cannot contain `/`**. tmux session names use `project/name` but widget IDs must use `--` as separator (e.g. `ds-qsic-data--direct-1`).
215
+ - **`git worktree remove` needs `--force`** for worktrees with uncommitted changes — without it the command fails silently in some states.
216
+ - **`git reflog` records branch creation origin** (`branch: Created from main`) — useful for recovering the base branch if `.fujimoto/meta.json` is missing.
217
+ - **`claude -p` (print mode)** runs non-interactively. For background tasks, pair with `--allowedTools` to scope permissions rather than `--dangerously-skip-permissions`.
218
+ - **Global find-replace for renames** works well but always verify test patch target strings — they are plain strings not checked by the import system. Run the full test suite after any rename.
219
+ - **Claude log entry types evolve** — real logs contain `last-prompt`, `queue-operation`, `progress` and other types beyond `assistant`/`user`/`system`/`file-history-snapshot`. The parser skips unrecognized types gracefully. `last-prompt` signals session end → `IDLE` state. `stop_reason=None` on assistant entries means interrupted/canceled (Esc) → `WAITING_FOR_USER`. Always smoke-test against real `~/.claude/projects/` data after changes.
220
+ - **Shift+Enter in tmux requires `extended-keys always` globally** — tmux strips modifier info by default, making Shift+Enter identical to Enter. The fix requires two server/global-level settings: `set-option -g extended-keys always` and `set-option -s -a terminal-features xterm*:extkeys`. Per-session (`-t`) doesn't work. `extended-keys on` (vs `always`) doesn't work because Claude Code doesn't send the kitty keyboard protocol activation sequence. Requires tmux 3.2+. See `_ensure_extended_keys()` in `tmux.py`.
221
+
222
+ ## Releases
223
+
224
+ Releases are published to PyPI by `.github/workflows/release.yml` on `v*`
225
+ tag push. The package version is **derived from the git tag** by `hatch-vcs`
226
+ (`dynamic = ["version"]` in `pyproject.toml`) — do not add a static `version`
227
+ field, do not edit a version string when cutting a release. The release flow,
228
+ recovery procedures, and one-time setup are documented in
229
+ [CONTRIBUTING.md](CONTRIBUTING.md#releasing).
230
+
231
+ ## Git Commits and PRs
232
+
233
+ Do not mention Claude or AI when authoring git commits or pull requests. No co-authored-by lines referencing Claude.
234
+
235
+ ## Linting and Type Checking
236
+
237
+ Pre-commit hooks handle all linting and formatting automatically — do not run `ruff`, `ty`, or other linters manually. Let the hooks run at commit time and fix any issues they report. Any new linting or formatting tools should be added to `.pre-commit-config.yaml`, not run ad hoc.
238
+
239
+ Current hooks:
240
+ - **ruff** — linting and formatting
241
+ - **ty** — type checking (strict: no `unresolved-attribute` allowed)
242
+
243
+ All widget state must use typed instance variables, not dynamic attributes on Textual widgets.
@@ -0,0 +1,179 @@
1
+ # Contributing
2
+
3
+ ## Getting Started
4
+
5
+ ```sh
6
+ git clone https://github.com/jongracecox/fujimoto.git
7
+ cd fujimoto
8
+ uv sync
9
+ ```
10
+
11
+ ## Running Locally
12
+
13
+ You must be inside a git repository. `FUJIMOTO_WORKTREE_ROOT` is optional —
14
+ when unset, worktrees are created at `<repo>/.fujimoto/worktrees/`:
15
+
16
+ ```sh
17
+ export FUJIMOTO_WORKTREE_ROOT=~/git/worktrees/ # optional
18
+ uv run fujimoto
19
+ ```
20
+
21
+ ## Installing Globally
22
+
23
+ After making changes, reinstall to test the global `fujimoto` command:
24
+
25
+ ```sh
26
+ uv tool install --force --reinstall .
27
+ ```
28
+
29
+ Both `--force` and `--reinstall` are required — `--force` alone reuses cached wheel builds and won't pick up code changes.
30
+
31
+ ## Project Layout
32
+
33
+ ```
34
+ src/fujimoto/
35
+ ├── cli.py # Textual TUI app and entry point
36
+ ├── config.py # Env var loading, path construction, session metadata
37
+ ├── git.py # Git subprocess wrappers
38
+ ├── tmux.py # tmux session management
39
+ └── claude/
40
+ ├── __init__.py # Re-exports public API
41
+ └── log_parser.py # Parse Claude JSONL session logs
42
+ ```
43
+
44
+ See [CLAUDE.md](CLAUDE.md) for detailed architecture documentation.
45
+
46
+ ## Code Style
47
+
48
+ Pre-commit hooks enforce:
49
+
50
+ - **ruff** for linting and auto-formatting
51
+ - **ty** for type checking
52
+
53
+ Hooks run automatically on `git commit`. If a hook fails, it may auto-fix files — re-stage and commit again.
54
+
55
+ ### Type Safety
56
+
57
+ The ty type checker runs in strict mode. Key rules:
58
+
59
+ - Do not set dynamic attributes on Textual widgets (e.g. `item._data = value`). Use a dictionary on the app instance instead.
60
+ - All instance variables must be declared with type annotations in `__init__`.
61
+
62
+ ## Architecture Notes
63
+
64
+ ### TUI View Pattern
65
+
66
+ All views follow the same async pattern:
67
+
68
+ ```python
69
+ async def _show_some_view(self) -> None:
70
+ await self._clear_main() # Remove all children from #main
71
+ main = self.query_one("#main")
72
+ await main.mount( # Mount new widgets
73
+ Container(...)
74
+ )
75
+ self.query_one("#some-widget").focus() # Set focus
76
+ ```
77
+
78
+ Every `remove_children()` and `mount()` call must be awaited to prevent DOM race conditions.
79
+
80
+ ### tmux Handoff
81
+
82
+ The Textual app cannot run simultaneously with tmux attach (both need the terminal). The pattern is:
83
+
84
+ 1. TUI sets `self._launch_target = (project, path, tmux_name)`
85
+ 2. TUI calls `self.exit()` to cleanly shut down the event loop
86
+ 3. `main()` reads `_launch_target` after `app.run()` returns
87
+ 4. `launch_claude_in_tmux()` creates or attaches to the tmux session
88
+
89
+ ### Adding a New View
90
+
91
+ 1. Add an `async def _show_*` method following the pattern above
92
+ 2. Add a `@on(ListView.Selected, "#your-list-id")` handler
93
+ 3. Wire navigation from an existing view
94
+ 4. Add any new state to `__init__` with type annotations
95
+
96
+ ### Adding New Git/tmux Operations
97
+
98
+ - Git wrappers go in `git.py` using the `_run()` helper
99
+ - tmux operations go in `tmux.py` using `subprocess.run`
100
+ - Both should raise their respective error types (`GitError`, `TmuxError`)
101
+ - Import and use them in `cli.py`
102
+
103
+ ### Claude Log Integration
104
+
105
+ The `claude/` subpackage parses Claude Code's JSONL session logs (`~/.claude/projects/`). Not yet wired into the TUI — currently a standalone module with its own test suite.
106
+
107
+ - Add new entry types or stop reasons to the StrEnums in `log_parser.py` — unknown values raise `ClaudeLogError` immediately
108
+ - Path encoding matches Claude's convention: `str(path).replace("/", "-")`
109
+ - Test with `tmp_path` fixtures — never access real `~/.claude/` in tests
110
+
111
+ ## Releasing
112
+
113
+ Releases are published to PyPI automatically by `.github/workflows/release.yml`
114
+ when a tag matching `v*` is pushed.
115
+
116
+ ### Versioning
117
+
118
+ The package version is derived from git tags by `hatch-vcs` — there is **no**
119
+ `version` field in `pyproject.toml` to maintain. The version of a built
120
+ artifact is whatever git tag points at the build commit (e.g. tag `v0.1.1` →
121
+ package version `0.1.1`). Builds from untagged commits get a dev version like
122
+ `0.1.1.dev3`.
123
+
124
+ ### Cutting a release
125
+
126
+ ```sh
127
+ git checkout main
128
+ git pull
129
+ git tag v0.1.1
130
+ git push origin v0.1.1
131
+ ```
132
+
133
+ The workflow will then:
134
+
135
+ 1. **Build** sdist + wheel with `uv build` (single artifact reused by both
136
+ publish jobs).
137
+ 2. **Publish to TestPyPI** — waits for manual approval in the `testpypi`
138
+ GitHub environment. After approval, uploads via OIDC trusted publishing.
139
+ 3. **Publish to PyPI** — waits for a second manual approval in the `pypi`
140
+ environment, then uploads.
141
+
142
+ Approve in **Actions → release run → Review deployments**. Between the two
143
+ approvals, sanity-check the upload at https://test.pypi.org/project/fujimoto/.
144
+
145
+ ### Recovering from a bad release
146
+
147
+ PyPI/TestPyPI versions are immutable — you cannot re-upload `v0.1.1` once
148
+ published. To fix issues:
149
+
150
+ - **Caught at TestPyPI stage**: reject the PyPI deployment in Actions, fix
151
+ the problem, push a new tag (e.g. `v0.1.2`).
152
+ - **Caught after PyPI publish**: yank the broken version on pypi.org
153
+ (Manage → Yank), then push a fix tag.
154
+
155
+ Never delete and re-push a tag — the immutability rule still applies on the
156
+ registry side.
157
+
158
+ ### One-time setup (already done)
159
+
160
+ - TestPyPI and PyPI projects exist with pending trusted publishers configured
161
+ for this repo, workflow `release.yml`, environments `testpypi` and `pypi`.
162
+ - GitHub `testpypi` and `pypi` environments exist with required reviewers.
163
+
164
+ If these need to be reconfigured, see PyPA's Trusted Publishing docs:
165
+ https://docs.pypi.org/trusted-publishers/
166
+
167
+ ## Testing Manually
168
+
169
+ 1. Run `fujimoto` from a git repo
170
+ 2. Create a new worktree — verify the directory and git branch are created
171
+ 3. Detach from tmux (`Ctrl+A D`)
172
+ 4. Run `fujimoto` again — the worktree should show a green circle, direct sessions listed
173
+ 5. Select an existing session — should show the actions submenu (Connect/Launch/Finish)
174
+ 6. Test the Finish flow on a worktree with unpushed commits
175
+ 7. Test error cases:
176
+ - Run outside a git repo
177
+ - Create a worktree with a name that already exists
178
+ 8. With `FUJIMOTO_WORKTREE_ROOT` unset: confirm worktrees land in
179
+ `<repo>/.fujimoto/worktrees/` and the directory is gitignored
fujimoto-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jon Grace-Cox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.