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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +7 -0
  2. package/.oxlintrc.json +9 -0
  3. package/README.ja.md +131 -0
  4. package/README.ko.md +131 -0
  5. package/README.md +131 -0
  6. package/bin/arbors.ts +278 -0
  7. package/dist/arbors.js +1094 -0
  8. package/dist/arbors.js.map +1 -0
  9. package/dist/bun-EMN2NS2M.js +48 -0
  10. package/dist/bun-EMN2NS2M.js.map +1 -0
  11. package/dist/ja-F4DBSAAZ.js +38 -0
  12. package/dist/ja-F4DBSAAZ.js.map +1 -0
  13. package/dist/ko-MTIAHJOR.js +38 -0
  14. package/dist/ko-MTIAHJOR.js.map +1 -0
  15. package/dist/node-LCODN3HC.js +56 -0
  16. package/dist/node-LCODN3HC.js.map +1 -0
  17. package/package.json +54 -0
  18. package/pnpm-workspace.yaml +1 -0
  19. package/shell/arbors-wrapper.sh +21 -0
  20. package/shell/arbors-wrapper.zsh +21 -0
  21. package/skills/arbors-usage/SKILL.md +129 -0
  22. package/src/config.ts +66 -0
  23. package/src/git/exclude.ts +63 -0
  24. package/src/git/safety.ts +40 -0
  25. package/src/git/worktree.ts +171 -0
  26. package/src/i18n/en.ts +63 -0
  27. package/src/i18n/index.ts +37 -0
  28. package/src/i18n/ja.ts +40 -0
  29. package/src/i18n/ko.ts +40 -0
  30. package/src/project/registry.ts +108 -0
  31. package/src/project/setup.ts +74 -0
  32. package/src/runtime/adapter.ts +16 -0
  33. package/src/runtime/bun.ts +49 -0
  34. package/src/runtime/index.ts +17 -0
  35. package/src/runtime/node.ts +58 -0
  36. package/src/tui/App.tsx +87 -0
  37. package/src/tui/FuzzyList.tsx +111 -0
  38. package/src/tui/ProjectSelector.tsx +48 -0
  39. package/src/tui/WorktreeSelector.tsx +46 -0
  40. package/tests/config.test.ts +108 -0
  41. package/tests/exclude.test.ts +120 -0
  42. package/tests/i18n.test.ts +75 -0
  43. package/tests/registry.test.ts +136 -0
  44. package/tests/safety.test.ts +58 -0
  45. package/tests/setup-detection.test.ts +105 -0
  46. package/tests/setup.test.ts +87 -0
  47. package/tsconfig.json +22 -0
  48. package/tsup.config.ts +14 -0
  49. 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
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "rules": {
4
+ "eslint/no-unused-vars": "warn",
5
+ "eslint/no-console": "off",
6
+ "eslint/eqeqeq": "error"
7
+ },
8
+ "ignorePatterns": ["dist", "node_modules"]
9
+ }
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
+ });