@n-seiji/nuthatch 0.2.1 → 0.2.2

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.
package/README.ja.md CHANGED
@@ -1,198 +1,75 @@
1
- [English](README.md)
2
-
3
- <p align="center">
4
- <img src="docs/assets/logo.png" alt="nuthatch — hop between git worktrees" width="560">
5
- </p>
6
-
7
- <p align="center"><em>ナットハッチが木々を渡るように、git worktree 間を hop する。</em></p>
8
-
9
- `nuthatch` は zero-setup な git worktree manager。単一コマンド **`hop`** が、
10
- 任意の branch の worktree へジャンプする — 存在しなければその場で作成する。
11
- 人間 (対話的 picker) と AI coding agent (非対話、`--json`) の両方に対応。
12
-
13
- ## なぜ
14
-
15
- - **Zero setup** — 状態ファイルなし、init コマンドなし。
16
- `git worktree list --porcelain` が唯一の正本。任意のリポジトリで即動く。
17
- - **Convention over config** — worktree は常に
18
- `<root の親>/_worktree/<repo>/<branch>` に置かれるため、一覧やパスの推測が
19
- 速く予測可能。
20
- - **AI native** — 全コマンドが非対話で完結し、`--json` はどこでも使え、path
21
- は stdout・ログは stderr に出て、エラーは次の一手を教えてくれる。
22
-
23
- ## 使い方
24
-
25
- ```sh
26
- hop # worktree/branch を対話的に選んで cd
27
- hop feat/foo # feat/foo の worktree へ cd (未作成なら --create でその場で作成)
28
- hop root # root clone へ cd
29
- hop - # 直前の worktree へ戻る
30
-
31
- hop ls [--json] # worktree 一覧 (dirty, ahead/behind, kind)
32
- hop rm <branch> # worktree を削除 (branch は残す) — managed/external 問わず、dirty には --force が必要
33
- hop clean # ゴミ worktree を自動判定して削除 (managed のみ)
34
- hop root <branch> # root clone を一時的に切替 (動作確認用) —
35
- # 対象 branch が他所で checkout 済みなら、clean かつ unlocked な holder を swap する
36
- hop root - # root clone を元に戻す (root の branch のみ; swap で detach した holder はそのまま)
37
-
38
- hop -- <branch> # 予約コマンドと被る branch 名をエスケープ
39
- # (`hop` 自体と同名の branch (例: `hop -- hop`) にも必要)
40
- hop --help # usage を表示 (-h / hop help でも同じ)
41
-
42
- hop --version # hop の version を表示
43
- hop --update # standalone バイナリを最新リリースへ置換 (それ以外は更新方法を表示)
44
- hop --update --check # 更新があるかを報告するだけ — 何も変更しない
45
- ```
46
-
47
- 同じ branch が複数の worktree で checkout されている場合、branch だけを指定する
48
- `hop rm` は削除対象を推測せず拒否する。対話的 picker は選択した path を、lock 内の
49
- 最終安全確認まで保持する。
50
-
51
- ### 対話的 picker
52
-
53
- picker は候補を section に分けて表示する — 既存の worktree (root が先頭) と
54
- 未作成の branch — ステータスマーカー・整列した列・短縮パス付きで:
55
-
56
- ```
57
- WORKTREES
58
- ❯ ● main root ~/ghq/.../nuthatch
59
- ○ feat/picker managed …/_worktree/feat__picker
60
- ● codex/fix-x ext …/.claude/worktrees/x
61
-
62
- BRANCHES — Enter creates a worktree
63
- + feat/idea local
64
- + origin/hotfix remote
65
-
66
- (●=dirty ○=clean +=not created)
67
- ```
68
-
69
- candidate が 0 件になった section は (header ごと) 丸ごと消える —
70
- 検索クエリで絞り込んだ結果 0 件になった場合も同様。
71
-
72
- 各 section 内の順序は insertion 順ではなく固定: WORKTREES は root が先頭、
73
- 次に managed、その次に external (各グループ内は branch 名昇順、detached
74
- HEAD の worktree はグループ内最後); BRANCHES は remote より local が先
75
- (各グループ内は branch 名昇順)。これは検索フィルタ中も保たれる —
76
- 絞り込んでも残った項目の並びは変わらない。BRANCHES の行は dim 表示になり、
77
- ○/●/+ マーカーに加えて「既に worktree」か「まだ未作成」かが一目でわかる。
78
-
79
- picker は terminal の alternate screen buffer (fzf や vim と同じ仕組み) で
80
- 動くため、scrollback 履歴に残らない — 実行のたびに画面を上書きし、
81
- どう終了しても (選択、Esc/Ctrl-C、割り込み) shell の画面をきれいに復元する。
82
-
83
- ### 対話的 picker のキー
84
-
85
- action panel は候補一覧の隣に列として表示される (overlay ではない) ので、
86
- action を選ぶ間も一覧が見え続ける。横に並べられない幅 (おおよそ 94 列
87
- 未満) の terminal では、一覧の下にスタックする形にフォールバックする。
88
-
89
- | キー | 動作 |
90
- |---|---|
91
- | `Enter` | 選択中の候補へ cd |
92
- | `Tab`, `→`, `Ctrl+L`, `Ctrl+F` | 選択中の候補の action panel を開く (ここで cd / 削除 / root 切替) |
93
- | `Ctrl+X` | 選択中の worktree を削除 (常に先に y/N を確認) |
94
- | `Ctrl+R` | root clone を選択中の branch へ切替 (`external` は先に y/N を確認) |
95
- | `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K` | 選択を移動 (矢印・emacs・vim キーがすべて併用可) |
96
- | `Esc` | 静かにキャンセル — exit 0、stdout は空のまま (shell wrapper は cd しない) |
97
- | `Ctrl+C` | 割り込みとしてキャンセル — exit 130、実際の SIGINT と同じ |
98
-
99
- action panel 内: 同じ上下移動キー (左右は閉じる操作に予約されており、
100
- 移動としては使われない)、`Enter` でハイライト中の action を実行、
101
- `c`/`d`/`r` で cd/delete/switch-root を直接実行、`Esc`、`Tab`、`←`、
102
- `Ctrl+H` で一覧に戻る (`←`/`Ctrl+H` は開く操作の `→`/`Ctrl+L`/`Ctrl+F` と
103
- 対になっている; `Tab` はどちらの状態からでもトグルする — Ghostty のように
104
- Cmd+K のようなキーの組み合わせを Tab に remap する terminal で便利)。
105
- `delete` はすでに作成済みの worktree (`managed` / `external`) すべてに
106
- 表示される (panel からの削除は、`external` worktree と、git が作業ツリーを
107
- 見失った prunable な worktree なら先に y/N を確認する。Ctrl+X は常に
108
- 確認する); `switch root here` は root worktree 自身には表示されず、
109
- `external` worktree に対しては HEAD を detach する可能性があるため
110
- 同じく先に y/N を確認する。削除すると候補一覧が再読み込みされ、picker を
111
- 抜けずに削除を続けられる; cd と switch-root は終了して結果の path を
112
- 出力する (hop の stdout 契約どおり)。
113
-
114
- Shell 統合 (自動 `cd`):
115
-
116
- ```sh
117
- # ~/.zshrc
118
- eval "$(hop init zsh)"
119
- ```
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
3
+ <img src="docs/assets/logo-light.svg" alt="nuthatch" width="360">
4
+ </picture>
5
+
6
+ [English](README.md) · **日本語**
7
+
8
+ **git worktree を、作る・移る・消す。ぜんぶ `hop` ひとつで。**
9
+
10
+ `nuthatch` は、人間と AI coding agent のための zero-setup な git worktree manager。コマンドは `hop` ひとつで、設定も状態ファイルもいらない。[Web サイト](https://www.sijis.me/projects/nuthatch)
11
+
12
+ <table>
13
+ <tr>
14
+ <td align="center" valign="top" width="33%">
15
+ <img src="docs/assets/tile-create.svg" width="200" alt="worktree を作る"><br>
16
+ <b>作る</b><br>
17
+ <sub>branch も worktree も、その場で。</sub>
18
+ </td>
19
+ <td align="center" valign="top" width="33%">
20
+ <img src="docs/assets/tile-jump.svg" width="200" alt="worktree へ移る"><br>
21
+ <b>移る</b><br>
22
+ <sub><code>hop</code> で選び、<code>hop -</code> で戻る。</sub>
23
+ </td>
24
+ <td align="center" valign="top" width="33%">
25
+ <img src="docs/assets/tile-remove.svg" width="200" alt="worktree を消す"><br>
26
+ <b>消す</b><br>
27
+ <sub>1 つずつ、または merge 済みをまとめて。</sub>
28
+ </td>
29
+ </tr>
30
+ </table>
120
31
 
121
32
  ## Install
122
33
 
123
- > まだリリースされていない — バージョンは publish も tag もされていない。
124
- > 最初の `v*` tag が出た後は、以下の選択肢が記載どおりに動く。
125
-
126
- 推奨: GitHub Release のバイナリ (bun 製、mise なら
127
- `mise use github:n-seiji/nuthatch`)。npm 版は Node で動くため git 呼び出しが遅い
128
- (hop ls ~400ms vs ~60ms) — CI や頻繁な呼び出しにはバイナリを使う。
129
-
130
34
  ```sh
131
- # ビルド済みバイナリ (macOS arm64/x64, Linux x64) — Node.js 不要、最速:
132
35
  curl -fsSL https://raw.githubusercontent.com/n-seiji/nuthatch/main/install.sh | sh
133
- mise use github:n-seiji/nuthatch # mise、GitHub Release のバイナリ経由
134
-
135
- # npm (低速: Node 経由で git を呼ぶ — バイナリの hop ls ~60ms に対し ~400ms)
136
- npm i -g @n-seiji/nuthatch # または: bunx @n-seiji/nuthatch
137
- mise use -g npm:@n-seiji/nuthatch # mise、npm 経由
138
36
  ```
139
37
 
140
- install script は `hop` を `~/.local/bin` に置き (`HOP_INSTALL_DIR` で
141
- 上書き可)、常に最新の GitHub Release を取得する; 特定バージョンに固定する
142
- には `HOP_VERSION=vX.Y.Z` を使う。Linux arm64 はビルド済みバイナリが
143
- まだないため、npm install を使う。
144
-
145
- v0.2.0 より後のリリースのバイナリにはビルド来歴 (build provenance) の attestation
146
- が付く。ダウンロードしたバイナリを手動で検証するには:
38
+ 続けて `~/.zshrc` に次を足すと、`hop` が `cd` までしてくれる。
147
39
 
148
40
  ```sh
149
- gh attestation verify ./hop-darwin-arm64 --repo n-seiji/nuthatch \
150
- --signer-workflow github.com/n-seiji/nuthatch/.github/workflows/release.yml
41
+ eval "$(hop init zsh)"
151
42
  ```
152
43
 
153
- `install.sh` は attestation を確認せず、SHA-256 だけを検証する。
154
-
155
- ### 更新
156
-
157
- ```sh
158
- hop --update # standalone バイナリを最新リリースへ置換
159
- hop --update --check # 報告だけ: current / latest / インストール方法 / すべきこと
160
- hop --version # 今どの version か
161
- ```
44
+ npm、mise、Linux arm64、バイナリの検証、更新については [ガイド](docs/guide.ja.md#install) を参照。
162
45
 
163
- `hop --update` が置換するのは `install.sh` で入れたバイナリだけで、置き場の
164
- ディレクトリに書き込めて (バイナリと checksum のダウンロードの前に確認する)、
165
- かつリリースに添付された `.sha256` と SHA-256 が一致した場合に限る — 不一致なら
166
- 何も置換しない (確認するのは SHA-256 のみで、attestation は見ない)。それ以外のインストールでは、何も実行せず、リクエストも送らず、
167
- 更新方法を 1 行表示して終了する (exit 1):
46
+ ## 使い方
168
47
 
169
- | インストール方法 | 更新方法 |
48
+ | コマンド | 動作 |
170
49
  |---|---|
171
- | `install.sh` (`~/.local/bin` のバイナリ) | `hop --update`、または `install.sh` を再実行 |
172
- | mise | `mise upgrade` |
173
- | npm / bun / pnpm / yarn の global | `npm i -g @n-seiji/nuthatch@latest` (または使っているパッケージマネージャの同等コマンド) |
174
- | Homebrew・Nix・aqua・proto・ソースチェックアウト・その他のツール | そのツール自身の upgrade コマンド |
50
+ | `hop` | worktree/branch を対話的に選んで `cd` |
51
+ | `hop <branch>` | その branch の worktree へ `cd` |
52
+ | `hop <branch> --create` | 未作成ならその場で worktree を作成 |
53
+ | `hop -` | 直前の worktree へ戻る |
54
+ | `hop root` | root clone へ `cd` |
55
+ | `hop ls [--json]` | worktree 一覧 (dirty, ahead/behind, kind) |
56
+ | `hop rm <branch>` | worktree を削除 (branch は残す) |
57
+ | `hop clean` | ゴミ worktree を自動判定して削除 (既定では managed のみ) |
58
+ | `hop --update` | standalone バイナリを最新リリースへ置換 |
175
59
 
176
- `hop --update --check` はどのインストール方法でも使え、同じ案内を報告する。
177
- hop がネットワークに出るのは GitHub の固定アドレスへの HTTPS リクエストだけで、
178
- タイムアウトとサイズ上限つき。
60
+ picker とそのキー、`hop root <branch>` などは [ガイド](docs/guide.ja.md#使い方) にある。
179
61
 
180
- ## トラブルシューティング
62
+ ## 安全がデフォルト
181
63
 
182
- `hop: git executable not found. Looked in: …` は git のバイナリを見つけられ
183
- なかったという意味。hop は `PATH` の絶対パスエントリを順に探し、その後
184
- `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` を見る。git が
185
- それ以外の場所にある場合は、直接指定する:
64
+ - `hop rm` は、`--force` を付けない限り dirty な worktree を拒否する。`hop clean` と `hop root` は dirty な worktree に触れない。
65
+ - git が locked と報告する worktree は、`--force` でも削除しない。
66
+ - `_worktree/<repo>/` の外にある (external) worktree を picker から削除するときは y/N を確認する。Ctrl+X は常に確認する。
67
+ - `hop clean` が既定で対象にするのは、hop が管理する worktree だけ。external は `--ext` を付けたときだけ対象になる。
68
+ - `hop rm` は branch を残す。
186
69
 
187
- ```sh
188
- export HOP_GIT=/path/to/git
189
- ```
190
-
191
- ## Agent skill (Claude Code / Codex plugin)
70
+ ## AI coding agent 向け
192
71
 
193
- このリポジトリは plugin marketplace も兼ねており、
194
- [`using-hop`](skills/using-hop/SKILL.md) skill を配布する —
195
- coding agent に `hop` を非対話・安全に扱う方法を教える skill。
72
+ 全コマンドが非対話で完結する。`--json` はどこでも使え、path は stdout、ログは stderr に出る。このリポジトリは [`using-hop`](plugins/hop/skills/using-hop/SKILL.md) skill を plugin として配布している。agent が `hop` を安全に使うための skill。
196
73
 
197
74
  ```sh
198
75
  # Claude Code
@@ -204,11 +81,10 @@ codex plugin marketplace add n-seiji/nuthatch
204
81
  codex plugin install hop
205
82
  ```
206
83
 
207
- ## Docs
208
-
209
- - [docs/design.md](docs/design.md) — 設計書 (全文)
210
- - [AGENTS.md](AGENTS.md) — このリポジトリで作業する coding agent 向けガイド
211
-
212
- ## License
84
+ ## その他
213
85
 
214
- GPL-3.0 — [LICENSE](LICENSE) 参照。
86
+ - [Web サイト](https://www.sijis.me/projects/nuthatch)
87
+ - [ガイド](docs/guide.ja.md): 使い方、picker、インストール、更新、トラブルシューティング
88
+ - [docs/design.md](docs/design.md): 設計書
89
+ - [AGENTS.md](AGENTS.md): このリポジトリで作業する coding agent 向けガイド
90
+ - [LICENSE](LICENSE): GPL-3.0
package/README.md CHANGED
@@ -1,204 +1,75 @@
1
- [日本語](README.ja.md)
2
-
3
- <p align="center">
4
- <img src="docs/assets/logo.png" alt="nuthatch — hop between git worktrees" width="560">
5
- </p>
6
-
7
- <p align="center"><em>Hop between git worktrees like a nuthatch hops between trees.</em></p>
8
-
9
- `nuthatch` is a zero-setup git worktree manager. Its single command, **`hop`**,
10
- jumps you to the worktree of any branch — creating it on the fly when it
11
- doesn't exist yet. Built for humans (interactive picker) and AI coding agents
12
- (non-interactive, `--json`) alike.
13
-
14
- ## Why
15
-
16
- - **Zero setup** — no state files, no init. `git worktree list --porcelain` is
17
- the single source of truth. Works in any repository immediately.
18
- - **Convention over config** — worktrees always live at
19
- `<parent-of-root>/_worktree/<repo>/<branch>`, so listing and path inference
20
- are fast and predictable.
21
- - **AI native** — every command completes non-interactively, `--json`
22
- everywhere, paths on stdout / logs on stderr, and errors that tell you the
23
- next step.
24
-
25
- ## Usage
26
-
27
- ```sh
28
- hop # pick a worktree/branch interactively and cd into it
29
- hop feat/foo # cd into feat/foo's worktree (add --create to create it if missing)
30
- hop root # cd into the root clone
31
- hop - # cd back to the previous worktree
32
-
33
- hop ls [--json] # list worktrees (dirty, ahead/behind, kind)
34
- hop rm <branch> # remove a worktree (branch is kept) — managed or external, dirty needs --force
35
- hop clean # auto-detect and remove garbage worktrees (managed only)
36
- hop root <branch> # temporarily switch the root clone (for verification) —
37
- # swaps out a clean, unlocked holder if the branch is checked out elsewhere
38
- hop root - # switch the root clone back (only root's branch; a swapped-out holder stays detached)
39
-
40
- hop -- <branch> # escape a branch name that collides with a reserved command
41
- # (also required for a branch named the same as your `hop` binary/alias, e.g. `hop -- hop`)
42
- hop --help # print usage (also -h / hop help)
43
-
44
- hop --version # print hop's version
45
- hop --update # replace a standalone binary with the latest release (other installs: prints how to update)
46
- hop --update --check # only report whether an update is available — changes nothing
47
- ```
48
-
49
- If Git reports the same branch checked out in multiple worktrees, the
50
- branch-only `hop rm` command refuses rather than guessing which path to
51
- remove. The interactive picker keeps the selected path through the final
52
- lock-protected safety check.
53
-
54
- ### Interactive picker
55
-
56
- The picker groups candidates into sections — existing worktrees (root first)
57
- and not-yet-created branches — with a status marker, aligned columns, and
58
- shortened paths:
59
-
60
- ```
61
- WORKTREES
62
- ❯ ● main root ~/ghq/.../nuthatch
63
- ○ feat/picker managed …/_worktree/feat__picker
64
- ● codex/fix-x ext …/.claude/worktrees/x
65
-
66
- BRANCHES — Enter creates a worktree
67
- + feat/idea local
68
- + origin/hotfix remote
69
-
70
- (●=dirty ○=clean +=not created)
71
- ```
72
-
73
- A section (header included) disappears entirely when it has no candidates —
74
- including when a search query filters it down to zero.
75
-
76
- Ordering within each section is fixed, not insertion order: WORKTREES puts
77
- root first, then managed worktrees, then external ones (branch name
78
- ascending within each group, detached-HEAD worktrees last within theirs);
79
- BRANCHES puts local branches before remote ones (branch name ascending
80
- within each group). This holds under search filtering too — narrowing the
81
- list never reshuffles what's left. BRANCHES rows render dim so "already a
82
- worktree" vs. "not created yet" reads at a glance, on top of the ○/●/+
83
- markers.
84
-
85
- The picker runs in the terminal's alternate screen buffer (the same
86
- mechanism fzf and vim use), so it never gets pushed into your scrollback
87
- history — each run paints over itself and cleanly restores your shell's
88
- screen on exit, however it exits (selection, Esc/Ctrl-C, or an interrupt).
89
-
90
- ### Interactive picker keys
91
-
92
- The action panel renders as a column beside the candidate list (not an
93
- overlay), so the list stays visible while you pick an action. On terminals
94
- too narrow to fit both (under about 94 columns) it falls back to stacking
95
- below the list instead.
96
-
97
- | Key | Action |
98
- |---|---|
99
- | `Enter` | cd into the selected candidate |
100
- | `Tab`, `→`, `Ctrl+L`, `Ctrl+F` | Open the action panel for the selected candidate (cd / delete / switch root here) |
101
- | `Ctrl+X` | Delete the selected worktree (always asks y/N first) |
102
- | `Ctrl+R` | Switch the root clone to the selected branch (asks y/N first for an `external` worktree) |
103
- | `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K` | Move the selection (arrow, emacs, and vim keys all work side by side) |
104
- | `Esc` | Cancel quietly — exit 0, stdout stays empty (the shell wrapper just doesn't cd) |
105
- | `Ctrl+C` | Cancel like an interrupt — exit 130, same as a real SIGINT |
106
-
107
- Inside the action panel: the same up/down movement keys (left/right are
108
- reserved for closing, so they never double as movement), `Enter` to run the
109
- highlighted action, `c`/`d`/`r` to run cd/delete/switch-root directly, and
110
- `Esc`, `Tab`, `←`, or `Ctrl+H` to close it back to the list (`←`/`Ctrl+H`
111
- mirror the `→`/`Ctrl+L`/`Ctrl+F` that open it; `Tab` toggles either way —
112
- handy on terminals like Ghostty that remap a chord such as Cmd+K to Tab). `delete`
113
- appears for any already-created worktree (`managed` or `external` — from the
114
- panel, deleting an `external` worktree, or a prunable one (git can no longer
115
- find its working tree), asks y/N first; Ctrl+X always asks); `switch
116
- root here` doesn't appear on the root worktree itself, and also asks y/N
117
- first for an `external` worktree, since it can detach that worktree's HEAD.
118
- Deleting reloads the
119
- candidate list so you can keep deleting without leaving the picker; cd and
120
- switch-root exit and print the resulting path, per hop's stdout contract.
121
-
122
- Shell integration (auto-`cd`):
123
-
124
- ```sh
125
- # ~/.zshrc
126
- eval "$(hop init zsh)"
127
- ```
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
3
+ <img src="docs/assets/logo-light.svg" alt="nuthatch" width="360">
4
+ </picture>
5
+
6
+ **English** · [日本語](README.ja.md)
7
+
8
+ **Create, jump between, and remove git worktrees. All with `hop`.**
9
+
10
+ `nuthatch` is a zero-setup git worktree manager for humans and AI coding agents. Its one command, `hop`, needs no config and no state files. [Website](https://www.sijis.me/en/projects/nuthatch)
11
+
12
+ <table>
13
+ <tr>
14
+ <td align="center" valign="top" width="33%">
15
+ <img src="docs/assets/tile-create.svg" width="200" alt="Create a worktree"><br>
16
+ <b>Create</b><br>
17
+ <sub>The branch and its worktree, on the spot.</sub>
18
+ </td>
19
+ <td align="center" valign="top" width="33%">
20
+ <img src="docs/assets/tile-jump.svg" width="200" alt="Jump between worktrees"><br>
21
+ <b>Jump</b><br>
22
+ <sub><code>hop</code> to pick, <code>hop -</code> to go back.</sub>
23
+ </td>
24
+ <td align="center" valign="top" width="33%">
25
+ <img src="docs/assets/tile-remove.svg" width="200" alt="Remove a worktree"><br>
26
+ <b>Remove</b><br>
27
+ <sub>One at a time, or clean up what's merged.</sub>
28
+ </td>
29
+ </tr>
30
+ </table>
128
31
 
129
32
  ## Install
130
33
 
131
- Recommended: the GitHub Release binary (built with bun; via mise,
132
- `mise use github:n-seiji/nuthatch`). The npm version runs on Node, so its git
133
- calls are slower (hop ls ~400ms vs ~60ms) — prefer the binary for CI or
134
- frequent invocations.
135
-
136
34
  ```sh
137
- # Prebuilt binary (macOS arm64/x64, Linux x64) — no Node.js required, fastest:
138
35
  curl -fsSL https://raw.githubusercontent.com/n-seiji/nuthatch/main/install.sh | sh
139
- mise use github:n-seiji/nuthatch # mise, via the GitHub Release binary
140
-
141
- # npm (slower: shells out to git via Node — hop ls ~400ms vs ~60ms for the binary)
142
- npm i -g @n-seiji/nuthatch # or: bunx @n-seiji/nuthatch
143
- mise use -g npm:@n-seiji/nuthatch # mise, via npm
144
36
  ```
145
37
 
146
- The install script places `hop` in `~/.local/bin` (override with
147
- `HOP_INSTALL_DIR`) and always fetches the latest GitHub Release; pin a
148
- specific version with `HOP_VERSION=vX.Y.Z`. Linux arm64 has no prebuilt
149
- binary yet — use the npm install instead.
150
-
151
- Release binaries from releases after v0.2.0 carry a build-provenance
152
- attestation. To verify a downloaded binary by hand:
38
+ Then add this to `~/.zshrc` so `hop` can `cd` for you:
153
39
 
154
40
  ```sh
155
- gh attestation verify ./hop-darwin-arm64 --repo n-seiji/nuthatch \
156
- --signer-workflow github.com/n-seiji/nuthatch/.github/workflows/release.yml
41
+ eval "$(hop init zsh)"
157
42
  ```
158
43
 
159
- `install.sh` does not check it; it verifies the SHA-256 only.
44
+ For npm, mise, Linux arm64, verifying binaries, and updating, see the [guide](docs/guide.md#install).
160
45
 
161
- ### Updating
162
-
163
- ```sh
164
- hop --update # replace a standalone binary with the latest release
165
- hop --update --check # just report: current, latest, how hop was installed, what to do
166
- hop --version # which version do I have?
167
- ```
168
-
169
- `hop --update` only replaces the binary that `install.sh` installs, and only if
170
- its directory is writable (checked before the binary or its checksum is
171
- downloaded) and its SHA-256 matches the `.sha256` attached to the release; on a
172
- mismatch nothing is replaced (SHA-256 only; attestations are not checked). For any other install it runs nothing, makes no
173
- request, and prints one line saying how to update it (exit 1):
46
+ ## Usage
174
47
 
175
- | Installed with | Update with |
48
+ | Command | What it does |
176
49
  |---|---|
177
- | `install.sh` (binary in `~/.local/bin`) | `hop --update`, or re-run `install.sh` |
178
- | mise | `mise upgrade` |
179
- | npm / bun / pnpm / yarn global | `npm i -g @n-seiji/nuthatch@latest` (or your package manager's equivalent) |
180
- | Homebrew, Nix, aqua, proto, a source checkout, any other tool | that tool's own upgrade command |
50
+ | `hop` | Pick a worktree or branch interactively and `cd` into it |
51
+ | `hop <branch>` | `cd` into the branch's worktree |
52
+ | `hop <branch> --create` | Create the worktree if it is missing |
53
+ | `hop -` | `cd` back to the previous worktree |
54
+ | `hop root` | `cd` into the root clone |
55
+ | `hop ls [--json]` | List worktrees (dirty, ahead/behind, kind) |
56
+ | `hop rm <branch>` | Remove a worktree. The branch is kept |
57
+ | `hop clean` | Find and remove garbage worktrees (managed only by default) |
58
+ | `hop --update` | Replace a standalone binary with the latest release |
181
59
 
182
- `hop --update --check` works for every install and reports the same hint. The
183
- only network traffic is HTTPS requests to fixed GitHub addresses, each with a
184
- timeout and a size cap.
60
+ The [guide](docs/guide.md#usage) covers the picker and its keys, `hop root <branch>`, and the rest.
185
61
 
186
- ## Troubleshooting
62
+ ## Safe by default
187
63
 
188
- `hop: git executable not found. Looked in: …` means hop could not find a git
189
- binary. hop searches every absolute entry in `PATH`, then `/opt/homebrew/bin`,
190
- `/usr/local/bin`, `/usr/bin` and `/bin`. If your git lives somewhere else,
191
- point hop straight at it:
64
+ - `hop rm` refuses a dirty worktree unless you pass `--force`. `hop clean` and `hop root` never touch one.
65
+ - A worktree that git reports as locked is never removed, even with `--force`.
66
+ - Deleting an external worktree (one not under `_worktree/<repo>/`) from the picker asks y/N. Ctrl+X always asks.
67
+ - By default, `hop clean` only picks worktrees that hop manages. External ones need `--ext`.
68
+ - `hop rm` keeps the branch.
192
69
 
193
- ```sh
194
- export HOP_GIT=/path/to/git
195
- ```
196
-
197
- ## Agent skill (Claude Code / Codex plugin)
70
+ ## For AI coding agents
198
71
 
199
- This repo doubles as a plugin marketplace that ships the
200
- [`using-hop`](skills/using-hop/SKILL.md) skill — it teaches coding agents how
201
- to drive `hop` non-interactively and safely.
72
+ Every command can complete non-interactively. `--json` works everywhere, paths go to stdout, and logs go to stderr. The repo ships the [`using-hop`](plugins/hop/skills/using-hop/SKILL.md) skill as a plugin. It teaches agents to drive `hop` safely.
202
73
 
203
74
  ```sh
204
75
  # Claude Code
@@ -210,11 +81,10 @@ codex plugin marketplace add n-seiji/nuthatch
210
81
  codex plugin install hop
211
82
  ```
212
83
 
213
- ## Docs
214
-
215
- - [docs/design.md](docs/design.md) — full design document
216
- - [AGENTS.md](AGENTS.md) — guide for coding agents working on this repo
217
-
218
- ## License
84
+ ## More
219
85
 
220
- GPL-3.0 — see [LICENSE](LICENSE).
86
+ - [Website](https://www.sijis.me/en/projects/nuthatch)
87
+ - [Guide](docs/guide.md): usage, picker, install, updating, troubleshooting
88
+ - [docs/design.md](docs/design.md): the design document
89
+ - [AGENTS.md](AGENTS.md): guide for coding agents working on this repo
90
+ - [LICENSE](LICENSE): GPL-3.0
@@ -12,7 +12,7 @@ import {
12
12
  loadRepoContext,
13
13
  nestedWorktrees,
14
14
  worktreeDirtyState
15
- } from "./cli-y8vsqre6.js";
15
+ } from "./cli-4f1vz23g.js";
16
16
  import {
17
17
  EXIT_USAGE_ERROR,
18
18
  ok,
@@ -9,7 +9,7 @@ import {
9
9
  loadRepoContext,
10
10
  isWorktreeDirty,
11
11
  resolveBranchCheckout
12
- } from "./cli-y8vsqre6.js";
12
+ } from "./cli-4f1vz23g.js";
13
13
  import {
14
14
  EXIT_SAFE_REJECTION,
15
15
  ok,
@@ -37,7 +37,7 @@ var reportFatalError = (command, json, error) => {
37
37
  // package.json
38
38
  var package_default = {
39
39
  name: "@n-seiji/nuthatch",
40
- version: "0.2.1",
40
+ version: "0.2.2",
41
41
  description: "git worktree manager (hop)",
42
42
  homepage: "https://github.com/n-seiji/nuthatch#readme",
43
43
  bugs: {
@@ -1,10 +1,10 @@
1
1
  import"./cli-ph0gp56k.js";
2
2
  import {
3
3
  rm2
4
- } from "./cli-9n04gsq1.js";
4
+ } from "./cli-vt5mddc4.js";
5
5
  import {
6
6
  root2
7
- } from "./cli-hz0r7zw8.js";
7
+ } from "./cli-aj2y82fq.js";
8
8
  import {
9
9
  graphemes,
10
10
  displayWidth,
@@ -26,7 +26,7 @@ import {
26
26
  isOptionLikeRef,
27
27
  loadRepoContext,
28
28
  worktreeDirtyState
29
- } from "./cli-y8vsqre6.js";
29
+ } from "./cli-4f1vz23g.js";
30
30
  import {
31
31
  EXIT_CANCELLED,
32
32
  ok
@@ -3,7 +3,7 @@ import {
3
3
  wantsJson,
4
4
  reportFatalError,
5
5
  VERSION
6
- } from "./cli-sre95k9x.js";
6
+ } from "./cli-mx5n3y1f.js";
7
7
  import {
8
8
  messageOf
9
9
  } from "./cli-ph0gp56k.js";
@@ -9,7 +9,7 @@ import {
9
9
  loadRepoContext,
10
10
  nestedWorktrees,
11
11
  worktreeDirtyState
12
- } from "./cli-y8vsqre6.js";
12
+ } from "./cli-4f1vz23g.js";
13
13
  import {
14
14
  EXIT_GENERAL_ERROR,
15
15
  EXIT_USAGE_ERROR,
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  wantsJson,
10
10
  reportFatalError,
11
11
  VERSION
12
- } from "./cli-sre95k9x.js";
12
+ } from "./cli-mx5n3y1f.js";
13
13
  import {
14
14
  GIT_EXECUTABLE_OVERRIDE_ENV,
15
15
  GIT_NOT_FOUND_ERROR_CODE,
@@ -442,14 +442,14 @@ var getPorts = () => {
442
442
  var JSON_FLAG = "json";
443
443
  var runLs = async (json) => {
444
444
  const [{ ls }, { reportLs }] = await Promise.all([
445
- import("./ls-3hyjf60d.js"),
445
+ import("./ls-hvwtaxv8.js"),
446
446
  import("./render-ls-x4hpnsvy.js")
447
447
  ]);
448
448
  const { git, fs } = getPorts();
449
449
  reportLs(await ls(git, fs, { cwd: process.cwd() }), json);
450
450
  };
451
451
  var runJump = async (target, options) => {
452
- const { jump } = await import("./jump-1zkraat1.js");
452
+ const { jump } = await import("./jump-vrwxwb1f.js");
453
453
  const { git, fs } = getPorts();
454
454
  const result = await jump(git, fs, {
455
455
  cwd: process.cwd(),
@@ -465,7 +465,7 @@ var runInteractivePick = async (json) => {
465
465
  loadPickCandidates,
466
466
  renderSwitchRootOutcome,
467
467
  runInteractivePicker
468
- } = await import("./cli-pick-yjd4qpd9.js");
468
+ } = await import("./cli-pick-8qtma4v0.js");
469
469
  const { git, fs } = getPorts();
470
470
  const candidates = await loadPickCandidates(git, fs, json);
471
471
  if (candidates === null) {
@@ -524,7 +524,7 @@ var runRm = async (tokens) => {
524
524
  positionals: [{ name: "branch", required: true }],
525
525
  booleans: ["force", JSON_FLAG]
526
526
  });
527
- const { rm } = await import("./rm-eg3j8ae1.js");
527
+ const { rm } = await import("./rm-a5d1c4mh.js");
528
528
  const { git, fs } = getPorts();
529
529
  const result = await rm(git, fs, {
530
530
  cwd: process.cwd(),
@@ -537,7 +537,7 @@ var runClean = async (tokens) => {
537
537
  const args = parseCliArgs(tokens, {
538
538
  booleans: ["ext", "withBranch", "dryRun", "yes", JSON_FLAG]
539
539
  });
540
- const { clean } = await import("./clean-rn2476my.js");
540
+ const { clean } = await import("./clean-6q90wtve.js");
541
541
  const { git, fs, term } = getPorts();
542
542
  const result = await clean(git, fs, term, {
543
543
  cwd: process.cwd(),
@@ -554,7 +554,7 @@ var runRoot = async (tokens) => {
554
554
  booleans: [JSON_FLAG],
555
555
  strings: ["track"]
556
556
  });
557
- const { root } = await import("./root-49vcb8x2.js");
557
+ const { root } = await import("./root-zbrsb9md.js");
558
558
  const { git, fs } = getPorts();
559
559
  const { branch } = args.positionals;
560
560
  const { track } = args.strings;
@@ -592,7 +592,7 @@ var dispatchAndRun = async (dispatch) => {
592
592
  await (command === undefined ? runJumpFromArgs(dispatch.args) : command(dispatch.args));
593
593
  };
594
594
  var runUpdate = async (rawArgs) => {
595
- const update = await import("./cli-update-x0r4b4xk.js");
595
+ const update = await import("./cli-update-rjqr81xx.js");
596
596
  await update.runUpdate(rawArgs, getPorts().term);
597
597
  };
598
598
  var main = async () => {
@@ -8,7 +8,7 @@ import {
8
8
  refArgumentRejection,
9
9
  loadRepoContext,
10
10
  resolveBranchCheckout
11
- } from "./cli-y8vsqre6.js";
11
+ } from "./cli-4f1vz23g.js";
12
12
  import {
13
13
  EXIT_GENERAL_ERROR,
14
14
  EXIT_SAFE_REJECTION,
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  loadRepoContext,
6
6
  worktreeDirtyState
7
- } from "./cli-y8vsqre6.js";
7
+ } from "./cli-4f1vz23g.js";
8
8
  import {
9
9
  ok
10
10
  } from "./cli-77yanq37.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  rm2
3
- } from "./cli-9n04gsq1.js";
3
+ } from "./cli-vt5mddc4.js";
4
4
  import"./cli-ph0gp56k.js";
5
5
  export {
6
6
  rm2 as rm
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  root2
3
- } from "./cli-hz0r7zw8.js";
3
+ } from "./cli-aj2y82fq.js";
4
4
  import"./cli-ph0gp56k.js";
5
5
  export {
6
6
  root2 as root
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n-seiji/nuthatch",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "git worktree manager (hop)",
5
5
  "homepage": "https://github.com/n-seiji/nuthatch#readme",
6
6
  "bugs": {
@@ -12,14 +12,14 @@ import {
12
12
  classifyWorktreePath
13
13
  } from "./cli-a56b2ar0.js";
14
14
 
15
+ // src/infra/repo.ts
16
+ import { basename, dirname, join } from "node:path";
17
+
15
18
  // src/domain/branch-name.ts
16
19
  var isOptionLikeRef = (name) => name.startsWith("-");
17
20
  var optionLikeRefRejection = (label, name) => isOptionLikeRef(name) ? fail(EXIT_USAGE_ERROR, `Refusing ${label} "${name}": names starting with "-" would be parsed as git options.`) : null;
18
21
  var refArgumentRejection = (branch, track) => optionLikeRefRejection("branch", branch) ?? (track === undefined ? null : optionLikeRefRejection("--track ref", track));
19
22
 
20
- // src/infra/repo.ts
21
- import { basename, dirname, join } from "node:path";
22
-
23
23
  // src/domain/tracking.ts
24
24
  var resolveTrackingRef = (branch, remotes) => {
25
25
  if (remotes.includes("origin")) {