opencode-worktree 0.3.4 → 0.3.5
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.md +64 -15
- package/package.json +1 -1
- package/src/config.ts +5 -0
- package/src/opencode.ts +27 -4
- package/src/ui.ts +67 -18
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-worktree
|
|
2
2
|
|
|
3
|
-
Terminal UI for managing git worktrees and launching
|
|
3
|
+
Terminal UI for managing git worktrees and launching your preferred coding tool in the selected worktree.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -9,17 +9,18 @@ Terminal UI for managing git worktrees and launching `opencode` in the selected
|
|
|
9
9
|
- Status indicators: `[main]` for main worktree, `[*]` for uncommitted changes, `[local]` for local-only branches
|
|
10
10
|
- Create new worktrees directly from the TUI
|
|
11
11
|
- Post-create hooks: automatically run commands (e.g., `npm install`) after creating a worktree
|
|
12
|
-
- Open worktree folder in file manager
|
|
12
|
+
- Open worktree folder in file manager or custom editor
|
|
13
13
|
- Unlink worktrees (remove directory, keep branch)
|
|
14
14
|
- Delete worktrees and local branches (never remote)
|
|
15
15
|
- Multi-select delete mode for batch deletion
|
|
16
|
-
-
|
|
16
|
+
- **Customizable launch command**: use `opencode`, `cursor`, `claude`, `code`, or any CLI tool
|
|
17
17
|
- Refresh list on demand
|
|
18
|
+
- Update notifications when new versions are available
|
|
18
19
|
|
|
19
20
|
## Requirements
|
|
20
21
|
|
|
21
22
|
- Git
|
|
22
|
-
-
|
|
23
|
+
- A CLI tool available on your PATH (e.g., `opencode`, `cursor`, `claude`, `code`)
|
|
23
24
|
|
|
24
25
|
## Install (npm)
|
|
25
26
|
|
|
@@ -42,11 +43,11 @@ opencode-worktree /path/to/your/repo
|
|
|
42
43
|
## Keybindings
|
|
43
44
|
|
|
44
45
|
- `Up`/`Down` or `j`/`k`: navigate
|
|
45
|
-
- `Enter`: open selected worktree in
|
|
46
|
+
- `Enter`: open selected worktree in configured tool (or toggle selection in delete mode)
|
|
46
47
|
- `o`: open worktree folder in file manager or custom editor (configurable)
|
|
47
48
|
- `d`: enter multi-select delete mode (press again to confirm deletion)
|
|
48
49
|
- `n`: create new worktree
|
|
49
|
-
- `c`: edit configuration (
|
|
50
|
+
- `c`: edit configuration (hooks, open command, launch command)
|
|
50
51
|
- `r`: refresh list
|
|
51
52
|
- `q` or `Esc`: quit (or cancel dialogs/modes)
|
|
52
53
|
|
|
@@ -59,18 +60,33 @@ opencode-worktree /path/to/your/repo
|
|
|
59
60
|
|
|
60
61
|
## Configuration
|
|
61
62
|
|
|
62
|
-
You can configure per-repository settings by creating a `.opencode-worktree.json` file in your repository root.
|
|
63
|
+
You can configure per-repository settings by creating a `.opencode-worktree.json` file in your repository root, or by pressing `c` in the TUI.
|
|
64
|
+
|
|
65
|
+
### Configuration options
|
|
66
|
+
|
|
67
|
+
| Option | Description | Default |
|
|
68
|
+
|--------|-------------|---------|
|
|
69
|
+
| `postCreateHook` | Command to run after creating a worktree | none |
|
|
70
|
+
| `openCommand` | Command for opening worktree folders (`o` key) | system default |
|
|
71
|
+
| `launchCommand` | Command to launch when selecting a worktree (`Enter` key) | `opencode` |
|
|
72
|
+
|
|
73
|
+
### Example configuration
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"postCreateHook": "npm install",
|
|
78
|
+
"openCommand": "code",
|
|
79
|
+
"launchCommand": "cursor"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
63
82
|
|
|
64
83
|
### First-time setup
|
|
65
84
|
|
|
66
|
-
When you first run `opencode-worktree` in a repository without a configuration file, you'll be prompted to configure
|
|
85
|
+
When you first run `opencode-worktree` in a repository without a configuration file, you'll be prompted to configure settings. You can also skip this step and configure later by pressing `c`.
|
|
67
86
|
|
|
68
87
|
### Editing configuration
|
|
69
88
|
|
|
70
|
-
Press `c` at any time to edit your configuration.
|
|
71
|
-
|
|
72
|
-
- **Post-create hook**: Command to run after creating a worktree (e.g., `npm install`)
|
|
73
|
-
- **Open command**: Custom command for opening worktree folders (e.g., `webstorm`, `code`)
|
|
89
|
+
Press `c` at any time to edit your configuration. Use `Tab` to switch between fields.
|
|
74
90
|
|
|
75
91
|
### Post-create hooks
|
|
76
92
|
|
|
@@ -82,7 +98,7 @@ Run a command automatically after creating a new worktree. Useful for installing
|
|
|
82
98
|
}
|
|
83
99
|
```
|
|
84
100
|
|
|
85
|
-
The hook output is streamed to the TUI in real-time. If the hook fails, you can choose to open
|
|
101
|
+
The hook output is streamed to the TUI in real-time. If the hook fails, you can choose to open the tool anyway or cancel.
|
|
86
102
|
|
|
87
103
|
**Examples:**
|
|
88
104
|
|
|
@@ -116,16 +132,49 @@ Use a custom command when pressing `o` to open worktree folders. Useful for open
|
|
|
116
132
|
}
|
|
117
133
|
```
|
|
118
134
|
|
|
135
|
+
### Custom launch command
|
|
136
|
+
|
|
137
|
+
Use a different tool instead of `opencode` when pressing `Enter` to open a worktree. This allows you to use any CLI-based coding tool.
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"launchCommand": "cursor"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Examples:**
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"launchCommand": "claude"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"launchCommand": "code"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"launchCommand": "zed"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Full configuration example
|
|
166
|
+
|
|
119
167
|
```json
|
|
120
168
|
{
|
|
121
169
|
"postCreateHook": "npm install",
|
|
122
|
-
"openCommand": "
|
|
170
|
+
"openCommand": "code",
|
|
171
|
+
"launchCommand": "cursor"
|
|
123
172
|
}
|
|
124
173
|
```
|
|
125
174
|
|
|
126
175
|
## Update notifications
|
|
127
176
|
|
|
128
|
-
When a new version is published to npm, the
|
|
177
|
+
When a new version is published to npm, the TUI will show a non-intrusive update message in the title bar. The version check runs in the background and doesn't slow down startup.
|
|
129
178
|
|
|
130
179
|
## Development
|
|
131
180
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
export type Config = {
|
|
5
5
|
postCreateHook?: string;
|
|
6
6
|
openCommand?: string; // Custom command to open worktree folder (e.g., "webstorm", "code")
|
|
7
|
+
launchCommand?: string; // Custom command to launch instead of opencode (e.g., "cursor", "claude")
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
const CONFIG_FILENAME = ".opencode-worktree.json";
|
|
@@ -51,6 +52,10 @@ export const loadRepoConfig = (repoRoot: string): Config => {
|
|
|
51
52
|
config.openCommand = parsed.openCommand;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
if (typeof parsed.launchCommand === "string") {
|
|
56
|
+
config.launchCommand = parsed.launchCommand;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
return config;
|
|
55
60
|
} catch {
|
|
56
61
|
// If we can't read or parse the config, return empty
|
package/src/opencode.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Check if a command is available in PATH
|
|
5
|
+
*/
|
|
6
|
+
export const isCommandAvailable = (command: string): boolean => {
|
|
7
|
+
const result = spawnSync(command, ["--version"], {
|
|
5
8
|
stdio: "ignore",
|
|
6
9
|
});
|
|
7
10
|
return result.status === 0;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use isCommandAvailable instead
|
|
15
|
+
*/
|
|
16
|
+
export const isOpenCodeAvailable = (): boolean => {
|
|
17
|
+
return isCommandAvailable("opencode");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Launch a command in a worktree directory
|
|
22
|
+
* If customCommand is provided, uses that instead of opencode
|
|
23
|
+
*/
|
|
24
|
+
export const launchCommand = (cwd: string, customCommand?: string): void => {
|
|
25
|
+
const command = customCommand || "opencode";
|
|
26
|
+
|
|
27
|
+
const child = spawn(command, [], {
|
|
12
28
|
cwd,
|
|
13
29
|
stdio: "inherit",
|
|
14
30
|
});
|
|
@@ -19,6 +35,13 @@ export const launchOpenCode = (cwd: string): void => {
|
|
|
19
35
|
});
|
|
20
36
|
};
|
|
21
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use launchCommand instead
|
|
40
|
+
*/
|
|
41
|
+
export const launchOpenCode = (cwd: string): void => {
|
|
42
|
+
launchCommand(cwd);
|
|
43
|
+
};
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Open a path in the system file manager or with a custom command
|
|
24
47
|
* If customCommand is provided, uses that instead of the system default
|
package/src/ui.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
resolveRepoRoot,
|
|
23
23
|
unlinkWorktree,
|
|
24
24
|
} from "./git.js";
|
|
25
|
-
import {
|
|
25
|
+
import { isCommandAvailable, launchCommand, openInFileManager } from "./opencode.js";
|
|
26
26
|
import { WorktreeInfo } from "./types.js";
|
|
27
27
|
import { loadRepoConfig, saveRepoConfig, configExists, type Config } from "./config.js";
|
|
28
28
|
import { runPostCreateHook, type HookResult } from "./hooks.js";
|
|
@@ -81,6 +81,7 @@ class WorktreeSelector {
|
|
|
81
81
|
|
|
82
82
|
private opencodeAvailable = false;
|
|
83
83
|
private repoRoot: string | null = null;
|
|
84
|
+
private repoConfig: Config = {};
|
|
84
85
|
private isCreatingWorktree = false;
|
|
85
86
|
private worktreeOptions: SelectOption[] = [];
|
|
86
87
|
|
|
@@ -103,7 +104,8 @@ class WorktreeSelector {
|
|
|
103
104
|
private configContainer: BoxRenderable | null = null;
|
|
104
105
|
private configHookInput: InputRenderable | null = null;
|
|
105
106
|
private configOpenInput: InputRenderable | null = null;
|
|
106
|
-
private
|
|
107
|
+
private configLaunchInput: InputRenderable | null = null;
|
|
108
|
+
private configActiveField: "hook" | "open" | "launch" = "hook";
|
|
107
109
|
private isFirstTimeSetup = false;
|
|
108
110
|
|
|
109
111
|
constructor(
|
|
@@ -113,7 +115,8 @@ class WorktreeSelector {
|
|
|
113
115
|
) {
|
|
114
116
|
// Load worktrees first to get initial options
|
|
115
117
|
this.repoRoot = resolveRepoRoot(this.targetPath);
|
|
116
|
-
this.
|
|
118
|
+
this.repoConfig = this.repoRoot ? loadRepoConfig(this.repoRoot) : {};
|
|
119
|
+
this.opencodeAvailable = isCommandAvailable(this.repoConfig.launchCommand || "opencode");
|
|
117
120
|
this.worktreeOptions = this.buildInitialOptions();
|
|
118
121
|
|
|
119
122
|
this.title = new TextRenderable(renderer, {
|
|
@@ -308,14 +311,18 @@ class WorktreeSelector {
|
|
|
308
311
|
return;
|
|
309
312
|
}
|
|
310
313
|
if (key.name === "tab") {
|
|
311
|
-
//
|
|
314
|
+
// Cycle between fields: hook -> open -> launch -> hook
|
|
312
315
|
if (this.configActiveField === "hook") {
|
|
313
316
|
this.configActiveField = "open";
|
|
314
317
|
this.configHookInput?.blur();
|
|
315
318
|
this.configOpenInput?.focus();
|
|
319
|
+
} else if (this.configActiveField === "open") {
|
|
320
|
+
this.configActiveField = "launch";
|
|
321
|
+
this.configOpenInput?.blur();
|
|
322
|
+
this.configLaunchInput?.focus();
|
|
316
323
|
} else {
|
|
317
324
|
this.configActiveField = "hook";
|
|
318
|
-
this.
|
|
325
|
+
this.configLaunchInput?.blur();
|
|
319
326
|
this.configHookInput?.focus();
|
|
320
327
|
}
|
|
321
328
|
this.renderer.requestRender();
|
|
@@ -388,13 +395,14 @@ class WorktreeSelector {
|
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
const worktree = value as WorktreeInfo;
|
|
398
|
+
const cmdName = this.repoConfig.launchCommand || "opencode";
|
|
391
399
|
if (!this.opencodeAvailable) {
|
|
392
|
-
this.setStatus(
|
|
400
|
+
this.setStatus(`${cmdName} is not available on PATH.`, "error");
|
|
393
401
|
return;
|
|
394
402
|
}
|
|
395
403
|
|
|
396
404
|
this.cleanup(false);
|
|
397
|
-
|
|
405
|
+
launchCommand(worktree.path, this.repoConfig.launchCommand);
|
|
398
406
|
}
|
|
399
407
|
|
|
400
408
|
private openWorktreeInFileManager(): void {
|
|
@@ -521,10 +529,10 @@ class WorktreeSelector {
|
|
|
521
529
|
this.pendingWorktreePath = result.path;
|
|
522
530
|
this.runHook(result.path, config.postCreateHook);
|
|
523
531
|
} else {
|
|
524
|
-
// No hook, launch
|
|
532
|
+
// No hook, launch command directly
|
|
525
533
|
this.hideCreateWorktreeInput();
|
|
526
534
|
this.cleanup(false);
|
|
527
|
-
|
|
535
|
+
launchCommand(result.path, this.repoConfig.launchCommand);
|
|
528
536
|
}
|
|
529
537
|
} else {
|
|
530
538
|
this.setStatus(`Failed to create worktree: ${result.error}`, "error");
|
|
@@ -610,12 +618,12 @@ class WorktreeSelector {
|
|
|
610
618
|
this.setStatus("Hook completed successfully!", "success");
|
|
611
619
|
this.renderer.requestRender();
|
|
612
620
|
|
|
613
|
-
// Brief delay to show success, then launch
|
|
621
|
+
// Brief delay to show success, then launch command
|
|
614
622
|
setTimeout(() => {
|
|
615
623
|
this.hideHookOutput();
|
|
616
624
|
if (this.pendingWorktreePath) {
|
|
617
625
|
this.cleanup(false);
|
|
618
|
-
|
|
626
|
+
launchCommand(this.pendingWorktreePath, this.repoConfig.launchCommand);
|
|
619
627
|
}
|
|
620
628
|
}, 1000);
|
|
621
629
|
}
|
|
@@ -676,7 +684,7 @@ class WorktreeSelector {
|
|
|
676
684
|
if (choice === "open" && this.pendingWorktreePath) {
|
|
677
685
|
this.hideHookOutput();
|
|
678
686
|
this.cleanup(false);
|
|
679
|
-
|
|
687
|
+
launchCommand(this.pendingWorktreePath, this.repoConfig.launchCommand);
|
|
680
688
|
} else {
|
|
681
689
|
// Cancel - return to list
|
|
682
690
|
this.hideHookOutput();
|
|
@@ -737,7 +745,7 @@ class WorktreeSelector {
|
|
|
737
745
|
left: 2,
|
|
738
746
|
top: 3,
|
|
739
747
|
width: 76,
|
|
740
|
-
height:
|
|
748
|
+
height: 15,
|
|
741
749
|
borderStyle: "single",
|
|
742
750
|
borderColor: "#38BDF8",
|
|
743
751
|
title,
|
|
@@ -771,13 +779,13 @@ class WorktreeSelector {
|
|
|
771
779
|
});
|
|
772
780
|
this.configContainer.add(this.configHookInput);
|
|
773
781
|
|
|
774
|
-
// Open command field
|
|
782
|
+
// Open folder command field
|
|
775
783
|
const openLabel = new TextRenderable(this.renderer, {
|
|
776
784
|
id: "config-open-label",
|
|
777
785
|
position: "absolute",
|
|
778
786
|
left: 1,
|
|
779
787
|
top: 4,
|
|
780
|
-
content: "Open folder command (e.g.,
|
|
788
|
+
content: "Open folder command (e.g., code, webstorm):",
|
|
781
789
|
fg: "#94A3B8",
|
|
782
790
|
});
|
|
783
791
|
this.configContainer.add(openLabel);
|
|
@@ -795,12 +803,36 @@ class WorktreeSelector {
|
|
|
795
803
|
});
|
|
796
804
|
this.configContainer.add(this.configOpenInput);
|
|
797
805
|
|
|
806
|
+
// Launch command field (instead of opencode)
|
|
807
|
+
const launchLabel = new TextRenderable(this.renderer, {
|
|
808
|
+
id: "config-launch-label",
|
|
809
|
+
position: "absolute",
|
|
810
|
+
left: 1,
|
|
811
|
+
top: 7,
|
|
812
|
+
content: "Launch command (e.g., cursor, claude, code):",
|
|
813
|
+
fg: "#94A3B8",
|
|
814
|
+
});
|
|
815
|
+
this.configContainer.add(launchLabel);
|
|
816
|
+
|
|
817
|
+
this.configLaunchInput = new InputRenderable(this.renderer, {
|
|
818
|
+
id: "config-launch-input",
|
|
819
|
+
position: "absolute",
|
|
820
|
+
left: 1,
|
|
821
|
+
top: 8,
|
|
822
|
+
width: 72,
|
|
823
|
+
placeholder: "opencode (default)",
|
|
824
|
+
value: existingConfig.launchCommand || "",
|
|
825
|
+
focusedBackgroundColor: "#1E293B",
|
|
826
|
+
backgroundColor: "#1E293B",
|
|
827
|
+
});
|
|
828
|
+
this.configContainer.add(this.configLaunchInput);
|
|
829
|
+
|
|
798
830
|
// Help text
|
|
799
831
|
const helpText = new TextRenderable(this.renderer, {
|
|
800
832
|
id: "config-help",
|
|
801
833
|
position: "absolute",
|
|
802
834
|
left: 1,
|
|
803
|
-
top:
|
|
835
|
+
top: 10,
|
|
804
836
|
content: "Tab to switch fields • Leave empty to use defaults",
|
|
805
837
|
fg: "#64748B",
|
|
806
838
|
});
|
|
@@ -831,12 +863,16 @@ class WorktreeSelector {
|
|
|
831
863
|
if (this.configOpenInput) {
|
|
832
864
|
this.configOpenInput.blur();
|
|
833
865
|
}
|
|
866
|
+
if (this.configLaunchInput) {
|
|
867
|
+
this.configLaunchInput.blur();
|
|
868
|
+
}
|
|
834
869
|
|
|
835
870
|
if (this.configContainer) {
|
|
836
871
|
this.renderer.root.remove(this.configContainer.id);
|
|
837
872
|
this.configContainer = null;
|
|
838
873
|
this.configHookInput = null;
|
|
839
874
|
this.configOpenInput = null;
|
|
875
|
+
this.configLaunchInput = null;
|
|
840
876
|
}
|
|
841
877
|
|
|
842
878
|
this.selectElement.visible = true;
|
|
@@ -859,6 +895,7 @@ class WorktreeSelector {
|
|
|
859
895
|
|
|
860
896
|
const hookValue = (this.configHookInput?.value || "").trim();
|
|
861
897
|
const openValue = (this.configOpenInput?.value || "").trim();
|
|
898
|
+
const launchValue = (this.configLaunchInput?.value || "").trim();
|
|
862
899
|
const config: Config = {};
|
|
863
900
|
|
|
864
901
|
if (hookValue) {
|
|
@@ -867,13 +904,24 @@ class WorktreeSelector {
|
|
|
867
904
|
if (openValue) {
|
|
868
905
|
config.openCommand = openValue;
|
|
869
906
|
}
|
|
907
|
+
if (launchValue) {
|
|
908
|
+
config.launchCommand = launchValue;
|
|
909
|
+
}
|
|
870
910
|
|
|
871
911
|
const success = saveRepoConfig(this.repoRoot, config);
|
|
872
912
|
|
|
873
913
|
if (success) {
|
|
914
|
+
// Update the in-memory config
|
|
915
|
+
this.repoConfig = config;
|
|
916
|
+
|
|
917
|
+
// Re-check if the launch command is available
|
|
918
|
+
const cmdName = config.launchCommand || "opencode";
|
|
919
|
+
this.opencodeAvailable = isCommandAvailable(cmdName);
|
|
920
|
+
|
|
874
921
|
const changes: string[] = [];
|
|
875
922
|
if (hookValue) changes.push(`hook: "${hookValue}"`);
|
|
876
923
|
if (openValue) changes.push(`open: "${openValue}"`);
|
|
924
|
+
if (launchValue) changes.push(`launch: "${launchValue}"`);
|
|
877
925
|
|
|
878
926
|
if (changes.length > 0) {
|
|
879
927
|
this.setStatus(`Config saved: ${changes.join(", ")}`, "success");
|
|
@@ -922,9 +970,10 @@ class WorktreeSelector {
|
|
|
922
970
|
);
|
|
923
971
|
}
|
|
924
972
|
|
|
925
|
-
this.
|
|
973
|
+
const cmdName = this.repoConfig.launchCommand || "opencode";
|
|
974
|
+
this.opencodeAvailable = isCommandAvailable(cmdName);
|
|
926
975
|
if (!this.opencodeAvailable) {
|
|
927
|
-
this.setStatus(
|
|
976
|
+
this.setStatus(`${cmdName} is not available on PATH.`, "error");
|
|
928
977
|
}
|
|
929
978
|
|
|
930
979
|
this.renderer.requestRender();
|