arbors 0.1.0
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/.claude-plugin/plugin.json +7 -0
- package/.oxlintrc.json +9 -0
- package/README.ja.md +131 -0
- package/README.ko.md +131 -0
- package/README.md +131 -0
- package/bin/arbors.ts +278 -0
- package/dist/arbors.js +1094 -0
- package/dist/arbors.js.map +1 -0
- package/dist/bun-EMN2NS2M.js +48 -0
- package/dist/bun-EMN2NS2M.js.map +1 -0
- package/dist/ja-F4DBSAAZ.js +38 -0
- package/dist/ja-F4DBSAAZ.js.map +1 -0
- package/dist/ko-MTIAHJOR.js +38 -0
- package/dist/ko-MTIAHJOR.js.map +1 -0
- package/dist/node-LCODN3HC.js +56 -0
- package/dist/node-LCODN3HC.js.map +1 -0
- package/package.json +54 -0
- package/pnpm-workspace.yaml +1 -0
- package/shell/arbors-wrapper.sh +21 -0
- package/shell/arbors-wrapper.zsh +21 -0
- package/skills/arbors-usage/SKILL.md +129 -0
- package/src/config.ts +66 -0
- package/src/git/exclude.ts +63 -0
- package/src/git/safety.ts +40 -0
- package/src/git/worktree.ts +171 -0
- package/src/i18n/en.ts +63 -0
- package/src/i18n/index.ts +37 -0
- package/src/i18n/ja.ts +40 -0
- package/src/i18n/ko.ts +40 -0
- package/src/project/registry.ts +108 -0
- package/src/project/setup.ts +74 -0
- package/src/runtime/adapter.ts +16 -0
- package/src/runtime/bun.ts +49 -0
- package/src/runtime/index.ts +17 -0
- package/src/runtime/node.ts +58 -0
- package/src/tui/App.tsx +87 -0
- package/src/tui/FuzzyList.tsx +111 -0
- package/src/tui/ProjectSelector.tsx +48 -0
- package/src/tui/WorktreeSelector.tsx +46 -0
- package/tests/config.test.ts +108 -0
- package/tests/exclude.test.ts +120 -0
- package/tests/i18n.test.ts +75 -0
- package/tests/registry.test.ts +136 -0
- package/tests/safety.test.ts +58 -0
- package/tests/setup-detection.test.ts +105 -0
- package/tests/setup.test.ts +87 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arbors",
|
|
3
|
+
"description": "Git worktree manager — create, switch, and remove worktrees with auto dependency install, exclude file copying, and interactive fuzzy search TUI. Use this plugin when working with git worktrees or managing parallel development branches.",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "sonsu"
|
|
6
|
+
}
|
|
7
|
+
}
|
package/.oxlintrc.json
ADDED
package/README.ja.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# arbors
|
|
2
|
+
|
|
3
|
+
[한국어](./README.ko.md) | [English](./README.md)
|
|
4
|
+
|
|
5
|
+
git worktreeを簡単に扱うためのCLIツール。
|
|
6
|
+
|
|
7
|
+
ブランチごとに別ディレクトリを作成し、**stash/switchなしで**複数ブランチを同時に作業できる。worktree作成時にexcludeファイルのコピーと依存関係のインストールを自動で行う。
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone git@github.com:sungsulee/arbors.git
|
|
13
|
+
cd arbors
|
|
14
|
+
pnpm install && pnpm build
|
|
15
|
+
npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Shell integration(worktree切り替え後の自動`cd`):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# ~/.zshrc
|
|
22
|
+
source /path/to/arbors/shell/arbors-wrapper.zsh
|
|
23
|
+
|
|
24
|
+
# ~/.bashrc
|
|
25
|
+
source /path/to/arbors/shell/arbors-wrapper.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Workflows
|
|
29
|
+
|
|
30
|
+
### 新機能開発
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# mainを基準に新しいブランチ + worktreeを作成
|
|
34
|
+
arbors add -c feature/login --base main
|
|
35
|
+
|
|
36
|
+
# 自動で以下を実行:
|
|
37
|
+
# 1. git fetch origin main
|
|
38
|
+
# 2. ~/arbors/{repo}/feature-login にworktreeを作成
|
|
39
|
+
# 3. .git/info/excludeに記載されたファイルをコピー(.envなど)
|
|
40
|
+
# 4. pnpm install(lockfileから自動検出)
|
|
41
|
+
|
|
42
|
+
cd ~/arbors/my-project/feature-login
|
|
43
|
+
# 作業開始
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
作業が終わったら:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
arbors remove feature/login
|
|
50
|
+
# コミットされていない変更がある場合は削除を拒否する
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 同僚のPRコードレビュー
|
|
54
|
+
|
|
55
|
+
リモートブランチをローカルworktreeとしてチェックアウト:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# originから自動でfetch + worktree作成
|
|
59
|
+
arbors add feature/payment
|
|
60
|
+
|
|
61
|
+
# ローカルに既にあるブランチならworktreeのみ作成
|
|
62
|
+
# → ローカル優先、なければoriginから取得
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
レビューが終わったら:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
arbors remove feature/payment
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 複数ブランチの同時作業
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
arbors add -c feature/auth --base main
|
|
75
|
+
arbors add -c fix/header-bug --base main
|
|
76
|
+
|
|
77
|
+
arbors list
|
|
78
|
+
# feature/auth ~/arbors/my-project/feature-auth
|
|
79
|
+
# fix/header-bug ~/arbors/my-project/fix-header-bug
|
|
80
|
+
|
|
81
|
+
# 各ディレクトリで独立して作業。stash不要。
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
arbors add <branch> 既存ブランチをチェックアウト(ローカル → リモート自動)
|
|
88
|
+
arbors add -c <branch> [--base <branch>] 新しいブランチ + worktree作成
|
|
89
|
+
arbors remove <branch> worktree削除(安全チェック付き)
|
|
90
|
+
arbors list [--plain] 管理中のworktree一覧
|
|
91
|
+
arbors excluded excludeパターン確認
|
|
92
|
+
arbors config 現在の設定確認
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
`~/.arbors/config.json`(グローバル)または `.arbors/config.json`(プロジェクト別、優先):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"runtime": "node",
|
|
102
|
+
"language": "ja",
|
|
103
|
+
"packageManager": "auto",
|
|
104
|
+
"copyExcludes": true,
|
|
105
|
+
"copySkip": ["node_modules"],
|
|
106
|
+
"worktreeDir": "~/arbors/{repo}"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Key | Values | Default |
|
|
111
|
+
| ---------------- | ------------------------------------- | ------------------- |
|
|
112
|
+
| `runtime` | `"node"`, `"bun"` | `"node"` |
|
|
113
|
+
| `language` | `"en"`, `"ko"`, `"ja"` | `"en"` |
|
|
114
|
+
| `packageManager` | `"auto"`, `"pnpm"`, `"yarn"`, `"npm"` | `"auto"` |
|
|
115
|
+
| `copyExcludes` | `true`, `false` | `true` |
|
|
116
|
+
| `copySkip` | `string[]` | `["node_modules"]` |
|
|
117
|
+
| `worktreeDir` | string (`{repo}` placeholder) | `"~/arbors/{repo}"` |
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pnpm test # vitest
|
|
123
|
+
pnpm lint # oxlint
|
|
124
|
+
pnpm format # oxfmt
|
|
125
|
+
pnpm build # tsup
|
|
126
|
+
pnpm typecheck # tsc --noEmit
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# arbors
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
git worktree를 편하게 쓰기 위한 CLI 도구.
|
|
6
|
+
|
|
7
|
+
브랜치마다 별도의 디렉토리를 만들어서, **stash/switch 없이** 여러 브랜치를 동시에 작업할 수 있다. worktree 생성 시 exclude 파일 복사, 의존성 설치까지 자동으로 처리한다.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone git@github.com:sungsulee/arbors.git
|
|
13
|
+
cd arbors
|
|
14
|
+
pnpm install && pnpm build
|
|
15
|
+
npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Shell integration (worktree 전환 후 자동 `cd`):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# ~/.zshrc
|
|
22
|
+
source /path/to/arbors/shell/arbors-wrapper.zsh
|
|
23
|
+
|
|
24
|
+
# ~/.bashrc
|
|
25
|
+
source /path/to/arbors/shell/arbors-wrapper.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Workflows
|
|
29
|
+
|
|
30
|
+
### 새 기능 개발
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# main 기준으로 새 브랜치 + worktree 생성
|
|
34
|
+
arbors add -c feature/login --base main
|
|
35
|
+
|
|
36
|
+
# 자동으로 다음을 수행:
|
|
37
|
+
# 1. git fetch origin main
|
|
38
|
+
# 2. ~/arbors/{repo}/feature-login 에 worktree 생성
|
|
39
|
+
# 3. .git/info/exclude에 있는 파일들 복사 (.env 등)
|
|
40
|
+
# 4. pnpm install (lockfile 기준 자동 감지)
|
|
41
|
+
|
|
42
|
+
cd ~/arbors/my-project/feature-login
|
|
43
|
+
# 작업 시작
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
작업이 끝나면:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
arbors remove feature/login
|
|
50
|
+
# 커밋되지 않은 변경사항이 있으면 삭제를 거부한다
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 동료의 PR 코드리뷰
|
|
54
|
+
|
|
55
|
+
원격 브랜치를 로컬 worktree로 바로 체크아웃:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# origin에 있는 브랜치를 자동으로 fetch + worktree 생성
|
|
59
|
+
arbors add feature/payment
|
|
60
|
+
|
|
61
|
+
# 이미 로컬에 있는 브랜치라면 그대로 worktree만 생성
|
|
62
|
+
# → 로컬 우선, 없으면 origin에서 가져옴
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
리뷰가 끝나면:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
arbors remove feature/payment
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 동시에 여러 브랜치 작업
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
arbors add -c feature/auth --base main
|
|
75
|
+
arbors add -c fix/header-bug --base main
|
|
76
|
+
|
|
77
|
+
arbors list
|
|
78
|
+
# feature/auth ~/arbors/my-project/feature-auth
|
|
79
|
+
# fix/header-bug ~/arbors/my-project/fix-header-bug
|
|
80
|
+
|
|
81
|
+
# 각 디렉토리에서 독립적으로 작업. stash 불필요.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
arbors add <branch> 기존 브랜치 체크아웃 (로컬 → 원격 자동)
|
|
88
|
+
arbors add -c <branch> [--base <branch>] 새 브랜치 + worktree 생성
|
|
89
|
+
arbors remove <branch> worktree 삭제 (안전 검사 포함)
|
|
90
|
+
arbors list [--plain] 관리 중인 worktree 목록
|
|
91
|
+
arbors excluded exclude 패턴 확인
|
|
92
|
+
arbors config 현재 설정 확인
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
`~/.arbors/config.json` (글로벌) 또는 `.arbors/config.json` (프로젝트별 우선):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"runtime": "node",
|
|
102
|
+
"language": "ko",
|
|
103
|
+
"packageManager": "auto",
|
|
104
|
+
"copyExcludes": true,
|
|
105
|
+
"copySkip": ["node_modules"],
|
|
106
|
+
"worktreeDir": "~/arbors/{repo}"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Key | Values | Default |
|
|
111
|
+
| ---------------- | ------------------------------------- | ------------------- |
|
|
112
|
+
| `runtime` | `"node"`, `"bun"` | `"node"` |
|
|
113
|
+
| `language` | `"en"`, `"ko"`, `"ja"` | `"en"` |
|
|
114
|
+
| `packageManager` | `"auto"`, `"pnpm"`, `"yarn"`, `"npm"` | `"auto"` |
|
|
115
|
+
| `copyExcludes` | `true`, `false` | `true` |
|
|
116
|
+
| `copySkip` | `string[]` | `["node_modules"]` |
|
|
117
|
+
| `worktreeDir` | string (`{repo}` placeholder) | `"~/arbors/{repo}"` |
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pnpm test # vitest
|
|
123
|
+
pnpm lint # oxlint
|
|
124
|
+
pnpm format # oxfmt
|
|
125
|
+
pnpm build # tsup
|
|
126
|
+
pnpm typecheck # tsc --noEmit
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# arbors
|
|
2
|
+
|
|
3
|
+
[한국어](./README.ko.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
A CLI tool for managing git worktrees.
|
|
6
|
+
|
|
7
|
+
Create a separate directory for each branch and work on multiple branches simultaneously — no stash or switch needed. Automatically copies exclude files and installs dependencies when creating worktrees.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone git@github.com:sungsulee/arbors.git
|
|
13
|
+
cd arbors
|
|
14
|
+
pnpm install && pnpm build
|
|
15
|
+
npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Shell integration (auto `cd` after worktree selection):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# ~/.zshrc
|
|
22
|
+
source /path/to/arbors/shell/arbors-wrapper.zsh
|
|
23
|
+
|
|
24
|
+
# ~/.bashrc
|
|
25
|
+
source /path/to/arbors/shell/arbors-wrapper.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Workflows
|
|
29
|
+
|
|
30
|
+
### New feature development
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# Create a new branch + worktree based on main
|
|
34
|
+
arbors add -c feature/login --base main
|
|
35
|
+
|
|
36
|
+
# This automatically:
|
|
37
|
+
# 1. git fetch origin main
|
|
38
|
+
# 2. Creates worktree at ~/arbors/{repo}/feature-login
|
|
39
|
+
# 3. Copies files listed in .git/info/exclude (.env, etc.)
|
|
40
|
+
# 4. Runs pnpm install (auto-detects from lockfile)
|
|
41
|
+
|
|
42
|
+
cd ~/arbors/my-project/feature-login
|
|
43
|
+
# Start working
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When done:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
arbors remove feature/login
|
|
50
|
+
# Refuses to delete if there are uncommitted changes
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Code reviewing a colleague's PR
|
|
54
|
+
|
|
55
|
+
Check out a remote branch as a local worktree:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# Automatically fetches from origin and creates worktree
|
|
59
|
+
arbors add feature/payment
|
|
60
|
+
|
|
61
|
+
# If the branch already exists locally, just creates the worktree
|
|
62
|
+
# → Tries local first, falls back to origin
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When review is done:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
arbors remove feature/payment
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Working on multiple branches at once
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
arbors add -c feature/auth --base main
|
|
75
|
+
arbors add -c fix/header-bug --base main
|
|
76
|
+
|
|
77
|
+
arbors list
|
|
78
|
+
# feature/auth ~/arbors/my-project/feature-auth
|
|
79
|
+
# fix/header-bug ~/arbors/my-project/fix-header-bug
|
|
80
|
+
|
|
81
|
+
# Work independently in each directory. No stashing needed.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
arbors add <branch> Checkout existing branch (local → remote auto)
|
|
88
|
+
arbors add -c <branch> [--base <branch>] Create new branch + worktree
|
|
89
|
+
arbors remove <branch> Remove worktree (with safety checks)
|
|
90
|
+
arbors list [--plain] List managed worktrees
|
|
91
|
+
arbors excluded Show exclude patterns
|
|
92
|
+
arbors config Show current config
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
`~/.arbors/config.json` (global) or `.arbors/config.json` (per-project, takes precedence):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"runtime": "node",
|
|
102
|
+
"language": "en",
|
|
103
|
+
"packageManager": "auto",
|
|
104
|
+
"copyExcludes": true,
|
|
105
|
+
"copySkip": ["node_modules"],
|
|
106
|
+
"worktreeDir": "~/arbors/{repo}"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Key | Values | Default |
|
|
111
|
+
| ---------------- | ------------------------------------- | ------------------- |
|
|
112
|
+
| `runtime` | `"node"`, `"bun"` | `"node"` |
|
|
113
|
+
| `language` | `"en"`, `"ko"`, `"ja"` | `"en"` |
|
|
114
|
+
| `packageManager` | `"auto"`, `"pnpm"`, `"yarn"`, `"npm"` | `"auto"` |
|
|
115
|
+
| `copyExcludes` | `true`, `false` | `true` |
|
|
116
|
+
| `copySkip` | `string[]` | `["node_modules"]` |
|
|
117
|
+
| `worktreeDir` | string (`{repo}` placeholder) | `"~/arbors/{repo}"` |
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pnpm test # vitest
|
|
123
|
+
pnpm lint # oxlint
|
|
124
|
+
pnpm format # oxfmt
|
|
125
|
+
pnpm build # tsup
|
|
126
|
+
pnpm typecheck # tsc --noEmit
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/bin/arbors.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadConfig } from "../src/config.js";
|
|
3
|
+
import { copyExcludedFiles, getExcludePatterns } from "../src/git/exclude.js";
|
|
4
|
+
import { validateWorktreeName, canSafelyRemove } from "../src/git/safety.js";
|
|
5
|
+
import {
|
|
6
|
+
branchExists,
|
|
7
|
+
checkoutRemoteWorktree,
|
|
8
|
+
checkoutWorktree,
|
|
9
|
+
createWorktree,
|
|
10
|
+
getRepoRoot,
|
|
11
|
+
listWorktrees,
|
|
12
|
+
remoteBranchExists,
|
|
13
|
+
removeWorktree,
|
|
14
|
+
} from "../src/git/worktree.js";
|
|
15
|
+
import { loadMessages } from "../src/i18n/index.js";
|
|
16
|
+
import { getWorktrees, registerProject, registerWorktree, unregisterWorktree } from "../src/project/registry.js";
|
|
17
|
+
import { runSetup } from "../src/project/setup.js";
|
|
18
|
+
import { createAdapter } from "../src/runtime/index.js";
|
|
19
|
+
|
|
20
|
+
const parseArgs = (argv: string[]) => {
|
|
21
|
+
const args = argv.slice(2);
|
|
22
|
+
const command = args[0];
|
|
23
|
+
|
|
24
|
+
const flags = args.reduce<Record<string, string>>((acc, arg, i) => {
|
|
25
|
+
if (arg.startsWith("--") && args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
26
|
+
acc[arg.slice(2)] = args[i + 1];
|
|
27
|
+
}
|
|
28
|
+
if (arg === "--plain") acc.plain = "true";
|
|
29
|
+
if (arg === "--create" || arg === "-c") acc.create = "true";
|
|
30
|
+
if (arg === "--help" || arg === "-h") acc.help = "true";
|
|
31
|
+
if (arg === "--version" || arg === "-v") acc.version = "true";
|
|
32
|
+
return acc;
|
|
33
|
+
}, {});
|
|
34
|
+
|
|
35
|
+
const name = args.slice(1).find((a) => !a.startsWith("-"));
|
|
36
|
+
|
|
37
|
+
return { command, name, flags };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const printHelp = (msg: typeof import("../src/i18n/en.js").en) => {
|
|
41
|
+
console.log(chalk.cyan.bold(msg.version));
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(chalk.white(msg.usage));
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(chalk.white(msg.commands));
|
|
46
|
+
console.log(" add <branch> Checkout existing branch (local or remote)");
|
|
47
|
+
console.log(" add -c <branch> [--base <br>] Create a new branch worktree");
|
|
48
|
+
console.log(" remove <branch> Remove a worktree");
|
|
49
|
+
console.log(" list List worktrees");
|
|
50
|
+
console.log(" excluded Show exclude patterns");
|
|
51
|
+
console.log(" config Show current config");
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(chalk.white(msg.options));
|
|
54
|
+
console.log(" --plain Machine-readable output");
|
|
55
|
+
console.log(" -h, --help Show help");
|
|
56
|
+
console.log(" -v, --version Show version");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const main = async () => {
|
|
60
|
+
const { command, name, flags } = parseArgs(process.argv);
|
|
61
|
+
const config = await loadConfig(
|
|
62
|
+
async (p) => {
|
|
63
|
+
const { readFile } = await import("node:fs/promises");
|
|
64
|
+
return readFile(p, "utf-8");
|
|
65
|
+
},
|
|
66
|
+
async (p) => {
|
|
67
|
+
const { stat } = await import("node:fs/promises");
|
|
68
|
+
try {
|
|
69
|
+
await stat(p);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const msg = await loadMessages(config.language);
|
|
78
|
+
const adapter = await createAdapter(config.runtime);
|
|
79
|
+
|
|
80
|
+
if (flags.version) {
|
|
81
|
+
console.log(msg.version);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (flags.help || !command) {
|
|
86
|
+
printHelp(msg);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
switch (command) {
|
|
91
|
+
case "add": {
|
|
92
|
+
if (!name) {
|
|
93
|
+
console.error(chalk.red("✗ Usage: arbors add [-c] <branch> [--base <branch>]"));
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!validateWorktreeName(name)) {
|
|
98
|
+
console.error(chalk.red(`✗ ${msg.invalidName}`));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(chalk.cyan.bold("arbors add"));
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
let worktreePath: string;
|
|
108
|
+
let created = false;
|
|
109
|
+
let newBranch = false;
|
|
110
|
+
|
|
111
|
+
if (flags.create) {
|
|
112
|
+
// arbors add -c <branch> [--base main]
|
|
113
|
+
console.log(chalk.gray(msg.creating));
|
|
114
|
+
worktreePath = await createWorktree(adapter, name, config.worktreeDir, flags.base);
|
|
115
|
+
created = true;
|
|
116
|
+
newBranch = true;
|
|
117
|
+
console.log(chalk.green(`✓ ${msg.created}: ${worktreePath}`));
|
|
118
|
+
console.log(chalk.gray(` Branch: ${name} (from ${flags.base ?? "default"})`));
|
|
119
|
+
} else if (await branchExists(adapter, name)) {
|
|
120
|
+
console.log(chalk.gray(`Checking out ${name}...`));
|
|
121
|
+
const result = await checkoutWorktree(adapter, name, config.worktreeDir);
|
|
122
|
+
worktreePath = result.path;
|
|
123
|
+
created = result.created;
|
|
124
|
+
console.log(chalk.green(`✓ ${msg.created}: ${worktreePath}`));
|
|
125
|
+
console.log(chalk.gray(` Branch: ${name}`));
|
|
126
|
+
} else if (await remoteBranchExists(adapter, name)) {
|
|
127
|
+
console.log(chalk.gray(`Fetching ${name} from origin...`));
|
|
128
|
+
const result = await checkoutRemoteWorktree(adapter, name, config.worktreeDir);
|
|
129
|
+
worktreePath = result.path;
|
|
130
|
+
created = result.created;
|
|
131
|
+
newBranch = result.created;
|
|
132
|
+
console.log(chalk.green(`✓ ${msg.created}: ${worktreePath}`));
|
|
133
|
+
console.log(chalk.gray(` Branch: ${name} (from origin/${name})`));
|
|
134
|
+
} else {
|
|
135
|
+
console.error(
|
|
136
|
+
chalk.red(`✗ Branch '${name}' not found locally or on origin. Use 'arbors add -c' to create.`),
|
|
137
|
+
);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
if (config.copyExcludes) {
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(chalk.gray(msg.copying));
|
|
146
|
+
const copied = await copyExcludedFiles(adapter, worktreePath, config.copySkip);
|
|
147
|
+
console.log(chalk.green(`✓ ${msg.copied} (${copied.length} files)`));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.gray(msg.installing));
|
|
152
|
+
await runSetup(adapter, worktreePath, config.packageManager);
|
|
153
|
+
console.log(chalk.green(`✓ ${msg.installed}`));
|
|
154
|
+
|
|
155
|
+
const repoRoot = await getRepoRoot(adapter);
|
|
156
|
+
await registerProject(adapter, name, repoRoot);
|
|
157
|
+
await registerWorktree(adapter, worktreePath, name, repoRoot);
|
|
158
|
+
} catch (setupErr) {
|
|
159
|
+
console.error(chalk.red(`✗ ${(setupErr as Error).message}`));
|
|
160
|
+
if (created) {
|
|
161
|
+
console.log(chalk.gray("Rolling back worktree..."));
|
|
162
|
+
await adapter
|
|
163
|
+
.exec("git", ["worktree", "remove", "--force", worktreePath])
|
|
164
|
+
.catch(() => {});
|
|
165
|
+
if (newBranch) {
|
|
166
|
+
await adapter.exec("git", ["branch", "-D", name]).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(chalk.gray(` cd ${worktreePath}`));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case "remove": {
|
|
179
|
+
if (!name) {
|
|
180
|
+
console.error(chalk.red("✗ Usage: arbors remove <branch>"));
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk.cyan.bold("arbors remove"));
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
// Find the worktree by branch name
|
|
190
|
+
const worktrees = await listWorktrees(adapter);
|
|
191
|
+
const target = worktrees.find((wt) => wt.branch === name);
|
|
192
|
+
|
|
193
|
+
if (!target) {
|
|
194
|
+
console.error(chalk.red(`✗ No worktree found for branch '${name}'`));
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { safe, reason } = await canSafelyRemove(adapter, target.path);
|
|
200
|
+
if (!safe) {
|
|
201
|
+
const errorMsg = reason ? msg[reason as keyof typeof msg] : "Cannot remove";
|
|
202
|
+
console.error(chalk.red(`✗ ${errorMsg}`));
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(chalk.gray(msg.removing));
|
|
208
|
+
await removeWorktree(adapter, target.path, target.branch);
|
|
209
|
+
await unregisterWorktree(adapter, target.path);
|
|
210
|
+
console.log(chalk.green(`✓ ${msg.removed}: ${name}`));
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case "list": {
|
|
215
|
+
const repoRootForList = await getRepoRoot(adapter);
|
|
216
|
+
const dbWorktrees = await getWorktrees(adapter, repoRootForList);
|
|
217
|
+
const gitWorktrees = await listWorktrees(adapter);
|
|
218
|
+
const gitPaths = new Set(gitWorktrees.map((wt) => wt.path));
|
|
219
|
+
|
|
220
|
+
// Reconcile: remove db entries that no longer exist in git
|
|
221
|
+
const stale = dbWorktrees.filter((w) => !gitPaths.has(w.path));
|
|
222
|
+
for (const w of stale) {
|
|
223
|
+
await unregisterWorktree(adapter, w.path);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const managedWorktrees = dbWorktrees.filter((w) => gitPaths.has(w.path));
|
|
227
|
+
|
|
228
|
+
if (flags.plain) {
|
|
229
|
+
managedWorktrees.forEach((wt) => console.log(`${wt.branch}\t${wt.path}`));
|
|
230
|
+
} else if (managedWorktrees.length === 0) {
|
|
231
|
+
console.log(chalk.gray(msg.noWorktrees));
|
|
232
|
+
} else {
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(chalk.cyan.bold("arbors list"));
|
|
235
|
+
console.log();
|
|
236
|
+
managedWorktrees.forEach((wt) => {
|
|
237
|
+
console.log(chalk.white(wt.branch));
|
|
238
|
+
console.log(chalk.gray(` ${wt.path}`));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case "excluded": {
|
|
245
|
+
const patterns = await getExcludePatterns(adapter);
|
|
246
|
+
if (patterns.length === 0) {
|
|
247
|
+
console.log(chalk.gray("No exclude patterns found in .git/info/exclude"));
|
|
248
|
+
} else {
|
|
249
|
+
console.log();
|
|
250
|
+
console.log(chalk.cyan.bold("arbors excluded"));
|
|
251
|
+
console.log();
|
|
252
|
+
patterns.forEach((p) => console.log(` ${p}`));
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "config": {
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(chalk.cyan.bold("arbors config"));
|
|
260
|
+
console.log();
|
|
261
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
262
|
+
console.log(` ${chalk.white(key)}: ${chalk.gray(String(value))}`);
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
default: {
|
|
268
|
+
console.error(chalk.red(`✗ Unknown command: ${command}`));
|
|
269
|
+
printHelp(msg);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
main().catch((err: Error) => {
|
|
276
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
});
|